大部分 OceanBase 的用户在碰到性能问题时容易束手无策。用户可能会熟悉自己的业务 SQL ,但不熟悉 OceanBase 。熟悉 OceanBase 的人员可能不熟悉业务 SQL。这之间有个“鸿沟”需要填补。本文就是为此而写──分享如何快速定位业务性能问题。
排除集群环境不稳定因素
OceanBase 集群有强一致和高可用能力,其次才是高性能。用户容易理解高性能,可能不理解强一致和高可用背后的要付出的成本。如集群节点间要保持时间同步误差和网络延时最小。集群节点之间的网络延时太大、时间同步延时太大会影响集群的性能甚至稳定性。
所以性能诊断首先要排除环境因素的干扰。
OceanBase 集群里数据通常是三副本,读写的是主副本,事务提交的时候使用Paxos协议,将数据变化日志同步到备副本并落盘。OceanBase 没有为了性能使用异步同步或者异步落盘技术,这些很可能会牺牲数据的一致性和安全性。事务的 COMMIT 一定会等待三个副本中至少两个成员接收到事务日志并落盘。所以事务日志盘(/data/log1
)的顺序写性能、集群多数派节点之间的网络延时会影响 COMMIT 的延时。这个 COMMIT 的延时后期是可以观察监控的。OceanBase 集群对网络的延时容忍程度还是很大的,最大能到 50ms。不过生产环境肯定不能有这么大的延时。尽管 OceanBase 可以工作,性能也会下降的很厉害。一般建议同机房 5ms以内,同城 10ms 以内。
OceanBase 集群节点间的时间误差需要控制在一定范围内。某个节点时间如果跟多数派节点时间误差过大,这个节点被选为主副本的可能性会很低。当节点的时间延时超出一定范围后,会引发分区的切主事件(主副本逃离该节点)。当这个异常持续时间超过集群的永久离线时间参数(server_permanent_offline_time
,默认值 3600s ),节点就会下线。由于这个默认值很大,通常问题发生的时候并不容易为人所察觉。
好消息是节点的时间同步并不完全受网络延时影响。即使网络延时有点大,节点间时间同步也是有可能做到1ms以内。生产环境建议用 NTP
或 chrony
做时间同步,这点不是问题。大部分客户的测试环境都没有可靠的 NTP 服务器,导致 OceanBase 在测试环境部署时经常碰到时间同步问题要解决。一个变通的方法是选定一台服务器临时当 NTP
服务(如选 OCP
服务器),其他 OB 节点都跟这台同步。如果感觉 NTP 或者 chrony
同步不可靠,最粗暴直接的方法就是直接在节点的 crontab
任务里直接配置每分钟使用 ntpdate
直接同步一次时间。实际 POC 发现这个总是有效的。如果集群节点机型不一致(CPU类型和主频不一致),这个时间误差很难缩小到1ms以内,能做到 10ms 以内也可以。
判断节点时间延时的命令是 clockdiff
。
[root@observer01 ~]# clockdiff observer05 ................................................... host=observer05 rtt=1(0)ms/1ms delta=-15ms/-16ms Sat Mar 20 11:09:42 2021 [root@observer01 ~]# ntpdate -u ntp8.cloud.aliyuncs.com 20 Mar 11:09:54 ntpdate[14167]: adjust time server 100.100.5.2 offset -0.010433 sec [root@observer01 ~]# ntpdate -u ntp8.cloud.aliyuncs.com 20 Mar 11:10:02 ntpdate[14974]: adjust time server 100.100.5.2 offset -0.004026 sec [root@observer01 ~]# clockdiff observer05 .................................................. host=observer05 rtt=1(0)ms/1ms delta=1ms/0ms Sat Mar 20 11:10:12 2021 [root@observer01 ~]#
观察租户整体性能
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 租户创建一个只读的账户,能查看系统视图。
grant select on oceanbase.* to dbamonitor identified by '123456';
dooba
脚本在 /home/admin/oceanbase/bin/
下,是个 python
脚本。我的做了一些修改,会稍有不同(最上面一行高亮的是近一定记录数的平均值)。
python dooba.py -hobserver00 -udbamonitor@sys#obdemo -P2883 -p123456
说明:
查看脚本内容就知道各个指标缩写的含义。其数据多取自于视图 gv$sysstat
。各项指标如下。
一级
分类
二级分类缩写全称含义GallerySQL 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)租户内存性能数据
⊿ACTIVEactive memstore used平均每秒增量内存变化量TOTALtotal memstore used增量内存累计总量PCT.total memstore used增量内存占比IOPS集群的IO性能数据
SES.active sessions当前活跃会话数IORio read count平均每秒读IO次数IOR-SZio read bytes平均每次读IO大小IOWio write count平均每秒写IO次数IOW-SZio write bytes平均每次写IO大小SQL Pageobserver每个节点的性能数据
Active Sessactive sessions租户在该节点当前活跃会话数CPUcpu usage租户在该节点的CPU利用率(相对值)Cache-BI Hitblock index cache hit租户在该节点数据块的索引块的命中率Cache-Blk Hitblock cache hit租户在该节点数据块的命中率Cache-Loc Hitlocation cache hit租户分区在该节点位置缓存命中率Cache-Row Hitrow cache hit租户分区在该节点行缓存命中率IO-R Cntio read count租户在该节点平均每秒读IO次数IO-R Sizeio read bytes租户在该节点平均每次读IO大小IO-W Cntio write count租户在该节点平均每次写IO次数IO-W Sizeio write bytes租户在该节点平均每次写IO大小SQL每个节点的SQL性能数据
SSCsql select count租户在该节点平均每秒查询次数SSRTsql select time租户在该节点平均每次查询耗时SICsql insert count租户在该节点平均每秒插入次数SIRTsql insert time租户在该节点平均每次插入耗时SUCsql update count租户在该节点平均每秒更新次数SURTsql update time租户在该节点平均每次更新耗时SDCsql delete count租户在该节点平均每秒删除次数SDRTsql delete time租户在该节点平均每次删除耗时SRCsql replace count租户在该节点平均每秒替换次数SRRTsql replace time租户在该节点平均每次替换耗时TCCtrans commit count租户在该节点平均每秒事务提交次数TCRTtrans commit time租户在该节点平均每次事务提交延时SLCsql local count租户在该节点平均每秒本地SQL次数SRCsql remote count租户在该节点平均每秒远程SQL次数
这些指标可以解答如下问题:
- 是不是每个节点都在提供读写服务?性能分别如何?
- 连接数达到多少了?每个节点是多少?
- IO吞吐量有多少?每个节点是多少?
- 数据库SQL耗时有多少?每个节点是多少?
- 每个节点的跨节点访问的SQL是多少?
- 每个节点的数据缓存命中率多少?
- 内存使用率如何?是否有转储?
从上面截图的监控数据,可以简单看出以下信息:
- 租户虽然是3节点,实际只有一个节点在提供读写服务。
- 业务SQL大头是查询和更新,其次是少量插入和删除。
- 查询平均延时 900us (微秒),插入平均延时 320us ,更新平均延时 230us , 删除平均延时 200us 。
- 租户事务平均延时 800us,事务比较小。
- 租户有少量的IO读,吞吐量在230MB.
- 节点远程SQL比例约占总SQL比例在 20% 左右
以上这些信息在 OCP 的租户性能监控里也可以看到。以前的文章有展示,官方文档也有很详细的说明,这里不再重复。个人喜欢命令行下监控分析。
观察租户 SQL 整体性能
租户的绝大部分SQL的执行计划都会进缓存(如果执行计划大小超过20M就不会进缓存)。SQL执行计划缓存视图是 gv$plan_cache_plan_stat
和 gv$plan_cache_plan_explain
。前者查看所有SQL的执行性能信息(次数、耗时、行数等等)。分析它可以根据执行耗时、执行次数、逻辑读、物理读排序获得相应的TOP SQL。
SELECT s.tenant_id, svr_ip,plan_Id,TYPE, d.database_name , query_sql, first_load_time, avg_exe_usec, slow_count,executions, slowest_exe_usec,round( rows_processed / executions ) rows_per, disk_reads ,buffer_gets FROM `gv$plan_cache_plan_stat` s LEFT JOIN `gv$database` d ON (s.tenant_id =d.tenant_id AND s.db_id =d.database_id ) WHERE s.tenant_id=1001 -- 改成具体的 tenant_id AND d.database_name in ('TPCC') AND s.executions > 10 -- and query_sql like '%emp%' ORDER BY avg_exe_usec desc, slow_count desc limit 10 ;

从这个结果里看到几点可疑 SQL 。下面就逐条分析SQL。
SQL性能调优
有3条查询SQL的平均耗时在 1ms 以上,执行次数也非常高。
- 第一条SQL平均返回行数是2708 行,那超过 1ms 是可以解释的。这个估计是业务相关。
- 第二条SQL看平均返回行数就意义不大,因为是
count(*)
操作。需要看看实际 SQL 和执行计划。
分析 SQL 实际执行计划和解析执行计划
SELECT * FROM `gv$plan_cache_plan_explain` WHERE tenant_id=1001 AND ip = '172.30.118.70' AND port=2882 AND plan_id=338606 ;
从运行中的执行计划看不容易看出这个执行计划是否合理。可以再对SQL进行解析看看执行计划是否一致。
进业务租户,用 explain
命令看看这个SQL的执行计划。
EXPLAIN extended_noaddr SELECT count(*) AS low_stock FROM ( SELECT s_w_id, s_i_id, s_quantity FROM bmsql_stock WHERE s_w_id = 409 AND s_quantity < 11 AND s_i_id IN ( SELECT ol_i_id FROM bmsql_district JOIN bmsql_order_line ON ol_w_id = d_w_id AND ol_d_id = d_id AND ol_o_id >= d_next_o_id - 20 AND ol_o_id < d_next_o_id WHERE d_w_id = 409 AND d_id = 3 ) ) \G *************************** 1. row *************************** Query Plan: =========================================================== |ID|OPERATOR |NAME |EST. ROWS|COST | ----------------------------------------------------------- |0 |SCALAR GROUP BY | |1 |335053| |1 | NESTED-LOOP JOIN | |2078 |334656| |2 | SUBPLAN SCAN |VIEW1 |8677 |19956 | |3 | HASH DISTINCT | |8677 |18758 | |4 | NESTED-LOOP JOIN| |8949 |9610 | |5 | TABLE GET |BMSQL_DISTRICT |1 |53 | |6 | TABLE SCAN |BMSQL_ORDER_LINE|26847 |11293 | |7 | TABLE GET |BMSQL_STOCK |1 |37 | =========================================================== Outputs & filters: ------------------------------------- 0 - output([T_FUN_COUNT(*)]), filter(nil), group(nil), agg_func([T_FUN_COUNT(*)]) 1 - output([1]), filter(nil), conds(nil), nl_params_([VIEW1.BMSQL_ORDER_LINE.OL_I_ID]), inner_get=false, self_join=false, batch_join=true 2 - output([VIEW1.BMSQL_ORDER_LINE.OL_I_ID]), filter(nil), access([VIEW1.BMSQL_ORDER_LINE.OL_I_ID]) 3 - output([BMSQL_ORDER_LINE.OL_I_ID]), filter(nil), distinct([BMSQL_ORDER_LINE.OL_I_ID]) 4 - output([BMSQL_ORDER_LINE.OL_I_ID]), filter(nil), conds(nil), nl_params_([BMSQL_DISTRICT.D_NEXT_O_ID - 20], [BMSQL_DISTRICT.D_NEXT_O_ID]), inner_get=false, self_join=false, batch_join=true 5 - output([BMSQL_DISTRICT.D_NEXT_O_ID], [BMSQL_DISTRICT.D_NEXT_O_ID - 20]), filter([BMSQL_DISTRICT.D_NEXT_O_ID > BMSQL_DISTRICT.D_NEXT_O_ID - 20]), access([BMSQL_DISTRICT.D_NEXT_O_ID]), partitions(p0), is_index_back=false, filter_before_indexback[false], range_key([BMSQL_DISTRICT.D_W_ID], [BMSQL_DISTRICT.D_ID]), range[409,3 ; 409,3], range_cond([BMSQL_DISTRICT.D_W_ID = 409], [BMSQL_DISTRICT.D_ID = 3]) 6 - output([BMSQL_ORDER_LINE.OL_I_ID]), filter(nil), access([BMSQL_ORDER_LINE.OL_I_ID]), partitions(p0), is_index_back=false, range_key([BMSQL_ORDER_LINE.OL_W_ID], [BMSQL_ORDER_LINE.OL_D_ID], [BMSQL_ORDER_LINE.OL_O_ID], [BMSQL_ORDER_LINE.OL_NUMBER]), range(MIN ; MAX), range_cond([BMSQL_ORDER_LINE.OL_W_ID = 409], [BMSQL_ORDER_LINE.OL_D_ID = 3], [BMSQL_ORDER_LINE.OL_O_ID >= ?], [BMSQL_ORDER_LINE.OL_O_ID < ?]) 7 - output([1]), filter([BMSQL_STOCK.S_QUANTITY < 11]), access([BMSQL_STOCK.S_QUANTITY]), partitions(p0), is_index_back=false, filter_before_indexback[false], range_key([BMSQL_STOCK.S_W_ID], [BMSQL_STOCK.S_I_ID]), range(MIN ; MAX), range_cond([BMSQL_STOCK.S_W_ID = 409], [BMSQL_STOCK.S_I_ID = ?]) 1 row in set (0.03 sec)
SQL 执行计划是否合理,没有简单明确的判断方法。这个需要了解常见的 SQL 用法的执行计划特点,以及一些实践经验。可以看我以前关于执行计划的文章。
解析的执行计划跟实际执行计划是一致的。本文示例是 TPCC 场景,所有 SQL 都非常优化,执行计划没有问题。
如果是传统应用,还是容易看出一些明显的问题。如缺乏索引、或者 SQL 写法不是最优。
分析 SQL执行位置和数据副本分布
- 第三条SQL的Type为 2 是远程SQL。根据这个 SQLID 再找找所有类似的SQL。
SELECT s.tenant_id, svr_ip,plan_Id,TYPE, d.database_name , sql_id, query_sql, first_load_time, avg_exe_usec, slow_count,executions, slowest_exe_usec ,round( rows_processed / executions,2 ) avg_rows, round(disk_reads/executions ) avg_disk_reads,round(buffer_gets /executions ) avg_buffer_gets FROM `gv$plan_cache_plan_stat` s LEFT JOIN `gv$database` d ON (s.tenant_id =d.tenant_id AND s.db_id =d.database_id ) WHERE s.tenant_id=1001 -- 改成具体的 tenant_id AND d.database_name in ('TPCC') AND s.executions > 10 -- and query_sql like '%emp%' and s.sql_id = 'A460265EC2F0763A15DD27CE9E4E2200' ORDER BY avg_exe_usec desc, slow_count desc limit 10 ;
从结果看同样的SQL在2个节点上都有请求发生。其中一个节点的SQL类型是远程SQL(TYPE=2),另外一个节点是本地SQL(TYPE=1)。
再查看各个表的主副本位置分布,SQL 如下.
SELECT t.table_name, tg.tablegroup_name , t.part_num , t2.partition_id, t2.ZONE, t2.svr_ip, t2.role -- , a.primary_zone , IF(t.locality = '' OR t.locality IS NULL, a.locality, t.locality) AS locality , t2.row_count, round(data_size/1024/1024,0) data_size_MB 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 (1001 ) AND t.table_type IN (3) AND d.database_name = 'TPCC' ORDER BY t.tenant_id, tg.tablegroup_name, d.database_name, t.table_name, t2.partition_id ; +-------------------+-----------------+----------+--------------+-------+----------------+------+-----------+--------------+ | table_name | tablegroup_name | part_num | partition_id | ZONE | svr_ip | role | row_count | data_size_MB | +-------------------+-----------------+----------+--------------+-------+----------------+------+-----------+--------------+ | BMSQL_CONFIG | NULL | 1 | 0 | zone2 | 172.30.118.70 | 1 | 4 | 2 | | BMSQL_ITEM | NULL | 1 | 0 | zone3 | 172.23.152.229 | 1 | 100000 | 10 | | BMSQL_CUSTOMER | TPCC_GROUP2 | 1 | 0 | zone2 | 172.30.118.70 | 1 | 30000000 | 14138 | | BMSQL_DISTRICT | TPCC_GROUP2 | 1 | 0 | zone2 | 172.30.118.70 | 1 | 10000 | 4 | | BMSQL_HISTORY | TPCC_GROUP2 | 1 | 0 | zone2 | 172.30.118.70 | 1 | 32103424 | 748 | | BMSQL_NEW_ORDER | TPCC_GROUP2 | 1 | 0 | zone2 | 172.30.118.70 | 1 | 9218558 | 6 | | BMSQL_OORDER | TPCC_GROUP2 | 1 | 0 | zone2 | 172.30.118.70 | 1 | 32181498 | 342 | | BMSQL_OORDER2 | TPCC_GROUP2 | 1 | 0 | zone2 | 172.30.118.70 | 1 | 1000000 | 12 | | BMSQL_OORDER3 | TPCC_GROUP2 | 1 | 0 | zone2 | 172.30.118.70 | 1 | 1000000 | 6 | | BMSQL_ORDER_LINE | TPCC_GROUP2 | 1 | 0 | zone2 | 172.30.118.70 | 1 | 321795064 | 11116 | | BMSQL_ORDER_LINE2 | TPCC_GROUP2 | 1 | 0 | zone2 | 172.30.118.70 | 1 | 300149409 | 8668 | | BMSQL_STOCK | TPCC_GROUP2 | 1 | 0 | zone2 | 172.30.118.70 | 1 | 100000000 | 23820 | | BMSQL_WAREHOUSE | TPCC_GROUP2 | 1 | 0 | zone2 | 172.30.118.70 | 1 | 1000 | 2 | +-------------------+-----------------+----------+--------------+-------+----------------+------+-----------+--------------+ 13 rows in set (0.04 sec)
从分布中看,绝大部分表都在节点 172.30.118.70
上,因为它们在同一个表分组 TPCC_GROUP2
中。表 BMSQL_ITEM
表在节点 172.23.152.229
上。再看表 BMSQL_ITEM
也不是复制表。
SHOW CREATE TABLE bmsql_item; CREATE TABLE "BMSQL_ITEM" ( "I_ID" NUMBER(38) NOT NULL, "I_NAME" VARCHAR2(24), "I_PRICE" NUMBER(5,2), "I_DATA" VARCHAR2(50), "I_IM_ID" NUMBER(38), CONSTRAINT "BMSQL_ITEM_OBPK_1615598299895820" PRIMARY KEY ("I_ID"), CONSTRAINT "BMSQL_ITEM_UK" UNIQUE ("I_NAME") ) COMPRESS FOR ARCHIVE REPLICA_NUM = 3 BLOCK_SIZE = 16384 USE_BLOOM_FILTER = FALSE TABLET_SIZE = 134217728 PCTFREE = 10
针对表BMSQL_ITEM
的那些 SQL类型是 1 的 SQL 都是正常的,平均延时在 180us 。主键访问(TABLE GET
),加上频繁访问命中行缓存,这个延时水平是正常的。而远程访问的那个是不正常,增加了网络上的传输时间。
这点可以通过它的执行计划验证。
SELECT PLAN_DEPTH ,PLAN_LINE_ID ,OPERATOR ,NAME,ROWS,COST,PROPERTY FROM `gv$plan_cache_plan_explain` WHERE tenant_id=1001 AND ip = '172.30.118.70' AND port=2882 AND plan_id=340985 order by PLAN_LINE_ID ; +------------+--------------+----------------------+------------+------+------+----------------------------------------------------------------------------------------------------------------------------+ | PLAN_DEPTH | PLAN_LINE_ID | OPERATOR | NAME | ROWS | COST | PROPERTY | +------------+--------------+----------------------+------------+------+------+----------------------------------------------------------------------------------------------------------------------------+ | 0 | 0 | PHY_DIRECT_RECEIVE | NULL | 1 | 52 | NULL | | 1 | 1 | PHY_DIRECT_TRANSMIT | NULL | 1 | 52 | NULL | | 2 | 2 | PHY_TABLE_SCAN | BMSQL_ITEM | 1 | 52 | table_rows:100000, physical_range_rows:1, logical_range_rows:1, index_back_rows:0, output_rows:1, est_method:local_storage | +------------+--------------+----------------------+------------+------+------+----------------------------------------------------------------------------------------------------------------------------+ 3 rows in set (0.01 sec)
远程SQL问题在于跨节点访问,其自身执行计划往往没问题。所以后面继续分析为什么这个 SQL 是远程的。
分析 SQL 所在会话的事务的全部 SQL
要找到 SQL 是远程访问的原因就要找出 SQL 的会话信息。这里就需要借助 OceanBase 的 SQL审计视图 (gv$sql_audit
)。
访问 sys 租户,根据 SQLID 查询实际执行记录。
SELECT /*+ read_consistency(weak) ob_querytimeout(100000000) */ substr(usec_to_time(request_time),1,19) request_time_, s.svr_ip, s.sid, TRANSACTION_HASH ,s.query_sql, s.affected_rows, s.return_rows, s.ret_code, s.event, s.elapsed_time, s.queue_time, s.execute_time, plan_type FROM gv$sql_audit s WHERE s.tenant_id=1001 and user_name='TPCC' AND request_time > ( time_to_usec(now())-36000000000*1) and sql_id='A460265EC2F0763A15DD27CE9E4E2200' and PLAN_TYPE = 2 ORDER BY REQUEST_ID DESC LIMIT 100; +---------------------+---------------+------------+----------------------+---------------------------------------------------------------------------+---------------+-------------+----------+----------+--------------+------------+--------------+-----------+ | request_time_ | svr_ip | sid | TRANSACTION_HASH | query_sql | affected_rows | return_rows | ret_code | event | elapsed_time | queue_time | execute_time | plan_type | +---------------------+---------------+------------+----------------------+---------------------------------------------------------------------------+---------------+-------------+----------+----------+--------------+------------+--------------+-----------+ | 2021-03-20 17:58:11 | 172.30.118.70 | 3222006301 | 17714189861425713202 | SELECT i_price, i_name, i_data FROM bmsql_item WHERE i_id = 75723 | 0 | 1 | 0 | sync rpc | 1981 | 26 | 1903 | 2 | | 2021-03-20 17:58:11 | 172.30.118.70 | 3222006303 | 15077805753115918393 | SELECT i_price, i_name, i_data FROM bmsql_item WHERE i_id = 79859 | 0 | 1 | 0 | sync rpc | 1715 | 23 | 1625 | 2 | | 2021-03-20 17:58:11 | 172.30.118.70 | 3222006308 | 2930088302319928265 | SELECT i_price, i_name, i_data FROM bmsql_item WHERE i_id = 65475 | 0 | 1 | 0 | sync rpc | 1957 | 16 | 1879 | 2 | | 2021-03-20 17:58:11 | 172.30.118.70 | 3222006307 | 8484084185755511457 | SELECT i_price, i_name, i_data FROM bmsql_item WHERE i_id = 83934 | 0 | 1 | 0 | sync rpc | 1709 | 16 | 1636 | 2 | | 2021-03-20 17:58:11 | 172.30.118.70 | 3222006305 | 10846175025601860115 | SELECT i_price, i_name, i_data FROM bmsql_item WHERE i_id = 6083 | 0 | 1 | 0 | sync rpc | 1694 | 24 | 1613 | 2 | | 2021-03-20 17:58:11 | 172.30.118.70 | 3222005734 | 1898163990547624876 | SELECT i_price, i_name, i_data FROM bmsql_item WHERE i_id = 35331 | 0 | 1 | 0 | sync rpc | 1901 | 11 | 1801 | 2 | | 2021-03-20 17:58:11 | 172.30.118.70 | 3222006304 | 14242749847107990387 | SELECT i_price, i_name, i_data FROM bmsql_item WHERE i_id = 90094 | 0 | 1 | 0 | sync rpc | 1715 | 17 | 1626 | 2 | | 2021-03-20 17:58:11 | 172.30.118.70 | 3222006301 | 17714189861425713202 | SELECT i_price, i_name, i_data FROM bmsql_item WHERE i_id = 73073 | 0 | 1 | 0 | sync rpc | 1621 | 17 | 1553 | 2 | | 2021-03-20 17:58:11 | 172.30.118.70 | 3222006307 | 8484084185755511457 | SELECT i_price, i_name, i_data FROM bmsql_item WHERE i_id = 82931 | 0 | 1 | 0 | sync rpc | 1689 | 19 | 1609 | 2 | | 2021-03-20 17:58:11 | 172.30.118.70 | 3222006308 | 2930088302319928265 | SELECT i_price, i_name, i_data FROM bmsql_item WHERE i_id = 64992 | 0 | 1 | 0 | sync rpc | 1989 | 37 | 1885 | 2 | <.....> | 2021-03-20 17:58:11 | 172.30.118.70 | 3222004245 | 10292058482827229406 | SELECT i_price, i_name, i_data FROM bmsql_item WHERE i_id = 1235 | 0 | 1 | 0 | sync rpc | 1657 | 16 | 1562 | 2 | | 2021-03-20 17:58:11 | 172.30.118.70 | 3222000235 | 6630926809476864374 | SELECT i_price, i_name, i_data FROM bmsql_item WHERE i_id = 94062 | 0 | 1 | 0 | sync rpc | 1753 | 18 | 1672 | 2 | | 2021-03-20 17:58:11 | 172.30.118.70 | 3222006307 | 6638448895855477417 | SELECT i_price, i_name, i_data FROM bmsql_item WHERE i_id = 82801 | 0 | 1 | 0 | sync rpc | 2034 | 17 | 1962 | 2 | +---------------------+---------------+------------+----------------------+---------------------------------------------------------------------------+---------------+-------------+----------+----------+--------------+------------+--------------+-----------+ 100 rows in set (0.60 sec)
抽取上面最后一笔记录的 transaction_hash
值,这是事务的标识。然后根据事务标识查看该事务的所有SQL记录。(注意:227以前的版本同一会话开启新事务的时候,事务标识不变化,给定位带来一点点麻烦。此后修复了。)
SELECT /*+ ob_querytimeout(100000000) */substr(usec_to_time(request_time),1,19) request_time_, s.svr_ip, s.query_sql, plan_type,TRANSACTION_HASH FROM gv$sql_audit s WHERE s.tenant_id=1001 and user_name='TPCC' -- and sid = 3222006308 and TRANSACTION_HASH ='6638448895855477417' ORDER BY REQUEST_ID LIMIT 100;
这一步操作的目的就是要获取到这个业务事务的全部 SQL 。SQL 审计视图的好处就是如果业务研发讲不清楚业务事务的逻辑,DBA 还是可以从数据库层面获取到相关的信息。(注意:OB 2.2.7.x 里 SQL审计视图能保存的数据受租户变量 ob_sql_audit_percentage 控制,同时内部还有个固定上限(1GB)。在大内存高频访问租户下,这个显得有点小,所以可能存在5分钟之前的SQL运行记录就找不到了。不过到OCP里还是可以找到。)
上面这个业务事务的逻辑大概是下单逻辑。下单的时候锁定相关表的记录,查询实际库存和商品价格和信息。由于查询商品表 bmsql_item
的 SQL 不是事务里的第一条 SQL,所以事务的SQL都被发送到第一个 SQL 访问的表 BMSQL_DISTRICT
主副本所在的节点 172.30.118.70
上。后期访问节点 172.23.152.229
的 SQL 自然就是远程 SQL。
复制表减少 SQL 远程节点访问
接下来就是怎么办。由于 表分组 TPCC_GROUP2 中的表实际上应该是分区表(这里是单表,不影响结论),所以 BMSQL_ITEM
表是不能跟它们在一个表分组里。此时,OceanBase的解决方案就是把表 BMSQL_ITEM
表变为复制表,这样它的分区在所有节点的副本都是全同步(跟主副本严格一致),备副本数据跟主副本也严格一致,此时业务事务可以就近访问该节点上的本地备副本。
表变为复制表的语法比之前简单了,只要一句命令,立即生效。(当然,严格来说如果租户规格是 2-2-2
的时候,是会触发一个副本复制的过程。这里测试环境是 1-1-1
,节点上本来就有备副本,所以是立即生效)。
ALTER TABLE bmsql_item duplicate_scope='cluster';
此时再次查询 SQL 的执行计划,可以看到都是本地访问 SQL 了。

此时,再回头查看租户 SQL 整体性能时,在 TOP SQL里已经看不到上面这条 SQL 了。
总结
OceanBase 的性能诊断首先是判断租户当前的读写压力,对性能有个初步的判断。识别出明显的问题。如 SQL 延时异常,或者内存写异常(后面介绍)。
对于 SQL延时异常就从租户全局的 SQL 统计信息找出慢 SQL,从执行计划、执行类型、数据量多个角度去分析优化。
如果这些都没有明显的问题,那就继续深入分析每个业务 SQL 的调用情况,看看是否有不合理的地方。如不合理的远程调用、不合理的调用次数等。
最后一步是非常耗时的,对于传统核心从 ORACLE 迁移到分布式数据库 OceanBase 上这一步是非常有必要的。OceanBase 的性能诊断方法可以把以前在 ORACLE 上应用不合理的设计问题暴露出来,也可以把那些不适合分布式数据库的设计问题暴露出来。