OceanBase 数据库是多租户的数据库系统,为了确保租户间不出现资源争抢保障业务稳定运行, OceanBase 数据库针对租户间的资源进行了隔离。
OceanBase 数据库中把 Unit 当作给租户分配资源的基本单位,一个 Unit 可以类比于一个 Docker 容器。一个 OBServer 上可以创建多个 Unit,在 OBServer上每创建一个 Unit 都会占用一部分该 OBServer 的 CPU、内存等物理资源,OBServer 的资源分配情况会记录在内部表中以便 DBA 查看。
一个租户可以在多个 OBServer上放置多个 Unit,但一个特定的租户在某个 OBServer 上只能有一个 Unit。一个租户的多个 Unit 相互独立,OceanBase 数据库目前没有汇总多个 Unit 的资源占用进行全局的资源控制, 具体来讲,不会因为一个租户在某个 OBServer 上的资源没得到满足,就让它在另一个 OBServer 上去抢其它租户的资源。
所谓资源隔离,就是 OBServer 控制本地多个 Unit 间的资源分配的行为, 它是 OBServer 本地的行为。类似的技术是 Docker 和虚拟机,但 OceanBase 数据库并没有依赖 Docker 或虚拟机技术,而是在数据库内部实现资源隔离。
OceanBase 数据库租户隔离的优势
相比 Docker 和虚拟机,OceanBase 数据库的租户隔离更加轻量,并且便于实现优先级等高级特性。 从 OceanBase 数据库的需求来看,Docker 或虚拟机的主要有以下几个问题:
Docker 或虚拟机运行环境的开销太重,OceanBase 数据库需要支持轻量级租户。
Docker 或虚拟机规格变化以及迁移开销比较大,OceanBase 数据库希望租户的规格变化和迁移尽量快。
Docker 或虚拟机不便于租户间的资源共享, 例如,对象池的共享。
Docker 或虚拟机的资源隔离很难定制,例如,租户内的优先级支持。
除此之外,Docker 或虚拟机的实现不便于暴露统一视图。
普通用户的视角看隔离效果
从普通用户的角度,我们可以看到的隔离效果如下:
内存完全隔离。
具体包括:
CPU 通过用户态调度实现隔离。
一个租户能使用的 CPU 资源是由 Unit 规格决定的。
为了达到更好的隔离效果,从 V4.0.0 版本开始,支持配置 cgroup 来进行 CPU 隔离。
大部分数据结构是分离的。
具体包括:
SQL 的 Plan Cache 是租户分离的,一个租户的 Plan Cache 淘汰不会影响另一个租户。
SQL 的 Audit 表是分离的,一个租户的 QPS 太高,不会冲洗掉另一个租户的 Audit 信息。
事务相关的数据结构是分离的。
具体包括:
一个租户的行锁挂起,不会影响到其他租户。
一个租户的事务挂起,不会影响到其他租户。
一个租户的回放出问题,不会影响到其它租户。
Clog 也是分离的。
日志流的目录以租户为单位,最终在磁盘上的实现表现为如下:
tenant_id ├── unit_id_0 │ ├── log │ └── meta ├── unit_id_1 │ ├── log │ └── meta └── unit_id_2 ├── log └── meta
资源分类
从设计上,OceanBase 数据库从 V4.0.0 版本支持租户间 CPU、Memory、IOPS 隔离,一个 Unit 可以指定 CPU、Memory、IOPS、log_disk_size 这 4 种资源。
obclient> CREATE RESOURCE UNIT name
MAX_CPU = 1, [MIN_CPU = 1,]
MEMORY_SIZE = '2G',
[MAX_IOPS = 1024, MIN_IOPS = 1024, IOPS_WEIGHT=0,]
[LOG_DISK_SIZE = '2G'];
OBServer 的可用 CPU
OBServer 在启动时会探测物理机或容器的在线 CPU 个数,如果 OBServer 探测得不准确(例如,在容器化环境里),您也可以通过 cpu_count 配置项来指定。
由于 OBServer 会为后台线程预留两个 CPU,故实际可以分给租户的 CPU 总数会少两个。
OBServer 的可用 Memory
OBServer 在启动时会探测物理机或容器的内存,由于 OBServer 需要为其它进程预留一部分内存,故 observer 进程的可用内存等于 物理内存 * memory_limit_percentage。您也可以通过配置项 memory_limit 直接配置 observer 进程可用的总内存大小。
observer 进程可用的内存需要进一步扣除掉内部共用模块的那一部分,这部分内存大小由配置项 system_memory 指定,剩下的内存才是租户可用的总内存。
关于内存配置的更多信息,请参见 内存管理 章节。
查看每个 OBServer 的可用资源
您可以通过 oceanbase.GV$OB_SERVERS 视图查看每个 OBServer 的可用资源,示例如下:
obclient> SELECT * FROM oceanbase.GV$OB_SERVERS\G
*************************** 1. row ***************************
SVR_IP: xx.xx.xx.xx
SVR_PORT: 57234
ZONE: zone1
SQL_PORT: 57235
CPU_CAPACITY: 14
CPU_CAPACITY_MAX: 14
CPU_ASSIGNED: 6.5
CPU_ASSIGNED_MAX: 6.5
MEM_CAPACITY: 10737418240
MEM_ASSIGNED: 6442450944
LOG_DISK_CAPACITY: 316955164672
LOG_DISK_ASSIGNED: 15569256438
LOG_DISK_IN_USE: 939524096
DATA_DISK_CAPACITY: 10737418240
DATA_DISK_IN_USE: 624951296
DATA_DISK_HEALTH_STATUS: NORMAL
DATA_DISK_ABNORMAL_TIME: NULL
1 row in set
资源隔离
内存隔离
OBServer 的内存资源实际上不支持超卖,引入内存超卖反而会导致租户工作不稳定,从 V4.0.0 版本开始,OceanBase 数据库不再支持内存超卖。废弃 MIN_MEMORY 和 MAX_MEMORY 配置,采用 MEMORY_SIZE 参数代替。
CPU隔离
在 OceanBase 数据库 V3.1.x 版本之前,主要通过控制线程数来控制 CPU 的占用;在 OceanBase 数据库 V3.1.x 及之后的版本,允许配置 cgroup 来控制 CPU 的占用。
线程分类
OBServer 会启动很多不同功能的线程,详细的分类请参见 observer 线程,本节按照最粗略的标准可以分为以下两类:
一类是处理 SQL 和事务提交的线程,统称为租户工作线程。
其余的是处理网络 IO、磁盘 IO、Compaction 以及定时任务的线程,统称为后台线程。
在当前 V4.0.0 版本,租户工作线程和大部分后台线程是分租户的,网络线程是共享线程的。
基于线程数的租户工作线程的 CPU 隔离
Unit 的 CPU 隔离是通过一个 Unit 的活跃租户工作线程数实现的。
由于 SQL 执行过程中可能会有 IO 等待、锁等待等,所以一个线程无法用满一个物理 CPU,故在缺省配置下,OBServer 会给每个 CPU 启动 4 个线程,4 这个倍数可以通过配置 cpu_quota_concurrency 来控制。这就意味着如果一个 Unit 的 MAX_CPU 是 10,那么它能同时运行的活跃线程是 40,最大物理 CPU 的占用是 400%。
基于 cgroup 的租户工作线程的 CPU 隔离
开启 cgroup 后最大的变化是不同租户的工作线程放到不同的 cgroup 目录内,租户间的 CPU 隔离效果会更好。最后的隔离效果如下:
如果一个 OBServer 上只有一个租户负载很高,其余租户比较空闲,那么这个负载高的租户的工作线程可以用满与线程数相同数目的物理 CPU。
延续上面的场景,如果有多个空闲的租户的负载上升了,导致物理 CPU 不够了,cgroup 会按照权重分配时间片。
后台线程的 CPU 隔离
V4.0.0 版本的后台拆分了租户,每个租户下有对应的线程数控制。
为了更好的隔离效果,后台线程也会放到不同的 cgroup 目录内,实现租户间隔离,租户内和工作线程隔离。
大查询处理
我们认为相比于大查询,让短查询尽快返回对用户更有意义,即大查询的查询优先级更低,当大查询和短查询同时争抢 CPU 时,系统会限制大查询的 CPU 使用。
当一个线程执行的 SQL 查询耗时太长,这条查询就会被判定为大查询, 一旦判定为大查询,执行大查询的线程会等在一个 Pthread Condition 上,这样就为其它的租户工作线程让出了 CPU。
具体实现上,OBServer 在代码中插入了很多检查点,租户工作线程在运行过程中会通过检查点定期检查自己的状态,如果判断应该挂起,那么线程就会等待在一个 Pthread Condition 上,等到合适的时机再被唤醒。
如果同时有大查询和小查询,大查询最多占用 30% 的租户工作线程,30% 这个百分比值可以通过配置项 large_query_worker_percentage 来设置。
有两点需要说明:
当没有小查询的时候,大查询可以用到 100% 的租户工作线程。只有当同时有大查询和小查询时,30% 的比例才生效。
一个租户工作线程因为执行大查询被挂起时,作为补偿,系统可能会新创建一个租户工作线程,但是总的租户工作线程数不能超过
MAX_CPU的 10 倍,10 这个倍数可以通过配置项workers_per_cpu_quota来设置。
提前识别大查询
由于 OBServer 挂起一个大查询线程,就会启动一个新的租户工作线程, 但是如果有大量大查询涌入,OBServer 新创建的线程还是被用来处理大查询,很快达到租户工作线程数上限,在这批大查询消耗完之前就没有机会再处理短查询了。
为了优化这个场景,OBServer 会在 SQL 开始执行之前预判它是不是大查询,预判的本质就是估计 SQL 的执行时间。预判主要依据以下假设场景:如果两条 SQL 的执行 Plan 是一样的,可以猜测它们的执行时间也是相似的,这样就可以用 Plan 最近的执行时间来判断 SQL 会不会是大查询。
如果某条 SQL 被预判为大查询,那么该查询就会被放入一个特殊的大查询队列,其线程会被释放,系统就会接着执行后面的请求了。
监控项和日志
在日志中搜索 dump tenant info 关键字,可看到租户的规格、线程、队列及请求统计等信息,且这条日志每个租户每 30s 打印一次。
日志示例:
grep 'dump tenant info.tenant={id:1002' log/observer.log | sed 's/,/,\n/g'
[2022-07-20 14:55:40.774143] INFO [SERVER.OMT] run1 (ob_multi_tenant.cpp:1993) [80700][MultiTenant][T0][Y0-0000000000000000-0-0] [lt=621]
dump tenant info(tenant={id:1002, //租户 ID
tenant_meta:{unit:{tenant_id:1002, // tenant_meta 是租户元数据包括 Unit 配置信息和 SuperBlcok 以及 create_status
unit_id:1001, //Unit ID
has_memstore:true, // 是否有 MemTable
unit_status:"NORMAL", //Unit 的状态: UNIT_NORMAL/UNIT_MIGRATE_IN/UNIT_MIGRATE_OUT/UNIT_MARK_DELETING/UNIT_WAIT_GC_IN_OBSERVER/UNIT_DELETING_IN_OBSERVER/UNIT_ERROR_STAT
config:{unit_config_id:1003, // Unit 的配置 ID
name:"2c2g", //Unit 配置的名字
resource:{min_cpu:2, //Unit 配置的最小 CPU 数
max_cpu:2, // Unit 配置的最大 CPU 数
memory_size:"1.5GB", // Unit 配置的内存大小
log_disk_size:"5.4GB", // Unit 配置的日志盘大小
min_iops:20000, // Unit 配置的最小磁盘 IOPS
max_iops:20000, // Unit 配置的最大磁盘 IOPS
iops_weight:2}}, // Unit 配置的 IOPS 权重
mode:1, // CompatMode 0:MYSQL 1:ORACLE
create_timestamp:1658298418435426, // Unit 的创建时间
is_removed:false}, // 该 Unit 是否被标记删除
super_block:{tenant_id:1002, // ObTenantSuperBlock 信息
replay_start_point:ObLogCursor{file_id=1, // 租户 slog 的日志回放点
log_id=440,
offset=343322},
ls_meta_entry:[140](ver=0, // 租户 ls meta 的 slog checkpoint 入口点
mode=0,
seq=139),
tablet_meta_entry:[141](ver=0, // 租户 tablet meta 的 slog checkpoint 的入口点
mode=0,
seq=140)},
create_status:1}, // 租户的创建状态: CREATING = 0,CREATE_COMMIT=1,CREATE_ABORT=2,DELETING=3,DELETE_COMMIT=4
unit_min_cpu:"2.000000000000000000e+00", //最小 CPU 核数,保证提供
unit_max_cpu:"2.000000000000000000e+00", //最大 CPU 核数,限制上限
slice:"0.000000000000000000e+00",
slice_remain:"0.000000000000000000e+00",
token_cnt:8, //调度器分配的 Token 数,一个 Token 会转换为一个工作线程
sug_token_cnt:8,
ass_token_cnt:8, //租户当前确认的 Token 数(根据 token_cnt 确认,一般两者相等)
lq_tokens:2, //Large Query Token 个数,根据 token_cnt 乘以大请求比例设置
used_lq_tokens:0, //当前持有 LQ Token 的 Worker 数
stopped:false, //租户 Unit 是否正在删除
idle_us:6076629, //一轮(10秒)中工作线程空闲的总时间和, 所谓空闲实际只统计了等待队列的时间
recv_hp_rpc_cnt:1, //租户累计收到不同级别的 RPC 请求数,hp(High), np(Normal), lp(Low)
recv_np_rpc_cnt:5,
recv_lp_rpc_cnt:0,
recv_mysql_cnt:24, //租户累计收到的 mysql 请求数
recv_task_cnt:1, //租户累计收到的内部任务数
recv_large_req_cnt:0, //租户累计预判的大请求数,只会递增,不会清零。实际是重试的时候递增的。
tt_large_quries:0, //租户累计处理的大请求数, 只会递增,不会清零。实际是打点 check 的时候递增的。
pop_normal_cnt:1024555,
actives:8, //活跃工作线程数:租户活跃工作线程数 + 大查询挂起的线程数,一般和 workers 相等,它们的差为:租户非活跃并等待延迟回收的线程。
workers:8, //租户持有的工作线程数,租户活跃工作线程数 + 大查询挂起的线程数 + 租户非活跃并等待延迟回收的线程。
nesting workers:7, //租户持有的嵌套请求专用线程数,共 7 个线程对应 7 个嵌套层级。
lq waiting workers:0, //处于被判定为处理大查询,并被挂起等待调度的工作线程
req_queue:total_size=0 queue[0]=0 queue[1]=0 queue[2]=0 queue[3]=0 queue[4]=0 queue[5]=0 , //不同优先级的工作队列,数字越小优先级越高
large queued:0, //当前预判出的大请求个数
multi_level_queue:total_size=0 queue[0]=0 queue[1]=0 queue[2]=0 queue[3]=0 queue[4]=0 queue[5]=0 queue[6]=0 queue[7]=0, queue[8]=0, queue[9]=0, //存放嵌套请求的工作队列,1~7对应7个嵌套层级(queue[5]同时也存放 innersql 请求)。
recv_level_rpc_cnt:cnt[0]=0 cnt[1]=0 cnt[2]=0 cnt[3]=0 cnt[4]=0 cnt[5]=0 cnt[6]=0 cnt[7]=0 cnt[8]=0 cnt[9]=0, //每个嵌套层级累计收到的请求数
group_map:group_id = 1, //Group_map 后面跟着每个 group_id 对应 Group 的线程和排队情况
queue_size = 0, //Group 队列排队情况
recv_req_cnt = 13526, //Group 累计 Push 到队列请求数
pop_req_cnt = 13526, //Group 线程累计 Pop 出的请求数
token_cnt = 2, //Group 分配的 Token 数,一个 Token 会转换为一个工作线程
min_token_cnt = 2,
max_token_cnt = 2,
ass_token_cnt = 2 , //Group 当前确认的 Token 数(根据 token_cnt 确认,一般两者相等)
rpc_stat_info: pcode=0x150a:cnt=1489 pcode=0x150b:cnt=1091}) //租户一段时间内收到的最多的 RPC pcode,统计周期 10 秒,最多打印 Top 5




