本文主要分享 OBCE V3 培训的理论和试验知识。个人观点,理解不当欢迎指出。
OBCE培训材料和试验官网地址:https://www.oceanbase.com/training/detail?level=OBCE
OB 是单进程多线程软件,进程在第一次启动的时候需要指定内存参数(memory_limit 或 memory_limit_percentage)。初始化 V3.2 版本的 OB 集群时,memory_limit 建议最小有 10G 。这样进程 observer 就可以支配操作系统 10G 内存(不是一开始就占用,后面使用的时候慢慢涨上去)。

业务租户的内存主要包括存放基线数据的 KVCACHE 、存放增量数据的 MEMSTORE 以及其他 SQL 执行模块(如SQL AREA、WORK AREA、PLAN CACHE、SQL AUDIT、分区和事务等)的内存。内存管理使用 Context 和Module。Context 是上下文内存,是一组线程间局部的内存集合。Mod是内存模块,直接从操作系统内存中分配。每个Context 可能有多个 Mod,同一类型的 Mod 也可能在多个 Context 中独立分配。像 MemStore、KVCache 都属于 Context 。所有 Context 的内存不能超过租户的内存。

不同 Context 的内存管理策略不一定一样。MemTable 的内存有上限限制(跟集群节点参数 memstore_percentage 有关)。KVCache 的内存是可以动态伸缩的。每个 Mod 模块申请内存的单位有 64KB、2MB 甚至更大的粒度。
查看租户的 Context 和 Modudle 内存用下面 SQL (V3.2版本)
SELECTtenant_id,svr_ip,ctx_id,ctx_name,mod_name,count,round(HOLD 1024 1024, 2) hold_mb,round(used 1024 1024, 2) used_mbFROMoceanbase.__all_virtual_memory_infoWHEREtenant_id = 1002AND svr_ip IN ('10.0.0.61')-- AND ctx_id=32ORDER BY used DESCLIMIT 10;

MemStore 的内存主要存放增量数据,当已使用内存水位超过租户冻结参数 freeze_trigger_percentage 后,MemStore 里的分区开始转储到磁盘上,释放内存。当水位进一步上升到租户限流参数 writing_throttling_trigger_percentage(默认值 100(%),强烈建议设置为 90 )时,租户写入就降速(申请MemStore 内存出现等待)。当内存水位接近 100% 时就会有租户事务申请内存失败,租户写入开始大量报错。。转储和合并会释放内存,事务写入会消耗内存。通过调整转储参数、水位比例参数可以实现租户水位永远不会接近 100% 同时性能还是最优状态。这个需要反复观察和调优。
查看租户 MemStore 内存使用情况用下面 SQL 。
SELECTtenant_id,ip ,round(active 1024 1024, 2) active_mb,round(total 1024 1024, 2) total_mb,round(freeze_trigger 1024 1024, 2 ) freeze_trg_mb,round(mem_limit 1024 1024, 2) mem_limit_mb,freeze_cntFROMoceanbase.gv$memstore mWHEREm.tenant_id = 1003ORDER BY ip ;

查看表在 MemStore 中内存占用用下面 SQL 。
SELECTip,t.table_name,usec_to_time(base_version) base_version_time,round(used 1024 1024, 1) AS used_mb,hash_items,btree_items,is_activeFROMgv$memstore_info mJOIN gv$table t ON(m.tenant_id = t.tenant_idAND m.table_id = t.table_id)WHEREt.tenant_id = 1003AND t.database_name IN ('TPCC')AND t.table_name IN ('TAB_NO_QUEUE', 'TAB_QUEUE')ORDER BYt.table_name ,m.ip,m.base_version DESC ;

每个表的分区在的转储和合并历史用下面 SQL 查看。
SELECTm.svr_ip,t.table_name,TYPE,ACTION,usec_to_time(version) version,start_time,end_time ,macro_block_count blocksFROMgv$merge_info mJOIN gv$table t ON(m.tenant_id = t.tenant_idAND m.table_id = t.table_id)WHEREm.tenant_id = 1003AND t.table_name IN ('TAB_NO_QUEUE', 'TAB_QUEUE')AND t.database_name = 'TPCC'ORDER BYt.table_name,start_time DESC ;

这两个表是后面测试 OB buffer 表(或者叫 queue 表)用的。
OB 的读写模型是 LSM-Tree 模型,所有的写操作(增删改)都是在原表基础上追加数据方式,不像传统数据库BTree 模型那样是直接修改原来的数据块。LSM-Tree 模型对写入非常友好,对读就不那么友好。读的时候要把 基线数据跟中间写产生的增量数据在内存中合并一起读出就是最新的数据。如果加上一个版本管理,就是 MVCC 多版本一致性读。为了降低读的性能衰减,OB 对 KVCache 中的块有 Block 缓存,对MemStore 内存中的行记录有 Row 缓存。此外内存中还为每个表维护了两套索引机制(HASH 索引和 B-Tree 索引)。HASH 索引方便主键查询场景,B-Tree 索引方便范围扫描场景。
LSM-Tree 有个问题,当表记录数反复插入和删除的时候,中间过程的记录都在内存中保留,既占用内存空间,又导致查询扫描的时候扫描了很多无意义的数据块(因为记录最终都被删除了)。OB 3.2 版本开始解决这个问题,设计了 Queue 表逻辑。对于 Queue 表,当删除的数据量或比例达到阈值后,直接开始转储(Minor Merge),转储落盘的版本里不包含中间被删除掉的记录,所以查询又可以更快了。不过这个设计还不完善,在 4.2 版本里 OB 将这个进一步发展为自适应的合并策略(还在继续完善中),不需要明确设置为 Queue 表(即 4.x 不支持 Queue 表设置了)。
以上特征可以通过观察 SQL审计记录中扫描的 sstable 数量以及执行耗时来观察优化效果。
SELECTusec_to_time(request_time) req_time,tenant_name,svr_ip,user_name,db_name,ret_code,trace_idFROMgv$sql_auditWHEREtenant_id = 1002AND user_name = 'root'AND ret_code IN (-4030,-4013)ORDER BYrequest_id DESCLIMIT 10;
OB 的内存 Context 中,除了 MemStore 外,KVCache、PlanCache、WorkArea 等内存都可以通过视图查看。SQL 如下:
SELECTtenant_id,svr_ip,cache_name,priority,round(cache_size 1024 1024, 2) cache_mb,round(cache_store_size 1024 1024, 2) cache_store_mb,round(cache_map_size 1024 1024, 2) cache_map_mb,kv_cnt,hit_ratioFROMoceanbase.__all_virtual_kvcache_infoWHEREtenant_id = 1003ORDER BY total_hit_cnt DESC ;SELECTtenant_id,svr_ip,round(max_workarea_size 1024 1024 1024, 2) max_workarea_gb,round(workarea_hold_size 1024 1024 1024, 2) workarea_hold_gb,round(max_auto_workarea_size 1024 1024 1024, 2) max_auto_workarea_gb,round(mem_target 1024 1024 1024, 2) mem_target_gb,round(total_mem_used / 1024 / 1024, 2) total_mem_used_mb,round(global_mem_bound / 1024 / 1024 / 1024, 2) global_mem_bound_gb,workarea_count,manual_calc_count,drift_sizeFROMoceanbase.__all_virtual_sql_workarea_memory_infoWHEREtenant_id = 1003ORDER BY workarea_hold_size DESC;SELECTtenant_id,svr_ip,sql_num,round(mem_used / 1024 / 1024, 2) mem_used_mb,round(mem_hold / 1024 / 1024, 2) mem_hold_mb,round(mem_limit / 1024 / 1024, 2) mem_limit_mbFROMoceanbase.gv$plan_cache_statWHEREtenant_id = 1002ORDER BYmem_used DESC;
下面开始试验。
试验是两道题。

租户用户和建表初始化语句如下。
create user tpcc identified by obce_test;grant dba to tpcc;alter session set current_schema=tpcc;drop table tab_no_queue purge;drop table tab_queue purge;create table tab_no_queue (id int primary key, name varchar(10), contact varchar(20), addr varchar(100),gmt_create timestamp DEFAULT current_timestamp);create table tab_queue(id int primary key, name varchar(10), contact varchar(20),addr varchar(100),gmt_create timestamp DEFAULT current_timestamp) table_mode='queuing';
租户的Queue表转储参数设置。
show parameters like '_ob_queuing_fast_freeze_min_count';alter system set "_ob_queuing_fast_freeze_min_count"=19999;
这个查看方法后面版本查不出来。查看这个参数。如果在 SYS 租户,则用下面 SQL 查询。
select gmt_modified , svr_type, svr_ip , name, data_type , value ,`section` ,`scope` ,edit_levelfrom `__all_sys_parameter` p where p.name='_ob_queuing_fast_freeze_min_count' ;
如果是在 ORACLE 租户里,则用下面SQL查询。
SELECT effective_tenant_id() tenant_id, ZONE,svr_ip,name,value,info,scope FROMSYS.all_virtual_tenant_parameter_stat WHEREname like '%_ob_queuing%';

查看两个表的主副本位置(如果是单节点集群,没必要。三节点集群要看看)。
两个表数据初始化脚本。
SELECTtenant.tenant_name,meta.table_id,tab.table_name,partition_id,ZONE,svr_ip,svr_port,CASEWHEN ROLE = 1 THEN 'leader'WHEN ROLE = 2THEN 'follower'ELSE NULLEND AS ROLE,tab.primary_zoneFROM__all_virtual_meta_table metaINNER JOIN __all_tenant tenant ONmeta.tenant_id = tenant.tenant_idINNER JOIN __all_virtual_table tab ONmeta.tenant_id = tab.tenant_idAND meta.table_id = tab.table_idWHEREtenant.tenant_id = 1001AND tab.table_name IN ( 'TAB_QUEUE','TAB_NO_QUEUE' )AND ROLE=1ORDER BYtenant.tenant_name,table_name,partition_id,ZONE;+-------------+------------------+--------------+--------------+-------+-----------+----------+--------+--------------+| tenant_name | table_id | table_name | partition_id | ZONE | svr_ip | svr_port | ROLE | primary_zone |+-------------+------------------+--------------+--------------+-------+-----------+----------+--------+--------------+| ob_ora | 1100611139453777 | TAB_NO_QUEUE | 0 | zone1 | 127.0.0.1 | 2882 | leader | || ob_ora | 1100611139453778 | TAB_QUEUE | 0 | zone1 | 127.0.0.1 | 2882 | leader | |+-------------+------------------+--------------+--------------+-------+-----------+----------+--------+--------------+2 rows in set (0.00 sec)
初始化数据脚本。
insert into tab_no_queue select level,case mod(level,5)when 0 then '张一'when 1 then '李一'when 2 then '王一'when 3 then '赵一'when 4 then '钱一'else nullend, '1234567890','Asia-China-Sichuan-Chengdu'from dual where mod(level,5)=3 connect by level <=500000;commit;insert into tab_queue select level,case mod(level,5)when 0 then '张一'when 1 then '李一'when 2 then '王一'when 3 then '赵一'when 4 then '钱一'else nullend, '1234567890','Asia-China-Sichuan-Chengdu'from dual where mod(level,5)=3 connect by level <=500000;commit;
然后在 SYS 租户发起一次转储。查看两个表转储记录。

模拟业务反复插入和删除数据。下面脚本反复执行 5 次以上。
insert into tab_no_queue select level,case mod(level,5)when 0 then '张一'when 1 then '李一'when 2 then '王一'when 3 then '赵一'when 4 then '钱一'else nullend, '1234567890','Asia-China-Sichuan-Chengdu' from dual where mod(level,5) in (2)connect by level <=150000;commit;update tab_no_queue set name = '王二' where mod(id,5) in (2);commit;delete from tab_no_queue where mod(id,5) in (2);commit;insert into tab_queue select level,case mod(level,5)when 0 then '张一'when 1 then '李一'when 2 then '王一'when 3 then '赵一'when 4 then '钱一'else nullend, '1234567890','Asia-China-Sichuan-Chengdu' from dual where mod(level,5) in (2)connect by level <=150000;commit;update tab_queue set name = '王二' where mod(id,5) in (2);commit;delete from tab_queue where mod(id,5) in (2);commit;
在反复写入和删除过程中,tab_queue 会自动发生转储。忘记截图了,用以前的图代替一下。

对两个表发起全表扫描的查询,然后分析 SQL 审计记录。
SELECT /*+ queue test */ sysdate, count(*) cnt FROM tab_no_queue WHERE name <> 'A';SELECT /*+ queue test */ sysdate, count(*) cnt FROM tab_queue WHERE name <> 'A';

SELECTusec_to_time(request_time) req_time,tenant_name,user_name,db_name,query_sql,table_scan,elapsed_time,queue_time,memstore_read_row_count,ssstore_read_row_countFROMgv$sql_auditWHEREtenant_id = 1001AND user_name = 'TPCC'AND query_sql LIKE '%queue test%'ORDER BYrequest_id DESCLIMIT 2;

对比发现,tab_queue 扫描时 ssstore_read_row_count 为 0 ,elapsed_time 也是最小。当然为什么 ssstore_read_row_count 为 0 而 memstore_read_row_count 是 130000 可能跟试验操作中步骤有关。这个原理探索还可以深挖下去。
第二道题是模拟内存不足报错

OceanBase 内存报错常见的错误码有两个:
-4013:No memory or reach tenant memory limit,表示工作区内存不足。
-4030:Over tenant memory limits,表示租户内存不足。
这里工作区内存和租户内存概念都很笼统,多个Context 内存不足都会报这个错。不同 OB 版本同样场景报错输出可能也不一样。
测试方法是用 sysbench初始化20张大表。sysbench的初始化都是 batch insert,针对的是MYSQL租户,测试用的租户内存故意只有 1G .所以基本上在初始化过程中就报错了。
下面是两种常见的报错提示:
FATAL: mysql_drv_query() returned error 4013 (No memory or reach tenant memory limit) for query 'INSERT INTO sbtest14(k, c, pad) VALUES(73749, '21193722787-18720102623-69520781658-95951112776-05555420568-46122341858-30494609327-52072194153-55191042883-71894089296', '70074786292-71607403023-45794377849-11590951158-23666382947'),(FATAL: mysql_drv_query() returned error 4030 (Over tenant memory limits) for query 'INSERT INTO sbtest18(k, c, pad) VALUES(100798, '37484404278-70837112123-17341207174-65887986996-99761190525-94347944961-40587198928-12112065516-19289839965-93817982203', '90533187257-41968545636-87518525717-55200946857-71813055854'),(99752, '5404
在 3.2.3.0 版本里,看到比较多的是 4030 错误,4013错误比较少碰到。报错后,通过查看SQL审计视图 GV$SQL_AUDIT 能抓到最近的报错信息。
SELECTusec_to_time(request_time) req_time,tenant_name,user_name,db_name,ret_code,trace_idFROMgv$sql_auditWHEREtenant_id = 1002AND user_name = 'root'AND ret_code IN (-4030,-4013)ORDER BYrequest_id DESCLIMIT 10;+----------------------------+-------------+-----------+---------+----------+-----------------------------------+| req_time | tenant_name | user_name | db_name | ret_code | trace_id |+----------------------------+-------------+-----------+---------+----------+-----------------------------------+| 2023-10-16 21:48:25.213912 | ob_mysql | root | sbtest | -4030 | YB427F000001-000607D4BEB5F5EF-0-0 || 2023-10-16 21:48:24.552431 | ob_mysql | root | sbtest | -4030 | YB427F000001-000607D4BE65F613-0-0 || 2023-10-16 21:48:24.258515 | ob_mysql | root | sbtest | -4030 | YB427F000001-000607D4C075F601-0-0 || 2023-10-16 21:48:24.206143 | ob_mysql | root | sbtest | -4030 | YB427F000001-000607D4BF45F60C-0-0 || 2023-10-16 21:48:23.870126 | ob_mysql | root | sbtest | -4030 | YB427F000001-000607D4C075F600-0-0 || 2023-10-16 21:48:23.832037 | ob_mysql | root | sbtest | -4030 | YB427F000001-000607D4BE75F601-0-0 || 2023-10-16 21:48:23.774265 | ob_mysql | root | sbtest | -4030 | YB427F000001-000607D4BEA5F60C-0-0 || 2023-10-16 21:48:23.744486 | ob_mysql | root | sbtest | -4030 | YB427F000001-000607D4BEA5F60B-0-0 || 2023-10-16 21:48:23.735562 | ob_mysql | root | sbtest | -4030 | YB427F000001-000607D4BF95F5FE-0-0 || 2023-10-16 21:48:23.658504 | ob_mysql | root | sbtest | -4013 | YB427F000001-000607D4C075F5FF-0-0 |+----------------------------+-------------+-----------+---------+----------+-----------------------------------+10 rows in set (0.02 sec)'
GV$SQL_AUDIT 有字段 trace_id ,根据这个 trace_id 和 svr_ip、request_time 信息到 OBServer 节点上去搜索对应时间段的日志。
实际测试可能碰到两类问题:
GV$SQL_AUDIT 没有抓到记录。主要是默认SQL_AUDIT 内存太小,在租户变量里将比例调大到30%,这样可以保存更多的最近 SQL。
OBServer 的Log里找不到记录。跟日志级别和日志输出限速有关。建议调大日志限速到50MB甚至更多,避免有些日志没有输出。
在 3.2.3.0 版本里能看到的日志不多。
-4013 错误的日志示例,留意图中的关键字。

-4030的错误日志更少。

基本上看不出来是哪个Context 或 Module 内存超限信息。这个主要是日志级别 是 INFO 导致输出信息很少。改为 TRACE 后日志稍微多一些,但也没有找到说明。估计要改为 DEBUG 级别。只是 DEBUG 级别日志会多很多,日志限速还要再提高一些。

如果试验的 OB 版本跟手册里 OB 版本一致的化,那根据这个关键字还能找到这些信息。如果 OB 版本不一样,这个输出逻辑可能变化很大。
在 3.2.4.0 版本里测试,基本上看到的都是 4013 错误,很少看到 4030 错误。并且 OBServer 进程的日志级别多了很多,默认是 INFO 级别,也看不到具体报错原因。需要将日志级别下调到 WDIAG 。
ALTER SYSTEM SET syslog_level='WDIAG';
这样能看到类似下面这样的日志。
[2023-10-16 16:57:48.307577] WDIAG ob_tenant_ctx_allocator.h:154 [88314][0][YB420A00003D-000607D0ECD18ACD-0-0] [lt=5] [dc=0][errcode=-4013] [OOPS] alloc failed reason: tenant memory has reached the upper limit(tenant_id: 1002, tenant_hold: 1073741824, tenant_limit: 1073741824, alloc_size: 2097152)[2023-10-16 16:57:48.307585] WDIAG ob_tenant_ctx_allocator.h:159 [88314][0][YB420A00003D-000607D0ECD18ACD-0-0] [lt=5] [dc=0][errcode=-4013] oops, alloc failed, tenant_id=1002, ctx_id=32, ctx_name=TX_CALLBACK_CTX_ID, ctx_hold=201326592, ctx_limit=9223372036854775807, tenant_hold=1073741824, tenant_limit=1073741824[2023-10-16 16:57:48.307610] ERROR issue_dba_error (ob_log.cpp:2322) [88314][0][YB420A00003D-000607D0ECD18ACD-0-0] [lt=5] [dc=0][errcode=-4388] Unexpected internal error happen, please checkout the internal errcode(errcode=-4013, file="ob_malloc.h", line_no=54, info="allocate memory fail")[2023-10-16 16:57:48.307621] EDIAG [LIB] ob_malloc.h:54 [88314][0][YB420A00003D-000607D0ECD18ACD-0-0] [lt=8] [dc=0][errcode=-4013] allocate memory fail(attr=tenant_id=1002, label=MemtableCallbac, ctx_id=32, prio=0, nbyte=8192) BACKTRACE:0x111b1ee0 0x11198fe4 0x5ee706b 0x5ee6bb8 0x5ee6934 0x5f1f49e 0x5f1f24d 0x5f1c543 0x5f023c0 0x5d70ee9 0x5d70cbd 0x5d70ac4 0x5d70979 0x5d701a3 0x5d6fea2 0x5d6b4a9 0x5d6b26b 0x5d6a914 0x5d6a113 0x5e2e499 0x5e2cdee 0x5e2b1d0 0x5e2a5a6 0x5d1f2ce 0x5d1f554 0x5d1f092 0x5c9f695 0x5c9aae6 0x5c955d9 0x5d21860 0x5d86184 0x5c88999 0x5c88814 0x5c8859e 0x5c881fc 0x5c9e16c 0x5c9814e 0x5c95290 0x5c868b3 0x5c814ba 0x5c7ef5d 0x5c7cf5e 0xcd57fa1 0x5c7c087 0xcd55adc 0x5c77819 0xcd56087 0x10b33ae3 0x10b3393f 0x11aea4ff
其中关键信息就是:[errcode=-4013] oops, alloc failed, tenant_id=1002, ctx_id=32, ctx_name=TX_CALLBACK_CTX_ID, ctx_hold=201326592, ctx_limit=9223372036854775807, tenant_hold=1073741824, tenant_limit=1073741824 。CTX_ID 为 32 的 TX_CALLBACK_CTX_ID 内存不足。
然后回头可以查看系统视图,或者在日志里查看最近的 内存 DUMP 数据。类似下面这种日志。
178522 [2023-10-16 14:50:04.451495] INFO [LIB] ob_malloc_allocator.cpp:495 [96905][0][Y0-0000000000000000-0-0] [lt=17] [dc=0] [MEMORY] tenant: 1002, limit: 1,073,741,824 hold: 662,700,032 rpc_hold: 0 cache_hold: 23,068,672 cache_used: 23,068,672 cache_item_count: 11178523 [MEMORY] ctx_id= DEFAULT_CTX_ID hold_bytes= 228,589,568178524 [MEMORY] ctx_id= MEMSTORE_CTX_ID hold_bytes= 75,497,472178525 [MEMORY] ctx_id= SQL_EXEC_CTX_ID hold_bytes= 4,194,304178526 [MEMORY] ctx_id= TRANS_CTX_MGR_ID hold_bytes= 16,777,216178527 [MEMORY] ctx_id= REPLAY_STATUS_CTX_ID hold_bytes= 16,777,216178528 [MEMORY] ctx_id= PLAN_CACHE_CTX_ID hold_bytes= 35,651,584178529 [MEMORY] ctx_id= WORK_AREA hold_bytes= 2,097,152178530 [MEMORY] ctx_id= TX_CALLBACK_CTX_ID hold_bytes= 260,046,848178531 [MEMORY][PM] tid= 317 used= 196,800 hold= 2,097,152 pm=0x7f4772e83dc0 ctx_name=SQL_EXEC_CTX_ID178532 [MEMORY][PM] tid= summary used= 196,800 hold= 2,097,152178533178586 [2023-10-16 14:50:04.451874] INFO [LIB] ob_tenant_ctx_allocator.cpp:167 [96905][0][Y0-0000000000000000-0-0] [lt=6] [dc=0]178587 [MEMORY] tenant_id= 1002 ctx_id= PLAN_CACHE_CTX_ID hold= 35,651,584 used= 2,524,112178588 [MEMORY] hold= 1,872,064 used= 1,829,448 count= 160 avg_used= 11,434 mod=OB_SQL_PHY_PLAN178589 [MEMORY] hold= 524,560 used= 497,480 count= 371 avg_used= 1,340 mod=OB_SQL_PLAN_CACHE178590 [MEMORY] hold= 115,584 used= 114,688 count= 14 avg_used= 8,192 mod=PRE_CALC_EXPR178591 [MEMORY] hold= 11,904 used= 11,712 count= 3 avg_used= 3,904 mod=TenantCtxAlloca178592 [MEMORY] hold= 2,524,112 used= 2,453,328 count= 548 avg_used= 4,476 mod=SUMMARY178593178594 [2023-10-16 14:50:04.451893] INFO [LIB] ob_tenant_ctx_allocator.cpp:167 [96905][0][Y0-0000000000000000-0-0] [lt=5] [dc=0]178595 [MEMORY] tenant_id= 1002 ctx_id= WORK_AREA hold= 2,097,152 used= 27,776178596 [MEMORY] hold= 27,776 used= 27,328 count= 7 avg_used= 3,904 mod=TenantCtxAlloca178597 [MEMORY] hold= 27,776 used= 27,328 count= 7 avg_used= 3,904 mod=SUMMARY178598
每个 Context 的明细 Mod 内存使用情况都在日志里有输出。




