---
title: "租户间的资源隔离 | OceanBase 文档中心"
description: 租户间的资源隔离 OceanBase 数据库是多租户的数据库系统，为了确保租户间不出现资源争抢保障业务稳定运行， OceanBase 数据库针对租户间的资源进行了隔离。 OceanBase 数据库中把 Unit 当作给租户分配资源的基本单位，一个 Unit 可以类比于一个 Docker 容器。一个 OBServer …
---
切换语言

- 中文站 - 简体中文
- International - English
- 日本站 - 日本語

文档反馈![](https://mdn.alipayobjects.com/huamei_22khvb/afts/img/A*P8CuR4UJ_FkAAAAAAAAAAAAADiGDAQ/original) OceanBase 数据库分布式版 - V 3.1.4 社区版

# 租户间的资源隔离

更新时间：2023-08-03 17:20:32

[编辑](https://github.com/oceanbase/oceanbase-doc/edit/V3.1.4/zh-CN/1500.oceanbase-database-overview/500.multi-tenant-architecture/500.tenant-and-resource-management/200.multi-tenant-resource-isolation.md)  

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 或虚拟机的实现不便于暴露统一视图。

## 普通用户的视角看隔离效果

从普通用户的角度，我们可以看到的隔离效果如下：

- 内存完全隔离。

  具体包括：

     - SQL 执行过程各种算子使用的内存是分离的，一个租户的内存耗尽不会影响到另一个租户。

      SQL 执行过程的各种算子的详细信息请参见 [用户接口和查询语言](https://www.oceanbase.com/docs/community-observer-cn-10000000000450636) 章节。
     - Block Cache 和 MEMTable 是分离的，一个租户的内存耗尽不会影响到另一个租户的写入和读取。

      Block Cache 的详细介绍信息请参见 [多级缓存](https://www.oceanbase.com/docs/community-observer-cn-10000000000449627)。

      MEMTable 的详细介绍信息请参见 [MEMTable](https://www.oceanbase.com/docs/community-observer-cn-10000000000449403)。
 - CPU 通过用户态调度实现隔离。

  一个租户能使用的 CPU 资源是由 Unit 规格决定的，不过 OBServer 目前是允许租户 CPU 超卖的。
 - 大部分数据结构是分离的。

  具体包括：

     - SQL 的 Plan Cache 是租户分离的，一个租户的 Plan Cache 淘汰不会影响另一个租户。
     - SQL 的 Audit 表是分离的，一个租户的 QPS 太高，不会冲洗掉另一个租户的 Audit 信息。
 - 事务相关的数据结构是分离的。

  具体包括：

     - 一个租户的行锁挂起，不会影响到其他租户。
     - 一个租户的事务挂起，不会影响到其他租户。
     - 一个租户的回放出问题，不会影响到其它租户。
 - Clog 是共享的。

  一个 OBServer 上的不同租户共享 Clog 文件，这个设计主要是为了让事务的 Group Commit 能有更好的效果。

## 资源分类

从设计上，一个 Unit 可以指定 CPU、Memory、IOPS、Disk Size、Session Number 这 5 种资源的下限和上限。

```sql
obclient> CREATE RESOURCE UNIT box1 MIN_CPU 4, MAX_CPU 4, MIN_MEMORY 34359738368, MAX_MEMORY 34359738368, min_iops 128, MAX_IOPS 128, MIN_DISK_SIZE '5G', MAX_DISK_SIZE '5G', MIN_SESSION_NUM 64, MAX_SESSION_NUM 64;

```

目前 IOPS、Disk Size、Session Number 这三种资源是没有被管理的，即目前的资源隔离主要考虑的是CPU 和 Memory。

### OBServer 的可用 CPU

OBServer 在启动时会探测物理机或容器的在线 CPU 个数，如果 OBServer 探测得不准确（例如，在容器化环境里)，您也可以通过 `cpu_count` 配置项来指定。

由于 OBServer 会为后台线程预留两个 CPU，故实际可以分给租户的 CPU 总数会少两个。

### OBServer 的可用 Memory

OBServer 在启动时会探测物理机或容器的内存，由于 OBServer 需要为其它进程预留一部分内存，故 observer 进程的可用内存等于 `物理内存 * memory_limit_percentage`。您也可以通过配置项 `memory_limit` 直接配置 observer 进程可用的总内存大小。

observer 进程可用的内存需要进一步扣除掉内部共用模块的那一部分，这部分内存大小由配置项 `system_memory` 指定，剩下的内存才是租户可用的总内存。

关于内存配置的更多信息，请参见 [内存管理](https://www.oceanbase.com/docs/community-observer-cn-10000000000449462) 章节。

### 查看每个 OBServer 的可用资源

您可以通过 `oceanbase.__all_virtual_server_stat` 表查看每个 OBServer 的可用资源，示例如下：

```sql
obclient> SELECT * FROM __all_virtual_server_stat \G
*************************** TODO 1. row ***************************
               svr_ip: 10.10.10.1
             svr_port: 20900
                 zone: z1
            cpu_total: 14
         cpu_assigned: 2.5
 cpu_assigned_percent: 17
            mem_total: 53687091200
         mem_assigned: 12884901888
 mem_assigned_percent: 24
           disk_total: 10737418240
        disk_assigned: 10737418240
disk_assigned_percent: 100
             unit_num: 1
   migrating_unit_num: 0
       merged_version: 1
         leader_count: 1283
                 load: 0.21379327157484151
           cpu_weight: 0.4266211604095563
        memory_weight: 0.5733788395904437
          disk_weight: 0
                   id: 1
           inner_port: 20901
        build_version: 3.2.1_1-3c4c42fc31ba322cd7dfd441a8a71f648835eced(Sep  1 2021 16:16:06)
        register_time: 0
  last_heartbeat_time: 1631074048327156
block_migrate_in_time: 0
   start_service_time: 1631073245084853
    last_offline_time: 0
            stop_time: 0
 force_stop_heartbeat: 0
         admin_status: NORMAL
     heartbeat_status: alive
      with_rootserver: 1
       with_partition: 1
           mem_in_use: 0
          disk_in_use: 12582912
      clock_deviation: -11
    heartbeat_latency: 182
    clock_sync_status: SYNC
         cpu_capacity: 14
     cpu_max_assigned: 5
         mem_capacity: 53687091200
     mem_max_assigned: 16106127360
 ssl_key_expired_time: 0

```

## 资源隔离

### CPU 和内存的超卖

如果一个 OBServer 上放置的所有 Unit 的 `MAX_CPU` 的值加起来超过这个 OBServer 的可用 CPU 总数，则表示该 OBServer 上的 CPU 是超卖的。同理，如果所有 Unit 的 `MAX_MEMORY` 加起来超过该 OBServer 可以分配的 Memory，则表示该 OBServer 上的 Memory 是超卖的。

OBServer 资源超卖的比例受配置项 `resource_hard_limit` 的控制，假设 `resource_hard_limit=200`，那就意味着可以超卖成 2 倍，即 16 个 CPU 可以超卖成 32 个，16G 的内存可以超卖成 32G。

超卖是通过牺牲稳定性来获取更高的资源利用率的方案，是否要开启超卖需要根据应用特性和应用的 SLA 要求仔细评估。例如，一个比较适合的场景就是业务的研发测试环境。

注意，即使在分配资源的时候没有超卖，在实现 CPU 资源隔离时，Unit 可以使用的 CPU 并没有严格限制在 `MAX_CPU` 的值, 这个意义上的 CPU 默认是超卖的。更多 CPU 超卖的介绍信息可参见本文中的 **CPU 隔离** 。

### 内存隔离

内存是个刚性的资源，也就是内存一旦被占用了，很难保证能快速收回。所以内存是不适合超卖的。

Unit 的内存上限由 `MAX_MEMORY` 决定，Unit 的 `MIN_MEMORY` 仅用来决定 Block Cache 的挤占时机，除此之外没有其它用途。

生产系统中推荐将 `MIN_MEMORY` 和 `MAX_MEMORY` 设置为相等的值。

### CPU隔离

相比于内存，CPU 是更弹性的资源，所以 OBServer 目前允许一个 Unit 使用的物理 CPU 超过分配给它的配额。

在 OceanBase 数据库 V3.1.x 版本之前，主要通过控制线程数来控制 CPU 的占用；在 OceanBase 数据库 V3.1.x 及之后的版本，允许配置 Cgroup 来控制 CPU 的占用。

#### 线程分类

OBServer 会启动很多不同功能的线程，细化的分类请参考ObServer线程的相关章节，本节按照最粗略的标准可以分为以下两类：

- 一类是处理 SQL 和事务提交的线程，统称为 Worker 线程。
 - 其余的是处理网络 IO、磁盘 IO、Compaction 以及定时任务的线程。

Worker 线程是分租户的，非 Worker 线程是所有租户共享的。本节的租户 CPU 隔离都是针对 Worker线程的。

#### 基于线程数的 CPU 隔离

Unit 的 CPU 隔离是通过一个 Unit 的活跃 Worker 线程数实现的。

由于 SQL 执行过程中可能会有 IO 等待、锁等待等，所以一个线程无法用满一个物理 CPU，故在缺省配置下，OBServer 会给每个 CPU 启动 4 个线程，4 这个倍数可以通过配置 `cpu_quota_concurrency` 来控制。这就意味着如果一个 Unit 的 `MAX_CPU` 是 10，那么它能同时运行的活跃线程是 40。

#### 基于 Cgroup 的 CPU 隔离

开启 Cgroup 后最大的变化是不同租户的 Worker 线程放到不同的 Cgroup 目录内，租户间的 CPU 隔离效果会更好。最后的隔离效果如下：

- 如果一个 OBServer 上只有一个租户负载很高，其余租户比较空闲，那么这个负载高的租户的 CPU 可以超过 `MAX_CPU` 的限制，超卖倍数仍然受 `cpu_quota_concurrency` 控制，这个行为主要是为了兼容。
 - 延续上面的场景，如果有多个空闲的租户的负载上升了，导致物理 CPU 不够了，Cgroup 会按照权重分配时间片。

#### 大查询处理

我们认为相比于大查询，让短查询尽快返回对用户更有意义，即大查询的查询优先级更低，当大查询和短查询同时争抢 CPU 时，系统会限制大查询的 CPU 使用。

当一个线程执行的 SQL 查询耗时太长，这条查询就会被判定为大查询, 一旦判定为大查询，执行大查询的 Worker 会等在一个 Pthread Condition 上，这样就为其它的 Worker 线程让出了 CPU。

具体实现上，OBServer 在代码中插入了很多检查点，Worker 线程在运行过程中会通过检查点定期检查自己的状态，如果判断应该挂起，那么线程就会等待在一个 Pthread Condition 上，等到合适的时机再被唤醒。

如果同时有大查询和小查询，大查询最多占用 30% 的 Worker 线程，30% 这个百分比值可以通过配置项 `large_query_worker_percentage` 来设置。

有两点需要说明：

- 当没有小查询的时候，大查询可以用到 100% 的 Worker 线程。只有当同时有大查询和小查询时，30% 的比例才生效。
 - 一个 Worker 因为执行大查询被挂起时，作为补偿，系统可能会新创建一个 Worker 线程，但是总的 Worker 线程不能超过 `MAX_CPU` 的 10 倍，10 这个倍数可以通过配置项 `workers_per_cpu_quota` 来设置。

#### 提前识别大查询

由于 OBServer 挂起一个大查询线程，就会启动一个新的 Worker 线程, 但是如果有大量大查询涌入，OBServer 新创建的线程还是被用来处理大查询，很快达到 Worker 数上限，在这批大查询消耗完之前就没有机会再处理短查询了。

为了优化这个场景，OBServer 会在 SQL 开始执行之前预判它是不是大查询，预判的本质就是估计 SQL 的执行时间。预判主要依据以下假设场景：如果两条 SQL 的执行 Plan 是一样的，可以猜测它们的执行时间也是相似的，这样就可以用 Plan 最近的执行时间来判断 SQL 会不会是大查询。

如果某条 SQL 被预判为大查询，那么该查询就会被放入一个特殊的大查询队列，其 Worker 线程会被释放，系统就会接着执行后面的请求了。

### 监控项和日志

在日志中搜索 `dump tenant info` 关键字，可看到租户的规格、线程、队列及请求统计等信息，且这条日志每个租户每 10s 打印一次。

日志示例：

```shell
grep 'dump tenant info.*tenant={id:1002' log/observer.log.*[2021-05-10 16:56:22.564978] INFO  [SERVER.OMT] ob_multi_tenant.cpp:803 [48820][2116][Y0-0000000000000000] [lt=5] dump tenant info(tenant={id:1002, compat_mode:1, unit_min_cpu:"1.000000000000000000e+01", unit_max_cpu:"1.500000000000000000e+01", slice:"0.000000000000000000e+00", slice_remain:"0.000000000000000000e+00", token_cnt:30, ass_token_cnt:30, lq_tokens:3, used_lq_tokens:3, stopped:false, idle_us:4945506, recv_hp_rpc_cnt:2420622, recv_np_rpc_cnt:7523808, recv_lp_rpc_cnt:0, recv_mysql_cnt:4561007, recv_task_cnt:337865, recv_large_req_cnt:1272, tt_large_quries:3648648, actives:35, workers:35, nesting workers:7, lq waiting workers:5, req_queue:total_size=48183 queue[0]=47888 queue[1]=0 queue[2]=242 queue[3]=5 queue[4]=48 queue[5]=0 , large queued:12, 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 , recv_level_rpc_cnt:cnt[0]=0 cnt[1]=0 cnt[2]=0 cnt[3]=0 cnt[4]=0 cnt[5]=165652 cnt[6]=10 cnt[7]=0 })

```

示例日志中部分字段的说明如下表所示。

| 字段 | 说明 |
| --- | --- |
| Id | 租户 ID。 |
| unit_min_cpu | 表示最小 CPU 核数，即保证能提供的 CPU 核数。 |
| unit_max_cpu | 表示最大 CPU 核数，即限制上限。 |
| slice | 无实际意义。 |
| slice_remain | 无实际意义。 |
| token_cnt | 表示调度器分配的 Token 数，一个 Token 会转换为一个工作线程。 |
| ass_token_cnt | 租户当前确认的 Token 数，可以根据 `token_cnt` 来确认，一般两者的值相等。 |
| lq_tokens | 大请求的 Token 个数，根据 `token_cnt` 的值乘以大请求的比例设置。 |
| used_lq_tokens | 当前持有大查询 Token 的 Worker 数。 |
| stopped | 表示租户的 Unit 是否正在删除。 |
| idle_us | 一轮（10秒）中工作线程空闲的总时间和，所谓空闲实际只统计了等待队列的时间。 |
| recv_hp/np/lp_rpc_cnt | 租户累计收到不同级别（hp(High)、np(Normal)、lp(Low)）的 RPC 请求数。 |
| recv_mysql_cnt | 租户累计收到的 MySQL 请求数。 |
| recv_task_cnt | 租户累计收到的内部任务数。 |
| recv_large_req_cnt | 租户累计预判的大请求数，只会递增，不会清零。实际是重试时是递增的。 |
| tt_large_quries | 租户累计处理的大请求数, 只会递增，不会清零。实际是打点 Check 的时候递增的。 |
| actives | 活跃工作线程数，一般和 Worker 数相等，它们的差包含：租户工作线程缓存+带工作线程的大请求缓存。 |
| workers | 租户持有的工作线程数。 |
| nesting workers | 租户持有的嵌套请求专用线程数，共 7 个线程对应 7 个嵌套层级。 |
| lq waiting workers | 处于等待调度的工作线程。 |
| req_queue | 不同优先级的工作队列，数字越小优先级越高。 |
| large queued | 当前预判出的大请求个数。 |
| multi_level_queue | 存放嵌套请求的工作队列，1～7 对应 7 个嵌套层级，其中，`queue[0]` 暂时不用。 |

 上一篇 下一篇 ![有帮助](https://gw.alipayobjects.com/mdn/ob_asset/afts/img/A*y6ocSqN8cqsAAAAAAAAAAAAAARQnAQ)![无帮助](https://gw.alipayobjects.com/mdn/ob_asset/afts/img/A*BG9IQJyLHF8AAAAAAAAAAAAAARQnAQ)![反馈](https://gw.alipayobjects.com/mdn/ob_asset/afts/img/A*eTWdQKCRKHwAAAAAAAAAAAAAARQnAQ)[AI](https://www.oceanbase.com/obi) 咨询热线
