本文摘要
本文剖析并复现了 OceanBase 频繁更新数据后读性能降落景象的起因,并给出了性能改善倡议。
背景
测试在做 OceanBase 纯读性能压测的时候,发现对数据做过更新操作后,读性能会有较为显著的降落。具体复现步骤如下。
复现形式
环境准备
部署 OB
应用 OBD 部署单节点 OB。
版本 | IP | |
---|---|---|
OceanBase | 4.0.0.0 CE | 10.186.16.122 |
参数均为默认值,其中内存以及转储合并等和本次试验相干的重要参数值具体如下:
参数名 | 含意 | 默认值 |
---|---|---|
memstore_limit_percentage |
设置租户应用 memstore 的内存占其总可用内存的百分比。 | 50 |
freeze_trigger_percentage |
触发全局解冻的租户应用内存阈值。 | 20 |
major_compact_trigger |
设置多少次小合并触发一次全局合并。 | 0 |
minor_compact_trigger |
管制分层转储触发向下一层下压的阈值。当该层的 Mini SSTable 总数达到设定的阈值时,所有 SSTable 都会被下压到下一层,组成新的 Minor SSTable。 | 2 |
创立 sysbench 租户
create resource unit sysbench_unit max_cpu 26, memory_size '21g';
create resource pool sysbench_pool unit = 'sysbench_unit', unit_num = 1, zone_list=('zone1');
create tenant sysbench_tenant resource_pool_list=('sysbench_pool'), charset=utf8mb4, zone_list=('zone1'), primary_zone=RANDOM set variables ob_compatibility_mode='mysql', ob_tcp_invited_nodes='%';
数据准备
创立 30 张 100 万行数据的表。
sysbench ./oltp_read_only.lua --mysql-host=10.186.16.122 --mysql-port=12881 --mysql-db=sysbenchdb --mysql-user="sysbench@sysbench_tenant" --mysql-password=sysbench --tables=30 --table_size=1000000 --threads=256 --time=60 --report-interval=10 --db-driver=mysql --db-ps-mode=disable --skip-trx=on --mysql-ignore-errors=6002,6004,4012,2013,4016,1062 prepare
环境调优
手动触发大合并
ALTER SYSTEM MAJOR FREEZE TENANT=ALL;
# 查看合并进度
SELECT * FROM oceanbase.CDB_OB_ZONE_MAJOR_COMPACTION\G
数据更新前的纯读 QPS
sysbench ./oltp_read_only.lua --mysql-host=10.186.16.122 --mysql-port=12881 --mysql-db=sysbenchdb --mysql-user="sysbench@sysbench_tenant" --mysql-password=sysbench --tables=30 --table_size=1000000 --threads=256 --time=60 --report-interval=10 --db-driver=mysql --db-ps-mode=disable --skip-trx=on --mysql-ignore-errors=6002,6004,4012,2013,4016,1062 run
read_only 的 QPS 体现如下:
第一次 | 第二次 | 第三次 | 第四次 | 第五次 |
---|---|---|---|---|
344727.36 | 325128.58 | 353141.76 | 330873.54 | 340936.48 |
数据更新后的纯读 QPS
执行三次 write_only 脚本,其中包含了 update/delete/insert 操作,命令如下:
sysbench ./oltp_write_only.lua --mysql-host=10.186.16.122 --mysql-port=12881 --mysql-db=sysbenchdb --mysql-user="sysbench@sysbench_tenant" --mysql-password=sysbench --tables=30 --table_size=1000000 --threads=256 --time=60 --report-interval=10 --db-driver=mysql --db-ps-mode=disable --skip-trx=on --mysql-ignore-errors=6002,6004,4012,2013,4016,1062 run
再执行 read_only 的 QPS 体现如下:
第一次 | 第二次 | 第三次 | 第四次 | 第五次 |
---|---|---|---|---|
170718.07 | 175209.29 | 173451.38 | 169685.38 | 166640.62 |
数据做一次大合并后纯读 QPS
手动触发大合并,执行命令:
ALTER SYSTEM MAJOR FREEZE TENANT=ALL;
# 查看合并进度
SELECT * FROM oceanbase.CDB_OB_ZONE_MAJOR_COMPACTION\G
再次执行 read_only,QPS 体现如下,能够看到读的 QPS 复原至初始程度。
第一次 | 第二次 | 第三次 | 第四次 | 第五次 |
---|---|---|---|---|
325864.95 | 354866.82 | 331337.10 | 326113.78 | 340183.18 |
景象总结
比照数据更新前后的纯读 QPS,发现在做过批量更新操作后,读性能降落 17W 左右,做一次大合并后性能又能够晋升回来。
排查过程
手法 1:火焰图
火焰图差别比照
收集数据更新前后进行压测时的火焰图,比照的不同点集中在上面标注的蓝框中。
放大到办法里进一步查看,发现低 QPS 火焰图顶部多了几个 ‘ 平台 ’,指向同一个办法 oceanbase::blocksstable::ObMultiVersionMicroBlockRowScanner::inner_get_next_row
。
查看源码
火焰图中指向的办法,会进一步调用 ObMultiVersionMicroBlockRowScanner::inner_get_next_row_impl
。后者的次要作用是借嵌套 while 循环进行多版本数据行的读取,并将符合条件的行合并交融(do_compact
中会调用 fuse_row
),返回一个合并后的行(ret_row
)作为最终后果,源码如下:
int ObMultiVersionMicroBlockRowScanner::inner_get_next_row_impl(const ObDatumRow *&ret_row)
{
int ret = OB_SUCCESS;
// TRUE:For the multi-version row of the current rowkey, when there is no row to be read in this micro_block
bool final_result = false;
// TRUE:For reverse scanning, if this micro_block has the last row of the previous rowkey
bool found_first_row = false;
bool have_uncommited_row = false;
const ObDatumRow *multi_version_row = NULL;
ret_row = NULL;
while (OB_SUCC(ret)) {
final_result = false;
found_first_row = false;
// 定位到以后要读取的地位
if (OB_FAIL(locate_cursor_to_read(found_first_row))) {if (OB_UNLIKELY(OB_ITER_END != ret)) {LOG_WARN("failed to locate cursor to read", K(ret), K_(macro_id));
}
}
LOG_DEBUG("locate cursor to read", K(ret), K(finish_scanning_cur_rowkey_),
K(found_first_row), K(current_), K(reserved_pos_), K(last_), K_(macro_id));
while (OB_SUCC(ret)) {
multi_version_row = NULL;
bool version_fit = false;
// 读取下一行
if (read_row_direct_flag_) {if (OB_FAIL(inner_get_next_row_directly(multi_version_row, version_fit, final_result))) {if (OB_UNLIKELY(OB_ITER_END != ret)) {LOG_WARN("failed to inner get next row directly", K(ret), K_(macro_id));
}
}
} else if (OB_FAIL(inner_inner_get_next_row(multi_version_row, version_fit, final_result, have_uncommited_row))) {if (OB_UNLIKELY(OB_ITER_END != ret)) {LOG_WARN("failed to inner get next row", K(ret), K_(macro_id));
}
}
if (OB_SUCC(ret)) {
// 如果读取到的行版本不匹配,则不进行任何操作
if (!version_fit) {// do nothing}
// 如果匹配,则进行合并交融
else if (OB_FAIL(do_compact(multi_version_row, row_, final_result))) {LOG_WARN("failed to do compact", K(ret));
} else {
// 记录物理读取次数
if (OB_NOT_NULL(context_)) {++context_->table_store_stat_.physical_read_cnt_;}
if (have_uncommited_row) {row_.set_have_uncommited_row();
}
}
}
LOG_DEBUG("do compact", K(ret), K(current_), K(version_fit), K(final_result), K(finish_scanning_cur_rowkey_),
"cur_row", is_row_empty(row_) ? "empty" : to_cstring(row_),
"multi_version_row", to_cstring(multi_version_row), K_(macro_id));
// 该行多版本如果在以后微块曾经全副读取结束,就将以后微块的行缓存并跳出内层循环
if ((OB_SUCC(ret) && final_result) || OB_ITER_END == ret) {
ret = OB_SUCCESS;
if (OB_FAIL(cache_cur_micro_row(found_first_row, final_result))) {LOG_WARN("failed to cache cur micro row", K(ret), K_(macro_id));
}
LOG_DEBUG("cache cur micro row", K(ret), K(finish_scanning_cur_rowkey_),
"cur_row", is_row_empty(row_) ? "empty" : to_cstring(row_),
"prev_row", is_row_empty(prev_micro_row_) ? "empty" : to_cstring(prev_micro_row_),
K_(macro_id));
break;
}
}
// 完结扫描,将最终后果放到 ret_row,跳出外层循环。if (OB_SUCC(ret) && finish_scanning_cur_rowkey_) {if (!is_row_empty(prev_micro_row_)) {ret_row = &prev_micro_row_;} else if (!is_row_empty(row_)) {ret_row = &row_;}
// If row is NULL, means no multi_version row of current rowkey in [base_version, snapshot_version) range
if (NULL != ret_row) {(const_cast<ObDatumRow *>(ret_row))->mvcc_row_flag_.set_uncommitted_row(false);
const_cast<ObDatumRow *>(ret_row)->trans_id_.reset();
break;
}
}
}
if (OB_NOT_NULL(ret_row)) {if (!ret_row->is_valid()) {LOG_ERROR("row is invalid", KPC(ret_row));
} else {LOG_DEBUG("row is valid", KPC(ret_row));
if (OB_NOT_NULL(context_)) {++context_->table_store_stat_.logical_read_cnt_;}
}
}
return ret;
}
剖析
从火焰图来看,QPS 升高,耗费集中在对多版本数据行的解决上,也就是一行数据的频繁更新操作对应到存储引擎里是多条记录,查问的 SQL 在外部解决时,理论可能须要扫描的行数量可能远大于自身的行数。
手法 2:剖析 SQL 执行过程
通过 GV$OB_SQL_AUDIT
审计表,能够查看每次申请客户端起源、执行服务器信息、执行状态信息、期待事件以及执行各阶段耗时等。
GV$OB_SQL_AUDIT
用法参考:https://www.oceanbase.com/docs/common-oceanbase-database-cn-1…
比照性能降落前后雷同 SQL 的执行信息
因为本文场景没有理论的慢 sql,这里抉择在 GV$OB_SQL_AUDIT
中,依据 SQL 执行耗时 (elapsed_time
) 筛出 TOP10,取一条进行排查:SELECT c FROM sbtest% WHERE id BETWEEN ? AND ? ORDER BY c
。
执行更新操作前(也就是高 QPS 时):
MySQL [oceanbase]> select TRACE_ID,TENANT_NAME,USER_NAME,DB_NAME,QUERY_SQL,RETURN_ROWS,IS_HIT_PLAN,ELAPSED_TIME,EXECUTE_TIME,MEMSTORE_READ_ROW_COUNT,SSSTORE_READ_ROW_COUNT,DATA_BLOCK_READ_CNT,DATA_BLOCK_CACHE_HIT,INDEX_BLOCK_READ_CNT,INDEX_BLOCK_CACHE_HIT from GV$OB_SQL_AUDIT where TRACE_ID='YB42AC110005-0005F9ADDCDF0240-0-0' \G
*************************** 1. row ***************************
TRACE_ID: YB42AC110005-0005F9ADDCDF0240-0-0
TENANT_NAME: sysbench_tenant
USER_NAME: sysbench
DB_NAME: sysbenchdb
QUERY_SQL: SELECT c FROM sbtest20 WHERE id BETWEEN 498915 AND 499014 ORDER BY c
PLAN_ID: 10776
RETURN_ROWS: 100
IS_HIT_PLAN: 1
ELAPSED_TIME: 16037
EXECUTE_TIME: 15764
MEMSTORE_READ_ROW_COUNT: 0
SSSTORE_READ_ROW_COUNT: 100
DATA_BLOCK_READ_CNT: 2
DATA_BLOCK_CACHE_HIT: 2
INDEX_BLOCK_READ_CNT: 2
INDEX_BLOCK_CACHE_HIT: 1
1 row in set (0.255 sec)
执行更新操作后(低 QPS 值时):
MySQL [oceanbase]> select TRACE_ID,TENANT_NAME,USER_NAME,DB_NAME,QUERY_SQL,RETURN_ROWS,IS_HIT_PLAN,ELAPSED_TIME,EXECUTE_TIME,MEMSTORE_READ_ROW_COUNT,SSSTORE_READ_ROW_COUNT,DATA_BLOCK_READ_CNT,DATA_BLOCK_CACHE_HIT,INDEX_BLOCK_READ_CNT,INDEX_BLOCK_CACHE_HIT from GV$OB_SQL_AUDIT where TRACE_ID='YB42AC110005-0005F9ADE2E77EC0-0-0' \G
*************************** 1. row ***************************
TRACE_ID: YB42AC110005-0005F9ADE2E77EC0-0-0
TENANT_NAME: sysbench_tenant
USER_NAME: sysbench
DB_NAME: sysbenchdb
QUERY_SQL: SELECT c FROM sbtest7 WHERE id BETWEEN 501338 AND 501437 ORDER BY c
PLAN_ID: 10848
RETURN_ROWS: 100
IS_HIT_PLAN: 1
ELAPSED_TIME: 36960
EXECUTE_TIME: 36828
MEMSTORE_READ_ROW_COUNT: 33
SSSTORE_READ_ROW_COUNT: 200
DATA_BLOCK_READ_CNT: 63
DATA_BLOCK_CACHE_HIT: 63
INDEX_BLOCK_READ_CNT: 6
INDEX_BLOCK_CACHE_HIT: 4
1 row in set (0.351 sec)
剖析
下面查问结果显示字段 IS_HIT_PLAN
的值为 1,阐明 SQL 命中了执行打算缓存,没有走物理生成执行打算的门路。咱们依据 PLAN_ID
进一步到 V$OB_PLAN_CACHE_PLAN_EXPLAIN
查看物理执行打算(数据更新前后执行打算雷同,上面仅列出数据更新后的执行打算)。
注:拜访
V$OB_PLAN_CACHE_PLAN_EXPLAIN
,必须给定tenant_id
和plan_id
的值,否则零碎将返回空集。
MySQL [oceanbase]> SELECT * FROM V$OB_PLAN_CACHE_PLAN_EXPLAIN WHERE tenant_id = 1002 AND plan_id=10848 \G
*************************** 1. row ***************************
TENANT_ID: 1002
SVR_IP: 172.17.0.5
SVR_PORT: 2882
PLAN_ID: 10848
PLAN_DEPTH: 0
PLAN_LINE_ID: 0
OPERATOR: PHY_SORT
NAME: NULL
ROWS: 100
COST: 51
PROPERTY: NULL
*************************** 2. row ***************************
TENANT_ID: 1002
SVR_IP: 172.17.0.5
SVR_PORT: 2882
PLAN_ID: 10848
PLAN_DEPTH: 1
PLAN_LINE_ID: 1
OPERATOR: PHY_TABLE_SCAN
NAME: sbtest20
ROWS: 100
COST: 6
PROPERTY: table_rows:1000000, physical_range_rows:100, logical_range_rows:100, index_back_rows:0, output_rows:100, est_method:local_storage, avaiable_index_name[sbtest20], pruned_index_name[k_20], estimation info[table_id:500294, (table_type:12, version:-1--1--1, logical_rc:100, physical_rc:100)]
2 rows in set (0.001 sec)
从 V$OB_PLAN_CACHE_PLAN_EXPLAIN
查问后果看,执行打算波及两个算子:范畴扫描算子 PHY_TABLE_SCAN
和排序算子 PHY_SORT
。依据范畴扫描算子 PHY_TABLE_SCAN
中的 PROPERTY
信息,能够看出该算子应用的是主键索引,不波及回表,行数为 100。综上来看,该 SQL 的执行打算正确且已是最优,没有调整的空间。
再比照两次性能压测下 GV$OB_SQL_AUDIT
表,当性能降落后,MEMSTORE_READ_ROW_COUNT
(MemStore 中读的行数)和 SSSTORE_READ_ROW_COUNT
(SSSTORE 中读的行数)加起来读的总行数为 233,是理论返回行数的两倍多。合乎下面察看到的火焰图上的问题,即理论读的行数大于自身的行数,该处耗费了零碎更多的资源,导致性能降落。
论断
OceanBase 数据库的存储引擎基于 LSM-Tree 架构,以基线加增量的形式进行存储,当在一个表中进行大量的插入、删除、更新操作后,查问每一行数据的时候须要依据版本从新到旧遍历所有的 MemTable 以及 SSTable,将每个 Table 中对应主键的数据熔合在一起返回,此时体现进去的就是查问性能显著降落,即读放大。
性能改善形式
对于曾经运行在线上的 buffer
表问题,官网文档中给出的应急解决计划如下:
- 对于存在可用索引,但 OB 优化器打算生成为全表扫描的场景。须要进行执行打算 binding 来固定打算。
- 如果 SQL 查问的次要过滤字段无可用索引,此时举荐在线创立可用索引并绑定该打算。
- 如果业务场景临时无奈创立索引,或者执行的 SQL 多为范畴扫描,此时可依据业务场景须要决定是否手动触发合并,将删除或更新的数据版本进行清理,升高全表扫描的数据量,晋升速度。
另外,从 2.2.7 版本开始,OceanBase 引入了 buffer minor merge
设计,实现对 Queuing 表的非凡转储机制,彻底解决有效扫描问题,通过将表的模式设置为 queuing
来开启。对于设计阶段曾经明确的 Queuing 表场景,举荐开启该个性作为长期解决方案。
ALTER TABLE table_name TABLE_MODE = 'queuing';
然而社区版 4.0.0.0 的公布记录中看到,不再反对 Queuing 表。后查问社区有解释:OB 在 4.x 版本(预计 4.1 实现)采纳自适应的形式反对 Queuing 表的这种场景,不须要再人为指定,也就是 Release Note 中提到的不再反对 Queuing 表。
参考资料
- Queuing 表查问迟缓问题
- 大批量数据处理后拜访慢问题解决
- OceanBase Queuing 表(buffer 表)解决最佳实际
- ob4.0 确定不反对 Queuing 表了吗?
本文关键字:#OceanBase# #火焰图# #性能调优 #