去年团队做数据库选型对比,我负责牵头测OceanBase,第一天就遇到一个特别反直觉的现象:32C64G的机器上,小事务写入的峰值能打到接近二十万TPS,但压测跑了一个小时后,同样的场景反而掉到十万出头。后来翻了OB的合并日志、看了内存表冻结和转储的节奏,才意识到问题出在我对OceanBase架构原理的底层理解不够——它的存储引擎和传统关系型数据库完全不是一回事。这篇就把我研究下来最核心的架构原理、以及把这些原理落到实际连接和压测中的经验整理出来。
1. 为什么OceanBase值得花时间研究:从一次压测的意外说起
1.1 压测暴露的第一个“反常”
当时用的压测模型很简单,一张订单表、十几个字段、主键点查和等值写入各占一半。刚开始跑,指标非常漂亮,读多写少的场景下内存命中率接近99%,因为热数据都在内存里。但持续跑下去,我注意到底层磁盘IO每隔一段时间就会涌出一波明显的写流量,同时性能出现抖动。查了一圈,定位到是LSM-Tree结构里的冻结MemTable在转储(Minor Compaction)。这时候我才意识到,OceanBase的写入路径,并不是传统“写日志-改数据页-刷脏页”那套,而是先在内存里把增量数据攒成一个大块,再批量往下沉。这个“先攒后写”的机制,决定了它的架构风格,也决定了压测场景必须专门设计。
1.2 整体架构:这个分布式数据库由哪些部件组成
我第一次看OceanBase的架构图时,感觉它像是一套“分片数据库外挂了一个协调者”。严格说,OceanBase是一个多租户的分布式关系型数据库,整个集群由若干台OBServer节点组成。每个OBServer既承载SQL引擎,也承载事务引擎和存储引擎,数据和计算并不分离。这意味着,你连到任意一台OBServer,它都能把你的SQL路由到正确的位置去执行。
再看上层,有一个叫RootService的总控服务,它负责集群资源管理、分区负载均衡、DDL操作、日常的备份恢复调度等。RootService本身也有高可用,不是单点。在OceanBase的体系里,同一份数据会被拆成很多个分区(Partition),每个分区在多个OBServer上保存副本,这些副本之间用Paxos协议做日志同步,保证数据一致。所以整个系统的核心链路是“分区→Paxos副本组→日志流”,这套设计和传统主从复制完全不同,传统主从通常是整库级别复制,OB是分区级别复制,粒度细得多。
| 核心组件 | 职责定位 | 和传统数据库对比 |
| OBServer节点 | SQL引擎+事务引擎+存储引擎 | 相当于实例进程,但可以水平扩展 |
| RootService | 资源调度、分区均衡、DDL、容灾决策 | 类似控制节点,传统库没有 |
| 分区(Partition) | 数据分片的基本单元 | 类似Oracle分区表,但OB每个分区都是独立Paxos组 |
| 日志流(Log Stream) | Paxos日志同步的单位 | 类似redo日志,但跨节点同步 |
| 租户(Tenant) | 资源隔离与账户体系 | 类似实例/数据库的概念 |
这套架构里,最值得花时间理解的是“租户”这个概念。一个OB集群可以创建多个租户,每个租户独享一部分CPU和内存资源,租户之间资源隔离。平时我们用MySQL客户端连接OB,实际上就是连接某个租户,而不是直接连接“集群”。如果面试或者汇报时把这个讲清楚,是非常加分的,因为很多人一上来就把它理解成“一个MySQL”,根本没有意识到背后还有资源隔离和分布式调度的层次。
1.3 兼容MySQL背后的三层含义
OceanBase对外兼容MySQL协议,这一点大家在选型时都很看重。但“兼容”分为几个层次,每一层其实都能看到架构的影子:
- 协议兼容:客户端握手、认证、查询返回格式和MySQL一样,所以DataGrip、IDEA自带的数据库工具、Navicat都能直接连。
- 语法兼容:绝大多数MySQL SQL语法、数据类型、常用函数都能直接跑,存量业务迁移成本低。
- 生态兼容:JDBC驱动、ORM框架、连接池都能用,应用层几乎不用改。
但是,协议兼容不代表内核实现一样。MySQL的InnoDB用B+树组织数据,OceanBase用LSM-Tree;MySQL主从复制基于binlog,OceanBase用Paxos日志流。所以,用MySQL的心智模型去推断OB的行为,很容易踩坑。我自己压测时就吃过亏——按InnoDB的写入放大思路去调innodb_buffer_pool_size、innodb_flush_log_at_trx_commit之类的参数,结果在OB里压根没有这些参数,配置体系完全不同。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 存储引擎:LSM-Tree与内存事务的合并逻辑
2.1 数据从写入到落盘的完整路径
OceanBase存储引擎从大的层面看是LSM-Tree架构。一条数据写进来后,先写到MemTable(内存表),同时把事务日志写到Clog(Commit Log)里保证持久性。当MemTable达到一定大小或者触发了冻结条件,它就不再接受新写入,变成一份只读的“冻结MemTable”,后台线程会把它转储成SSTable放到磁盘上。新来的写入继续写到新的MemTable里。
这里有一个特别容易被忽略的点:读请求不一定要查磁盘。OB的读路径会先去MemTable里查最新数据,再去SSTable里查历史数据,最后做一次合并。这个“合并”不是把数据读出来再拼起来,而是按版本号做多版本读,只返回符合当前快照的版本。所以,OB天然支持多版本并发控制(MVCC),而且版本管理比传统数据库更贴合LSM结构。理解这条链路之后,压测时看到内存命中率高,就知道是MemTable和Block Cache在起作用。
为了说明写入路径,我一般会画这么一条时间线:
- 客户端发起事务,写入数据。
- 事务引擎记录Clog,Paxos组内同步日志。
- 数据写入本机MemTable,事务提交成功。
- 后台触发冻结,MemTable变为只读。
- 转储线程将冻结MemTable写入SSTable。
- 多次转储后,后台做Major Compaction合并SSTable,清理冗余。
如果用大白话类比,这就是“先在黑板上记,黑板写满了再抄到笔记本上,笔记本多了再整理成一本厚书”。很多人一开始觉得这个机制比B+树复杂,但它换来的是顺序写盘和批量合并,在机械硬盘时代是降维打击,在NVMe SSD时代依然能有效减少随机小IO。
2.2 合并为什么是性能的关键,又是运维的痛点
理解了数据落盘路径,就绕不开合并(Compaction)。OB的合并分为两类:
- 转储(Minor Compaction):把冻结的MemTable转成SSTable,代价低,但会产生多个小SSTable。
- 合并(Major Compaction):把多个SSTable合并成一个,同时做全局数据整理,代价高,一般在业务低峰期自动触发,也可以手动
ALTER SYSTEM MAJOR FREEZE触发。
我压测时遇到的那个性能掉坑,其实就是Minor Compaction频繁触发。因为写入压力大,MemTable很快就满了,后台一直在做转储,磁盘IO瞬时被打满,前端的写入就出现了毛刺。解决思路不是关掉合并(那会让SSTable无限膨胀,读放大更严重),而是调整冻结阈值、转储线程数,以及把合并调度放到低峰期。
运维层面最容易忽略的是写入放大和读放大的权衡。SSTable越多,读的时候要查的文件越多,读放大越严重;SSTable越少,合并越频繁,写放大越严重。这个平衡点需要根据业务模型反复调。以我压测的订单场景为例,正常的做法是控制单表分区数量,让每个分区的数据量落在合理区间,同时把转储阈值调高,减少频繁的小转储,让合并更集中。Order场景是典型的高写入,减少合并次数对提升峰值TPS很有效。
2.3 数据压缩与编码设计的意义
OB存储引擎还有一个容易被低估的设计:数据编码和压缩。和传统数据库按行存不一样,OB在SSTable里支持行列混合的存储方式。默认情况下,它会根据字段的特征自动做编码,比如对某个列做字典编码、差值编码或前缀编码。这意味着,重复率高的字段在磁盘上可以压缩得非常狠。我实测过一个用户表,三个字段是城市、来源渠道、状态,这三个字段的重复度很高,整体表空间占用只有MySQL InnoDB的四分之一左右。对存储成本敏感的场景,这个优势非常实在。
但要注意一点:压缩和编码在消耗CPU。对CPU密集型的查询,压缩率过高反而会导致解压开销变大。所以OB提供了表级、列级的压缩/编码配置,压测时建议先关闭压缩跑一轮,再打开压缩跑一轮,对比CPU和IO的占比差异,确定是否需要针对大表调整编码方式。
3. 高可用基石:Paxos协议在分布式部署中的具体落地
3.1 一个分区怎么变成一个Paxos组
OceanBase的高可用不是靠主从复制,而是靠Paxos协议。每个分区会配置多个副本,这些副本分布在不同的OBServer上,共同组成一个Paxos组。一个Paxos组里,有一个主副本(Leader)和若干个从副本(Follower)。所有读写都走主副本,从副本通过同步Clog日志保持最新状态。
这个设计有几个非常显著的架构收益:
- 故障切换粒度小:传统主从切换通常是整个实例切换,OB是按分区级别的Paxos组切换,某个分区的主副本所在节点故障,只切换这个分区的副本,其他分区不受影响。
- 写扩展能力更强:同一张表的不同分区,主副本可以分布在不同的OBServer上,理论上可以把写压力分散到多台机器。
- 强一致保证真实可靠:Paxos协议的多数派确认机制,保证提交成功的事务不会因为少数派节点故障而丢失。
我见过很多人问:OB能不能做到RPO=0?准确答案是,在Paxos协议的多数派同步下,已提交事务的日志已经复制到多数派节点,所以真正提交成功的数据不会丢,RPO是0。这一点和异步复制的MySQL在本质上完全不同。
3.2 主副本切换的完整链路
当主副本所在的OBServer宕机,Paxos组内会发起新一轮选举。以我实际观察的切换过程来看,大致链路是:节点心跳超时→RootService感知到异常→该节点上的分区Leader失去多数派心跳→从Follower中选出新的Leader→新Leader补拉Cliog日志(如果之前落后)→恢复读写服务。整个过程一般在秒级以内。
但这里有个非常容易踩的坑:切换时长并不等于所有分区切换时长的总和,而是取决于状态最差的那个分区。如果一个节点同时持有大量分区的Leader角色,宕机时这些分区都要重新选举,可能造成几十秒甚至更长的不可用窗口。所以生产环境部署时,不仅要考虑副本数,还要考虑Leader角色在节点间的均匀分布。OB的负载均衡策略会尽量分散Leader,但如果你手动指定了Primary Zone或者Table Group,限制条件多了,均衡效果会打折。
3.3 容灾设计中的常见认知误区
第一个误区是“副本数越多越安全”。Paxos需要多数派才能提交,假如5副本里2个同时故障,Paxos仍然可以正常工作,但如果3个故障,整个Paxos组就不可用了。所以副本数不是越多越好,而是要根据故障域设计合理的冗余。一般生产环境推荐3副本或5副本,同时把副本分布到不同的机房或不同的机架,尽量做到机房级容灾。
第二个误区是“备库可以随意读写”。OB默认情况下从副本不提供读服务,除非开启备租户可读或者针对特定副本设置只读路由。因为Paxos组是强一致模型,从副本的数据落后于主副本,如果直接读从副本,是读不到最新数据的,除非业务能接受弱一致。做读写分离前,一定要想清楚业务能不能容忍读延迟。
我在实际运维中特意做过一次故障演练:直接kill -9掉一个OBServer进程,观察业务侧的影响。结果显示,持有Leader分区的业务在切换期间产生了报错,应用层配置的JDBC重连机制生效后,请求才恢复正常。所以,即使OB切换很快,应用层也该做好重试和连接池预热,不能以为数据库高可用,应用就无需处理故障。
4. 分布式SQL执行:一条查询从发起到返回的完整旅程
4.1 连接是怎么被处理的
你在DataGrip或IDEA里连上OceanBase,发送一条SELECT,这条SQL会先被某个OBServer接收。这个OBServer充当协调节点,它内部有一套标准的SQL引擎流程:
- Parser:把SQL文本解析成语法树。
- Resolver:做语义解析,验证表名、字段名、权限。
- Transformer:做等价改写,比如视图展开、子查询去关联化。
- Optimizer:生成执行计划,基于统计信息做代价估算。
- Code Generator:生成可执行算子。
- Executor:真正执行,并行调度算子。
这里面,OB的优化器是一个基于代价的优化器(CBO),它会结合表的统计信息、分区裁剪信息、索引情况来生成计划。如果没有收集统计信息,优化器会选错执行计划,这其实是运维里最常见的慢查询根因之一。我的习惯是,业务表数据量变化超过阈值后就执行ANALYZE TABLE更新统计信息,或在低峰期开始自动统计信息收集任务。
4.2 分布式计划是怎么生成的
和单机数据库相比,OB的SQL引擎多了一个关键步骤:把单机执行计划改写成分布式执行计划。例如,你查一张有32个分区的表,优化器会考虑在所有相关分区上并行扫描,再把结果汇总到协调节点。
分布式计划生成有几个核心概念:
- 算子下推:把过滤条件、投影、聚合操作下推到分区所在的OBServer上执行,尽量减少网络传输。
- 分区裁剪:如果WHERE条件包含分区键,只扫描对应的分区,大幅减少IO。
- 并行执行(PX):大查询可以拆成多个并行线程,跨节点并行扫描,再汇总结果。
我遇到过一条统计SQL,在全表扫描时走了串行计划,跑了几十秒。后来发现是因为分区键是用户ID,而查询条件只带时间范围,无法裁剪分区,导致全分区扫描。改成在时间维度上做二级分区后,查询秒回。这就是分布式执行计划里分区键设计的重要性,ON排障时可以优先看执行计划里的Partitions字段,排查是否发生了全分区扫描。
4.3 分布式事务与隔离级别
OceanBase的分布式事务走的是两阶段提交(2PC)。跨分区的写事务,协调者会先让所有参与分区预提交,全部成功后再统一提交。这个机制保证了跨分区、跨节点事务的原子性。
这里有一个架构级别的细节:OB有一个全局时间戳服务(GTS),负责生成全局单调递增的时间戳,用来确定事务的快照版本。每次读事务拿到一个快照时间戳,然后基于这个时间戳去读取数据,保证在同一瞬间看到一致的数据。这也是OB默认隔离级别是读已提交但实际支持可重复读的原因,关键在于快照版本的控制。
如果你要基于OB做数据一致性验证,可以开两个会话同时读写同一行数据,观察不同隔离级别下的表现。注意OB对“当前读”和“快照读”的处理不同:快照读不加锁,当前读会加行锁。
4.4 面试和实践中常被问到的SQL层问题
围绕OceanBase的面试问题,总是绕不开SQL引擎和事务控制。结合我自己去大厂交流的经验,最常被问的问题有这些:
- OceanBase和MySQL底层实现的区别:MySQL用B+树,OB用LSM-Tree;MySQL主从复制是binlog异步,OB是Paxos同步。
- OB如何保证跨节点事务的一致性:两阶段提交+全局时间戳服务。
- OB的查询计划为什么可能是分布式计划:因为数据分布在多个OBServer的分区上,需要算子下推和结果汇总。
- 什么情况下会出现分布式事务:只要一个事务涉及多个分区,就会走2PC,即使这些分区在同一台服务器上也是如此。
这些问题网上有很多答案,但真正有深度的回答一定要落到OB的物理存储和执行机制上,而不是停留在概念层。面试时能画出写入路径、讲清MemTable到SSTable的流程,再结合压测数据来说明调优手段,基本就能证明你对OceanBase架构原理是真懂,而不是背题。
5. 连接、压测与排障:把架构知识用到日常操作中
5.1 DataGrip/IDEA连OceanBase的实测配置
很多人拿到一个OB集群,第一件事就是找可视化工具连接。DataGrip和IDEA连接OB其实不难,关键是配置细节要对。
DataGrip里新建数据源时,选择MySQL驱动即可,但JDBC URL要用OB的格式:
bash复制jdbc:mysql://10.0.0.5:2881/test_db?useUnicode=true&characterEncoding=utf8&useSSL=false&connectTimeout=5000&socketTimeout=600000
需要注意,OB默认的SQL端口是2881,不是3306。租户信息方面,用户名通常包含租户名和账号名,比如tenant1#user1,如果你用的是MySQL兼容模式下的OB 4.x,用户名格式可能是user1@tenant1,具体以创建租户时的配置为准。DataGrip里“数据库”一栏填写的是租户下的库名,比如test_db。
IDEA的连接方式基本一致,在Database面板里选MySQL数据源,填上同样的URL和账号信息。最容易踩的坑是驱动版本选太老。OB 4.x对MySQL 8.0的协议兼容性比较好,建议选mysql-connector-j 8.0以上版本。另外,连接池参数里建议把maxLifetime设短一些,比如60秒,避免OB侧因长时间空闲把连接断掉后,连接池还拿着失效连接不放。
5.2 压测工具怎么选,结果怎么看
压测OB,我实测下来比较顺手的工具是sysbench和JMeter。sysbench适合做纯数据库压力测试,JMeter适合模拟业务接口层流量。
sysbench跑OB时,参考命令大致如下:
bash复制sysbench --db-driver=mysql --mysql-host=10.0.0.5 --mysql-port=2881 --mysql-user=user1@tenant1 --mysql-password=xxx --mysql-db=test_db --tables=16 --table-size=1000000 --threads=64 --time=300 --report-interval=10 oltp_read_write run
这里有几个参数和压测结果直接相关:
--tables和--table-size决定数据量,建议数据量至少超过内存大小,否则全程内存命中,测不出真实的磁盘IO行为。--threads要逐步加压,从16、32、64、128这样递增,观察TPS和延迟拐点。--report-interval=10每10秒输出一次实时指标,方便观察合并时间段的性能抖动。
压测OB时,除了看TPS和QPS,还要额外关注三个指标:写延迟的P99、磁盘IO的等待时间、合并触发频率。如果P99经常出现周期性尖峰,大概率是Minor Compaction或者Major Compaction在做后台合并。此时可以看OB的日志或视图确认:
sql复制SELECT * FROM oceanbase.GV$OB_MERGE_INFO ORDER BY START_TIME DESC LIMIT 10;
通过这个视图能看到每次合并的开始时间、结束时间、涉及的分区数。如果在压测时间段内恰好有合并,就可以把压测数据和合并时间对应起来分析。
5.3 从日志看架构:一次慢查询排查的完整思路
最后说一个非常实用的排查案例。某次业务反馈一条报表查询很慢,表有8个分区,数据量不大,但每次都要跑十几秒。我当时的排查链路是这样的:
第一步,先看执行计划是否走了分布式并行计划:
sql复制EXPLAIN SELECT COUNT(*), user_type FROM order_table WHERE create_time > '2024-01-01' GROUP BY user_type;
发现计划里Partitions字段显示访问了8个分区,但Operator里没有PX算子。说明这个查询是串行执行的,8个分区逐个扫描汇总,没有利用并行能力。
第二步,检查表统计信息。查询视图发现最后统计时间是很久以前的,数据量已经翻了十倍,但优化器还拿着旧统计信息做代价估算,于是走了一个保守的串行计划。执行ANALYZE TABLE order_table;之后,再查执行计划,优化器选择了并行扫描,执行时间从十几秒降到了两秒多。
第三步,如果统计信息没问题但依然慢,再看存储层。打开OB的慢查询日志或者查询GV$OB_SQL_AUDIT视图,查看wait_event,如果大量等待是db_file_data_read,说明读放大严重,SSTable文件太多,需要触发Major Compaction合并。
这个排查思路里,每一步其实都在用架构知识:执行计划对应分布式查询优化,统计信息对应CBO的依赖,读放大对应LSM-Tree的SSTable管理。学OceanBase架构原理,最大的价值不是背概念,而是遇到线上问题的时候,能快速在脑子里建立“这个现象对应哪个模块、哪个机制”的映射。
我对OceanBase的整体感觉是:它把分布式的一致性、存储的LSM-Tree、SQL的分布式执行这三条主线拧在了一起,理解任一条主线都需要另外两条的配合。如果你是刚开始学,建议从一条写入SQL的完整旅程切入,跟着数据从客户端到MemTable、再从MemTable到SSTable,同时观察Paxos日志同步的节奏,这个过程走通之后,再去看查询计划和事务隔离,就会豁然开朗。后面我也打算写一篇基于这套架构原理的压测调优实战,把参数调整和场景设计详细展开,到时候再回来补充。
