数据库基础体系 · 第 57/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

OceanBase 分布式数据库:分区、副本、事务、高可用和运维

OceanBase 是面向分布式部署的关系型数据库。它保留了 SQL、事务、索引和约束等关系数据库能力,同时把数据分布、日志复制、故障转移和资源隔离纳入数据库内核。

理解 OceanBase,不能只把它看成“带分片功能的 MySQL”或“多个数据库拼接而成的集群”。它的核心关系是:

租户(Tenant)
  └── 日志流(Log Stream,LS)
        └── Tablet
              └── 分区(Partition)中的物理数据

在此基础上:

  • 分区决定数据如何切分以及请求如何定位;
  • 副本决定数据如何复制以及节点故障时能否继续服务;
  • 事务决定跨分区、跨日志流修改的原子性和隔离性;
  • 高可用决定副本如何选主、故障如何切换,以及 RPO/RTO 的边界;
  • 运维负责验证这些状态是否真的符合预期,而不是只检查“进程是否存活”。

本文以 OceanBase 稳定版本公开语义为准。OceanBase 同时支持 MySQL 租户和 Oracle 租户,部分 SQL、系统视图和参数会随租户模式、版本及部署方式变化,示例会明确适用边界。


一、先建立整体模型:集群、租户、Zone、Server、LS 和 Tablet

1. 集群不是单一数据库实例

一个 OceanBase 集群通常包含多个 Zone。Zone 通常对应一个可独立故障的机房、可用区或故障域;每个 Zone 中有一个或多个 Server,即实际运行 OceanBase observer 进程的服务器。

可以抽象为:

Cluster
├── Zone A
│   ├── Server A1
│   └── Server A2
├── Zone B
│   ├── Server B1
│   └── Server B2
└── Zone C
    ├── Server C1
    └── Server C2

OceanBase 集群还可以划分出多个 Tenant。租户不是简单的数据库名称,而是一个逻辑上的数据库服务单元,通常具有:

  • 独立的 SQL 访问入口;
  • 独立的资源规格;
  • 独立的系统变量和部分参数;
  • 独立的表、分区和事务空间;
  • 与其他租户隔离的 CPU、内存、磁盘 I/O 等资源。

同一集群中的租户可以用于承载不同业务,也可以用于隔离不同环境。

2. 资源单元和资源池

租户的资源通常通过以下抽象管理:

  • Resource Unit:资源单元,描述 CPU、内存等规格;
  • Resource Pool:资源池,把资源单元放置到若干 Server;
  • Tenant:使用资源池运行的租户。

一个租户可能拥有多个资源单元,分别部署在不同 Server 上。资源单元的数量和位置会影响租户可以承载的副本、并发和故障能力。

因此,扩容不能只理解成“给某台机器增加 CPU”。至少要区分:

  1. 集群是否新增了 Server;
  2. 租户是否获得了新的资源单元;
  3. 数据副本是否重新分布;
  4. LS 和 Tablet 是否发生迁移;
  5. 连接路由是否能够感知新的服务位置。

3. Log Stream:复制和故障恢复的基本单元

Log Stream,简称 LS,日志流,是 OceanBase 中承载一组 Tablet、并以日志复制方式保持一致性的物理组织单元。

可以把 LS 理解为:

一个 LS
├── 一组 Tablet
├── 事务日志
├── 复制状态
├── Leader/Follower 角色
└── 在多个 Zone 上的副本

LS 不是 SQL 层的分区名称,也不是用户直接查询的表。用户看到的是表、分区和索引;数据库内部会把相关物理数据组织到 Tablet 和 LS 中。

LS 的作用很重要:

  • 日志复制通常围绕 LS 进行;
  • 选主和副本状态与 LS 有关;
  • LS 的 Leader 所在位置会影响读写路由;
  • 故障恢复、迁移和负载均衡需要处理 LS 与 Tablet 的状态。

4. Tablet:分区的物理数据单元

Tablet 是 OceanBase 中较小的物理数据组织单元。一个表分区通常对应一个或多个 Tablet,索引也会形成相应的物理数据组织。

不要把以下概念混为一谈:

概念 主要含义
SQL 层对象
分区 SQL 层对表数据的逻辑切分
Tablet 物理存储和调度单元
LS 承载一组 Tablet 并进行日志复制的单元
Replica Tablet/LS 数据在不同 Server 或 Zone 上的副本

用户创建一个分区,并不意味着数据库只生成一个永远不变的物理文件。随着合并、迁移、分裂、负载均衡和版本演进,物理组织可能变化,但 SQL 层的数据语义必须保持不变。


二、分区:把一个逻辑表切成可定位、可迁移的数据范围

1. 为什么需要分区

单表数据量增长后,所有数据集中在一个物理单元会造成几个问题:

  • 单个 Tablet 过大,迁移、合并和恢复成本高;
  • 范围查询和分区裁剪无法发挥作用;
  • 热点数据与冷数据难以隔离;
  • 数据无法均匀分布到多个 LS 和 Server;
  • 扩容时可迁移的数据粒度过粗。

分区的目标不是“让每条 SQL 都更快”,而是让数据具备可定位、可管理、可分布的边界。

2. Hash 分区

Hash 分区根据分区键计算哈希值,再映射到固定数量的分区。

CREATE TABLE orders (
    order_id    BIGINT NOT NULL,
    user_id     BIGINT NOT NULL,
    order_time  DATETIME NOT NULL,
    amount      DECIMAL(18, 2) NOT NULL,
    status      VARCHAR(32) NOT NULL,
    PRIMARY KEY (order_id)
)
PARTITION BY HASH(user_id)
PARTITIONS 8;

直观上可以表示为:

partition_id = hash(user_id) mod 8

例如,假设经过哈希后:

user_id = 101  -> partition 3
user_id = 205  -> partition 6
user_id = 309  -> partition 3

user_id=101 的数据会进入第 3 个分区,user_id=205 的数据进入第 6 个分区。

Hash 分区适合:

  • 按用户、账户、设备等高基数字段均匀分布;
  • 点查和等值查询较多;
  • 希望避免某个连续时间范围集中在一个分区。

但它不适合直接支持按时间删除历史数据。因为一个时间范围会散落在多个 Hash 分区中,执行:

DELETE FROM orders
WHERE order_time < '2024-01-01';

可能仍需扫描多个分区,不能像按时间范围分区那样直接删除一个旧分区。

3. Range 分区

Range 分区根据键值范围划分数据,常用于时间序列和生命周期管理。

CREATE TABLE event_log (
    event_id    BIGINT NOT NULL,
    event_time  DATETIME NOT NULL,
    payload     VARCHAR(1024),
    PRIMARY KEY (event_id, event_time)
)
PARTITION BY RANGE COLUMNS(event_time) (
    PARTITION p202401 VALUES LESS THAN ('2024-02-01'),
    PARTITION p202402 VALUES LESS THAN ('2024-03-01'),
    PARTITION pmax    VALUES LESS THAN (MAXVALUE)
);

这组边界的含义是:

event_time < 2024-02-01       -> p202401
2024-02-01 <= event_time < 2024-03-01 -> p202402
event_time >= 2024-03-01      -> pmax

分区边界有两个容易忽略的性质:

  1. VALUES LESS THAN 的上界通常是开区间
  2. MAXVALUE 会兜底接收未被前面范围覆盖的数据。

例如:

INSERT INTO event_log(event_id, event_time, payload)
VALUES (1, '2024-02-15', 'x');

该行进入 p202402,而不是 p202401

Range 分区适合:

  • 按日期查询;
  • 按时间归档;
  • 按时间删除历史数据;
  • 通过分区裁剪减少扫描范围。

但是它存在明显的热点风险。若业务持续写入当前时间,所有写入都会集中在当前分区。分区数量多并不能自动消除热点,分区键设计和写入模式必须同时考虑。

4. List 分区

List 分区按枚举值划分数据。例如按区域编码分区:

CREATE TABLE customer (
    customer_id BIGINT NOT NULL,
    region_code VARCHAR(16) NOT NULL,
    name        VARCHAR(128),
    PRIMARY KEY (customer_id)
)
PARTITION BY LIST COLUMNS(region_code) (
    PARTITION p_east VALUES IN ('EAST'),
    PARTITION p_west VALUES IN ('WEST'),
    PARTITION p_other VALUES IN ('NORTH', 'SOUTH')
);

List 分区适合值域明确、业务含义清晰的分类字段,但要注意新业务值是否有对应分区。如果没有 DEFAULT 或兜底分区,插入新值可能失败。

5. 二级分区

二级分区把两种分区策略组合起来,例如先按业务租户 Hash,再按月份 Range:

一级分区:tenant_id Hash
二级分区:event_time Range

这种设计可以同时获得:

  • 不同租户之间的均匀分布;
  • 单个租户内部按时间管理;
  • 对时间条件进行分区裁剪。

但分区数量会相乘。若一级有 16 个分区、二级每个有 24 个时间分区,则逻辑分区数为:

16 × 24 = 384

分区数量增加会带来:

  • 元数据数量增加;
  • 优化器枚举和裁剪成本增加;
  • 任务调度、合并和迁移对象增加;
  • DDL 管理复杂度增加。

因此,分区数量不是越多越好。需要同时估算数据量、查询模式、写入热点、生命周期操作和运维窗口。

6. 分区裁剪为什么成立

假设表按 user_id Hash 分区:

SELECT *
FROM orders
WHERE user_id = 101;

优化器可以根据分区表达式推导出目标分区:

hash(101) mod 8 = 3

于是只访问第 3 个分区,这叫分区裁剪

如果查询没有分区键条件:

SELECT *
FROM orders
WHERE status = 'PAID';

优化器通常无法只凭 status 推导唯一分区,因此可能访问多个分区,再汇总结果。

这说明分区键必须与访问路径匹配。把一个字段设为分区键,并不会自动让所有查询受益。


三、副本:数据复制、投票和读服务不是同一个概念

1. 副本解决什么问题

分区解决“数据如何切开”,副本解决“切开的数据如何保存多份”。

一个数据单元至少可能有:

Leader replica
Follower replica
Follower replica

副本一般放在不同 Server,生产环境还应尽量放在不同 Zone。否则同一机架、同一主机或同一可用区故障时,多份副本可能同时不可用。

副本的核心目标包括:

  • 节点故障后仍能访问数据;
  • Leader 故障后能够重新选主;
  • 通过多数派确认提交日志;
  • 在不丢失已确认数据的前提下恢复服务。

2. Paxos 多数派条件

OceanBase 的高可用复制依赖多数派一致性机制。对一个由 N 个投票副本构成的复制组,法定多数派大小为:

majority = floor(N / 2) + 1

如果要容忍 f 个副本故障,至少需要:

N >= 2f + 1

例如:

3 个投票副本 -> 多数派为 2 -> 可容忍 1 个副本故障
5 个投票副本 -> 多数派为 3 -> 可容忍 2 个副本故障

假设 3 个副本分别位于 Zone A、B、C:

Zone A: Leader
Zone B: Follower
Zone C: Follower

一次日志提交至少需要 Leader 获得另一个投票副本确认。若 Zone A 故障,Zone B 和 Zone C 仍然构成多数派,可以选出新的 Leader。

但如果副本都在 Zone A:

Zone A: 3 个副本
Zone B: 0
Zone C: 0

那么它虽然有 3 份副本,却不能容忍 Zone A 整体故障。这就是“副本数量”和“故障域冗余”不同的原因。

3. Full、Read-Only、Log-Only 副本

OceanBase 支持不同类型的副本。不同版本和部署形态对副本类型的可用能力、投票属性和读服务行为可能存在差异,不能仅凭名称推断。

从语义上可以这样理解:

  • Full replica:保存完整数据和日志,通常参与一致性复制,是常规生产高可用的主要副本类型;
  • Read-only replica:主要用于只读访问或特定读流量隔离,通常不等同于可替代 Full replica 的投票副本;
  • Log-only replica:保存日志,不承担完整数据读取,主要用于增强日志冗余和灾备能力。

因此,“配置了三个副本”还不够,必须继续确认:

  1. 三个副本分别是什么类型;
  2. 哪些副本参与多数派;
  3. 哪些副本可以承载读请求;
  4. 副本是否跨 Zone;
  5. 副本之间的同步状态是否正常。

4. 同步复制不等于所有请求立即可读

Leader 提交事务时,需要满足一致性协议的提交条件。Follower 接收日志后,还要完成重放和数据版本构建。

于是可能出现:

日志已复制到 Follower
但 Follower 尚未完成重放

或者:

Follower 已经可读
但普通弱一致读暂时看不到最新提交

读请求还会受到一致性级别影响。通常要区分:

  • 强一致读:要求读取满足更严格的最新版本约束;
  • 弱一致读:允许读取副本的较新但未必最新的数据,以换取更低延迟或更灵活的读路由。

这不是“副本是否同步”的二元问题,而是:

日志复制进度
+ 数据重放进度
+ 读一致性要求
+ 路由策略

共同决定读到什么。


四、事务:本地事务和分布式事务的边界

1. 事务的基本目标

事务需要满足原子性、一致性、隔离性和持久性:

  • 原子性:事务内的修改要么全部生效,要么全部不生效;
  • 一致性:提交后数据满足约束和数据库规则;
  • 隔离性:并发事务之间按照隔离级别观察彼此;
  • 持久性:提交成功后,已确认的数据不会因单节点故障消失。

在 OceanBase 中,事务边界仍由 SQL 连接和 COMMITROLLBACK 等语句控制,但事务内部可能涉及多个分区、LS 和副本。

2. 本地事务

如果一个事务涉及的数据都位于同一个本地事务参与范围内,协调过程相对简单。例如:

START TRANSACTION;

UPDATE orders
SET status = 'PAID'
WHERE order_id = 1001;

INSERT INTO order_audit(order_id, action)
VALUES (1001, 'PAY');

COMMIT;

这段事务是否是“本地事务”,不能只根据 SQL 文本判断,还取决于:

  • 两张表的目标分区;
  • 两条记录是否位于同一 LS;
  • 索引维护是否引入其他参与者;
  • 事务执行计划和内部组织方式。

因此,不能把“只写一张表”简单等同于本地事务,也不能把“写两张表”简单等同于跨节点事务。

3. 分布式事务和两阶段提交

如果一个事务修改多个 LS,系统需要协调多个参与者。典型流程可以抽象为两阶段提交:

协调者                    参与者 P1       参与者 P2

开始事务
  |                         |              |
  |---- 执行修改 ----------->|              |
  |--------------------------|---- 执行修改->|
  |
  |---- PREPARE ------------>|              |
  |--------------------------|---- PREPARE ->|
  |<--- READY ---------------|              |
  |<-------------------------|---- READY ---|
  |
  |---- COMMIT -------------->|              |
  |---------------------------|---- COMMIT ->|

具体实现内部还涉及事务状态、日志、提交版本和恢复机制,但理解上应抓住两个关键点:

  1. PREPARE 阶段确认参与者已经准备好提交;
  2. COMMIT 阶段让所有参与者最终落入提交状态。

如果参与者在 PREPARE 后暂时失联,协调者不能简单地把事务当作“没发生过”。它必须依据持久化的事务状态和日志进行恢复,否则可能破坏原子性。

4. 分布式事务的完整算例

假设有两个分区:

orders:按 user_id Hash 分区
accounts:按 account_id Hash 分区

一次支付事务同时修改订单和账户:

START TRANSACTION;

UPDATE accounts
SET balance = balance - 100
WHERE account_id = 7
  AND balance >= 100;

UPDATE orders
SET status = 'PAID'
WHERE order_id = 1001
  AND user_id = 42;

COMMIT;

假设:

account_id=7  -> LS-A
user_id=42    -> LS-B

那么事务至少有两个参与者:

LS-A:账户余额修改
LS-B:订单状态修改

可能的状态变化如下:

T0:事务开始
    LS-A、LS-B 都没有提交

T1:执行 UPDATE
    LS-A 产生账户修改
    LS-B 产生订单修改

T2:PREPARE
    LS-A 持久化“已准备”
    LS-B 持久化“已准备”

T3:COMMIT
    协调者记录提交决定
    LS-A、LS-B 都应用提交结果

T4:事务完成
    账户扣款和订单状态同时对外生效

如果 LS-A 在 T2 后暂时不可访问,系统不能安全地单方面提交 LS-B。恢复过程会依据事务日志和提交决定继续完成或回滚,从而避免:

账户已经扣款,但订单仍然是未支付

当然,分布式事务的代价也是真实存在的:

  • 参与者越多,协调和日志交互越多;
  • 锁或事务状态持续时间可能更长;
  • 网络抖动更容易放大为事务延迟;
  • 事务失败时的诊断比单分区事务复杂。

5. 隔离级别和快照

OceanBase 支持常见关系数据库事务隔离语义,实际默认值和可用语法应以当前租户模式和版本为准。可以先查看租户的系统变量:

SHOW VARIABLES LIKE 'tx_isolation';

某些版本或模式下变量名可能不同,应以实际返回结果为准。

以常见的 READ COMMITTED 语义为例:

事务 T1:读取一行
事务 T2:提交修改
事务 T1:再次读取

第二次读取可能看到 T2 已提交的新版本。若使用更强的快照语义,则同一事务内的读取可能维持一致视图。

分布式场景下,快照不能只由单个节点的本地时间决定。多个 LS 的版本需要满足一个全局可比较的时间约束,否则会出现:

从 LS-A 读到较新的数据
从 LS-B 读到较旧的数据

这类跨分区读不一定违反单行一致性,但可能不满足业务所需的事务快照一致性。OceanBase 会通过全局时间戳、事务版本和参与者协调等机制提供分布式事务语义;具体读协议还会受强弱一致性设置影响。

6. 事务失败时客户端必须正确处理

应用不能把“网络异常”直接等同于“事务回滚”。

例如客户端执行:

COMMIT;

随后连接断开,可能存在两种情况:

情况 A:COMMIT 尚未到达数据库,事务最终回滚
情况 B:数据库已经提交,但 COMMIT 响应丢失

如果应用在两种情况下都重试扣款,可能造成重复业务操作。

正确的设计通常包括:

  • 使用业务唯一号;
  • 对扣款、订单等操作建立幂等约束;
  • 在不确定提交结果时查询事务业务状态;
  • 不用“收到网络错误”推断事务一定未提交。

五、路由:分区键、Leader 和连接入口必须配合

1. 为什么客户端不应随机连接任意节点

OceanBase 集群中,数据和 Leader 分布在多个 Server。若客户端随机连接某个 Server,数据库仍可能完成请求,但会增加:

  • 跨节点转发;
  • Leader 重定向;
  • 网络往返;
  • 故障切换时的重试复杂度。

生产环境通常使用 OceanBase Proxy(OBProxy)或其他官方支持的访问方式,让连接入口根据集群拓扑和 Leader 信息进行路由。

典型路径可以表示为:

应用
  |
  v
OBProxy
  |
  +--> 目标分区 Leader
  |
  +--> 其他分区 Leader

一个跨分区 SQL 可能由入口节点接收,再由内部执行框架访问多个参与分区。

2. 分区键为什么同时影响性能和事务成本

假设业务最常执行:

SELECT *
FROM orders
WHERE user_id = ?;

user_id 作为分区键有两个好处:

  1. 可以进行分区裁剪;
  2. 同一用户的相关数据更可能集中在稳定的访问范围内。

但是,若一个事务经常同时修改不同用户:

UPDATE orders SET status = 'PAID' WHERE user_id = 1;
UPDATE orders SET status = 'PAID' WHERE user_id = 2;

它可能涉及不同分区甚至不同 LS,从而成为分布式事务。

因此,分区键设计要同时回答:

  • 查询能否裁剪分区;
  • 写入是否均匀;
  • 事务是否会跨分区;
  • 热点是否集中;
  • 数据生命周期是否易于管理。

3. 强一致读和弱一致读的取舍

强一致读通常更适合:

  • 支付结果确认;
  • 库存扣减后的校验;
  • 需要立即读到刚提交数据的请求。

弱一致读通常可以用于:

  • 推荐结果;
  • 统计看板;
  • 对短暂延迟不敏感的列表;
  • 允许从只读副本读取的场景。

错误的做法是全局使用弱一致读,然后在业务代码中假设“刚写入的数据一定马上可读”。如果读请求被路由到尚未追平的副本,业务就可能看到旧值。


六、高可用:故障时究竟发生了什么

1. 单 Server 故障

假设三个 Zone 中各有一个 Full replica:

Zone A:Leader
Zone B:Follower
Zone C:Follower

当 Zone A 的 Server 故障时:

  1. Zone B、C 检测到 Leader 不可用;
  2. 剩余副本确认是否仍有多数派;
  3. 在满足选举条件时选出新 Leader;
  4. Proxy 或客户端更新路由;
  5. 新 Leader 继续接受写请求;
  6. 原故障 Server 恢复后重新追日志并补齐数据。

如果故障发生前已经提交的日志至少被多数派持久化,那么新 Leader 可以继续恢复这些提交结果。

2. Zone 故障

如果副本跨三个 Zone:

Zone A:1
Zone B:1
Zone C:1

丢失一个 Zone 后仍有两个副本,通常可以保持多数派。

如果只有两个 Zone:

Zone A:1
Zone B:1

其中一个 Zone 故障后只剩一个副本。它可能仍然能够提供部分读服务,但不能在没有多数派的情况下安全地继续推进需要一致性确认的写入。

这不是 OceanBase 特有的限制,而是多数派协议的基本约束:

2 个副本中丢失 1 个:
剩余 1 < majority(2)=2

因此,两副本部署不能等同于三副本部署的容灾能力。

3. RPO 和 RTO

  • RPO:故障后可能丢失的数据时间范围;
  • RTO:从故障发生到服务恢复所需时间。

在多数派同步复制并且提交成功的前提下,已确认提交的数据通常不会因为单副本故障丢失,RPO 可以接近零。但这不意味着所有故障都 RPO=0:

  • 整个多数派故障;
  • 跨地域复制链路中断;
  • 灾备副本是异步复制;
  • 业务使用了异步写入或外部缓存;
  • 应用在提交结果未知时重复操作。

RTO 则取决于:

  • 故障检测时间;
  • 选主时间;
  • 副本追平时间;
  • Proxy 路由刷新时间;
  • 连接池重连和请求重试;
  • 长事务和未完成事务的清理。

所以,“有副本”只能说明具备恢复基础,不能直接推出固定 RTO。

4. 选主期间的写请求表现

Leader 发生切换时,常见表现包括:

  • 短时间连接失败;
  • 请求收到重定向或暂时不可服务错误;
  • 事务提交结果不确定;
  • 长连接需要重新建立;
  • 连接池继续把请求发往旧节点。

应用需要对可重试错误进行分类处理,而不是无条件重试所有写请求。尤其是事务提交阶段,必须结合业务幂等设计。

5. 副本不健康时系统会怎样

如果某个 Follower 长时间落后:

  • 复制延迟可能增加;
  • 可用副本数可能下降;
  • 自动负载均衡可能受到影响;
  • 某些故障发生后无法立即恢复到完整冗余;
  • 读请求若被路由到该副本,可能产生旧读或不可服务。

高可用不是“当前还能查询”这么简单,还要检查冗余是否完整、日志是否追平、Leader 是否均衡。


七、部署边界:三副本、跨 Zone 和资源规划

1. 三副本部署的基本含义

一个常见生产拓扑是:

Zone A:Full replica
Zone B:Full replica
Zone C:Full replica

它的逻辑目标是:

  • 任意一个 Zone 故障后仍有多数派;
  • Leader 可以在剩余 Zone 中重新选出;
  • 数据和日志仍然存在于多个故障域。

但这只是副本层的条件。还必须检查:

  • 每个 Zone 是否有足够 CPU 和内存;
  • 租户资源单元是否真的分布到不同 Zone;
  • 磁盘是否有足够空间;
  • 网络延迟是否满足复制要求;
  • 连接入口是否配置了所有可用 Zone;
  • 监控、备份和运维账号是否可用。

2. 跨地域部署的额外代价

跨地域副本可以增强灾备能力,但会引入:

  • 更高网络延迟;
  • 更高日志确认成本;
  • 更复杂的故障域管理;
  • 链路抖动引起的副本追平延迟;
  • 跨地域流量和存储成本。

不能只因为“副本跨地域”就断言“主地域故障时业务自动无感”。是否能够自动切换,取决于部署模式、复制策略、仲裁位置、路由配置和业务客户端行为。

3. 分区数量与副本数量的乘积效应

假设:

逻辑分区数:1000
Full 副本数:3

从数据组织角度看,系统需要维护大量物理副本、索引、日志和元数据。即使实际物理实现会进行组织和复用,分区数量过大仍然会扩大:

  • 迁移任务数量;
  • 合并任务数量;
  • 统计信息维护范围;
  • 故障恢复对象数量;
  • Schema 变更复杂度。

因此,分区数应根据数据量和访问模式计算,而不是用“分区越多,扩展性越好”作为原则。


八、运维:先确认状态,再执行变更

OceanBase 运维的关键不是记住某一条命令,而是建立以下闭环:

观察状态
  -> 判断故障域和一致性状态
  -> 执行最小范围变更
  -> 验证数据、复制和路由
  -> 观察恢复
  -> 记录变更结果

1. 连接到正确的租户

使用 obclient 时,必须明确连接的是:

  • 系统租户;
  • MySQL 租户;
  • Oracle 租户。

例如,连接参数通常包括:

obclient -h <host> -P <port> -u <user>@<tenant>#<cluster> -p

具体连接格式会因部署方式、代理入口和账号配置变化。不能把系统租户账号直接当作业务租户账号使用,也不能假设所有环境都使用相同端口。

进入租户后,先确认基本上下文:

SELECT DATABASE();

SHOW VARIABLES LIKE 'version_comment';

如果使用的是 Oracle 租户,还应使用适合该租户模式的元数据视图和 SQL 语法。

2. 查看租户和资源状态

在系统租户中,常见检查方向包括租户状态、资源池和资源单元。例如:

SELECT tenant_id, tenant_name, status
FROM oceanbase.DBA_OB_TENANTS;

可能看到类似结果:

tenant_id | tenant_name | status
----------+-------------+--------
1002      | app_tenant  | NORMAL

结果中的 NORMAL 只能说明租户状态正常,不能证明:

  • 所有 LS 都有完整副本;
  • 所有副本都已追平;
  • Leader 分布均衡;
  • 业务连接都能正确路由。

资源视图、LS 视图和副本视图的名称可能随版本变化,应以当前版本文档和 SHOW TABLES、数据字典为准。生产排查时要记录查询时间,因为副本状态是动态变化的。

3. 检查 LS 和副本

运维检查应至少关注:

LS ID
Leader 所在 Zone/Server
副本类型
副本状态
日志同步进度
数据重放进度
是否存在迁移、恢复或合并任务

系统视图可能包含如下类别的信息:

  • 租户级 LS 信息;
  • 集群级 LS 信息;
  • Tablet 和副本信息;
  • 服务器和 Zone 状态;
  • 复制与恢复任务。

不要只执行一条“节点存活”查询。一个 Server 进程存活,并不代表它承载的副本健康;一个副本状态正常,也不代表租户资源没有超卖或磁盘即将耗尽。

4. 查看分区和执行计划

对于分区表,先检查表定义:

SHOW CREATE TABLE orders;

确认:

  • 分区键是否符合预期;
  • 分区数量是否正确;
  • 是否存在 MAXVALUE 或默认分区;
  • 主键和唯一索引是否满足当前引擎的约束要求。

再检查查询是否发生分区裁剪:

EXPLAIN
SELECT *
FROM orders
WHERE user_id = 101;

期望观察到的不是某个固定字符串,而是执行计划中的访问分区范围少于全部分区。具体计划格式会随版本和 SQL 模式变化。

反例:

EXPLAIN
SELECT *
FROM orders
WHERE status = 'PAID';

status 不是分区键,且没有其他可以推导分区的条件,计划可能需要访问多个分区。此时仅仅给 status 建索引,也不一定能消除分区访问范围。

5. 扩容的正确验证方式

扩容至少分为三层:

第一层:Server 是否加入

确认新 Server 已被集群识别,并且状态正常。

第二层:租户是否获得资源

确认资源池、资源单元和租户规格发生了预期变化。只加入 Server 不等于租户自动获得资源。

第三层:数据是否重新分布

观察:

  • LS Leader 是否均衡;
  • Tablet 是否发生迁移;
  • 副本是否落到新 Server;
  • 磁盘和 I/O 是否均衡;
  • 业务请求是否开始使用新的服务位置。

若只完成前两层,可能出现“机器增加了,但热点数据仍在原节点”的假扩容。

6. 变更副本 locality 的风险

副本位置通常通过租户 locality 等配置表达。示意形式可能类似:

ALTER TENANT app_tenant
  LOCALITY = 'F@zone1,F@zone2,F@zone3';

其中 F 表示 Full replica 的一种表达方式,但实际语法、Zone 名称和副本类型必须以当前版本为准。

这类变更不是瞬时修改,它可能触发:

生成或删除副本
  -> 复制数据和日志
  -> 等待副本追平
  -> 更新多数派和 Leader 状态
  -> 执行旧副本清理

执行前必须确认:

  • 新 Zone 有足够容量;
  • 网络带宽能够支撑迁移;
  • 副本删除不会使系统失去多数派;
  • 变更期间是否有合并、备份或大批量导入;
  • 是否有回滚方案。

不要先删除旧 Zone 副本,再等待新 Zone 副本建立。正确顺序通常是先建立足够的新冗余,确认同步完成,再清理旧冗余。

7. 分区 DDL 的风险

例如给 Range 分区表增加新分区:

ALTER TABLE event_log
ADD PARTITION (
    PARTITION p202403 VALUES LESS THAN ('2024-04-01')
);

这条语句能否执行,取决于原有分区边界。如果表已经有:

pmax VALUES LESS THAN (MAXVALUE)

那么直接增加一个位于 pmax 之前的新分区,通常还需要按照当前版本支持的语法重新调整边界或拆分分区,不能想当然地把新分区插入任意位置。

执行 DDL 前后应验证:

SHOW CREATE TABLE event_log;

并测试边界值:

INSERT INTO event_log(event_id, event_time, payload)
VALUES (10, '2024-03-31', 'before-boundary');

INSERT INTO event_log(event_id, event_time, payload)
VALUES (11, '2024-04-01', 'at-boundary');

预期是:

2024-03-31 进入新旧边界前的分区
2024-04-01 进入下一个边界对应的分区

具体分区名称和边界归属应通过执行计划或系统元数据验证,而不能只凭名称推断。


九、故障诊断:按数据流而不是按现象猜测

1. 写请求失败

写请求失败可能发生在多个阶段:

客户端连接
  -> Proxy 路由
  -> Leader 接收
  -> 事务执行
  -> 日志复制
  -> 多数派确认
  -> COMMIT 响应

不同阶段的故障含义不同:

阶段 常见表现 优先检查
连接 连接超时、拒绝 DNS、Proxy、Server、连接数
路由 重定向、Leader 不可用 Leader 状态、Proxy 拓扑
执行 锁等待、资源不足 活跃事务、资源组、内存
复制 提交延迟、不可提交 多数派、副本同步、网络
提交响应 客户端超时 事务最终状态、业务幂等

尤其要区分“执行失败”和“提交结果未知”。后者不能直接重试具有副作用的业务操作。

2. 查询变慢

查询变慢至少要区分:

  1. 是否发生了分区裁剪;
  2. 是否访问了非 Leader 副本或产生转发;
  3. 是否出现跨分区分布式执行;
  4. 是否等待事务锁;
  5. 是否等待副本追平或强一致读条件;
  6. 是否发生资源单元限流;
  7. 是否存在合并、迁移、恢复等后台任务。

诊断顺序可以是:

EXPLAIN 查询计划
  -> 确认访问分区数量
  -> 确认 Join/聚合是否跨分区
  -> 检查活跃事务和锁
  -> 检查租户资源使用
  -> 检查 LS/副本状态
  -> 检查 Proxy 路由和网络

不要一看到慢查询就增加分区数量。若根因是跨分区 Join 或热点写入,增加分区可能只会增加协调成本。

3. 副本延迟

副本延迟要区分两种进度:

日志接收进度
数据重放进度

日志已经到达,不代表查询一定能看到该版本;数据已经重放,也不代表弱一致读一定选择该副本。

排查时应比较:

  • Leader 和 Follower 的日志位置;
  • 重放位置;
  • 副本状态;
  • 网络延迟和丢包;
  • 磁盘写入延迟;
  • 服务器 CPU、内存和 I/O;
  • 是否发生大事务或大量 DDL。

在副本未恢复前强行删除落后副本,会降低容灾能力;在无法形成多数派时继续修改副本配置,可能把可恢复故障变成不可用故障。

4. 磁盘满不是普通性能问题

OceanBase 的存储会受到数据、事务日志、合并产生的中间空间、备份和恢复任务影响。磁盘接近上限时,可能出现:

  • 写入失败;
  • 合并无法推进;
  • 副本迁移失败;
  • 日志无法继续保存;
  • 故障恢复空间不足。

处理顺序应包括:

  1. 确认哪个 Server、Zone 或租户占用异常;
  2. 区分数据文件、日志和临时空间;
  3. 停止继续放大的导入或异常任务;
  4. 按官方支持的方式扩容或清理;
  5. 验证合并、复制和事务状态恢复。

不应直接删除数据库目录中的文件。数据库认为仍在使用的日志或数据文件被手工删除,可能造成不可恢复的数据损坏。


十、备份、恢复与副本的边界

副本不是备份。

副本主要解决:

节点或故障域故障

备份主要解决:

误删、错误 DDL、应用逻辑错误、长时间数据损坏、灾难恢复

例如:

事务误执行 DELETE,随后提交

三个同步副本会把这个错误一起复制到三个位置。此时副本越健康,错误传播得越完整;只有备份、日志归档或其他恢复机制才能帮助找回历史状态。

生产恢复演练应验证:

  • 备份是否真正可读;
  • 恢复目标时间点是否满足要求;
  • 恢复后的租户能否执行读写;
  • 账号、权限、序列和外部依赖是否完整;
  • 恢复过程中的 RTO 是否达到业务要求。

“备份任务成功”只表示某次任务没有报告错误,不等于已经完成可用性验证。


十一、常见误解和反例

误解一:分区越多,性能越好

反例:

表只有 100 万行,却建立数千个分区

结果可能是:

  • 元数据和计划管理成本增加;
  • 统计信息更加分散;
  • DDL 和迁移任务增多;
  • 查询仍然因为没有分区键条件而扫描大量分区。

分区数量应由数据规模和访问模式决定。

误解二:三份副本就一定能容忍任意两台机器故障

三份投票副本只能在多数派仍存在时继续推进一致性。3 个副本通常只能容忍 1 个副本故障:

3 个副本,故障 2 个
剩余 1 < 多数派 2

如果两个副本位于同一个 Zone,那么该 Zone 故障可能同时带走两个副本。

误解三:读不到刚写入的数据说明提交失败

可能的真实情况包括:

  • 读请求路由到落后副本;
  • 使用了弱一致读;
  • 连接仍指向旧 Leader;
  • 提交响应丢失但事务实际已提交;
  • 读取发生在另一个事务快照中。

必须结合事务结果、读一致性设置、路由和副本状态判断。

误解四:分布式数据库会自动消除热点

如果所有请求都访问同一个用户、同一个时间分区或同一个递增键范围,热点仍会集中在少数分区或 LS。

可行的方向可能包括:

  • 重新选择分区键;
  • 对热点键做业务层打散;
  • 采用 Hash 与 Range 的组合;
  • 拆分高频写入表;
  • 让读取和写入路径分离。

但打散会增加查询聚合和事务复杂度,不能只看单点吞吐。

误解五:扩容等于加机器

若新 Server 没有获得租户资源,或者 Tablet/LS 没有迁移过去,业务流量可能仍集中在旧节点。扩容必须用资源、数据分布、Leader 分布和实际请求路径共同验证。


十二、设计时的推导方法

面对一个新业务,可以按以下顺序推导 OceanBase 设计。

第一步:确定业务主访问键

例如订单系统主要按 user_id 查询:

访问条件:user_id = ?

则 Hash(user_id) 通常比按 order_time Range 更适合点查均衡。

第二步:确定生命周期操作

如果主要需求是按月归档:

删除 24 个月前的全部数据

则 Range(event_time) 更容易通过分区级操作管理历史数据。

第三步:列出事务参与对象

例如支付事务修改:

账户余额
订单状态
支付流水

然后判断这些表的分区键是否能让同一业务对象的数据保持较近的分布范围。若无法避免跨分区,也要明确接受分布式事务的协调成本。

第四步:确定故障域

先给出故障目标:

容忍单 Server 故障?
容忍单 Zone 故障?
容忍地域级故障?

再推导副本:

容忍 1 个投票副本故障 -> 至少 3 个投票副本
容忍 2 个投票副本故障 -> 至少 5 个投票副本

然后确认这些副本是否真正位于独立故障域。

第五步:确定读一致性

把读请求分为:

必须读到最新提交结果
允许短暂旧读

前者使用更严格的一致性要求并承担相应延迟;后者可以利用更灵活的副本路由,但必须让业务接受旧读。

第六步:设计恢复和验证

最后明确:

  • 副本故障如何切换;
  • 事务提交超时如何查询;
  • 数据误删如何恢复;
  • 扩容后如何验证流量是否迁移;
  • 备份是否做过恢复演练。

这样得到的方案才是从业务语义、数据分布、复制协议和运维闭环共同推导出来的,而不是只选择一个“看起来合理”的分区数和副本数。


OceanBase 的分布式能力最终落在几个相互制约的事实之上:

分区决定访问和分布边界;
LS 决定物理复制与调度边界;
副本决定故障时是否保有多数派;
事务决定跨边界修改如何保持原子性;
路由决定请求是否到达合适的副本;
运维决定这些设计是否持续处于可用状态。

只有同时理解这些边界,才能正确解释 OceanBase 中的慢查询、提交延迟、旧读、故障切换和扩容效果。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。