作者:李锡超,苏商银行DBA,负责数据库和中间件运维和建设。擅长 MySQL、Python、Oracle,爱好骑行、技术研究和分享。
爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。
本文约 4500 字,预计阅读需要 15 分钟。
近期,在进行 OceanBase 测试时,执行了一次单机转单副本的测试。按照官方文档步骤操作后,发现了不少问题,在此进行总结。
建议先对 sys
租户进行扩容,这应该是必要步骤,否则扩容后可能是“伪集群”。官方文档似乎遗漏了这一步骤,笔者在后续过程中发现问题,尝试解决时才注意到。
建议在扩容业务租户前执行以下操作:
ALTER RESOURCE POOL sys_pool ZONE_LIST=('zone1','zone2','zone3');
ALTER TENANT sys LOCALITY='F@zone1,F@zone2';
ALTER TENANT sys LOCALITY='F@zone1,F@zone2,F@zone3';
根据测试结果,OceanBase 4.X 版本的表副本管理基本自动化,主要基于租户的 PRIMARY_ZONE 等配置。同时,早期版本中很多关于表副本管理的命令在该版本已被废弃。例如:
ALTER TABLE test_t1 set locality = 'F@zone3,F@zone2,F@zone1';
ALTER TABLE test_t1 locality='F,R{all_server}@ZONE1, F,R{all_server}@ZONE2, F,R{all_server}@ZONE3';
执行上述命令时会报错,提示语法错误,需根据 OceanBase 版本查看正确语法。
本次测试遇到最大问题是 log_disk_size
参数的设置,导致第一轮测试完全失败,且经过很长时间才找到问题原因。这也是分布式数据库与传统数据库在运维上的典型差异。
完成租户扩容后,通过以下语句检查表的分布:
SELECT tenant_name,database_name, table_name, partition_name, index_name, tablet_id,ls_id,zone,svr_ip,role
FROM OCEANBASE.CDB_OB_TABLE_LOCATIONS a join oceanbase.DBA_OB_TENANTS b
on a.tenant_id=b.tenant_id
WHERE DATABASE_NAME='MYDB' AND TENANT_NAME='mysql_tenant1'
order by tenant_name,database_name, table_name, partition_name, zone;
SELECT zone,svr_ip,role,count(1)
FROM OCEANBASE.CDB_OB_TABLE_LOCATIONS a join oceanbase.DBA_OB_TENANTS b
on a.tenant_id=b.tenant_id
WHERE DATABASE_NAME='MYDB' AND TENANT_NAME='mysql_tenant1'
group by zone,svr_ip,role
order by zone;
返回结果
根据返回结果可知,无论表是否分区,其 LEADER 都只在 zone1、zone2,在 zone3 上全是 FOLLOWER。
确认租户 PRIMARY_ZONE='zone1,zone2,zone3'
,并建立大量表和分区后,均未发现问题。尝试通过建表时指定分区/表的位置,发现相关指令只能在老版本中执行。停止 zone2 进行强制切换,发现 zone2 停止后,所有 LEADER 都迁移到了 zone1,zone3 仍然全是 FOLLOWER。
重新安装 3 节点集群、创建租户并导入测试数据后,问题依旧存在。做如下尝试:
PRIMARY_ZONE='zone1,zone2,zone3'
不生效,尝试设置 PRIMARY_ZONE=RANDOM
,问题未解决。enable_rebalance/enable_transfer
参数,均采用默认值(true),表示会开启自动负载均衡功能。了解到小租户的概念及 LEADER 分布规则,尝试建立多个租户后,问题依旧存在。同时,根据资料提示编写命令尝试切换,但 OceanBase 4.X 中不支持相关语法。ALTER SYSTEM SWITCH REPLICA
命令无法执行,但 4.X 文档提示支持该语法。根据文档语句生成具体命令:ALTER SYSTEM SWITCH REPLICA LEADER LS = 1002 SERVER = '10.0.139.175:2882' TENANT = mysql_tenant1;
执行切换后,发现所有表和分区的 LEADER 位于 zone2 的,都切换到了 zone3,说明 zone3 正常,只是由于某种原因无法“承担”表的 LEADER。查看日志流分布的语句为:
SELECT a.TENANT_ID,a.LS_ID,a.SVR_IP,a.SVR_PORT,a.ZONE,a.role,b.TENANT_NAME,b.TENANT_TYPE FROM oceanbase.CDB_OB_LS_LOCATIONS a, oceanbase.DBA_OB_TENANTS b WHERE a.TENANT_ID=b.TENANT_ID;
返回结果
根据返回结果显示,日志流的 LEADER 也只位于 zone1、zone3。
<data_dir>/clog/tenant_xxxx
(xxxx 表示租户 ID),sys
租户对应则为 <data_dir>/clog/tenant_1
;文件以从 0 开始的编号(一直递增,不重复)进行命名;单个日志文件大小为 64MB。该方式与传统数据库的 Redo 存在明显差异。sys
租户包括一个 LS,LSID 为 1;各 META$xxxx
(xxxx 表示租户 ID)租户包括一个 LS,LSID 为 1;各业务租户包含 N + 1 个 LS(N 表示租户中 Zone 的个数),LSID 分别为 1、1001、1002、...,其中 LSID = 1
对应各租户的内部数据库的 Tablet,如 OceanBase、MySQL,1001 或更高的 LSID 在租户的各个 Zone 各有一个,对应所有业务数据库内的 Tablet。log_disk_size
log_disk_size
参数。log_disk_percentage
配置项设置的值分配日志盘空间。log_disk_size = 100G
,那么 OBServer 节点第一次启动时,就会预分配 100G 的日志文件作为日志文件池。<data_dir>/clog/log_pool/
下很多 64MB 的文件。log_disk_percentage
log_disk_size = 0
时,才会根据 log_disk_percentage
配置项设置的值分配日志盘空间;否则 log_disk_percentage
不生效。log_disk_percentage = 0
、log_disk_size = 0
同时满足时,系统会根据日志和数据是否共用同一磁盘来自动计算 Redo 日志占用其所在磁盘总空间的百分比:log_disk_size
属性指定,最小为 2G(否则规格将创建失败)。当未指定时,log_disk_size
将默认设置为内存的 3 倍大小。meta
租户(且相互隔离):其中 meta
租户 = max(log_disk_size * 10%, 512M)
,剩余的给业务租户。Unit
失败。log_disk_utilization_threshold
时,进行日志文件重用。log_disk_utilization_limit_threshold
时,不再允许日志写入。了解日志流原理及与表/分区的关系后,发现表/分区的 LEADER 分布由日志流的 LEADER 分布决定。再次检查表的分布和日志流分布:
SELECT a.tenant_id,tenant_name,database_name,ls_id,zone,svr_ip,role,count(1)
FROM OCEANBASE.CDB_OB_TABLE_LOCATIONS a join oceanbase.DBA_OB_TENANTS b
on a.tenant_id=b.tenant_id
group by tenant_name,database_name,ls_id,zone,svr_ip,role
order by a.tenant_id,database_name,ls_id;
发现 meta
租户的日志流 LSID = 1 已正常建立完成,业务租户的 LSID = 1 日志流已正常建立完成,LSID = 1001 已正常建立完成,LSID = 1002 只建立了两个副本,LSID = 1003 无副本。
通过 CDB_OB_LS
检查日志流的状态,发现日志状态一直处于 CREATING 中:
select TENANT_ID,LS_ID,STATUS,PRIMARY_ZONE from oceanbase.CDB_OB_LS;
该状态表示日志流一直处于“创建中”。那为何会出现该状态呢!
/home/admin/oceanbase/log
。RENAME
为其归档,命名是在后面加上时间戳,比如 observer.log.20240901123456
。Y0 - 0000000000000000 - 0 - 0
,可以根据找到一条 SQL 的执行过程,是排查问题的重要手段。也可以通过 SELECT last_trace_id();
显示获取 trace_id。检查 wf 日志,发现频繁产生 ERROR 错误:
[2024-10-29 00:03:56.112302] ERROR issue_dba_error (ob_Log.cpp:1875)[38115][T1060_L0_G0][T1060][YB420A008BAD-000624C0A09C1A91-0-0] [Lt-17][errcode=4388] Unexpect
ed internal error happen, please checkout the internal errcode(errcode=-4264, file="ob_log_service.cpp", Lne_no=b89, info="create_ls failed!!!")
根据 wf 日志的 trace_id
在 observer.log 过滤,然后根据时间戳搜索具体的日志行(此处未展示具体日志内容)。
根据日志,看到 create_ls failed,失败原因是 OB_LOG_OUTOF_DISK_SPACE
。进一步发现 PALF(Paxos - base Append - only File System,是 OceanBase 日志同步服务进行打包后的一个框架简称。每个租户的 zone 都有一个 palf,负责对应 zone、租户下的日志的写入和同步服务)模块中返回异常:PalfEnv can not hold more instance,并反馈 log_disk_size
相关参数。
执行以下 SQL 检查数据库的日志使用情况:
select SVR_IP,SVR_PORT,UNIT_ID,TENANT_ID,
ceil(LOG_DISK_SIZE/1024/1024) LOG_DISK_SIZE_M,ceil
(LOG_DISK_IN_USE/1024/1024) LOG_DISK_IN_USE_M,
round(LOG_DISK_IN_USE/LOG_DISK_SIZE*100) LOG_USE_PCT,
ceil(DATA_DISK_IN_USE/1024/1024/1024) DATA_DISK_IN_USE_G,
STATUS from oceanbase.GV$OB_UNITS
order by SVR_IP,TENANT_ID;
select SVR_IP,ZONE,
round(LOG_DISK_CAPACITY/1024/1024/1024) LOG_DISK_CAPACITY_G,
round(LOG_DISK_ASSIGNED/1024/1024/1024) LOG_DISK_ASSIGN_G,
round(LOG_DISK_ASSIGNED/LOG_DISK_CAPACITY*100) LOG_USE_PCT
from oceanbase.GV$OB_SERVERS;
根据 OceanBase 日志管理机制和当前使用数据,发现对应的业务租户的使用率偏低,未见异常。
确认租户创建语句:
CREATE RESOURCE UNIT S4_unit_config
MEMORY_SIZE = '1536M',
MAX_CPU = 1, MIN_CPU = 1,
LOG_DISK_SIZE = '2048M',
MAX_IOPS = 10000, MIN_IOPS = 10000, IOPS_WEIGHT=1;
CREATE RESOURCE POOL mq_pool_04
UNIT='S4_unit_config',
UNIT_NUM=1,
ZONE_LIST=('zone1','zone2','zone3');
CREATE TENANT IF NOT EXISTS mysql_tenant4
PRIMARY_ZONE='zone1,zone2,zone3',
RESOURCE_POOL_LIST=('mq_pool_04')
set OB_TCP_INVITED_NODES='%';
指定了租户的 LOG_DISK_SIZE 为 2GB,结合日志使用情况,OceanBase 对 meta
租户分配了 512MB,业务租户分配了 1536MB。通过官方相关参数规范,满足最小值要求,但创建仍失败。
根据以上测试结果,发现只有当 LOG_DISK_SIZE 大小必须大于 2560M 时,所有 LS 才会创建成功,否则会创建失败。
根据 observer.log 的日志内容,在 palf_env_impl.cpp/palf_env.cpp
添加断点,然后重复创建租户。跟踪发现租户创建过程中存在如下堆栈:
#0 oceanbase::palf::PalfEnvImpl::check_can_create_palf_handle_impl_() const (this=0x7fe7c33fc030) at ./src/logservice/palf/palf_env_impl.cpp:1197
#1 0x00007fe820571e72 in oceanbase::palf::PalfEnvImpl::create_palf_handle_impl_(long, oceanbase::palf::AccessMode const&, oceanbase::palf::PalfBaseInfo const&, oceanbase::palf::LogReplicaType, oceanbase::palf::IPalfHandleImpl*&) (this=0x7fe7c33fc030, palf_id=1001, access_mode=@0x7fe7bb14d2ec: APPEND, palf_base_info=..., replica_type=NORMAL_REPLICA,
ipalf_handle_impl=@0x7fe7bb14d0f0: 0x0) at ./src/logservice/palf/palf_env_impl.cpp:416
#2 0x00007fe82056aac1 in oceanbase::palf::PalfEnvImpl::create_palf_handle_impl(long, oceanbase::palf::AccessMode const&, oceanbase::palf::PalfBaseInfo const&, oceanbase::palf::IPalfHandleImpl*&) (this=0x7fe7c33fc030, palf_id=1001, access_mode=@0x7fe7bb14d2ec: APPEND, palf_base_info=..., palf_handle_impl=@0x7fe7bb14d0f0: 0x0) at ./src/logservice/palf/palf_env_impl.cpp:382
#3 0x00007fe82056a91c in oceanbase::palf::PalfEnv::create(long, oceanbase::palf::AccessMode const&, oceanbase::palf::PalfBaseInfo const&, oceanbase::palf::PalfHandle&) (
this=0x7fe7c33fc030, id=1001, access_mode=@0x7fe7bb14d2ec: APPEND, palf_base_info=..., handle=...) at ./src/logservice/palf/palf_env.cpp:110
#4 0x00007fe81f7fd54e in oceanbase::logservice::ObLogService::create_ls_(oceanbase::share::ObLSID const&, oceanbase::common::ObReplicaType const&, oceanbase::share::ObTenantRole const&, oceanbase::palf::PalfBaseInfo const&, bool, oceanbase::logservice::ObLogHandler&, oceanbase::logservice::ObLogRestoreHandler&) (this=0x7fe7a5404030, id=...,
replica_type=@0x7fe7bb14d834: REPLICA_TYPE_FULL, tenant_role=..., palf_base_info=..., allow_log_sync=true, log_handler=..., restore_handler=...)
at ./src/logservice/ob_log_service.cpp:665
#5 0x00007fe81f7fcdcc in oceanbase::logservice::ObLogService::create_ls(oceanbase::share::ObLSID const&, oceanbase::common::ObReplicaType const&, oceanbase::share::ObTenantRole const&, oceanbase::palf::PalfBaseInfo const&, bool, oceanbase::logservice::ObLogHandler&, oceanbase::logservice::ObLogRestoreHandler&) (this=0x7fe7a5404030, id=...,
replica_type=@0x7fe7bb14d834: REPLICA_TYPE_FULL, tenant_role=..., palf_base_info=..., allow_log_sync=true, log_handler=..., restore_handler=...)
at ./src/logservice/ob_log_service.cpp:343
#6 0x00007fe828b07498 in oceanbase::storage::ObLS::create_ls(oceanbase::share::ObTenantRole, oceanbase::palf::PalfBaseInfo const&, oceanbase::common::ObReplicaType const&, bool) (
this=0x7fe7996cc150, tenant_role=..., palf_base_info=..., replica_type=@0x7fe7bb14d834: REPLICA_TYPE_FULL, allow_log_sync=true) at ./src/storage/ls/ob_ls.cpp:405
#7 0x00007fe828c79ebf in oceanbase::storage::ObLSService::create_ls(oceanbase::obrpc::ObCreateLSArg const&) (this=0x7fe7909ce030, arg=...)
at ./src/storage/tx_storage/ob_ls_service.cpp:508
#8 0x00007fe8232d8c52 in oceanbase::observer::ObRpcCreateLSP::process() (this=0x7fe7bb15a700) at ./src/observer/ob_rpc_processor_simple.cpp:1521
#9 0x00007fe81f601db0 in oceanbase::obrpc::ObRpcProcessorBase::run() (this=0x7fe7bb15a700) at ./deps/oblib/src/rpc/obrpc/ob_rpc_processor_base.cpp:89
#10 0x00007fe822b07e66 in oceanbase::omt::ObWorkerProcessor::process_one(oceanbase::rpc::ObRequest&) (this=0x7fe7775931a8, req=...) at ./src/observer/omt/ob_worker_processor.cpp:88
#11 0x00007fe81f2959d3 in oceanbase::omt::ObWorkerProcessor::process(oceanbase::rpc::ObRequest&) (this=0x7fe7775931a8, req=...) at ./src/observer/omt/ob_worker_processor.cpp:157
#12 0x00007fe822b06d01 in oceanbase::omt::ObThWorker::process_request(oceanbase::rpc::ObRequest&) (this=0x7fe7775930e0, req=...) at ./src/observer/omt/ob_th_worker.cpp:248
#13 0x00007fe81f294a8f in oceanbase::omt::ObThWorker::worker(long&, long&, int&) (this=0x7fe7775930e0, tenant_id=@0x7fe7bb14ec98: 1046,
req_recv_timestamp=@0x7fe7bb14ec90: 1729995314847463, worker_level=@0x7fe7bb14ec8c: 0) at ./src/observer/omt/ob_th_worker.cpp:387
#14 0x00007fe822b072bc in oceanbase::omt::ObThWorker::run(long) (this=0x7fe7775930e0, idx=0) at ./src/observer/omt/ob_th_worker.cpp:424
#15 0x00007fe82ea0c271 in oceanbase::lib::Thread::run() (this=0x7fe777593310) at ./deps/oblib/src/lib/thread/thread.cpp:164
#16 0x00007fe82ea0bdae in oceanbase::lib::Thread::__th_start(void*) (arg=0x7fe777593310) at ./deps/oblib/src/lib/thread/thread.cpp:322
#17 0x00007fe819435dc5 in start_thread () from /lib64/libpthread.so.0
#18 0x00007fe81916473d in clone () from /lib64/libc.so.6
在执行 PALF 处理接口创建过程中,需要执行如下检查:
bool PalfEnvImpl::check_can_create_palf_handle_impl_() const{
bool bool_ret = true;
// 统计Palf处理接口的个数。例如当租户的PRIMARY_ZONE三个zone时,返回3.只有一个zone时,返回1
int64_t count = palf_handle_impl_map_.count();
// 获取磁盘的参数,包括总大小,回收阈值,限制写入阈值等属性。其中log_disk_usage_limit_size_即为当前该租户创建指定的log_disk_size(2048M) - 租户的日志组大小(512M) = 1536M.
const PalfDiskOptions disk_opts = disk_options_wrapper_.get_disk_opts_for_recycling_blocks();
// MIN_DISK_SIZE_PER_PALF_INSTANCE源码定义为512M
// 计算 (3+1)*512=2048,2048 <= 1536不成立,因此将返回bool_ret为false
bool_ret = (count + 1) * MIN_DISK_SIZE_PER_PALF_INSTANCE <= disk_opts.log_disk_usage_limit_size_;
return bool_ret;
}
结合日志流相关原理和实现代码:
当需要创建一个包含 3 个 zone 的租户时,所需的日志具体数量如下:
因此,当租户指定的 PRIMARY_ZONE 为 3 个 zone 时,租户在创建时,需要为业务租户创建 4 个日志流,所需的最小日志流大小:
(3+1) × 每个 PALF 的最小日志磁盘空间 = 1 × 512MB = 2048M
由于当前业务租户总的日志大小为总的规格大小 - meta
租户的日志大小为 1536M,小于所需的 2048M,无法满足日志需求检查不通过,LSID=1003 无法创建成功。
综合以上分析过程,导致本次单机改三副本操作完成后,表的副本无法均分到三个 zone 的根本原因为:租户创建过程中,规格的 log_disk_size
为 2048M,去除 meta
租户的日志大小,业务租户剩余大小为 1536M。租户的 PRIMARY_ZONE 包含 3 个 zone,OceanBase 内部在创建日志流时,需要检查业务租户的日志空间是否满足总的日志大小大于 4×512M,由于无法满足,导致 LSID=1003 的日志流无法创建成功。因此对应的 zone 无法创建表的主副本。
增加规格的 log_disk_size
的大小。如日志空间充足,建议采用默认方式,不指定 log_disk_size
的大小。默认分配为内存的三倍。
如日志空间有限,必须满足 >= 租户的 (PRIMARY_ZONE 个数+2)+512M。
本文关键字:#OceanBase# #单机版# #日志# #扩容#