提交 ab09b320 编写于 作者: O obpilot 提交者: LINGuanRen

Add chapter05/06/07/08 #692

上级 f41ee5fb
# 第 5 章:如何运维 OceanBase 社区版
[TOC]
## 运维概述
OceanBase 运维目标主要是维护集群稳定性,管理集群资源,保证集群性能最优。
OceanBase 运维具体的工作包含:
+ 集群扩容/缩容/替换/搬迁/重启/备份/监控/升级
+ 租户扩容/缩容/性能调优
+ 故障处理
OceanBase 社区版提供了自动化运维工具 OBD 方便集群部署,也能承担部分扩容和升级操作,但严格来说这个工具的定位不是“运维平台”。OceanBase 的官方运维产品 OCP 后期也会支持社区版。那个产品有产品文档手册,这里就不再重复。本课主要是介绍 OceanBase 运维操作的具体原理和步骤,自动化产品只是将这些基本操作封装了一下而已。第三方运维平台也可以参考这里的原理和步骤介绍进行开发,将 OceanBase 纳入自己的产品支持列表中。
## 本章目录
+ [如何管理 OceanBase 集群](5.1.md)
+ [如何管理 OceanBase 租户](5.2.md)
+ [如何对 OceanBase 进行备份和恢复](5.3.md)
+ [如何监控 OceanBase 和配置告警](5.4.md)
+ [如何对 OceanBase 进行简单性能诊断](5.5.md)
+ [如何快速处理 OceanBase 故障](5.6.md)
+ [如何使用 OBD 运维](5.7.md)
+ [附录](5.8.md)
## 如何联系我们
欢迎广大 OceanBase 爱好者、用户和客户有任何问题联系我们反馈:
+ 社区版官网论坛:[https://open.oceanbase.com/answer](https://open.oceanbase.com/answer)
+ 社区版项目网站提 `Issue`[https://github.com/oceanbase/oceanbase/issues](https://github.com/oceanbase/oceanbase/issues)
+ 钉钉群:群号 `33254054`
\ No newline at end of file
# 如何管理 OceanBase 集群
## OceanBase 集群架构简述
### 进程简述
OceanBase 是单进程软件,进程名是:`observer`,通常每个机器启动一个进程就是一个节点。在前面部署一课中介绍了 OceanBase 的目录架构。如下是查看 OceanBase 和 OBProxy 安装后的目录。
```bash
[admin@obce02 ~]$
[admin@obce02 ~]$ pwd
/home/admin
[admin@obce02 ~]$
[admin@obce02 ~]$ tree -L 3 --filelimit 10 oceanbase/ obproxy/ /redo /data/
oceanbase/
├── bin
│   ├── import_time_zone_info.py
│   └── observer
├── etc
│   ├── observer.config.bin
│   ├── observer.config.bin.history
│   └── timezone_V1.log
├── lib
│   ├── libaio.so -> libaio.so.1.0.1
│   ├── libaio.so.1 -> libaio.so.1.0.1
│   ├── libaio.so.1.0.1
│   ├── libmariadb.so -> libmariadb.so.3
│   └── libmariadb.so.3
├── log [588 entries exceeds filelimit, not opening dir]
├── run
│   ├── mysql.sock
│   └── observer.pid
└── store
└── obdemo
├── clog -> /redo/obdemo/clog
├── etc2 -> /redo/obdemo/etc2
├── etc3 -> /data/obdemo/etc3
├── ilog -> /redo/obdemo/ilog
├── slog -> /redo/obdemo/slog
└── sstable -> /data/obdemo/sstable
obproxy/
/redo
└── obdemo
├── clog [160 entries exceeds filelimit, not opening dir]
├── etc2
├── ilog [17 entries exceeds filelimit, not opening dir]
└── slog
├── 5
└── 6
/data/
└── obdemo
├── etc3
└── sstable
└── block_file
21 directories, 15 files
[admin@obce02 ~]$
```
OceanBase 目录里需要关注下面几个目录:
+ 软件安装目录。如果是用 RPM 包手动安装的,默认会安装在用户 `admin` 的 HOME 目录下(`~/`)。如果是 OBD 部署的,采取的是将 RPM 解压缩到用户的 HOME 目录下隐藏文件夹里(`~/.obd`)。
+ 工作目录。本课程里进程 `observer` 工作目录是 `~/oceanbase` ,进程 `obproxy` 启动目录是 `~/obproxy` 。进程 `observer``obproxy` 的启动都有共同特点,会在工作目录下寻找文件夹 `etc` 子目录,读取默认配置文件。如果目录不存在就默认创建一个文件夹 `etc` 。同时还会创建日志目录 `log` 等。所以需要知道进程 `observer``obproxy` 的启动目录,以便进程故障的时候,能够快速拉起进程恢复。此外,启动用户也是要保持一致。本课程使用的是用户 `admin` 。使用 OBD 部署 OceanBase 和 OBProxy 的时候,可以指定工作目录。
+ 参数目录。默认参数文件目录在工作目录下的 `etc` 目录里,此外还可以通过参数 `config_additional_dir` 设定冗余的参数目录。参数文件是二进制文件,不能直接编辑和读取,可以通过 `strings` 命令读取。
+ 日志目录。默认日志目录会在工作目录下的 `log` 目录。如果不存在,就自动生成一个。日志目录里会有三类日志:`observer.log``rootservice.log``election.log` 。每个日志又有一个完整的目录和一个日志级别在 `WARN` 级别以上(含)的日志。后者可以通过参数关闭。`observer.log` 通过参数设置可以滚动生成,最多保留指定数量。 当数据库访问量非常大的时候,进程 `observer` 的日志输出量是比较大的。所以这个目录在生产环境建议可用空间不少于 100G 。并且在集群的参数里还会真多日志输出限流。
+ 总数据目录。默认目录在工作目录下的 `store` 文件夹。当然进程 `observer` 也可以通过参数 `-d` 指定特定目录。本课程里手动部署 OceanBase 的时候,建议 `store` 目录下有集群名作为目录,然后 `-d` 指定到那一级目录(如 `-d ~/oceanbase/store/obdemo` )。这个目录下会有实际的数据文件目录(`sstable`) 和事务日志目录(`slog``ilog``clog`)。通常建议数据文件目录和事务日志目录用独立的盘存放,所以这个下面的数据文件目录和事务日志目录是一个软链接。
+ 数据文件目录。默认文件夹名叫 `sstable` ,下面只有一个大文件 `block_file` 。进程 `observer` 启动的时候初始化,初始化大小默认是磁盘可用空间的 `80%` 。这个大小可以通过参数控制。这个目录通常不要跟事务日志目录共用一块盘存储。如果节点要重置,这个目录下的内容必须清理掉。
+ 事务日志目录。这包含三个文件夹。`slog` 存储静态数据写入的事务日志,`clog` 存储动态数据写入的事务日志,`ilog` 存储日志目录。这些日志 OceanBase 会自动管理,不要手动删除。这三个目录通常建议独立一块盘存储。如果节点要重置,这三个目录下的内容必须清理掉。
### 进程监听端口
下面分别阐述进程 `observer``obproxy` 的监听端口。
进程 `observer` 第一次启动的时候,需要通过参数 `-p` 指定连接端口,参数 `-P`指定节点 RPC 通信端口。默认是 `-p 2881 -P 2882` 。进程启动成功后,不到几秒就能走到监听环节。查看方法:
```bash
[admin@obce02 oceanbase]$ pidof observer
56779
[admin@obce02 oceanbase]$ ps -ef | grep observer | grep -v grep
admin 56779 1 99 16:26 ? 00:00:49 bin/observer
[admin@obce02 oceanbase]$
[admin@obce02 oceanbase]$ netstat -ntlp | grep -i observer
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 0.0.0.0:2881 0.0.0.0:* LISTEN 56779/bin/observer
tcp 0 0 0.0.0.0:2882 0.0.0.0:* LISTEN 56779/bin/observer
[admin@obce02 oceanbase]$
```
最后一列中数字是进程 ID 。
排查问题的时候可以查看节点之间的 TPC 连接状态。或者查看哪个 IP 和 PORT 流量最大等。
```bash
# 查看 TCP 连接
netstat -np |grep -i observer
# 查看 TCP 连接汇总
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
# 查看网卡 IP 流量
iftop -i eth0 -nNB
# 按 L 、T 、3 、t 、 B 、l 、 p 找出具体哪个 IP 和 PORT 流量最大
```
### 集群节点组织架构
OceanBase 集群是由多个节点的进程组合而成,集群最小规模是 1 个节点,这是一个单副本集群,没有高可用能力。在生产环境,OceanBase 集群最小规模是三节点。会分为三个 `Zone` 。每个 `Zone` 最少一个节点。实际上集群里节点组织关系如下:
| 集群 | ZONE | REGION | IDC | IP |
|--------|-------|---------|------|----------------|
| obdemo | ZONE1 | region1 | idc1 | 192.168.249.52 |
| obdemo | ZONE2 | region1 | idc1 | 192.168.249.49 |
| obdemo | ZONE3 | region1 | idc2 | 192.168.249.51 |
| obdemo | ZONE4 | region1 | idc2 | 192.168.249.54 |
| obdemo | ZONE5 | region2 | idc3 | 192.168.249.55 |
节点的 `Zone` 是在进程启动的时候指定的。节点的 `region``idc` 是跟 `Zone` 绑定的,在集群初始化好后查看和修改。
下面只是示例,并不表示一定要修改这个。
```sql
alter system change zone 'zone1' region 'region1';
alter system change zone 'zone2' region 'region1';
alter system change zone 'zone3' region 'region2';
alter system modify zone zone1 idc='idc1';
alter system modify zone zone2 idc='idc2';
alter system modify zone zone3 idc='idc3';
select * from __all_zone where name in ('region','idc','status','zone_type','cluster') order by name;
+----------------------------+----------------------------+-------+-----------+-------+-----------+
| gmt_create | gmt_modified | zone | name | value | info |
+----------------------------+----------------------------+-------+-----------+-------+-----------+
| 2021-09-12 14:49:43.533839 | 2021-09-12 14:49:43.533839 | | cluster | 0 | obdemo |
| 2021-09-12 14:49:43.536334 | 2021-09-20 17:08:13.727095 | zone1 | idc | 0 | idc1 |
| 2021-09-12 14:49:43.537407 | 2021-09-20 17:08:20.052725 | zone2 | idc | 0 | idc2 |
| 2021-09-12 14:49:43.538453 | 2021-09-20 17:08:25.589423 | zone3 | idc | 0 | idc3 |
| 2021-09-12 14:49:43.536334 | 2021-09-20 17:07:44.025486 | zone1 | region | 0 | region1 |
| 2021-09-12 14:49:43.537407 | 2021-09-20 17:07:48.804019 | zone2 | region | 0 | region1 |
| 2021-09-12 14:49:43.538453 | 2021-09-20 17:07:57.702095 | zone3 | region | 0 | region2 |
| 2021-09-12 14:49:43.535302 | 2021-09-14 10:25:55.748709 | zone1 | status | 2 | ACTIVE |
| 2021-09-12 14:49:43.536334 | 2021-09-12 14:49:43.536334 | zone2 | status | 2 | ACTIVE |
| 2021-09-12 14:49:43.537407 | 2021-09-12 14:49:43.537407 | zone3 | status | 2 | ACTIVE |
| 2021-09-12 14:49:43.536334 | 2021-09-12 14:49:43.536334 | zone1 | zone_type | 0 | ReadWrite |
| 2021-09-12 14:49:43.537407 | 2021-09-12 14:49:43.537407 | zone2 | zone_type | 0 | ReadWrite |
| 2021-09-12 14:49:43.538453 | 2021-09-12 14:49:43.538453 | zone3 | zone_type | 0 | ReadWrite |
+----------------------------+----------------------------+-------+-----------+-------+-----------+
13 rows in set (0.003 sec)
```
当集群所有节点都是同机房的时候,`region``idc` 就只设置为同一个值即可。只有真的三机房部署的时候,才需要设置这些,方便集群正确管理各个节点。比如说避免把一个机房的节点错划到另外一个机房的 `zone` 里了。这些元数据是靠 DBA 维护,前面集群部署时对节点的要求只是连通性、时间延时和网络延时满足要求即可,并不要求机器是否一定在同一个机房。
在后期对集群做读写分离规划时,`region``idc` 还是会对 OBPROXY 的 SQL 路由策略有很大影响的。所以,尽量正确的设置。
## 如何管理 OceanBase 集群参数
OceanBase 集群支持参数定制,以适应不同的场景需求。参数的查看和修改主要是在集群内部租户 SYS 里。
### 参数简介
参数英文名是:`parameter` 。部分参数的生效范围是集群内所有实例,可以通过命令控制是在哪些节点生效。少数参数是在租户范围内生效。
大部分参数的变更是立即生效,极少数参数的变更是需要重启节点。
参数的值、描述、生效范围、是否要重启节点等,都可以通过命令查看。命令是:`show parameters [ like '%参数名特征%' ;`
参数的修改命令是:`alter system set 参数名 = 参数值(如果是字符串,用两个单引号引起来)[ scope = 'xxx.xxx.xxx.xxx:2882' ] ;`
除了集群参数可以定制集群行为外,租户也可以定制,通过变量(`variable`)设置。变量跟参数其实是同一类概念,参数的定位和风格取自 ORACLE 数据库,变量的定位和风格取自 MySQL 数据库。集群 SYS 租户一样可以设置变量值(SYS 租户变量通常不用调整,必须很谨慎),不同租户的变量设置彼此独立,互不影响。大部分变量的设置是立即生效,生效范围根据设置方法不同有实例全局层面和会话层面生效。极少数变量类似 MySQL 的初始化变量,不能在租户里后期修改,只是在 SYS 租户里新建业务租户的时候设置。当然也能后期在 SYS 租户后期修改,只是这个修改也必须充分评估影响。
变量的值、描述都可以通过命令查看。命令是: ` show global | [session] variables [ like '%变量名特征%' ] ; ` , 或者 `show global | [session] variables where variable_name in ('参数1' [, '参数2']) ;`
变量的值的修改命令是: ` set global | [session] 变量名 = 变量值(如果是字符串,用两个单引号引起来); ``global` 指全局生效,对新建会话生效,当前会话不生效。 `session` 或者没有限定符,都表示对当前会话生效。会话断开重连后,又会取全局层面的默认值。
部分变量还有语句级别的设置方法,通过 SQL HINT,影响范围是所修饰的语句。语句级的变量名名字会去掉前面的 `ob_` 部分。下面是常用的几个语句级别的变量命名映射关系。
| 全局|会话变量名 | 语句级变量名 | 描述 |
|---------------------|------------------|------------------------------|
| ob_query_timeout | query_timeout | 指定语句执行超时的阈值 |
| ob_read_consistency | read_consistency | 指定读一致性级别,默认是强一致性读,可以修改为弱一致性读 |
常用集群参数作用的分类如下:
+ 影响节点可用性的判断。如 `server_permanent_offline_time``server_temporary_offline_time`
+ 影响集群日志的特点。如 `enable_syslog_recycle``enable_syslog_wf``max_syslog_file_count``syslog_level``syslog_io_bandwidth_limit`
+ 影响集群冻结和合并行为。如 `minor_freeze_times``major_freeze_duty_time``enable_major_freeze``freeze_trigger_percentage``micro_block_merge_verify_level``minor_merge_concurrency``merge_thread_count``zone_merge_timeout``enable_merge_by_turn``zone_merge_order``zone_merge_concurrency` `enable_manual_merge`
+ 影响集群节点内存分配的行为。如 `memstore_limit_percentage``memory_limit_percentage``sql_audit_memory_limit``system_memory``memory_limit`
+ 影响集群负载均衡行为。如 `server_balance_cpu_mem_tolerance_percent``server_balance_disk_tolerance_percent``balancer_tolerance_percentage``data_copy_concurrency``server_data_copy_out_concurrency``server_data_copy_in_concurrency`
以上这些参数的作用在命令里可以查,也可以查询官方文档。在后面运维、性能调优的时候用到这些参数的时候再详细描述。
### 参数文件
查看集群的参数除了登录集群 SYS 实例,还可以通过查看参数文件的方法。OB 集群参数文件名字固定是:`observer.config.bin` ,在目录 `etc/` 下。这个目录在进程工作目录下。如果是 OBD 部署的 OceanBase 集群,配置文件里里的 `home_path` 就是集群的工作目录。另外一个确定进程工作目录方法就是直接看进程的运行环境。
```bash
[admin@obce02 oceanbase]$ sudo ls -lrth /proc/`pidof observer`/cwd
lrwxrwxrwx 1 admin admin 0 Sep 21 13:10 /proc/56779/cwd -> /home/admin/oceanbase
```
进程目录下的 `cwd` 就是指向实际工作目录的软链接。集群的参数文件是二进制文件,不能直接编辑和查看,可以使用命令 `strings`
```bash
[admin@obce02 oceanbase]$ strings /home/admin/oceanbase/etc/observer.config.bin | grep config_addition
config_additional_dir=/data/obdemo/etc3;/redo/obdemo/etc2
```
通常集群里参数修改成功后都会持久化到参数文件里。
### 进程启动参数
如果不小心参数文件设置的不合理,可能会导致进程启动失败。而这个参数文件又不能直接修改,此时的办法就是启动进程 `observer` 的时候指定启动参数。
如:
```bash
cd /home/admin/oceanbase && bin/observer -o "memory_limit=50G, system_memory=10G"
```
这种适合调整少数参数,让进程启动起来。启动成功后,这个参数也会持久化到进程的参数文件里。
## 如何管理 OceanBase 内存
有关 OceanBase 相关的内存可以先看这个内存分配图。
![oceanbase memory](media/16357302538843.jpg)
下面逐个详细介绍。
### 进程总内存
OceanBase 进程启动后会从主机那里拿到绝大部分资源(CPU/内存/空间),具体多少就是由集群参数控制的。其中集群节点进程启动需要多少内存是由参数 `memory_limit``memory_limit_percentage` 共同控制的。生产环境通常是不设置 `memory_limit` 值(默认值为 0),只设置 `memory_limit_percentage` 值(默认值 80% )。这样每个节点进程获取的总内存是主机可用内存的 80% 。当主机内存扩容后(比如说是虚拟机),OB 节点能获得的内存也相应增加。如果设置了 `memory_limit` 的值,那进程获取的总内存就是它指定的具体的值。这个值最少不能少于 8G 。 设置这个参数通常是测试环境跑 OceanBase ,或者想单机启动多个 OceanBase 进程测试用。
这两个参数可以在线调整,但是注意,最大不能超过主机可用内存,最小不能少于 8G 。通常调大是安全的,调小可能有风险。
### 保留内存
OceanBase 内部租户除了 SYS 租户外,还有一批不可见的内部租户需要一些内存,这部分内存通过参数 `system_memory` 大小控制。不同版本的 OB,这个参数默认值不一样。生产环境主机内存有 386G 内存以上的时候,这个参数默认值是 50G 。如果主机内存只有 256G 的时候,这个参数内存可以小一些,如 30G 左右。第 2 课里为了在 10G 内存的机器上跑 OceanBase ,这个参数值是 3~5G 。
这个内存在内部租户 ID 500 的租户下,给业务租户某些内部操作使用,是有作用的。
### SYS 租户内存
SYS 租户是集群初始化的时候自动创建,SYS 租户的资源规格随着可用内存大小、版本变化,会有一些细节差别。SYS 租户的内存大小是由默认资源单元 `sys_unit_config` 的规格定义的。
```sql
select unit_config_id, name, round(max_memory/1024/1024) max_mem_MB, round(min_memory/1024/1024) min_mem_MB from __all_unit_config where unit_config_id = 1;
+----------------+-----------------+------------+------------+
| unit_config_id | name | max_mem_MB | min_mem_MB |
+----------------+-----------------+------------+------------+
| 1 | sys_unit_config | 1536 | 1024 |
+----------------+-----------------+------------+------------+
1 row in set (0.009 sec)
```
默认的 `sys_unit_config` 的内存规格最小值和最大值不一样,为了更准确的计算集群可用资源,建议把这个内存最小值跟最大值更新为同一个值(只能变大,不要小于默认最大值)。后续创建业务租户的时候,也强烈建议 CPU 和 内存的最小值和最大值保持一致。
```sql
alter resource unit sys_unit_config min_memory = '1536M';
```
如果集群节点规模在 10台以内, SYS 租户资源通常不需要调大。如果集群规模有几十台时,观察一下 SYS 租户的内存和CPU 利用率,如果有出现资源瓶颈,就适当调整 SYS 租户资源规格。最大的时候只需要独占一台机器资源。 SYS 租户最多只需要独占 3台机器。
### MEMSTORE 内存
每个租户分得的内存,默认有一半用于存放租户的增量数据,也叫 MEMSTOER 。这个内存大小通过参数 `memstore_limit_percentage` 控制。默认值是 `50%` 。当写多读少的时候,可以将这个参数值调高,比如说在大批量导数据的时候,没有读的时候,可以临时调整到 `80%` 。当然这个这个参数也不是任意可以调整。租户的内存除去增量数据占用的内存外,其他的放静态的数据(`KVCache`)、SQL执行计划缓存等(`PLAN CACHE`)、SQL运算的临时内存等(`SQLAREA`)。如果这些内存资源过少,可能有时候会有报错,或者性能很慢的现象出现。目前 OceanBase 对内存的管理还是手动分配为主,将来或许可以做到动态管理。
OceanBase 的读写模型是 LSM-Tree,写操作也是追加模式。数据表是以有序表格,又叫 `SSTABLE`,全称 `SORTED STRING TABLE` 。写不是在原来的数据块上修改,而是新开辟内存记录变化的增量。原有的数据保存在 `KVCache` 不变,称之为基线数据。绝大部分业务一天的数据变化量相当于数据总量来说比重很小,OceanBase 的这种增量写是很节省内存的。所以,通常说 OceanBase 的增量数据都是在内存里不落盘的。严格来说并不对,是延迟落盘。
### 如何对OB 集群进行合并
OceanBase 会每天定时将这部分增量数据跟基线数据在内存中合并成新的数据块然后以追加方式写回到磁盘数据文件中。这个合并操作叫 `major freeze`,合并的时间调度是通过参数 `major_freeze_duty_time` 设定,默认值是 `02:00` 。每天调度一次。合并操作也可以手动发起。合并的时候会消耗一定资源,对影响有一些影响。不过合并的并发也是可以通过参数 `merge_thread_count` 控制的,并且参数 `merger_warm_up_duration_time` 调整合并线程启动时间。调整这两个参数可以在合并的速度和稳定性之间取一个最佳平衡。
当租户内存不是很大,或者业务写量非常大的时候,这个租户的增量内存可能会写满,然后业务就写不进去,报错 `NO MEMORY` 。这是 OceanBase 运维要极力避免的。
命令:`alter system major freeze;`
```sql
SELECT ZONE,svr_ip,major_version,ss_store_count,merged_ss_store_count,modified_ss_store_count,merge_start_time,merge_finish_time,merge_process
FROM __all_virtual_partition_sstable_image_info
order by major_version desc ;
```
最终合并结果确认是查看 合并状态。
```sql
select * from __all_zone where name in ('merge_status','all_merged_version','broadcast_version');
```
正常情况都是 'IDLE',且合并版本和广播版本保持一致。
### 如何对OB 集群进行限速和转储
首先,OceanBase 针对租户可以有一个增量内存写限速的设计,触发限速有两个维度,一是增量内存利用率到达参数 `writing_throttling_trigger_percentage` 设置,二是预估剩余增量内存持续可用时间小于或等于参数 `writing_throttling_maximum_duration` 定义。默认 `writing_throttling_trigger_percentage` 参数的值是 `100%`,就是不限速。实际业务建议都改为 最大 `90%` 。这个限速设计并不是特别精准,所以如果写入量非常大的话的,这个参数还要适当再调低。当业务写被限速的时候,业务表现为普通的 DML SQL 性能会变慢(延时变大),等待事件是 `mem allocation` 等。限速很影响性能,所以也是要极力避免的。这就要靠下一个机制。
第二,OceanBase 除了 `major freeze` 操作可以释放内存外,还有 `minor freeze` 操作也可以释放内存,这个通常也叫转储。转储相比合并,对资源的占用更少,对性能的影响也很小。转储是直接将内存中的增量数据以 SSTABLE 格式直接写到磁盘数据文件中,从而释放增量内存。
转储的触发有两个机制。一是手动触发,命令是 `alter system minor freeze ;` 。命令立即返回,命令发生时内存中的增量块会被冻结禁止写入,新的写入会生成新的增量内存块去接受写入。二是自动触发,当增量内存部分的利用率达到参数 `freeze_trigger_percentage` 指定的值(默认是 `70`,表示 `70%`),自动触发 `minor freeze` 操作。转储可以有很多轮,当转储的次数达到参数 `minor_freeze_times` 指定的值,会自动触发合并(`major freeze`)操作。
转储也是可以调优的,通过调整转储的并发参数(`minor_merge_concurrency`),也可以在转储的速度和稳定性之间取得一个平衡。
实际经验表明,当租户内存规模在 50G 以上时,通过调整转储参数,绝大部分时候是可以避免增量内存写入限速或内存不足的错误。如果业务增量写非常大的时候,那说明租户内存有瓶颈了,那就是要对内存资源扩容的信号。
注意:如果是临时大批量导入数据,可以临时对租户内存进行扩容。等导入完毕后,一轮合并结束后,就可以最租户内存进行缩容。在线扩容和缩容,也是 OceanBase 最核心和实用的能力。
## 如何重启 OceanBase 节点和集群
OceanBase 集群生产环境默认是有三副本,任意一个副本不可用,OceanBase 的数据库内部可能会有一次故障切换。对业务的影响是部分数据访问会中断,恢复时间在 30s 左右,数据绝对不丢。这个故障切换过程不需要运维介入,可靠性很高。
如果是计划中的节点重启,OceanBase 集群内部也会自动发起切换,对业务的影响是大部分读写是感知不到切换。少数大事务会中断需要重试。所以,整体上,OceanBase 集群计划重启是可以做到业务不用停机,数据库服务不中断。具体的做法就是按机器所属的 ZONE ,分批次重启 OceanBase 机器。
### (建议)OB 集群合并
由于 OceanBase 节点重启后会有一个恢复的过程,恢复的时间取决于从最近的基线版本到当前时间要恢复的数据量。OceanBase 可能要应用很多 `clog` 去追赶数据。如果追赶的 `clog` 数据量实在太大,OceanBase 节点可能会直接从主副本那里拉取最新的数据。
如果条件允许的话,在重启节点之前,先对整个集群发起一次合并。待合并结束后,每个节点内存中就只有最近一段时间的增量数据,那么节点重启后的恢复时间也是最短的。
+ 发起合并操作
命令:`alter system major freeze;`
具体合并操作确认逻辑请参考前面章节:如何对 OceanBase 集群进行合并 。
### 如何重启 OceanBase 节点
+ 节点停服
OceanBase 并没有提供重启节点的命令。但是提供了节点停服的命令。
命令是:`alter system stop server '节点ip:2882' ;`
节点停服后,节点上如果有主副本,会自动切换为备副本。节点的备副本依然参与投票,但不会当选为主副本。OB 节点停服跟 OceanBase 宕机性质不一样,节点停服时间可以超出参数永久下线时间(`server_permanent_offline_time`) 而不会导致节点真的下线。
节点停服后,大概1-2秒后就能观察到有主备副本切换事件。
确认 SQL 如下:
```sql
SELECT DATE_FORMAT(gmt_create, '%b%d %H:%i:%s') gmt_create_ , module, event, name1, value1, name2, value2, rs_svr_ip,name3,value3,name4,value4
FROM __all_rootservice_event_history
WHERE 1 = 1
AND module IN ('leader_coordinator', 'balancer' )
ORDER BY gmt_create DESC
LIMIT 20;
```
通常几秒钟所有分区副本的主备切换就结束了。
+ 杀进程
然后在主机里杀掉 节点进程 `observer` 。命令如下:
```bash
kill `pidof observer`
```
如果是测试环境,比较着急可以不用发起节点停服命令,直接强行杀进程。
```bash
kill -9 `pidof observer`
```
+ 启动进程
启动节点进程最关键的是在上一次工作目录启动。所以建议第一次就把工作目录跟安装目录保持一致,那就不会有出错的可能。
```bash
cd /home/admin/oceanbase && bin/observer
```
通常启动的时候不需要再带启动参数了。除非这次启动节点就是为了修改某个参数,那就加上 `-o` 带上具体的参数。示例如下:
```bash
cd /home/admin/oceanbase && bin/observer -o "datafile_size=80G,clog_disk_usage_limit_percentage=96 "
```
进程启动后等 5~10 秒钟后再查看一下确认进程启动成功。
```bash
# 确认进程还在
ps -ef | grep observer | grep -v grep
# 确认端口监听成功
netstat -ntlp | grep `pidof observer`
```
+ 启动服务并确认节点服务状态
进程监听成功还不够,节点还需要一个数据恢复过程,就是应用 CLOG 的过程。
如果此前对节点停服了,先把节点服务启动。
```sql
alter system start server '节点IP:2882' ;
```
确认节点的服务状态。
```sql
select a.zone,concat(a.svr_ip,':',a.svr_port) observer, cpu_total, (cpu_total-cpu_assigned) cpu_free, round(mem_total/1024/1024/1024) mem_total_gb, round((mem_total-mem_assigned)/1024/1024/1024) mem_free_gb, usec_to_time(b.last_offline_time) last_offline_time, usec_to_time(b.start_service_time) start_service_time, b.status, usec_to_time(b.stop_time) stop_time, b.build_version
from __all_virtual_server_stat a join __all_server b on (a.svr_ip=b.svr_ip and a.svr_port=b.svr_port)
order by a.zone, a.svr_ip
;
```
关注:
+ 节点状态 `status` :升级前没有 `inactive` 值,升级过程中会有。
+ 节点服务时间 `start_service_time` 的时间不是默认值(`1970-01-01 08:00:00.000000`)。如果是,则表示节点还没有恢复结束。
+ 节点停止时间 `stop_time` 的时间是默认值(`1970-01-01 08:00:00.000000`)。如果不是,则表示节点被停服( `stop server`) 了,先启动服务(`start server`)。
一定要确认节点的`start_service_time` 正常了才去重启其他 Zone 对应的其他副本所在的机器。同一个 Zone 的多台机器是可以并行重启的。
## 如何重置 OceanBase 节点
重置 OceanBase 节点属于应急手段之一,是高危操作,仅用在特殊的场景。如下:
+ 节点磁盘损坏后修复,怀疑数据有丢失的。此时需要重做副本。
+ 想对节点数据文件大小进行缩容,重新初始化进程 `observer`,相当于重做副本。
重置 OceanBase 节点的步骤如下。
### 停掉节点进程
因为是要重置节点,所以停进程的方法就不用像重启节点那么繁琐。直接杀进程。
```bash
kill -9 `pid of observer`
```
如果进程本来就停掉了,就不用这步操作。
杀进程之前,确保知道此前的进程启动时工作目录是什么。用下面命令
```bash
ll /proc/`pidof observer`/cwd
lrwxrwxrwx 1 admin admin 0 Sep 25 08:18 /proc/15495/cwd -> /home/admin/oceanbase-ce
```
### 确认节点永久下线
通常情况下,节点进程异常,相当于节点掉线。首先节点掉线时间超过参数 `server_temporary_offline_time` 值(默认 60s )后,状态会进入“临时下线”状态。
此后,如果节点进程能重新正常起来,节点还是集群的成员,会自动同步落后的数据。
示例:
```sql
select a.zone,concat(a.svr_ip,':',a.svr_port) observer, usec_to_time(b.last_offline_time) last_offline_time, usec_to_time(b.start_service_time) start_service_time, b.status, usec_to_time(b.stop_time) stop_time
from __all_virtual_server_stat a join __all_server b on (a.svr_ip=b.svr_ip and a.svr_port=b.svr_port)
order by a.zone, a.svr_ip
;
输出:
+-------+--------------------+----------------------------+----------------------------+----------+----------------------------+
| zone | observer | last_offline_time | start_service_time | status | stop_time |
+-------+--------------------+----------------------------+----------------------------+----------+----------------------------+
| zone1 | 172.20.249.52:2882 | 1970-01-01 08:00:00.000000 | 2021-09-25 08:19:06.622351 | active | 1970-01-01 08:00:00.000000 |
| zone2 | 172.20.249.49:2882 | 1970-01-01 08:00:00.000000 | 2021-09-25 08:19:07.392669 | active | 1970-01-01 08:00:00.000000 |
| zone3 | 172.20.249.51:2882 | 1970-01-01 08:00:00.000000 | 1970-01-01 08:00:00.000000 | inactive | 1970-01-01 08:00:00.000000 |
+-------+--------------------+----------------------------+----------------------------+----------+----------------------------+
3 rows in set (0.008 sec)
```
节点掉线后进入“临时下线”状态时,上面节点视图的 `status` 列会变为 `inactive`
同时,在 OceanBase 事件日志视图里也会有一笔“临时下线”记录。
```sql
SELECT DATE_FORMAT(gmt_create, '%b%d %H:%i:%s') gmt_create_ , module, event, name1, value1, name2, value2, rs_svr_ip
FROM __all_rootservice_event_history
WHERE 1 = 1
AND module IN ('server','root_service')
and gmt_create > SUBDATE(now(),interval 1800 second)
ORDER BY gmt_create DESC
LIMIT 10;
输出:
+----------------+--------------+------------------+--------+----------------------+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+
| gmt_create_ | module | event | name1 | value1 | name2 | value2 | rs_svr_ip |
+----------------+--------------+------------------+--------+----------------------+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+
| Sep26 12:39:32 | root_service | admin_set_config | ret | 0 | arg | {items:[{name:"rootservice_list", value:"172.20.249.52:2882:2881;172.20.249.49:2882:2881", comment:"", zone:"", server:"0.0.0.0", tenant_name:"", exec_tenant_id:1, tenant_ids:[]}], is_inner:false} | 172.20.249.52 |
| Sep26 12:39:32 | server | lease_expire | server | "172.20.249.51:2882" | | | 172.20.249.52 |
+----------------+--------------+------------------+--------+----------------------+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+
2 rows in set (0.001 sec)
```
节点掉线首先会有个 `lease_expire` 事件,都是节点掉线。节点掉线原因可以有很多。如进程宕掉、网络超时或延时过大、时间误差过大等。
此外,由于这个示例集群是 三节点,所以一个节点的掉线对总控服务成员也是有影响的,所以参数 `rootservice_list` 会自动变化,踢掉了故障节点。
如果节点掉线时间再超过参数 `server_permanent_offline_time` 值(默认是 3600s )后,节点会进入“永久下线”状态。此时,集群会清空该节点上的数据副本,并自动在同 Zone 其他节点寻求资源补足被清空的数据副本。如果没有可用资源,则这个副本对应的分区就只剩下两个副本(或者四个副本)。此时依然是多数派副本可用,所以数据读写是正常的,只是如果再宕机就面临进一步风险,可能就是不可用。
```sql
SELECT DATE_FORMAT(gmt_create, '%b%d %H:%i:%s') gmt_create_ , module, event, name1, value1, name2, value2, rs_svr_ip FROM __all_rootservice_event_history WHERE 1 = 1 AND module IN ('server','root_service') and gmt_create > SUBDATE(now(),interval 7200 second) ORDER BY gmt_create DESC LIMIT 10;
输出:
+----------------+--------------+----------------------+--------+----------------------+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+
| gmt_create_ | module | event | name1 | value1 | name2 | value2 | rs_svr_ip |
+----------------+--------------+----------------------+--------+----------------------+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+
| Sep26 13:39:48 | server | clear_with_partition | server | "172.20.249.51:2882" | | | 172.20.249.52 |
| Sep26 13:39:22 | server | permanent_offline | server | "172.20.249.51:2882" | | | 172.20.249.52 |
| Sep26 12:39:32 | root_service | admin_set_config | ret | 0 | arg | {items:[{name:"rootservice_list", value:"172.20.249.52:2882:2881;172.20.249.49:2882:2881", comment:"", zone:"", server:"0.0.0.0", tenant_name:"", exec_tenant_id:1, tenant_ids:[]}], is_inner:false} | 172.20.249.52 |
| Sep26 12:39:32 | server | lease_expire | server | "172.20.249.51:2882" | | | 172.20.249.52 |
+----------------+--------------+----------------------+--------+----------------------+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+
4 rows in set (0.004 sec)
```
从上面可以看到,节点被永久下线时,会有事件 `permanent_offline`,节点的数据也会随后被清空,有事件 `clear_with_partition`
从“临时下线”到“永久下线”时间可能有点长,默认 1个小时。如果你不想等,可以临时把这个节点的参数 `server_permanent_offline_time` 调小。等节点永久下线后再重新上线时,把参数再改回来。
```sql
alter system set server_permanent_offline_time='360s' server='172.20.249.51:2882';
```
### (可选)清理数据库相关文件
通常如果目的是重建副本,这一步并不是必须的。当想借重建副本操作重新定义一下数据文件的大小。
清理的文件包括:
+ 运行日志。包括 `log` 目录下 `observer.log``rootservice.log``election.log`
+ 数据文件。包括 `sstable` 下的 `block_file` 文件。
+ 事务日志文件。包括 目录 {`ilog``clog``slog` } 下的文件。
注意,参数文件 `etc` 不要删除,这样再次启动节点进程的时候方便一些,只需要在启动参数 `-o` 里指定需要修改的参数。
下面删除只是示例,注意目录结构:
```bash
[admin@obce03 store]$ cd /home/admin/oceanbase-ce/store
[admin@obce03 store]$ ls
clog ilog slog sstable
[admin@obce03 store]$ pwd
/home/admin/oceanbase-ce/store
[admin@obce03 store]$ /bin/rm -rf */*
```
### 启动进程
如果没有清理参数文件,则可以直接启动进程 `observer` 。可以通过 `-o` 修改自己的参数。
```bash
cd /home/admin/oceanbase-ce && bin/observer
cd /home/admin/oceanbase-ce && bin/observer -o "datafile_size=100G"
```
启动后确认端口监听正常,节点状态正常,这个前面已经阐述,就不再重复了。
## 如何给 OceanBase 集群扩容/缩容/替换机器
OceanBase 集群扩容或缩容主要是调整集群机器资源池。
扩容就是往集群里加机器,考虑点很简单,复杂的是缩容。缩容的前提是剩余的机器能容纳所有租户的资源需求。如果资源不足,通常就要先对集群里的租户进行缩容,这个会在后面介绍到。所以集群的缩容留到租户缩容介绍后再补充介绍。
集群机器替换就是先扩容后缩容,集群机器资源池并没有发生变化,所以操作也很简单。
### 集群扩容节点
OceanBase 集群的标准部署模式是有三个 ZONE,每个ZONE 的机器数量都对等,每个 ZONE 里可以存在不同配置的机器,但是不同 ZONE 之间的机器配置是对等的,机器资源总量也是对等的。非标准部署就是 ZONE 之间的机器配置不对等、数量也不对等。技术上这也是可以运行的,只是资源最少的机器或 ZONE 会成为集群资源的瓶颈。
OceanBase 集群扩容的标准形式就是向每个 ZONE 里添加同等配置的机器。非标准做法就是只向一个 ZONE 里添加机器。步骤如下:
+ 初始化新机器环境,启动进程 OBSERVER ,注意在启动参数里指定该机器所属的 ZONE 。
OBSERVER 进程第一启动,需要指定一些参数,具体步骤可以参考第 2 课安装部署。部分参数再重复说明一下。
```
-p : 这是连接端口。默认是 2881 ,可以根据实际情况修改
-P : 这是 RPC 端口,默认是 2882 ,可以根据实际情况修改
-r :指定 `rootservice list`,跟集群当前的 `rootservice list` 保持一致。
-z :指定 节点所属 ZONE 。
```
+ 在集群 SYS 租户里添加这个新节点。
命令是:`ALTER SYSTEM ADD SERVER '节点IP:RPC端口' ZONE '节点所属ZONE';`
```sql
alter system add server '11.166.87.5:2882' zone 'zone1';
```
以上命令立即返回,然后查看节点状态就可以看到新节点的状态是 `active`
```sql
select a.zone,concat(a.svr_ip,':',a.svr_port) observer, cpu_total, (cpu_total-cpu_assigned) cpu_free, round(mem_total/1024/1024/1024) mem_total_gb, round((mem_total-mem_assigned)/1024/1024/1024) mem_free_gb, usec_to_time(b.last_offline_time) last_offline_time, usec_to_time(b.start_service_time) start_service_time, b.status, usec_to_time(b.stop_time) stop_time
from __all_virtual_server_stat a join __all_server b on (a.svr_ip=b.svr_ip and a.svr_port=b.svr_port)
order by a.zone, a.svr_ip
;
```
然后换下一个 ZONE 继续新增机器。当三个 ZONE 都扩容完毕,集群扩容操作就完成了。是不是很简单?
不过,这里只是运维的工作结束了。集群扩容后,新的节点还不会马上被集群里的租户使用到。集群资源池扩大后,新节点的利用率是 0 ,约 1 min 集群就会发现资源利用不均衡,就会启动负载均衡逻辑,尝试将其他租户的资源单元迁移到新的节点上。集群缩容也会触发负载均衡。有关负载均衡的介绍留在后面。
集群给每个 ZONE 扩容节点后,集群整体的资源池能力新增了,可用的资源也增加了。但是租户的资源并没有增加。租户不一定能利用上新机器,所以后面还有租户扩容。
### 集群缩容节点
OceanBase 集群的标准部署模式是有三个 ZONE,每个ZONE 的机器数量都对等。集群缩容节点就是减小某个 ZONE 的节点数。通常缩容节点建议每个 ZONE 都减少相同的节点。实际操作没有这个限制,是按 ZONE 操作。
集群缩容节点时,会出发租户资源的负载均衡。剩余的节点必须能容纳租户资源需求。否则,这个节点缩容命令会报错。所以,集群节点缩容之前,也是要计算集群可用资源和了解各个节点的资源池分布。
集群缩容节点命令就是将某个节点从某个 ZONE 里删除。
示例 SQL 如下:
```sql
alter system delete server '11.166.87.5:2882' zone 'zone1';
```
集群缩容节点,一定会触发租户资源负载均衡操作,会触发数据迁移逻辑。建议关注数据迁移的进度和影响。
查看集群事件日志视图,可以观察到这个。
```sql
SELECT DATE_FORMAT(gmt_create, '%b%d %H:%i:%s') gmt_create_ , module, event, name1, value1, name2, value2, rs_svr_ip
FROM __all_rootservice_event_history
WHERE 1 = 1
AND gmt_create > SUBDATE(now(),interval 1 hour)
ORDER BY gmt_create DESC
LIMIT 20;
```
## 如何给 OceanBase 集群扩容/缩容副本
### 三副本扩容到五副本
生产环境 OceanBase 集群通常有三个 ZONE,分布在一个机房或者同城三个机房,数据有三份也简称三副本。当要建设异地容灾机房的时候,可能会选择从三个 ZONE 扩容到 五个 ZONE(即五副本)。另外一种就是机房搬迁。将集群从一个机房在线搬迁到另外一个机房。这其中都涉及到三副本扩容到五副本。
+ 机器节点进程初始化
主要是 ZONE4 和 ZONE5 的机器初始化,注意启动参数中所属的 ZONE 要正确。
+ 新增 ZONE
```sql
alter system add zone 'ZONE4';
alter system add zone 'ZONE5';
```
+ 给新增的 ZONE 添加节点
```sql
alter system add server '11.166.87.5:2882' zone 'zone4';
alter system add server '11.166.87.6:2882' zone 'zone5';
```
+ 为租户在新增节点上分配资源
```sql
create resource pool test_pool_z4 unit='unit_4c16g' , unit_num=1, zone_list=('ZONE_4');
create resource pool test_pool_z5 unit='unit_4c16g' , unit_num=1, zone_list=('ZONE_5');
```
该命令立即返回。
+ 修改租户的 LOCALITY ,给租户增加新增的资源池
注意,每次只能给租户新增一个资源池。
```sql
alter tenant test227 resource_pool_list=('test_pool_z1','test_pool_z2','test_pool_z3','test_pool_z4');
alter tenant test227 resource_pool_list=('test_pool_z1','test_pool_z2','test_pool_z3','test_pool_z4','test_pool_z5');
```
该命令立即返回。但是租户在新增 ZONE 补数据副本是异步进行的,租户分区数越多,数据量越大,这个时间越长。
+ (可选)确认复制表的副本是否扩容
复制表的 LOCALITY 可以指定在哪些 ZONE 里配置复制表副本,也可以不指定,默认是整个集群范围内。如果原来指定 LOCALITY,这里需要手动添加新的 ZONE 。
这个待验证。
```sql
alter table BMSQL_ITEM locality='F,R{all_server}@ZONE1, F,R{all_server}@ZONE2, F,R{all_server}@ZONE3, F,R{all_server}@ZONE4,F,R{all_server}@ZONE_5';
```
+ 确认集群和租户副本扩容结果
```sql
select a.zone,concat(a.svr_ip,':',a.svr_port) observer, cpu_total, (cpu_total-cpu_assigned) cpu_free, round(mem_total/1024/1024/1024) mem_total_gb, round((mem_total-mem_assigned)/1024/1024/1024) mem_free_gb, usec_to_time(b.last_offline_time) last_offline_time, usec_to_time(b.start_service_time) start_service_time, b.status, usec_to_time(b.stop_time) stop_time
from __all_virtual_server_stat a join __all_server b on (a.svr_ip=b.svr_ip and a.svr_port=b.svr_port)
order by a.zone, a.svr_ip
;
select t1.name resource_pool_name, t2.`name` unit_config_name, t2.max_cpu, t2.min_cpu, t2.max_memory/1024/1024/1024 max_mem_gb, t2.min_memory/1024/1024/1024 min_mem_gb, t3.unit_id, t3.zone, concat(t3.svr_ip,':',t3.`svr_port`) observer,t4.tenant_id, t4.tenant_name
from __all_resource_pool t1 join __all_unit_config t2 on (t1.unit_config_id=t2.unit_config_id)
join __all_unit t3 on (t1.`resource_pool_id` = t3.`resource_pool_id`)
left join __all_tenant t4 on (t1.tenant_id=t4.tenant_id)
order by t1.`resource_pool_id`, t2.`unit_config_id`, t3.unit_id
;
select * from __all_tenant;
```
### 五副本缩容到三副本
将上面步骤反过来执行即可。
+ (可选)调整复制表的 LOCALITY ,缩减副本数。
如果有复制表,并且当初复制表创建的时候指定了
+ 修改租户的 LOCALITY,缩减副本数。
```sql
alter tenant sys resource_pool_list = ('sys_pool','sys_pool_1');
alter tenant sys resource_pool_list = ('sys_pool');
alter tenant test227 resource_pool_list=('test_pool_z1','test_pool_z2','test_pool_z3','test_pool_z4');
alter tenant test227 resource_pool_list=('test_pool_z1','test_pool_z2','test_pool_z3');
```
注意,SYS 租户也许要缩减副本数。
这一步命令立即返回,但是删除副本会异步进行,速度也很快,分区数多一些也不要紧,基本上在分钟级别完成。
+ 删除多余的资源池
```sql
drop resource pool sys_pool_1 ;
drop resource pool sys_pool_2 ;
drop resource pool test_pool_z4 ;
drop resource pool test_pool_z5 ;
```
## 如何升级 OceanBase 集群
OceanBase 集群升级就是将集群所有节点进程 OBSERVER 的版本升级。通常小版本的升级是直接替换可执行文件 `observer` 然后将进程重启即可。但有时候升级还会涉及到元数据的变更,这个就需要参考新版本发布时具体的 RELEASE NOTE 说明。
### 确认集群当前状态正常
OceanBase 集群可以在线不停服升级,具体版本就是按 ZONE 滚动升级。升级前首先要确认集群和各个 ZONE 状态都正常。避免停掉某个 ZONE 时出现多数派故障,数据库访问不可用问题等。
+ 确保集群节点状态正常
```sql
select a.zone,concat(a.svr_ip,':',a.svr_port) observer, cpu_total, (cpu_total-cpu_assigned) cpu_free, round(mem_total/1024/1024/1024) mem_total_gb, round((mem_total-mem_assigned)/1024/1024/1024) mem_free_gb, usec_to_time(b.last_offline_time) last_offline_time, usec_to_time(b.start_service_time) start_service_time, b.status, usec_to_time(b.stop_time) stop_time, b.build_version
from __all_virtual_server_stat a join __all_server b on (a.svr_ip=b.svr_ip and a.svr_port=b.svr_port)
order by a.zone, a.svr_ip
;
```
关注:
+ 节点状态 `status` :升级前没有 `inactive` 值,升级过程中会有。
+ 节点服务时间 `start_service_time` 的时间不是默认值(`1970-01-01 08:00:00.000000`)。如果是,则表示节点异常。
+ 节点停止时间 `stop_time` 的时间是默认值(`1970-01-01 08:00:00.000000`)。如果不是,则表示节点被 `stop server` 了。
+ 节点版本 `b.build_version` 。升级重启后会变为新版本。
+ 观察集群最近一段时间的事件,确保无异常事件
```sql
SELECT DATE_FORMAT(gmt_create, '%b%d %H:%i:%s') gmt_create_ , module, event, name1, value1, name2, value2, rs_svr_ip
FROM __all_rootservice_event_history
WHERE 1 = 1
AND module IN ('server','root_service','daily_merge')
and gmt_create > SUBDATE(now(),interval 1 day)
ORDER BY gmt_create DESC
LIMIT 100;
```
如果大量正常的事件影响查看,就过滤掉。
留意报错的事件是否有影响。下列异常事件一定要先解决。如:合并异常、副本创建/搬迁异常等。
+ (可选)发起合并(`MAJOR FREEZE`
由于集群升级都需要重启节点。为了减少节点重启后的恢复时间,建议升级之前发起一次合并。具体合并方法和确认方法跟“如何重启OB节点”中方法一样。
```sql
alter system major freeze;
```
### 停止 ZONE 服务
OceanBase 集群的升级,按 ZONE 滚动升级。首先选中一个 ZONE ,将该 ZONE 停服。
```sql
alter system stop zone 'zone1';
SELECT * FROM __all_zone where name in ('status','merge_status') and zone = 'zone1';
输出:
+----------------------------+----------------------------+-------+--------------+-------+----------+
| gmt_create | gmt_modified | zone | name | value | info |
+----------------------------+----------------------------+-------+--------------+-------+----------+
| 2021-09-25 08:19:05.067944 | 2021-09-29 15:44:27.162171 | zone1 | merge_status | 0 | IDLE |
| 2021-09-25 08:19:05.066915 | 2021-09-29 19:36:13.108063 | zone1 | status | 1 | INACTIVE |
+----------------------------+----------------------------+-------+--------------+-------+----------+
2 rows in set (0.028 sec)
```
查看事件日志确认。
```sql
SELECT DATE_FORMAT(gmt_create, '%b%d %H:%i:%s') gmt_create_ , module, event, name1, value1, name2, value2, rs_svr_ip
FROM __all_rootservice_event_history
WHERE 1 = 1
AND module IN ('server','root_service','leader_coordinator')
AND gmt_create > SUBDATE(now(),interval 1 hour)
ORDER BY gmt_create DESC
LIMIT 20;
输出:
+----------------+--------------------+--------------------------+------------+----------------------+-----------+--------------------------------+---------------+
| gmt_create_ | module | event | name1 | value1 | name2 | value2 | rs_svr_ip |
+----------------+--------------------+--------------------------+------------+----------------------+-----------+--------------------------------+---------------+
| Sep29 19:36:40 | root_service | full_rootservice | result | 0 | self_addr | "172.20.249.51:2882" | 172.20.249.51 |
| Sep29 19:36:39 | server | load_servers | ret | 0 | has_build | 1 | 172.20.249.51 |
| Sep29 19:36:39 | server | online | server | "172.20.249.52:2882" | | | 172.20.249.51 |
| Sep29 19:36:39 | server | online | server | "172.20.249.49:2882" | | | 172.20.249.51 |
| Sep29 19:36:39 | server | load_servers | ret | 0 | has_build | 1 | 172.20.249.51 |
| Sep29 19:36:39 | root_service | finish_start_rootservice | result | 0 | self_addr | "172.20.249.51:2882" | 172.20.249.51 |
| Sep29 19:36:39 | root_service | start_rootservice | self_addr | "172.20.249.51:2882" | | | 172.20.249.51 |
| Sep29 19:36:39 | root_service | finish_wait_stop | cost | 350586 | | | 172.20.249.52 |
| Sep29 19:36:38 | root_service | finish_stop_thread | ret | 0 | self_addr | "172.20.249.52:2882" | 172.20.249.52 |
| Sep29 19:36:38 | root_service | stop_rootservice | self_addr | "172.20.249.52:2882" | | | 172.20.249.52 |
| Sep29 19:36:38 | leader_coordinator | switch_leader | current_rs | "172.20.249.52:2882" | tenant_id | 1 | 172.20.249.52 |
| Sep29 19:36:22 | leader_coordinator | switch_leader | current_rs | "172.20.249.52:2882" | tenant_id | 1 | 172.20.249.52 |
| Sep29 19:36:13 | leader_coordinator | switch_leader | current_rs | "172.20.249.52:2882" | tenant_id | 1002 | 172.20.249.52 |
| Sep29 19:36:13 | leader_coordinator | switch_leader | current_rs | "172.20.249.52:2882" | tenant_id | 1002 | 172.20.249.52 |
| Sep29 19:36:13 | root_service | stop_zone | ret | 0 | sql_text | alter system stop zone 'zone1' | 172.20.249.52 |
+----------------+--------------------+--------------------------+------------+----------------------+-----------+--------------------------------+---------------+
15 rows in set (0.026 sec)
```
从事件日志看出,当停掉 ZONE 的时候,如果总控服务(`rootservice`)的主副本也在该 ZONE 时,总控服务会发生切换(停掉老的服务,在新的节点上启动新的服务,其他 ZONE 所有节点重新上线),SYS 租户和业务租户都发生切换。
确认节点状态:
```sql
select a.zone,concat(a.svr_ip,':',a.svr_port) observer, usec_to_time(b.last_offline_time) last_offline_time, usec_to_time(b.start_service_time) start_service_time, b.status, usec_to_time(b.stop_time) stop_time
from __all_virtual_server_stat a join __all_server b on (a.svr_ip=b.svr_ip and a.svr_port=b.svr_port)
order by a.zone, a.svr_ip
;
输出:
+-------+--------------------+----------------------------+----------------------------+--------+----------------------------+
| zone | observer | last_offline_time | start_service_time | status | stop_time |
+-------+--------------------+----------------------------+----------------------------+--------+----------------------------+
| zone1 | 172.20.249.52:2882 | 1970-01-01 08:00:00.000000 | 2021-09-25 08:19:06.622351 | active | 1970-01-01 08:00:00.000000 |
| zone2 | 172.20.249.49:2882 | 1970-01-01 08:00:00.000000 | 2021-09-25 08:19:07.392669 | active | 1970-01-01 08:00:00.000000 |
| zone3 | 172.20.249.51:2882 | 1970-01-01 08:00:00.000000 | 2021-09-26 14:05:58.641570 | active | 1970-01-01 08:00:00.000000 |
+-------+--------------------+----------------------------+----------------------------+--------+----------------------------+
3 rows in set (0.017 sec)
```
### 更新节点的 OBSERVER 软件包
通常是 RPM 包更新。方法是:`rpm -uvh oceanbase-xxx.rpm`
命令会自动覆盖可执行文件。但是由 LINUX 系统的设计可知,运行中的 OBSERVER 进程依然持有老的文件句柄,所以不会释放文件。只有在 OceanBase 节点进程退出后才释放。
如果使用了 obd 管理 OceanBase 集群,软件包还需要维护到 OBD 本地仓库或远程仓库里。
# 如何管理 OceanBase 租户
## OceanBase 租户原理
生产环境 OceanBase 集群通常是三副本架构,有三个 ZONE,每个 ZONE 至少一台主机,集群架构是 `N-N-N` (N>=1) 。OceanBase 集群将所有节点的资源(主要是 CPU 、内存和磁盘空间)聚合成大的资源池子后,再二次分配出租户。租户就是逻辑实例,每个租户对应一部分资源(资源主要是CPU、内存)。租户的资源也叫资源池,由每个 ZONE 里一个小的资源池组成。通常三个 ZONE 的资源池大小规格是一样的(可以不一样)。每个 ZONE 里的资源池由至少一个资源单元组成,每个资源单元只能从一个 节点上分配资源。资源单元规格 * 资源单元数 就是租户的可以利用的最大资源。
![](media/16321130545360/16332475304698.jpg)
如上图:
+ 集群的架构是 `3-3-3` 。每个节点总资源是 `30C 200G 内存` ,集群总资源是 `270C 1800G 内存`
+ 默认租户 `sys` 的架构是 `1-1-1`,每个 ZONE 里 的资源单元大小是 `5C20G`
+ 业务租户 `t_trade` 的架构是 `1-1-1`,每个 ZONE 里 的资源单元大小是 `20C40G`
+ 业务租户 `t_pay` 的架构是 `2-2-2` ,每个 ZONE 里 的资源单元大小是 `20C100G` ,每个 ZONE 有两个资源单元,分布在 2 个节点上。
每个租户的数据就从租户的资源池对应的资源单元中分配。租户每个 ZONE 的资源单元数就组成租户的架构:`n-n-n` ( 1<=n<=N)。租户的数据管理单位是分区,分区是表的子集。普通表是一个分区,分区表有多个分区。每个分区有三个副本,分布在三个 ZONE 里,具体就在租户的某三个资源单元中。
![](media/16321093510830/16332484797678.jpg)
说明:
+`t1` 是普通表,只有一个分区,标识是 `t1(p0)`,有三个副本,分别在资源单元 UNIT7, UNIT8, UNIT9 上。
+`t3` 是分区表,有三个分区,标识是 `t3(p0)``t3(p1)``t3(p2)` ,每个分区有三个副本。每个分区的三副本分别在三个资源单元上。
+ 单看一个 ZONE ,表 `t3` 的三个分区分散在两个节点上。如 `t3(p1)` 一个节点, `t3(p0)``t3(p2)` 是一个节点。
总结:一个表有多个分区,这个技术类似存储的条带技术(`Striping`), 每个分区有三个副本,类似存储的镜像技术(`Mirroring`)。
## 如何修改 OceanBase 租户变量/参数
集群的参数有部分参数的生效范围是租户,这部分参数也称为租户的参数,会影响租户的行为。
常用租户参数如下。
| 参数名 | 参数值 | 参数含义 |
|---------------------------------------|-----|-------------------------|
| writing_throttling_maximum_duration | 1h | 增量内存写入限速剩余时间目标上限 |
| writing_throttling_trigger_percentage | 100 | 增量内存写入限速触发阈值。建议设置为 90 。 |
| max_stale_time_for_weak_consistency | 5s | 弱一致性读能容忍最大延时 |
除了参数,租户更常用的是用变量(`variable`)来定制租户的行为。
变量跟参数其实是同一类概念,参数的定位和风格取自 ORACLE 数据库,变量的定位和风格取自 MySQL 数据库。集群 SYS 租户一样可以设置变量值(SYS 租户变量通常不用调整,必须很谨慎),不同租户的变量设置彼此独立,互不影响。大部分变量的设置是立即生效,生效范围根据设置方法不同有实例全局层面和会话层面生效。极少数变量类似 MySQL 的初始化变量,不能在租户里后期修改,只是在 SYS 租户里新建业务租户的时候设置。当然也能后期在 SYS 租户后期修改,只是这个修改也必须充分评估影响。
变量的值、描述都可以通过命令查看。命令是: ` show global | [session] variables [ like '%变量名特征%' ] ; ` , 或者 `show global | [session] variables where variable_name in ('参数1' [, '参数2']) ; `
变量的值的修改命令是: ` set global | [session] 变量名 = 变量值(如果是字符串,用两个单引号引起来); ``global` 指全局生效,对新建会话生效,当前会话不生效。 `session` 或者没有限定符,都表示对当前会话生效。会话断开重连后,又会取全局层面的默认值。
常用的租户变量如下:
| 变量名 | 变量默认值(OB) | 变量含义 |
|---------------------------|--------------------|----------------------------------------------------------------------------------|
| autocommit | ON | 是否自动提交。ON 表示每个DML后会自动添加 commit 语句;OFF 表示不会自动 commit,需要用户主动 commit 或 rollback 事务。 |
| max_allowed_packet | 4194304 | 需要调大,否则 SQL文本或数据量很大的时候可能报错。 |
| sql_mode | STRICT_ALL_TABLES | |
| transaction_isolation | READ-COMMITTED | |
| tx_isolation | READ-COMMITTED | |
| lower_case_table_names | 1 | 1(ON) 表示自动将表名转小写。 |
| ob_tcp_invited_nodes | % | % 表示允许所有 IP 访问。 |
| foreign_key_checks | ON | ON:检查外键约束;OFF:不检查外键约束。 |
| ob_query_timeout | 10000000 | 语句超时时间,单位是微秒(us)。 |
|---------------------|-----------|-------------------------------|
| ob_trx_idle_timeout | 120000000 | 事务空闲超时时间,单位是微秒(us)。 |
| ob_trx_timeout | 100000000 | 事务超时时间,单位是微秒(us)。 |
| ob_trx_lock_timeout | -1 | 锁等待超时时间,默认是 -1,不超时。单位是微秒(us)。 |
| ob_sql_work_area_percentage | 3 | 租户里 SQL可用内存占租户内存比例。|
部分变量还有语句级别的设置方法,通过 SQL HINT,影响范围是所修饰的语句。语句级的变量名名字会去掉前面的 `ob_` 部分。下面是常用的几个语句级别的变量命名映射关系。
| 全局|会话变量名 | 语句级变量名 | 描述 |
|---------------------|------------------|------------------------------|
| ob_query_timeout | query_timeout | 指定语句执行超时的阈值 |
| ob_read_consistency | read_consistency | 指定读一致性级别,默认是强一致性读,可以修改为弱一致性读 |
新建租户常用租户变量修改。修改语句超时时间,避免复杂的语句查询经常超时报错。
如下面是语句超时报错示例:
```sql
MySQL [test]> select sleep(11);
ERROR 4012 (HY000): Timeout
```
租户变量初始化示例:
```sql
set global ob_query_timeout = 100000000;
set global ob_trx_idle_timeout = 1200000000;
set global ob_trx_timeout = 1000000000;
set global ob_trx_lock_timeout = 1000000;
set global ob_sql_work_area_percentage = 50;
```
以上修改需要会话断开重连后生效。
## 如何对租户资源扩容/缩容
租户扩容或缩容主要是指资源扩容或缩容,具体就是调整租户的 CPU 和内存规格。对租户资源扩容的前提是集群中有节点可以分配出租户需要的资源。通常会首先考虑租户所在节点是否有可用资源满足租户资源扩容需求;如果不满足,就考虑集群中其他节点是否有资源。后者会触发集群中租户的资源负载均衡,会伴随着数据迁移逻辑。这个数据迁移完全是 OceanBase 内部逻辑,具备高可用能力,不怕异常宕机,不需要 DBA 介入。
### 租户资源池分裂
在三副本集群里,租户资源池通常也是由三个 ZONE 里三个节点里的资源单元(`resource unit`)组成。在前面创建租户的过程中,每个资源单元使用了统一的资源单元规格,创建了一个资源池包含三个资源单元(同一个资源单元规格)。这是一种最简单的创建和使用形态。通过下面 SQL 查看资源池细节。
```sql
select t1.name resource_pool_name, t2.`name` unit_config_name, t2.max_cpu, t2.min_cpu, round(t2.max_memory/1024/1024/1024) max_mem_gb, round(t2.min_memory/1024/1024/1024) min_mem_gb, t3.unit_id, t3.zone, concat(t3.svr_ip,':',t3.`svr_port`) observer,t4.tenant_id, t4.tenant_name
from __all_resource_pool t1 join __all_unit_config t2 on (t1.unit_config_id=t2.unit_config_id)
join __all_unit t3 on (t1.`resource_pool_id` = t3.`resource_pool_id`)
left join __all_tenant t4 on (t1.tenant_id=t4.tenant_id)
order by t1.`resource_pool_id`, t2.`unit_config_id`, t3.unit_id
;
输出:
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
| resource_pool_name | unit_config_name | max_cpu | min_cpu | max_mem_gb | min_mem_gb | unit_id | zone | observer | tenant_id | tenant_name |
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 1 | zone1 | 172.20.249.52:2882 | 1 | sys |
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 2 | zone2 | 172.20.249.49:2882 | 1 | sys |
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 3 | zone3 | 172.20.249.51:2882 | 1 | sys |
| my_pool | unit1 | 9 | 9 | 11 | 11 | 1001 | zone1 | 172.20.249.52:2882 | 1002 | obmysql |
| my_pool | unit1 | 9 | 9 | 11 | 11 | 1002 | zone2 | 172.20.249.49:2882 | 1002 | obmysql |
| my_pool | unit1 | 9 | 9 | 11 | 11 | 1003 | zone3 | 172.20.249.51:2882 | 1002 | obmysql |
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
6 rows in set (0.132 sec)
```
说明:
+ SYS 租户有一个资源池 `sys_pool`,规格是 `sys_unit_config`
+ 业务租户 `obmysql` 有一个资源池 `my_pool` ,规格是 `unit1`
下面示例是将业务租户的资源池分裂为三个资源池。
```sql
ALTER RESOURCE POOL `my_pool` SPLIT INTO ('my_pool_zone1','my_pool_zone2','my_pool_zone3') ON ('zone1','zone2','zone3');
再次确认资源池细节,输出:
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
| resource_pool_name | unit_config_name | max_cpu | min_cpu | max_mem_gb | min_mem_gb | unit_id | zone | observer | tenant_id | tenant_name |
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 1 | zone1 | 172.20.249.52:2882 | 1 | sys |
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 2 | zone2 | 172.20.249.49:2882 | 1 | sys |
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 3 | zone3 | 172.20.249.51:2882 | 1 | sys |
| my_pool_zone1 | unit1 | 9 | 9 | 11 | 11 | 1001 | zone1 | 172.20.249.52:2882 | 1002 | obmysql |
| my_pool_zone2 | unit1 | 9 | 9 | 11 | 11 | 1002 | zone2 | 172.20.249.49:2882 | 1002 | obmysql |
| my_pool_zone3 | unit1 | 9 | 9 | 11 | 11 | 1003 | zone3 | 172.20.249.51:2882 | 1002 | obmysql |
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
6 rows in set (0.010 sec)
```
将原本一个资源池打散为三个资源池,主要是为了便于调整单独 ZONE 里的资源单元规格。这个不是必须的,只是为了便于更细粒度的扩容或缩容租户资源。
### 租户资源扩容
租户资源扩容就是调整租户单元使用的资源规格的大小,或者更换资源单元规格。
示例:
```sql
alter resource unit sys_unit_config min_cpu=5,min_memory='2G';
alter resource unit unit1 max_cpu=5, min_cpu=5, max_memory='8G', min_memory='8G' ;
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
| resource_pool_name | unit_config_name | max_cpu | min_cpu | max_mem_gb | min_mem_gb | unit_id | zone | observer | tenant_id | tenant_name |
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 1 | zone1 | 172.20.249.52:2882 | 1 | sys |
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 2 | zone2 | 172.20.249.49:2882 | 1 | sys |
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 3 | zone3 | 172.20.249.51:2882 | 1 | sys |
| my_pool_zone1 | unit1 | 5 | 5 | 8 | 8 | 1001 | zone1 | 172.20.249.52:2882 | 1002 | obmysql |
| my_pool_zone2 | unit1 | 5 | 5 | 8 | 8 | 1002 | zone2 | 172.20.249.49:2882 | 1002 | obmysql |
| my_pool_zone3 | unit1 | 5 | 5 | 8 | 8 | 1003 | zone3 | 172.20.249.51:2882 | 1002 | obmysql |
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
6 rows in set (0.026 sec)
CREATE resource unit unit2 max_cpu=8, min_cpu=8, max_memory='10G', min_memory='10G', max_iops=10000, min_iops=1000, max_session_num=1000000, max_disk_size='1024G';
alter resource pool my_pool_zone1 unit = 'unit2';
查看资源池分布细节输出:
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
| resource_pool_name | unit_config_name | max_cpu | min_cpu | max_mem_gb | min_mem_gb | unit_id | zone | observer | tenant_id | tenant_name |
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 1 | zone1 | 172.20.249.52:2882 | 1 | sys |
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 2 | zone2 | 172.20.249.49:2882 | 1 | sys |
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 3 | zone3 | 172.20.249.51:2882 | 1 | sys |
| my_pool_zone1 | unit2 | 8 | 8 | 10 | 10 | 1001 | zone1 | 172.20.249.52:2882 | 1002 | obmysql |
| my_pool_zone2 | unit1 | 5 | 5 | 8 | 8 | 1002 | zone2 | 172.20.249.49:2882 | 1002 | obmysql |
| my_pool_zone3 | unit1 | 5 | 5 | 8 | 8 | 1003 | zone3 | 172.20.249.51:2882 | 1002 | obmysql |
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
6 rows in set (0.047 sec)
```
除了调整租户资源单元对应的规格这种扩容方式外,还有一种扩容方式就是保留资源单元规格不变,但是增加每个 ZONE 里的资源单元数量。这个有个前提就是每个 ZONE 里至少有 2 个节点。
具体语法如下:
```sql
alter resource pool my_pool_zone1 unit_num = 2;
```
默认情况下,租户在每个 ZONE 里的资源单元数量是 1 ,这种租户架构也是 `1-1-1` 。这种租户也叫小租户。这种扩容命令执行时,会从集群其他节点里找到可用资源分配该规格的资源单元。
租户资源扩容的时候,很容易遇到资源不足的报错,信息如下:
```sql
MySQL [oceanbase]> alter resource pool my_pool_zone1 unit_num = 2;
ERROR 4624 (HY000): machine resource 'zone1' is not enough to hold a new unit
MySQL [oceanbase]>
```
为了避免遇到资源不足的错误,需要对集群可用资源进行准确统计。这就是前面常用的 SQL。为了确保可用资源统计准确,要求所有租户资源的 `min_cpu``max_cpu``min_memory``max_memory` 值保持一致。默认 SYS 租户的规格 `sys_unit_config` 的这个最小值和最大值经常不一样。需要手动调整一下。
```sql
alter resource unit sys_unit_config min_cpu=5,min_memory='2G';
select a.zone,concat(a.svr_ip,':',a.svr_port) observer, cpu_total, (cpu_total-cpu_assigned) cpu_free, round(mem_total/1024/1024/1024) mem_total_gb, round((mem_total-mem_assigned)/1024/1024/1024) mem_free_gb, usec_to_time(b.last_offline_time) last_offline_time, usec_to_time(b.start_service_time) start_service_time, b.status, usec_to_time(b.stop_time) stop_time
from __all_virtual_server_stat a join __all_server b on (a.svr_ip=b.svr_ip and a.svr_port=b.svr_port)
order by a.zone, a.svr_ip
;
输出:
+-------+--------------------+-----------+----------+--------------+-------------+----------------------------+----------------------------+--------+----------------------------+
| zone | observer | cpu_total | cpu_free | mem_total_gb | mem_free_gb | last_offline_time | start_service_time | status | stop_time |
+-------+--------------------+-----------+----------+--------------+-------------+----------------------------+----------------------------+--------+----------------------------+
| zone1 | 172.20.249.52:2882 | 14 | 1 | 13 | 1 | 1970-01-01 08:00:00.000000 | 2021-09-25 08:19:06.622351 | active | 1970-01-01 08:00:00.000000 |
| zone2 | 172.20.249.49:2882 | 14 | 4 | 13 | 3 | 1970-01-01 08:00:00.000000 | 2021-09-25 08:19:07.392669 | active | 1970-01-01 08:00:00.000000 |
| zone3 | 172.20.249.51:2882 | 14 | 4 | 13 | 3 | 1970-01-01 08:00:00.000000 | 2021-09-26 14:05:58.641570 | active | 1970-01-01 08:00:00.000000 |
+-------+--------------------+-----------+----------+--------------+-------------+----------------------------+----------------------------+--------+----------------------------+
3 rows in set (0.010 sec)
```
### 租户资源缩容
租户资源缩容就是调小租户的资源。具体方法跟扩容时命令一样。一种是调小资源规格,另外一种方法就是调小资源池对应的资源单元数量(`unit_num`)。
租户资源缩容需要考虑一种场景,当租户有大量增量数据在增量内存 MEMSTORE 中时,如果缩小租户资源的内存规格,可能会导致 MEMSTORE 内存水位变高触发转储或限流。为了避免这种情形,建议租户资源缩容之前,先对集群发起合并,降低 MEMSTORE 里已使用的资源。此外,租户资源内存缩容可以分多步缩容。
对租户资源池对于你给的资源单元数量缩容时,会面临一个选择,该释放那个资源单元呢。此时如果不指定 UNIT ID,会有 OceanBase 自动选择。租户缩容结束后,可能会出现集群资源的负载均衡。为了避免这种不必要的负载均衡,可以指定删除的资源单元(UNIT ID)
示例 SQL , `unit_num` 从 5 变为 4 :
```sql
ALTER RESOURCE POOL pool_total_zone1 unit_num = 4 DELETE unit = (1013 );
```
租户缩容资源,很可能会触发租户资源负载均衡操作,会触发数据迁移逻辑。建议关注数据迁移的进度和影响。
查看集群事件日志视图,可以观察到这个。
```sql
SELECT DATE_FORMAT(gmt_create, '%b%d %H:%i:%s') gmt_create_ , module, event, name1, value1, name2, value2, rs_svr_ip
FROM __all_rootservice_event_history
WHERE 1 = 1
AND gmt_create > SUBDATE(now(),interval 1 hour)
ORDER BY gmt_create DESC
LIMIT 20;
```
# 如何对 OceanBase 进行备份和恢复
## OceanBase 备份/恢复概述
## 逻辑备份和物理备份
OceanBase 的备份分逻辑备份和物理备份。
逻辑备份就是数据导出,数据库对象的定义和数据导出为 SQL 或 CSV 文件等。逻辑备份的优点就是功能灵活,可以指定租户名、库名、表名和对象类型等导出。针对表的逻辑备份甚至可以指定 QUERY SQL 带条件导出数据。缺点是只支持离线导出,不支持增量导出。逻辑备份的恢复就是数据导入。同样也很灵活。
物理备份是数据块备份。目前是按集群级别备份,集群里的所有租户都会被备份。备份的内容包含全量数据、增量数据和事务日志。理论上当拥有全量数据备份以及其后的所有事务日志备份,就可以还原出该数据库到历史任意时间点。
## 备份介质
OceanBase 开源版本 3.1.0 仅支持逻辑备份与恢复,3.1.1 支持物理备份与恢复。
OceanBase 的逻辑备份与恢复就是用工具 OBDUMPER 和 OBLOADER 进行数据导出和导入,具体操作请参考第4课《如何迁移MySQL 数据到 OB》。
OceanBase 的物理备份是将基线数据(`sstable`)和事务日志(`clog`)备份到一个共享目录里。该共享目录可以是 NFS 目录、阿里云的 OSS 存储、或 腾讯云的 COS 存储 。针对 AWS S3 存储的支持 OceanBase 还在排期开发。每个目录都要挂载到每个 OceanBase 节点的本地文件系统上,OceanBase 备份会自动选择从哪个节点备份数据到该节点的备份目录。在恢复的时候,目标 OceanBase 集群的每个节点上也需要挂载一个共享目录到本地文件系统,上面有全部的备份文件,这样就可以还原到历史任意时间点。后面这个共享目录可以跟备份是同一个共享目录,也可以不是同一个。后者通常用于将生产环境的备份还原到线下测试环境。
## 备份策略
OceanBase 的备份策略跟传统数据库备份一样,支持数据全量备份、数据增量备份和事务日志备份。在开启全量备份之前,需要先开启事务日志备份。OceanBase 的事务日志备份跟传统数据库的日志备份不完全一样,它是近实时备份的。平均每秒钟检查一下事务日志是否有新增,如果有就备份到备份目录里。
OceanBase 的备份跟传统数据库的备份还有个不一样的地方是全量备份的触发要求。OceanBase 的全量备份实际上是备份数据文件里的基线数据(`sstable`)。所以要求在全量备份之前,先对集群发起一次合并(`major freeze`)操作。集群合并结束后,数据文件生成新的基线版本,全量备份就直接备份这个版本的数据。由于 OceanBase 合并之后生成的基线版本是全局一致性的版本,所以 OceanBase 的全量备份也就很容易满足了全局一致性要求。此后,在 OceanBase 集群下一次合并之前,数据文件里的版本基本不变,再次做全量备份的意义就不大。所以 OceanBase 会要求每次全量备份之前都有新的基线版本生成,否则就会报错。
## 租户恢复
OceanBase 目前的物理备份是集群级别的,整个集群一起备份。虽然集群数据是三副本,备份只会备份一个副本的数据。OceanBase 的恢复是按租户进行恢复,如果租户架构也是三副本的,那么恢复的时候会自动恢复出三副本,并且有一个主副本和两个备副本。
## 如何配置备份恢复目录
如果备份介质是云厂商提供的对象存储,如阿里云的 OSS,腾讯云的 COS ,那就不需要特别配置了,可以直接在集群里配置备份目录。
```sql
alter system set backup_dest='oss://xxx.xxx.xxx.xxx/backup';
```
如果备份介质是 NAS 存储或者其他大容量的机器的本地盘,则需要配置 NFS 共享目录。
## NFS 服务端配置
+ 安装 NFS 工具包
```bash
sudo yum install nfs-utils
```
+ 设置 NFS 目录
```bash
mkdir -p /data/backup
```
将该目录设置 NFS 共享目录。修改配置文件 `/etc/exports`
```bash
vim /etc/exports
/data/backup/ 172.20.0.0/16(rw,sync,all_squash)
```
备注:
+ `172.20.0.0/16` 是设置允许访问这个 NFS 目录的客户端 IP 网段。尽量只包含 OceanBase 集群相关节点服务器的 IP 的网段。
+ `rw`:允许客户端读写这个目录。
+ `sync`:同步写模式。
+ 将客户端的所有 UIDs 和 GIDs 映射到 NFSS SERVER 端的匿名用户。
+ 设置 NFS 目录权限
```bash
# centos7
sudo chown nfsnobody:nfsnobody -R /data/backup
# centos8 去掉了 nfsnobody
sudo chown nobody:nobody -R /data/backup
```
+ 重启 NFS 服务
```bash
[admin@obce00 ~]$ sudo systemctl restart nfs-server
[admin@obce00 ~]$ sudo systemctl status nfs-server
● nfs-server.service - NFS server and services
Loaded: loaded (/usr/lib/systemd/system/nfs-server.service; enabled; vendor preset: disabled)
Drop-In: /run/systemd/generator/nfs-server.service.d
└─order-with-mounts.conf
Active: active (exited) since Sat 2021-10-23 14:19:25 CST; 9min ago
Process: 1241106 ExecStopPost=/usr/sbin/exportfs -f (code=exited, status=0/SUCCESS)
Process: 1241104 ExecStopPost=/usr/sbin/exportfs -au (code=exited, status=0/SUCCESS)
Process: 1241102 ExecStop=/usr/sbin/rpc.nfsd 0 (code=exited, status=0/SUCCESS)
Process: 1241129 ExecStart=/bin/sh -c if systemctl -q is-active gssproxy; then systemctl reload gssproxy ; fi (code=exited, status=0/SUCCESS)
Process: 1241117 ExecStart=/usr/sbin/rpc.nfsd (code=exited, status=0/SUCCESS)
Process: 1241114 ExecStartPre=/usr/sbin/exportfs -r (code=exited, status=0/SUCCESS)
Main PID: 1241129 (code=exited, status=0/SUCCESS)
Tasks: 0 (limit: 195588)
Memory: 0B
CGroup: /system.slice/nfs-server.service
Oct 23 14:19:25 obce00 systemd[1]: Starting NFS server and services...
Oct 23 14:19:25 obce00 systemd[1]: Started NFS server and services.
[admin@obce00 ~]$ sudo showmount -e
Export list for obce00:
/data/backup 172.20.0.0/16
# centos 8
[admin@obce00 ~]$ sudo exportfs -arv
exporting 172.20.0.0/16:/data/backup
[admin@obce00 ~]$ sudo exportfs -s
/data/backup 172.20.0.0/16(sync,wdelay,hide,no_subtree_check,sec=sys,rw,secure,root_squash,all_squash)
```
## NFS 客户端配置
所有 OceanBase 集群节点都要部署 NFS 客户端,将 NFS SERVER 的共享目录挂载到本机。
客户端也要先安装 NFS 软件。
+ 安装 NFS 工具包
```bash
sudo yum install nfs-utils
```
+ 修改内核参数
```bash
sudo vim /etc/sysctl.conf +R
sunrpc.tcp_max_slot_table_entries=128
```
+ 挂载目录
```bash
sudo mount -tnfs4 -o rw,timeo=30,wsize=1048576,rsize=1048576,namlen=512,sync,lookupcache=positive 172.20.249.54:/data/backup /backup
```
查看结果
```bash
[admin@obce01 ~]$ mount |grep backup
172.20.249.54:/data/backup on /backup type nfs4 (rw,relatime,sync,vers=4.2,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=30,retrans=2,sec=sys,clientaddr=172.20.249.53,lookupcache=pos,local_lock=none,addr=172.20.249.54)
[admin@obce01 ~]$
```
注意,手动挂载的目录,在机器重启后需要重新挂载。也可以在文件 `/etc/fstab` 目录里配置。
## 如何发起 OceanBase 集群备份
OceanBase 的集群备份过程包含以下几步:
+ 发起 MINOR FREEZE
+ 开启事务日志备份。
+ 发起 MAJOR FREEZE
+ 发起全量数据备份。
## (可选)配置事务日志备份
如果备份目标是能恢复到历史任意时间点,则必须配置事务日志备份。事务日志备份支持两种模式:`Optional``Mandatory`
+ `Optional` 模式:表示业务优先。当事务日志备份来不及情况下,日志可能来不及备份就被回收了,可能会发生事务日志备份断流。这是默认行为。
+ `Mandatory` 模式:表示备份优先。在该模式下,如果事务日志备份速度跟不上用户事务日志的写入速度,可能会导致事务提交变慢甚至报错。
通常如果事务日志盘目录空间满足要求(OBSERVER 节点内存的 3~4 倍),并且备份存储性能不是太差,出现这种备份事务日志赶不上的概率很低。
事务日志压缩目前支持的压缩算法有:`zstd_1.3.8``lz4_1.0`,默认使用 压缩算法 `lz4_1.0` 。日志压缩需要显示开启。
```sql
ALTER SYSTEM SET backup_log_archive_option = 'optional compression= enable';
```
设置后,也可以修改备份模式和压缩算法。
```sql
ALTER SYSTEM SET backup_log_archive_option='mandatory compression= zstd_1.3.8';
```
也可以关闭相应备份模式下的事务日志压缩。虽然默认备份模式是 `optional` ,建议修改时都带上实际的备份模式。
```sql
ALTER SYSTEM SET backup_log_archive_option = 'optional compression= disable';
ALTER SYSTEM SET backup_log_archive_option = 'mandatory compression= disable';
```
## 发起 MINOR FREEZE
事务日志开始备份时,并不是从当前的事务日志开始备份,而是从上一次 `minor freeze` 开始。注意上次合并点时默认也会有一个 `minor freeze`
所以,为了降低事务日志开始备份的时间,先发起一个 `minor freeze`
```sql
alter system minor freeze ;
```
`minor freeze` 命令会瞬时返回,很快会有转储操作。转储的时间根据当前内存中增量数据多少而定,通常在几秒到几十秒。转储操作对租户读写性能的影响很低。
## 发起 ARCHIVELOG
事务日志的备份是通过命令 `archihvelog` 触发的。`archivelog` 启动后,集群的事务日志定期备份到指定的备份目录。触发的时间间隔是由参数 `log_archive_checkpoint_interval` 指定,范围 [5s, 1h] ,默认时间是 120s 。以当前版本(3.1.1)的能力,这个参数默认值不建议调小。因为事务日志的备份是以分区或者分区组为单位进行的,当租户的分区数非常多的时候,备份并发的小 IO 会比较多,短期内的 IOPS 可能到达事务日志盘的瓶颈。
```sql
alter system archivelog;
```
事务日志备份开始后, 可以查看备份进度确认备份任务开始。
```sql
MySQL [oceanbase]> select incarnation, tenant_id, status, min_first_time, max_next_time, now() from CDB_OB_BACKUP_ARCHIVELOG_SUMMARY;
+-------------+-----------+--------+----------------------------+----------------------------+---------------------+
| incarnation | tenant_id | status | min_first_time | max_next_time | now() |
+-------------+-----------+--------+----------------------------+----------------------------+---------------------+
| 1 | 1 | DOING | 2021-10-20 17:49:15.079198 | 2021-10-24 16:18:24.531211 | 2021-10-24 16:20:17 |
| 1 | 1001 | DOING | 2021-10-20 17:49:15.079198 | 2021-10-24 16:18:24.531211 | 2021-10-24 16:20:17 |
+-------------+-----------+--------+----------------------------+----------------------------+---------------------+
2 rows in set (0.589 sec)
```
事务日志备份是按租户分开进行的。列 `status``DOING` 时,表示事务日志在进行。`max_next_time` 表示最近的备份时间。从时间可以看出基本上是 2 分钟触发一次。
## 发起 MAJOR FREEZE
默认情况下,OceanBase 增量数据都在内存里。当触发转储的时候,内存中的增量数据直接以 SSTable 格式写到磁盘,但并没有跟磁盘上基线数据进行合并。所以在上一次合并结束后,基线数据的内容是没有变化的。
OceanBase 的数据备份就是备份磁盘上数据文件里的基线数据。如果这个基线数据的时间是在事务日志开始备份的时间之前,那这个基线数据即使备份成功了也没有意义。如果上次全量备份成功之后基线数据的内容还是没有变化,那再次全量备份也没有意义。
所以,在开启全量数据备份之前要发起合并操作,并且该合并操作是在事务日志备份开启成功后发起。否则,全量备份会收到相应报错。
```sql
alter system major freeze;
```
合并的时间取决于增量数据的多少,通常要几分钟到几十分钟。可以通过下面 SQL 观察合并进度。
```sql
SELECT ZONE,svr_ip,major_version,ss_store_count,merged_ss_store_count,modified_ss_store_count,merge_start_time,merge_finish_time,merge_process
FROM __all_virtual_partition_sstable_image_info
order by major_version desc , zone ;
```
如果列 `merge_finish_time` 有值了,表示合并结束。也可以通过下面视图确认合并结束。
```sql
SELECT * FROM __all_zone WHERE name='merge_status';
+----------------------------+----------------------------+-------+--------------+-------+------+
| gmt_create | gmt_modified | zone | name | value | info |
+----------------------------+----------------------------+-------+--------------+-------+------+
| 2021-10-20 15:21:20.765982 | 2021-10-24 16:45:57.990705 | | merge_status | 0 | IDLE |
| 2021-10-20 15:21:20.767009 | 2021-10-24 16:44:55.001369 | zone1 | merge_status | 0 | IDLE |
| 2021-10-20 15:21:20.767009 | 2021-10-24 16:45:47.005686 | zone2 | merge_status | 0 | IDLE |
| 2021-10-20 15:21:20.769822 | 2021-10-24 16:44:31.857504 | zone3 | merge_status | 0 | IDLE |
+----------------------------+----------------------------+-------+--------------+-------+------+
4 rows in set (0.734 sec)
```
当列 `info` 的值都是 `IDLE` 时,表示该 ZONE 合并完成。
## 发起全量备份
发起全量备份之前,可以设置备份密码。这一步是可选的。设置密码是会话级别设置,不设置就是空。
```sql
MySQL [oceanbase]> set encryption on identified by 'bakBAK123456' only;
Query OK, 0 rows affected (0.225 sec)
```
全量备份发起命令是 `alter system backup database` 。如果事务日志备份开始了,那必须等事务日志开始备份成功。否则命令会报错。
```sql
alter system backup database;
MySQL [oceanbase]> alter system backup database;
ERROR 9040 (HY000): backup can not start, because log archive is not doing. log archive status : BEGINNING.
```
此外,重新开始全量备份之前距离事务日志备份开始之后需要有一次合并,否则命令也会报错。
```sql
MySQL [oceanbase]> alter system backup database;
ERROR 9040 (HY000): backup can not start, because log archive start timestamp is bigger than frozen timestamp, need major freeze first. start timestamp : 1635069322951942, frozen timestamp : 1635064748441948 .
MySQL [oceanbase]>
```
查看全量备份任务先看视图 `CDB_OB_BACKUP_PROGRESS`
```sql
SELECT incarnation, tenant_id, backup_type, bs_key, partition_count, start_time, completion_time, status
FROM CDB_OB_BACKUP_PROGRESS;
+-------------+-----------+-------------+--------+-----------------+----------------------------+----------------------------+---------+
| incarnation | tenant_id | backup_type | bs_key | partition_count | start_time | completion_time | status |
+-------------+-----------+-------------+--------+-----------------+----------------------------+----------------------------+---------+
| 1 | 1 | D | 6 | 0 | 2021-10-24 16:56:02.682700 | 2021-10-24 16:56:14.035231 | RUNNING |
| 1 | 1001 | D | 6 | 220 | 2021-10-24 16:56:02.682700 | 2021-10-24 16:56:12.510089 | RUNNING |
+-------------+-----------+-------------+--------+-----------------+----------------------------+----------------------------+---------+
2 rows in set (0.456 sec)
```
如果这个视图有记录,表示有全量备份任务在进行。如果视图为空,则表示没有全量备份任务或者全量备份任务已经结束。
可以查看备份历史记录视图 `CDB_OB_BACKUP_SET_DETAILS`
```sql
select incarnation, tenant_id, bs_key , backup_type, encryption_mode, start_time, completion_time, elapsed_secondes , keep, output_bytes_display, output_rate_bytes_display, status
from CDB_OB_BACKUP_SET_DETAILS
order by start_time desc limit 10;
+-------------+-----------+--------+-------------+-----------------+----------------------------+----------------------------+------------------+------+----------------------+---------------------------+-----------+
| incarnation | tenant_id | bs_key | backup_type | encryption_mode | start_time | completion_time | elapsed_secondes | keep | output_bytes_display | output_rate_bytes_display | status |
+-------------+-----------+--------+-------------+-----------------+----------------------------+----------------------------+------------------+------+----------------------+---------------------------+-----------+
| 1 | 1 | 6 | D | PASSWORD | 2021-10-24 16:56:02.682700 | 2021-10-24 16:58:36.480644 | 154 | NO | 7.17GB | 47.71MB/S | COMPLETED |
| 1 | 1001 | 6 | D | PASSWORD | 2021-10-24 16:56:02.682700 | 2021-10-24 16:58:23.446364 | 141 | NO | 7.17GB | 52.13MB/S | COMPLETED |
| 1 | 1001 | 5 | D | NONE | 2021-10-21 08:46:28.618568 | 2021-10-21 08:47:53.129814 | 85 | NO | 3.61GB | 43.70MB/S | COMPLETED |
| 1 | 1 | 5 | D | NONE | 2021-10-21 08:46:28.618568 | 2021-10-21 08:47:56.047487 | 87 | NO | 3.61GB | 42.24MB/S | COMPLETED |
| 1 | 1001 | 4 | D | NONE | 2021-10-20 20:26:46.729579 | 2021-10-20 20:27:38.728365 | 52 | NO | 1.42GB | 27.92MB/S | COMPLETED |
| 1 | 1 | 4 | D | NONE | 2021-10-20 20:26:46.729579 | 2021-10-20 20:27:41.517514 | 55 | NO | 1.42GB | 26.50MB/S | COMPLETED |
| 1 | 1 | 3 | D | NONE | 2021-10-20 19:48:24.574229 | 2021-10-20 19:49:27.693007 | 63 | NO | 1.52GB | 24.63MB/S | COMPLETED |
| 1 | 1001 | 3 | D | NONE | 2021-10-20 19:48:24.574229 | 2021-10-20 19:49:26.604282 | 62 | NO | 1.52GB | 25.07MB/S | COMPLETED |
| 1 | 1 | 2 | D | PASSWORD | 2021-10-20 18:14:42.848095 | 2021-10-20 18:15:39.021754 | 56 | NO | 1.11GB | 20.22MB/S | COMPLETED |
| 1 | 1001 | 2 | D | PASSWORD | 2021-10-20 18:14:42.848095 | 2021-10-20 18:15:38.164658 | 55 | NO | 1.11GB | 20.54MB/S | COMPLETED |
+-------------+-----------+--------+-------------+-----------------+----------------------------+----------------------------+------------------+------+----------------------+---------------------------+-----------+
10 rows in set (0.015 sec)
```
## (可选) 停止全量备份和事务日志备份
如果全量备份在进行中,想要取消备份的时候,可以通过命令 `alter system cancel backup` 取消。如果全量备份已经结束或者没有全量备份任务时,这个命令会报错。
```sql
ALTER SYSTEM CANCEL BACKUP;
ERROR 9049 (HY000): backup status is stopped, can not cancel
```
停止事务日志备份任务,可以通过命令 `alter system noarchivelog` 。如果事务日志备份已经停止,这个命令会报错。
```sql
MySQL [oceanbase]> ALTER SYSTEM NOARCHIVELOG;
Query OK, 0 rows affected (0.552 sec)
MySQL [oceanbase]> ALTER SYSTEM NOARCHIVELOG;
ERROR 9024 (HY000): log archive backup is already disabled
```
## 设置备份清理策略
默认情况下,OceanBase 的备份会一直保存。可以通过参数 `backup_recovery_window` 设置。参数默认值是 0 表示备份永不过期,可以设置为 `7d` 等。
```sql
MySQL [oceanbase]> alter system set backup_recovery_window='2d';
Query OK, 0 rows affected (0.442 sec)
```
仅设置参数 `backup_recovery_window` 还不会自动清理备份。还需要设置参数 `auto_delete_expired_backup` 值为 `true`
```sql
MySQL [oceanbase]> alter system set auto_delete_expired_backup=true;
Query OK, 0 rows affected (0.071 sec)
```
自动删除备份会在参数变更后异步调用。查看备份历史可以看到很多备份集在自动删除中。
```sql
select incarnation, tenant_id, bs_key , backup_type, encryption_mode, start_time, completion_time, elapsed_secondes , keep, output_bytes_display, output_rate_bytes_display, status
from CDB_OB_BACKUP_SET_DETAILS
order by start_time desc ;
+-------------+-----------+--------+-------------+-----------------+----------------------------+----------------------------+------------------+------+----------------------+---------------------------+-----------+
| incarnation | tenant_id | bs_key | backup_type | encryption_mode | start_time | completion_time | elapsed_secondes | keep | output_bytes_display | output_rate_bytes_display | status |
+-------------+-----------+--------+-------------+-----------------+----------------------------+----------------------------+------------------+------+----------------------+---------------------------+-----------+
| 1 | 1 | 7 | D | PASSWORD | 2021-10-24 19:05:29.023969 | 2021-10-24 19:07:39.488182 | 130 | NO | 7.29GB | 57.23MB/S | COMPLETED |
| 1 | 1001 | 7 | D | PASSWORD | 2021-10-24 19:05:29.023969 | 2021-10-24 19:07:33.432748 | 124 | NO | 7.29GB | 60.02MB/S | COMPLETED |
| 1 | 1 | 6 | D | PASSWORD | 2021-10-24 16:56:02.682700 | 2021-10-24 16:58:36.480644 | 154 | NO | 7.17GB | 47.71MB/S | COMPLETED |
| 1 | 1001 | 6 | D | PASSWORD | 2021-10-24 16:56:02.682700 | 2021-10-24 16:58:23.446364 | 141 | NO | 7.17GB | 52.13MB/S | COMPLETED |
| 1 | 1 | 5 | D | NONE | 2021-10-21 08:46:28.618568 | 2021-10-21 08:47:56.047487 | 87 | NO | 3.61GB | 42.24MB/S | COMPLETED |
| 1 | 1001 | 5 | D | NONE | 2021-10-21 08:46:28.618568 | 2021-10-21 08:47:53.129814 | 85 | NO | 3.61GB | 43.70MB/S | COMPLETED |
| 1 | 1 | 4 | D | NONE | 2021-10-20 20:26:46.729579 | 2021-10-20 20:27:41.517514 | 55 | NO | 1.42GB | 26.50MB/S | DELETING |
| 1 | 1001 | 4 | D | NONE | 2021-10-20 20:26:46.729579 | 2021-10-20 20:27:38.728365 | 52 | NO | 1.42GB | 27.92MB/S | DELETING |
| 1 | 1 | 3 | D | NONE | 2021-10-20 19:48:24.574229 | 2021-10-20 19:49:27.693007 | 63 | NO | 1.52GB | 24.63MB/S | DELETING |
| 1 | 1001 | 3 | D | NONE | 2021-10-20 19:48:24.574229 | 2021-10-20 19:49:26.604282 | 62 | NO | 1.52GB | 25.07MB/S | DELETING |
| 1 | 1 | 2 | D | PASSWORD | 2021-10-20 18:14:42.848095 | 2021-10-20 18:15:39.021754 | 56 | NO | 1.11GB | 20.22MB/S | DELETING |
| 1 | 1001 | 2 | D | PASSWORD | 2021-10-20 18:14:42.848095 | 2021-10-20 18:15:38.164658 | 55 | NO | 1.11GB | 20.54MB/S | DELETING |
+-------------+-----------+--------+-------------+-----------------+----------------------------+----------------------------+------------------+------+----------------------+---------------------------+-----------+
12 rows in set (0.155 sec)
```
关于自动删除备份具体逻辑还比较复杂,详情可以查看社区版官网文档。
如果自动删除过期备份没有起作用,可以手动删除备份,命令是:`ALTER SYSTEM DELETE BACKUPSET [备份集ID]` 。其中 备份集ID 是从视图 `CDB_OB_BACKUP_SET_DETAILS` 的列 `BS_KEY` 获取的。重复删除同一个备份集会提示删除中。
```sql
MySQL [oceanbase]> ALTER SYSTEM DELETE BACKUPSET 1;
Query OK, 0 rows affected (0.759 sec)
MySQL [oceanbase]> ALTER SYSTEM DELETE BACKUPSET 1;
ERROR 9044 (HY000): delete backup data is in progress
```
查看视图 `CDB_OB_BACKUP_TASK_CLEAN_HISTORY` 可以获取备份删除历史。
```sql
select incarnation, tenant_id, bs_key, backup_type, partition_count, start_time, completion_time, status
from CDB_OB_BACKUP_TASK_CLEAN_HISTORY;
+-------------+-----------+--------+-------------+-----------------+----------------------------+----------------------------+--------+
| incarnation | tenant_id | bs_key | backup_type | partition_count | start_time | completion_time | status |
+-------------+-----------+--------+-------------+-----------------+----------------------------+----------------------------+--------+
| 1 | 1 | 1 | D | 218 | 2021-10-20 17:58:42.301646 | 2021-10-20 17:59:36.557908 | FINISH |
| 1 | 1 | 2 | D | 218 | 2021-10-20 18:14:42.848095 | 2021-10-20 18:15:39.021754 | FINISH |
| 1 | 1 | 3 | D | 218 | 2021-10-20 19:48:24.574229 | 2021-10-20 19:49:27.693007 | FINISH |
| 1 | 1 | 4 | D | 218 | 2021-10-20 20:26:46.729579 | 2021-10-20 20:27:41.517514 | FINISH |
| 1 | 1001 | 1 | D | 218 | 2021-10-20 17:58:42.301646 | 2021-10-20 17:59:35.724704 | FINISH |
| 1 | 1001 | 2 | D | 218 | 2021-10-20 18:14:42.848095 | 2021-10-20 18:15:38.164658 | FINISH |
| 1 | 1001 | 3 | D | 218 | 2021-10-20 19:48:24.574229 | 2021-10-20 19:49:26.604282 | FINISH |
| 1 | 1001 | 4 | D | 218 | 2021-10-20 20:26:46.729579 | 2021-10-20 20:27:38.728365 | FINISH |
+-------------+-----------+--------+-------------+-----------------+----------------------------+----------------------------+--------+
8 rows in set (0.003 sec)
```
当备份集很大的时候,备份的清理会需要一些时间。如果要取消正在删除中的备份,可以使用命令 `ALTER SYSTEM CANCEL DELETE BACKUP;` 取消正在执行的清理任务。已经删除的备份文件不会恢复出来。 同时还要关闭自动清理备份机制。
```sql
ALTER SYSTEM SET auto_delete_expired_backup = 'False';
ALTER SYSTEM CANCEL DELETE BACKUP;
```
## 如何发起 OceanBase 租户恢复
OceanBase 的恢复是按租户恢复,且只能恢复到一个空租户环境。
这里我模拟租户被删除故障,执行租户恢复过程。
```sql
MySQL [oceanbase]> select now();
+---------------------+
| now() |
+---------------------+
| 2021-10-24 22:55:20 |
+---------------------+
1 row in set (0.002 sec)
MySQL [oceanbase]> drop tenant obmysql force;
Query OK, 0 rows affected (0.110 sec)
MySQL [oceanbase]> drop resource pool pool_mysql;
```
## 准备空租户的资源池
准备租户首先是创建资源池开始。
```sql
create resource pool my_pool unit='my_unit_config',unit_num=2;
```
## 查看备份集
查看当前有效的备份集。
```sql
select incarnation, tenant_id, bs_key , backup_type, encryption_mode, start_time, completion_time, elapsed_secondes , keep, output_bytes_display, output_rate_bytes_display, status
from CDB_OB_BACKUP_SET_DETAILS
order by start_time desc ;
+-------------+-----------+--------+-------------+-----------------+----------------------------+----------------------------+------------------+------+----------------------+---------------------------+-----------+
| incarnation | tenant_id | bs_key | backup_type | encryption_mode | start_time | completion_time | elapsed_secondes | keep | output_bytes_display | output_rate_bytes_display | status |
+-------------+-----------+--------+-------------+-----------------+----------------------------+----------------------------+------------------+------+----------------------+---------------------------+-----------+
| 1 | 1 | 7 | D | PASSWORD | 2021-10-24 19:05:29.023969 | 2021-10-24 19:07:39.488182 | 130 | NO | 7.29GB | 57.23MB/S | COMPLETED |
| 1 | 1001 | 7 | D | PASSWORD | 2021-10-24 19:05:29.023969 | 2021-10-24 19:07:33.432748 | 124 | NO | 7.29GB | 60.02MB/S | COMPLETED |
| 1 | 1 | 6 | D | PASSWORD | 2021-10-24 16:56:02.682700 | 2021-10-24 16:58:36.480644 | 154 | NO | 7.17GB | 47.71MB/S | COMPLETED |
| 1 | 1001 | 6 | D | PASSWORD | 2021-10-24 16:56:02.682700 | 2021-10-24 16:58:23.446364 | 141 | NO | 7.17GB | 52.13MB/S | COMPLETED |
| 1 | 1 | 5 | D | NONE | 2021-10-21 08:46:28.618568 | 2021-10-21 08:47:56.047487 | 87 | NO | 3.61GB | 42.24MB/S | COMPLETED |
| 1 | 1001 | 5 | D | NONE | 2021-10-21 08:46:28.618568 | 2021-10-21 08:47:53.129814 | 85 | NO | 3.61GB | 43.70MB/S | COMPLETED |
+-------------+-----------+--------+-------------+-----------------+----------------------------+----------------------------+------------------+------+----------------------+---------------------------+-----------+
6 rows in set (0.008 sec)
```
这里有3个备份集,其中 5 是没有加密的,6 和 7 是加密的。假设我使用备份集 7 来恢复。那么需要在会话级别先设置备份集的解密密码。
```sql
MySQL [oceanbase]> SET DECRYPTION IDENTIFIED BY 'bakBAK123456';
Query OK, 0 rows affected (0.001 sec)
```
## 打开租户恢复参数
参数 `restore_concurrency` 指定了恢复线程的并发数,默认是 0, 不恢复。需要修改为大于 0 的值。
```sql
MySQL [oceanbase]> alter system set restore_concurrency = 8;
Query OK, 0 rows affected (0.130 sec)
```
通常开启恢复命令后默认还会等待一段时间才开始恢复,整个恢复期间会有三次等待。每次等待时间是由内部参数 `_restore_idle_time` 设置,默认值是 60s 。注意,隐含参数未来版本可能会发生变化。在生产环境不建议去调整这个参数。在测试的时候,如果追求恢复调度时间尽可能的短,可以缩小这个时间到 10s 等。
```sql
MySQL [oceanbase]> ALTER SYSTEM SET _restore_idle_time = '10s';
Query OK, 0 rows affected (0.017 sec)
```
## 开始恢复
恢复命令稍微比较复杂, 是 `ALTER SYSTEM RESTORE <dest_tenant_name> FROM <source_tenan_tname> at 'uri' UNTIL 'timestamp' WITH 'restore_option'; `
| 参数 | 描述 |
|--------------------|---------------------------------------------------------------------------------------------------------------------------|
| dest_tenant_name | 指恢复的新租户的名称。 |
| source_tenant_name | 指原集群的租户。 |
| uri | 指备份时设置的 backup_dest。 |
| timestamp | 指恢复的时间戳,需要大于等于最早备份的数据备份的 CDB_OB_BACKUP_SET_DETAILS 的 START_TIME,小于等于日志备份 CDB_OB_BACKUP_ARCHIVELOG_SUMMARY 的MAX_NEXT_TIME。 |
| restore_option | 支持 backup_cluster_name、backup_cluster_id、pool_list、locality、kms_encrypt: |
| | backup_cluster_name 为必选项,填写源集群的名称。 |
| | backup_cluster_id 为必选项,填写源集群的 cluster_id |
| | pool_list为必选项,填写用户的资源池。 |
| | locality 为可选项,填写租户的 Locality 信息。 |
| | kms_encrypt为可选项,为 true 则表示在恢复时需要使用步骤 5 指定的 kms_encrypt_info。 |
```sql
MySQL [oceanbase]> ALTER SYSTEM RESTORE obmysql2 FROM obmysql at 'file:///backup' UNTIL '2021-10-24 19:07:39.488' WITH 'backup_cluster_name=obce-3zones&backup_cluster_id=2&pool_list=my_pool';
Query OK, 0 rows affected (0.015 sec)
MySQL [oceanbase]> select * from __all_tenant;
+----------------------------+----------------------------+-----------+-------------+-------------+-------------------+-------------------+--------+----------------+---------------+-----------+-----------------------+------------------+
| gmt_create | gmt_modified | tenant_id | tenant_name | replica_num | zone_list | primary_zone | locked | collation_type | info | read_only | rewrite_merge_version | locality |
+----------------------------+----------------------------+-----------+-------------+-------------+-------------------+-------------------+--------+----------------+---------------+-----------+-----------------------+------------------+
| 2021-10-20 15:21:20.569347 | 2021-10-20 15:21:20.569347 | 1 | sys | -1 | zone1;zone2;zone3 | zone1;zone2,zone3 | 0 | 0 | system tenant | 0 | 0 | FULL{1}@zone1, FU|
| 2021-10-25 00:08:10.322557 | 2021-10-25 00:08:10.322557 | 1005 | obmysql2 | -1 | zone1;zone2;zone3 | RANDOM | 0 | 0 | | 0 | 0 | FULL{1}@zone1, FU|
+----------------------------+----------------------------+-----------+-------------+-------------+-------------------+-------------------+--------+----------------+---------------+-----------+-----------------------+------------------+
2 rows in set (0.005 sec)
```
## 查看恢复进度和结果
+ 查看集群事件日志
```sql
SELECT DATE_FORMAT(gmt_create, '%b%d %H:%i:%s') gmt_create_ , module, event, name1, value1, name2, value2, rs_svr_ip,name3,value3,name4,value4
FROM __all_rootservice_event_history
WHERE 1 = 1
AND module IN ('physical_restore','leader_coordinator')
ORDER BY gmt_create DESC
limit 30;
+----------------+--------------------+-----------------------+------------+----------------------+-------------+----------+---------------+---------------+------------------------+--------------+--------+
| gmt_create_ | module | event | name1 | value1 | name2 | value2 | rs_svr_ip | name3 | value3 | name4 | value4 |
+----------------+--------------------+-----------------------+------------+----------------------+-------------+----------+---------------+---------------+------------------------+--------------+--------+
| Oct25 00:12:23 | leader_coordinator | switch_leader | current_rs | "172.20.249.53:2882" | tenant_id | 1005 | 172.20.249.53 | tablegroup_id | 1105558941729769 | partition_id | 7 |
| Oct25 00:12:23 | leader_coordinator | switch_leader | current_rs | "172.20.249.53:2882" | tenant_id | 1005 | 172.20.249.53 | tablegroup_id | 1105009185964893 | partition_id | 0 |
| Oct25 00:12:23 | leader_coordinator | switch_leader | current_rs | "172.20.249.53:2882" | tenant_id | 1005 | 172.20.249.53 | tablegroup_id | 1105558941729769 | partition_id | 6 |
| Oct25 00:12:23 | leader_coordinator | switch_leader | current_rs | "172.20.249.53:2882" | tenant_id | 1005 | 172.20.249.53 | tablegroup_id | 1105558941729769 | partition_id | 5 |
| Oct25 00:12:23 | leader_coordinator | switch_leader | current_rs | "172.20.249.53:2882" | tenant_id | 1005 | 172.20.249.53 | tablegroup_id | 1105558941729769 | partition_id | 4 |
| Oct25 00:12:23 | leader_coordinator | switch_leader | current_rs | "172.20.249.53:2882" | tenant_id | 1005 | 172.20.249.53 | tablegroup_id | 1105558941729769 | partition_id | 3 |
| Oct25 00:12:23 | leader_coordinator | switch_leader | current_rs | "172.20.249.53:2882" | tenant_id | 1005 | 172.20.249.53 | tablegroup_id | 1105558941729769 | partition_id | 2 |
| Oct25 00:12:23 | leader_coordinator | switch_leader | current_rs | "172.20.249.53:2882" | tenant_id | 1005 | 172.20.249.53 | tablegroup_id | 1105009185964881 | partition_id | 0 |
| Oct25 00:12:23 | leader_coordinator | switch_leader | current_rs | "172.20.249.53:2882" | tenant_id | 1005 | 172.20.249.53 | tablegroup_id | 1105558941729769 | partition_id | 1 |
| Oct25 00:12:23 | leader_coordinator | switch_leader | current_rs | "172.20.249.53:2882" | tenant_id | 1005 | 172.20.249.53 | tablegroup_id | 1105558941729769 | partition_id | 0 |
| Oct25 00:12:16 | physical_restore | restore_success | tenant | obmysql2 | | | 172.20.249.53 | | | | |
| Oct25 00:12:16 | physical_restore | change_restore_status | job_id | 10 | tenant | obmysql2 | 172.20.249.53 | status | RESTORE_SUCCESS | | |
| Oct25 00:12:16 | physical_restore | change_restore_status | job_id | 10 | tenant | obmysql2 | 172.20.249.53 | status | POST_CHECK | | |
| Oct25 00:12:16 | physical_restore | change_restore_status | job_id | 10 | tenant | obmysql2 | 172.20.249.53 | status | REBUILD_INDEX | | |
| Oct25 00:09:45 | leader_coordinator | switch_leader | current_rs | "172.20.249.53:2882" | tenant_id | 1005 | 172.20.249.53 | tablegroup_id | 1105009185914881 | partition_id | 0 |
| Oct25 00:09:34 | physical_restore | change_restore_status | job_id | 10 | tenant | obmysql2 | 172.20.249.53 | status | RESTORE_USER_REPLICA | | |
| Oct25 00:09:33 | physical_restore | change_restore_status | job_id | 10 | tenant | obmysql2 | 172.20.249.53 | status | CREATE_USER_PARTITIONS | | |
| Oct25 00:09:27 | physical_restore | change_restore_status | job_id | 10 | tenant | obmysql2 | 172.20.249.53 | status | MODIFY_SCHEMA | | |
| Oct25 00:09:27 | physical_restore | change_restore_status | job_id | 10 | tenant | obmysql2 | 172.20.249.53 | status | UPGRADE_POST | | |
| Oct25 00:09:27 | physical_restore | change_restore_status | job_id | 10 | tenant | obmysql2 | 172.20.249.53 | status | UPGRADE_PRE | | |
| Oct25 00:09:12 | leader_coordinator | switch_leader | current_rs | "172.20.249.53:2882" | tenant_id | 1005 | 172.20.249.53 | tablegroup_id | 1105009185914881 | partition_id | 0 |
| Oct25 00:08:10 | physical_restore | change_restore_status | job_id | 10 | tenant | obmysql2 | 172.20.249.53 | status | RESTORE_SYS_REPLICA | | |
| Oct25 00:08:10 | physical_restore | change_restore_status | job_id | 10 | tenant_name | obmysql2 | 172.20.249.53 | status | CREATE_TENANT | | |
| Oct25 00:08:10 | physical_restore | restore_start | ret | 0 | tenant_name | obmysql2 | 172.20.249.53 | | | | |
| Oct25 00:05:56 | physical_restore | restore_failed | tenant | obmysql2 | | | 172.20.249.53 | | | | |
| Oct25 00:05:56 | physical_restore | change_restore_status | job_id | 9 | tenant | obmysql2 | 172.20.249.53 | status | RESTORE_FAIL | | |
| Oct25 00:05:56 | physical_restore | change_restore_status | job_id | 9 | tenant_name | obmysql2 | 172.20.249.53 | status | CREATE_TENANT | | |
| Oct25 00:05:56 | physical_restore | restore_start | ret | 0 | tenant_name | obmysql2 | 172.20.249.53 | | | | |
| Oct25 00:05:10 | leader_coordinator | switch_leader | current_rs | "172.20.249.53:2882" | tenant_id | 1004 | 172.20.249.53 | tablegroup_id | 1104459430101993 | partition_id | 7 |
| Oct25 00:05:10 | leader_coordinator | switch_leader | current_rs | "172.20.249.53:2882" | tenant_id | 1004 | 172.20.249.53 | tablegroup_id | 1104459430101993 | partition_id | 6 |
+----------------+--------------------+-----------------------+------------+----------------------+-------------+----------+---------------+---------------+------------------------+--------------+--------+
30 rows in set (0.083 sec)
```
+ 查看恢复进度看视图 `__all_restore_info`
```sql
obclient> SELECT * FROM __all_restore_info;
```
+ 查看恢复历史 `__all_restore_history`
```sql
MySQL [oceanbase]> SELECT * FROM __all_restore_history order by gmt_create desc limit 1;
+----------------------------+----------------------------+--------+-----------------+-----------+-------------+-----------------+----------------------------+----------------------------+----------+-----------------+-----------------++
| gmt_create | gmt_modified | job_id | external_job_id | tenant_id | tenant_name | status | start_time | completion_time | pg_count | finish_pg_count | partition_count ||
+----------------------------+----------------------------+--------+-----------------+-----------+-------------+-----------------+----------------------------+----------------------------+----------+-----------------+-----------------++
| 2021-10-25 00:12:16.700153 | 2021-10-25 00:12:16.700153 | 10 | -1 | 1005 | obmysql2 | RESTORE_SUCCESS | 2021-10-25 00:08:10.270205 | 2021-10-25 00:12:16.700153 | 100 | 100 | 100 ||
+----------------------------+----------------------------+--------+-----------------+-----------+-------------+-----------------+----------------------------+----------------------------+----------+-----------------+-----------------++
1 row in set (0.003 sec)
```
+ 查看恢复结果
```sql
[admin@obce00 ~]$ obclient -h172.20.249.54 -uroot@obmysql2#obce-3zones -P2883 -p123456 -c -A tpccdb -e "select max(o_entry_d) from bmsql_oorder;"
+---------------------+
| max(o_entry_d) |
+---------------------+
| 2021-10-24 19:07:39 |
+---------------------+
```
# 如何监控 OceanBase 和配置告警
## 如何用传统监控产品监控 OB
OceanBase 是单进程软件,其性能瓶颈通常不会首先是 IO ,而更可能是 CPU 和内存、网络等。当然,IO 也会影响 OceanBase 租户的读写性能。
在部署的时候,介绍 OceanBase 软件的 IO 有三类:运行日志、数据文件 和事务日志。生产环境建议用三块独立的磁盘存储,或者至少用 三个独立的文件系统。
针对 OceanBase 主机,建议部署下面监控。
| 监控项 | 描述 | 应对策略 |
|----------------------|-------------------------------------------|----------------------------------------------------------------|
| CPU | 监控CPU 的 USER、SYS、IOWAIT 和整体利用率。 | 分析 CPU 利用率高的进程。如果是 OBSERVER 进程,则进一步分析 SQL。 |
| LOAD | 主机的负载,跟 CPU 通常密切相关。 | |
| 内存 | 监控剩余内存。 | 剩余内存通常很稳定,如果小于1G ,则进程 OBSERVER 或 OBPROXY 有 OOM 风险。 |
| 监听 2881/2883/2884 端口 | 2881 是 OBSERVER 连接端口,2883 是 OBPROXY 连接端口。 | 确认进程是否存活,查看进程运行日志,分析进程故障或监听失败原因。立即重启进程。 |
| 网络流量 | 监控流量是否打满网卡(万兆) | 分析集群是否发生负载均衡,调低数据迁移的并发或者分析是否有大量数据抽取。 |
| IO 吞吐量、利用率和延时 | 监控数据盘和日志盘的IO 延时、吞吐量和IO 利用率。 | 分析集群是否发生负载均衡,调低数据迁移的并发或者分析是否有大量数据抽取。分析是否坏盘。分析业务SQL 是否执行计划有问题等。 |
## 如何用 PROMETHEUS 监控 OB
在第 2 课里介绍 OceanBase 监控插件 OBAgent 部署。OBAgent 启动后会自动生成适合 `Prometheus` 系统的配置文件目录 `prometheus_config`
### PROMETHEUS 安装部署
+ 下载 `Promethueus` 软件,地址:`https://prometheus.io/download/`
+ 解压缩安装
```bash
sudo tar zxvf prometheus-2.30.3.linux-amd64.tar.gz -C /usr/local/
# 复制 OBAgent 生成的 Prometheus 配置文件到 Prometheus 安装目录中。
sudo mv prometheus_config/ /usr/local/prometheus-2.30.3.linux-amd64/
```
+ Prometheus 服务文件
```bash
sudo mkdir /var/lib/prometheus
sudo vim /etc/systemd/system/prometheus.service
[Unit]
Description=Prometheus Server
Documentation=https://prometheus.io/docs/introduction/overview/
After=network-online.target
[Service]
Restart=on-failure
ExecStart=/usr/local/prometheus-2.30.3.linux-amd64/prometheus --config.file=/usr/local/prometheus-2.30.3.linux-amd64/prometheus_config/prometheus.yaml --storage.tsdb.path=/var/lib/prometheus --web.enable-lifecycle --web.external-url=http://172.20.249.54:9090
[Install]
WantedBy=multi-user.target
```
启动服务
```bash
sudo systemctl daemon-reload
sudo systemctl start prometheus
sudo systemctl status prometheus
[admin@obce00 ~]$ sudo systemctl status prometheus
● prometheus.service - Prometheus Server
Loaded: loaded (/etc/systemd/system/prometheus.service; disabled; vendor preset: disabled)
Active: active (running) since Thu 2021-10-21 15:54:42 CST; 49s ago
Docs: https://prometheus.io/docs/introduction/overview/
Main PID: 902555 (prometheus)
Tasks: 13 (limit: 195588)
Memory: 40.6M
CGroup: /system.slice/prometheus.service
└─902555 /usr/local/prometheus-2.30.3.linux-amd64/prometheus --config.file=/usr/local/prometheus-2.30.3.linux-amd64/prometheus_config/prometheus.yaml --storage.tsdb.path=/var/lib/prometheus --web.enable-lifecycle --web.external-url=http://172.20.249.54:9090
Oct 21 15:54:42 obce00 prometheus[902555]: level=info ts=2021-10-21T07:54:42.275Z caller=head.go:479 component=tsdb msg="Replaying on-disk memory mappable chunks if any"
Oct 21 15:54:42 obce00 prometheus[902555]: level=info ts=2021-10-21T07:54:42.275Z caller=head.go:513 component=tsdb msg="On-disk memory mappable chunks replay completed" duration=2.127µs
Oct 21 15:54:42 obce00 prometheus[902555]: level=info ts=2021-10-21T07:54:42.275Z caller=head.go:519 component=tsdb msg="Replaying WAL, this may take a while"
Oct 21 15:54:42 obce00 prometheus[902555]: level=info ts=2021-10-21T07:54:42.275Z caller=head.go:590 component=tsdb msg="WAL segment loaded" segment=0 maxSegment=0
Oct 21 15:54:42 obce00 prometheus[902555]: level=info ts=2021-10-21T07:54:42.275Z caller=head.go:596 component=tsdb msg="WAL replay completed" checkpoint_replay_duration=39.378µs wal_replay_duration=185.207µs total_replay_duration=242.438µs
Oct 21 15:54:42 obce00 prometheus[902555]: level=info ts=2021-10-21T07:54:42.277Z caller=main.go:849 fs_type=XFS_SUPER_MAGIC
Oct 21 15:54:42 obce00 prometheus[902555]: level=info ts=2021-10-21T07:54:42.277Z caller=main.go:852 msg="TSDB started"
Oct 21 15:54:42 obce00 prometheus[902555]: level=info ts=2021-10-21T07:54:42.277Z caller=main.go:979 msg="Loading configuration file" filename=/usr/local/prometheus-2.30.3.linux-amd64/prometheus_config/prometheus.yaml
Oct 21 15:54:42 obce00 prometheus[902555]: level=info ts=2021-10-21T07:54:42.281Z caller=main.go:1016 msg="Completed loading of configuration file" filename=/usr/local/prometheus-2.30.3.linux-amd64/prometheus_config/prometheus.yaml totalDuration=4.630509ms db_storage=1>
Oct 21 15:54:42 obce00 prometheus[902555]: level=info ts=2021-10-21T07:54:42.281Z caller=main.go:794 msg="Server is ready to receive web requests."
[admin@obce00 ~]$
```
+ 确认 Prometheus 启动成功
```bash
sudo netstat -ntlp | grep 9090
[admin@obce00 ~]$ sudo netstat -ntlp | grep 9090
tcp6 0 0 :::9090 :::* LISTEN 902555/prometheus
```
### PROMETHEUS 使用
使用浏览器测试:`http://172.20.249.54:9090/alerts`
+ 查看告警事件
![](media/16321093510830/16348034195467.jpg)
+ 查看节点 LOAD
![](media/16321093510830/16348064335069.jpg)
这里面会涉及到很多自定义的指标名。目前支持的指标名如下。
+ 主机指标
| 指标名 | Ladel | 描述 | 类型 |
|------------------------------------|---------------------------------|--------------|---------|
| node_cpu_seconds_total | cpu,mode,svr_ip | CPU 时间 | counter |
| node_disk_read_bytes_total | device,svr_ip | 磁盘读取字节数 | counter |
| node_disk_read_time_seconds_total | device,svr_ip | 磁盘读取消耗总时间 | counter |
| node_disk_reads_completed_total | device,svr_ip | 磁盘读取完成次数 | counter |
| node_disk_written_bytes_total | device,svr_ip | 磁盘写入字节数 | counter |
| node_disk_write_time_seconds_total | device,svr_ip | 磁盘写入消耗总时间 | counter |
| node_disk_writes_completed_total | device,svr_ip | 磁盘写入完成次数 | counter |
| node_filesystem_avail_bytes | device,fstype,mountpoint,svr_ip | 文件系统可用大小 | gauge |
| node_filesystem_readonly | device,fstype,mountpoint,svr_ip | 文件系统是否只读 | gauge |
| node_filesystem_size_bytes | device,fstype,mountpoint,svr_ip | 文件系统大小 | gauge |
| node_load1 | svr_ip | 1 分钟平均 load | gauge |
| node_load5 | svr_ip | 5 分钟平均 load | gauge |
| node_load15 | svr_ip | 15 分钟平均 load | gauge |
| node_memory_Buffers_bytes | svr_ip | 内存 buffer 大小 | gauge |
| node_memory_Cached_bytes | svr_ip | 内存 cache 大小 | gauge |
| node_memory_MemFree_bytes | svr_ip | 内存 free 大小 | gauge |
| node_memory_MemTotal_bytes | svr_ip | 内存总大小 | gauge |
| node_network_receive_bytes_total | device,svr_ip | 网络接受总字节数 | counter |
| node_network_transmit_bytes_total | device,svr_ip | 网络发送总字节数 | counter |
| node_ntp_offset_seconds | svr_ip | NTP 时钟偏移 | |
# 如何对 OceanBase 进行简单性能诊断
## 如何使用 TSAR 实时监控 OceanBase 主机性能
`tsar` 是阿里巴巴开源的主机性能采集软件,C语言开发,工作效率非常高,对主机性能占用很低。
`tsar` 开源地址:`github.com/alibaba/tsar``tsar` 可以编译安装。安装后默认每分钟采集一次主机性能,数据存放在 `/var/log/` 目录下,文件名是 `tsar.data` 。文件滚动存放,占用空间很小。
`tsar` 常用用法如下:
```bash
#查看历史性能数据概况,每分钟一笔
tsar -i 1
# 查看 CPU、LOAD、网络历史性能数据,每分钟一笔
tsar --cpu --load --traffic -i 1
# 查看实时 CPU、IO 性能数据,每3秒一笔记录
[root@obce00 ob-loader-dumper-2.1.13-SNAPSHOT]# df -h
文件系统 容量 已用 可用 已用% 挂载点
devtmpfs 15G 0 15G 0% /dev
tmpfs 15G 0 15G 0% /dev/shm
tmpfs 15G 508K 15G 1% /run
tmpfs 15G 0 15G 0% /sys/fs/cgroup
/dev/vda1 100G 6.8G 94G 7% /
/dev/mapper/obvg-lvredo 20G 45M 19G 1% /redo
/dev/mapper/obvg-lvdata 79G 57M 75G 1% /data
tmpfs 3.0G 0 3.0G 0% /run/user/0
[root@obce00 ob-loader-dumper-2.1.13-SNAPSHOT]# ll /dev/mapper/obvg-lvredo
lrwxrwxrwx 1 root root 7 9月 22 20:10 /dev/mapper/obvg-lvredo -> ../dm-0
[root@obce00 ob-loader-dumper-2.1.13-SNAPSHOT]# tsar --cpu --io -I dm-0 -l -i 3
Time -----------------------cpu---------------------- ------------------------------------------------------------------dm-0------------------------------------------------------------------
Time user sys wait hirq sirq util rrqms wrqms %rrqm %wrqm rs ws rsecs wsecs rqsize rarqsz warqsz qusize await rawait wawait svctm util
29/09/21-22:32:30 0.08 0.08 0.00 0.08 0.04 0.29 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
^C
```
## 查看 CPU 性能
CPU用`top`命令也可以看。原理相同。通常情况下,数据库很忙的时候,`load`会偏高、`user`利用率会相对比较高。再从`top`命令里看是哪个进程。如果是数据库进程,那基本确认是数据库内部性能问题。如果`sys`利用率也相对比较高(如超过`20`),则留意OS的异常。如果`wait`利用率相对比较高,则看看`IO`的性能。
`load`的结果里`load1``load5``load15`分别代表1分钟内,5分钟内,15分钟内平均值。可以看出`load`变化趋势。
```bash
$tsar --cpu --load -l -i 3
Time -----------------------cpu---------------------- -------------------load-----------------
Time user sys wait hirq sirq util load1 load5 load15 runq plit
07/07/19-15:24:54 25.59 10.62 3.08 0.00 0.42 36.63 13.78 12.99 12.15 5.00 2.8K
07/07/19-15:24:57 25.42 10.63 7.10 0.00 0.42 36.46 13.72 12.99 12.15 10.00 2.8K
07/07/19-15:25:00 25.25 10.11 3.85 0.00 0.40 35.77 13.58 12.97 12.15 3.00 2.8K
07/07/19-15:25:03 29.34 11.31 4.89 0.00 0.48 41.13 13.58 12.97 12.15 7.00 2.8K
07/07/19-15:25:06 24.80 9.93 5.92 0.00 0.36 35.09 13.37 12.94 12.14 25.00 2.8K
^C
```
## 查看网络性能
首先通过`ethtool`命令查看网卡的速度。然后通过`tsar`命令看网卡实际上传下载速度以及丢包率。如果接近或者超过网卡的能力,则表示网卡此刻接近吞吐量瓶颈。
```bash
#ethtool bond0
Settings for bond0:
Supported ports: [ ]
Supported link modes: Not reported
Supported pause frame use: No
Supports auto-negotiation: No
Advertised link modes: Not reported
Advertised pause frame use: No
Advertised auto-negotiation: No
Speed: 2000Mb/s
Duplex: Full
Port: Other
PHYAD: 0
Transceiver: internal
Auto-negotiation: off
Link detected: yes
[root@OceanBase081181186.et15sqa /home/qing.meiq]
#tsar --traffic -l -i 3
Time ---------------------traffic--------------------
Time bytin bytout pktin pktout pkterr pktdrp
07/07/19-15:58:24 8.7M 2.3M 9.9K 6.1K 0.00 0.00
07/07/19-15:58:27 9.0M 2.6M 10.0K 6.2K 0.00 0.00
07/07/19-15:58:30 9.7M 2.7M 10.7K 6.5K 0.00 0.00
07/07/19-15:58:33 10.7M 2.6M 11.0K 6.1K 0.00 0.00
07/07/19-15:58:36 8.5M 2.2M 9.5K 5.7K 0.00 0.00
^C
```
在分析带宽流量的时候,还可以借助 OS 自己的命令:`iftop`
```bash
# 查看网卡 IP 流量
iftop -i eth0 -nNB
# 按 L 、T 、3 、t 、 B 、l 、 p 找出具体哪个 IP 和 PORT 流量最大
```
## 查看内存性能
Memory主要是关注`free``buffer``cache`的变化是否正常。通常运行一段时间后数据库主机的内存分布就比较固定了。如有异常变化,还需要结合其他信息判断。
```
#tsar --mem -l -i 3
Time -----------------------mem----------------------
Time free used buff cach total util
07/07/19-15:31:46 3.0G 31.2G 1.8G 58.2G 94.2G 33.16
07/07/19-15:31:49 3.0G 31.2G 1.8G 58.2G 94.2G 33.13
07/07/19-15:31:52 3.0G 31.2G 1.8G 58.2G 94.2G 33.15
07/07/19-15:31:55 3.0G 31.2G 1.8G 58.2G 94.2G 33.15
^C
```
## 查看 IO 性能
看IO性能首先要确认当前的磁盘分区、文件系统设置等。通过`fdisk``mount``df -h`命令查看。找到数据库数据和文件所在的磁盘,使用`tsar`观察`IO`性能。对于一个磁盘的吞吐能力,响应时间水平应该事先有所了解。
```bash
[root@OceanBase081181186.et15sqa /home/qing.meiq]
#ll /dev/mapper/ob_vg-ob_data
lrwxrwxrwx 1 root root 7 Jun 12 15:28 /dev/mapper/ob_vg-ob_data -> ../dm-1
[root@OceanBase081181186.et15sqa /home/qing.meiq]
#ll /dev/mapper/ob_vg-ob_log
lrwxrwxrwx 1 root root 7 Jun 12 15:28 /dev/mapper/ob_vg-ob_log -> ../dm-0
[root@OceanBase081181186.et15sqa /home/qing.meiq]
#tsar --io -I dm-0 -l -i 3
Time ------------------------------------------dm-0------------------------------------------
Time rrqms wrqms rs ws rsecs wsecs rqsize qusize await svctm util
07/07/19-15:38:09 0.00 0.00 0.00 251.67 0.00 5.3K 21.67 2.00 8.89 3.97 99.87
07/07/19-15:38:12 0.00 0.00 0.00 231.67 0.00 4.7K 20.85 2.00 9.08 3.95 91.50
07/07/19-15:38:15 0.00 0.00 0.00 227.33 0.00 1.7K 7.73 1.00 8.71 4.39 99.73
07/07/19-15:38:18 0.00 0.00 0.00 213.33 0.00 1.3K 6.36 1.00 8.31 4.31 92.00
07/07/19-15:38:21 0.00 0.00 0.00 202.33 0.00 1.1K 5.54 1.00 9.09 4.51 91.20
07/07/19-15:38:24 0.00 0.00 0.00 230.67 0.00 1.3K 5.57 1.00 8.34 4.32 99.73
07/07/19-15:38:27 0.00 0.00 0.00 203.33 0.00 1.1K 5.65 1.00 8.65 4.49 91.27
07/07/19-15:38:30 0.00 0.00 0.00 224.67 0.00 1.5K 6.84 1.00 8.34 4.43 99.47
07/07/19-15:38:33 0.00 0.00 0.00 232.33 0.00 3.0K 13.11 2.00 8.61 3.96 92.07
07/07/19-15:38:36 0.00 0.00 0.00 210.67 0.00 1.2K 5.80 1.00 8.27 4.36 91.87
07/07/19-15:38:39 0.00 0.00 0.00 227.33 0.00 1.3K 5.75 1.00 8.16 4.39 99.90
^C
```
其中`IO Util`是个比较复杂的参数。如果是机械盘,这个`Util`可以反映出磁盘的繁忙程度,`svctm`会在几毫秒以上。如果是`SSD`盘,`svctm`会在零点几毫秒。 关于利用率有个近似公式:`利用率U = 吞吐量 * 每次平均服务时间`. 所以SSD盘需要看响应时间(`svctm`)、等待时间(`await`)等信息综合判断是否到瓶颈。
**备注:**
主机的性能是最容易看最基础的,如果主机在某方面呈现性能瓶颈,数据库的性能和稳定性很可能会受影响。二者之间的联系并不会表现的很直接,需要多观察总结成经验。
## 如何使用DOOBA 实时监控 OceanBase 租户性能
`dooba.py` 是 Python 语言编写的脚本,作用是实时读取 OceanBase 租户的性能。
ORACLE 数据库有 AWR 报表,可以方便诊断人员快速了解数据库在某个时间点的性能问题和原因。AWR 依赖很多内部视图,其中部分视图在 OceanBase 里也实现了。只是暂时 OceanBase 还没有实现 AWR 报表。OceanBase 运维平台 OCP 里提供了丰富的性能展示功能方便对 OceanBase 进行诊断。
OceanBase 里也记录了会话和 SQL 的等待事件,这一块功能还不是很成熟,大部分性能问题只要分析 SQL 就能解决。所以等待事件就不看了。
OceanBase 的 SQL 诊断,建议关注 租户的 `QPS`(每秒 SQL 请求数,包括 `SELECT``INSERT``UPDATE``DELETE` )以及其 `RT`(SQL 执行耗时)、`TPS`(每秒事务数,跟 ORACLE 一致 )以及其 `RT`(事务提交延时)。此外,关注这些指标在 OceanBase 集群节点上的性能信息。
OceanBase 集群的性能瓶颈不会首先在 IO ,而更容易在内存,其次是 CPU 。所以还需要关注 每秒内存的变化。具体是指增量内存的使用情况。
使用 OceanBase 自带的命令行监控脚本 `dooba` 可以观察很方便实时观察 OceanBase 租户性能。
在 sys 租户创建一个只读的账户,能查看系统视图。
```sql
grant select on oceanbase.* to dbamonitor identified by '123456';
```
```bash
python dooba.py -hobserver00 -udbamonitor@sys#obdemo -P2883 -p123456
```
## 快捷键
运行之后,按 `1` 查看 帮助。
```bash
Help Shown: 0 / Valid: 0 / Total: 0
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Global Keys - oceanbase
----------------------------------------------
c : Switch between tenants
w : write a screenshot file to current window
Global Keys - Widget
----------------------------------------------
Tab : Select next widget
m : Connect to oceanbase lms for this cluster using mysql
j : ssh to selected host
Global Keys - Page
----------------------------------------------
1 F1 : Help page
2 F2 : Gallery page
3 F3 : Observer page
4 F4 : History page
d : Delete selected widget
R : Restore deleted widgets
= : Filter Columns for current page (ms,cs,ups page only)
Global Keys - Test
----------------------------------------------
p : Messsage box tooltips
Global Keys - Selection Box
----------------------------------------------
DOWN TAB J P : Next item
UP K N : Previous item
SPC ENTER : Select current item
Q q : Quit selection box
Global Keys - System
----------------------------------------------
q : quit dooba
Support
----------------------------------------------
Author : Yudi Shi (fufeng.syd)
Mail : fufeng.syd@alipay.com
project page :
bug report :
feature req :
```
使用最多的快捷键如下:
+ `1` : 查看帮助
+ `2` :查看租户性能总览
+ `3` : 查看各个节点的性能总览。节点多的时候展示不全。
+ `c` :选择租户。通常要观察业务租户。
+ `tab` : 在各个 TAB 之间跳转。
+ `d` :删除当前 TAB 。屏幕显示不够的时候用。
+ `R` : 恢复所有的 TAB 。
![dooba 2](media/16162119416651.jpg)
![dooba 3](media/16162119732384.jpg)
## 性能指标说明
查看脚本内容就知道各个指标缩写的含义。其数据多取自于视图 `gv$sysstat` 。各项指标如下。
| 一级分类 | 二级分类 | 缩写 | 全称 | 含义 |
|----------|-----------|---------------|-----------------------|--------------------|
| Gallery | SQL COUNT | | | 租户 SQL QPS数据 |
| | | SEL. | sql select count | 平均每秒查询次数 |
| | | INS. | sql insert count | 平均每秒插入次数 |
| | | UPD. | sql update count | 平均每秒更新次数 |
| | | DEL. | sql delete count | 平均每秒删除次数 |
| | | REP. | sql replace count | 平均每秒替换次数 |
| | | CMT. | trans commit count | 平均每秒事务提交次数 |
| | | ROL. | trans rollback count | 平均每秒事务回滚次数 |
| | SQL RT | | | 租户 SQL延时数据 |
| | | SEL. | sql select time | 平均每次查询耗时 |
| | | INS. | sql insert time | 平均每次插入耗时 |
| | | UPD. | sql update time | 平均每次更新耗时 |
| | | DEL. | sql delete time | 平均每次删除耗时 |
| | | REP. | sql replace time | 平均每次替换耗时 |
| | | CMT. | trans commit time | 平均每次事务提交延时 |
| | RPC | | | 网络信息,不准,忽略 |
| | MEMORY(T) | | | 租户内存性能数据 |
| | | ⊿ACTIVE | active memstore used | 平均每秒增量内存变化量 |
| | | TOTAL | total memstore used | 增量内存累计总量 |
| | | PCT. | total memstore used | 增量内存占比 |
| | IOPS | | | 集群的IO性能数据 |
| | | SES. | active sessions | 当前活跃会话数 |
| | | IOR | io read count | 平均每秒读IO次数 |
| | | IOR-SZ | io read bytes | 平均每次读IO大小 |
| | | IOW | io write count | 平均每秒写IO次数 |
| | | IOW-SZ | io write bytes | 平均每次写IO大小 |
| SQL Page | zone | | | 每个节点的性能数据 |
| | | Active Sess | active sessions | 租户在该节点当前活跃会话数 |
| | | CPU | cpu usage | 租户在该节点的CPU利用率(相对值) |
| | | Cache-BI Hit | block index cache hit | 租户在该节点数据块的索引块的命中率 |
| | | Cache-Blk Hit | block cache hit | 租户在该节点数据块的命中率 |
| | | Cache-Loc Hit | location cache hit | 租户分区在该节点位置缓存命中率 |
| | | Cache-Row Hit | row cache hit | 租户分区在该节点行缓存命中率 |
| | | IO-R Cnt | io read count | 租户在该节点平均每秒读IO次数 |
| | | IO-R Size | io read bytes | 租户在该节点平均每次读IO大小 |
| | | IO-W Cnt | io write count | 租户在该节点平均每次写IO次数 |
| | | IO-W Size | io write bytes | 租户在该节点平均每次写IO大小 |
| | SQL | | | 每个节点的SQL性能数据 |
| | | SSC | sql select count | 租户在该节点平均每秒查询次数 |
| | | SSRT | sql select time | 租户在该节点平均每次查询耗时 |
| | | SIC | sql insert count | 租户在该节点平均每秒插入次数 |
| | | SIRT | sql insert time | 租户在该节点平均每次插入耗时 |
| | | SUC | sql update count | 租户在该节点平均每秒更新次数 |
| | | SURT | sql update time | 租户在该节点平均每次更新耗时 |
| | | SDC | sql delete count | 租户在该节点平均每秒删除次数 |
| | | SDRT | sql delete time | 租户在该节点平均每次删除耗时 |
| | | SRC | sql replace count | 租户在该节点平均每秒替换次数 |
| | | SRRT | sql replace time | 租户在该节点平均每次替换耗时 |
| | | TCC | trans commit count | 租户在该节点平均每秒事务提交次数 |
| | | TCRT | trans commit time | 租户在该节点平均每次事务提交延时 |
| | | SLC | sql local count | 租户在该节点平均每秒本地SQL次数 |
| | | SRC | sql remote count | 租户在该节点平均每秒远程SQL次数 |
这些指标可以解答如下问题:
+ 是不是每个节点都在提供读写服务?性能分别如何?
+ 连接数达到多少了?每个节点是多少?
+ IO吞吐量有多少?每个节点是多少?
+ 数据库SQL耗时有多少?每个节点是多少?
+ 每个节点的跨节点访问的SQL是多少?
+ 每个节点的数据缓存命中率多少?
从上面截图的监控数据,可以简单看出以下信息:
+ 租户虽然是3节点,实际只有一个节点在提供读写服务。
+ 业务SQL大头是查询和更新,其次是少量插入和删除。
+ 查询平均延时 900us (微秒),插入平均延时 320us ,更新平均延时 230us , 删除平均延时 200us 。
+ 租户事务平均延时 800us,事务比较小。
+ 租户有少量的IO读,吞吐量在230MB.
+ 节点远程SQL比例约占总SQL比例在 20% 左右
## 如何对 OceanBase 租户内存进行调优
前面章节“如何管理 OceanBase 内存”里介绍了 OceanBase 内存的组成。每个租户的内存默认会有一半用于增量写入(MEMSTORE),剩余的用于基线数据、SQL 执行以及其他等模块。实际经验中,调优租户内存主要是调整 MEMSTORE 和 SQL工作区内存大小。
## 调优增量内存
调优租户内存的思路是两方面。一方面尽可能扩大节点总内存资源、租户内存配额以及 MEMSTORE 内存的比例。MEMSTORE 内存占比默认是 50%,如果写比读多时,这个值可以小范围上调。另外一方面思路就是调优内存转储,当 MEMSTORE 内存剩余不多的时候,尽可能的转储释放内存。下面是示例,具体参数值的大小还要根据实际内存和性能情况微调。第三个思路就是租户设置内存写入限速,也有的是应用自身设置内存限速(比如说 DATAX 就有自己限速的设计)。
```sql
alter system set memory_limit_percentage = 90; -- OB占系统总内存的比例,提高OB可用的内存量。默认值是 80, 主机内存大于 256G 时,这个可以设置到 90,最大不要超过 90 。
alter system set memstore_limit_percentage = 55; -- memstore占租户的内存比,尽量增大memstore的空间(但是可能对读操作有负面影响)。
alter system set freeze_trigger_percentage = 40; -- 启动major/minor freeze的时机,让转储(minor freeze)尽早启动,memstore内存尽早释放。
alter system set minor_freeze_times = 100; -- minor freeze的次数,尽量不在测试期间触发major freeze。
alter system set minor_warm_up_duration_time = 0; -- 加快minor freeze
```
在调整的过程中也可以通过视图 `gv$memstore` 观察 MEMSTORE 的使用情况,使用下面 SQL:
视图说明:
| 字段名称 | 类型 | 是否可以为 NULL | 描述 |
|----------------|--------------|------------|------------------------------|
| CON_ID | NUMBER(38) | NO | 租户 ID |
| SVR_IP | VARCHAR2(32) | NO | 服务器的 IP |
| SVR_PORT | NUMBER(38) | NO | 服务器端口 |
| ACTIVE | NUMBER(38) | NO | 当前活跃的 Memtable 的内存占用大小,单位为字节 |
| TOTAL | NUMBER(38) | NO | 当前所有 Memtable 的内存占用大小,单位为字节 |
| FREEZE_TRIGGER | NUMBER(38) | NO | 触发 Memtable 冻结的内存大小,单位为字节 |
| MEM_LIMIT | NUMBER(38) | NO | Memtable 的内存大小限制,单位为字节 |
| FREEZE_CNT | NUMBER(38) | NO | Memtable 的冻结次数 |
```sql
SELECT tenant_id, ip, round(active/1024/1024/1024) active_gb, round(total/1024/1024/1024) total_gb, round(freeze_trigger/1024/1024/1024) freeze_trg_gb, round(mem_limit/1024/1024/1024) mem_limit_gb
, freeze_cnt , round((active/freeze_trigger),2) freeze_pct, round(total/mem_limit, 2) mem_usage
FROM `gv$memstore`
WHERE tenant_id =1002
ORDER BY tenant_id, ip;
输出:
+-----------+---------------+-----------+----------+---------------+--------------+------------+------------+-----------+
| tenant_id | ip | active_gb | total_gb | freeze_trg_gb | mem_limit_gb | freeze_cnt | freeze_pct | mem_usage |
+-----------+---------------+-----------+----------+---------------+--------------+------------+------------+-----------+
| 1002 | 172.20.249.49 | 1 | 1 | 4 | 5 | 0 | 0.35 | 0.25 |
| 1002 | 172.20.249.51 | 1 | 1 | 4 | 5 | 0 | 0.33 | 0.23 |
| 1002 | 172.20.249.52 | 1 | 2 | 4 | 5 | 0 | 0.34 | 0.41 |
+-----------+---------------+-----------+----------+---------------+--------------+------------+------------+-----------+
3 rows in set (0.004 sec)
```
一般来说要维持 `mem_usage` 在 90% 以下,如果达到或超过 90%,则有可能触发租户内存写入限速,从而显著降低应用写入速度。
## 调优其他内存
其他内存模块的大小可以查看视图 `__all_virtual_mem_info`
```sql
select zone, svr_ip, label, ctx_name, mod_name, round(hold/1024/1024) hold_mb, round(used/1024/1024) used_mb, count, alloc_count
from __all_virtual_memory_info
where tenant_id = 1002 and label <> 'OB_MEMSTORE'
order by hold desc limit 10;
输出:
+-------+---------------+------------------+----------------+------------------+---------+---------+-------+-------------+
| zone | svr_ip | label | ctx_name | mod_name | hold_mb | used_mb | count | alloc_count |
+-------+---------------+------------------+----------------+------------------+---------+---------+-------+-------------+
| zone1 | 172.20.249.52 | MysqlRequesReco | DEFAULT_CTX_ID | MysqlRequesReco | 197 | 196 | 99 | 0 |
| zone2 | 172.20.249.49 | MysqlRequesReco | DEFAULT_CTX_ID | MysqlRequesReco | 189 | 188 | 95 | 0 |
| zone3 | 172.20.249.51 | MysqlRequesReco | DEFAULT_CTX_ID | MysqlRequesReco | 125 | 124 | 63 | 0 |
| zone1 | 172.20.249.52 | TransAudit | DEFAULT_CTX_ID | TransAudit | 32 | 32 | 1 | 0 |
| zone2 | 172.20.249.49 | TransAudit | DEFAULT_CTX_ID | TransAudit | 32 | 32 | 1 | 0 |
| zone3 | 172.20.249.51 | TransAudit | DEFAULT_CTX_ID | TransAudit | 32 | 32 | 1 | 0 |
| zone2 | 172.20.249.49 | SqlPlanMon | DEFAULT_CTX_ID | SqlPlanMon | 15 | 15 | 8 | 0 |
| zone3 | 172.20.249.51 | OB_KVSTORE_CACHE | DEFAULT_CTX_ID | OB_KVSTORE_CACHE | 14 | 14 | 7 | 0 |
| zone2 | 172.20.249.49 | OB_KVSTORE_CACHE | DEFAULT_CTX_ID | OB_KVSTORE_CACHE | 14 | 14 | 7 | 0 |
| zone1 | 172.20.249.52 | Election | DEFAULT_CTX_ID | Election | 12 | 12 | 6 | 0 |
+-------+---------------+------------------+----------------+------------------+---------+---------+-------+-------------+
10 rows in set (0.019 sec)
```
找出内存比较大的模块,联系 OceanBase 技术支持一起分析。
# 如何快速处理 OceanBase 故障
## 如何判断定位 OceanBase 故障
当 OceanBase 故障时,应用端可能会有很多报错信息反馈过来。此时需要初步判断应用是全部失败,还是有成功有失败。理论上 OceanBase 局部节点故障,应用只会是局部数据库读写故障或者中断,在1分钟左右应用就能全部恢复。
同时需要尽快确认 OceanBase 集群的状态。
+ 首先确认集群节点状态
```sql
select a.zone,concat(a.svr_ip,':',a.svr_port) observer, cpu_total, (cpu_total-cpu_assigned) cpu_free, round(mem_total/1024/1024/1024) mem_total_gb, round((mem_total-mem_assigned)/1024/1024/1024) mem_free_gb, usec_to_time(b.last_offline_time) last_offline_time, usec_to_time(b.start_service_time) start_service_time, b.status, usec_to_time(b.stop_time) stop_time
from __all_virtual_server_stat a join __all_server b on (a.svr_ip=b.svr_ip and a.svr_port=b.svr_port)
order by a.zone, a.svr_ip
;
```
关注:
+ 节点状态 `status` :升级前没有 `inactive` 值,升级过程中会有。
+ 节点服务时间 `start_service_time` 的时间不是默认值(`1970-01-01 08:00:00.000000`)。如果是,则表示节点还没有恢复结束。
+ 节点停止时间 `stop_time` 的时间是默认值(`1970-01-01 08:00:00.000000`)。如果不是,则表示节点被停服( `stop server`) 了,先启动服务(`start server`)。
+ 其次确认集群近期事件
```sql
SELECT DATE_FORMAT(gmt_create, '%b%d %H:%i:%s') gmt_create_ , module, event, name1, value1, name2, value2, rs_svr_ip
FROM __all_rootservice_event_history
WHERE 1 = 1
AND module IN ('server','root_service','balancer')
AND gmt_create > SUBDATE(now(),interval 1 hour)
ORDER BY gmt_create DESC
LIMIT 50;
```
注意留意节点掉线和上线事件、合并超时事件、数据迁移事件等。
## 如何处理节点掉线或宕机故障
## 修改租户变量允许 DDL
如果有租户的架构是 `1-1-1` 并且宕机的节点就是该租户的成员,宕机后可能导致租户的 DDL 报错。
```sql
MySQL [test]> create table t2 like t1;
ERROR 4624 (HY000): machine resource is not enough to hold a new unit
```
此时需要将全局变量 `ob_create_table_strict_mode` 值设置为 OFF 。
```sql
set global ob_create_table_strict_mode = off;
```
然后重新登录业务租户,就可以做 DDL 了。
注意:这个变量关闭还是有一点风险,需要尽快修复故障节点。
## 启动 OBSERVER 进程
如果节点掉线了,但是节点的进程还在,则很可能有两个原因:
+ 节点跟其他节点的时间误差超过 200ms 。这个时候首先检查时间是否同步。
+ 节点的事务日志目录空间使用率超过参数(`clog_disk_usage_limit_percentage`,默认值是 95 ,意为 95% )定义。
如果节点进程不在,则首先尝试拉起进程。然后再观察几分钟,看进程是否再次推出。启动进程时注意用正确的方式。
```bash
# 切换到正确用户下
cd /home/admin/oceanbase-ce && bin/observer
```
如果进程还是会退出,则查看最近的进程运行日志,搜索 ERROR 信息。
```bash
cd /home/admin/oceanbase-ce
grep ERROR log/observer.log | vim -
```
## 如何处理节点时间不同步故障
### 判断集群节点时间同步误差
节点时间同步问题需要准确判断。需要对集群中所有节点进行相互测试,测试命令是:`clockdiff`
```bash
[root@obce01 ~]# clockdiff 172.20.249.49
...
host=172.20.249.49 rtt=428(314)ms/0ms delta=0ms/0ms Thu Sep 30 21:33:16 2021
[root@obce01 ~]# clockdiff 172.20.249.51
...
host=172.20.249.51 rtt=426(314)ms/0ms delta=0ms/0ms Thu Sep 30 21:33:22 2021
[root@obce01 ~]#
[root@obce02 ~]# clockdiff 172.20.249.50
.
host=172.20.249.50 rtt=750(187)ms/0ms delta=0ms/0ms Thu Sep 30 21:34:10 2021
[root@obce02 ~]# clockdiff 172.20.249.52
...
host=172.20.249.52 rtt=423(315)ms/0ms delta=-1ms/-1ms Thu Sep 30 21:34:14 2021
```
在某些客户机房环境了,命令 `clockdiff` 可能会报错(原因不明)。此时可以换用 `ping` 命令,查看返回的报文信息判断时间误差。
```bash
ping -T tsandaddr 172.20.249.49 -c 3
输出:
[root@obce01 ~]# ping -T tsandaddr 172.20.249.49 -c 3
PING 172.20.249.49 (172.20.249.49) 56(124) bytes of data.
64 bytes from 172.20.249.49: icmp_seq=1 ttl=64 time=26.3 ms
TS: 172.20.249.52 48963954 absolute
172.20.249.49 13
172.20.249.49 0
172.20.249.52 13
64 bytes from 172.20.249.49: icmp_seq=2 ttl=64 time=0.333 ms
TS: 172.20.249.52 48964963 absolute
172.20.249.49 1
172.20.249.49 0
172.20.249.52 0
64 bytes from 172.20.249.49: icmp_seq=3 ttl=64 time=0.480 ms
TS: 172.20.249.52 48966011 absolute
172.20.249.49 0
172.20.249.49 0
172.20.249.52 0
--- 172.20.249.49 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2057ms
rtt min/avg/max/mdev = 0.333/9.038/26.302/12.207 ms
```
### 修复集群节点时间同步服务
此时,主要是检查每个节点的时间 同步服务是否正常。强烈推荐使用 `chrony` 同步服务,不要使用 `ntpd`
+ 检查 `chrony` 节点同步服务
使用 `chrony` 时间服务是为了保证 OceanBase 集群各个节点时间尽可能保证同步,下面这些命令供参考。具体使用请查看 `chrony` 官方使用说明:[Chronyc Frequently Asked Questions](https://chrony.tuxfamily.org/faq.html)
```bash
查看时间同步活动
chronyc activity
查看时间服务器
chronyc sources
查看同步状态
chronyc sources -v
校准时间服务器:
chronyc tracking
```
+ 检查节点跟选中的时间服务器之间的时间差
这里也是使用 `clockdiff` 命令。
有些时间服务器是禁止`ping` 命令的,或者防火墙是禁止 `ping` 命令的,则无法探知时间误差。
这里面的判断可能会比较复杂。
一个快速的校正时间方法就是直接使用 `ntpdate` 命令同步本节点跟时间源之间的时间。测试环境如果时间总是不同步,可以将这个命令写到 `crontab` 里,每分钟运行一次。
```bash
ntpdate -b xxx.xxx.xxx.xxx
```
# 如何使用 OBD 运维
## 如何使用 OBD 调整 OceanBase 集群参数
示例:通过 OBD 给集群节点内存扩容(从 8G 扩容到 16G)。
+ 使用 OBD 命令编辑参数文件
```bash
obd cluster edit-config obce-3zones
修改:
memory_limit: 16G # The maximum running memory for an observer
保存:
oceanbase-ce-3.1.0 already installed.
obproxy-3.1.0 already installed.
Search param plugin and load ok
Parameter check ok
Save deploy "obce-3zones" configuration
```
+ 使用 OBD 命令 `reload` 参数
```bash
obd cluster reload obce-3zones
输出:
Get local repositories and plugins ok
Open ssh connection ok
Cluster status check ok
Connect to observer ok
Connect to obproxy ok
obce-3zones reload
```
+ 登录 SYS 租户确认参数变更
```sql
MySQL [oceanbase]> show parameters like 'memory_limit';
+-------+----------+---------------+----------+--------------+-----------+-------+--------------------------------------------------------------------------------------------------------------------------------+----------+---------+---------+-------------------+
| zone | svr_type | svr_ip | svr_port | name | data_type | value | info | section | scope | source | edit_level |
+-------+----------+---------------+----------+--------------+-----------+-------+--------------------------------------------------------------------------------------------------------------------------------+----------+---------+---------+-------------------+
| zone2 | observer | 172.20.249.49 | 2882 | memory_limit | NULL | 16G | the size of the memory reserved for internal use(for testing purpose), 0 means follow memory_limit_percentage. Range: 0, [8G,) | OBSERVER | CLUSTER | DEFAULT | DYNAMIC_EFFECTIVE |
| zone1 | observer | 172.20.249.52 | 2882 | memory_limit | NULL | 16G | the size of the memory reserved for internal use(for testing purpose), 0 means follow memory_limit_percentage. Range: 0, [8G,) | OBSERVER | CLUSTER | DEFAULT | DYNAMIC_EFFECTIVE |
| zone3 | observer | 172.20.249.51 | 2882 | memory_limit | NULL | 16G | the size of the memory reserved for internal use(for testing purpose), 0 means follow memory_limit_percentage. Range: 0, [8G,) | OBSERVER | CLUSTER | DEFAULT | DYNAMIC_EFFECTIVE |
+-------+----------+---------------+----------+--------------+-----------+-------+--------------------------------------------------------------------------------------------------------------------------------+----------+---------+---------+-------------------+
3 rows in set (0.005 sec)
```
+ 确认集群可用内存变化
```sql
select a.zone,concat(a.svr_ip,':',a.svr_port) observer, cpu_total, (cpu_total-cpu_assigned) cpu_free, round(mem_total/1024/1024/1024) mem_total_gb, round((mem_total-mem_assigned)/1024/1024/1024) mem_free_gb, usec_to_time(b.last_offline_time) last_offline_time, usec_to_time(b.start_service_time) start_service_time, b.status, usec_to_time(b.stop_time) stop_time, b.build_version
from __all_virtual_server_stat a join __all_server b on (a.svr_ip=b.svr_ip and a.svr_port=b.svr_port)
order by a.zone, a.svr_ip
;
输出:
+-------+--------------------+-----------+----------+--------------+-------------+----------------------------+----------------------------+--------+----------------------------+------------------------------------------------------------------------+
| zone | observer | cpu_total | cpu_free | mem_total_gb | mem_free_gb | last_offline_time | start_service_time | status | stop_time | build_version |
+-------+--------------------+-----------+----------+--------------+-------------+----------------------------+----------------------------+--------+----------------------------+------------------------------------------------------------------------+
| zone1 | 172.20.249.52:2882 | 14 | 0 | 13 | 8 | 1970-01-01 08:00:00.000000 | 2021-09-25 08:19:06.622351 | active | 1970-01-01 08:00:00.000000 | 3.1.0_3-b20901e8c84d3ea774beeaca963c67d7802e4b4e(Aug 10 2021 08:10:38) |
| zone2 | 172.20.249.49:2882 | 14 | 0 | 13 | 8 | 1970-01-01 08:00:00.000000 | 2021-09-25 08:19:07.392669 | active | 1970-01-01 08:00:00.000000 | 3.1.0_3-b20901e8c84d3ea774beeaca963c67d7802e4b4e(Aug 10 2021 08:10:38) |
| zone3 | 172.20.249.51:2882 | 14 | 0 | 13 | 8 | 1970-01-01 08:00:00.000000 | 2021-09-26 14:05:58.641570 | active | 1970-01-01 08:00:00.000000 | 3.1.0_3-b20901e8c84d3ea774beeaca963c67d7802e4b4e(Aug 10 2021 08:10:38) |
+-------+--------------------+-----------+----------+--------------+-------------+----------------------------+----------------------------+--------+----------------------------+------------------------------------------------------------------------+
3 rows in set (0.009 sec)
select t1.name resource_pool_name, t2.`name` unit_config_name, t2.max_cpu, t2.min_cpu, round(t2.max_memory/1024/1024/1024) max_mem_gb, round(t2.min_memory/1024/1024/1024) min_mem_gb, t3.unit_id, t3.zone, concat(t3.svr_ip,':',t3.`svr_port`) observer,t4.tenant_id, t4.tenant_name
from __all_resource_pool t1 join __all_unit_config t2 on (t1.unit_config_id=t2.unit_config_id)
join __all_unit t3 on (t1.`resource_pool_id` = t3.`resource_pool_id`)
left join __all_tenant t4 on (t1.tenant_id=t4.tenant_id)
order by t1.`resource_pool_id`, t2.`unit_config_id`, t3.unit_id
;
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
| resource_pool_name | unit_config_name | max_cpu | min_cpu | max_mem_gb | min_mem_gb | unit_id | zone | observer | tenant_id | tenant_name |
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 1 | zone1 | 172.20.249.52:2882 | 1 | sys |
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 2 | zone2 | 172.20.249.49:2882 | 1 | sys |
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 3 | zone3 | 172.20.249.51:2882 | 1 | sys |
| my_pool | unit1 | 9 | 9 | 3 | 3 | 1001 | zone1 | 172.20.249.52:2882 | 1002 | obmysql |
| my_pool | unit1 | 9 | 9 | 3 | 3 | 1002 | zone2 | 172.20.249.49:2882 | 1002 | obmysql |
| my_pool | unit1 | 9 | 9 | 3 | 3 | 1003 | zone3 | 172.20.249.51:2882 | 1002 | obmysql |
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
6 rows in set (0.010 sec)
```
新增可用内存 8G 。
+ 调大业务租户内存规格
```sql
alter resource unit unit1 max_memory='11G',min_memory='11G';
# 重复查看集群可用内存
+-------+--------------------+-----------+----------+--------------+-------------+----------------------------+----------------------------+--------+----------------------------+------------------------------------------------------------------------+
| zone | observer | cpu_total | cpu_free | mem_total_gb | mem_free_gb | last_offline_time | start_service_time | status | stop_time | build_version |
+-------+--------------------+-----------+----------+--------------+-------------+----------------------------+----------------------------+--------+----------------------------+------------------------------------------------------------------------+
| zone1 | 172.20.249.52:2882 | 14 | 0 | 13 | 0 | 1970-01-01 08:00:00.000000 | 2021-09-25 08:19:06.622351 | active | 1970-01-01 08:00:00.000000 | 3.1.0_3-b20901e8c84d3ea774beeaca963c67d7802e4b4e(Aug 10 2021 08:10:38) |
| zone2 | 172.20.249.49:2882 | 14 | 0 | 13 | 0 | 1970-01-01 08:00:00.000000 | 2021-09-25 08:19:07.392669 | active | 1970-01-01 08:00:00.000000 | 3.1.0_3-b20901e8c84d3ea774beeaca963c67d7802e4b4e(Aug 10 2021 08:10:38) |
| zone3 | 172.20.249.51:2882 | 14 | 0 | 13 | 0 | 1970-01-01 08:00:00.000000 | 2021-09-26 14:05:58.641570 | active | 1970-01-01 08:00:00.000000 | 3.1.0_3-b20901e8c84d3ea774beeaca963c67d7802e4b4e(Aug 10 2021 08:10:38) |
+-------+--------------------+-----------+----------+--------------+-------------+----------------------------+----------------------------+--------+----------------------------+------------------------------------------------------------------------+
3 rows in set (0.005 sec)
```
## 如何使用 OBD 对 OceanBase 集群 或 OBPROXY 集群扩容
OBD 对 OceanBase 集群扩容主要是增加每个 ZONE 内的节点数。前面已经介绍过集群扩容手动步骤,这里主要是介绍 OBD 工具的操作步骤。
注意,OBD 的定位并不是运维工具,所以对于集群扩容节点并不是特别顺畅。
OBD 对集群扩容步骤:
+ OBD 初始化新机器节点目录。
+ OBD 启动新机器节点进程。
在操作之前,先查看当前部署的集群信息。
```bash
[admin@obce00 ~]$ obd cluster display obce-3zones
Get local repositories and plugins ok
Open ssh connection ok
Cluster status check ok
Connect to observer ok
Wait for observer init ok
+-------------------------------------------------+
| observer |
+---------------+---------+------+-------+--------+
| ip | version | port | zone | status |
+---------------+---------+------+-------+--------+
| 172.20.249.49 | 3.1.0 | 2881 | zone2 | active |
| 172.20.249.51 | 3.1.0 | 2881 | zone3 | active |
| 172.20.249.52 | 3.1.0 | 2881 | zone1 | active |
+---------------+---------+------+-------+--------+
Connect to obproxy ok
+-------------------------------------------------+
| obproxy |
+---------------+------+-----------------+--------+
| ip | port | prometheus_port | status |
+---------------+------+-----------------+--------+
| 172.20.249.52 | 2883 | 2884 | active |
| 172.20.249.49 | 2883 | 2884 | active |
| 172.20.249.51 | 2883 | 2884 | active |
+---------------+------+-----------------+--------+
[admin@obce00 ~]$
[admin@obce00 ~]$ obd --version
OceanBase Deploy: 1.1.0
REVISION:
BUILD_BRANCH:
BUILD_TIME: Aug 10 2021 11:50:40OURCE
Copyright (C) 2021 OceanBase
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
```
### OBD 初始化新机器节点目录
当要给集群扩容节点的时候,建议每个 ZONE 新增相同配置的机器。但是不能直接在现有配置文件中编辑。当前版本(`1.1.0`) 采取的方式是复制现有的配置文件为新文件,然后修改配置文件中的 `servers` 下的 IP 为新的 IP 。如果 OBPROXY 集群不扩容,就删除 OBPROXY 配置节。
然后再运行 `obd cluster deploy` 命令。 `deploy` 命令会自动在新节点安装 OceanBase 软件包、初始化相关目录。
```bash
obd cluster deploy obce-3zones2 -c obce-3zones2.yaml
```
注意,deploy 成功后,不要运行 `obd cluster start` 命令,那个会初始化一个新的集群,不符合原本的目的。
### OBD 启动老集群节点
此时需要使用命令 `obd cluster edit-config` 编辑当前运行的集群的配置文件,在 `servers` 下加入新的节点 IP 。新 IP 紧跟在老的 IP 之后(换行)。
然后针对老的集群运行 `obd cluster start` 命令。这个会启动集群中所有节点。如果节点的进程已经启动,就会跳过。所以,只会启动新增节点的进程。
```bash
obd cluster start obce-3zones
```
### OceanBase 集群添加新节点
对于 OceanBase 集群扩容节点,还需要在集群 SYS 租户里添加新的节点到相应 ZONE 里。
命令是:`ALTER SYSTEM ADD SERVER '节点IP:RPC端口' ZONE '节点所属ZONE';`
```sql
alter system add server '11.166.87.5:2882' zone 'zone1';
```
以上命令立即返回,然后查看节点状态就可以看到新节点的状态是 `active`
# 附录
## A1. OceanBase 的常用指标的查询表达式
本文介绍 OBAgent 常用指标的查询表达式。
常用指标在查询时,必须将变量替换成待查询的具体信息。需要替换的字段如下:
+ @LABELS,替换为具体 Label 的过滤条件。
+ @INTERVAL,替换为计算周期。
+ @GBLABELS,替换为聚合的 Label 名称。
| 指标 | 表达式 | 单位 |
|------------------------|---------|------|
| 活跃 MEMStore 大小 | sum(ob_sysstat{stat_id="130000",@LABELS}) by (@GBLABELS) / 1048576| MB |
| 当前活跃会话数 | sum(ob_active_session_num{@LABELS}) by ([@GBLABELS)| - |
| 块缓存命中率 | 100 * 1 / (1 + sum(rate(ob_sysstat{stat_id="50009",@LABELS}[@INTERVAL])) by ([@GBLABELS)](/GBLABELS)) / sum(rate(ob_sysstat{stat_id="50008",@LABELS}[@INTERVAL])) by (@GBLABELS))| % |
| 块缓存大小 | sum(ob_cache_size_bytes{cache_name="user_block_cache",@LABELS}) by ([@GBLABELS)](/GBLABELS)) / 1048576| MB |
| 每秒提交的事务日志大小 | sum(rate(ob_sysstat{stat_id="80057",@LABELS}[@INTERVAL])) by ([@GBLABELS)](/GBLABELS))| byte |
| 每次事务日志写盘平均耗时 | sum(rate(ob_sysstat{stat_id="80041",@LABELS}[@INTERVAL])) by (@GBLABELS) / sum(rate(ob_sysstat{stat_id="80040",@LABELS}[@INTERVAL])) by (@GBLABELS)| us |
| CPU 使用率 | 100 * (1 - sum(rate(node_cpu_seconds_total{mode="idle", @LABELS}[@INTERVAL])) by (@GBLABELS) / sum(rate(node_cpu_seconds_total{@LABELS}[@INTERVAL])) by (@GBLABELS))| % |
| 磁盘分区已使用容量 | sum(node_filesystem_size_bytes{@LABELS} - node_filesystem_avail_bytes{@LABELS}) by (@GBLABELS) / 1073741824| GB |
| 每秒读次数 | avg(rate(node_disk_reads_completed_total{@LABELS}[@INTERVAL])) by (@GBLABELS)| 次/s |
| 每次读取数据量 | avg(rate(node_disk_read_bytes_total{@LABELS}[@INTERVAL])) by (@GBLABELS) / 1048576| MB |
| SSStore 每秒读次数 | sum(rate(ob_sysstat{stat_id="60000",@LABELS}[@INTERVAL])) by (@GBLABELS)| 次/s |
| SSStore 每次读取平均耗时 | sum(rate(ob_sysstat{stat_id="60001",@LABELS}[@INTERVAL])) by (@GBLABELS) / sum(rate(ob_sysstat{stat_id="60000",@LABELS}[@INTERVAL])) by (@GBLABELS)| us |
| SSStore 每秒读取数据量 | sum(rate(ob_sysstat{stat_id="60002",@LABELS}[@INTERVAL])) by (@GBLABELS)| byte |
| 每秒读取平均耗时 | 1000000 * (avg(rate(node_disk_read_time_seconds_total{@LABELS}[@INTERVAL])) by (@GBLABELS)) / (avg(rate(node_disk_reads_completed_total{@LABELS}[@INTERVAL])) by (@GBLABELS))| us |
| 平均每次 IO 读取耗时 | 1000000 * (avg(rate(node_disk_read_time_seconds_total{@LABELS}[@INTERVAL])) by (@GBLABELS)) / (avg(rate(node_disk_reads_completed_total{@LABELS}[@INTERVAL])) by (@GBLABELS))| us |
| 每秒写次数 | avg(rate(node_disk_writes_completed_total{@LABELS}[@INTERVAL])) by (@GBLABELS)| 次/s |
| 每次写入数据量 | avg(rate(node_disk_written_bytes_total{@LABELS}[@INTERVAL])) by (@GBLABELS) / 1048576| MB |
| SSStore 每秒写次数 | sum(rate(ob_sysstat{stat_id="60003",@LABELS}[@INTERVAL])) by (@GBLABELS)| 次/s |
| SSStore 每次写入平均耗时 | sum(rate(ob_sysstat{stat_id="60004",@LABELS}[@INTERVAL])) by (@GBLABELS) / sum(rate(ob_sysstat{stat_id="60003",@LABELS}[@INTERVAL])) by (@GBLABELS)| us |
| SSStore 每秒写入数据量 | sum(rate(ob_sysstat{stat_id="60005",@LABELS}[@INTERVAL])) by (@GBLABELS)| byte |
| 每秒写入平均耗时 | 1000000 * (avg(rate(node_disk_write_time_seconds_total{@LABELS}[@INTERVAL])) by (@GBLABELS)) / (avg(rate(node_disk_writes_completed_total{@LABELS}[@INTERVAL])) by (@GBLABELS))| us |
| 平均每次 IO 写入耗时 | 1000000 * (avg(rate(node_disk_write_time_seconds_total{@LABELS}[@INTERVAL])) by (@GBLABELS)) / (avg(rate(node_disk_writes_completed_total{@LABELS}[@INTERVAL])) by (@GBLABELS))| us |
| 过去 1 分钟系统平均负载 | avg(node_load1{@LABELS}) by (@GBLABELS)| - |
| 过去 15 分钟系统平均负载 | avg(node_load15{@LABELS}) by (@GBLABELS)| - |
| 过去 5 分钟系统平均负载 | avg(node_load5{@LABELS}) by (@GBLABELS)| - |
| 触发合并阈值 | sum(ob_sysstat{stat_id="130002",@LABELS}) by (@GBLABELS) / 1048576| MB |
| 内核 Buffer Cache 大小 | avg(node_memory_Buffers_bytes{@LABELS}) by (@GBLABELS) / 1073741824| GB |
| 可用物理内存大小 | avg(node_memory_MemFree_bytes{@LABELS}) by (@GBLABELS) / 1073741824| GB |
| 使用物理内存大小 | (avg(node_memory_MemTotal_bytes{@LABELS}) by (@GBLABELS)| GB |
| || |
| | avg(node_memory_MemFree_bytes{@LABELS}) by (@GBLABELS)| |
| || |
| | avg(node_memory_Cached_bytes{@LABELS}) by (@GBLABELS)| |
| || |
| | avg(node_memory_Buffers_bytes{@LABELS}) by (@GBLABELS)) / 1073741824| |
| MEMStore 的上限 | sum(ob_sysstat{stat_id="130004",@LABELS}) by (@GBLABELS) / 1048576| MB |
| MEMStore 使用百分比 | 100 * sum(ob_sysstat{stat_id="130001",@LABELS}) by (@GBLABELS) / sum(ob_sysstat{stat_id="130004",@LABELS}) by (@GBLABELS)| % |
| 写锁等待失败次数 | sum(rate(ob_sysstat{stat_id="60022",@LABELS}[@INTERVAL])) by (@GBLABELS)| 次/s |
| 写锁等待成功次数 | sum(rate(ob_sysstat{stat_id="60021",@LABELS}[@INTERVAL])) by (@GBLABELS)| 次/s |
| 写锁平均等待耗时 | sum(rate(ob_sysstat{stat_id="60023",@LABELS}[@INTERVAL])) by (@GBLABELS) / (sum(rate(ob_sysstat{stat_id="60021",@LABELS}[@INTERVAL])) by (@GBLABELS) + sum(rate(ob_sysstat{stat_id="60022",@LABELS}[@INTERVAL])) by (@GBLABELS))| us |
| 每秒接收数据量 | avg(rate(node_network_receive_bytes_total{@LABELS}[@INTERVAL])) by (@GBLABELS) / 1048576| MB |
| 每秒发送数据量 | avg(rate(node_network_transmit_bytes_total{@LABELS}[@INTERVAL])) by (@GBLABELS) / 1048576| MB |
| CPU 使用率 | 100 * sum(ob_sysstat{stat_id="140006",@LABELS}) by (@GBLABELS) / sum(ob_sysstat{stat_id="140005",@LABELS}) by (@GBLABELS)| % |
| 分区数量 | sum(ob_partition_num{@LABELS}) by (@GBLABELS)| - |
| 执行计划缓存命中率 | 100 * sum(rate(ob_plan_cache_hit_total{@LABELS}[@INTERVAL])) by (@GBLABELS) / sum(rate(ob_plan_cache_access_total{@LABELS}[@INTERVAL])) by (@GBLABELS)| % |
| 执行计划缓存大小 | sum(ob_plan_cache_memory_bytes{@LABELS}) by (@GBLABELS) / 1048576| MB |
| 平均每秒 SQL 进等待队列的次数 | sum(rate(ob_sysstat{stat_id="20001",@LABELS}[@INTERVAL])) by (@GBLABELS)| 次/s |
| SQL 在等待队列中等待耗时 | sum(rate(ob_sysstat{stat_id="20002",@LABELS}[@INTERVAL])) by (@GBLABELS) / sum(rate(ob_sysstat{stat_id="20001",@LABELS}[@INTERVAL])) by (@GBLABELS)| us |
| 行缓存命中率 | 100 * 1 / (1 + sum(rate(ob_sysstat{stat_id="50001",@LABELS}[@INTERVAL])) by (@GBLABELS) / sum(rate(ob_sysstat{stat_id="50000",@LABELS}[@INTERVAL])) by (@GBLABELS))| % |
| 缓存大小 | sum(ob_cache_size_bytes{@LABELS}) by (@GBLABELS) / 1048576| MB |
| RPC 收包吞吐量 | sum(rate(ob_sysstat{stat_id="10001",@LABELS}[@INTERVAL])) by (@GBLABELS)| byte |
| RPC 收包平均耗时 | (sum(rate(ob_sysstat{stat_id="10005",@LABELS}[@INTERVAL])) by (@GBLABELS) + sum(rate(ob_sysstat{stat_id="10006",@LABELS}[@INTERVAL])) by (@GBLABELS)) / sum(rate(ob_sysstat{stat_id="10000",@LABELS}[@INTERVAL])) by (@GBLABELS)| us |
| RPC 发包吞吐量 | sum(rate(ob_sysstat{stat_id="10003",@LABELS}[@INTERVAL])) by (@GBLABELS)| byte |
| RPC 发包平均耗时 | (sum(rate(ob_sysstat{stat_id="10005",@LABELS}[@INTERVAL])) by (@GBLABELS) + sum(rate(ob_sysstat{stat_id="10006",@LABELS}[@INTERVAL])) by (@GBLABELS)) / sum(rate(ob_sysstat{stat_id="10002",@LABELS}[@INTERVAL])) by (@GBLABELS)| us |
| 每秒处理 SQL 语句数 | sum(rate(ob_sysstat{stat_id="40002",@LABELS}[@INTERVAL])) by (@GBLABELS) + sum(rate(ob_sysstat{stat_id="40004",@LABELS}[@INTERVAL])) by (@GBLABELS) + sum(rate(ob_sysstat{stat_id="40006",@LABELS}[@INTERVAL])) by (@GBLABELS) + sum(rate(ob_sysstat{stat_id="40008",@LABELS}[@INTERVAL])) by (@GBLABELS) + sum(rate(ob_sysstat{stat_id="40000",@LABELS}[@INTERVAL])) by (@GBLABELS)| 次/s |
| 服务端每条 SQL 语句平均处理耗时 | (sum(rate(ob_sysstat{stat_id="40003",@LABELS}[@INTERVAL])) by (@GBLABELS) + sum(rate(ob_sysstat{stat_id="40005",@LABELS}[@INTERVAL])) by (@GBLABELS) + sum(rate(ob_sysstat{stat_id="40007",@LABELS}[@INTERVAL])) by (@GBLABELS) + sum(rate(ob_sysstat{stat_id="40009",@LABELS}[@INTERVAL])) by (@GBLABELS) + sum(rate(ob_sysstat{stat_id="40001",@LABELS}[@INTERVAL])) by (@GBLABELS)) /(sum(rate(ob_sysstat{stat_id="40002",@LABELS}[@INTERVAL])) by (@GBLABELS) + sum(rate(ob_sysstat{stat_id="40004",@LABELS}[@INTERVAL])) by (@GBLABELS) + sum(rate(ob_sysstat{stat_id="40006",@LABELS}[@INTERVAL])) by (@GBLABELS) + sum(rate(ob_sysstat{stat_id="40008",@LABELS}[@INTERVAL])) by (@GBLABELS) + sum(rate(ob_sysstat{stat_id="40000",@LABELS}[@INTERVAL])) by (@GBLABELS)) | us |
| 每秒处理 Delete 语句数 | sum(rate(ob_sysstat{stat_id="40008",@LABELS}[@INTERVAL])) by (@GBLABELS)| us |
| 服务端每条 Delete 语句平均处理耗时 | sum(rate(ob_sysstat{stat_id="40009",@LABELS}[@INTERVAL])) by (@GBLABELS) / sum(rate(ob_sysstat{stat_id="40008",@LABELS}[@INTERVAL])) by (@GBLABELS)| us |
| 每秒处理分布式执行计划数 | sum(rate(ob_sysstat{stat_id="40012",@LABELS}[@INTERVAL])) by (@GBLABELS)| 次/s |
| 每秒处理 Insert 语句数 | sum(rate(ob_sysstat{stat_id="40002",@LABELS}[@INTERVAL])) by (@GBLABELS)| 次/s |
| 服务端每条 Insert 语句平均处理耗时 | sum(rate(ob_sysstat{stat_id="40003",@LABELS}[@INTERVAL])) by (@GBLABELS) / sum(rate(ob_sysstat{stat_id="40002",@LABELS}[@INTERVAL])) by (@GBLABELS)| us |
| 每秒处理本地执行数 | sum(rate(ob_sysstat{stat_id="40010",@LABELS}[@INTERVAL])) by (@GBLABELS)| 次/s |
| 每秒处理远程执行计划数 | sum(rate(ob_sysstat{stat_id="40011",@LABELS}[@INTERVAL])) by (@GBLABELS)| 次/s |
| 每秒处理 Replace 语句数 | sum(rate(ob_sysstat{stat_id="40004",@LABELS}[@INTERVAL])) by (@GBLABELS)| 次/s |
| 服务端每条 Replace 语句平均处理耗时 | sum(rate(ob_sysstat{stat_id="40005",@LABELS}[@INTERVAL])) by (@GBLABELS) / sum(rate(ob_sysstat{stat_id="40004",@LABELS}[@INTERVAL])) by (@GBLABELS)| us |
| 每秒处理 Select 语句数 | sum(rate(ob_sysstat{stat_id="40000",@LABELS}[@INTERVAL])) by (@GBLABELS)| 次/s |
| 服务端每条 Select 语句平均处理耗时 | sum(rate(ob_sysstat{stat_id="40001",@LABELS}[@INTERVAL])) by (@GBLABELS) / sum(rate(ob_sysstat{stat_id="40000",@LABELS}[@INTERVAL])) by (@GBLABELS)| us |
| 每秒处理 Update 语句数 | sum(rate(ob_sysstat{stat_id="40006",@LABELS}[@INTERVAL])) by (@GBLABELS)| 次/s |
| 服务端每条 Update 语句平均处理耗时 | sum(rate(ob_sysstat{stat_id="40007",@LABELS}[@INTERVAL])) by (@GBLABELS) / sum(rate(ob_sysstat{stat_id="40006",@LABELS}[@INTERVAL])) by (@GBLABELS)| us |
| 表数量 | max(ob_table_num{@LABELS}) by (@GBLABELS)| - |
| MEMStore 总大小 | sum(ob_sysstat{stat_id="130001",@LABELS}) by (@GBLABELS) / 1048576| MB |
| 每秒处理事务数 | sum(rate(ob_sysstat{stat_id="30005",@LABELS}[@INTERVAL])) by (@GBLABELS)| 次/s |
| 服务端每个事务平均处理耗时 | sum(rate(ob_sysstat{stat_id="30006",@LABELS}[@INTERVAL])) by (@GBLABELS) / sum(rate(ob_sysstat{stat_id="30005",@LABELS}[@INTERVAL])) by (@GBLABELS)| us |
| 每秒提交的事务日志数 | sum(rate(ob_sysstat{stat_id="30002",@LABELS}[@INTERVAL])) by (@GBLABELS)| 次/s |
| 每次事务日志网络同步平均耗时 | sum(rate(ob_sysstat{stat_id="30000",@LABELS}[@INTERVAL])) by (@GBLABELS) / sum(rate(ob_sysstat{stat_id="30001",@LABELS}[@INTERVAL])) by (@GBLABELS)| us |
| 每秒等待事件次数 | sum(rate(ob_waitevent_wait_total{@LABELS}[@INTERVAL])) by (@GBLABELS)| 次/s |
| 等待事件平均耗时 | sum(rate(ob_waitevent_wait_seconds_total{@LABELS}[@INTERVAL])) by (@GBLABELS) / sum(rate(ob_waitevent_wait_total{@LABELS}[@INTERVAL])) by (@GBLABELS)| s |
# 第 6 章:如何测试 OceanBase 社区版性能
## 本章目录
+ [性能测试概述](6.1.md)
+ [影响性能的因素](6.2.md)
+ [如何跑 SYSBENCH 测试](6.3.md)
+ [如何跑 TPC-C 测试](6.4.md)
+ [如何跑 TPC-H 测试](6.5.md)
+ [如何使用 跑业务场景测试](6.6.md)
## 如何联系我们
欢迎广大 OceanBase 爱好者、用户和客户有任何问题联系我们反馈:
+ 社区版官网论坛:[https://open.oceanbase.com/answer](https://open.oceanbase.com/answer)
+ 社区版项目网站提 `Issue`[https://github.com/oceanbase/oceanbase/issues](https://github.com/oceanbase/oceanbase/issues)
+ 钉钉群:群号 `33254054`
\ No newline at end of file
# 性能测试概述
## 测试标准
测试OceanBase集群性能的方法有很多,如`sysbench``benchmarksql`。不过如果想对比OceanBase数据库跟传统数据库、其他分布式数据库产品的性能差异,还需要找到一个能适用彼此的测试方案。`sysbench`的功能太弱,且只有0.4 支持ORACLE,不同版本的sql还不完全一样。`benchmarksql`倒是很合适,有一定业务模型,且标准简单容易统一比较。不过如果客户还想测试一点点业务sql,那就只能自己写程序了。如果嫌写程序麻烦,还有个简单的办法就是使用`JMeter`
## 多副本同步
传统数据库或者基于 MYSQL 或 PG 数据库的分布式数据库,通常都能以单副本实例运行。为了高可用和容灾,也支持一主多备形态部署。不过主备之间同步多是异步同步。异步同步的优点是写性能好,风险是主副本实例故障的时候,主备很可能会不一致。即使使用了 MYSQL 的半同步技术,也不能严格保证主备数据强一致。OceanBase 生产环境默认就是以三副本形态运行,主备副本之前同步的是事务日志,物理同步,同步协议使用 PAXOS 强同步协议。OceanBase 不支持异步同步,不支持牺牲数据安全换取性能。
## 故障应对能力
OceanBase 集群是部署在普通商用服务器上,充分考虑了服务器的故障风险。所以OceanBase 的架构设计始终把数据安全放在首位,平时就是三副本强同步协议,这样任意时刻发生故障,OceanBase都能自动切换,选出新的主副本出来。这个故障切换不需要 DBA 介入,数据库恢复时间 RTO 大概是 30s 左右,数据库恢复后数据绝对不丢 (RPO = 0)。OceanBase 的这个能力平时很难看到,只有发生故障了才能体现。
所以,测试 OceanBase 的性能时需要带着业务压力,并随机注入故障,才是接近实际生产情况。
OceanBase 的在线扩容和缩容、大表在线 DDL,都是有高可用保障。节点故障后恢复时,原有的任务都自动继续进行。
# 影响性能的因素
## 磁盘划分
如果 OBSERVER 运行日志、事务日志、数据文件都在一块盘上,称为单盘部署。这个风险比较大,目前有一些已知问题。
+ 事务日志(CLOG) 空间利用率超过 80% 才开始回收,超过 95% 就节点停写、掉线。
+ 转储和合并时需要额外的 IO、CPU 资源,跟业务读写的 IO 可能会互相影响。
+ 运行日志(LOG)写的太多太快,可能导致节点掉线;LOG 写满可能导致节点异常。
+ 影响 `liboblog` 同步,影响 CLOG 回收或者副本迁移慢等。
+ 参数文件 `observer.config.bin` 因为盘满导致持久化失败。
以上影响 TPC-C 、SYSBENCH 测试多一些,对 TPC-H 影响少一些。
应对策略:
+ CLOG 、LOG、SSTABLE 分多个独立文件系统。不管是多盘还是单盘用 LVM 划分出多个 LV 。
+ 开启 LOG 限流,参数 `syslog_io_bandwidth_limit` 。 SQL: `alter system set syslog_io_bandwidth_limit='10M';`
+ LOG 滚动输出,设置最大数目。SQL: `alter system set enable_syslog_recycle=true; alter system set max_syslog_file_count=50;`
+ CLOG 参数 `clog_disk_usage_limit_percentage` 参数默认 95 ,不要轻易调大。`clog_disk_utilization_threshold` 默认 80,可以下调到 50,但是不能太低。
## OceanBase & OBPROXY 参数
### OceanBase 参数
```sql
alter system set net_thread_count = 32; -- 网络线程数
alter system set _clog_aggregation_buffer_amount=4;
alter system set _flush_clog_aggregation_buffer_timeout='1ms' ; -- 把大概几毫秒之内的日志都聚合到一个rpc中发送,减小网络开销,提高并发读
alter system set trace_log_slow_query_watermark='10s' ; --打印SQL QUERY的阈值
alter system set large_query_threshold='1s' ; -- large query的阈值,超过后进入大查询队列,避免大查询阻塞小查询
alter system set syslog_level='PERF'; -- 控制日志输出级别
alter system set clog_sync_time_warn_threshold='2000ms' ; -- clog日志同步慢的时候触发debug日志的输出。
alter system set syslog_io_bandwidth_limit='10M' ; -- 超过10M会日志限流,减小写日志文件带来的IO消耗
alter system set enable_sql_audit=false -- 关闭sql audit
alter system set enable_perf_event=false; -- 关闭监控项
alter system set clog_max_unconfirmed_log_count=5000; -- 增大滑动窗口,解决clog滑动窗口满导致的性能问题
alter system set minor_warm_up_duration_time=0 ; -- 转储sstable预热时间,转储完成后到预热时间内,所有对应partition的流量会逐步从memtable过渡到sstable,设置0转储之后流量马上切到sstable,使得memtable快速释放 ,内存释放更快。
alter system set clog_transport_compress_all = True; -- 开启clog传输压缩,提高clog做Paxos同步的效率。但是会在clog传输前和接受后增加压缩的开销
alter system set enable_clog_persistence_compress = true; -- 开启clog存储压缩,提高clog落盘效率。这个参数会减少clog落盘的IO,但是会在clog落盘前增加压缩的开销
alter system set memory_chunk_cache_size = '150G'; -- 降低OB内部2MB内存块被OS回收的概率,增大2MB内存块在OB内部的复用率,减少RPC由于内存操作慢而导致超时的风险。
alter system set minor_merge_concurrency = 32; -- 增大转储的线程数,期望提高转储的速度。
alter system set _mini_merge_concurrency = 8; -- 增大mini_merge的线程数,期望提高mini_merge的速度
alter system set freeze_trigger_percentage = 40; -- 触发转储的时机
alter system set autoinc_cache_refresh_interval='86400s'; -- 调大自增列刷新的频率,减少性能损耗。
alter system set cpu_quota_concurrency=2; -- 这个数*租户cpu=工作线程数,具体调整的数值需要根据业务模型和机器配置调整,工作线程超过实际CPU核数也没有意义同时会增加CPU调度和上下文切换的开销
alter system set builtin_db_data_verify_cycle = 0 ; -- 宏块巡检周期参数,当设置为0时关闭巡检
alter system set micro_block_merge_verify_level = 0; -- 微块校验等级设置,0:不做任何校验;1:对encoding做decode校验;2:对encoding做decode校验,对压缩做解压校验
alter system set _ob_get_gts_ahead_interval = '0ms';
alter system set bf_cache_priority = 10; -- 为频繁空查的宏块建立bloomfilter并缓存,减少磁盘IO和CPU消耗,提升写入性能
alter system set user_block_cache_priority=5;
alter system set merge_stat_sampling_ratio = 1 ; -- 合并时统计信息采样率,当设置为0时则关闭统计信息采集
alter system set _enable_static_typing_engine=true;
alter system set autoinc_cache_refresh_interval='86400s'; -- 调大自增列刷新的频率,对自增列和sequence场景有性能帮助
alter system set enable_early_lock_release=false tenant=all; -- 提前解行锁场景下,用于租户级别控制,是否打开该优化
```
### OBPROXY 参数
```sql
alter proxyconfig set enable_ob_protocol_v2=false ;
alter proxyconfig set enable_qos=false ;
alter proxyconfig set enable_compression_protocol=false ;
alter proxyconfig set automatic_match_work_thread = false; -- 关闭自动计算线程个数
alter proxyconfig set work_thread_num = 32; -- 手动设置工作线程个数,需要restart
alter proxyconfig set syslog_level='ERROR';
alter proxyconfig set monitor_log_level = 'ERROR';
alter proxyconfig set enable_monitor_stat = false ;
alter proxyconfig set xflush_log_level="ERROR";
```
### JDBC URL 参数
```bash
conn=jdbc:mysql://x.x.x.x(ip):xx(port)/xxxx(dbname)?rewriteBatchedStatements=true&allowMultiQueries=true&useLocalSessionState=true&useUnicode=true&characterEncoding=utf-8&socketTimeout=3000000
```
+ rewriteBatchedStatements :该参数非常重要,会严重影响导数据效率,不可以忽略。
+ useLocalSessionState :是否使用autocommit,read_only和transaction isolation的内部值(jdbc端的本地值),建议设置为true,如果设置为false,则需要发语句到远端请求,增加发送请求频次,影响性能。
## 转储&合并
转储是租户级别的,当一台服务器中一个租户中MemTable的内存占用超过租户内存上限的一定比例时,就会触发小版本冻结(Minor Freeze)。所谓冻结,就是禁止当前活跃MemTable的写入,并同时创建新的空MemTable以支持新的写入。被冻结后的MemTable就成为一个静态的数据结构,可以被后台线程读取并转储为SSTable。
合并是全局级别的,合并会把当前大版本的SSTable和MemTable与前一个大版本的全量静态数据进行合并,产生新的全量数据。合并操作包含转储操作。
转储和合并是把内存数据刷到磁盘上,存储层统计信息可以更准确,生成的执行计划也就更稳定准确。
MemTable 扫描性能很差,合并后 RANGE 查询性能会有提升。
## PRIMARY_ZONE 设置
租户的 PRIMARY_ZONE 是用来控制租户里主副本的分布。如果设置为 RANDOM,就可以将不同分区的主副本分散在不同 ZONE 的节点上,机器利用率最大化。反过来,设置为某个具体的 ZONE,则只能利用部分节点。
## 使用分区表
分区表是使用一定的水平拆分策略将一个大表切割为多个独立的分区。独立的分区并不是分表,依然是分区表的一部分。
分区表有两个作用:
+ 将海量数据分散到不同分区存储,不同分区可以分散在不同节点存储,从而化解单机空间瓶颈。
+ 将海量请求分散到不同分区,不同分区主副本可以分散在不同节点,从而化解单机处理能力瓶颈。
分区表的索引分为全局索引和本地索引。
全局索引对某些不带分区键的查询比较友好,但是对写不友好,可能导致分布式事务。此外,当 RANGE 查询要返回很多数据时,全局索引也会有很多分布式查询,性能并不一定比本地索引好。
使用本地索引读写数据,可以规避分布式事务,可以多分区多节点并行读,性能也很不错。
## 使用表分组 TABLE GROUP
表分组是将一组业务联系紧密的表的分区分布调度在一起。
+ `table_group` :TABLE GROUP 是一个逻辑概念,它和物理数据文件没有关联关系,TABLE GROUP 只影响表分区的调度方法,OceanBase 数据库会优先把属于同一个 Table Group 的相同分区号的分区,调度到同一台节点上,以减少跨节点分布式事务。
+ `partition_group`: TABLE GROUP 的每个分区表中下标相同的一组分区为 PARTITION GROUP,作用是将分区号相同的partition放到 1 个 PG 的物理结构里面达到提升性能的目的。
同一个表分组里面的分区表的分区策略必须一致。
# 如何跑 SYSBENCH 测试
SYSBENCH 是一个跨平台且支持多线程的模块化基准测试工具,可以执行CPU/内存/线程/IO/数据库等方面的性能测试,用于评估系统在运行高负载的数据库时相关核心参数的性能表现。使用SYSBENCH 是为了绕过复杂的数据库基准设置,甚至在没有安装数据库的前提下,快速了解数据库系统的性能。
SYSBENCH 可以做多种性能测试。本文主要介绍对数据库性能(OLTP)的测试。
## SYSBENCH 安装
### 编译安装
代码下载:`https://github.com/akopytov/sysbench/releases/tag/1.0.20`
解压缩后,编译安装。
```bash
cd sysbench-1.0.20
# yum -y install automake libtool
# yum -y install mysql.x86_64 mysql-devel.x86_64 --allowerasing
# ./autogen.sh
# ./configure --prefix=/usr/sysbench/ --with-mysql-includes=/usr/include/mysql/ --with-mysql-libs=/usr/lib64/mysql/ --with-mysql
# make
# make install
# cp /usr/sysbench/bin/sysbench /usr/local/bin/
# cd /usr/sysbench/
# bin/sysbench --help
```
查看帮助命令,验证 SYSBENCH 是否安装成功:
```bash
[root@obce-0000 ~]# cd /usr/sysbench/
[root@obce-0000 sysbench]# bin/sysbench --help
Usage:
sysbench [options]... [testname] [command]
Commands implemented by most tests: prepare run cleanup help
General options:
--threads=N number of threads to use [1]
--events=N limit for total number of events [0]
--time=N limit for total execution time in seconds [10]
--forced-shutdown=STRING number of seconds to wait after the --time limit before forcing shutdown, or 'off' to disable [off]
--thread-stack-size=SIZE size of stack per thread [64K]
--rate=N average transactions rate. 0 for unlimited rate [0]
--report-interval=N periodically report intermediate statistics with a specified interval in seconds. 0 disables intermediate reports [0]
--report-checkpoints=[LIST,...] dump full statistics and reset all counters at specified points in time. The argument is a list of comma-separated values representing the amount of time in seconds elapsed from start of test when report checkpoint(s) must be performed. Report checkpoints are off by default. []
--debug[=on|off] print more debugging info [off]
--validate[=on|off] perform validation checks where possible [off]
--help[=on|off] print help and exit [off]
--version[=on|off] print version and exit [off]
--config-file=FILENAME File containing command line options
--tx-rate=N deprecated alias for --rate [0]
--max-requests=N deprecated alias for --events [0]
--max-time=N deprecated alias for --time [0]
--num-threads=N deprecated alias for --threads [1]
Pseudo-Random Numbers Generator options:
--rand-type=STRING random numbers distribution {uniform,gaussian,special,pareto} [special]
--rand-spec-iter=N number of iterations used for numbers generation [12]
--rand-spec-pct=N percentage of values to be treated as 'special' (for special distribution) [1]
--rand-spec-res=N percentage of 'special' values to use (for special distribution) [75]
--rand-seed=N seed for random number generator. When 0, the current time is used as a RNG seed. [0]
--rand-pareto-h=N parameter h for pareto distribution [0.2]
Log options:
--verbosity=N verbosity level {5 - debug, 0 - only critical messages} [3]
--percentile=N percentile to calculate in latency statistics (1-100). Use the special value of 0 to disable percentile calculations [95]
--histogram[=on|off] print latency histogram in report [off]
General database options:
--db-driver=STRING specifies database driver to use ('help' to get list of available drivers) [mysql]
--db-ps-mode=STRING prepared statements usage mode {auto, disable} [auto]
--db-debug[=on|off] print database-specific debug information [off]
Compiled-in database drivers:
mysql - MySQL driver
mysql options:
--mysql-host=[LIST,...] MySQL server host [localhost]
--mysql-port=[LIST,...] MySQL server port [3306]
--mysql-socket=[LIST,...] MySQL socket
--mysql-user=STRING MySQL user [sbtest]
--mysql-password=STRING MySQL password []
--mysql-db=STRING MySQL database name [sbtest]
--mysql-ssl[=on|off] use SSL connections, if available in the client library [off]
--mysql-ssl-cipher=STRING use specific cipher for SSL connections []
--mysql-compression[=on|off] use compression, if available in the client library [off]
--mysql-debug[=on|off] trace all client library calls [off]
--mysql-ignore-errors=[LIST,...] list of errors to ignore, or "all" [1213,1020,1205]
--mysql-dry-run[=on|off] Dry run, pretend that all MySQL client API calls are successful without executing them [off]
Compiled-in tests:
fileio - File I/O test
cpu - CPU performance test
memory - Memory functions speed test
threads - Threads subsystem performance test
mutex - Mutex performance test
See 'sysbench <testname> help' for a list of options for each test.
```
### 常见安装问题
可能遇到的报错:
+ `automake 1.10.x (aclocal) wasn’t found, exiting`
解决办法:操作系统没有安装automake,运行命令:yum install automake.noarch,即可安装。
+ `libtoolize 1.4+ wasn’t found, exiting`
解决办法:操作系统没有安装libtool,运行命令:yum install libtool,即可安装。
+ `drv_mysql.c:35:19: fatal error: mysql.h: No such file or directory`
解决办法:操作系统没有安装mysql的开发lib库,运行命令:yum install mysql-devel.x86_64,即可安装。
+ `error while loading shared libraries: libmysqlclient_r.so.16`
这错误通常是直接下载编译好的文件碰到的。
解决办法:将下载文件中的`libmysqlclient_r.so.16.0.0` 复制到目录 `/usr/lib64/mysql` 中,并做一个软链接
```bash
sudo cp libmysqlclient_r.so.16.0.0 /usr/lib64/mysql
sudo ln -s /usr/lib64/mysql/libmysqlclient_r.so.16.0.0 /usr/lib64/mysql/libmysqlclient_r.so.16
sudo ldconfig
```
## 测试准备
### 参数设置
+ OS 参数
```bash
sudo sh -c 'for x in /sys/class/net/eth0/queues/rx-*; do echo ff>$x/rps_cpus; done'
sudo sh -c "echo 32768 > /proc/sys/net/core/rps_sock_flow_entries"
sudo sh -c "echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt"
```
说明:ffffffff表示使用32个核。请根据实际配置修改,例如ECS为8核,则输入ff。
+ OceanBase 参数
SYS 租户参数
```sql
alter system set enable_auto_leader_switch=false;
alter system set enable_one_phase_commit=false;
alter system set enable_monotonic_weak_read = true;
alter system set weak_read_version_refresh_interval='5s';
alter system _ob_minor_merge_schedule_interval='5s';
alter system set memory_limit_percentage = 90; -- OB占系统总内存的比例,提高OB可用的内存量。
alter system set memstore_limit_percentage = 55; -- memstore占租户的内存比,尽量增大memstore的空间(但是可能对读操作有负面影响)。
alter system set freeze_trigger_percentage = 70; -- 启动major/minor freeze的时机,让转储(minor freeze)尽早启动,memstore内存尽早释放。
alter system set minor_freeze_times = 50; -- minor freeze的次数,尽量不在测试期间触发major freeze。
alter system set minor_warm_up_duration_time = 0; -- 加快minor freeze
alter system set merge_thread_count = 32; -- 增大合并的线程数。
alter system set minor_merge_concurrency = 8; -- 增大转储的线程数,期望提高转储的速度。
alter system set _mini_merge_concurrency = 4; -- 增大mini_merge的线程数,期望提高mini_merge的速度(默认值为3)。调大为8以后,发现会导致压测中CPU使用率有时飙升至90%,对性能有影响。
```
PROXY 参数
```sql
alter proxyconfig set proxy_mem_limited='4G'; --防止oom,可根据实际环境动态调整
alter proxyconfig set enable_compression_protocol=false; --关闭压缩,降低cpu%
alter proxyconfig set work_thread_num=32; -- 调整工作线程数,寻找最优性能
alter proxyconfig set enable_compression_protocol=false;
alter proxyconfig set enable_metadb_used=false;
alter proxyconfig set enable_standby=false;
alter proxyconfig set enable_strict_stat_time=false;
alter proxyconfig set use_local_dbconfig=true;
```
租户参数
```sql
数据库下租户设置,防止事务超时
set global ob_timestamp_service='GTS' ;
set global autocommit=ON;
set global ob_query_timeout=36000000000;
set global ob_trx_timeout=36000000000;
set global max_allowed_packet=67108864;
set global ob_sql_work_area_percentage=100;
set global parallel_max_servers=800;
set global parallel_servers_target=800;
```
### 数据准备
+ 准备数据库账户
```sql
create database sysbenchdb;
grant all privileges on sysbenchdb.* to u_sysbench identified by '123456';
```
+ (可选)修改建表语句
建表语句在 `share/sysbench/oltp_common.lua` 里。
```bash
cd /usr/sysbench/
vim share/sysbench/oltp_common.lua +150
150 function create_table(drv, con, table_num)
```
如果想建分区表,就修改这里的脚本。分区表需要选择一个分区键,并且主键要包含分区键。
+ 初始化表和数据
SYSBENCH 参数说明:
- `--tables` :指定表的数量
- `--table_size` :指定表的数据量
- `--threads`:指定并发数
- `--mysql-ignore-errors`:指定忽略的错误号,忽略后就继续跑。否则,报错就中断。
- `--time` : 指定运行时间。
- `--report-interval`:报告间隔。
示例:
```bash
sysbench --test=./oltp_read_only.lua --mysql-host=172.20.249.54 --mysql-port=2883 --mysql-db=sysbenchdb --mysql-user="obce-3zones:obmysql:u_sysbench" --mysql-password=123456 --tables=12 --table_size=10000000 --threads=12 --time=300 --report-interval=3 --db-driver=mysql --db-ps-mode=disable --skip-trx=on --mysql-ignore-errors=6002,6004,4012,2013,4016,1062 prepare
输出:
WARNING: the --test option is deprecated. You can pass a script name or path on the command line without any options.
sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)
Initializing worker threads...
Creating table 'sbtest3'...
Creating table 'sbtest1'...
Creating table 'sbtest6'...
Creating table 'sbtest7'...
Creating table 'sbtest5'...
Creating table 'sbtest2'...
Creating table 'sbtest8'...
Creating table 'sbtest4'...
Inserting 1000000 records into 'sbtest3'
Inserting 1000000 records into 'sbtest1'
Inserting 1000000 records into 'sbtest6'
Inserting 1000000 records into 'sbtest5'
Inserting 1000000 records into 'sbtest7'
Inserting 1000000 records into 'sbtest8'
Inserting 1000000 records into 'sbtest4'
Inserting 1000000 records into 'sbtest2'
Creating a secondary index on 'sbtest6'...
Creating a secondary index on 'sbtest8'...
Creating a secondary index on 'sbtest3'...
Creating a secondary index on 'sbtest5'...
Creating a secondary index on 'sbtest4'...
Creating a secondary index on 'sbtest1'...
Creating a secondary index on 'sbtest7'...
Creating a secondary index on 'sbtest2'...
Creating table 'sbtest9'...
Inserting 1000000 records into 'sbtest9'
Creating a secondary index on 'sbtest9'...
[root@obce-0000 sysbench]#
```
常见初始化问题:
+ `FATAL: mysql_drv_query() returned error 4030 (Over tenant memory limits) for query 'INSERT INTO sbtest5`
这就是典型的增量内存消耗速度高于转储速度。如果租户内存很小的话,这个写入的并发不能太高。
上面初始化配置供参考:
```sql
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
| resource_pool_name | unit_config_name | max_cpu | min_cpu | max_mem_gb | min_mem_gb | unit_id | zone | observer | tenant_id | tenant_name |
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 1 | zone1 | 172.20.249.52:2882 | 1 | sys |
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 2 | zone2 | 172.20.249.49:2882 | 1 | sys |
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 3 | zone3 | 172.20.249.51:2882 | 1 | sys |
| my_pool_zone1 | unit1 | 9 | 9 | 19 | 19 | 1001 | zone1 | 172.20.249.52:2882 | 1002 | obmysql |
| my_pool_zone2 | unit1 | 9 | 9 | 19 | 19 | 1002 | zone2 | 172.20.249.49:2882 | 1002 | obmysql |
| my_pool_zone3 | unit1 | 9 | 9 | 19 | 19 | 1003 | zone3 | 172.20.249.51:2882 | 1002 | obmysql |
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
6 rows in set (0.042 sec)
alter system set merge_thread_count = 32; -- 增大合并的线程数。
alter system set minor_merge_concurrency = 8; -- 增大转储的线程数,期望提高转储的速度。
alter system set _mini_merge_concurrency = 4; -- 增大mini_merge的线程数,期望提高mini_merge的速度(默认值为3)。调大为8以后,发现会导致压测中CPU使用率有时飙升至90%,对性能有影响。
alter system set memory_limit = '24G'; -- OB占系统总内存的比例,提高OB可用的内存量。
alter system set memstore_limit_percentage = 55; -- memstore占租户的内存比,尽量增大memstore的空间(但是可能对读操作有负面影响)。
alter system set freeze_trigger_percentage = 70; -- 启动major/minor freeze的时机,让转储(minor freeze)尽早启动,memstore内存尽早释放。
alter system set minor_freeze_times = 50; -- minor freeze的次数,尽量不在测试期间触发major freeze。
alter system set minor_warm_up_duration_time = 0; -- 加快minor freeze
```
## 场景测试
不同场景使用不同的 LUA 脚本文件。
```bash
[root@obce-0000 sysbench]# pwd
/usr/sysbench/share/sysbench
[root@obce-0000 sysbench]# ls *.lua
bulk_insert.lua oltp_delete.lua oltp_point_select.lua oltp_read_write.lua oltp_update_non_index.lua select_random_points.lua
oltp_common.lua oltp_insert.lua oltp_read_only.lua oltp_update_index.lua oltp_write_only.lua select_random_ranges.lua
```
复制初始化脚本命令,替换场景文件名,`prepare` 改为 `run`
SYSBENCH 的场景测试,建议同一个场景设置不同的并发都跑一遍,然后用 EXCEL 记录相应的性能值,连成曲线,观察线性扩展关系。
+ 只读测试
只读测试的 SQL 都是读。OceanBase 的读默认是强一致性读,只读取数据分区的主副本。如果要观察 OceanBase 的读写分离能力,可以修改这个测试脚本,在读 SQL 里增加弱一致性读 Hint(`/*+ read_consistency(weak) */` ) 即可随机读取备副本。
稍微不同的是,OceanBase 的弱一致性读备副本还受参数 `max_stale_time_for_weak_consistency` 限制。如果备副本延迟时间超出这个参数定义(默认 5s),则备副本不提供读服务。
```bash
sysbench --test=./oltp_read_only.lua --mysql-host=172.20.249.52 --mysql-port=2883 --mysql-db=sysbenchdb --mysql-user="obce-3zones:obmysql:u_sysbench" --mysql-password=123456 --tables=9 --table_size=1000000 --threads=8 --time=300 --report-interval=60 --db-driver=mysql --db-ps-mode=disable --skip-trx=on --mysql-ignore-errors=6002,6004,4012,2013,4016,1062 run
输出:
WARNING: the --test option is deprecated. You can pass a script name or path on the command line without any options.
sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)
Running the test with following options:
Number of threads: 8
Report intermediate results every 60 second(s)
Initializing random number generator from current time
Initializing worker threads...
Threads started!
[ 60s ] thds: 8 tps: 114.73 qps: 1607.23 (r/w/o: 1607.23/0.00/0.00) lat (ms,95%): 179.94 err/s: 0.00 reconn/s: 0.00
[ 120s ] thds: 8 tps: 79.95 qps: 1119.26 (r/w/o: 1119.26/0.00/0.00) lat (ms,95%): 262.64 err/s: 0.00 reconn/s: 0.00
[ 180s ] thds: 8 tps: 97.37 qps: 1362.75 (r/w/o: 1362.75/0.00/0.00) lat (ms,95%): 215.44 err/s: 0.00 reconn/s: 0.00
[ 240s ] thds: 8 tps: 109.87 qps: 1538.27 (r/w/o: 1538.27/0.00/0.00) lat (ms,95%): 193.38 err/s: 0.00 reconn/s: 0.00
[ 300s ] thds: 8 tps: 193.03 qps: 2702.82 (r/w/o: 2702.82/0.00/0.00) lat (ms,95%): 95.81 err/s: 0.00 reconn/s: 0.00
SQL statistics:
queries performed:
read: 499870
write: 0
other: 0
total: 499870
transactions: 35705 (118.99 per sec.)
queries: 499870 (1665.81 per sec.)
ignored errors: 0 (0.00 per sec.)
reconnects: 0 (0.00 per sec.)
General statistics:
total time: 300.0755s
total number of events: 35705
Latency (ms):
min: 11.09
avg: 67.22
max: 1347.51
95th percentile: 179.94
sum: 2400071.63
Threads fairness:
events (avg/stddev): 4463.1250/52.63
execution time (avg/stddev): 300.0090/0.03
```
+ 纯写场景
纯写场景就是 `insert``update``delete` SQL 都有。如果表是分区表,大概率会有跨节点的分布式事务。OceanBase 的分布式事务是强一致性模型,所以在并发不高节点规模不大时,相比传统数据库,这个测试性能会相对差一些。随着数据量的增长,并发的提升以及机器规模的增长,OceanBase 的多活和线性扩展优势会逐步体现出来。
```bash
sysbench --test=./oltp_write_only.lua --mysql-host=172.20.249.52 --mysql-port=2883 --mysql-db=sysbenchdb --mysql-user="obce-3zones:obmysql:u_sysbench" --mysql-password=123456 --tables=9 --table_size=1000000 --threads=8 --time=300 --report-interval=60 --db-driver=mysql --db-ps-mode=disable --skip-trx=on --mysql-ignore-errors=6002,6004,4012,2013,4016 run
```
+ 读写混合场景
```bash
sysbench --test=./oltp_read_write.lua --mysql-host=172.20.249.52 --mysql-port=2883 --mysql-db=sysbenchdb --mysql-user="obce-3zones:obmysql:u_sysbench" --mysql-password=123456 --tables=9 --table_size=1000000 --threads=8 --time=300 --report-interval=60 --db-driver=mysql --db-ps-mode=disable --skip-trx=on --mysql-ignore-errors=6002,6004,4012,2013,4016,1062 run
```
## 性能调优经验
### 转储&合并
对sysbench来说(1000000*30 行表),sysbench一般来说数据量较小,导完数据后不会发生转储,此时做合并有消极影响。
影响在于:合并转储将 `memtable` 刷到硬盘,多了一个 `major sstable`,从硬盘操作数据肯定比在 `memtable`要慢(`memtable scan` 另说),导致性能下降。
| | 合并转储前(qps/rt) | 转储后(qps/rt) | 合并后(qps/rt) | |
|---------------|-----------------|-----------------|-----------------|----|
| point_select | 367240.41/7.70 | 332106.87/8.28 | 342172.34/8.43 | |
| write_only | 137125.60/81.48 | 138654.65/84.47 | 135978.23/84.47 | |
| 3千万行单表range查询 | 18.601s | 10.853s | 10.709s | |
### 调整 PRIMARY_ZONE
| | 集中式部署(zone1) | random部署 |
|------------------------|-----------------|------------------|
| sysbench(point_select) | 261505.51/11.87 | 373923.98/7.56 |
| sysbench(read_write) | 81818.40/297.92 | 125315.97/427.07 |
# 如何跑 TPC-C 测试
TPC(Transaction Processing Performance Council,事务处理性能委员会)是由数十家会员公司创建的非盈利组织,总部设在美国。 TPC的成员主要是计算机软硬件厂家,而非计算机用户,其功能是制定商务应用基准程序的标准规范、性能和价格度量,并管理测试结果的发布。
TPC-C是TPC(事务处理性能委员会)推出的一系列性能测试标准中的一款,自1992年推出,便成为了数据库性能测试的标杆,各个数据库大厂都向TPC委员会提交了测试结果,以期在TPC-C测试的排行榜上拥有一席之地。2019 年之前,TPC-C 榜首一直是 ORACLE 公司,持续 8 年多。2019 年 9 月底,OceanBase 以分布式数据库身份参与 TPC-C 评测,夺得榜首成绩。2020 年 5 月,OceanBase 再次参与 TPC-C 评测,刷新了之前的成绩(提升了 10 倍)。
OceanBase 参加第一次 TPC-C 使用的是云服务器,规模在204 台。第二次依然是云服务器,规模在 1500台左右。这是由于 TPC-C 的标准定义非常严格,对数据量、业务读写行为要求都很具体,导致机器规模数很大。普通的企业测试肯定不能采取这个标准。业界参照 TPC-C 的业务模型和标准有类似的测试程序开源。如 BenchmarkSQL 。
## TPC-C 简介
### 数据库模型
在测试开始前,TPC-C Benchmark规定了数据库的初始状态,也就是数据库中数据生成的规则,其中ITEM表中固定包含10万种商品,仓库的数量可进行调整,假设WAREHOUSE表中有W条记录,那么:
+ STOCK 表中应有 W×10万 条记录(每个仓库对应 10万 种商品的库存数据);
+ DISTRICT 表中应有 W×10 条记录(每个仓库为 10 个地区提供服务);
+ CUSTOMER 表中应有 W×10×3000 条记录(每个地区有 3000 个客户);
+ HISTORY 表中应有 W×10×3000 条记录(每个客户一条交易历史);
+ ORDER 表中应有 W×10×3000 条记录(每个地区 3000 个订单),并且最后生成的 900 个订单被添加到 NEW-ORDER 表中,每个订单随机生成 5~15 条 ORDER-LINE 记录。
在测试过程中,每一个地区(DISTRICT)都有一个对应的终端(Terminal),模拟为用户提供服务。在每个终端的生命周期内,要循环往复地执行各类事务,每个事务的流程如图所示,当终端执行完一个事务的周期后,就进入下一个事务的周期,如下图所示。
![tpcc](media/16330578400106.jpg)
客户下单后,包含若干个订单明细(ORDER-LINE)的订单(ORDER)被生成,并被加入新订单(NEW-ORDER)列表。
客户对订单支付还会产生交易历史(HISTORY)。
每个订单(ORDER) 平均包含10条订单项(ORDER-LINE), 其中1% 需要从远程仓库中获取.
这些就是TPC-C模型中的9个数据表。其中,仓库的数量W可以根据系统的实际情况进行调整,以使系统性能测试结果达到最佳。
### 事务类型
事务类型
该benchmark包含5类事务
+ NewOrder: 新订单的生成从某一仓库随机选取5-15件商品, 创建新订单. 其中1%的事务需要回滚(即err). 一般来说新订单请求不可能超出全部事务请求的45% |
+ Payment : 订单付款更新客户账户余额, 反映其支付情况. 占比 43%
+ OrderStatus : 最近订单查询 随机显示一个用户显示其最有益条订单, 显示该订单内的每个商品状态. 占比4%
+ Delivery : 配送, 模拟批处理交易更新该订单用户的余额, 把发货单从neworder中删除. 占比4%
+ StockLevel : 库存缺货状态分析 , 占比4%
## 软件准备
### BenchmarkSQL 下载
BenchmarkSQL 是开源的项目,官方下载地址是:[https://sourceforge.net/projects/benchmarksql/](https://sourceforge.net/projects/benchmarksql/)
OceanBase 团队对这个程序稍微修改了一下,允许导入数据报错的时候,能只针对报错的仓库进行补充加载,而不是所有数据重新导入,节省测试时间。该版本可以向 OceanBase 技术支持人员获取。或者直接从 [https://github.com/obpilot/benchmarksql-5.0] 下载。
下载后直接使用。需要有 JAVA 运行环境,版本不低于 1.8.0 。
```bash
[root@obce-0000 bmsql_vivid]# pwd
/root/bmsql_vivid
[root@obce-0000 bmsql_vivid]# tree -L 1
.
├── build
├── build.xml
├── dist
├── doc
├── HOW-TO-RUN.txt
├── lib
├── README.md
├── run
└── src
6 directories, 3 files
[root@obce-0000 bmsql_vivid]#
```
### 配置文件
配置文件 `props.ob``run` 目录下。。
```
db=oracle
driver=com.alipay.oceanbase.jdbc.Driver
// conn=jdbc:oceanbase://122.71.221.125:2883/icbc?useUnicode=true&characterEncoding=utf-8
conn=jdbc:oceanbase://172.20.249.52:2883/tpccdb?useUnicode=true&characterEncoding=utf-8&rewriteBatchedStatements=true&allowMultiQueries=true
user=u_tpcc@obmysql#obce-3zones
password=123456
warehouses=10
loadWorkers=2
//fileLocation=/data/temp/
terminals=10
//To run specified transactions per terminal- runMins must equal zero
runTxnsPerTerminal=0
//To run for specified minutes- runTxnsPerTerminal must equal zero
runMins=10
//Number of total transactions per minute
limitTxnsPerMin=0
//Set to true to run in 4.x compatible mode. Set to false to use the
//entire configured database evenly.
terminalWarehouseFixed=true
//The following five values must add up to 100
newOrderWeight=45
paymentWeight=43
orderStatusWeight=4
deliveryWeight=4
stockLevelWeight=4
// Directory name to create for collecting detailed result data.
// Comment this out to suppress.
resultDirectory=my_result_%tY-%tm-%td_%tH%tM%tS
osCollectorScript=./misc/os_collector_linux.py
osCollectorInterval=1
//osCollectorSSHAddr=user@dbhost
//osCollectorDevices=net_eth0 blk_sda
//LoadStartW=1
//LoadStopW=1000
```
说明:
+ `db` 指定数据库类型。这里复用 `oracle` 的类型。后面 OceanBase 相应的 LIBRARY 放到对应目录下 `lib/`
+ `warehouses` 指定仓库数。通常仓库数就决定了这个性能测试理论上的成绩。如果期望测试结果越高,仓库数就不能太低。生产环境机器测试,建议 5000 仓库起步。机器配置差的话,建议 100 仓起步。
+ `loadWorkers` 指定仓库数据加载时的并发。如果机器配置很好,这个可以设置大一些,比如说 100 个。 如果机器配置不高(尤其是内存),这个就小一些,如 10个 并发。并发指定的过高,可能导致内存消耗的太快,出现报错,导致数据加载前功尽弃。第一次使用时,建议就并发低一些,宁可慢一点,也不要报错。
+ `terminals` 指定性能压测时的并发数。这个并发数不要高于仓库数*10 。否则,有不必要的锁等待。生产环境的经验,这个并发数能到 1000 就很高了。一般环境测试建议从 100 开始。
+ `runMins` 指定性能测试持续的时间。时间越久,越能考验数据库的性能和稳定性。建议不要少于 10 分钟。生产环境机器建议不少于 1 小时。
+ `LoadStartW``LoadStopW` 指定补仓的时候,开始值和截止值。如果导数据发现某个仓库数据导入失败(大事务超时),则可以指定这个仓库重新导入。
### 库文件
OceanBase 提供自己的驱动文件 `oceanbase-client-x.x.x.jar` 文件。不管是测试 MySQL 租户还是 ORACLE 租户,都可以使用这个驱动文件。
驱动文件的读取是脚本 `run/func.sh` 里的逻辑。
```bash
# ----
# getCP()
#
# Determine the CLASSPATH based on the database system.
# ----
function setCP()
{
case "$(getProp db)" in
firebird)
cp="../lib/firebird/*:../lib/*"
;;
oracle)
cp="../lib/oracle/*"
if [ ! -z "${ORACLE_HOME}" -a -d ${ORACLE_HOME}/lib ] ; then
cp="${cp}:${ORACLE_HOME}/lib/*"
fi
cp="${cp}:../lib/*"
;;
postgres)
cp="../lib/postgres/*:../lib/*"
;;
oceanbase)
# cp="../lib/oceanbase/oceanbase-client-1.0.1.jar:../lib/oceanbase/guava-parent-18.0-site.jar:../lib/*"
cp="../lib/oceanbase/*:../lib/*"
;;
esac
myCP=".:${cp}:../dist/*"
export myCP
}
```
所以只要把 OceanBase 的驱动放到 `lib/` 目录或 `lib/oracle` 下即可。
```bash
[root@obce-0000 bmsql_vivid]# tree lib/
lib/
├── apache-log4j-extras-1.1.jar
├── firebird
│   ├── connector-api-1.5.jar
│   └── jaybird-2.2.9.jar
├── gsjdbc4.jar
├── log4j-1.2.17.jar
├── mysql-connector-java-5.1.47.jar
├── oceanbase
│   ├── commons-lang-2.3.jar
│   ├── guava-18.0.jar
│   ├── json-20160810.jar
│   ├── oceanbaseclient1.1.10.jar
│   └── toolkit-common-logging-1.10.jar
├── oracle
│   ├── oceanbaseclient1.1.10.jar
│   └── README.txt
└── postgres
└── postgresql-9.3-1102.jdbc41.jar
4 directories, 14 files
```
## 数据准备
### 建表
建表脚本通常放在 `run/sql.common` 下或者 其他指定目录也行。
建表脚本可以选择非分区表方案 和 分区表方案。
+ 非分区表
```sql
[root@obce-0000 run]# sh runSQL.sh props.ob sql.common/tableCreates.sql
# ------------------------------------------------------------
# Loading SQL file sql.common/tableCreates.sql
# ------------------------------------------------------------
.:../lib/oracle/*:../lib/*:../dist/*
-Dprop=props.ob -DcommandFile=sql.common/tableCreates.sql
create table bmsql_config (
cfg_name varchar(30) primary key,
cfg_value varchar(50)
);
create tablegroup tpcc_group ;
create table bmsql_warehouse (
w_id integer not null,
w_ytd decimal(12,2),
w_tax decimal(4,4),
w_name varchar(10),
w_street_1 varchar(20),
w_street_2 varchar(20),
w_city varchar(20),
w_state char(2),
w_zip char(9),
primary key(w_id)
)tablegroup=tpcc_group;
create table bmsql_district (
d_w_id integer not null,
d_id integer not null,
d_ytd decimal(12,2),
d_tax decimal(4,4),
d_next_o_id integer,
d_name varchar(10),
d_street_1 varchar(20),
d_street_2 varchar(20),
d_city varchar(20),
d_state char(2),
d_zip char(9),
PRIMARY KEY (d_w_id, d_id)
)tablegroup=tpcc_group ;
create table bmsql_customer (
c_w_id integer not null,
c_d_id integer not null,
c_id integer not null,
c_discount decimal(4,4),
c_credit char(2),
c_last varchar(16),
c_first varchar(16),
c_credit_lim decimal(12,2),
c_balance decimal(12,2),
c_ytd_payment decimal(12,2),
c_payment_cnt integer,
c_delivery_cnt integer,
c_street_1 varchar(20),
c_street_2 varchar(20),
c_city varchar(20),
c_state char(2),
c_zip char(9),
c_phone char(16),
c_since timestamp,
c_middle char(2),
c_data varchar(500),
PRIMARY KEY (c_w_id, c_d_id, c_id)
)tablegroup=tpcc_group ;
create table bmsql_history (
hist_id integer,
h_c_id integer,
h_c_d_id integer,
h_c_w_id integer,
h_d_id integer,
h_w_id integer,
h_date timestamp,
h_amount decimal(6,2),
h_data varchar(24)
)tablegroup=tpcc_group ;
create table bmsql_new_order (
no_w_id integer not null ,
no_d_id integer not null,
[detached from 1248.pts-0.obce-0000]
[root@obce-0000 run]# cat sql.common/tableCreates.sql
create table bmsql_config (
cfg_name varchar(30) primary key,
cfg_value varchar(50)
);
create tablegroup tpcc_group ;
create table bmsql_warehouse (
w_id integer not null,
w_ytd decimal(12,2),
w_tax decimal(4,4),
w_name varchar(10),
w_street_1 varchar(20),
w_street_2 varchar(20),
w_city varchar(20),
w_state char(2),
w_zip char(9),
primary key(w_id)
)tablegroup=tpcc_group;
create table bmsql_district (
d_w_id integer not null,
d_id integer not null,
d_ytd decimal(12,2),
d_tax decimal(4,4),
d_next_o_id integer,
d_name varchar(10),
d_street_1 varchar(20),
d_street_2 varchar(20),
d_city varchar(20),
d_state char(2),
d_zip char(9),
PRIMARY KEY (d_w_id, d_id)
)tablegroup=tpcc_group ;
create table bmsql_customer (
c_w_id integer not null,
c_d_id integer not null,
c_id integer not null,
c_discount decimal(4,4),
c_credit char(2),
c_last varchar(16),
c_first varchar(16),
c_credit_lim decimal(12,2),
c_balance decimal(12,2),
c_ytd_payment decimal(12,2),
c_payment_cnt integer,
c_delivery_cnt integer,
c_street_1 varchar(20),
c_street_2 varchar(20),
c_city varchar(20),
c_state char(2),
c_zip char(9),
c_phone char(16),
c_since timestamp,
c_middle char(2),
c_data varchar(500),
PRIMARY KEY (c_w_id, c_d_id, c_id)
)tablegroup=tpcc_group ;
create table bmsql_history (
hist_id integer,
h_c_id integer,
h_c_d_id integer,
h_c_w_id integer,
h_d_id integer,
h_w_id integer,
h_date timestamp,
h_amount decimal(6,2),
h_data varchar(24)
)tablegroup=tpcc_group ;
create table bmsql_new_order (
no_w_id integer not null ,
no_d_id integer not null,
no_o_id integer not null,
PRIMARY KEY (no_w_id, no_d_id, no_o_id)
)tablegroup=tpcc_group ;
create table bmsql_oorder (
o_w_id integer not null,
o_d_id integer not null,
o_id integer not null,
o_c_id integer,
o_carrier_id integer,
o_ol_cnt integer,
o_all_local integer,
o_entry_d timestamp,
PRIMARY KEY (o_w_id, o_d_id, o_id)
)tablegroup=tpcc_group ;
create table bmsql_order_line (
ol_w_id integer not null,
ol_d_id integer not null,
ol_o_id integer not null,
ol_number integer not null,
ol_i_id integer not null,
ol_delivery_d timestamp,
ol_amount decimal(6,2),
ol_supply_w_id integer,
ol_quantity integer,
ol_dist_info char(24),
PRIMARY KEY (ol_w_id, ol_d_id, ol_o_id, ol_number)
)tablegroup=tpcc_group ;
create table bmsql_item (
i_id integer not null,
i_name varchar(24),
i_price decimal(5,2),
i_data varchar(50),
i_im_id integer,
PRIMARY KEY (i_id)
)tablegroup=tpcc_group;
create table bmsql_stock (
s_w_id integer not null,
s_i_id integer not null,
s_quantity integer,
s_ytd integer,
s_order_cnt integer,
s_remote_cnt integer,
s_data varchar(50),
s_dist_01 char(24),
s_dist_02 char(24),
s_dist_03 char(24),
s_dist_04 char(24),
s_dist_05 char(24),
s_dist_06 char(24),
s_dist_07 char(24),
s_dist_08 char(24),
s_dist_09 char(24),
s_dist_10 char(24),
PRIMARY KEY (s_w_id, s_i_id)
)tablegroup=tpcc_group;
```
+ 分区表语法
分区表是一种水平拆分方案,大部分表安装仓库ID 做 HASH 分区。分区数取决于要测试的数据规模和机器数。如果只有三台机器,分区数以 3-9 个为宜。如果是 5000仓,9台机器之类,那分区数可以调整到 99 或 100 都行。通常来说 HASH 分区数没必要过 100 。
```sql
[root@obce-0000 run]# cat sql.common/tableCreates_parts.sql
create table bmsql_config (
cfg_name varchar(30) primary key,
cfg_value varchar(50)
);
-- drop tablegroup tpcc_group;
create tablegroup tpcc_group partition by hash partitions 3;
create table bmsql_warehouse (
w_id integer not null,
w_ytd decimal(12,2),
w_tax decimal(4,4),
w_name varchar(10),
w_street_1 varchar(20),
w_street_2 varchar(20),
w_city varchar(20),
w_state char(2),
w_zip char(9),
primary key(w_id)
)tablegroup='tpcc_group' partition by hash(w_id) partitions 3;
create table bmsql_district (
d_w_id integer not null,
d_id integer not null,
d_ytd decimal(12,2),
d_tax decimal(4,4),
d_next_o_id integer,
d_name varchar(10),
d_street_1 varchar(20),
d_street_2 varchar(20),
d_city varchar(20),
d_state char(2),
d_zip char(9),
PRIMARY KEY (d_w_id, d_id)
)tablegroup='tpcc_group' partition by hash(d_w_id) partitions 3;
create table bmsql_customer (
c_w_id integer not null,
c_d_id integer not null,
c_id integer not null,
c_discount decimal(4,4),
c_credit char(2),
c_last varchar(16),
c_first varchar(16),
c_credit_lim decimal(12,2),
c_balance decimal(12,2),
c_ytd_payment decimal(12,2),
c_payment_cnt integer,
c_delivery_cnt integer,
c_street_1 varchar(20),
c_street_2 varchar(20),
c_city varchar(20),
c_state char(2),
c_zip char(9),
c_phone char(16),
c_since timestamp,
c_middle char(2),
c_data varchar(500),
PRIMARY KEY (c_w_id, c_d_id, c_id)
)tablegroup='tpcc_group' partition by hash(c_w_id) partitions 3;
create table bmsql_history (
hist_id integer,
h_c_id integer,
h_c_d_id integer,
h_c_w_id integer,
h_d_id integer,
h_w_id integer,
h_date timestamp,
h_amount decimal(6,2),
h_data varchar(24)
)tablegroup='tpcc_group' partition by hash(h_w_id) partitions 3;
create table bmsql_new_order (
no_w_id integer not null ,
no_d_id integer not null,
no_o_id integer not null,
PRIMARY KEY (no_w_id, no_d_id, no_o_id)
)tablegroup='tpcc_group' partition by hash(no_w_id) partitions 3;
create table bmsql_oorder (
o_w_id integer not null,
o_d_id integer not null,
o_id integer not null,
o_c_id integer,
o_carrier_id integer,
o_ol_cnt integer,
o_all_local integer,
o_entry_d timestamp,
PRIMARY KEY (o_w_id, o_d_id, o_id)
)tablegroup='tpcc_group' partition by hash(o_w_id) partitions 3;
create table bmsql_order_line (
ol_w_id integer not null,
ol_d_id integer not null,
ol_o_id integer not null,
ol_number integer not null,
ol_i_id integer not null,
ol_delivery_d timestamp,
ol_amount decimal(6,2),
ol_supply_w_id integer,
ol_quantity integer,
ol_dist_info char(24),
PRIMARY KEY (ol_w_id, ol_d_id, ol_o_id, ol_number)
)tablegroup='tpcc_group' partition by hash(ol_w_id) partitions 3;
create table bmsql_item (
i_id integer not null,
i_name varchar(24),
i_price decimal(5,2),
i_data varchar(50),
i_im_id integer,
PRIMARY KEY (i_id)
) duplicate_scope='cluster';
create table bmsql_stock (
s_w_id integer not null,
s_i_id integer not null,
s_quantity integer,
s_ytd integer,
s_order_cnt integer,
s_remote_cnt integer,
s_data varchar(50),
s_dist_01 char(24),
s_dist_02 char(24),
s_dist_03 char(24),
s_dist_04 char(24),
s_dist_05 char(24),
s_dist_06 char(24),
s_dist_07 char(24),
s_dist_08 char(24),
s_dist_09 char(24),
s_dist_10 char(24),
PRIMARY KEY (s_w_id, s_i_id)
)tablegroup='tpcc_group' use_bloom_filter=true partition by hash(s_w_id) partitions 3;
```
运行命令建表。
```bash
sh runSQL.sh props.ob sql.common/tableCreates_parts.sql
输出:
[root@obce-0000 run]# sh runSQL.sh props.ob sql.common/tableCreates_parts.sql
# ------------------------------------------------------------
# Loading SQL file sql.common/tableCreates_parts.sql
# ------------------------------------------------------------
.:../lib/oracle/*:../lib/*:../dist/*
-Dprop=props.ob -DcommandFile=sql.common/tableCreates_parts.sql
<.....>
```
### 加载数据
加载数据就是做数据初始化,仓库数越多时间越长。1000 仓可能要近 1个小时,5000 仓可能要半天以上。具体性能取决于机器配置。
```bash
sh runLoader.sh props.ob
输出:
[root@obce-0000 run]# sh runLoader.sh props.ob
Starting BenchmarkSQL LoadData
driver=com.alipay.oceanbase.jdbc.Driver
conn=jdbc:oceanbase://172.20.249.52:2883/tpccdb?useUnicode=true&characterEncoding=utf-8&rewriteBatchedStatements=true&allowMultiQueries=true
user=u_tpcc@obmysql#obce-3zones
password=***********
warehouses=10
loadWorkers=2
fileLocation (not defined)
csvNullValue (not defined - using default 'NULL')
LoadStartW (not defined)
LoadStopW (not defined)
Worker 000: Loading ITEM
Worker 001: Loading Warehouse 1
Worker 000: Loading ITEM done
Worker 000: Loading Warehouse 2
<.....>
```
加载数据的 INSERT SQL 都使用了 `batch insert` 特性。这点是在 `props.ob` 里的 JDBC URL 里指定的。这个写入性能最好。
### 创建索引
当数据初始化完后再补充两个索引。
```sql
[root@obce-0000 run]# cat sql.common/indexCreates.sql
create index bmsql_customer_idx1
on bmsql_customer (c_w_id, c_d_id, c_last, c_first) local;
create index bmsql_oorder_idx1
on bmsql_oorder (o_w_id, o_d_id, o_carrier_id, o_id) local;
```
### (可选)删除表
删除表就是删除所有的表和表分组。当需要修改表结构分区数的时候执行这个。
```bash
[root@obce-0000 run]# cat sql.common/tableDrops.sql
drop table bmsql_config;
drop table bmsql_new_order;
drop table bmsql_order_line;
drop table bmsql_oorder;
drop table bmsql_history;
drop table bmsql_customer;
drop table bmsql_stock;
drop table bmsql_item;
drop table bmsql_district;
drop table bmsql_warehouse;
purge recyclebin;
-- tpcc_group
drop tablegroup tpcc_group;
```
运行命令:
```bash
sh runSQL.sh props.ob sql.common/tableDrops.sql
输出:
[root@obce-0000 run]# sh runSQL.sh props.ob sql.common/tableDrops.sql
# ------------------------------------------------------------
# Loading SQL file sql.common/tableDrops.sql
# ------------------------------------------------------------
.:../lib/oracle/*:../lib/*:../dist/*
-Dprop=props.ob -DcommandFile=sql.common/tableDrops.sql
drop table bmsql_config;
pdrop table bmsql_new_order;
wdrop table bmsql_order_line;
drop table bmsql_oorder;
ddrop table bmsql_history;
drop table bmsql_customer;
drop table bmsql_stock;
drop table bmsql_item;
drop table bmsql_district;
drop table bmsql_warehouse;
purge recyclebin;
-- tpcc_group
drop tablegroup tpcc_group;
purge recyclebin;
```
## 性能测试
数据初始化确认没有错误(没有仓库数据报错),就可以跑性能测试。建议跑之前先做一次集群合并( `major freeze` )。
```bash
sh runBenchmark.sh props.ob
输出:
[root@obce-0000 run]# sh runBenchmark.sh props.ob
10:21:44,894 [main] INFO jTPCC : Term-00,
10:21:44,896 [main] INFO jTPCC : Term-00, +-------------------------------------------------------------+
10:21:44,896 [main] INFO jTPCC : Term-00, BenchmarkSQL v5.0
10:21:44,896 [main] INFO jTPCC : Term-00, +-------------------------------------------------------------+
10:21:44,896 [main] INFO jTPCC : Term-00, (c) 2003, Raul Barbosa
10:21:44,896 [main] INFO jTPCC : Term-00, (c) 2004-2016, Denis Lussier
10:21:44,898 [main] INFO jTPCC : Term-00, (c) 2016, Jan Wieck
10:21:44,898 [main] INFO jTPCC : Term-00, +-------------------------------------------------------------+
10:21:44,898 [main] INFO jTPCC : Term-00,
10:21:44,898 [main] INFO jTPCC : Term-00, db=oracle
10:21:44,898 [main] INFO jTPCC : Term-00, driver=com.alipay.oceanbase.jdbc.Driver
10:21:44,898 [main] INFO jTPCC : Term-00, conn=jdbc:oceanbase://172.20.249.52:2883/tpccdb?useUnicode=true&characterEncoding=utf-8&rewriteBatchedStatements=true&allowMultiQueries=true
10:21:44,898 [main] INFO jTPCC : Term-00, user=u_tpcc@obmysql#obce-3zones
10:21:44,898 [main] INFO jTPCC : Term-00,
10:21:44,899 [main] INFO jTPCC : Term-00, warehouses=10
10:21:44,899 [main] INFO jTPCC : Term-00, terminals=10
10:21:44,900 [main] INFO jTPCC : Term-00, runMins=10
10:21:44,900 [main] INFO jTPCC : Term-00, limitTxnsPerMin=0
10:21:44,900 [main] INFO jTPCC : Term-00, terminalWarehouseFixed=true
10:21:44,900 [main] INFO jTPCC : Term-00,
10:21:44,900 [main] INFO jTPCC : Term-00, newOrderWeight=45
10:21:44,900 [main] INFO jTPCC : Term-00, paymentWeight=43
10:21:44,900 [main] INFO jTPCC : Term-00, orderStatusWeight=4
10:21:44,901 [main] INFO jTPCC : Term-00, deliveryWeight=4
10:21:44,901 [main] INFO jTPCC : Term-00, stockLevelWeight=4
10:21:44,901 [main] INFO jTPCC : Term-00,
10:21:44,901 [main] INFO jTPCC : Term-00, resultDirectory=my_result_%tY-%tm-%td_%tH%tM%tS
10:21:44,901 [main] INFO jTPCC : Term-00, osCollectorScript=./misc/os_collector_linux.py
10:21:44,901 [main] INFO jTPCC : Term-00,
10:21:44,925 [main] INFO jTPCC : Term-00, copied props.ob to my_result_2021-10-01_102144/run.properties
10:21:44,926 [main] INFO jTPCC : Term-00, created my_result_2021-10-01_102144/data/runInfo.csv for runID 326
10:21:44,926 [main] INFO jTPCC : Term-00, writing per transaction results to my_result_2021-10-01_102144/data/result.csv
10:21:44,926 [main] INFO jTPCC : Term-00, osCollectorScript=./misc/os_collector_linux.py
10:21:44,926 [main] INFO jTPCC : Term-00, osCollectorInterval=1
10:21:44,927 [main] INFO jTPCC : Term-00, osCollectorSSHAddr=null
10:21:44,927 [main] INFO jTPCC : Term-00, osCollectorDevices=null
10:21:45,031 [main] INFO jTPCC : Term-00,
10:21:45,611 [main] INFO jTPCC : Term-00, C value for C_LAST during load: 144
10:21:45,613 [main] INFO jTPCC : Term-00, C value for C_LAST this run: 215
10:21:45,614 [main] INFO jTPCC : Term-00,
0:21:45,611 [main] INFO jTPCC : Term-00, C value for C_LAST during load: 14Term-00, Running Average tpmTOTAL: 5376.32 Current tpmTOTAL: 356448 Memory Usage: 44MB / 366MB 10:31:46,707 [Thread-7] INFO jTPCC : Term-00, 10:31:46,707 [Thread-7] INFO jTPCC : Term-00, 10:31:46,707 [Thread-7] INFO jTPCC : Term-00, Measured tpmC (NewOrders) = 2410.28
10:31:46,708 [Thread-7] INFO jTPCC : Term-00, Measured tpmTOTAL = 5376.33
10:31:46,708 [Thread-7] INFO jTPCC : Term-00, Session Start = 2021-10-01 10:21:46
10:31:46,708 [Thread-7] INFO jTPCC : Term-00, Session End = 2021-10-01 10:31:46
10:31:46,708 [Thread-7] INFO jTPCC : Term-00, Transaction Count = 53776
[root@obce-0000 run]#
```
TPC-C用 `tpmC` 值(Transactions per Minute)来衡量系统最大有效吞吐量. 其中 Transactions 以 NewOrder Transaction 为准,即最终衡量单位为每分钟处理的订单数。
## 性能调优
性能测试的结果取决于租户资源大小、内存转储的设置、客户端的性能(JVM 的大小)等等。
### 转储&合并
| | 合并转储前 | 合并后 | 提升率 |
|-------------------------|-----------|-----------|--------|
| bmsql(1000仓、600并发 tpmc) | 230211.65 | 264401.03 | 11.30% |
### 调整 PRIMARY_ZONE
| | 集中式部署(zone1) | random部署 |
|------------------------|-----------------|------------------|
| bmsql | 88592.24 | 264401.03 |
### 使用 TABLEGROUP
使用 `table_group` 可以减少分布式查询和事务,提升性能。
| | 未使用pg | 使用pg |
|------------------------|----------------|----------------|
| bmsql | 60383.72 | 250249.36 |
## 常见问题
+ `Could not find the main class: ExecJDBC. Program will exit.`
可能原因有两个:要么升级jdk,要么升级jdbc版本
+ `Invalid number of terminals!`
这是my_oracle.properties中设置的terminals值不对,需填写正确范围内的terminals值
numTerminals <= 0 || numTerminals > 10*numWarehouses
+ 事务超时报错 `transaction timeout` .
需增大超时时间,建议为:36000000000
# 如何跑 TPC-H 测试
## TPC-H 简介
TPC-H 基准测试是由 TPC-D(由 TPC 组织于 1994 年指定的标准,用于决策支持系统方面的测试基准)发展而来的.TPC-H 用 3NF 实现了一个数据仓库,共包含 8 个基本关系,其主要评价指标是各个查询的响应时间,即从提交查询到结果返回所需时间.TPC-H 基准测试的度量单位是每小时执行的查询数( QphH@size),其中 H 表示每小时系统执行复杂查询的平均次数,size 表示数据库规模的大小,它能够反映出系统在处理查询时的能力.TPC-H 是根据真实的生产运行环境来建模的,这使得它可以评估一些其他测试所不能评估的关键性能参数.总而言之,TPC 组织颁布的TPC-H 标准满足了数据仓库领域的测试需求,并且促使各个厂商以及研究机构将该项技术推向极限。
详细可以参考 <http://www.tpc.org/tpc_documents_current_versions/pdf/tpc-h_v2.17.3.pdf>
## 测试准备
### 软件安装
+ 下载软件
TPC-H Tool 官方提供,下载地址:[http://tpc.org/TPC_Documents_Current_Versions/download_programs/tools-download-request5.asp?bm_type=TPC-H&bm_vers=3.0.0&mode=CURRENT-ONLY](http://tpc.org/TPC_Documents_Current_Versions/download_programs/tools-download-request5.asp?bm_type=TPC-H&bm_vers=3.0.0&mode=CURRENT-ONLY) 。文件名:`TPC-H_Tools_v3.0.0.zip`
```bash
cd /root/TPC-H_Tools_v3.0.0/dbgen
cp makefile.suite Makefile
```
+ 修改 `Makefile` 文件中的`CC``DATABASE``MACHINE``WORKLOAD` 等参数定义。
```bash
vim Makefile +103
CC = gcc
# Current values for DATABASE are: INFORMIX, DB2, TDAT (Teradata)
# SQLSERVER, SYBASE, ORACLE, VECTORWISE
# Current values for MACHINE are: ATT, DOS, HP, IBM, ICL, MVS,
# SGI, SUN, U2200, VMS, LINUX, WIN32
# Current values for WORKLOAD are: TPCH
DATABASE= MYSQL
MACHINE = LINUX
WORKLOAD = TPCH
```
+ 修改 `tpcd.h` 文件,并添加新的宏定义。
```bash
vim tpcd.h +$
#ifdef MYSQL
#define GEN_QUERY_PLAN ""
#define START_TRAN "START TRANSACTION"
#define END_TRAN "COMMIT"
#define SET_OUTPUT ""
#define SET_ROWCOUNT "limit %d;\n"
#define SET_DBASE "use %s;\n"
#endif
```
+ 对文件进行编译。
```bash
make
```
编译完成后该目录下会生成两个可执行文件:
- dbgen:数据生成工具。在使用InfiniDB官方测试脚本进行测试时,需要用该工具生成tpch相关表数据。
- qgen:SQL生成工具。生成初始化测试查询,由于不同的seed生成的查询不同,为了结果的可重复性,请使用附件提供的22个查询。
### 数据文件准备
`dbgen` 命令可以生成指定大小的数据。生成环境测试建议不少于 1000G 。本文示例 `10g`
```bash
./dbgen -s 10
输出:
TPC-H Population Generator (Version 3.0.0)
Copyright Transaction Processing Performance Council 1994 - 2010
mkdir tpch10
mv *.tbl tpch10/
[root@obce-0000 dbgen]# ls -lrth tpch10/
总用量 11G
-rw-r--r-- 1 root root 14M 10月 1 16:04 supplier.tbl
-rw-r--r-- 1 root root 389 10月 1 16:04 region.tbl
-rw-r--r-- 1 root root 233M 10月 1 16:04 part.tbl
-rw-r--r-- 1 root root 1.2G 10月 1 16:04 partsupp.tbl
-rw-r--r-- 1 root root 1.7G 10月 1 16:04 orders.tbl
-rw-r--r-- 1 root root 2.2K 10月 1 16:04 nation.tbl
-rw-r--r-- 1 root root 7.3G 10月 1 16:04 lineitem.tbl
-rw-r--r-- 1 root root 234M 10月 1 16:04 customer.tbl
```
### 查询语句准备
```bash
cp qgen queries/
cp dists.dss queries/
cd queries/
for i in `seq 22`; do echo $i; ./qgen -d $i -s 10 > db"$i".sql; done
dos2unix *.sql
```
去掉生成的 SQL 文件中的”limit -xx”, 去掉day 后面的(3), 并且加上parallel(96)并发,以 `db1.sql` 为例。
+ Q1
```sql
select /*+ TPCH_Q1 parallel(16) */
l_returnflag,
l_linestatus,
sum(l_quantity) as sum_qty,
sum(l_extendedprice) as sum_base_price,
sum(l_extendedprice * (1 - l_discount)) as sum_disc_price,
sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)) as sum_charge,
avg(l_quantity) as avg_qty,
avg(l_extendedprice) as avg_price,
avg(l_discount) as avg_disc,
count(*) as count_order
from
lineitem
where
l_shipdate <= date '1998-12-01' - interval '90' day (3)
group by
l_returnflag,
l_linestatus
order by
l_returnflag,
l_linestatus;
```
+ Q2
```sql
select /*+ TPCH_Q2 parallel(16) */
s_acctbal,
s_name,
n_name,
p_partkey,
p_mfgr,
s_address,
s_phone,
s_comment
from
part,
supplier,
partsupp,
nation,
region
where
p_partkey = ps_partkey
and s_suppkey = ps_suppkey
and p_size = 15
and p_type like '%BRASS'
and s_nationkey = n_nationkey
and n_regionkey = r_regionkey
and r_name = 'EUROPE'
and ps_supplycost = (
select
min(ps_supplycost)
from
partsupp,
supplier,
nation,
region
where
p_partkey = ps_partkey
and s_suppkey = ps_suppkey
and s_nationkey = n_nationkey
and n_regionkey = r_regionkey
and r_name = 'EUROPE'
)
order by
s_acctbal desc,
n_name,
s_name,
p_partkey;
```
以下其他 SQL 就不重复说明。
### 参数准备
+ 租户参数
```sql
set global autocommit=ON;
set global ob_query_timeout=36000000000;
set global ob_trx_timeout=36000000000;
set global max_allowed_packet=67108864;
set global ob_sql_work_area_percentage=80;
/*
parallel_max_servers和parallel_servers_target的值
推荐设置为测试租户分配的resource unit cpu数的10倍
如测试租户使用的unit配置为:create resource unit $unit_name max_cpu 26
那么该值设置为260
*/
set global parallel_max_servers=260;
set global parallel_servers_target=260;
```
### 建表
```sql
create tablegroup tpch_tg_10g_lineitem_order_group binding true partition by key 1 partitions 9;
create tablegroup tpch_tg_10g_partsupp_part binding true partition by key 1 partitions 9;
drop database if exists tpch_10g_part;
create database tpch_10g_part;
use tpch_10g_part;
CREATE TABLE lineitem (
l_orderkey bigint NOT NULL,
l_partkey bigint NOT NULL,
l_suppkey bigint NOT NULL,
l_linenumber bigint NOT NULL,
l_quantity bigint NOT NULL,
l_extendedprice bigint NOT NULL,
l_discount bigint NOT NULL,
l_tax bigint NOT NULL,
l_returnflag char(1) DEFAULT NULL,
l_linestatus char(1) DEFAULT NULL,
l_shipdate date NOT NULL,
l_commitdate date DEFAULT NULL,
l_receiptdate date DEFAULT NULL,
l_shipinstruct char(25) DEFAULT NULL,
l_shipmode char(10) DEFAULT NULL,
l_comment varchar(44) DEFAULT NULL,
primary key(l_orderkey, l_linenumber)
) tablegroup = tpch_tg_10g_lineitem_order_group
partition by key (l_orderkey) partitions 9;
create index I_L_ORDERKEY on lineitem(l_orderkey) local;
create index I_L_SHIPDATE on lineitem(l_shipdate) local;
CREATE TABLE orders (
o_orderkey bigint NOT NULL,
o_custkey bigint NOT NULL,
o_orderstatus char(1) DEFAULT NULL,
o_totalprice bigint DEFAULT NULL,
o_orderdate date NOT NULL,
o_orderpriority char(15) DEFAULT NULL,
o_clerk char(15) DEFAULT NULL,
o_shippriority bigint DEFAULT NULL,
o_comment varchar(79) DEFAULT NULL,
PRIMARY KEY (o_orderkey))
tablegroup = tpch_tg_10g_lineitem_order_group
partition by key(o_orderkey) partitions 9;
create index I_O_ORDERDATE on orders(o_orderdate) local;
CREATE TABLE partsupp (
ps_partkey bigint NOT NULL,
ps_suppkey bigint NOT NULL,
ps_availqty bigint DEFAULT NULL,
ps_supplycost bigint DEFAULT NULL,
ps_comment varchar(199) DEFAULT NULL,
PRIMARY KEY (ps_partkey, ps_suppkey))
tablegroup tpch_tg_10g_partsupp_part
partition by key(ps_partkey) partitions 9;
CREATE TABLE part (
p_partkey bigint NOT NULL,
p_name varchar(55) DEFAULT NULL,
p_mfgr char(25) DEFAULT NULL,
p_brand char(10) DEFAULT NULL,
p_type varchar(25) DEFAULT NULL,
p_size bigint DEFAULT NULL,
p_container char(10) DEFAULT NULL,
p_retailprice bigint DEFAULT NULL,
p_comment varchar(23) DEFAULT NULL,
PRIMARY KEY (p_partkey))
tablegroup tpch_tg_10g_partsupp_part
partition by key(p_partkey) partitions 9;
CREATE TABLE customer (
c_custkey bigint NOT NULL,
c_name varchar(25) DEFAULT NULL,
c_address varchar(40) DEFAULT NULL,
c_nationkey bigint DEFAULT NULL,
c_phone char(15) DEFAULT NULL,
c_acctbal bigint DEFAULT NULL,
c_mktsegment char(10) DEFAULT NULL,
c_comment varchar(117) DEFAULT NULL,
PRIMARY KEY (c_custkey))
partition by key(c_custkey) partitions 9;
CREATE TABLE supplier (
s_suppkey bigint NOT NULL,
s_name char(25) DEFAULT NULL,
s_address varchar(40) DEFAULT NULL,
s_nationkey bigint DEFAULT NULL,
s_phone char(15) DEFAULT NULL,
s_acctbal bigint DEFAULT NULL,
s_comment varchar(101) DEFAULT NULL,
PRIMARY KEY (s_suppkey)
) partition by key(s_suppkey) partitions 9;
CREATE TABLE nation (
n_nationkey bigint NOT NULL,
n_name char(25) DEFAULT NULL,
n_regionkey bigint DEFAULT NULL,
n_comment varchar(152) DEFAULT NULL,
PRIMARY KEY (n_nationkey));
CREATE TABLE region (
r_regionkey bigint NOT NULL,
r_name char(25) DEFAULT NULL,
r_comment varchar(152) DEFAULT NULL,
PRIMARY KEY (r_regionkey));
```
创建结束后查看表和表分组。
```sql
MySQL [tpch_10g_part]> show tables;
+-------------------------+
| Tables_in_tpch_10g_part |
+-------------------------+
| customer |
| lineitem |
| nation |
| orders |
| part |
| partsupp |
| region |
| supplier |
+-------------------------+
8 rows in set (0.365 sec)
MySQL [tpch_10g_part]> show tablegroups;
+----------------------------------+------------------+---------------+
| Tablegroup_name | Table_name | Database_name |
+----------------------------------+------------------+---------------+
| oceanbase | NULL | NULL |
| tpch_tg_10g_lineitem_order_group | lineitem | tpch_10g_part |
| tpch_tg_10g_lineitem_order_group | orders | tpch_10g_part |
| tpch_tg_10g_partsupp_part | part | tpch_10g_part |
| tpch_tg_10g_partsupp_part | partsupp | tpch_10g_part |
+----------------------------------+------------------+---------------+
5 rows in set (0.068 sec)
```
### 加载数据
使用 OceanBase 自带的 LOAD 命令逐个加载数据。
也可以像下面编写一个脚本批量加载数据。
+ 创建加载脚本目录
```bash
cd /root/TPC-H_Tools_v3.0.0/dbgen
mkdir load
cd load
cp ../dss.ri ../dss.ddl ./
```
+ 编写加载脚本
```bash
#/usr/bin/evn python
#-*- encoding:utf-8 -*-
import os
import sys
import time
import commands
hostname='172.20.249.52' #注意!!请填写某个observer所在服务器的ip地址
port='2881' #端口号
tenant='obmysql' #组户名
user='u_tpch' #用户名
password='123456' #密码
data_path='/tmp/tpch10' #注意!!请填写某个observer所在服务器下tbl所在目录
db_name='tpch_10g_part' #数据库名
cmd_str='mysql --default-auth=mysql_native_password,db_name -h%s -P%s -u%s@%s -c -A %s -e "show tables;" '%(hostname,port,user,tenant,db_name)
print cmd_str
result = commands.getstatusoutput(cmd_str)
print result
cmd_str=""" mysql --default-auth=mysql_native_password,db_name -h%s -P%s -u%s@%s -c -A %s -e "load data /*+ parallel(8) */ infile '%s/customer.tbl' into table customer fields terminated by '|';" """ %(hostname,port,user,tenant,db_name,data_path)
print cmd_str
result = commands.getstatusoutput(cmd_str)
print result
cmd_str=""" mysql --default-auth=mysql_native_password,db_name -h%s -P%s -u%s@%s -c -A %s -e "load data /*+ parallel(8) */ infile '%s/lineitem.tbl' into table lineitem fields terminated by '|';" """ %(hostname,port,user,tenant,db_name,data_path)
result = commands.getstatusoutput(cmd_str)
print result
cmd_str=""" mysql --default-auth=mysql_native_password,db_name -h%s -P%s -u%s@%s -c -A %s -e "load data /*+ parallel(8) */ infile '%s/nation.tbl' into table nation fields terminated by '|';" """ %(hostname,port,user,tenant,db_name,data_path)
result = commands.getstatusoutput(cmd_str)
print result
cmd_str=""" mysql --default-auth=mysql_native_password,db_name -h%s -P%s -u%s@%s -c -A %s -e "load data /*+ parallel(8) */ infile '%s/orders.tbl' into table orders fields terminated by '|';" """ %(hostname,port,user,tenant,db_name,data_path)
result = commands.getstatusoutput(cmd_str)
print result
cmd_str=""" mysql --default-auth=mysql_native_password,db_name -h%s -P%s -u%s@%s -c -A %s -e "load data /*+ parallel(8) */ infile '%s/partsupp.tbl' into table partsupp fields terminated by '|';" """ %(hostname,port,user,tenant,db_name,data_path)
result = commands.getstatusoutput(cmd_str)
print result
cmd_str=""" mysql --default-auth=mysql_native_password,db_name -h%s -P%s -u%s@%s -c -A %s -e "load data /*+ parallel(8) */ infile '%s/part.tbl' into table part fields terminated by '|';" """ %(hostname,port,user,tenant,db_name,data_path)
result = commands.getstatusoutput(cmd_str)
print result
cmd_str=""" mysql --default-auth=mysql_native_password,db_name -h%s -P%s -u%s@%s -c -A %s -e "load data /*+ parallel(8) */ infile '%s/region.tbl' into table region fields terminated by '|';" """ %(hostname,port,user,tenant,db_name,data_path)
result = commands.getstatusoutput(cmd_str)
print result
cmd_str=""" mysql --default-auth=mysql_native_password,db_name -h%s -P%s -u%s@%s -c -A %s -e "load data /*+ parallel(8) */ infile '%s/supplier.tbl' into table supplier fields terminated by '|';" """ %(hostname,port,user,tenant,db_name,data_path)
```
```bash
export MYSQL_PWD=123456
python load.py
```
## 性能调优
### 转储&合并
| | 合并转储前 | 合并后 | 提升率 |
|-------------------------|-----------|-----------|--------|
| tpch_10g(总用时) | 57.26 | 23.28 | 59.30% |
| Q1 | 2.43s | 2.02s | |
| Q2 | 0.86s | 0.32s | |
| Q3 | 2.08s | 1.49s | |
| Q4 | 0.45s | 0.34s | |
| Q5 | 0.96s | 1.58s | |
| Q6 | 1.29s | 0.83s | |
| Q7 | 2.63s | 1.41s | |
| Q8 | 3.60s | 1.39s | |
| Q9 | 1.53s | 1.85s | |
| Q10 | 4.23s | 2.38s | |
| Q11 | 0.59s | 0.47s | |
| Q12 | 2.01s | 1.00s | |
| Q13 | 2.34s | 0.85s | |
| Q14 | 0.51s | 0.21s | |
| Q15 | 1.09s | 0.57s | |
| Q16 | 1.18s | 0.41s | |
| Q17 | 10.54s | 0.94s | |
| Q18 | 1.94s | 0.98s | |
| Q19 | 2.32s | 1.02s | |
| Q20 | 10.46s | 1.25s | |
| Q21 | 2.49s | 1.42s | |
| Q22 | 1.73s | 0.55s | |
### 调整 PRIMARY_ZONE
| | 集中式部署(zone1) | random部署 |
|------------------------|-----------------|------------------|
| tpch | 590.42 | 149.13 |
| Q1 | 50.74s | 15.14s |
| Q2 | 2.25s | 0.47s |
| Q3 | 37.69s | 13.42s |
| Q4 | 42.78s | 2.57s |
| Q5 | 6.94s | 2.52s |
| Q6 | 26.62s | 7.38s |
| Q7 | 0.35s | 0.23s |
| Q8 | 67.92s | 11.69s |
| Q9 | 20.19s | 6.17s |
| Q10 | 54.79s | 24.00s |
| Q11 | 0.39s | 0.19s |
| Q12 | 40.71s | 8.60s |
| Q13 | 20.99s | 7.82s |
| Q14 | 22.99s | 1.52s |
| Q15 | 46.28s | 2.95s |
| Q16 | 7.41s | 2.88s |
| Q17 | 32.04s | 8.64s |
| Q18 | 26.64s | 8.29s |
| Q19 | 31.25s | 9.42s |
| Q20 | 38.14s | 10.74s |
| Q21 | 0.24s | 0.15s |
| Q22 | 13.07s | 4.34s |
### 使用 TABLEGROUP
| | 未使用pg | 使用pg |
|------------------------|----------------|----------------|
| tpch | 25.3 | 23.28 |
| Q1 | 2.23s | 2.02s |
| Q2 | 0.59s | 0.32s |
| Q3 | 1.59s | 1.49s |
| Q4 | 0.46s | 0.34s |
| Q5 | 1.65s | 1.58s |
| Q6 | 0.80s | 0.83s |
| Q7 | 1.58s | 1.41s |
| Q8 | 1.52s | 1.39s |
| Q9 | 1.83s | 1.85s |
| Q10 | 2.41s | 2.38s |
| Q11 | 0.54s | 0.47s |
| Q12 | 1.14s | 1.00s |
| Q13 | 0.93s | 0.85s |
| Q14 | 0.23s | 0.21s |
| Q15 | 0.63s | 0.57s |
| Q16 | 0.46s | 0.41s |
| Q17 | 0.98s | 0.94s |
| Q18 | 1.18s | 0.98s |
| Q19 | 1.03s | 1.02s |
| Q20 | 1.34s | 1.25s |
| Q21 | 1.55s | 1.42s |
| Q22 | 0.63s | 0.55s |
# 如何使用 跑业务场景测试
## 数据库驱动
使用 JMeter 测试OceanBase性能的时候也需要加载OceanBase的 Java 驱动。文件可以从官网的下载文件中获取。地址:[https://help.aliyun.com/document_detail/212815.html](https://help.aliyun.com/document_detail/212815.html)
下载的`oceanbase-client-1.x.x.jar`需要放到 `Jmeter``lib` 文件夹夹中。
## 业务场景定义
### 建表语句
请在obmysql租户下业务账户执行下面SQL 。
```sql
$obclient -h127.1 -utpcc@obbmsql#obdemo -P2883 -p123456 -c -A tpcc
CREATE TABLE account(id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY
, name varchar(50) NOT NULL UNIQUE
, value bigint NOT NULL
, gmt_create timestamp DEFAULT current_timestamp NOT NULL
, gmt_modified timestamp DEFAULT current_timestamp NOT NULL );
```
### 场景SQL
此次测试模拟一个分布式事务,SQL类似如下:
```sql
-- session A
begin ;
select value from account where id = 174 for update ;
update account set value = value - 7 , gmt_modified = current_timestamp where id = 174 ;
update account set value = value + 7 , gmt_modified = current_timestamp where id = 165 ;
-- commit or rollback;
commit;
```
## 新建JMeter测试计划
JMeter能在命令行下运行,也可以以图形界面运行。这里我简单点直接用图形界面这种方式。
新建测试计划
### 测试计划属性
![-w1152](media/15847798056795.jpg)
新建 `Thread-Group`
![-w1152](media/15847798711697.jpg)
这里有很多跟多线程运行有关的JMeter参数,具体说明可以查看JMeter官网文档。
注意这里的`bigint of Threads`是指压测的客户端线程数。
### JDBC连接属性
![-w1440](media/15848380171617.jpg)
这里面属性很多,都是一个连接池常具备的参数。有兴趣的可以看看网上关于Java连接池配置的经验。
这里面也有几个参数强调一下:
+ `Max bigint of Connections`:这个指连接池里最多多少个连接。如果压测线程数远高于这个值,那么压测线程可能会需要等待这个连接池创建或返还数据库连接(即到OceanBase的连接)给它。如果等不到可能会报错。在这个环节,客户端压测线程拿不到连接,不一定跟OceanBase数据库有直接关系。在Java应用里面也同理。
+ `Transaction Isolation`:这个是数据库连接使用的事务隔离级别,OceanBase支持两种事务隔离级别:读已提交(`Read-Committed`)和序列化(`Serializable`)。前者很常见容易理解,后者是ORACLE特有隔离级别,OceanBase也兼容了。有兴趣的可以看看《OceanBase事务引擎特性和应用实践分享》。理解序列化隔离级别的特点和场景可以加深自己对数据库事务的理解。
+ `Test While Idle`:这个是连接探活(`keepalive`)设置。这个设置对应用却很优必要。有时候应用会说数据库连接报错说在一个关闭的连接上执行SQL报错,这个就是因为连接池中的数据库连接因为其他原因已断开了。所以,数据库连接池通常都需要探活机制。这里由于是压测场景基本无闲置连接,所以可以设置为`False`
+ `Database URL`:数据库连接URL格式,直接类似填写 `jdbc:oceanbase://11.***.***.5:2883/tpcc`
+ `JDBC Driver Class`:数据库驱动中的Main类名,这个要按OceanBase格式填写,`com.alipay.oceanbase.obproxy.mysql.jdbc.Driver`
+ `Username`:用户格式,OceanBase的用户名格式比较特别,是`租户里用户名@租户名#集群名``集群名:租户名:租户里用户名`。如 `tpcc@obbmsql#obdemo`
### 事务参数(变量)
在这个测试里,有三个变量:账户A,账户B,转账金额,所以需要设置参数
![-w1440](media/15847856661453.jpg)
账户参数和金额采取随机数,随机数的值不要超出测试数据实际范围。
### 事务控制器
这里面是维护每个事务的逻辑。事务由一组`JDBC`请求组成。
+ 开启事务
![-w1440](media/15847810253110.jpg)
选择 `Autocommit(false)`,开启显式事务。
+ 查询账户A的余额
这里查询账户A的记录会同时锁住这笔记录,即常用的悲观锁技术。 这一步看测试需要,不是必需的。
```sql
select value from account where id = ? for update ;
```
![-w1440](media/15847852394348.jpg)
注意所有参数都使用`Prepared Statement`,以下同。
这一步后面按业务设计应该检查一下返回值是否大于要转账的值,如果不满足就是“转账余额不足”。这里我没有去研究JMeter如果根据查询返回值进行逻辑判断。有兴趣的朋友可以自己研究。
+ 扣减账户A的余额
SQL很简单。
```sql
update account set value = value - ? , gmt_modified = current_timestamp where id = ? ;
```
![-w1440](media/15847853936218.jpg)
多个绑定参数使用逗号(,)分隔。
然后要新增一个Post处理逻辑,获取更新返回值
![-w1440](media/15847854836534.jpg)
+ 增加账户B的余额
SQL很简单。
```sql
update account set value = value + ? , gmt_modified = current_timestamp where id = ? ;
```
![-w1440](media/15847857206738.jpg)
同样,需要增加一个Post处理器
![-w1440](media/15847857858980.jpg)
+ 判断逻辑——成功流程
如果上面两笔账户的更新成功,则提交事务。
新增判断控制 `IF`
![-w1440](media/15847858525132.jpg)
新增动作
![-w1440](media/15847860017082.jpg)
### 判断逻辑——失败流程
如果上面两笔账户的更新有一笔失败,则回滚事务。
新增判断控制 `IF`
![-w1440](media/15847859478920.jpg)
新增动作
![-w1440](media/15847859751936.jpg)
### 查看结果
可以查看成功、失败的结果
![-w1440](media/15847861075504.jpg)
查看汇总报告
![-w1440](media/15847861285271.jpg)
![-w1440](media/15847861581557.jpg)
# 第 7 章:如何诊断和调优 OceanBase 社区版性能
本章主要介绍OceanBase 性能诊断和调优技巧以及部分原理。
## 本章目录
+ [性能诊断调优概述](7.1.md)
+ [OBPROXY SQL 路由原理](7.2.md)
+ [如何管理 OceanBase 数据库连接](7.3.md)
+ [如何分析 SQL 审计视图](7.4.md)
+ [如何诊断和调优 OceanBase SQL 执行计划](7.5.md)
## 如何联系我们
欢迎广大 OceanBase 爱好者、用户和客户有任何问题联系我们反馈:
+ 社区版官网论坛:[https://open.oceanbase.com/answer](https://open.oceanbase.com/answer)
+ 社区版项目网站提 `Issue`[https://github.com/oceanbase/oceanbase/issues](https://github.com/oceanbase/oceanbase/issues)
+ 钉钉群:群号 `33254054`
\ No newline at end of file
# 性能诊断调优概述
客户端 SQL执行过程会分为下面几步:
+ 客户端将 SQL 发往 OBPROXY 节点。
+ OBPROXY 将 SQL 发往后端 OBSERVER 节点。
+ OBSERVER 解析 SQL,执行SQL,返回结果给 OBPROXY 节点。
+ OBPROXY 将数据返回给客户端。
OceanBase 的 SQL 性能诊断就是围绕这个链路上几个关键环节进行。如 OBPROXY 的 路由、OBSERVER 的 SQL引擎等。
## SQL 引擎简介
OBSERVER 进程包含 SQL 引擎、事务引擎和存储引擎。其中 SQL 引擎的设计跟 ORACLE SQL引擎设计理念一致。都有 SQL 语法解析、执行计划缓存、软解析、大纲等技术。
OBPROXY 进程并不包含 SQL 引擎,只能做很简单的 SQL 分析,判断 SQL 该路由到后端那个 OBSERVER 节点。未来 OBPROXY 可能会把 OBSERVER 的 SQL 引擎能力也包含进去。
### SQL 执行计划简介
SQL 执行计划简单来说就是 SQL 执行的路径或者算法,影响 SQL 的执行性能。差之毫厘,谬以千里。
OceanBase SQL 引擎支持SQL解析,解析后的执行计划会缓存,下次就可以复用。跟传统数据库一样,SQL 文本不一样,执行计划不会复用。此外,如果SQL文本一样,但是执行环境不一样,执行计划也不会复用。目前所知的执行环境包括运行节点IP、会话级别的并行参数。当然可能还有其他的。所有数据库的SQL执行引擎的成熟度可以从这个小点入手判断。
OceanBase 缓存执行计划的两个视图是:`gv$plan_cache_plan_stat``gv$plan_cache_plan_explain` 。这是实际运行使用的执行计划,是更准确的。不过初学的时候看这个比较麻烦。所以本文后面先探讨用 EXPLAIN 命令解析的执行计划。
先介绍一些执行计划的大的特点。
+ 没有绝对完美正确的执行计划,尤其是 SQL 比较复杂的时候。
+ 同样的SQL,不同 OBSERVER 版本可能会有执行计划的变化。
+ 同样的SQL,执行计划也不一定一尘不变。影响执行计划变化的因素可能有数据量(或者统计信息)、会话参数(或变量)、数据分布特点、SQL被路由到的节点等。后两者是 OceanBase 特有的。
+ EXPLAIN 解析的执行计划可能跟实际运行的执行计划不一样,受 OBPROXY 和 OBSERVER 的 LOCATION CACHE 和 SQL 路由策略影响。
+ SQL 执行计划可以通过 SQL 注释干预,这个需要改 SQL。如果 SQL 不可以改,可以通过大纲(OUTLINE)在线干预。干预的结果可能成功也可能失败。 这点以后也详细介绍。
### SQL HINTS
OceanBase 支持在 SQL 里使用 HTINTS 来改变执行计划或控制其他行为。
+ 语句级别的hint
```sql
FROZEN_VERSION
QUERY_TIMEOUT
READ_CONSISTENCY
LOG_LEVEL
QB_NAME
ACTIVATE_BURIED_POINT
TRACE_LOG
MAX_CONCURRENT
```
**说明:**
+ `/*+ QUERY_TIMEOUT(100000000) */`:是对SQL 超时时间进行控制。
+ `/*+ READ_CONSISTENCY(weak) */`:是用来设置弱一致性读。
+ `/*+ MAX_CONCURRENT(8) */`: 是用来对 SQL 的并发进行限制的。
+ 计划相关的hint
```sql
FULL
INDEX
LEADING
USE_MERGE
USE_HASH
USE_NL
ORDERED
NO_REWRITE
```
**说明:**
+ `/*+ FULL(表名) */`:是强制对表进行全表扫描。
+ `/*+ LEADING(表名) */`:是强制某表为驱动表。可以间接改变连接算法。
+ `/*+ USE_NL(表a, 表b, 表c) */`:设置表 a b c 的连接算法为嵌套循环。
+ `/*+ ORDERED */`:按照 `from` 子句后的表顺序进行表链接。
## OceanBase 性能诊断思路
OceanBase 性能问题通常包括:
+ 大表查询性能,大批量数据导出性能等。
+ 大事务性能,大批量数据加载性能等。
+ 普通的读写性能。
有些性能问题往往是集群出现稳定性问题之后出现的。比如说网络延时增大或者节点时间误差增大导致的节点状态不稳定,进而影响了性能。磁盘故障或者磁盘空间满导致的合并异常问题,也可能间接影响性能。由于 OceanBase 可靠性很高,即使集群不稳定,也不会出现不可用的状态,所以,稳定性问题隐藏的有点深,不为业务所见。业务更多的时候感知的是性能问题。
当然,运维使用一些监控手段,也是能够及时发现稳定性问题。
下面性能诊断就不考虑稳定性问题带来的性能问题。
性能诊断的方向有下面几个:
+ OBPROXY 节点的负载。包括 CPU、网络、内存等。
+ OBSERVER 节点的负载。包括 CPU、磁盘、内存、网络等。
+ SQL 的执行类型分布(本地 SQL 和远程 SQL 的比例)。
+ SQL 的执行计划。
+ 集群和租户的内存管理(内存参数、转储和合并进度等)。
+ 业务事务的具体 SQL 逻辑。
# OBPROXY SQL 路由原理
OBPROXY 的主要功能就是提供 SQL 路由。所以,首先要了解数据的位置在哪里,然后了解 SQL 路由策略。
## 查看租户资源单元位置
从前面课程知租户的资源通常是分布在每个 ZONE 里的机器上。租户的数据就在租户资源所在的机器上。集群规模很大的时候,每个租户可能只是在部分机器上。OBPROXY 首先需要知道租户的位置(`LOCATION CACHE`)。下面有 2 种方法确认租户位置。
+ SYS 租户直接查询
```sql
select t1.name resource_pool_name, t2.`name` unit_config_name, t2.max_cpu, t2.min_cpu, round(t2.max_memory/1024/1024/1024) max_mem_gb, round(t2.min_memory/1024/1024/1024) min_mem_gb, t3.unit_id, t3.zone, concat(t3.svr_ip,':',t3.`svr_port`) observer,t4.tenant_id, t4.tenant_name
from __all_resource_pool t1 join __all_unit_config t2 on (t1.unit_config_id=t2.unit_config_id)
join __all_unit t3 on (t1.`resource_pool_id` = t3.`resource_pool_id`)
left join __all_tenant t4 on (t1.tenant_id=t4.tenant_id)
order by t1.`resource_pool_id`, t2.`unit_config_id`, t3.unit_id
;
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
| resource_pool_name | unit_config_name | max_cpu | min_cpu | max_mem_gb | min_mem_gb | unit_id | zone | observer | tenant_id | tenant_name |
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 1 | zone1 | 172.20.249.52:2882 | 1 | sys |
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 2 | zone2 | 172.20.249.49:2882 | 1 | sys |
| sys_pool | sys_unit_config | 5 | 5 | 2 | 2 | 3 | zone3 | 172.20.249.51:2882 | 1 | sys |
| my_pool_zone1 | unit1 | 9 | 9 | 19 | 19 | 1001 | zone1 | 172.20.249.52:2882 | 1002 | obmysql |
| my_pool_zone2 | unit1 | 9 | 9 | 19 | 19 | 1002 | zone2 | 172.20.249.49:2882 | 1002 | obmysql |
| my_pool_zone3 | unit1 | 9 | 9 | 19 | 19 | 1003 | zone3 | 172.20.249.51:2882 | 1002 | obmysql |
+--------------------+------------------+---------+---------+------------+------------+---------+-------+--------------------+-----------+-------------+
6 rows in set (0.008 sec)
```
+ 业务租户直接查询
```sql
select tenant_name, unit_id, unit_config_name, resource_pool_name, zone, svr_ip ,max_cpu,min_cpu,round(max_memory/1024/1024/1024) max_mem_gb,round(min_memory/1024/1024/1024) min_mem_gb
from gv$unit
WHERE tenant_id=1002;
输出:
+-------------+---------+------------------+--------------------+-------+---------------+---------+---------+------------+------------+
| tenant_name | unit_id | unit_config_name | resource_pool_name | zone | svr_ip | max_cpu | min_cpu | max_mem_gb | min_mem_gb |
+-------------+---------+------------------+--------------------+-------+---------------+---------+---------+------------+------------+
| obmysql | 1001 | unit1 | my_pool_zone1 | zone1 | 172.20.249.52 | 9 | 9 | 19 | 19 |
| obmysql | 1002 | unit1 | my_pool_zone2 | zone2 | 172.20.249.49 | 9 | 9 | 19 | 19 |
| obmysql | 1003 | unit1 | my_pool_zone3 | zone3 | 172.20.249.51 | 9 | 9 | 19 | 19 |
+-------------+---------+------------------+--------------------+-------+---------------+---------+---------+------------+------------+
3 rows in set (0.031 sec)
```
在业务租户里,条件 `tenant_id=1002` 也可以不用,因为每个业务租户只能查看自己的租户资源单元信息。
直连 OBSERVER 的时候,如果该 OBSERVER 节点上没有这个租户的资源单元,连接会报错。
## 查看分区副本位置
每个租户的分区都是从租户的资源单元中分配的。
下面的 SQL 可以在 SYS 租户里查看租户中所有数据库的主副本位置。
```sql
SELECT a.tenant_name, t.table_name, d.database_name, tg.tablegroup_name , t.part_num , t2.partition_id, t2.ZONE, t2.svr_ip , round(t2.data_size/1024/1024/1024) data_size_gb
, IF(t.primary_zone = '' OR t.primary_zone IS NULL, a.primary_zone, t.primary_zone) primary_zone
FROM oceanbase.__all_tenant AS a
JOIN oceanbase.__all_virtual_database AS d ON ( a.tenant_id = d.tenant_id )
JOIN oceanbase.__all_virtual_table AS t ON (t.tenant_id = d.tenant_id AND t.database_id = d.database_id)
JOIN oceanbase.__all_virtual_meta_table t2 ON (t.tenant_id = t2.tenant_id AND (t.table_id=t2.table_id OR t.tablegroup_id=t2.table_id) AND t2.ROLE IN (1) )
LEFT JOIN oceanbase.__all_virtual_tablegroup AS tg ON (t.tenant_id = tg.tenant_id and t.tablegroup_id = tg.tablegroup_id)
WHERE a.tenant_id IN (1002 ) AND t.table_type IN (3)
AND d.database_name IN ( 'tpccdb')
ORDER BY t.tenant_id, tg.tablegroup_name, d.database_name, t.table_name, t2.partition_id
;
+-------------+------------------+---------------+-----------------+----------+--------------+-------+---------------+--------------+-------------------+
| tenant_name | table_name | database_name | tablegroup_name | part_num | partition_id | ZONE | svr_ip | data_size_gb | primary_zone |
+-------------+------------------+---------------+-----------------+----------+--------------+-------+---------------+--------------+-------------------+
| obmysql | bmsql_config | tpccdb | NULL | 1 | 0 | zone2 | 172.20.249.49 | 0 | zone2;zone1,zone3 |
| obmysql | bmsql_item | tpccdb | NULL | 1 | 0 | zone1 | 172.20.249.52 | 0 | zone1;zone2,zone3 |
| obmysql | bmsql_customer | tpccdb | tpcc_group | 3 | 0 | zone2 | 172.20.249.49 | 0 | RANDOM |
| obmysql | bmsql_customer | tpccdb | tpcc_group | 3 | 1 | zone1 | 172.20.249.52 | 0 | RANDOM |
| obmysql | bmsql_customer | tpccdb | tpcc_group | 3 | 2 | zone3 | 172.20.249.51 | 0 | RANDOM |
| obmysql | bmsql_district | tpccdb | tpcc_group | 3 | 0 | zone2 | 172.20.249.49 | 0 | RANDOM |
| obmysql | bmsql_district | tpccdb | tpcc_group | 3 | 1 | zone1 | 172.20.249.52 | 0 | RANDOM |
| obmysql | bmsql_district | tpccdb | tpcc_group | 3 | 2 | zone3 | 172.20.249.51 | 0 | RANDOM |
| obmysql | bmsql_history | tpccdb | tpcc_group | 3 | 0 | zone2 | 172.20.249.49 | 0 | RANDOM |
| obmysql | bmsql_history | tpccdb | tpcc_group | 3 | 1 | zone1 | 172.20.249.52 | 0 | RANDOM |
| obmysql | bmsql_history | tpccdb | tpcc_group | 3 | 2 | zone3 | 172.20.249.51 | 0 | RANDOM |
| obmysql | bmsql_new_order | tpccdb | tpcc_group | 3 | 0 | zone2 | 172.20.249.49 | 0 | RANDOM |
| obmysql | bmsql_new_order | tpccdb | tpcc_group | 3 | 1 | zone1 | 172.20.249.52 | 0 | RANDOM |
| obmysql | bmsql_new_order | tpccdb | tpcc_group | 3 | 2 | zone3 | 172.20.249.51 | 0 | RANDOM |
| obmysql | bmsql_oorder | tpccdb | tpcc_group | 3 | 0 | zone2 | 172.20.249.49 | 0 | RANDOM |
| obmysql | bmsql_oorder | tpccdb | tpcc_group | 3 | 1 | zone1 | 172.20.249.52 | 0 | RANDOM |
| obmysql | bmsql_oorder | tpccdb | tpcc_group | 3 | 2 | zone3 | 172.20.249.51 | 0 | RANDOM |
| obmysql | bmsql_order_line | tpccdb | tpcc_group | 3 | 0 | zone2 | 172.20.249.49 | 0 | RANDOM |
| obmysql | bmsql_order_line | tpccdb | tpcc_group | 3 | 1 | zone1 | 172.20.249.52 | 0 | RANDOM |
| obmysql | bmsql_order_line | tpccdb | tpcc_group | 3 | 2 | zone3 | 172.20.249.51 | 0 | RANDOM |
| obmysql | bmsql_stock | tpccdb | tpcc_group | 3 | 0 | zone2 | 172.20.249.49 | 0 | RANDOM |
| obmysql | bmsql_stock | tpccdb | tpcc_group | 3 | 1 | zone1 | 172.20.249.52 | 0 | RANDOM |
| obmysql | bmsql_stock | tpccdb | tpcc_group | 3 | 2 | zone3 | 172.20.249.51 | 0 | RANDOM |
| obmysql | bmsql_warehouse | tpccdb | tpcc_group | 3 | 0 | zone2 | 172.20.249.49 | 0 | RANDOM |
| obmysql | bmsql_warehouse | tpccdb | tpcc_group | 3 | 1 | zone1 | 172.20.249.52 | 0 | RANDOM |
| obmysql | bmsql_warehouse | tpccdb | tpcc_group | 3 | 2 | zone3 | 172.20.249.51 | 0 | RANDOM |
+-------------+------------------+---------------+-----------------+----------+--------------+-------+---------------+--------------+-------------------+
26 rows in set (0.095 sec)
```
`role` 是副本角色。1 表示 主副本, 2 表示 备副本。
下面 SQL 是 OBPROXY 查看一个具体的分区副本位置。视图 `__all_virtual_proxy_schema` 的访问要求非常严格,必须提供下面 4 个条件。通常不建议运维人员自己查询这个视图。
```sql
select table_name,partition_id, part_num, svr_ip,role, replica_num, table_type
from __all_virtual_proxy_schema
where tenant_name = 'obmysql' and database_name = 'tpccdb' and table_name = 'bmsql_oorder' and partition_id = 1 ;
+--------------+--------------+----------+---------------+------+-------------+------------+
| table_name | partition_id | part_num | svr_ip | role | replica_num | table_type |
+--------------+--------------+----------+---------------+------+-------------+------------+
| bmsql_oorder | 1 | 3 | 172.20.249.49 | 2 | 3 | 3 |
| bmsql_oorder | 1 | 3 | 172.20.249.51 | 2 | 3 | 3 |
| bmsql_oorder | 1 | 3 | 172.20.249.52 | 1 | 3 | 3 |
+--------------+--------------+----------+---------------+------+-------------+------------+
3 rows in set (0.028 sec)
```
OBPROXY 获取到每个分区的位置缓存在进程内部。如果后端 OBSERVER 节点宕机,发生部分分区主备故障切换,OBPROXY 不一定能立即感知到,而是在 SQL 发到老的主副本节点时报错后, OBPROXY 重新获取该分区新的主副本位置,然后更新自己的分区位置缓存。
## 调整 PRIMARY_ZONE 设置
PRIMARY_ZONE 就是设置主副本分布策略。通常可以在租户级别设置。MySQL 租户还可以在数据库级别再设置,数据库设置覆盖租户默认设置。此外,也可以在表分组或表级别设置,表分组和表的 PRIMARY_ZONE 设置会覆盖数据库或者租户的默认设置。
当调整主副本分布策略后,OceanBase 会很快发起在线主备切换。
+ 查看租户的 PRIMARY_ZONE 和 LOCALITY 设置。
```sql
+-----------+-------------+-------------------+---------------------------------------------+
| tenant_id | tenant_name | primary_zone | locality |
+-----------+-------------+-------------------+---------------------------------------------+
| 1 | sys | zone1;zone2,zone3 | FULL{1}@zone1, FULL{1}@zone2, FULL{1}@zone3 |
| 1002 | obmysql | RANDOM | FULL{1}@zone1, FULL{1}@zone2, FULL{1}@zone3 |
+-----------+-------------+-------------------+---------------------------------------------+
2 rows in set (0.011 sec)
```
PRIMARY_ZONE 取值可以有很多组合:
+ `zone1` ,等同于 `zone1; zone2, zone3` 。即主副本优先分布在 `zone1` 的节点上。如果 `zone1` 的节点不可用,会选举 `zone2``zone3` 中的节点。
+ `zone1; zone2; zone3` 。即主副本优先分布在 `zone1`的节点上。如果 `zone1`的节点不可用,优先考虑 `zone2` 的节点,其次才是考虑 `zone3` 的节点。
+ RANDOM 。即主副本随机分布。赋值语句 `set primary_zone =RANDOM` , 不需要单引号。
+ DEFAULT。即清空 PRIMARY_ZONE 设置,遵守默认值,只能设置数据库和表分组、表的 PRIMARY_ZONE 为 `default`
LOCALITY 设置是描述每个 ZONE 里的副本类型,默认情况下都是全功能副本(FULL)。其他副本类型还有:日志副本(LOGONLY)、只读副本(READONLY) 。
+ 查看数据库的 PRIMARY_ZONE 设置。
可以通过查看数据库的定义获取。
```sql
MySQL [test]> alter database test primary_zone='zone2';
Query OK, 0 rows affected (0.310 sec)
MySQL [test]> show create database test;
+----------+-----------------------------------------------------------------------------------------------------------+
| Database | Create Database |
+----------+-----------------------------------------------------------------------------------------------------------+
| test | CREATE DATABASE `test` DEFAULT CHARACTER SET = utf8mb4 REPLICA_NUM = 3 PRIMARY_ZONE = 'zone2;zone1,zone3' |
+----------+-----------------------------------------------------------------------------------------------------------+
1 row in set (0.016 sec)
```
+ 查看表分组和表的 PRIMARY_ZONE 设置
```sql
MySQL [test]> alter tablegroup tpcc_group primary_zone=RANDOM;
Query OK, 0 rows affected (0.427 sec)
MySQL [test]> show create tablegroup tpcc_group\G
*************************** 1. row ***************************
Tablegroup: tpcc_group
Create Tablegroup: CREATE TABLEGROUP IF NOT EXISTS `tpcc_group` PRIMARY_ZONE = RANDOM BINDING = FALSE
partition by hash partitions 3
1 row in set (0.010 sec)
MySQL [test]> alter table t1 primary_zone='zone3;zone2;zone1';
Query OK, 0 rows affected (0.405 sec)
MySQL [test]> show create table t1\G
*************************** 1. row ***************************
Table: t1
Create Table: CREATE TABLE `t1` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`c1` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) AUTO_INCREMENT = 1000001 DEFAULT CHARSET = utf8mb4 ROW_FORMAT = COMPACT COMPRESSION = 'zstd_1.3.8' REPLICA_NUM = 3 PRIMARY_ZONE = 'zone3;zone2;zone1' BLOCK_SIZE = 16384 USE_BLOOM_FILTER = FALSE TABLET_SIZE = 134217728 PCTFREE = 0
1 row in set (0.082 sec)
```
+ 查看分区主备副本切换事件
调整 PRIMARY_ZONE 可能引起分区副本主备角色切换,这个是在线切换,立即生效,对业务影响非常小。
可以通过下面 SQL 查看切换历史
```sql
select t.table_name, h.partition_idx, h.gmt_create, h.svr_ip, h.event, h.leader, h.info
from `__all_virtual_election_event_History` h join __all_virtual_table t on (h.table_id=t.table_id)
where t.table_id >> 40 = 1002
order by h.gmt_create desc
limit 10;
+------------------+---------------+----------------------------+---------------+-----------------+----------------------+--------------------+
| table_name | partition_idx | gmt_create | svr_ip | event | leader | info |
+------------------+---------------+----------------------------+---------------+-----------------+----------------------+--------------------+
| bmsql_order_line | 1 | 2021-10-03 16:31:05.822946 | 172.20.249.52 | leader takevoer | "172.20.249.52:2882" | change leader succ |
| bmsql_order_line | 1 | 2021-10-03 16:31:05.822150 | 172.20.249.49 | leader takevoer | "172.20.249.52:2882" | change leader succ |
| bmsql_order_line | 1 | 2021-10-03 16:31:05.821211 | 172.20.249.51 | leader revoke | "172.20.249.51:2882" | old leader revoke |
| bmsql_order_line | 1 | 2021-10-03 16:31:05.821211 | 172.20.249.51 | leader takevoer | "172.20.249.52:2882" | renew lease succ |
| nation | 0 | 2021-10-03 16:31:05.817790 | 172.20.249.51 | leader takevoer | "172.20.249.51:2882" | change leader succ |
| sbtest7 | 0 | 2021-10-03 16:31:05.817790 | 172.20.249.51 | leader takevoer | "172.20.249.51:2882" | change leader succ |
| sbtest1 | 0 | 2021-10-03 16:31:05.817790 | 172.20.249.51 | leader takevoer | "172.20.249.51:2882" | change leader succ |
| supplier | 3 | 2021-10-03 16:31:05.817790 | 172.20.249.51 | leader takevoer | "172.20.249.51:2882" | change leader succ |
| sbtest8 | 0 | 2021-10-03 16:31:05.817790 | 172.20.249.51 | leader takevoer | "172.20.249.51:2882" | change leader succ |
| customer | 0 | 2021-10-03 16:31:05.817790 | 172.20.249.51 | leader takevoer | "172.20.249.49:2882" | change leader succ |
+------------------+---------------+----------------------------+---------------+-----------------+----------------------+--------------------+
10 rows in set (0.107 sec)
```
## LDC 设置
OceanBase 数据库作为典型的高可用分布式关系型数据库,使用 Paxos 协议进行日志同步,天然支持多地多中心的部署方式以提供高可靠的容灾保证。但当真正多地多中心部署时,任何数据库都会面临异地路由延迟问题。
逻辑数据中心(Logical Data Center,LDC)路由正是为了解决这一问题而设计的,通过给 OceanBase 集群的每个 ZONE 设置 REGION 属性和 IDC 属性,并给 OBProxy 指定 IDC 名称配置项。当 OBPROXY 考虑随机发送时,会优先考虑同一个 IDC 或者同一个 REGION 的可用节点。
配置 LDC 方法:
+ OceanBase 集群节点添加 REGION 和 IDC 属性。
```sql
ALTER SYSTEM MODIFY zone zone1 SET region = SHANGHAI;
ALTER SYSTEM MODIFY zone zone1 SET idc = SH_IDC1;
ALTER SYSTEM MODIFY zone zone2 SET region = SHANGHAI;
ALTER SYSTEM MODIFY zone zone2 SET idc = SH_IDC2;
ALTER SYSTEM MODIFY zone zone3 SET region = HANGZHOU;
ALTER SYSTEM MODIFY zone zone3 SET idc = SH_IDC3;
```
一个集群可以有若干 REGION,一个 REGION 有若干个 ZONE ,每个 ZONE 对应一个 IDC(可以重复,如同机房三副本)。REGION 和 IDC 并不是随意设置就可以。需要保证租户的 PRIMARY_ZONE 里至少有两个 FULL 副本。
```sql
MySQL [oceanbase]> select * from __all_zone where name in ('region','idc');
+----------------------------+----------------------------+-------+--------+-------+---------+
| gmt_create | gmt_modified | zone | name | value | info |
+----------------------------+----------------------------+-------+--------+-------+---------+
| 2021-09-25 08:19:05.067944 | 2021-10-04 14:28:03.262961 | zone1 | idc | 0 | idc1 |
| 2021-09-25 08:19:05.067944 | 2021-10-04 14:29:17.004134 | zone1 | region | 0 | region1 |
| 2021-09-25 08:19:05.068993 | 2021-10-04 14:28:12.236866 | zone2 | idc | 0 | idc2 |
| 2021-09-25 08:19:05.068993 | 2021-10-04 14:29:23.306688 | zone2 | region | 0 | region2 |
| 2021-09-25 08:19:05.070055 | 2021-10-04 14:28:19.560596 | zone3 | idc | 0 | idc3 |
| 2021-09-25 08:19:05.070055 | 2021-10-04 14:29:30.604319 | zone3 | region | 0 | region3 |
+----------------------------+----------------------------+-------+--------+-------+---------+
6 rows in set (0.012 sec)
```
像上面这个设置就有问题,租户的 PRIMARY_ZONE 设置为任意一个 ZONE 都会报错。
```sql
MySQL [oceanbase]> alter tenant sys primary_zone='zone1';
ERROR 4179 (HY000): primary zone F type replica not enough in its region not allowed
MySQL [oceanbase]> alter tenant sys primary_zone='zone1';
Query OK, 0 rows affected (0.095 sec)
MySQL [oceanbase]> select tenant_name,primary_zone,locality from __all_tenant;
+-------------+--------------+---------------------------------------------+
| tenant_name | primary_zone | locality |
+-------------+--------------+---------------------------------------------+
| sys | zone1;zone2 | FULL{1}@zone1, FULL{1}@zone2, FULL{1}@zone3 |
| obmysql | RANDOM | FULL{1}@zone1, FULL{1}@zone2, FULL{1}@zone3 |
+-------------+--------------+---------------------------------------------+
2 rows in set (0.003 sec)
```
此时调整了 ZONE2 的 REGION 属性跟 ZONE1 的 REGION 属性一致后,就可以设置 PRIMARY_ZONE 为 `zone1` 或 `zone2` 。并且 `zone3` 不再出现在候选里了,因为 `zone3` 的 REGION `region3` 里只有一个 FULL 副本,不具备设置为 PRIMARY_ZONE 的资格。
开启 LDC 后,PRIMARY_ZONE 在设置为 RANDOM 还可能会碰到一个限制。
```sql
MySQL [oceanbase]> alter tenant obmysql primary_zone=RANDOM;
ERROR 1235 (0A000): tenant primary zone span regions when GTS is on not supported
```
登录业务租户查看
```sql
MySQL [test]> show global variables like 'ob_timestamp_service';
+----------------------+-------+
| Variable_name | Value |
+----------------------+-------+
| ob_timestamp_service | GTS |
+----------------------+-------+
1 row in set (0.008 sec)
MySQL [test]> set global ob_timestamp_service = LTS;
Query OK, 0 rows affected (0.120 sec)
```
再登录 SYS 租户修改。
```sql
MySQL [oceanbase]> alter tenant obmysql primary_zone=RANDOM;
Query OK, 0 rows affected (0.078 sec)
MySQL [oceanbase]> select tenant_name,primary_zone,locality from __all_tenant;
+-------------+-------------------+---------------------------------------------+
| tenant_name | primary_zone | locality |
+-------------+-------------------+---------------------------------------------+
| sys | zone1;zone2,zone3 | FULL{1}@zone1, FULL{1}@zone2, FULL{1}@zone3 |
| obmysql | RANDOM | FULL{1}@zone1, FULL{1}@zone2, FULL{1}@zone3 |
+-------------+-------------------+---------------------------------------------+
2 rows in set (0.014 sec)
```
这个时候虽然设置成功了,不过由于时间服务改用了 LTS ,那么就不支持跨节点的一致性读快照了。就会遇到下面这个问题。
```sql
MySQL [tpccdb]> select /*+ read_consistency(strong) query_timeout(10000000) */ count(*) from bmsql_warehouse;
ERROR 1235 (0A000): strong consistency across distributed node not supported
MySQL [tpccdb]>
MySQL [tpccdb]> select /*+ read_consistency(weak) */ count(*) from bmsql_warehouse;
+----------+
| count(*) |
+----------+
| 10 |
+----------+
1 row in set (0.269 sec)
```
LTS 服务下不支持跨节点的分布式查询,这个是业务不能接受的。所以反过来,当 OceanBase 集群有多个 REGION 的时候,就不能设置 PRIMARY_ZONE 为 RANDOM 。
+ OBPROXY 的 IDC 属性
OBPROXY 启动时可以指定参数 `proxy_idc_name` 为具体的 IDC ,也可以启动后修改参数值。
```sql
MySQL [(none)]> show proxyconfig like 'proxy_idc_name';
+----------------+-------+-----------------------------------------------------------------------------------------------------------------------------------+-------------+---------------+
| name | value | info | need_reboot | visible_level |
+----------------+-------+-----------------------------------------------------------------------------------------------------------------------------------+-------------+---------------+
| proxy_idc_name | | idc name for proxy ldc route. If is empty or invalid, treat as do not use ldc. User session vars 'proxy_session_ldc' can cover it | false | SYS |
+----------------+-------+-----------------------------------------------------------------------------------------------------------------------------------+-------------+---------------+
1 row in set (0.008 sec)
MySQL [(none)]> alter proxyconfig set proxy_idc_name = 'idc1';
Query OK, 0 rows affected (0.044 sec)
MySQL [(none)]> show proxyconfig like 'proxy_idc_name';
+----------------+-------+-----------------------------------------------------------------------------------------------------------------------------------+-------------+---------------+
| name | value | info | need_reboot | visible_level |
+----------------+-------+-----------------------------------------------------------------------------------------------------------------------------------+-------------+---------------+
| proxy_idc_name | idc1 | idc name for proxy ldc route. If is empty or invalid, treat as do not use ldc. User session vars 'proxy_session_ldc' can cover it | false | SYS |
+----------------+-------+-----------------------------------------------------------------------------------------------------------------------------------+-------------+---------------+
1 row in set (0.051 sec)
MySQL [(none)]> show proxyinfo idc;
+-----------------+--------------+----------------+----------------+--------------+--------------------------+--------------+
| global_idc_name | cluster_name | match_type | regions_name | same_idc | same_region | other_region |
+-----------------+--------------+----------------+----------------+--------------+--------------------------+--------------+
| idc1 | obce-3zones | MATCHED_BY_IDC | [[0]"region1"] | [[0]"zone1"] | [[0]"zone2", [1]"zone3"] | [] |
+-----------------+--------------+----------------+----------------+--------------+--------------------------+--------------+
1 row in set (0.010 sec)
```
## OBPROXY 路由策略简介
OBPROXY 的路由策略非常丰富,本节只是做一个大概的介绍。对于 OBPROXY 的每条路由策略,只需要知道是什么就够了。
### 弱一致性读路由策略
OceanBase 里默认读是强一致性读,即写后读立即可见(`READ AFTER WRITER`)。
强一致性读语句,OBPROXY 会优先路由到访问表的分区的主副本节点上,这点没有疑义。跟强一致性读对立的就是弱一致性读。弱一致性读不要求写后读立即可见,
弱一致性读可以路由到分区的主副本和备副本节点。通常有三副本所在节点可以选。但是开启弱一致性读后,如果 OBSERVER 和 OBPROXY 都开启了 LDC 特性,那么弱一致性读语句的路由策略就是优先路由到同一个机房或者同一个 REGION 的状态不是合并中(`merging`)的节点,其次是合并中的节点,最后是其他 REGION的不在合并的或在合并的节点。
也就是说 OBPROXY 会尽力避开合并中的节点。不过当 OceanBase 集群关闭了轮转合并(参数 `enable_merge_by_turn` 设置为 `false` ),合并(`major freeze`)的时候是所有节点都开始合并了,那么 OBPROXY 也就避不开了合并中的节点。
还有一些 SQL 不是访问数据,而是查看或者设置变量值等。如:
```sql
set @@autocommit=off
show variables like 'autocommit';
```
这类 SQL 的路由策略就是随机路由了。如果 OBSERVER 和 OBPROXY 开启了 LDC 设置,也会遵守上面特别的路由策略。
弱一致性读通常用在读写分离场景。不过在租户 PRIMARY_ZONE 为 RANDOM 的场景下,租户的所有分区的主副本也是分散在所有 ZONE 下,这时候弱一致性读备副本的意义也不是很大。
但是,如果使用了 只读副本。只读副设置为独立的 IDC,然后单独的 OBPROXY 设置为同一个 IDC,则这个 OBPROXY 可以用于只读副本的路由。
### 强一致性读路由策略
路由策略很多,这里由简单到复杂,列举一些。
+ 强一致性读路由策略就是将 SQL 路由到访问的表的分区的主副本所在节点。
这一条理解起来比较简单,实际 SQL 情形很复杂。
+ 如果 SQL 访问了两个表,会依据第一个表及其条件判断出该分区主副本节点。如果得不到,就随机发。所以 SQL 里多表连接时,表的前后顺序对路由策略是有影响的,间接对性能有影响。
+ 补充说明,如果要判断的表是分区表,会看条件是否是分区键等值条件。如果不是,则不能确定是哪个分区,就随机发到该表的所有分区所在的节点任意一个。
+ 如果开启事务了,则事务里开启事务的 SQL 的路由节点会作为事务后面其他 SQL 路由的目标节点,直到事务结束(提交或者回滚)为止。
当SQL 被 OBPROXY 路由到一个节点上时,如果要访问的数据分区的主副本恰好在那个节点上,SQL就在该节点执行,这个SQL的执行类型是本地SQL(`plan_type` 为1)。
如果要访问的数据分区的主副本不在这个节点上,SQL 会被 OBSERVER 再次转发。这个 SQL 的执行类型是 远程SQL(`plan_type` 为2)。
如果 SQL 执行计划要访问的数据分区是跨越多个节点,则这个SQL 的执行类型是 分布式SQL(`plan_type` 为3)。
+ 如果事务中有复制表的读 SQL,只要 SQL 被路由到的节点上有该复制表的备副本,则该 SQL 可以读取本地备副本。因为复制表的所有备副本跟主副本是强一致性。这个SQL 的执行类型是本地 SQL。
实际 SQL 类型很复杂,OBPROXY 的路由策略也变得很复杂。有时候会出现路由不准的情形。如果不符合设计预期就是 BUG,但很可能也是设计如此(`BY DESIGN`)。毕竟当前版本的 OBPROXY 只能做简单的 SQL 解析,不像 OBSERVER 那样做完整的执行计划解析。
当业务 SQL 很多很复杂时,远程 SQL 和分布式 SQL 会无法避免。主要观察其比例。比例很高的话,整体上业务 SQL 性能都不会很好。这时候优化策略就是尽可能的减少远程 SQL 和分布式 SQL 了。 常用的手段就是表分组、复制表和 PRIMARY_ZONE 设置了。
# 如何管理 OceanBase 数据库连接
由于执行计划跟连接方法也有关系,这里先介绍一下 OceanBase 数据库连接的原理。
连接 OceanBase 集群有两种方法:
+ 直连 OceanBase 集群节点的 2881 端口。任意一个 OBSERVER 节点,只要没有脱离集群(指掉线),都可以用来连接 OceanBase 集群。业务连接 OceanBase 集群不适合这个方法,因为节点比较多,且可能会变化。
+ 通过 OBPROXY 的 2883 端口连接 OceanBase 集群。OBPROXY 跟 OceanBase 集群保持联系,能感知集群节点状态变化。多个 OBPROXY 都可以用来连接 OceanBase 集群,OBPROXY 之间彼此独立工作,互不影响。
## OBPROXY 连接原理
OBPROXY 是个单进程程序,默认监听端口 2883 ,实现了 MySQL 连接协议。客户端跟 OBPROXY 建立连接后,访问 OceanBase 集群数据时,OBPROXY 会自动判断要访问的数据的主副本在哪个 OBSERVER 节点上,然后自动跟该 OBSERVER 建立一个长连接。通常我们把客户端跟 OBPROXY 建立的连接称为“前端连接”,把相应的 OBPROXY 跟后端 OBSERVER 节点之间的连接称之为“后端连接”。每个前端连接可能对应 1-N 个 后端连接。 N 是 OceanBase 租户所在的节点总数(包括备副本所在 OBSERVER 节点)。不同前端连接的后端连接是不复用的。
![obproxy connection](media/16332367569719.jpg)
OBPROXY 前端连接数受 OBPROXY 参数 `max_connections``client_max_connections` 限制。
```sql
MySQL [(none)]> show proxyconfig like '%connection%';
+-----------------------------------------+-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------+---------------+
| name | value | info | need_reboot | visible_level |
+-----------------------------------------+-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------+---------------+
| max_connections | 60000 | max fd proxy could use | false | SYS |
| client_max_connections | 8192 | client max connections for one obproxy, [0, 65535] | false | USER |
| enable_client_connection_lru_disconnect | False | if client connections reach throttle, true is that new connection will be accepted, and eliminate lru client connection, false is that new connection will disconnect, and err packet will be returned | false | SYS |
+-----------------------------------------+-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------+---------------+
3 rows in set (0.004 sec)
MySQL [(none)]> alter proxyconfig set client_max_connections=20000;
Query OK, 0 rows affected (0.005 sec)
```
## 如何管理连接
### 直连 OBPROXY 管理连接
+ 查看并直接 KILL 前端连接
`show processlist` 只能查看当前客户端连接,`show proxysession` 只能查看当前 OBPROXY 的连接。
```sql
MySQL [(none)]> show processlist;
+---------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
| Id | Tenant | User | Host | db | trans_count | svr_session_count | state | tid | pid |
+---------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
| 5 | proxysys | root | 172.20.249.50:32108 | NULL | 0 | 0 | MCS_ACTIVE_READER | 49646 | 49646 |
| 524299 | sys | root | 172.20.249.50:32082 | oceanbase | 0 | 1 | MCS_ACTIVE_READER | 49651 | 49646 |
| 1048583 | obmysql | root | 172.20.249.50:32120 | sysbenchdb | 0 | 2 | MCS_ACTIVE_READER | 49652 | 49646 |
+---------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
3 rows in set (0.007 sec)
MySQL [(none)]> show proxysession;
+--------------+---------+-------------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
| proxy_sessid | Id | Cluster | Tenant | User | Host | db | trans_count | svr_session_count | state | tid | pid |
+--------------+---------+-------------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
| 0 | 5 | obce-3zones | proxysys | root | 172.20.249.50:32108 | NULL | 0 | 0 | MCS_ACTIVE_READER | 49646 | 49646 |
| 15 | 524299 | obce-3zones | sys | root | 172.20.249.50:32082 | oceanbase | 0 | 1 | MCS_ACTIVE_READER | 49651 | 49646 |
| 19 | 1048583 | obce-3zones | obmysql | root | 172.20.249.50:32120 | sysbenchdb | 0 | 2 | MCS_ACTIVE_READER | 49652 | 49646 |
+--------------+---------+-------------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
3 rows in set (0.005 sec)
MySQL [(none)]> kill proxysession 1048583;
Query OK, 0 rows affected (0.009 sec)
MySQL [(none)]> show processlist;
+---------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
| Id | Tenant | User | Host | db | trans_count | svr_session_count | state | tid | pid |
+---------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
| 5 | proxysys | root | 172.20.249.50:32108 | NULL | 0 | 0 | MCS_ACTIVE_READER | 49646 | 49646 |
| 524299 | sys | root | 172.20.249.50:32082 | oceanbase | 0 | 1 | MCS_ACTIVE_READER | 49651 | 49646 |
| 1048584 | obmysql | root | 172.20.249.50:32124 | sysbenchdb | 0 | 2 | MCS_ACTIVE_READER | 49652 | 49646 |
+---------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
3 rows in set (0.046 sec)
MySQL [(none)]> show proxysession;
+--------------+---------+-------------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
| proxy_sessid | Id | Cluster | Tenant | User | Host | db | trans_count | svr_session_count | state | tid | pid |
+--------------+---------+-------------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
| 0 | 5 | obce-3zones | proxysys | root | 172.20.249.50:32108 | NULL | 0 | 0 | MCS_ACTIVE_READER | 49646 | 49646 |
| 15 | 524299 | obce-3zones | sys | root | 172.20.249.50:32082 | oceanbase | 0 | 1 | MCS_ACTIVE_READER | 49651 | 49646 |
| 20 | 1048584 | obce-3zones | obmysql | root | 172.20.249.50:32124 | sysbenchdb | 0 | 2 | MCS_ACTIVE_READER | 49652 | 49646 |
+--------------+---------+-------------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
3 rows in set (0.045 sec)
MySQL [(none)]> kill 1048584;
Query OK, 0 rows affected (0.030 sec)
```
`show proxysession` 里的 ID 就是 `show processlist` 中的 ID ,可以使用 `kill [id]``kill proxysession [id]` 杀前端连接。当前端连接被杀后,对应的后端连接也一并中断掉了。
不支持 `kill query` 语法。
+ 查看并直接 KILL 后端连接
```sql
MySQL [(none)]> show processlist;
+---------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
| Id | Tenant | User | Host | db | trans_count | svr_session_count | state | tid | pid |
+---------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
| 5 | proxysys | root | 172.20.249.50:32108 | NULL | 0 | 0 | MCS_ACTIVE_READER | 49646 | 49646 |
| 524299 | sys | root | 172.20.249.50:32082 | oceanbase | 0 | 1 | MCS_ACTIVE_READER | 49651 | 49646 |
| 1048585 | obmysql | root | 172.20.249.50:32130 | sysbenchdb | 0 | 3 | MCS_ACTIVE_READER | 49652 | 49646 |
+---------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
3 rows in set (0.074 sec)
MySQL [(none)]> show proxysession ;
+--------------+---------+-------------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
| proxy_sessid | Id | Cluster | Tenant | User | Host | db | trans_count | svr_session_count | state | tid | pid |
+--------------+---------+-------------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
| 0 | 5 | obce-3zones | proxysys | root | 172.20.249.50:32108 | NULL | 0 | 0 | MCS_ACTIVE_READER | 49646 | 49646 |
| 15 | 524299 | obce-3zones | sys | root | 172.20.249.50:32082 | oceanbase | 0 | 1 | MCS_ACTIVE_READER | 49651 | 49646 |
| 21 | 1048585 | obce-3zones | obmysql | root | 172.20.249.50:32130 | sysbenchdb | 0 | 3 | MCS_ACTIVE_READER | 49652 | 49646 |
+--------------+---------+-------------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
3 rows in set (0.200 sec)
MySQL [(none)]> show proxysession attribute 1048585 ;
+----------------------------------+---------------------+----------------+
| attribute_name | value | info |
+----------------------------------+---------------------+----------------+
| proxy_sessid | 21 | cs common |
| cs_id | 1048585 | cs common |
| cluster | obce-3zones | cs common |
| tenant | obmysql | cs common |
| user | root | cs common |
| host_ip | 172.20.249.50 | cs common |
| host_port | 32130 | cs common |
| db | sysbenchdb | cs common |
| total_trans_cnt | 0 | cs common |
| svr_session_cnt | 3 | cs common |
| active | false | cs common |
| read_state | MCS_ACTIVE_READER | cs common |
| tid | 49652 | cs common |
| pid | 49646 | cs common |
| idc_name | | cs common |
| modified_time | 0 | cs stat |
| reported_time | 0 | cs stat |
| hot_sys_var_version | 1 | cs var version |
| sys_var_version | 3 | cs var version |
| user_var_version | 0 | cs var version |
| last_insert_id_version | 0 | cs var version |
| db_name_version | 1 | cs var version |
| server_ip | 172.20.249.52 | last used ss |
| server_port | 2881 | last used ss |
| server_sessid | 3221634194 | last used ss |
| ss_id | 34 | last used ss |
| state | MSS_KA_CLIENT_SLAVE | last used ss |
| transact_count | 2 | last used ss |
| server_trans_stat | 0 | last used ss |
| hot_sys_var_version | 1 | last used ss |
| sys_var_version | 3 | last used ss |
| user_var_version | 0 | last used ss |
| last_insert_id_version | 0 | last used ss |
| db_name_version | 1 | last used ss |
| is_checksum_supported | 1 | last used ss |
| is_safe_read_weak_supported | 0 | last used ss |
| is_checksum_switch_supported | 1 | last used ss |
| checksum_switch | 1 | last used ss |
| enable_extra_ok_packet_for_stats | 1 | last used ss |
| server_ip | 172.20.249.51 | ss pool [0] |
| server_port | 2881 | ss pool [0] |
| server_sessid | 3222242117 | ss pool [0] |
| ss_id | 36 | ss pool [0] |
| state | MSS_KA_SHARED | ss pool [0] |
| transact_count | 2 | ss pool [0] |
| server_trans_stat | 0 | ss pool [0] |
| hot_sys_var_version | 1 | ss pool [0] |
| sys_var_version | 2 | ss pool [0] |
| user_var_version | 0 | ss pool [0] |
| last_insert_id_version | 0 | ss pool [0] |
| db_name_version | 1 | ss pool [0] |
| is_checksum_supported | 1 | ss pool [0] |
| is_safe_read_weak_supported | 0 | ss pool [0] |
| is_checksum_switch_supported | 1 | ss pool [0] |
| checksum_switch | 1 | ss pool [0] |
| enable_extra_ok_packet_for_stats | 1 | ss pool [0] |
| server_ip | 172.20.249.49 | ss pool [1] |
| server_port | 2881 | ss pool [1] |
| server_sessid | 3221895002 | ss pool [1] |
| ss_id | 35 | ss pool [1] |
| state | MSS_KA_SHARED | ss pool [1] |
| transact_count | 1 | ss pool [1] |
| server_trans_stat | 0 | ss pool [1] |
| hot_sys_var_version | 1 | ss pool [1] |
| sys_var_version | 2 | ss pool [1] |
| user_var_version | 0 | ss pool [1] |
| last_insert_id_version | 0 | ss pool [1] |
| db_name_version | 1 | ss pool [1] |
| is_checksum_supported | 1 | ss pool [1] |
| is_safe_read_weak_supported | 0 | ss pool [1] |
| is_checksum_switch_supported | 1 | ss pool [1] |
| checksum_switch | 1 | ss pool [1] |
| enable_extra_ok_packet_for_stats | 1 | ss pool [1] |
+----------------------------------+---------------------+----------------+
73 rows in set (0.081 sec)
```
通过 `show proxysession attribute [id]` 可以查看前端连接对应的后端连接。
+ {`proxy_sessid``cs_id``host_ip``host_port`}为前端连接元组。其中 `cs_id` 是 OBPROXY 内部标识的前端连接(客户端连接)的 ID 号,跟 `show processlist` 的 ID 列是一致的。
+ { `server_ip`, `server_port`, `server_sessid`,`ss_id` } 组成后端连接元组。 其中 `ss_id` 是 OBPROXY 内部标识的后端连接(OBPROXY 跟 OBSERVER 连接)的 ID 号。`server_sessid` 是 OBSERVER 上的客户端连接 ID 。
下面示例指定了前端连接标识和后端连接标识,可以针对性的杀后端连接。
```sql
MySQL [(none)]> kill proxysession 1048585 35;
Query OK, 0 rows affected (0.002 sec)
MySQL [(none)]> show proxysession attribute 1048585 ;
+----------------------------------+---------------------+----------------+
| attribute_name | value | info |
+----------------------------------+---------------------+----------------+
| proxy_sessid | 21 | cs common |
| cs_id | 1048585 | cs common |
| cluster | obce-3zones | cs common |
| tenant | obmysql | cs common |
| user | root | cs common |
| host_ip | 172.20.249.50 | cs common |
| host_port | 32130 | cs common |
| db | sysbenchdb | cs common |
| total_trans_cnt | 0 | cs common |
| svr_session_cnt | 2 | cs common |
| active | false | cs common |
| read_state | MCS_ACTIVE_READER | cs common |
| tid | 49652 | cs common |
| pid | 49646 | cs common |
| idc_name | | cs common |
| modified_time | 0 | cs stat |
| reported_time | 0 | cs stat |
| hot_sys_var_version | 1 | cs var version |
| sys_var_version | 3 | cs var version |
| user_var_version | 0 | cs var version |
| last_insert_id_version | 0 | cs var version |
| db_name_version | 1 | cs var version |
| server_ip | 172.20.249.52 | last used ss |
| server_port | 2881 | last used ss |
| server_sessid | 3221634194 | last used ss |
| ss_id | 34 | last used ss |
| state | MSS_KA_CLIENT_SLAVE | last used ss |
| transact_count | 2 | last used ss |
| server_trans_stat | 0 | last used ss |
| hot_sys_var_version | 1 | last used ss |
| sys_var_version | 3 | last used ss |
| user_var_version | 0 | last used ss |
| last_insert_id_version | 0 | last used ss |
| db_name_version | 1 | last used ss |
| is_checksum_supported | 1 | last used ss |
| is_safe_read_weak_supported | 0 | last used ss |
| is_checksum_switch_supported | 1 | last used ss |
| checksum_switch | 1 | last used ss |
| enable_extra_ok_packet_for_stats | 1 | last used ss |
| server_ip | 172.20.249.51 | ss pool [0] |
| server_port | 2881 | ss pool [0] |
| server_sessid | 3222242117 | ss pool [0] |
| ss_id | 36 | ss pool [0] |
| state | MSS_KA_SHARED | ss pool [0] |
| transact_count | 2 | ss pool [0] |
| server_trans_stat | 0 | ss pool [0] |
| hot_sys_var_version | 1 | ss pool [0] |
| sys_var_version | 2 | ss pool [0] |
| user_var_version | 0 | ss pool [0] |
| last_insert_id_version | 0 | ss pool [0] |
| db_name_version | 1 | ss pool [0] |
| is_checksum_supported | 1 | ss pool [0] |
| is_safe_read_weak_supported | 0 | ss pool [0] |
| is_checksum_switch_supported | 1 | ss pool [0] |
| checksum_switch | 1 | ss pool [0] |
| enable_extra_ok_packet_for_stats | 1 | ss pool [0] |
+----------------------------------+---------------------+----------------+
56 rows in set (0.026 sec)
```
### 通过 OBPROXY 连接 OceanBase 集群管理连接
+ 使用 OBPROXY 连接 OceanBase 集群
```bash
obclient -h172.20.249.52 -uroot@sys#obce-3zones -P2883 -p0EI5N08d -c -A oceanbase
```
在通过 OBPROXY 的连接里,`show processlist``show proxysession` 只能查看当前 OBPROXY 的客户端连接,`show proxysession` 可能看到其他 OceanBase 集群的连接(如果这个 OBPROXY 还可以为其他集群提供路由服务),`show full processlist` 能看到 OceanBase 集群的全部后端连接,包括通过其他 OBPROXY 连接过来的。
在这个连接里可以针对性的 KILL 当前 OBPROXY 的后端连接(不能 KILL 其他 OBPROXY 的后端连接),但是前端连接只能 kILL 自己,不能 KILL 其他连接。KILL 的时候支持 KILL CONNECTION 或 KILL QUERY 。
```sql
MySQL [oceanbase]> show proxysession;
+--------------+---------+-------------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
| proxy_sessid | Id | Cluster | Tenant | User | Host | db | trans_count | svr_session_count | state | tid | pid |
+--------------+---------+-------------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
| 23 | 524301 | obce-3zones | sys | root | 172.20.249.50:32214 | oceanbase | 0 | 1 | MCS_ACTIVE_READER | 49651 | 49646 |
| 21 | 1048585 | obce-3zones | obmysql | root | 172.20.249.50:32130 | sysbenchdb | 0 | 3 | MCS_ACTIVE_READER | 49652 | 49646 |
| 0 | 6 | obce-3zones | proxysys | root | 172.20.249.50:32192 | NULL | 0 | 0 | MCS_ACTIVE_READER | 49646 | 49646 |
+--------------+---------+-------------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
3 rows in set (0.001 sec)
MySQL [oceanbase]> show full processlist;
+------------+---------+---------+---------------------+------------+---------+------+--------+-----------------------+---------------+------+--------------+
| Id | User | Tenant | Host | db | Command | Time | State | Info | Ip | Port | Proxy_sessid |
+------------+---------+---------+---------------------+------------+---------+------+--------+-----------------------+---------------+------+--------------+
| 3222242175 | root | obmysql | 172.20.249.52:12714 | sysbenchdb | Sleep | 204 | SLEEP | NULL | 172.20.249.51 | 2881 | 21 |
| 3222242191 | root | sys | 172.20.249.52:12736 | oceanbase | Query | 0 | ACTIVE | show full processlist | 172.20.249.51 | 2881 | 23 |
| 3221895066 | root | obmysql | 172.20.249.52:4454 | sysbenchdb | Sleep | 200 | SLEEP | NULL | 172.20.249.49 | 2881 | 21 |
| 3221611294 | proxyro | sys | 172.20.249.52:55222 | oceanbase | Sleep | 13 | SLEEP | NULL | 172.20.249.52 | 2881 | 3 |
| 3221634194 | root | obmysql | 172.20.249.52:55346 | sysbenchdb | Sleep | 219 | SLEEP | NULL | 172.20.249.52 | 2881 | 21 |
+------------+---------+---------+---------------------+------------+---------+------+--------+-----------------------+---------------+------+--------------+
5 rows in set (0.026 sec)
MySQL [oceanbase]> kill 3222242191;
ERROR 1317 (70100): Query execution was interrupted
MySQL [oceanbase]> show full processlist;
ERROR 2013 (HY000): Lost connection to MySQL server during query
MySQL [oceanbase]> show full processlist;
ERROR 2006 (HY000): MySQL server has gone away
No connection. Trying to reconnect...
Connection id: 524302
Current database: oceanbase
+------------+---------+---------+---------------------+------------+---------+------+--------+-----------------------+---------------+------+--------------+
| Id | User | Tenant | Host | db | Command | Time | State | Info | Ip | Port | Proxy_sessid |
+------------+---------+---------+---------------------+------------+---------+------+--------+-----------------------+---------------+------+--------------+
| 3221659683 | root | sys | 172.20.249.52:55448 | oceanbase | Query | 0 | ACTIVE | show full processlist | 172.20.249.52 | 2881 | 24 |
| 3221611294 | proxyro | sys | 172.20.249.52:55222 | oceanbase | Sleep | 5 | SLEEP | NULL | 172.20.249.52 | 2881 | 3 |
| 3221634194 | root | obmysql | 172.20.249.52:55346 | sysbenchdb | Sleep | 231 | SLEEP | NULL | 172.20.249.52 | 2881 | 21 |
| 3221895066 | root | obmysql | 172.20.249.52:4454 | sysbenchdb | Sleep | 211 | SLEEP | NULL | 172.20.249.49 | 2881 | 21 |
| 3222242175 | root | obmysql | 172.20.249.52:12714 | sysbenchdb | Sleep | 216 | SLEEP | NULL | 172.20.249.51 | 2881 | 21 |
+------------+---------+---------+---------------------+------------+---------+------+--------+-----------------------+---------------+------+--------------+
5 rows in set (0.060 sec)
MySQL [oceanbase]> kill 3221634194;
ERROR 1094 (HY000): Unknown thread id: 3221634194
```
如果要 KILL 特定后端连接,得先通过后端 ID 找到 `proxysess_id` ,然后找到对应的前端连接 ID, 再通过命令 `show proxysession attribute [id]` 找到后端连接的 `ss_id`
```sql
MySQL [oceanbase]> show proxysession;
+--------------+---------+-------------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
| proxy_sessid | Id | Cluster | Tenant | User | Host | db | trans_count | svr_session_count | state | tid | pid |
+--------------+---------+-------------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
| 24 | 524302 | obce-3zones | sys | root | 172.20.249.50:32218 | oceanbase | 0 | 1 | MCS_ACTIVE_READER | 49651 | 49646 |
| 21 | 1048585 | obce-3zones | obmysql | root | 172.20.249.50:32130 | sysbenchdb | 0 | 3 | MCS_ACTIVE_READER | 49652 | 49646 |
| 0 | 6 | obce-3zones | proxysys | root | 172.20.249.50:32192 | NULL | 0 | 0 | MCS_ACTIVE_READER | 49646 | 49646 |
+--------------+---------+-------------+----------+------+---------------------+------------+-------------+-------------------+-------------------+-------+-------+
3 rows in set (0.019 sec)
MySQL [oceanbase]> show proxysession attribute 1048585;
+----------------------------------+---------------------+----------------+
| attribute_name | value | info |
+----------------------------------+---------------------+----------------+
| proxy_sessid | 21 | cs common |
| cs_id | 1048585 | cs common |
| cluster | obce-3zones | cs common |
| tenant | obmysql | cs common |
| user | root | cs common |
| host_ip | 172.20.249.50 | cs common |
| host_port | 32130 | cs common |
| db | sysbenchdb | cs common |
| total_trans_cnt | 0 | cs common |
| svr_session_cnt | 3 | cs common |
| active | false | cs common |
| read_state | MCS_ACTIVE_READER | cs common |
| tid | 49652 | cs common |
| pid | 49646 | cs common |
| idc_name | | cs common |
| modified_time | 0 | cs stat |
| reported_time | 0 | cs stat |
| hot_sys_var_version | 1 | cs var version |
| sys_var_version | 3 | cs var version |
| user_var_version | 0 | cs var version |
| last_insert_id_version | 0 | cs var version |
| db_name_version | 1 | cs var version |
| server_ip | 172.20.249.49 | last used ss |
| server_port | 2881 | last used ss |
| server_sessid | 3221895066 | last used ss |
| ss_id | 38 | last used ss |
| state | MSS_KA_CLIENT_SLAVE | last used ss |
| transact_count | 1 | last used ss |
| server_trans_stat | 0 | last used ss |
| hot_sys_var_version | 1 | last used ss |
| sys_var_version | 3 | last used ss |
| user_var_version | 0 | last used ss |
| last_insert_id_version | 0 | last used ss |
| db_name_version | 1 | last used ss |
| is_checksum_supported | 1 | last used ss |
| is_safe_read_weak_supported | 0 | last used ss |
| is_checksum_switch_supported | 1 | last used ss |
| checksum_switch | 1 | last used ss |
| enable_extra_ok_packet_for_stats | 1 | last used ss |
| server_ip | 172.20.249.51 | ss pool [0] |
| server_port | 2881 | ss pool [0] |
| server_sessid | 3222242175 | ss pool [0] |
| ss_id | 37 | ss pool [0] |
| state | MSS_KA_SHARED | ss pool [0] |
| transact_count | 2 | ss pool [0] |
| server_trans_stat | 0 | ss pool [0] |
| hot_sys_var_version | 1 | ss pool [0] |
| sys_var_version | 3 | ss pool [0] |
| user_var_version | 0 | ss pool [0] |
| last_insert_id_version | 0 | ss pool [0] |
| db_name_version | 1 | ss pool [0] |
| is_checksum_supported | 1 | ss pool [0] |
| is_safe_read_weak_supported | 0 | ss pool [0] |
| is_checksum_switch_supported | 1 | ss pool [0] |
| checksum_switch | 1 | ss pool [0] |
| enable_extra_ok_packet_for_stats | 1 | ss pool [0] |
| server_ip | 172.20.249.52 | ss pool [1] |
| server_port | 2881 | ss pool [1] |
| server_sessid | 3221634194 | ss pool [1] |
| ss_id | 34 | ss pool [1] |
| state | MSS_KA_SHARED | ss pool [1] |
| transact_count | 3 | ss pool [1] |
| server_trans_stat | 0 | ss pool [1] |
| hot_sys_var_version | 1 | ss pool [1] |
| sys_var_version | 3 | ss pool [1] |
| user_var_version | 0 | ss pool [1] |
| last_insert_id_version | 0 | ss pool [1] |
| db_name_version | 1 | ss pool [1] |
| is_checksum_supported | 1 | ss pool [1] |
| is_safe_read_weak_supported | 0 | ss pool [1] |
| is_checksum_switch_supported | 1 | ss pool [1] |
| checksum_switch | 1 | ss pool [1] |
| enable_extra_ok_packet_for_stats | 1 | ss pool [1] |
+----------------------------------+---------------------+----------------+
73 rows in set (0.001 sec)
MySQL [oceanbase]> kill proxysession 1048585 34 ;
Query OK, 0 rows affected (0.010 sec)
```
后端连接的被 kILL ,客户端时候感知不到的,再次查询的时候 OBPROXY 会自动建立新的后端连接。
### 直连 OBSERVER 管理连接
在 OBPROXY 的连接里查看后端连接的命令是 :`show full processlist` ,里面的 ID 列就是 OBSERVER 里客户端连接 ID 。可以在 OBSERVER 内部直接用 KILL QUERY 或 KILL CONNECTION 。
```sql
obclient -h172.20.249.52 -uroot@sys#obce-3zones -P2883 -p0EI5N08d -c -A oceanbase
MySQL [oceanbase]> show full processlist;
+------------+---------+---------+---------------------+------------+---------+------+--------+-----------------------+---------------+------+--------------+
| Id | User | Tenant | Host | db | Command | Time | State | Info | Ip | Port | Proxy_sessid |
+------------+---------+---------+---------------------+------------+---------+------+--------+-----------------------+---------------+------+--------------+
| 3221659683 | root | sys | 172.20.249.52:55448 | oceanbase | Query | 0 | ACTIVE | show full processlist | 172.20.249.52 | 2881 | 24 |
| 3221611294 | proxyro | sys | 172.20.249.52:55222 | oceanbase | Sleep | 16 | SLEEP | NULL | 172.20.249.52 | 2881 | 3 |
| 3221666993 | root | obmysql | 127.0.0.1:45726 | sysbenchdb | Sleep | 3 | SLEEP | NULL | 172.20.249.52 | 2881 | NULL |
| 3221663293 | root | obmysql | 172.20.249.52:55458 | sysbenchdb | Sleep | 281 | SLEEP | NULL | 172.20.249.52 | 2881 | 21 |
| 3221895066 | root | obmysql | 172.20.249.52:4454 | sysbenchdb | Sleep | 291 | SLEEP | NULL | 172.20.249.49 | 2881 | 21 |
| 3222242175 | root | obmysql | 172.20.249.52:12714 | sysbenchdb | Sleep | 285 | SLEEP | NULL | 172.20.249.51 | 2881 | 21 |
+------------+---------+---------+---------------------+------------+---------+------+--------+-----------------------+---------------+------+--------------+
6 rows in set (0.010 sec)
```
说明:
+ `Id`:后端连接的客户端 ID 。整个 OceanBase 集群内部唯一。
+ `Host`:前端连接的客户端 `IP:PORT` ,发起数据库连接的客户端信息。
+ `Ip`:后端连接的服务端 IP。即 OBSERVER 的 IP 。
+ `Proxy_sessid` :前端连接的 OBPROXY 内部ID,该 OBPROXY 内部唯一,不同 OBPROXY 的 `Proxy_sessid` 会重复。如果为 NULL ,表示是直连 OBSERVER 建立的连接。
以上同样的连接查看命令,到直连 OBSERVER 连接里查询,结果会有点不一样。
```sql
mysql -h127.1 -P2881 -uroot@obmysql -p123456 -c -A sysbenchdb
mysql> show processlist;
+------------+------+---------------------+------------+---------+------+--------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+------------+------+---------------------+------------+---------+------+--------+------------------+
| 3222242175 | root | 172.20.249.52:12714 | sysbenchdb | Sleep | 312 | SLEEP | NULL |
| 3221895066 | root | 172.20.249.52:4454 | sysbenchdb | Sleep | 318 | SLEEP | NULL |
| 3221666993 | root | 127.0.0.1:45726 | sysbenchdb | Query | 0 | ACTIVE | show processlist |
| 3221663293 | root | 172.20.249.52:55458 | sysbenchdb | Sleep | 308 | SLEEP | NULL |
+------------+------+---------------------+------------+---------+------+--------+------------------+
4 rows in set (0.05 sec)
```
说明:
+ `Host`:客户端连接信息(`IP:PORT`)。通常就是 OBPROXY 地址 或者发起连接的客户端。
```sql
mysql> show full processlist;
+------------+------+---------+---------------------+------------+---------+------+--------+-----------------------+---------------+------+
| Id | User | Tenant | Host | db | Command | Time | State | Info | Ip | Port |
+------------+------+---------+---------------------+------------+---------+------+--------+-----------------------+---------------+------+
| 3221666993 | root | obmysql | 127.0.0.1:45726 | sysbenchdb | Query | 0 | ACTIVE | show full processlist | 172.20.249.52 | 2881 |
| 3221663293 | root | obmysql | 172.20.249.52:55458 | sysbenchdb | Sleep | 312 | SLEEP | NULL | 172.20.249.52 | 2881 |
| 3221895066 | root | obmysql | 172.20.249.52:4454 | sysbenchdb | Sleep | 321 | SLEEP | NULL | 172.20.249.49 | 2881 |
| 3222242175 | root | obmysql | 172.20.249.52:12714 | sysbenchdb | Sleep | 315 | SLEEP | NULL | 172.20.249.51 | 2881 |
+------------+------+---------+---------------------+------------+---------+------+--------+-----------------------+---------------+------+
4 rows in set (0.00 sec)
mysql> show proxysession ;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your OceanBase version for the right syntax to use near 'proxysession' at line 1
mysql>
```
说明:
+ `Host`:前端连接的客户端,也就是发起连接的客户端信息(`IP:PORT`)。通常就是 OBPROXY 地址或者发起连接的客户端。
+ `Ip`:后端连接的服务端地址,通常就是 OBSERVER 地址。
+ `Id`:后端连接的ID 标识,整个 OceanBase 集群内部唯一。可以直接 KILL QUERY 或 KILL CONNECTION 。
直连 OBSERVER 的连接里,不支持 OBPROXY 的管理命令。
```sql
mysql> kill 3221663293;
Query OK, 0 rows affected (0.02 sec)
mysql> kill 3221895066;
Query OK, 0 rows affected (0.14 sec)
mysql> kill 3222242175;
Query OK, 0 rows affected (0.02 sec)
```
跟前面一样,如果后端连接被 KILL 了,客户端重新查询的时候, OBPROXY 会自动跟 OBSERVER 建立后端连接。
## 视图 `__all_virtual_processlist`
视图 `__all_virtual_processlist` 能查看到 OceanBase 集群里所有连接,包括通过 OBPROXY 连接过来的后端连接以及直连集群 OBSERVER 节点的连接。
```sql
SELECT id, host, command, sql_id, time, state, info, svr_ip, proxy_sessid, user_client_ip, trans_id ,thread_id, trace_id
FROM __all_virtual_processlist
where 1=1
;
输出:
+------------+---------------------+---------+----------------------------------+------+--------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+--------------+----------------+----------------------+-----------+-------------------------------+
| id | host | command | sql_id | time | state | info | svr_ip | proxy_sessid | user_client_ip | trans_id | thread_id | trace_id |
+------------+---------------------+---------+----------------------------------+------+--------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+--------------+----------------+----------------------+-----------+-------------------------------+
| 3222242518 | 172.20.249.52:19440 | Query | 17115A49B6D58958854D9B2E58CB821A | 0 | ACTIVE | SELECT id, host, command, sql_id, time, state, info, svr_ip, proxy_sessid, user_client_ip, trans_id ,thread_id, trace_id
FROM __all_virtual_processlist
where 1=1 | 172.20.249.51 | 26 | 172.20.249.50 | 0 | 19240 | YB42AC14F933-0005CCDFC90D9A46 |
| 3222242521 | 172.20.249.52:19442 | Sleep | | 458 | SLEEP | NULL | 172.20.249.51 | 27 | 192.168.0.6 | 0 | 0 | NULL |
| 3222242532 | 172.20.249.49:48664 | Sleep | | 1167 | SLEEP | NULL | 172.20.249.51 | 8 | 172.20.249.49 | 0 | 0 | NULL |
| 3221895453 | 172.20.249.52:11250 | Sleep | | 13 | SLEEP | NULL | 172.20.249.49 | 28 | 192.168.0.6 | 0 | 0 | NULL |
| 3221895423 | 172.20.249.52:11218 | Sleep | | 1024 | SLEEP | NULL | 172.20.249.49 | 31 | 172.20.249.50 | 0 | 0 | NULL |
| 3221611294 | 172.20.249.52:55222 | Sleep | | 9 | SLEEP | NULL | 172.20.249.52 | 3 | 172.20.249.52 | 15795690051229394980 | 0 | NULL |
| 3221591229 | 172.20.249.49:11510 | Sleep | | 1226 | SLEEP | NULL | 172.20.249.52 | 2 | 172.20.249.49 | 0 | 0 | NULL |
| 3221594049 | 172.20.249.52:62192 | Sleep | | 998 | SLEEP | NULL | 172.20.249.52 | 31 | 172.20.249.50 | 13683594962028267401 | 0 | NULL |
| 3221583881 | 172.20.249.52:62154 | Sleep | | 384 | SLEEP | NULL | 172.20.249.52 | 28 | 192.168.0.6 | 0 | 0 | NULL |
| 3221591227 | 172.20.249.49:11512 | Sleep | | 4 | SLEEP | NULL | 172.20.249.52 | 3 | 172.20.249.49 | 283711989801967624 | 0 | NULL |
| 3221603549 | 127.0.0.1:52488 | Sleep | | 324 | SLEEP | NULL | 172.20.249.52 | NULL | 127.0.0.1 | 0 | 0 | NULL |
+------------+---------------------+---------+----------------------------------+------+--------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------+--------------+----------------+----------------------+-----------+-------------------------------+
11 rows in set (0.011 sec)
```
这个视图的结果跟命令 `show full processlist` 的结果基本一致。这里再说明一下。
+ `proxy_sessid` :如果为 NULL ,表示直连 OBSERVER 的连接;如果不为 NULL 表示通过 OBPROXY 建立的连接。
+ `host` :是连接的客户端。通常是 OBPROXY 地址,如果是直连的连接,就是发起连接的那个实际客户端地址。
+ `svr_ip` :是连接的服务端。通常是 OBSERVER 地址。
+ `user_client_ip` :是连接的实际客户端地址,即发起连接的客户端地址。
+ `state`:后端连接的状态,通常状态是 SLEEP 。如果连接正在执行 SQL,状态是 ACTIVE 。
+ `time` : 后端连接当前状态持续时间,单位秒。如果状态发生变化,就重新计时。
+ `info` : 连接正在执行的 SQL 。如果 SQL 很快,很难捕捉到。
+ `sql_id` :连接正在执行的 SQL 的 SQLID ,可以根据这个 SQLID 去查执行计划。
+ `trans_id` : 连接的事务的 TRANS_ID ,这个不准确,供参考。如果连接开启了事务,这个是真正的事务 ID 。如果事务提交了后,在开启新事务之前,连接的这个 TRANS_ID 不一定会变化。
+ `trace_id` : 是连接用的 TRACE_ID,可以在 OBSERVER 的运行日志中根据这个 TRACE_ID 追踪相关日志。
## 分析连接的事务超时问题
事务有两个超时时间,分别通过参数 `ob_trx_idle_timeout``ob_trx_timeout` 控制。前者是事务空闲超时,后者是事务未提交超时。
为了研究这两个参数的影响, 下面示例故意把两个参数的值差异拉大。
### 事务空闲超时
客户端 1 查看空闲超时时间。事务空闲超时设置为 120 秒,事务未提交超时设置为 1000 秒。前者先超时。事务空闲实际超时时间会在一个范围内 [120 s, 120+10 s) 。
```sql
set global ob_trx_idle_timeout=120000000;
set global ob_trx_timeout=1000000000;
show global variables where variable_name in ('ob_trx_idle_timeout','ob_trx_timeout');
+---------------------+------------+
| Variable_name | Value |
+---------------------+------------+
| ob_trx_idle_timeout | 120000000 |
| ob_trx_timeout | 1000000000 |
+---------------------+------------+
2 rows in set (0.040 sec)
```
客户端 2 新建连接开启事务。
```sql
MySQL [test]> begin; update t1 set c1=now() where id=3;
Query OK, 0 rows affected (0.002 sec)
Query OK, 1 row affected (0.023 sec)
Rows matched: 1 Changed: 1 Warnings: 0
MySQL [test]> select * from t1;
+----+---------------------+
| id | c1 |
+----+---------------------+
| 1 | 2021-10-03 09:22:20 |
| 2 | 2021-09-29 21:16:05 |
| 3 | 2021-10-03 10:41:59 |
| 4 | 2021-09-29 21:16:06 |
| 5 | 2021-09-29 21:16:06 |
| 6 | 2021-09-29 21:16:18 |
| 7 | 2021-10-03 09:35:18 |
| 8 | 2021-09-29 21:16:19 |
+----+---------------------+
8 rows in set (0.029 sec)
```
客户端 2 停止操作一段时间。
客户端1 查看连接状态。
```sql
MySQL [oceanbase]> SELECT id, host, command, sql_id, time, state, info, svr_ip, proxy_sessid, user_client_ip, trans_id ,thread_id, trace_id FROM __all_virtual_processlist where 1=1 and id=3222242958 order by id;
+------------+---------------------+---------+--------+------+-------+------+---------------+--------------+----------------+---------------------+-----------+----------+
| id | host | command | sql_id | time | state | info | svr_ip | proxy_sessid | user_client_ip | trans_id | thread_id | trace_id |
+------------+---------------------+---------+--------+------+-------+------+---------------+--------------+----------------+---------------------+-----------+----------+
| 3222243128 | 172.20.249.52:19954 | Sleep | | 121 | SLEEP | NULL | 172.20.249.51 | 45 | 172.20.249.50 | 6678987957559799424 | 0 | NULL |
+------------+---------------------+---------+--------+------+-------+------+---------------+--------------+----------------+---------------------+-----------+----------+
1 row in set (0.007 sec)
```
`time` 列是连接空闲时间。
观察客户端 2 连接。
```sql
MySQL [test]> select * from t1;
ERROR 6002 (25000): transaction needs rollback
MySQL [test]>
```
可见,OceanBase 3.1 版本后对于事务空闲超时的处理行为变为不中断连接,而是提示事务要回滚。此时不管使用 COMMIT 还是 ROLLBACK ,都可以清理事务状态。下面示例故意用 COMMIT,实际上使用 ROLLBACK 更符合用户习惯。
```sql
MySQL [test]> commit;
ERROR 6002 (40000): Transaction rollbacked
MySQL [test]> select * from t1;
+----+---------------------+
| id | c1 |
+----+---------------------+
| 1 | 2021-10-03 09:22:20 |
| 2 | 2021-09-29 21:16:05 |
| 3 | 2021-09-29 21:16:05 |
| 4 | 2021-09-29 21:16:06 |
| 5 | 2021-09-29 21:16:06 |
| 6 | 2021-09-29 21:16:18 |
| 7 | 2021-10-03 09:35:18 |
| 8 | 2021-09-29 21:16:19 |
+----+---------------------+
8 rows in set (0.003 sec)
```
注意:不管是哪种超时,连接事务所持有的锁都会释放,只是事务的状态需要客户端主动清理一下。对于应用而言,就是需要捕捉事务超时报错,然后发起 ROLLBACK 。
### 事务未提交超时
客户端 1 查看空闲超时时间。事务空闲超时设置为 120 秒,事务未提交超时设置为 1000 秒。前者先超时。事务空闲实际超时时间会在一个范围内 [120 s, 120+10 s) 。
```sql
set global ob_trx_idle_timeout=1200000000;
set global ob_trx_timeout=100000000;
show variables where variable_name in ('ob_trx_idle_timeout','ob_trx_timeout');
+---------------------+------------+
| Variable_name | Value |
+---------------------+------------+
| ob_trx_idle_timeout | 120000000 |
| ob_trx_timeout | 1000000000 |
+---------------------+------------+
2 rows in set (0.003 sec)
```
客户端 2 新建连接开启事务。
```sql
MySQL [test]> begin; update t1 set c1=now() where id=3;
Query OK, 0 rows affected (0.001 sec)
Query OK, 1 row affected (0.005 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
客户端 2 停止操作一段时间。
```sql
MySQL [test]> select * from t1;
ERROR 4012 (25000): Transaction is timeout
```
此时连接处于“事务超时状态”。需要用 COMMIT 或者 ROLLBACK 清除状态。通常用 ROLLBACK 更符合用户习惯。下面示例故意用 COMMIT ,但是事务修改早已经回滚掉了。
```sql
MySQL [test]> commit;
ERROR 4012 (25000): Transaction is timeout
MySQL [test]> select * from t1;
+----+---------------------+
| id | c1 |
+----+---------------------+
| 1 | 2021-10-03 09:22:20 |
| 2 | 2021-09-29 21:16:05 |
| 3 | 2021-09-29 21:16:05 |
| 4 | 2021-09-29 21:16:06 |
| 5 | 2021-09-29 21:16:06 |
| 6 | 2021-09-29 21:16:18 |
| 7 | 2021-10-03 09:35:18 |
| 8 | 2021-09-29 21:16:19 |
+----+---------------------+
8 rows in set (0.014 sec)
```
注意:不管是哪种超时,连接事务所持有的锁都会释放,只是事务的状态需要客户端主动清理一下。对于应用而言,就是需要捕捉事务超时报错,然后发起 ROLLBACK 。
# 如何分析 SQL 审计视图
SQL 审计视图可以查看在 OceanBase 里执行过的所有 SQL,不管是成功的还是失败的。这个对开发同学了解自己的业务 SQL ,定位问题细节非常有帮助。
## SQL 审计视图概述
SQL 审计视图 `gv$sql_audit` 是虚拟表,是内存中一个 FIFO 队列。功能的开启和数据大小是通过下面 OceanBase 集群参数控制的。
| 参数名 | 参数值 | 参数含义 |
|------------------------|----------|--------------------------------------|
| enable_sql_audit | TRUE | 指定是否开启 SQL 审计。默认 TRUE 是开启。FALSE 是关闭。 |
| sql_audit_queue_size | 10000000 | SQL 审计视图里最大记录数。 |
| sql_audit_memory_limit | 3G | SQL 审计视图内存最大大小。 |
SQL审计能保留的数据大小跟租户内存资源的大小也有关系。通常不会特别大。建议用自己的方法近实时的将 SQL 审计视图的数据抽取走,然后做二次分析。
您也可以在租户里开启或关闭这个 SQL 审计功能。
```sql
set global ob_enable_sql_audit = on;
```
视图列定义如下:
| 列名 | 含义 |
|----------------|-------------------------------------------------|
| SVR_IP | SQL 执行的 OBSERVER 节点 IP 。 |
| SVR_PORT | SQL 执行的 OBSERVER 节点 端口 。 |
| REQUEST_ID | 唯一标识 。 |
| TRACE_ID | 该 SQL 的 TRACE_ID 信息,在 OBSERVER 节点日志里可以关联查询相关日志。 |
| SID | SQL 执行的 OBSERVER 节点上的会话 ID 。 |
| CLIENT_IP | 该 SQL 执行的客户端 IP。通常是 OBPROXY IP。 |
| TENANT_ID | 该 SQL 执行的租户 ID 。 |
| TENANT_NAME | 该 SQL 执行的租户 名称 。 |
| USER_NAME | 该 SQL 执行的租户内部用户名。 |
| USER_CLIENT_IP | 该 SQL 执行的实际客户端 IP。 |
| DB_ID | 该 SQL 执行的数据库 ID 。 |
| DB_NAME | 该 SQL 执行的数据库名称。 |
| SQL_ID | 该 SQL 的 SQL_ID |
| QUERY_SQL | 该 SQL 的文本。如果太长会截断。 |
| PLAN_ID | 该 SQL 的执行计划 ID。 |
| AFFECTED_ROWS | 该 SQL 的写影响行数。 |
| RETURN_ROWS | 该 SQL 的返回行数。 |
| PARTITION_CNT | 该 SQL 访问的分区数量。 |
| RET_CODE | 该 SQL 的返回代码。 |
| EVENT | 该 SQL 的主要等待事件。 |
| PLAN_TYPE | 该 SQL 的执行计划类型。1 :本地SQL;2:远程 SQL;3: 分布式SQL。 |
| IS_HIT_PLAN | 是否命中执行计划。 |
| REQUEST_TIME | 该 SQL 执行时间点(时间戳类型,通过 usec_to_time 转换为可读时间格式)。 |
| ELAPSED_TIME | 该 SQL 执行总耗时。 |
| NET_TIME | 该 SQL 执行网络消耗时间。 |
| QUEUE_TIME | 该 SQL 执行内部排队时间。 |
| DECODE_TIME | |
| GET_PLAN_TIME | 该 SQL 执行计划生成时间。 |
| EXECUTE_TIME | 该 SQL 实际内部执行时间(不包括CPU 排队时间)。 |
## 如何查看 SQL 审计视图
下面 SQL 可以在SYS 租户和业务租户里查询。业务租户只能查看属于自己的租户的 SQL 数据。
+ 查看近期所有 SQL
```sql
SELECT /*+ read_consistency(weak) ob_querytimeout(100000000) */ substr(usec_to_time(request_time),1,19) request_time_, s.svr_ip, s.client_Ip, s.sid,s.tenant_id, s.tenant_name, s.user_name, s.db_name, s.query_sql, s.affected_rows, s.return_rows, s.ret_code, s.event, s.elapsed_time, s.queue_time, s.execute_time, round(s.request_memory_used/1024/1024/1024,2) req_mem_mb, plan_type, is_executor_rpc, is_inner_sql, TRANSACTION_HASH, trace_id
FROM gv$sql_audit s
WHERE 1=1 and s.tenant_id = 1002
and user_name='u_tpcc'
and request_time >= time_to_usec(DATE_SUB(current_timestamp, INTERVAL 30 MINUTE) )
ORDER BY request_time DESC
LIMIT 100;
```
`request_time` 是时间戳,跟微秒数转换通过函数 `usec_to_time``time_to_usec` 转换。
+ 分析统计近期所有 SQL
根据 `sql_id` 统计平均总耗时、平均执行时间等等。
```sql
SELECT sql_id, count(*), round(avg(elapsed_time)) avg_elapsed_time, round(avg(execute_time)) avg_exec_time
FROM gv$sql_audit s
WHERE 1=1 and s.tenant_id = 1002
and user_name='u_tpcc'
and request_time >= time_to_usec(DATE_SUB(current_timestamp, INTERVAL 30 MINUTE) )
GROUP BY sql_id
order by avg_elapsed_time desc
;
```
+ 查看报错的 SQL
`ret_code` 是 SQL 执行报错时的错误码。正常是 0 ,错误码是负数。
```sql
SELECT /*+ read_consistency(weak) ob_querytimeout(100000000) */ substr(usec_to_time(request_time),1,19) request_time_, s.svr_ip, s.client_Ip, s.sid,s.tenant_id, s.tenant_name, s.user_name, s.db_name, s.sql_id, s.query_sql, s.affected_rows, s.return_rows, s.ret_code, s.event, s.elapsed_time, s.queue_time, s.execute_time, round(s.request_memory_used/1024/1024/1024,2) req_mem_mb, plan_type, is_executor_rpc, is_inner_sql, TRANSACTION_HASH, trace_id
FROM gv$sql_audit s
WHERE 1=1 and s.tenant_id = 1002
and user_name='u_tpcc'
and ret_code < 0
and request_time >= time_to_usec(DATE_SUB(current_timestamp, INTERVAL 30 MINUTE) )
ORDER BY request_time DESC
LIMIT 500;
```
+ 查看远程 SQL 和分布式 SQL
`plan_type` 的值有三个:1 表示本地 SQL; 2 表示远程 SQL; 3 表示分布式 SQL 。
```sql
SELECT /*+ read_consistency(weak) ob_querytimeout(100000000) */ substr(usec_to_time(request_time),1,19) request_time_, s.svr_ip, s.client_Ip, s.sid,s.tenant_id, s.tenant_name, s.user_name, s.db_name, s.sql_id, s.query_sql, s.affected_rows, s.return_rows, s.ret_code, s.event, s.elapsed_time, s.queue_time, s.execute_time, round(s.request_memory_used/1024/1024/1024,2) req_mem_mb, plan_type, is_executor_rpc, is_inner_sql, TRANSACTION_HASH, trace_id
FROM gv$sql_audit s
WHERE 1=1 and s.tenant_id = 1002
and user_name='u_tpcc'
and plan_type > 1
and request_time >= time_to_usec(DATE_SUB(current_timestamp, INTERVAL 30 MINUTE) )
ORDER BY request_time DESC
LIMIT 500;
```
远程 SQL 的出现需要结合事务的业务逻辑分析。
# 如何诊断和调优 OceanBase SQL 执行计划
## 如何用 EXPLAIN 查看理论执行计划
查看执行计划需要一个好的客户端,这样能将注意力集中在执行计划上。
一是命令行 obclient 命令。obclient 的好处是显示会格式化,当一行结果太长的时候格式会有点不好看,这个时候可以让 SQL 以 `\G` 替换 `;`结尾时,结果集会列传行展示,可读性更好一些。
二是 OceanBase 官方客户端 ODC。 直接选中 SQL,然后点击右上角的 “执行计划”。然后会以表格形式展示执行计划。
三是 开源的 DBeaver 客户端工具。由于 DBeaver 跟 OB还没有紧密适配,所以只能下 EXPLAIN [SQL] 的方式查看执行计划。 目前还是这个途径体验最好。
### EXPLAIN 说明
EXPLAIN 命令完整的语法是:
```sql
{EXPLAIN | DESCRIBE | DESC}
[BASIC | OUTLINE | EXTENDED | EXTENDED_NOADDR | PARTITIONS | FORMAT = {TRADITIONAL| JSON}]
{SELECT statement | DELETE statement | INSERT statement | REPLACE statement| UPDATE statement}
```
学习遵从由简入繁原则,先从默认的 `BASIC`格式入手。不用指定。 FORMAT 有 TRADITIONAL 和 JSON ,默认是 TRADITIONAL ,人可读性好一些。 JSON 格式对程序解析比较友好。
先看一个简单的 SQL 执行计划格式。
```sql
EXPLAIN
SELECT count(*) FROM BMSQL_ITEM
;
```
```sql
obclient> EXPLAIN
-> SELECT count(*) FROM BMSQL_ITEM \G
*************************** 1. row ***************************
Query Plan: ===============================================
|ID|OPERATOR |NAME |EST. ROWS|COST |
-----------------------------------------------
|0 |SCALAR GROUP BY| |1 |78754|
|1 | TABLE SCAN |BMSQL_ITEM|99995 |59653|
===============================================
Outputs & filters:
-------------------------------------
0 - output([T_FUN_COUNT(*)]), filter(nil),
group(nil), agg_func([T_FUN_COUNT(*)])
1 - output([1]), filter(nil),
access([BMSQL_ITEM.I_ID]), partitions(p0)
1 row in set (0.01 sec)
```
整个 EXPLAIN 结果是一行。在 OceanBase 内部,这个结果估计是以 JSON 格式储。如下:
```json
{
"ID": 1,
"OPERATOR": "GROUP BY",
"NAME": "GROUP BY",
"EST.ROWS": 1,
"COST": 4352454,
"output": [
"T_FUN_COUNT(*)"
],
"CHILD_1": {
"ID": 0,
"OPERATOR": "TABLE SCAN",
"NAME": "TABLE SCAN",
"EST.ROWS": 10000000,
"COST": 2442330,
"output": [
"1"
]
}
}
```
这个 JSON 内容描述的是一个树,对普通用户可读性不好。所以传统格式的展示分为两部分。
第一部分是用表格形式展示执行计划这棵树。每行是一个独立的操作,操作符是 `OPERATOR`, 操作有ID。操作展示可能会缩进。缩进表示是内部操作,可以嵌套。执行顺序遵循由内到外,由上到下。操作符支持的内容也是 SQL 引擎成熟度的一个体现。
**第一部分说明:**
+ `OPERATOR` : 表示操作算子的名称,`TABLE SCAN`, 是常用操作算子,表示扫描。
+ `NAME`: 表示算子操作的对象。可以是表名或者索引名,或者内部临时视图名。需要注意的是如果是扫描主键,展示的依然是表名。因为 OceanBase 里的表和索引的本质都是索引组织表。表数据跟主键索引是一个概念。
+ `EST. ROWS`:执行当前算子输出的行数,跟统计信息有关。OB里表的统计信息目前只有在集群合并的时候才更新。
+ `COST`: 执行当前算子预估的成本。COST计算比较复杂,暂时先不深入。
**第二部分说明:**
第二部分的内容跟第一部分有关,主要是描述第一部分算子的具体信息。有一些公共的信息如下:
+ `output` : 表示当前算子输出的表达式(也包含列)。
+ `filter` : 表示当前算子的过滤表达式,`nil`表示无。如果当前算子是访问存储层,这个过滤表达式可以下推(push)。
### 常见执行计划算子
+ `TABLE GET`
表示主键直接等值访问,后面接表名。OceanBase 里主键就是表数据。
```sql
EXPLAIN
SELECT i_id, i_name, I_PRICE FROM BMSQL_ITEM WHERE i_id=10
\G
*************************** 1. row ***************************
Query Plan: ========================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
----------------------------------------
|0 |TABLE GET|BMSQL_ITEM|1 |53 |
========================================
Outputs & filters:
-------------------------------------
0 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE]), filter(nil),
access([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE]), partitions(p0)
1 row in set (0.00 sec)
```
+ `TABLE SCAN`
表示全表扫描或者主键扫描、索引扫描。具体看后面的操作对象名是表还是索引。注意,扫描主键也是表名。
```sql
CREATE UNIQUE INDEX idx_item_uk ON bmsql_item(i_name);
EXPLAIN extended_noaddr
SELECT i_id, i_name, I_PRICE FROM BMSQL_ITEM WHERE i_name = 'w2uw7BJj5tG5BTlSdfT'
\G
*************************** 1. row ***************************
Query Plan: ======================================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
------------------------------------------------------
|0 |TABLE SCAN|BMSQL_ITEM(IDX_ITEM_UK)|1 |88 |
======================================================
Outputs & filters:
-------------------------------------
0 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE]), filter(nil),
access([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE]), partitions(p0),
is_index_back=true,
range_key([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.shadow_pk_0]), range(w2uw7BJj5tG5BTlSdfT,MIN ; w2uw7BJj5tG5BTlSdfT,MAX),
range_cond([BMSQL_ITEM.I_NAME = 'w2uw7BJj5tG5BTlSdfT'])
1 row in set (0.00 sec)
```
**说明:**
+ `range_cond` : 指条件中能用于索引的列。
+ `range_key` :指索引中能用于过滤的列及其值域。
+ `TOP-N SORT` & `LIMIT`
`TOP-N SORT` :常用的场景排序后可能只用返回最大或最小的前 N 条记录。
`LIMIT`: 这个算子限制返回的行数。
```sql
MySQL [tpccdb]> explain extended_noaddr SELECT i_id, i_name FROM BMSQL_ITEM WHERE i_name LIKE 'w2%' ORDER BY I_PRICE DESC limit 10 \G
*************************** 1. row ***************************
Query Plan: ========================================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
--------------------------------------------------------
|0 |LIMIT | |10 |479 |
|1 | TOP-N SORT | |10 |478 |
|2 | TABLE SCAN|bmsql_item(idx_item_uk)|61 |406 |
========================================================
Outputs & filters:
-------------------------------------
0 - output([bmsql_item.i_id], [bmsql_item.i_name]), filter(nil), limit(10), offset(nil)
1 - output([bmsql_item.i_id], [bmsql_item.i_name]), filter(nil), sort_keys([bmsql_item.i_price, DESC]), topn(10)
2 - output([bmsql_item.i_name], [bmsql_item.i_id], [bmsql_item.i_price]), filter(nil),
access([bmsql_item.i_name], [bmsql_item.i_id], [bmsql_item.i_price]), partitions(p0),
is_index_back=true,
range_key([bmsql_item.i_name], [bmsql_item.shadow_pk_0]), range(w2,MIN ; w2������������������������������� ,MAX),
range_cond([(T_OP_LIKE, bmsql_item.i_name, 'w2%', '\\')])
1 row in set (0.002 sec)
MySQL [tpccdb]>
```
+ `NESTED-LOOP JOIN`
```sql
EXPLAIN extended_noaddr
SELECT w.W_ID , w.W_NAME ,w.W_CITY ,d.D_ID ,d.D_NAME ,d.D_STATE
FROM BMSQL_WAREHOUSE w JOIN BMSQL_DISTRICT d ON (w.w_id = d.D_W_ID)
WHERE w.W_ID =1
ORDER BY D.D_name
;
====================================================
|ID|OPERATOR |NAME|EST. ROWS|COST|
----------------------------------------------------
|0 |SORT | |10 |154 |
|1 | NESTED-LOOP JOIN CARTESIAN| |10 |97 |
|2 | TABLE GET |W |1 |53 |
|3 | TABLE SCAN |D |10 |39 |
====================================================
Outputs & filters:
-------------------------------------
0 - output([W.W_ID], [W.W_NAME], [W.W_CITY], [D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil), sort_keys([D.D_NAME, ASC])
1 - output([W.W_ID], [W.W_NAME], [W.W_CITY], [D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil),
conds(nil), nl_params_(nil), inner_get=false, self_join=false, batch_join=false
2 - output([W.W_ID], [W.W_NAME], [W.W_CITY]), filter(nil),
access([W.W_ID], [W.W_NAME], [W.W_CITY]), partitions(p0),
is_index_back=false,
range_key([W.W_ID]), range[1 ; 1],
range_cond([W.W_ID = 1])
3 - output([D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil),
access([D.D_ID], [D.D_NAME], [D.D_STATE]), partitions(p0),
is_index_back=false,
range_key([D.D_W_ID], [D.D_ID]), range(1,MIN ; 1,MAX),
range_cond([D.D_W_ID = 1])
```
**说明:**
+ 这个算法的整体性能取决于外部表返回的记录数(循环的次数)和内部表的查询性能。
+ 通常经验是小表作为外部表,大表作为内部表。不过实际也不是以表大小看,而是看过滤条件应用后的结果集大小来定。可以对比下面 SQL 的 执行计划。
+ 两个表的访问都走了主键索引,主键就是数据,所以执行计划中每个算子后面的名称(`NAME`)里都是表名。留意 1 和 2 里的 `is_index_back=false`
+ 这里针对 内部表和外部表扫描时都传入了条件 `W_ID=1`,通过 2 和 3 的 `range_cond` 可知。所以这里的 `JOIN` 条件被消除,外层连接就变为笛卡儿积(`CARTESIAN`)。这也是常用的一个优化手段。
+ `MERGE JOIN`
`MERGE JOIN` 主要用于两个不是很小也很大的结果集的连接,它们没有有效的过滤条件或者这个条件上没有合适的索引。
`MERGE JOIN` 算法基本分两大阶段。一是排序,将两个结果集分别按连接字段排序。二是合并。分别从两个结果集里读取记录,比较、遍历等。如果结果集本来就是有序的,那么第一阶段可以优化。`MERGE JOIN` 可以用于等值运算,也可以用于不等值运算(小于、大于、小于等于、大于等于)。
`MERGE JOIN` 主要会利用数据主键或者索引的有序,此时它的性能很有可能会更好。很多时候数据量非常大的时候, `MERGE JOIN` 性能并不是很好,要设法规避。
```sql
EXPLAIN extended_noaddr
SELECT w.W_ID , w.W_NAME ,w.W_CITY ,d.D_ID ,d.D_NAME ,d.D_STATE
FROM BMSQL_WAREHOUSE w JOIN BMSQL_DISTRICT d ON (w.w_id = d.D_W_ID)
ORDER BY d.D_NAME
;
======================================
|ID|OPERATOR |NAME|EST. ROWS|COST |
--------------------------------------
|0 |SORT | |1000 |10093|
|1 | MERGE JOIN | |1000 |1312 |
|2 | TABLE SCAN|W |100 |59 |
|3 | TABLE SCAN|D |1000 |426 |
======================================
Outputs & filters:
-------------------------------------
0 - output([W.W_ID], [W.W_NAME], [W.W_CITY], [D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil), sort_keys([D.D_NAME, ASC])
1 - output([W.W_ID], [W.W_NAME], [W.W_CITY], [D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil),
equal_conds([W.W_ID = D.D_W_ID]), other_conds(nil)
2 - output([W.W_ID], [W.W_NAME], [W.W_CITY]), filter(nil),
access([W.W_ID], [W.W_NAME], [W.W_CITY]), partitions(p0),
is_index_back=false,
range_key([W.W_ID]), range(MIN ; MAX)always true
3 - output([D.D_W_ID], [D.D_ID], [D.D_NAME], [D.D_STATE]), filter(nil),
access([D.D_W_ID], [D.D_ID], [D.D_NAME], [D.D_STATE]), partitions(p0),
is_index_back=false,
range_key([D.D_W_ID], [D.D_ID]), range(MIN,MIN ; MAX,MAX)always true
```
**说明:**
+ 两个表在连接条件列上都有主键索引(即数据),所以返回的数据都是有序的,这里就使用了 `MERGE JOIN`.
+ 最后在外层再根据排序条件执行排序(`SORT`)。
+ `MERGE JOIN`可以临时排序的大小受集群隐含参数(`_sort_area_size`) 限制。
+ `HASH JOIN`
`HASH JOIN` 通常用于两个比较大的结果集之间的连接,通常没有比较好的过滤条件或者过滤条件上没有索引。
```sql
EXPLAIN extended_noaddr
SELECT c.C_W_ID , c.C_D_ID , c.C_FIRST || ',' || c.C_LAST , c.C_PAYMENT_CNT , c.C_YTD_PAYMENT , o.O_W_ID , o.O_D_ID ,o.O_OL_CNT
FROM BMSQL_CUSTOMER c JOIN BMSQL_OORDER o ON (c.C_ID = o.O_C_ID)
WHERE c.C_W_ID = 1 AND c.C_D_ID = 5 AND (c.C_W_ID <> o.O_W_ID OR c.C_D_ID <> o.O_D_ID )
ORDER BY c.C_W_ID , c.C_D_ID , c.C_LAST , o.O_W_ID , o.O_D_ID
;
=========================================
|ID|OPERATOR |NAME|EST. ROWS|COST |
-----------------------------------------
|0 |SORT | |2238162 |45593377|
|1 | HASH JOIN | |2238162 |3764643 |
|2 | TABLE SCAN|C |2973 |3515 |
|3 | TABLE SCAN|O |3019348 |785737 |
=========================================
Outputs & filters:
-------------------------------------
0 - output([C.C_W_ID], [C.C_D_ID], [(T_OP_CNN, (T_OP_CNN, C.C_FIRST, ?), C.C_LAST)], [C.C_PAYMENT_CNT], [C.C_YTD_PAYMENT], [O.O_W_ID], [O.O_D_ID], [O.O_OL_CNT]), filter(nil), sort_keys([C.C_LAST, ASC], [O.O_W_ID, ASC], [O.O_D_ID, ASC])
1 - output([C.C_W_ID], [C.C_D_ID], [C.C_FIRST], [C.C_LAST], [C.C_PAYMENT_CNT], [C.C_YTD_PAYMENT], [O.O_W_ID], [O.O_D_ID], [O.O_OL_CNT]), filter(nil),
equal_conds([C.C_ID = O.O_C_ID]), other_conds([C.C_W_ID != O.O_W_ID OR C.C_D_ID != O.O_D_ID])
2 - output([C.C_ID], [C.C_W_ID], [C.C_D_ID], [C.C_FIRST], [C.C_LAST], [C.C_PAYMENT_CNT], [C.C_YTD_PAYMENT]), filter(nil),
access([C.C_ID], [C.C_W_ID], [C.C_D_ID], [C.C_FIRST], [C.C_LAST], [C.C_PAYMENT_CNT], [C.C_YTD_PAYMENT]), partitions(p0),
is_index_back=false,
range_key([C.C_W_ID], [C.C_D_ID], [C.C_ID]), range(1,5,MIN ; 1,5,MAX),
range_cond([C.C_W_ID = 1], [C.C_D_ID = 5])
3 - output([O.O_C_ID], [O.O_W_ID], [O.O_D_ID], [O.O_OL_CNT]), filter(nil),
access([O.O_C_ID], [O.O_W_ID], [O.O_D_ID], [O.O_OL_CNT]), partitions(p0),
is_index_back=false,
range_key([O.O_W_ID], [O.O_D_ID], [O.O_ID]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true
```
**说明:**
+ 尽管条件 `(c.C_W_ID <> o.O_W_ID OR c.C_D_ID <> o.O_D_ID )` 不在 JOIN 的 ON 条件里,优化器还是能识别出来并自动应用。并且将条件分类为 `equal_conds``other_conds`
+ `HASH JOIN` 也分外部表和内部表。内部表也同时是 `probe table`,外部表是`hash table`。通常数据库会挑选结果集相对小的表作为外部表,并在连接条件上用哈希函数构建`hash table`。然后循环遍历`probe table`,对连接条件列用哈希函数,探测是否在`hash table`中存在。如果存在则返回匹配的记录。这个跟`NESTED-LOOP JOIN`很类似,单不同之处是`HASH JOIN`里会在连接条件列上用哈希函数,并在内存中构建`hash table`
+ OceanBase 优化器一次能构建的最大`hash table` 受内部参数(`_hash_table_size`) 限制。如果外部表的结果集比这个大,就需要分多次构建`hash table`,这个也叫 `multiple pass` ,会涉及到一些内存和文件数据交换,以及多次哈希探测,性能相对会下降一些。但有这个功能比没有更好。
+ `HASH JOIN` 的细节比较复杂,这里就不详细讨论。目前只要能识别出 `HASH JOIN`,以及掌握如果产生或者规避 `HASH JOIN` 算法。
+ `SUBPLAN SCAN``COUNT`
算子 `SUBPLAN SCAN``TABLE SCAN` 类似,不同的是 后者是从基表(或者索引)里扫描数据,前者是从视图(包括内部临时生成的)里读取数据。
还是前面那个分页查询场景例子。由于在 SELECT 后面增加了一列排序序号(计算出来的),执行计划就多了两步。
```sql
EXPLAIN extended_noaddr
SELECT i_id, i_name, i_price, i_data , rn
FROM (
SELECT rownum rn, i_id, i_name, I_PRICE, I_DATA FROM (
SELECT i_id, i_name, i_price , i_data FROM BMSQL_ITEM
WHERE i_name LIKE 'wu%'
ORDER BY i_price DESC
) t WHERE rownum <= 15
) WHERE rn > 10
;
============================================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
------------------------------------------------------------
|0 |COUNT | |5 |332 |
|1 | SUBPLAN SCAN |T |5 |331 |
|2 | LIMIT | |5 |330 |
|3 | TOP-N SORT | |15 |328 |
|4 | TABLE SCAN|BMSQL_ITEM(BMSQL_ITEM_UK)|30 |245 |
============================================================
Outputs & filters:
-------------------------------------
0 - output([T.I_ID], [T.I_NAME], [T.I_PRICE], [T.I_DATA], [rownum() + ?]), filter(nil)
1 - output([T.I_ID], [T.I_NAME], [T.I_PRICE], [T.I_DATA]), filter(nil),
access([T.I_ID], [T.I_NAME], [T.I_PRICE], [T.I_DATA])
2 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA]), filter(nil), limit(?), offset(?)
3 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA]), filter(nil), sort_keys([BMSQL_ITEM.I_PRICE, DESC]), topn(? + ?)
4 - output([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA]), filter(nil),
access([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA]), partitions(p0),
is_index_back=true,
range_key([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.shadow_pk_0]), range(wu,MIN ; wu￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿ ,MAX),
range_cond([(T_OP_LIKE, BMSQL_ITEM.I_NAME, ?, '\')])
```
上面 `COUNT` 算子是为了兼容 ORACLE 的 rownum 功能,作用在于为 rownum 表达式进行自增操作。在一般场景下,当SQL 查询含有 rownum 时,SQL 优化器就会在生成计划的时候分配一个 `COUNT` 算子。 如果 SELECT 里不涉及到 rownum 值的展示时, `COUNT` 算子也可能会被优化为 LIMIT 算子。
```sql
EXPLAIN extended_noaddr
SELECT * FROM (
SELECT i_id, i_name FROM BMSQL_ITEM WHERE i_name LIKE 'w2u%'
ORDER BY I_PRICE DESC
) WHERE rownum < 5
;
==========================================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
----------------------------------------------------------
|0 |LIMIT | |1 |91 |
|1 | TOP-N SORT | |1 |90 |
|2 | TABLE SCAN|BMSQL_ITEM(BMSQL_ITEM_UK)|1 |88 |
==========================================================
Outputs & filters:
-------------------------------------
0 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME]), filter(nil), limit(?), offset(nil)
1 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME]), filter(nil), sort_keys([BMSQL_ITEM.I_PRICE, DESC]), topn(?)
2 - output([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE]), filter(nil),
access([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE]), partitions(p0),
is_index_back=true,
range_key([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.shadow_pk_0]), range(w2u,MIN ; w2u￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿,MAX),
range_cond([(T_OP_LIKE, BMSQL_ITEM.I_NAME, ?, '\')])
```
+ `EXCHANGE IN|OUT REMOTE`
首先看要访问表的主副本节点,然后直连另外一个节点。人为构造一个远程执行计划。
示例 1
```bash
explain extended_Noaddr select /*+ test20210405 */ * from bmsql_item where i_id=100\G
"
obclient: [Warning] Using a password on the command line interface can be insecure.
*************************** 1. row ***************************
Query Plan: ===================================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
---------------------------------------------------
|0 |EXCHANGE IN REMOTE | |1 |54 |
|1 | EXCHANGE OUT REMOTE| |1 |53 |
|2 | TABLE GET |BMSQL_ITEM|1 |53 |
===================================================
Outputs & filters:
-------------------------------------
0 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA], [BMSQL_ITEM.I_IM_ID]), filter(nil)
1 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA], [BMSQL_ITEM.I_IM_ID]), filter(nil)
2 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA], [BMSQL_ITEM.I_IM_ID]), filter(nil),
access([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA], [BMSQL_ITEM.I_IM_ID]), partitions(p0),
is_index_back=false,
range_key([BMSQL_ITEM.I_ID]), range[100 ; 100],
range_cond([BMSQL_ITEM.I_ID = 100])
```
+ `Exchange` 算子是分布式场景下,用于线程间进行数据交互的算子。它一般都是成对出现的,数据源端有一个 `out` 算子,目的端会有一个 `in` 算子。
+ 算子 2 是实际取数据的执行计划,`TABLE GET` 是主键访问数据的算子。
+ 算子 1 的 `EXCHANGE OUT REMOTE` 在远端机器上负责读取数据并传输出去。
+ 算子 0 的 `EXCHANGE IN REMOTE` 在本地节点接收数据。
上面是主键扫描示例,下面是索引扫描的示例。
示例 2
```sql
explain extended_Noaddr select /*+ test20210405 */ * from bmsql_item where i_name LIKE 'Ax%'\G
obclient: [Warning] Using a password on the command line interface can be insecure.
*************************** 1. row ***************************
Query Plan: ====================================================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
--------------------------------------------------------------------
|0 |EXCHANGE IN REMOTE | |33 |294 |
|1 | EXCHANGE OUT REMOTE| |33 |264 |
|2 | TABLE SCAN |BMSQL_ITEM(BMSQL_ITEM_IDX1)|33 |264 |
====================================================================
Outputs & filters:
-------------------------------------
0 - output([BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA], [BMSQL_ITEM.I_IM_ID]), filter(nil)
1 - output([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA], [BMSQL_ITEM.I_IM_ID]), filter(nil)
2 - output([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA], [BMSQL_ITEM.I_IM_ID]), filter(nil),
access([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID], [BMSQL_ITEM.I_PRICE], [BMSQL_ITEM.I_DATA], [BMSQL_ITEM.I_IM_ID]), partitions(p0),
is_index_back=true,
range_key([BMSQL_ITEM.I_NAME], [BMSQL_ITEM.I_ID]), range(Ax,MIN ; Ax������������������������������� ,MAX),
range_cond([(T_OP_LIKE, BMSQL_ITEM.I_NAME, ?, '\')])
```
实际上业务都是通过 OBPROXY 连接,能正确路由到 OBSERVER 节点上,能很大程度规避远程执行计划。不过并不能从根本上避免。后面还会举例说明。
### 主备切换如何影响执行计划
当 SQL 包含多表连接的时候,如果多表的主副本都在同一个 OBSERVER 节点上也是当前连接的节点,这就是个本地 SQL 执行计划(`plan_type` = 1);如果多表的主副本是分散在不同的 OBSERVER 节点上,这就是分布式 SQL 执行计划(`plan_type=3`);如果多表的主副本都在同一个 OBSERVER 节点上但不是当前连接的节点,这个就是一个远程 SQL 执行计划(`plan_type=2`)。
通常我们不直接调整分区主副本的位置,而是通过调整表或表分组、数据库或者租户的 PRIMARY_ZONE 来间接改变表主副本的位置。理论上改变了主副本的位置后,执行计划也会变化。实际情况,由于分区主副本位置变动信息 OBPROXY 并不会立即获得,所有有时候观察 EXPLAIN 执行计划也不会变,这点需要留意。
+ 查看表的主副本位置。
```sql
SELECT a.tenant_name, d.database_name, tg.tablegroup_name ,t.table_name, t.part_num , t2.partition_id, t2.ZONE, t2.svr_ip , t2.svr_port, round(t2.data_size/1024/1024) data_size_mb, t2.row_count
FROM oceanbase.__all_tenant AS a
JOIN oceanbase.__all_virtual_database AS d ON ( a.tenant_id = d.tenant_id )
JOIN oceanbase.__all_virtual_table AS t ON (t.tenant_id = d.tenant_id AND t.database_id = d.database_id)
JOIN oceanbase.__all_virtual_meta_table t2 ON (t.tenant_id = t2.tenant_id AND (t.table_id=t2.table_id OR t.tablegroup_id=t2.table_id) AND t2.ROLE IN (1) )
LEFT JOIN oceanbase.__all_virtual_tablegroup AS tg ON (t.tenant_id = tg.tenant_id and t.tablegroup_id = tg.tablegroup_id)
WHERE a.tenant_id IN (1002 ) AND t.table_type IN (3)
AND d.database_name = 'tpccdb' and table_name in ('bmsql_item','bmsql_oorder')
ORDER BY t.tenant_id, tg.tablegroup_name, d.database_name, t.table_name, t2.partition_id
;
+-------------+---------------+-----------------+--------------+----------+--------------+-------+---------------+----------+--------------+-----------+
| tenant_name | database_name | tablegroup_name | table_name | part_num | partition_id | ZONE | svr_ip | svr_port | data_size_mb | row_count |
+-------------+---------------+-----------------+--------------+----------+--------------+-------+---------------+----------+--------------+-----------+
| obmysql | tpccdb | NULL | bmsql_item | 1 | 0 | zone3 | 172.20.249.51 | 2882 | 7 | 100000 |
| obmysql | tpccdb | tpcc_group | bmsql_oorder | 3 | 0 | zone3 | 172.20.249.51 | 2882 | 1 | 139802 |
| obmysql | tpccdb | tpcc_group | bmsql_oorder | 3 | 1 | zone2 | 172.20.249.49 | 2882 | 2 | 203152 |
| obmysql | tpccdb | tpcc_group | bmsql_oorder | 3 | 2 | zone1 | 172.20.249.52 | 2882 | 2 | 150858 |
+-------------+---------------+-----------------+--------------+----------+--------------+-------+---------------+----------+--------------+-----------+
4 rows in set (0.053 sec)
```
从上面结果可以看出,表 `bmsql_item` 是个单分区的普通表,主副本在节点 `172.20.249.51` 上。 表 `bmsql_oorder` 是有3个分区的表,主副本分别在三个节点上。
看下面 SQL 执行计划。
```sql
MySQL [tpccdb]> explain extended_noaddr select o.o_w_id , o.o_d_id ,o.o_id , i.i_name ,i.i_price ,o.o_c_id from bmsql_oorder o , bmsql_item i where o.o_id = i.i_id and o.o_w_id = 3 limit 10 \G
*************************** 1. row ***************************
Query Plan: ==========================================
|ID|OPERATOR |NAME|EST. ROWS|COST|
------------------------------------------
|0 |LIMIT | |10 |407 |
|1 | NESTED-LOOP JOIN| |10 |406 |
|2 | TABLE SCAN |o |10 |39 |
|3 | TABLE GET |i |1 |36 |
==========================================
Outputs & filters:
-------------------------------------
0 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil), limit(10), offset(nil)
1 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil),
conds(nil), nl_params_([o.o_id]), batch_join=true
2 - output([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), filter(nil),
access([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), partitions(p0),
is_index_back=false,
range_key([o.o_w_id], [o.o_d_id], [o.o_id]), range(3,MIN,MIN ; 3,MAX,MAX),
range_cond([o.o_w_id = 3])
3 - output([i.i_name], [i.i_price]), filter(nil),
access([i.i_name], [i.i_price]), partitions(p0),
is_index_back=false,
range_key([i.i_id]), range(MIN ; MAX),
range_cond([? = i.i_id])
1 row in set (0.008 sec)
```
从上面可以看出 `TABLE SCAN` 访问表 `o` 的时候,访问的是分区 `partitions(p0)` 。0 号分区的主副本在 `zone3` 的节点 `172.20.249.51` 上,跟 表 `bmsql_item` 在同一个节点,所以这是个本地 JOIN 。
如果调整表 `bmsql_item` 的 PRIMARY_ZONE ,换一个节点,再看看这个 SQL 。
```sql
MySQL [tpccdb]> alter table bmsql_item primary_zone='zone1';
Query OK, 0 rows affected (0.043 sec)
```
查看 主副本实际位置。
```
+-------------+---------------+-----------------+--------------+----------+--------------+-------+---------------+----------+--------------+-----------+
| tenant_name | database_name | tablegroup_name | table_name | part_num | partition_id | ZONE | svr_ip | svr_port | data_size_mb | row_count |
+-------------+---------------+-----------------+--------------+----------+--------------+-------+---------------+----------+--------------+-----------+
| obmysql | tpccdb | NULL | bmsql_item | 1 | 0 | zone1 | 172.20.249.52 | 2882 | 7 | 100000 |
| obmysql | tpccdb | tpcc_group | bmsql_oorder | 3 | 0 | zone3 | 172.20.249.51 | 2882 | 1 | 139802 |
| obmysql | tpccdb | tpcc_group | bmsql_oorder | 3 | 1 | zone2 | 172.20.249.49 | 2882 | 2 | 203152 |
| obmysql | tpccdb | tpcc_group | bmsql_oorder | 3 | 2 | zone1 | 172.20.249.52 | 2882 | 2 | 150858 |
+-------------+---------------+-----------------+--------------+----------+--------------+-------+---------------+----------+--------------+-----------+
4 rows in set (0.039 sec)
```
进 SYS 租户查看 `bmsql_item` 主副本在 `zone1` 的 节点 `172.20.249.52` 上了。
再进业务租户查看 SQL 执行计划。
```sql
MySQL [tpccdb]> explain extended_noaddr select o.o_w_id , o.o_d_id ,o.o_id , i.i_name ,i.i_price ,o.o_c_id from bmsql_oorder o , bmsql_item i where o.o_id = i.i_id and o.o_w_id = 3 limit 10 \G
*************************** 1. row ***************************
Query Plan: ==========================================
|ID|OPERATOR |NAME|EST. ROWS|COST|
------------------------------------------
|0 |LIMIT | |10 |407 |
|1 | NESTED-LOOP JOIN| |10 |406 |
|2 | TABLE SCAN |o |10 |39 |
|3 | TABLE GET |i |1 |36 |
==========================================
Outputs & filters:
-------------------------------------
0 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil), limit(10), offset(nil)
1 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil),
conds(nil), nl_params_([o.o_id]), batch_join=true
2 - output([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), filter(nil),
access([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), partitions(p0),
is_index_back=false,
range_key([o.o_w_id], [o.o_d_id], [o.o_id]), range(3,MIN,MIN ; 3,MAX,MAX),
range_cond([o.o_w_id = 3])
3 - output([i.i_name], [i.i_price]), filter(nil),
access([i.i_name], [i.i_price]), partitions(p0),
is_index_back=false,
range_key([i.i_id]), range(MIN ; MAX),
range_cond([? = i.i_id])
1 row in set (0.004 sec)
```
跟前面执行计划比,没有发生变化!这个不符合理论。其原因就是当分区主备副本发生切换后,OBSERVER 和 OBPROXY 内部的分区 LOCATION CACHE 并不是立即更新,而是在 SQL 第一次执行的时候才触发更新。EXPLAIN 查看执行计划的时候,并不会执行 SQL,所以看到的不是实际的执行计划,而是理论的。我们可以简单访问一下表 bmsql_item 触发 OceanBase 内部 LOCATION CACHE 更新。然后再查看上面执行计划。
```sql
MySQL [tpccdb]> explain extended_noaddr select o.o_w_id , o.o_d_id ,o.o_id , i.i_name ,i.i_price ,o.o_c_id from bmsql_oorder o , bmsql_item i where o.o_id = i.i_id and o.o_w_id = 3 limit 10 \G
*************************** 1. row ***************************
Query Plan: ==================================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
--------------------------------------------------
|0 |LIMIT | |10 |409 |
|1 | NESTED-LOOP JOIN | |10 |408 |
|2 | TABLE SCAN |o |10 |39 |
|3 | PX COORDINATOR | |1 |37 |
|4 | EXCHANGE OUT DISTR|:EX10000|1 |36 |
|5 | TABLE GET |i |1 |36 |
==================================================
Outputs & filters:
-------------------------------------
0 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil), limit(10), offset(nil)
1 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil),
conds(nil), nl_params_([o.o_id]), batch_join=false
2 - output([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), filter(nil),
access([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), partitions(p0),
is_index_back=false,
range_key([o.o_w_id], [o.o_d_id], [o.o_id]), range(3,MIN,MIN ; 3,MAX,MAX),
range_cond([o.o_w_id = 3])
3 - output([i.i_name], [i.i_price]), filter(nil)
4 - output([i.i_name], [i.i_price]), filter(nil), is_single, dop=1
5 - output([i.i_name], [i.i_price]), filter(nil),
access([i.i_name], [i.i_price]), partitions(p0),
is_index_back=false,
range_key([i.i_id]), range(MIN ; MAX),
range_cond([? = i.i_id])
1 row in set (0.004 sec)
```
从上面执行计划可以看出,对表 `bmsql_item` 的访问变成远程访问了 `EXCHANGE OUT DISTR` 。并且由于 `bmsql_oorder` 的条件过滤后会有很多记录,所以这里远程访问 `bmsql_item` 的数据还用了并行(`PX COORDINATOR`)。
上面更新的是表 `bmsql_item` 而不是 `bmsql_oorder` 也是有原因的,因为这个连接 SQL 被 OBPROXY 路由到哪个 OBSERVER 节点是由 `from` 语句后的表确定的,即 `bmsql_oorder``o.o_w_Id=3` 对应的分区的主副本。这个演示是先保证 OBPROXY 路由的节点是固定的,方便对比。
下面我们调整这个 SQL 里的表连接顺序再看看。先回滚 `bmsql_item` 的 PRIMARY_ZONE 变更。
```sql
MySQL [tpccdb]> explain extended_noaddr select o.o_w_id , o.o_d_id ,o.o_id , i.i_name ,i.i_price ,o.o_c_id from bmsql_item i , bmsql_oorder o where o.o_id = i.i_id and o.o_w_id = 3 limit 10\G
*************************** 1. row ***************************
Query Plan: ==========================================
|ID|OPERATOR |NAME|EST. ROWS|COST|
------------------------------------------
|0 |LIMIT | |10 |407 |
|1 | NESTED-LOOP JOIN| |10 |406 |
|2 | TABLE SCAN |o |10 |39 |
|3 | TABLE GET |i |1 |36 |
==========================================
Outputs & filters:
-------------------------------------
0 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil), limit(10), offset(nil)
1 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil),
conds(nil), nl_params_([o.o_id]), batch_join=true
2 - output([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), filter(nil),
access([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), partitions(p0),
is_index_back=false,
range_key([o.o_w_id], [o.o_d_id], [o.o_id]), range(3,MIN,MIN ; 3,MAX,MAX),
range_cond([o.o_w_id = 3])
3 - output([i.i_name], [i.i_price]), filter(nil),
access([i.i_name], [i.i_price]), partitions(p0),
is_index_back=false,
range_key([i.i_id]), range(MIN ; MAX),
range_cond([? = i.i_id])
1 row in set (0.003 sec)
```
调整表 `bmsql_item` 的 PRIMARY_ZONE 到 `zone1` 后,查看上面 SQL 的 EXPLAIN 结果,跟前面一样,没有发生变化。
那么再次使用前面的技巧,对 `bmsql_item` 访问一次。
```sql
MySQL [tpccdb]> select count(*) from bmsql_item;
+----------+
| count(*) |
+----------+
| 100000 |
+----------+
1 row in set (0.083 sec)
MySQL [tpccdb]> explain extended_noaddr select o.o_w_id , o.o_d_id ,o.o_id , i.i_name ,i.i_price ,o.o_c_id from bmsql_item i , bmsql_oorder o where o.o_id = i.i_id and o.o_w_id = 3 limit 10\G
*************************** 1. row ***************************
Query Plan: ==================================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
--------------------------------------------------
|0 |LIMIT | |10 |409 |
|1 | NESTED-LOOP JOIN | |10 |408 |
|2 | TABLE SCAN |o |10 |39 |
|3 | PX COORDINATOR | |1 |37 |
|4 | EXCHANGE OUT DISTR|:EX10000|1 |36 |
|5 | TABLE GET |i |1 |36 |
==================================================
Outputs & filters:
-------------------------------------
0 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil), limit(10), offset(nil)
1 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil),
conds(nil), nl_params_([o.o_id]), batch_join=false
2 - output([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), filter(nil),
access([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), partitions(p0),
is_index_back=false,
range_key([o.o_w_id], [o.o_d_id], [o.o_id]), range(3,MIN,MIN ; 3,MAX,MAX),
range_cond([o.o_w_id = 3])
3 - output([i.i_name], [i.i_price]), filter(nil)
4 - output([i.i_name], [i.i_price]), filter(nil), is_single, dop=1
5 - output([i.i_name], [i.i_price]), filter(nil),
access([i.i_name], [i.i_price]), partitions(p0),
is_index_back=false,
range_key([i.i_id]), range(MIN ; MAX),
range_cond([? = i.i_id])
1 row in set (0.003 sec)
```
从上面结果可见,执行计划再次发生变化,不过跟前面那个 SQL 的变化是一样的,对表 `bmsql_item` 的访问依然是远程访问。
看到这里您可能就会奇怪,既然 SQL 被路由到表 `bmsql_item` 的主副本所在的节点了,为什么对表 `bmsql_item` 的访问还是远程访问?
这个问题就出在 EXPLAIN 被路由到的节点上。OBPROXY 对 EXPLAIN 语句的路由第一次是随机的,后面每次都跟随前面的 EXPLAIN 语句的路由。虽然上面改变了表 `bmsql_item` 的位置,但是 EXPLAIN 语句依然发往同一个节点(即 `bmsql_oorder` 所在的节点),在那个节点上生产的执行计划就永远是上面这种。
以上 SQL 被路由到哪个节点信息,可以通过 SQL审计视图查询。
```sql
SELECT /*+ read_consistency(weak) ob_querytimeout(100000000) */ substr(usec_to_time(request_time),1,19) request_time_, s.svr_ip, s.plan_id, s.client_Ip, s.sid,s.tenant_id, s.tenant_name, s.user_name, s.db_name, s.query_sql, s.affected_rows, s.return_rows, s.ret_code, s.event, s.elapsed_time, s.queue_time, s.execute_time, round(s.request_memory_used/1024/1024/1024,2) req_mem_mb, plan_type, is_executor_rpc, is_inner_sql, TRANSACTION_HASH ,trace_id
FROM gv$sql_audit s
WHERE 1=1 and s.tenant_id = 1002
and user_name='u_tpcc' and query_sql like '%bmsql_item%'
and request_time >= time_to_usec(date_sub(CURRENT_TIMESTAMP, interval 30 minute ))
ORDER BY request_time DESC
LIMIT 100;
```
结果就不展示了。
下面可以换一个 OBPROXY ,或者把此前的 OBPROXY KILL 掉重新拉起。再看看上面 SQL 的执行计划,就变成我们期望的那种执行计划了。
```sql
MySQL [tpccdb]> explain extended_noaddr select o.o_w_id , o.o_d_id ,o.o_id , i.i_name ,i.i_price ,o.o_c_id from bmsql_item i , bmsql_oorder o where o.o_id = i.i_id and o.o_w_id = 3 limit 10\G
*************************** 1. row ***************************
Query Plan: ==================================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
--------------------------------------------------
|0 |LIMIT | |10 |409 |
|1 | NESTED-LOOP JOIN | |10 |408 |
|2 | PX COORDINATOR | |10 |40 |
|3 | EXCHANGE OUT DISTR|:EX10000|10 |39 |
|4 | TABLE SCAN |o |10 |39 |
|5 | TABLE GET |i |1 |36 |
==================================================
Outputs & filters:
-------------------------------------
0 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil), limit(10), offset(nil)
1 - output([o.o_w_id], [o.o_d_id], [o.o_id], [i.i_name], [i.i_price], [o.o_c_id]), filter(nil),
conds(nil), nl_params_([o.o_id]), batch_join=true
2 - output([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), filter(nil)
3 - output([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), filter(nil), is_single, dop=1
4 - output([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), filter(nil),
access([o.o_w_id], [o.o_id], [o.o_d_id], [o.o_c_id]), partitions(p0),
is_index_back=false,
range_key([o.o_w_id], [o.o_d_id], [o.o_id]), range(3,MIN,MIN ; 3,MAX,MAX),
range_cond([o.o_w_id = 3])
5 - output([i.i_name], [i.i_price]), filter(nil),
access([i.i_name], [i.i_price]), partitions(p0),
is_index_back=false,
range_key([i.i_id]), range(MIN ; MAX),
range_cond([? = i.i_id])
1 row in set (0.030 sec)
```
**结论:**
+ EXPLAIN 看执行计划还是很方便的,执行计划的关键是表连接算法和顺序、索引等等。这些都能通过 EXPLAIN 看出问题。
+ EXPLAIN 的执行计划可能跟期望的执行计划有些差别,往往是 LOCATION CACHE 和 SQL 路由导致的。这两个影响因素要记得。生产环境,表被频繁访问的时候,即使分区主副本切换了,OBSERVER 也很快更新了路由。虽然是被动更新,代价就是主备副本切换后业务第一次访问的 SQL 会变慢(早期 OceanBase 版本是报错,后期版本改为 OceanBase 内部重试,所以会略微变慢。重试后,OBSERVER LOCATION CACHE 就更新了)。
## 如何查看 SQL 实际执行计划
上面说 EXPLAIN 执行计划跟期望的执行计划有差别,这个不好说,但是我们可以查看 SQL 实际执行计划来验证是不是有差异。查看 SQL 的实际执行计划要求 SQL 执行过。
运行下面两个 SQL,然后查看 SQL 审计视图,获取到 执行节点 和 PLAN_ID 信息。
```sql
select o.o_w_id , o.o_d_id ,o.o_id , i.i_name ,i.i_price ,o.o_c_id from bmsql_oorder o , bmsql_item i where o.o_id = i.i_id and o.o_w_id = 3 limit 10 ;
select o.o_w_id , o.o_d_id ,o.o_id , i.i_name ,i.i_price ,o.o_c_id from bmsql_item i , bmsql_oorder o where o.o_id = i.i_id and o.o_w_id = 3 limit 10 ;
SELECT /*+ read_consistency(weak) ob_querytimeout(100000000) */ substr(usec_to_time(request_time),1,19) request_time_, s.svr_ip, s.client_Ip, s.sid,s.tenant_id, s.tenant_name, s.user_name, s.db_name, s.query_sql, s.plan_id, s.plan_type, s.affected_rows, s.return_rows, s.ret_code, s.event, s.elapsed_time, s.queue_time, s.execute_time
FROM oceanbase.gv$sql_audit s
WHERE 1=1 and s.tenant_id = 1002
and user_name='u_tpcc' and query_sql like 'select o.o_w_id%'
and request_time >= time_to_usec(date_sub(CURRENT_TIMESTAMP, interval 5 minute ))
ORDER BY request_time DESC
LIMIT 10 \G
输出:
*************************** 1. row ***************************
request_time_: 2021-10-05 11:24:50
svr_ip: 172.20.249.52
client_Ip: 172.20.249.51
sid: 3221668666
tenant_id: 1002
tenant_name: obmysql
user_name: u_tpcc
db_name: tpccdb
query_sql: select o.o_w_id , o.o_d_id ,o.o_id , i.i_name ,i.i_price ,o.o_c_id from bmsql_item i , bmsql_oorder o where o.o_id = i.i_id and o.o_w_id = 3 limit 10
plan_id: 3305
plan_type: 3
affected_rows: 0
return_rows: 10
ret_code: 0
event: default condition wait
elapsed_time: 20058
queue_time: 73
execute_time: 19726
*************************** 2. row ***************************
request_time_: 2021-10-05 11:24:46
svr_ip: 172.20.249.51
client_Ip: 172.20.249.51
sid: 3222238517
tenant_id: 1002
tenant_name: obmysql
user_name: u_tpcc
db_name: tpccdb
query_sql: select o.o_w_id , o.o_d_id ,o.o_id , i.i_name ,i.i_price ,o.o_c_id from bmsql_oorder o , bmsql_item i where o.o_id = i.i_id and o.o_w_id = 3 limit 10
plan_id: 273
plan_type: 3
affected_rows: 0
return_rows: 10
ret_code: 0
event: system internal wait
elapsed_time: 141562
queue_time: 48
execute_time: 139714
2 rows in set (0.119 sec)
```
其中 `tenant_id``svr_ip``svr_port``plan_id` 列信息很重要。查看视图 `gv$plan_cache_plan_explain` 需要这个字段信息。
```sql
MySQL [tpccdb]> SELECT ip, plan_depth, plan_line_id,operator,name,rows,cost,property from oceanbase.`gv$plan_cache_plan_explain` WHERE tenant_id=1002 AND ip = '172.20.249.52' AND port=2882 AND plan_id=3305;
+---------------+------------+--------------+---------------------------+------+------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| ip | plan_depth | plan_line_id | operator | name | rows | cost | property |
+---------------+------------+--------------+---------------------------+------+------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 172.20.249.52 | 0 | 0 | PHY_LIMIT | NULL | 10 | 408 | NULL |
| 172.20.249.52 | 1 | 1 | PHY_NESTED_LOOP_JOIN | NULL | 10 | 407 | NULL |
| 172.20.249.52 | 2 | 2 | PHY_PX_FIFO_COORD | NULL | 10 | 39 | NULL |
| 172.20.249.52 | 3 | 3 | PHY_PX_REDUCE_TRANSMIT | NULL | 10 | 38 | NULL |
| 172.20.249.52 | 4 | 4 | PHY_TABLE_SCAN | o | 10 | 38 | table_rows:139802, physical_range_rows:47417, logical_range_rows:47407, index_back_rows:0, output_rows:47407, est_method:local_storage, avaiable_index_name[bmsql_oorder], estimation info[table_id:1101710651081625, (table_type:1, version:0-1633335774562189-1633335774562189, logical_rc:47407, physical_rc:47407), (table_type:7, version:1633335566720232-1633335774562189-1633335792045894, logical_rc:0, physical_rc:0), (table_type:5, version:1633335566720232-1633335774562189-1633335792045894, logical_rc:0, physical_rc:0), (table_type:0, version:1633335792045894-1633335792045894-9223372036854775807, logical_rc:0, physical_rc:10)] |
| 172.20.249.52 | 2 | 5 | PHY_TABLE_SCAN | i | 1 | 35 | table_rows:100000, physical_range_rows:1, logical_range_rows:1, index_back_rows:0, output_rows:1, est_method:local_storage, estimation info[table_id:1101710651081627, (table_type:1, version:0-1633335774562189-1633335774562189, logical_rc:100000, physical_rc:100000), (table_type:7, version:1633335566970454-1633335774562189-1633335794237719, logical_rc:0, physical_rc:0), (table_type:5, version:1633335566970454-1633335774562189-1633335794237719, logical_rc:0, physical_rc:0), (table_type:0, version:1633335794237719-1633335794237719-9223372036854775807, logical_rc:0, physical_rc:1)] |
+---------------+------------+--------------+---------------------------+------+------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
6 rows in set (0.030 sec)
MySQL [tpccdb]> SELECT ip, plan_depth, plan_line_id,operator,name,rows,cost,property from oceanbase.`gv$plan_cache_plan_explain` where tenant_id=1002 AND ip = '172.20.249.51' AND port=2882 AND plan_id=273;
+---------------+------------+--------------+---------------------------+------+------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| ip | plan_depth | plan_line_id | operator | name | rows | cost | property |
+---------------+------------+--------------+---------------------------+------+------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 172.20.249.51 | 0 | 0 | PHY_LIMIT | NULL | 10 | 408 | NULL |
| 172.20.249.51 | 1 | 1 | PHY_NESTED_LOOP_JOIN | NULL | 10 | 407 | NULL |
| 172.20.249.51 | 2 | 2 | PHY_TABLE_SCAN | o | 10 | 38 | table_rows:139802, physical_range_rows:47417, logical_range_rows:47407, index_back_rows:0, output_rows:47407, est_method:local_storage, avaiable_index_name[bmsql_oorder], estimation info[table_id:1101710651081625, (table_type:1, version:0-1633335774562189-1633335774562189, logical_rc:47407, physical_rc:47407), (table_type:7, version:1633335563707246-1633335774562189-1633335792045894, logical_rc:0, physical_rc:0), (table_type:5, version:1633335563707246-1633335774562189-1633335792045894, logical_rc:0, physical_rc:0), (table_type:0, version:1633335792045894-1633335792045894-9223372036854775807, logical_rc:0, physical_rc:10)] |
| 172.20.249.51 | 2 | 3 | PHY_PX_FIFO_COORD | NULL | 1 | 36 | NULL |
| 172.20.249.51 | 3 | 4 | PHY_PX_REDUCE_TRANSMIT | NULL | 1 | 35 | NULL |
| 172.20.249.51 | 4 | 5 | PHY_TABLE_SCAN | i | 1 | 35 | table_rows:100000, physical_range_rows:1, logical_range_rows:1, index_back_rows:0, output_rows:1, est_method:local_storage, estimation info[table_id:1101710651081627, (table_type:1, version:0-1633335774562189-1633335774562189, logical_rc:100000, physical_rc:100000), (table_type:7, version:1633335563894016-1633335774562189-1633335794237719, logical_rc:0, physical_rc:0), (table_type:5, version:1633335563894016-1633335774562189-1633335794237719, logical_rc:0, physical_rc:0), (table_type:0, version:1633335794237719-1633335794237719-9223372036854775807, logical_rc:0, physical_rc:1)] |
+---------------+------------+--------------+---------------------------+------+------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
6 rows in set (0.065 sec)
```
如果是在网页上,上面的输出结果格式化正确的话,对比 2 个 SQL 的实际执行计划可以看出分别是对那个表进行远程访问。
除了通过 SQL 审计视图定位具体的 SQL 及其执行计划外,还可以通过查看缓存的执行计划汇总视图 `gv$plan_cache_plan_stat`
```sql
SELECT s.tenant_id, svr_ip,plan_Id,sql_id,TYPE, query_sql, first_load_time, avg_exe_usec, slow_count,executions, slowest_exe_usec,s.outline_id
FROM oceanbase.`gv$plan_cache_plan_stat` s
WHERE s.tenant_id = 1002 -- 改成具体的 tenant_id
ORDER BY avg_exe_usec desc limit 10
;
+-----------+---------------+---------+----------------------------------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+--------------+------------+------------+------------------+------------+
| tenant_id | svr_ip | plan_Id | sql_id | TYPE | query_sql | first_load_time | avg_exe_usec | slow_count | executions | slowest_exe_usec | outline_id |
+-----------+---------------+---------+----------------------------------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+--------------+------------+------------+------------------+------------+
| 1002 | 172.20.249.49 | 251 | FC3FED8CCB2946DE54F1C5BA3656023C | 1 | SELECT d_tax, d_next_o_id FROM bmsql_district WHERE d_w_id = 4 AND d_id = 3 FOR UPDATE | 2021-10-04 20:52:36.677973 | 25668895 | 5 | 39 | 999820024 | -1 |
| 1002 | 172.20.249.49 | 350 | 54B5A5861DAF78F52D9ADFBEE83D35B5 | 2 | INSERT INTO bmsql_new_order ( no_o_id, no_d_id, no_w_id) VALUES (4860, 9, 5) | 2021-10-04 20:52:38.032855 | 1231951 | 3 | 3 | 1237821 | -1 |
| 1002 | 172.20.249.49 | 322 | B447DE16B3F42D2409B2A2BE50328E63 | 2 | UPDATE bmsql_warehouse SET w_ytd = w_ytd + 3684.0 WHERE w_id = 8 | 2021-10-04 20:52:36.762523 | 486163 | 2 | 2 | 703346 | -1 |
| 1002 | 172.20.249.52 | 2596 | F4585305C4CB9B091C750826A7DEDD13 | 1 | UPDATE bmsql_district SET d_ytd = d_ytd + 134.32 WHERE d_w_id = 2 AND d_id = 9 | 2021-10-04 20:52:36.689589 | 237542 | 5 | 16877 | 999895631 | -1 |
| 1002 | 172.20.249.52 | 2602 | FC3FED8CCB2946DE54F1C5BA3656023C | 1 | SELECT d_tax, d_next_o_id FROM bmsql_district WHERE d_w_id = 2 AND d_id = 6 FOR UPDATE | 2021-10-04 20:52:36.727204 | 237286 | 4 | 16889 | 999746020 | -1 |
| 1002 | 172.20.249.51 | 272 | 9D276020142C5B8259B85C3E8966C579 | 3 | select o.o_w_id , o.o_d_id ,o.o_id , i.i_name ,i.i_price ,o.o_c_id
from bmsql_oorder o , bmsql_item i
where o.o_id = i.i_id
and o.o_w_id = 3 limit 10 | 2021-10-05 11:02:46.942620 | 159385 | 1 | 1 | 159385 | -1 |
| 1002 | 172.20.249.49 | 309 | B447DE16B3F42D2409B2A2BE50328E63 | 2 | UPDATE bmsql_warehouse SET w_ytd = w_ytd + 4878.65 WHERE w_id = 8 | 2021-10-04 20:52:36.741234 | 159196 | 4 | 8 | 452859 | -1 |
| 1002 | 172.20.249.51 | 273 | 274FB280E08E876819555632D6951C5A | 3 | select o.o_w_id , o.o_d_id ,o.o_id , i.i_name ,i.i_price ,o.o_c_id from bmsql_oorder o , bmsql_item i where o.o_id = i.i_id and o.o_w_id = 3 limit 10 | 2021-10-05 11:24:46.098640 | 141562 | 1 | 1 | 141562 | -1 |
| 1002 | 172.20.249.52 | 3367 | CDA629CFD7B13E58328149264FA50055 | 3 | SELECT /*+ read_consistency(weak) ob_querytimeout(100000000) */ substr(usec_to_time(request_time),1,19) request_time_, s.svr_ip, s.client_Ip, s.sid,s.tenant_id, s.tenant_name, s.user_name, s.db_name, s.query_sql, s.plan_id, s.plan_type, s.affected_rows, s.return_rows, s.ret_code, s.event, s.elapsed_time, s.queue_time, s.execute_time FROM oceanbase.gv$sql_audit s WHERE 1=1 and s.tenant_id = 1002 and user_name='u_tpcc' and query_sql like 'select o.o_w_id%' and request_time >= time_to_usec(date_sub(CURRENT_TIMESTAMP, interval 5 minute )) ORDER BY request_time DESC LIMIT 10 | 2021-10-05 11:25:07.245044 | 115650 | 1 | 1 | 115650 | -1 |
| 1002 | 172.20.249.52 | 3362 | 55E820294D4EEFB396B1BAD3CE27CD47 | 3 | SELECT /*+ read_consistency(weak) ob_querytimeout(100000000) */ substr(usec_to_time(request_time),1,19) request_time_, s.svr_ip, s.client_Ip, s.sid,s.tenant_id, s.tenant_name, s.user_name, s.db_name, s.query_sql, s.plan_id, s.plan_type, s.affected_rows, s.return_rows, s.ret_code, s.event, s.elapsed_time, s.queue_time, s.execute_time
FROM oceanbase.gv$sql_audit s
WHERE 1=1 and s.tenant_id = 1002
and user_name='u_tpcc' and query_sql like 'select o.o_w_id%'
and request_time >= time_to_usec(date_sub(CURRENT_TIMESTAMP, interval 60 minute ))
ORDER BY request_time DESC
LIMIT 100 | 2021-10-05 11:24:04.440565 | 111119 | 1 | 2 | 150067 | -1 |
+-----------+---------------+---------+----------------------------------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+--------------+------------+------------+------------------+------------+
10 rows in set (0.014 sec)
```
从这个视图里可以看到全局的 SQL 执行汇总。适合找 TOP N 慢SQL。根据里面的 节点信息、SQLID、PLANID 信息,既可以到 SQL 审计视图里定位具体的 SQL信息,也可以查看实际运行的执行计划信息。
执行计划可以清空,命令如下:
```sql
ALTER SYSTEM flush plan cache GLOBAL;
```
仅用于测试环境研究,生产环境的 SQL 执行计划缓存通常不要随便清空。清空执行计划会导致所有 SQL 要重新来一次硬解析。
## 如何使用 OUTLINE 改变执行计划
OceanBase 在 SQL执行和性能诊断方面的逻辑大量参考了ORACLE的设计思路。OceanBase也支持`SQL Outline`功能,能够修改在线运行的SQL执行计划。同时也支持SQL限流功能。
Outline的用法也是通过`SQL Hint`固定SQL的执行计划,可以调整表连接算法、使用的索引等等。
创建大纲的语法如下:
```sql
CREATE [OR REPLACE] OUTLINE outline_name ON stmt [ TO target_stmt ];
CREATE [OR REPLACE] OUTLINE outline_name ON 'SQLID' using 'HINTS';
```
1. 其中stmt为一个带有HINT的DML语句。限流或固定计划,通过stmt中的HINT来区分。
2. 如果期望对含有HINT的语句进行限流和固定计划,则需要TO target_stmt来指明相应的SQL。`create outline outline_name on stmt1 to stmt2;`的语意是说对`stmt2`创建outline,让`stmt2`使用`stmt1`中的`hint`
3. 指定 `OR REPLACE` 后,可以对已经存在执行计划或限流规则进行替换。(注:限流规则和执行计划间可以彼此替换)
4. 在使用 `target_stmt` 时,严格要求 `stmt``target_stmt` 在去掉 `hint` 后完全匹配(实现中为去掉`hint``signature`相同)。若是在创建限流时使用 `target_stmt`,则同时要求`fix_param` 完全匹配。
通过语句文本或者 SQLID 都可以创建大纲。不过由于文本中空格和换行如果有差异就会导致匹配不上。所以,不建议通过文本来创建大纲(除非文本非常简单)。这里通过 SQLID 来创建大纲。只要 SQL 文本不变,SQLID 就不会变。
### OUTLINE 相关视图
+ 存储outline的schema信息的系统表
```sql
oceanbase.__all_outline
oceanbase.__all_outline_history
```
+ 固定计划相关虚拟表和试图
展示的均是当前租户的信息:
```sql
oceanbase.__tenant_virtual_outline 用于outline迁移使用的虚拟表,同时显示固定计划的信息。
oceanbase.gv$outline __tenant_virutal_outline基础上创建的视图。
information_schema.dba_outlines __all_table上创建的视图。
```
+ 限流相关虚拟表和试图
下表展示的均是当前租户的信息:
```sql
oceanbase.__tenant_virtual_concurrent_limit_sql 展示限流信息
oceanbase.gv$concurrent_limit_sql __tenant_virtual_concurrent_limit_sql上创建的视图。
```
### OUTLINE 调优执行计划示例
下面示例还是针对前面测试 SQL ,尝试调整一下执行计划里表连接顺序。
+ 首先执行原 SQL ,通过 视图 `gv$sql_audit``gv$plan_cache_plan_stat` 找到该 SQL 的 SQLID 。
+ 然后根据 SQLID 创建大纲,指定 HINTS 。
```sql
create outline otl_test_1 on "9D276020142C5B8259B85C3E8966C579" using hint /*+ leading(i) */ ;
MySQL [tpccdb]> select * from oceanbase.__all_outline \G
*************************** 1. row ***************************
gmt_create: 2021-10-05 12:18:26.032586
gmt_modified: 2021-10-05 12:18:26.032586
tenant_id: 0
outline_id: 1001
database_id: 1052
schema_version: 1633407506030264
name: otl_test_1
signature:
outline_content: /*+leading(i) */
sql_text:
owner: root
used: 0
version: 3-b20901e8c84d3ea774beeaca963c67d7802e4b4e
compatible: 1
enabled: 1
format: 0
outline_params: ����
outline_target:
sql_id: 9D276020142C5B8259B85C3E8966C579
owner_id: NULL
1 row in set (0.006 sec)
```
+ 再次执行 SQL。
```sql
select o.o_w_id , o.o_d_id ,o.o_id , i.i_name ,i.i_price ,o.o_c_id
from bmsql_oorder o , bmsql_item i
where o.o_id = i.i_id
and o.o_w_id = 3 limit 10
;
```
+ 查看实际执行计划
```sql
SELECT s.tenant_id, svr_ip,plan_Id,sql_id,TYPE, query_sql, first_load_time, avg_exe_usec, slow_count,executions, slowest_exe_usec,s.outline_id
FROM oceanbase.`gv$plan_cache_plan_stat` s
WHERE s.tenant_id = 1002 and sql_id='9D276020142C5B8259B85C3E8966C579'
ORDER BY avg_exe_usec desc limit 10
;
+-----------+---------------+---------+----------------------------------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+--------------+------------+------------+------------------+------------------+
| tenant_id | svr_ip | plan_Id | sql_id | TYPE | query_sql | first_load_time | avg_exe_usec | slow_count | executions | slowest_exe_usec | outline_id |
+-----------+---------------+---------+----------------------------------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+--------------+------------+------------+------------------+------------------+
| 1002 | 172.20.249.51 | 282 | 9D276020142C5B8259B85C3E8966C579 | 3 | select o.o_w_id , o.o_d_id ,o.o_id , i.i_name ,i.i_price ,o.o_c_id
from bmsql_oorder o , bmsql_item i
where o.o_id = i.i_id
and o.o_w_id = 3 limit 10 | 2021-10-05 12:19:15.443750 | 141419 | 1 | 1 | 141419 | 1101710651032553 |
| 1002 | 172.20.249.52 | 3488 | 9D276020142C5B8259B85C3E8966C579 | 3 | select o.o_w_id , o.o_d_id ,o.o_id , i.i_name ,i.i_price ,o.o_c_id
from bmsql_oorder o , bmsql_item i
where o.o_id = i.i_id
and o.o_w_id = 3 limit 10 | 2021-10-05 12:18:36.746965 | 88434 | 0 | 1 | 88434 | 1101710651032553 |
+-----------+---------------+---------+----------------------------------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+--------------+------------+------------+------------------+------------------+
2 rows in set (0.092 sec)
SELECT ip, plan_depth, plan_line_id,operator,name,rows,cost,property
from oceanbase.`gv$plan_cache_plan_explain`
where tenant_id=1002 AND ip = '172.20.249.51' AND port=2882 AND plan_id=282 ;
+---------------+------------+--------------+---------------------------+------+-------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| ip | plan_depth | plan_line_id | operator | name | rows | cost | property |
+---------------+------------+--------------+---------------------------+------+-------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 172.20.249.51 | 0 | 0 | PHY_LIMIT | NULL | 10 | 126991 | NULL |
| 172.20.249.51 | 1 | 1 | PHY_MERGE_JOIN | NULL | 10 | 126990 | NULL |
| 172.20.249.51 | 2 | 2 | PHY_PX_FIFO_COORD | NULL | 1453 | 856 | NULL |
| 172.20.249.51 | 3 | 3 | PHY_PX_REDUCE_TRANSMIT | NULL | 1453 | 546 | NULL |
| 172.20.249.51 | 4 | 4 | PHY_TABLE_SCAN | i | 1453 | 546 | table_rows:100000, physical_range_rows:100001, logical_range_rows:100000, index_back_rows:0, output_rows:100000, est_method:local_storage, avaiable_index_name[bmsql_item], pruned_index_name[idx_item_uk], estimation info[table_id:1101710651081627, (table_type:1, version:0-1633335774562189-1633335774562189, logical_rc:100000, physical_rc:100000), (table_type:7, version:1633335563894016-1633335774562189-1633335794237719, logical_rc:0, physical_rc:0), (table_type:5, version:1633335563894016-1633335774562189-1633335794237719, logical_rc:0, physical_rc:0), (table_type:0, version:1633335794237719-1633335794237719-9223372036854775807, logical_rc:0, physical_rc:1)] |
| 172.20.249.51 | 2 | 5 | PHY_SORT | NULL | 689 | 125832 | NULL |
| 172.20.249.51 | 3 | 6 | PHY_TABLE_SCAN | o | 47407 | 11907 | table_rows:139802, physical_range_rows:47417, logical_range_rows:47407, index_back_rows:0, output_rows:47407, est_method:local_storage, avaiable_index_name[bmsql_oorder], estimation info[table_id:1101710651081625, (table_type:1, version:0-1633335774562189-1633335774562189, logical_rc:47407, physical_rc:47407), (table_type:7, version:1633335563707246-1633335774562189-1633335792045894, logical_rc:0, physical_rc:0), (table_type:5, version:1633335563707246-1633335774562189-1633335792045894, logical_rc:0, physical_rc:0), (table_type:0, version:1633335792045894-1633335792045894-9223372036854775807, logical_rc:0, physical_rc:10)] |
+---------------+------------+--------------+---------------------------+------+-------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
7 rows in set (0.026 sec)
SELECT ip, plan_depth, plan_line_id,operator,name,rows,cost,property
from oceanbase.`gv$plan_cache_plan_explain`
where tenant_id=1002 AND ip = '172.20.249.52' AND port=2882 AND plan_id=3488 ;
+---------------+------------+--------------+---------------------------+------+-------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| ip | plan_depth | plan_line_id | operator | name | rows | cost | property |
+---------------+------------+--------------+---------------------------+------+-------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 172.20.249.52 | 0 | 0 | PHY_LIMIT | NULL | 10 | 135657 | NULL |
| 172.20.249.52 | 1 | 1 | PHY_MERGE_JOIN | NULL | 10 | 135656 | NULL |
| 172.20.249.52 | 2 | 2 | PHY_TABLE_SCAN | i | 1453 | 546 | table_rows:100000, physical_range_rows:100001, logical_range_rows:100000, index_back_rows:0, output_rows:100000, est_method:local_storage, avaiable_index_name[bmsql_item], pruned_index_name[idx_item_uk], estimation info[table_id:1101710651081627, (table_type:1, version:0-1633335774562189-1633335774562189, logical_rc:100000, physical_rc:100000), (table_type:7, version:1633335566970454-1633335774562189-1633335794237719, logical_rc:0, physical_rc:0), (table_type:5, version:1633335566970454-1633335774562189-1633335794237719, logical_rc:0, physical_rc:0), (table_type:0, version:1633335794237719-1633335794237719-9223372036854775807, logical_rc:0, physical_rc:1)] |
| 172.20.249.52 | 2 | 3 | PHY_PX_FIFO_COORD | NULL | 47407 | 134808 | NULL |
| 172.20.249.52 | 3 | 4 | PHY_PX_REDUCE_TRANSMIT | NULL | 47407 | 125832 | NULL |
| 172.20.249.52 | 4 | 5 | PHY_SORT | NULL | 47407 | 125832 | NULL |
| 172.20.249.52 | 5 | 6 | PHY_TABLE_SCAN | o | 47407 | 11907 | table_rows:139802, physical_range_rows:47417, logical_range_rows:47407, index_back_rows:0, output_rows:47407, est_method:local_storage, avaiable_index_name[bmsql_oorder], estimation info[table_id:1101710651081625, (table_type:1, version:0-1633335774562189-1633335774562189, logical_rc:47407, physical_rc:47407), (table_type:7, version:1633335566720232-1633335774562189-1633335792045894, logical_rc:0, physical_rc:0), (table_type:5, version:1633335566720232-1633335774562189-1633335792045894, logical_rc:0, physical_rc:0), (table_type:0, version:1633335792045894-1633335792045894-9223372036854775807, logical_rc:0, physical_rc:10)] |
+---------------+------------+--------------+---------------------------+------+-------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
7 rows in set (0.008 sec)
```
从上面结果看出,这个 SQL 执行了两次,两次被路由到的节点不同,导致远程访问的表会不一样。但是两个 SQL 都是用了同一个 OUTLINE 成功了,实际执行计划里 ,表 `bmsql_item` 表确实是 驱动表,表连接算法也从 `NESTED-LOOP JOIN` 变成了 `MERGE JOIN`
当然,这个只是示例,这个 HINTS 并不一定会让 SQL 性能更好。
+ 删除 OUTLINE 。
```sql
drop outline otl_test_1 ;
```
删除大纲后,该 SQLID 的执行计划重新生成,又回归到原始的执行计划了。
## 如何更新统计信息
OceanBase 当前版本(3.1)还没有手动更新表统计信息能力,表的统计信息更新是在合并(`MAJOR FREEZE`)的时候更新的。默认情况下,OceanBase 收集统计信息会扫描所有数据块,所以统计信息里的分区大小和行数估计非常准确。如果表非常大,为了节省这个时间,也可以通过参数 `merge_stat_sampling_ratio` 设置一个采样比例(默认是100)。
在 SYS 租户运行:
```sql
alter system set merge_stat_sampling_ratio = 50 ;
```
+ 查看统计信息
```sql
SELECT t.tenant_id, a.tenant_name, t.table_name, d.database_name, tg.tablegroup_name , t.part_num , t2.partition_id, t2.ZONE, t2.svr_ip , round(t2.data_size/1024/1024) data_size_mb, t2.row_count
FROM oceanbase.__all_tenant AS a
JOIN oceanbase.__all_virtual_database AS d ON ( a.tenant_id = d.tenant_id )
JOIN oceanbase.__all_virtual_table AS t ON (t.tenant_id = d.tenant_id AND t.database_id = d.database_id)
JOIN oceanbase.__all_virtual_meta_table t2 ON (t.tenant_id = t2.tenant_id AND (t.table_id=t2.table_id OR t.tablegroup_id=t2.table_id) AND t2.ROLE IN (1) )
LEFT JOIN oceanbase.__all_virtual_tablegroup AS tg ON (t.tenant_id = tg.tenant_id and t.tablegroup_id = tg.tablegroup_id)
WHERE a.tenant_id IN (1002 ) AND t.table_type IN (3)
AND d.database_name = 'tpccdb'
ORDER BY t.tenant_id, tg.tablegroup_name, d.database_name, t.table_name, t2.partition_id
;
+-----------+-------------+------------------+---------------+-----------------+----------+--------------+-------+---------------+--------------+-----------+
| tenant_id | tenant_name | table_name | database_name | tablegroup_name | part_num | partition_id | ZONE | svr_ip | data_size_mb | row_count |
+-----------+-------------+------------------+---------------+-----------------+----------+--------------+-------+---------------+--------------+-----------+
| 1002 | obmysql | bmsql_config | tpccdb | NULL | 1 | 0 | zone2 | 172.20.249.49 | 0 | 4 |
| 1002 | obmysql | bmsql_item | tpccdb | NULL | 1 | 0 | zone1 | 172.20.249.52 | 7 | 100000 |
| 1002 | obmysql | bmsql_customer | tpccdb | tpcc_group | 3 | 0 | zone3 | 172.20.249.51 | 40 | 90000 |
| 1002 | obmysql | bmsql_customer | tpccdb | tpcc_group | 3 | 1 | zone2 | 172.20.249.49 | 54 | 120000 |
| 1002 | obmysql | bmsql_customer | tpccdb | tpcc_group | 3 | 2 | zone1 | 172.20.249.52 | 41 | 90000 |
| 1002 | obmysql | bmsql_district | tpccdb | tpcc_group | 3 | 0 | zone3 | 172.20.249.51 | 0 | 30 |
| 1002 | obmysql | bmsql_district | tpccdb | tpcc_group | 3 | 1 | zone2 | 172.20.249.49 | 0 | 40 |
| 1002 | obmysql | bmsql_district | tpccdb | tpcc_group | 3 | 2 | zone1 | 172.20.249.52 | 0 | 30 |
| 1002 | obmysql | bmsql_history | tpccdb | tpcc_group | 3 | 0 | zone3 | 172.20.249.51 | 3 | 137475 |
| 1002 | obmysql | bmsql_history | tpccdb | tpcc_group | 3 | 1 | zone2 | 172.20.249.49 | 5 | 200065 |
| 1002 | obmysql | bmsql_history | tpccdb | tpcc_group | 3 | 2 | zone1 | 172.20.249.52 | 4 | 149261 |
| 1002 | obmysql | bmsql_new_order | tpccdb | tpcc_group | 3 | 0 | zone3 | 172.20.249.51 | 0 | 31962 |
| 1002 | obmysql | bmsql_new_order | tpccdb | tpcc_group | 3 | 1 | zone2 | 172.20.249.49 | 0 | 45222 |
| 1002 | obmysql | bmsql_new_order | tpccdb | tpcc_group | 3 | 2 | zone1 | 172.20.249.52 | 0 | 33058 |
| 1002 | obmysql | bmsql_oorder | tpccdb | tpcc_group | 3 | 0 | zone3 | 172.20.249.51 | 1 | 139802 |
| 1002 | obmysql | bmsql_oorder | tpccdb | tpcc_group | 3 | 1 | zone2 | 172.20.249.49 | 2 | 203152 |
| 1002 | obmysql | bmsql_oorder | tpccdb | tpcc_group | 3 | 2 | zone1 | 172.20.249.52 | 2 | 150858 |
| 1002 | obmysql | bmsql_order_line | tpccdb | tpcc_group | 3 | 0 | zone3 | 172.20.249.51 | 49 | 1397517 |
| 1002 | obmysql | bmsql_order_line | tpccdb | tpcc_group | 3 | 1 | zone2 | 172.20.249.49 | 72 | 2029431 |
| 1002 | obmysql | bmsql_order_line | tpccdb | tpcc_group | 3 | 2 | zone1 | 172.20.249.52 | 53 | 1508395 |
| 1002 | obmysql | bmsql_stock | tpccdb | tpcc_group | 3 | 0 | zone3 | 172.20.249.51 | 75 | 300000 |
| 1002 | obmysql | bmsql_stock | tpccdb | tpcc_group | 3 | 1 | zone2 | 172.20.249.49 | 99 | 400000 |
| 1002 | obmysql | bmsql_stock | tpccdb | tpcc_group | 3 | 2 | zone1 | 172.20.249.52 | 75 | 300000 |
| 1002 | obmysql | bmsql_warehouse | tpccdb | tpcc_group | 3 | 0 | zone3 | 172.20.249.51 | 0 | 3 |
| 1002 | obmysql | bmsql_warehouse | tpccdb | tpcc_group | 3 | 1 | zone2 | 172.20.249.49 | 0 | 4 |
| 1002 | obmysql | bmsql_warehouse | tpccdb | tpcc_group | 3 | 2 | zone1 | 172.20.249.52 | 0 | 3 |
+-----------+-------------+------------------+---------------+-----------------+----------+--------------+-------+---------------+--------------+-----------+
26 rows in set (0.014 sec)
```
抽查一个表数据,可以看出行数统计还是很精准的。
```sql
MySQL [tpccdb]> select 'p0', count(*) from bmsql_oorder partition (p0)
union select 'p1', count(*) from bmsql_oorder partition (p1)
union select 'p2', count(*) from bmsql_oorder partition (p2)
;
+------+----------+
| p0 | count(*) |
+------+----------+
| p0 | 139802 |
| p1 | 203152 |
| p2 | 150876 |
+------+----------+
3 rows in set (0.623 sec)
```
# 第 8 课:OceanBase 社区版生态工具介绍
本章主要简单介绍OceanBase 相关的工具、产品等用法。
更多工具欢迎大家补充。
## 本章目录
+ [主机监控产品](8.1.md)
+ [数据迁移产品](8.2.md)
+ [运维工具](8.3.md)
## 如何联系我们
欢迎广大 OceanBase 爱好者、用户和客户有任何问题联系我们反馈:
+ 社区版官网论坛:[https://open.oceanbase.com/answer](https://open.oceanbase.com/answer)
+ 社区版项目网站提 `Issue`[https://github.com/oceanbase/oceanbase/issues](https://github.com/oceanbase/oceanbase/issues)
+ 钉钉群:群号 `33254054`
# 主机监控产品
## 传统监控产品
OceanBase 本质上是一个单进程软件,进程名是:`observer` 。进程启动时,默认会将主机的 CPU、内存和磁盘空间(指数据盘,启动参数里可以定义)大部分资源都占据。其中 CPU 的占用是声明式的,并不会实际占有。内存的占用是预分配的,默认会占用 80% 主机可用内存(参数`memory_limit_percentage` 指定)。通常 OceanBase 进程所在主机不适合在跑其他数据库或者应用软件等。磁盘的占用也是预分配的,默认会占用 90%(参数 `datafile_disk_percentage` 指定)。进程正常启动后会监听两个端口:默认是 `2881``2882`
跟其他传统数据库一样,可以使用已有的监控平台监控 OceanBase 主机的:
+ 主机可用性,可以 `ping`
+ 主机 `load`
+ 主机 `cpu`
+ 主机 `mem`。注意监控可用内存就行。可用内存低于 1G ,进程`observer` 有 OOM 风险。
+ 主机磁盘,包括 IO利用率、IO延时、IO 吞吐量等。分数据盘和日志盘等。
+ 主机网络监听端口,包括 `ssh` 的端口(默认 22 ,可以改)、`observer` 的连接端口(默认 2881,可以改)、`observer` 的 RPC 端口(默认 2882,可以改)。
+ 主机网卡流量。包括进程 `observer` 监听的那个网卡。当网卡流量接近能力上限(通常是10000Mb), SQL 性能变慢,load 变高。
## `tsar` 工具
`tsar` 很早就在 `github` 上开源了,项目地址是:`https://github.com/alibaba/tsar`
`tsar` 是淘宝的一个用来收集服务器系统和应用信息的采集报告工具,如收集服务器的系统信息(`cpu``mem` 等),以及应用数据(`nginx``swift` 等),收集到的数据存储在服务器磁盘上,可以随时查询历史信息,也可以将数据发送到 `nagios` 报警。`tsar` 带来的性能影响很小,存储空间也很小。可以独立运行,作为现有监控手段的一个补充。
`tsar` 还能够比较方便的增加模块,只需要按照 `tsar` 的要求编写数据的采集函数和展现函数,就可以把自定义的模块加入到`tsar` 中。
# 数据迁移产品
## DataX 数据交换平台
DataX 也是阿里巴巴开源的一款实用产品,项目地址是:`https://github.com/alibaba/datax`
DataX 是阿里云 DataWorks数据集成 的开源版本,在阿里巴巴集团内被广泛使用的离线数据同步工具/平台。DataX 实现了包括 OceanBase、MySQL、Oracle、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(OTS)、MaxCompute(ODPS)、Hologres、DRDS 等各种异构数据源之间高效的数据同步功能。
## Canal 数据增量工具
Canal 也是阿里巴巴开源的一款数据同步产品,项目地址时候:`https://github.com/alibaba/canal`
Canal 主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。
基于日志增量订阅和消费的业务包括
+ 数据库镜像
+ 数据库实时备份
+ 索引构建和实时维护(拆分异构索引、倒排索引等)
+ 业务 cache 刷新
+ 带业务逻辑的增量数据处理
当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x 。
Canal 跟 DataX 结合可以实现将 MySQL 数据实时同步到 OceanBase 的 MySQL 租户中。
# 运维工具
## `dooba` 性能监控
`dooba` 是 OceanBase 内部的一个运维脚本,用 Python 语言开发,金支持 Python 2.7 。
## 使用方法
`dooba` 的原理是使用 `mysql` 命令连接到 OceanBase 的 SYS 租户里,实时展示租户 SQL 的 QPS(包括 `select`/`update`/`insert`/`delete`/`commit` ) 以及相应 SQL 的平均延时(RT)。同时还可以看各个节点的 SQL 的 QPS 以及 RT 等。
如果没有 `mysql` 客户端,需要安装一个。尽量安装版本 5.5/5.6/5.7 的 MySQL 客户端,因为早期的 OBPROXY (版本低于 2.0.0)可能不完全兼容 MySQL 8.0 的连接协议。
使用命令如下:
```bash
python dooba.py -h OBPROXY地址 -u root@sys#集群名 -P OBPROXY端口 -p密码
如:
python dooba.py -h 172.20.249.54 -u root@sys#obce-3zones -P 2883 -pxxxxxx
```
登录后常用的快捷键如下:
+ `c` : 选择租户。一般是观察业务租户的性能。
+ `1`:查看快捷键。这里没有提到的快捷键尽量别用,不保证功能正确。
+ `2`:查看租户性能概览。
+ `3`:查看租户各个节点的性能概览。如果节点很多会显示不全(除非屏幕非常大)。
+ `tab`:在各个 `widget` 之间跳转。
+ `d`:删除 `tab` 焦点所在的 `widget`
+ `R`:恢复当前界面所有的 `widget` 布局(包括被删除的)。
脚本 `dooba` 的源码可以直接编辑查看,工具中各个缩写对应的含义在源码里都能找到解释。
## 租户性能概览
主要是查看租户层面的 SQL 的 QPS、RT 以及内存、IO 等实时信息。
`dooba`的性能数据取自 SYS 租户的内部视图 `gv$sysstat``dooba` 的监控信息的 SQL 都可以直接查看,可以参考它做类似的监控产品。
```sql
# QPS RT 信息
SELECT t1.con_id tenant_id,t2.zone, t1.svr_ip,t1.svr_port , t1.stat_id, t1.name, t1.value
FROM gv$sysstat t1 JOIN __all_server t2 ON (t1.svr_ip=t2.svr_ip and t1.svr_port=t2.svr_port) JOIN __all_resource_pool t3 ON (t1.con_id=t3.tenant_id) JOIN __all_unit_config t4 ON (t3.unit_config_id=t4.unit_config_id)
WHERE t1.con_id IN (1001)
and stat_id in (10000,10004,10005,10006,30007,30008,30009,40000,40001,40002,40003,40004,40005,40006,40007,40008,40009,40010,40011,40012,40013,50000,50001,50002,50003,50004,50005,50006,50007,50008,50009,50010,50011,60000,60002,60003,60005,130000,130001,130002,130004)
ORDER BY t1.con_id, t2.zone, t1.svr_ip, t1.svr_port;
```
这个 SQL 取出所有节点的性能数据,汇总就是整个租户的 性能数据。
![dooba](media/16336656341015.jpg)
这个界面主要关注点:
+ `SQL COUNT` : QPS 信息。包括各类 SQL 的每秒执行次数。SQL 类型分别是 `select``insert``update``delete``replace``commit``rollback` 的。前四类 SQL 总数跟 `commit` 的比值大致可以推出事务的平均语句数。`rollback` 是回滚的信息,如果太高,很有可能是数据库有问题或者业务设计有问题。
+ `SQL RT`:SQL 每次平均执行耗时。包括各类 SQL 的每次平均执行耗时,单位是毫秒(`ms`)。SQL 类型分别是 `select``insert``update``delete``replace``commit` 。前四类可以看出 SQL 正常情况下的执行耗时水平。如果有大幅变动,可能是数据量突增、有 SQL 缺乏索引、或者有锁等待、CPU 瓶颈等等。具体分析还要结合 SQL 审计视图 `gv$sql_audit` 验证。`commit` 的平均执行耗时也可以反映事务的大小。OceanBase 的 `commit` 会同步事务所有的事务日志(`clog`)到多数派节点(包括自身),这个时间会反映在 `commit` 的耗时里。如果这个值突然增长很大,说明可能有突发的大事务。当节点网络性能下降时或者时间同步误差变大的时候,都可能导致 `commit` 耗时增长。
+ `RPC`:反馈网络信息。这个统计信息不是很准,待以后完善。暂时不用看。
+ `Memory`:这个是 `Memstore` 内存的实时监控。默认情况下,租户可用内存的一半用于 `Memstore` 的内存,用于存放业务写操作带来的变化的数据。OceanBase 的特色之一就是写只记录数据的变化部分,并且数据会尽可能的维持在内存里不落盘,直到增量内存利用率到达阈值(参数 `freeze_trigger_percentage` )后触发转储或合并才将数据落盘并释放内存。转储是内存中增量数据直接写到磁盘暂存,合并是内存中增量数据跟磁盘中基线数据在内存中合并为最新的数据块(`sstable`)再写回到数据文件中。转储类似传统数据库的 `checkpoint` ,对租户性能影响很小并且可以调优,合并对租户性能稍大一些并且也是可以调优的。通常会建议尽量发起转储,等凌晨低峰期时再发起合并操作。合并可以定时触发,也可以手动触发。当转储次数(参数 `minor_freeze_times` )用尽时也会自动触发合并。这个监控看的就是增量内存的变化量、累积量和增量内存使用水位。这个监控结果再结合视图 `gv$mem_store` 基本上可以定位所有的增量内存不足问题。当这个内存水位增长到 `freeze_trigger_percentage` 就会触发转储,当水位继续增长到租户限速阈值(租户参数 `writing_throttling_trigger_percentage` )会触发租户写入限速,当水位增长到接近 100% 的时候,租户内存耗尽,业务写会大量报错(`Over tenant memory limit1`)。调优参数 `freeze_trigger_percentage` 和 转储内部并发数可以将租户的内存水位控制在一个合理的安全的范围内。
+ `IOPS`:这个是磁盘IO 的实时监控。`SESS` 是会话数供参考,量大的时候可能会不准。`IOR` 是读 IOPS ,`IOR-SZ` 是读 IO 吞吐量。当第一次读入数据或者数据不在内存中时就会有读物理 IO 。读物理 IO 如果非常高,会导致 SQL 的平均延时水平很高,说明内存不足,对 IO 依赖性变大。很可能导致 IO 触达瓶颈。`IOW`是写 IOPS,`IOW-SZ` 是写吞吐量。平时很少有写 IO ,因为 OceanBase 的写都在内存里。只有转储和合并的时候,能看到短暂的密集的写 IO 。
## 租户节点性能概览
查看租户在各个节点的 SQL 的 QPS、RT 以及一些缓存命中率等。
```sql
# 取节点 CPU
SELECT t1.con_id tenant_id,t2.zone, t1.svr_ip,t1.svr_port , round(t1.value/(100*t4.max_cpu), 3) cpu_usage
FROM gv$sysstat t1 JOIN __all_server t2 ON (t1.svr_ip=t2.svr_ip and t1.svr_port=t2.svr_port) JOIN __all_resource_pool t3 ON (t1.con_id=t3.tenant_id) JOIN __all_unit_config t4 ON (t3.unit_config_id=t4.unit_config_id)
WHERE t1.con_id IN (1001) and t1.stat_id=140006
ORDER BY t1.con_id, t2.zone, t1.svr_ip, t1.svr_port;
```
![dooba 2](media/16336656921276.jpg)
这个界面主要关注点:
+ 活跃会话数 : 供参考,不是严格精准。
+ CPU 利用率:这个是 OceanBase 内部统计的各个节点的 CPU 利用率,不是 OS 里看到的 CPU 利用率(OceanBase 是单进程软件)。
+ 各个缓存的命中率:这里有 `Cache Block` 以及其 `Cache Block Index` 的缓存命中率。主要关注前者。理想情况是 99% 或者 100% 。当初次读入大量数据、或者内存有瓶颈的时候、或者合并后时,这个利用率会低于 99%,甚至低于 90%。 如果长期低于 80%,那说明内存瓶颈问题很严重。其他行缓存命中率通常不是问题。
+ 物理IO 读写:这个各个节点的物理 IO 信息,有很大参考意义。
+ SQL 的 QPS 以及 RT 信息:各个节点的 QPS 可以看出租户请求在各个节点的流量比例关系,相应的 RT 可以看出各个节点的性能状况。其中最后两个 `SLC``SRC` 表示是节点上的本地 SQL 和远程 SQL 执行次数信息。通常本地 SQL 的性能会比远程 SQL 性能好,所以这里要留意远程 SQL 的比例,如果比例很高,这个节点的 CPU利用率、SQL 的 RT 均值都会比较高。这个就需要分析原因。可能是租户 PRIMARY_ZONE 不适合打散或者表结构设计不合理,可以考虑使用表分组、调整 PRIMARY_ZONE 或 UNIT_NUM 等等。
## `ob_admin` 工具
`ob_admin` 是 OceanBase 内部开发和运维使用一个工具,用于处理一些疑难问题。这个工具不是标准产品,有很大使用风险。仅供对 OceanBase 原理非常熟悉的技术人员使用。
目前 `ob_admin` 可以用于两个场景:
+ 测试数据盘性能,生成 IO 配置文件。
+ 多数派故障时将单副本强制激活为主副本。
## `ob_error` 工具
`ob_error` 是随着OceanBase 一起开源的工具,代码路径:`https://github.com/oceanbase/oceanbase/tree/master/tools/ob_error`
主要用来查看 OceanBase 错误码含义,节省查文档时间。
```bash
[root@obce00 ~]# ./ob_error 4030
OceanBase:
OceanBase Error Code: OB_TENANT_OUT_OF_MEM(-4030)
Message: Over tenant memory limits
Cause: Internal Error
Solution: Contact OceanBase Support
[root@obce00 ~]# ./ob_error 4013
OceanBase:
OceanBase Error Code: OB_ALLOCATE_MEMORY_FAILED(-4013)
Message: No memory or reach tenant memory limit
Cause: Internal Error
Solution: Contact OceanBase Support
Oracle:
Oracle Error Code: ORA-04013
Message: number to CACHE must be less than one cycle
Related OceanBase Error Code:
OB_ERR_SEQ_CACHE_TOO_LARGE(-4328)
```
## `addr2line` 命令
Addr2line 工具(它是标准的 GNU Binutils 中的一部分)是一个可以将指令的地址和可执行映像转换成文件名、函数名和源代码行数的工具。
一般适用于 debug 版本或带有 symbol 信息的库。
命令使用方法:`addr2line -pCfe $observer $symbol_addr`
其中,`$observer` 是进程 `observer` 的目录,`$symbol_addr` 是进程运行日志中打印的十六进制字符串。
示例如下:
```bash
[admin@obce01 oceanbase-ce]$ grep ERROR log/observer.log -A1
[2021-10-08 15:35:14.343634] ERROR easy_client_dispatch (easy_client.c:30) [90761][8][YB42AC14F934-0005CDD26B1C3D3A] [lt=0] [dc=0] easy_io_dispatch is failure: easy not started
BACKTRACE:0x9124b6e 0x9060f71 0x9057dcf 0x905abd4 0x92ff0d5 0x9395b48 0x9395cf3 0x930a68e 0x4397e89 0x936beea 0x246f0a5 0x
```
输出结果如下,是函数的调用关系和参数值。这些反馈给 OceanBase 技术支持人员。
```bash
[admin@obce01 oceanbase-ce]$ addr2line -pCfe bin/observer 0x9124b6e 0x9060f71 0x9057dcf 0x905abd4 0x92ff0d5 0x9395b48 0x9395cf3 0x930a68e 0x4397e89 0x936beea 0x246f0a5 0x
oceanbase::common::lbt() at ??:?
oceanbase::common::ObLogger::check_error_log(oceanbase::common::ObPLogItem&) at ??:?
oceanbase::common::ObLogger::async_log_message_kv(char const*, int, oceanbase::common::ObLogger::LogLocation const&, unsigned long, char const*, long) at ??:?
oceanbase::common::ObLogger::log_message_va(char const*, int, char const*, int, char const*, unsigned long, char const*, __va_list_tag*) at ??:?
oceanbase::common::easy_log_format_adaptor(int, char const*, int, char const*, unsigned long, char const*, ...) at ??:?
easy_client_dispatch at ??:?
easy_client_send at ??:?
oceanbase::rpc::frame::ObReqTransport::send_session(easy_session_t*) const at ??:?
int oceanbase::rpc::frame::ObReqTransport::send<oceanbase::obrpc::ObRpcPacket>(oceanbase::rpc::frame::ObReqTransport::Request<oceanbase::obrpc::ObRpcPacket> const&, oceanbase::rpc::frame::ObReqTransport::Result<oceanbase::obrpc::ObRpcPacket>&) const at ??:?
oceanbase::obrpc::ObRpcProxy::send_request(oceanbase::rpc::frame::ObReqTransport::Request<oceanbase::obrpc::ObRpcPacket> const&, oceanbase::rpc::frame::ObReqTransport::Result<oceanbase::obrpc::ObRpcPacket>&) const at ??:?
int oceanbase::obrpc::ObRpcProxy::rpc_call<oceanbase::obrpc::ObFetchAliveServerArg, oceanbase::obrpc::ObFetchAliveServerResult>(oceanbase::obrpc::ObRpcPacketCode, oceanbase::obrpc::ObFetchAliveServerArg const&, oceanbase::obrpc::ObFetchAliveServerResult&, oceanbase::obrpc::Handle*, oceanbase::obrpc::ObRpcOpts const&) at ??:?
?? ??:0
[admin@obce01 oceanbase-ce]$
```
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册