OceanBase调优实战:从合并策略到执行计划的工程化调优体系
OceanBase的运维调优,最容易陷入的误区是把注意力集中在参数列表和命令语法上,却忽略了参数背后的机制耦合。一个内存参数的调整可能同时影响写入限速、合并触发和查询缓存的行为;一个索引的添加可能改善某条查询的执行计划,却给写入路径带来未被预期的合并压力。分布式架构把这种耦合放大了,因为任何一个局部的变更都会通过副本同步、共识协议和资源调度传播到整个集群。
真正有效的调优,建立在对三个层次之间关系的清晰理解之上。最底层是LSM-Tree存储引擎的写入与合并机制,它决定了数据以什么节奏从内存落到磁盘、以什么代价被查询读取。中间层是SQL优化器与执行引擎,它决定了每一条查询走哪条路径、消耗多少资源。最上层是资源调度与路由层,它决定了计算和存储在多个租户、多个副本之间如何分配。调优的动作发生在某一层,但效果的传导跨越三层。
本文沿着这条从底层机制到上层策略的路径展开。先拆解LSM-Tree架构下合并与转储的调优逻辑,再进入SQL层的执行计划稳定性与并行执行效率,然后讨论锁与等待事件的诊断方法,最后落到资源隔离、读写分离与诊断工具链的工程实践。文章的目标不是提供一份参数速查表,而是建立一套判断框架:在遇到性能异常时,知道先看哪一层,知道一个调整会在哪些地方产生副作用。
第一章 LSM-Tree的调优逻辑
1.1 基线加增量的读写分离
OceanBase的存储引擎基于LSM-Tree架构,将数据分为静态基线数据和动态增量数据两部分。SSTable是只读的,一旦生成就不再被修改,存储于磁盘;MemTable支持读写,存储于内存。DML操作首先写入MemTable,当MemTable达到一定大小后转储到磁盘成为SSTable。查询时需要分别对SSTable和MemTable进行查询,并将结果归并后返回。
这种设计的核心取舍是用写放大换取读放大。写入路径始终是顺序追加,MemTable到SSTable的转储是批量顺序写,写吞吐因此可以很高。但读取时需要合并多个版本的数据,合并的代价取决于SSTable的数量和层次。为了控制读放大,系统需要通过合并将多个SSTable归并为更少、更大的文件。
OceanBase在这个通用LSM-Tree模型上做了一个关键的差异化设计:宏块粒度的合并重用。数据文件按照2MB的基本粒度切分为宏块,每个宏块内部再拆分出变长的微块。合并时,没有更新的数据宏块不会被重新打开读取,只有发生变化的数据需要被重写。这种设计显著降低了合并期间的写放大,使OceanBase在保持LSM-Tree优点的同时,避免了传统LSM-Tree数据库合并代价过高的缺陷。
1.2 转储与合并的分工
转储和合并是两个不同粒度的操作。转储将MemTable中的数据刷写到磁盘成为增量的SSTable,它的粒度是单个租户的单个分区,资源消耗相对较小,对在线业务的影响较低。合并将增量SSTable与基线SSTable归并为新的基线,它的粒度涉及整个租户甚至整个集群的SSTable,资源消耗大,对QPS和TPS有中度影响。
转储的频率由freeze_trigger_percentage控制,它决定了租户内存使用达到什么比例时触发冻结。默认值是70,取值范围为1到99。降低这个值会让冻结更早发生,MemTable中的内存更早被释放,但转储的频率也会上升。提高这个值会减少转储次数,但MemTable可能在高写入负载下更快逼近内存上限。
合并的频率由minor_freeze_times和major_compact_trigger共同控制。minor_freeze_times设置单个租户冻结多少次后触发一次合并,默认值为5。这个参数是转储与合并之间的调节阀:值越大,转储积累的增量SSTable越多才触发合并,合并的频率降低,写入吞吐可能提升,但查询时需要合并的SSTable数量增加,读放大上升,数据时效性也可能下降。值越小,合并更频繁,读放大被控制在较低水平,但合并占用的CPU和IO资源更频繁地出现。
调优的判断依据不是寻找一个最优的默认值,而是根据业务的读写比例和延迟要求来校准。读密集且对查询延迟敏感的业务,应该让合并更频繁地发生,把读放大控制在低位。写密集且可以容忍查询路径上多一层合并的业务,可以适当放宽合并频率,把资源优先留给写入。
1.3 并行合并的边界条件
并行合并默认开启,它将一个compaction任务按照key range拆分成若干子任务并行执行。并行度与分区数的匹配关系直接影响合并效率。一般来说,并行度与分区数符合整比例关系时,比如分区数为8、并行度设为4或8,能够获得较好的性能。
并行合并有一个容易被忽视的边界:当Major SSTable的数据量很小而增量数据很多时,并行合并的range拆分可能把增量数据都划分到同一个compaction任务中,此时并行合并等价于单线程执行增量数据的合并。这意味着并行合并的收益取决于数据在key range上的分布是否均匀。如果增量数据集中在少数key range上,即使开启了并行合并,实际的有效并行度也远低于配置值。
合并线程数由merge_thread_count控制,L0层转储线程数由_mini_merge_concurrency控制。这些参数的调整需要与硬件资源匹配。合并是CPU和IO密集型操作,线程数超过CPU核心数时,线程之间的争用会抵消并行度的收益;线程数远低于核心数时,合并速度受限于单线程的处理能力,积压的增量数据会延长读放大持续的时间。
转储的并发分区数需要平衡。并发转储的分区过少,MemTable内存释放不够快,写入可能因为内存不足而受限;并发转储的分区过多,会消耗过多资源,影响在线交易的性能。
1.4 合并策略与压缩的权衡
合并过程中,数据会被重新编码和压缩。压缩算法的选择直接影响存储成本和查询延迟。更高的压缩率节约磁盘空间,但牺牲性能。ZSTD比LZ4节省更多空间,但ZSTD合并的时间更长,查询的响应时间也更大。
对于写入密集且查询以点查为主的OLTP业务,LZ4或更轻量的压缩算法更合适,因为点查的延迟对解压开销敏感。对于查询频率较低的历史数据或分析型负载,ZSTD的高压缩比带来的存储成本节约可能超过解压开销的代价。压缩策略的选择应该与数据的访问模式对齐,而不是追求单一的全局最优压缩率。
压缩算法的选择还需要考虑合并窗口的约束。高压缩率的算法在合并期间消耗更多的CPU时间,如果维护窗口有限,合并可能无法在窗口内完成。对于合并窗口紧张的场景,可以在维护窗口期间临时切换到低压缩率的算法,窗口结束后再切回高压缩率。这种动态调整需要权衡存储成本和合并窗口的约束。
1.5 合并积压的诊断与处理
合并积压是LSM-Tree架构下最常见的性能退化模式。当增量数据的产生速度超过合并的处理速度时,增量SSTable的数量持续增长,查询需要归并的文件越来越多,读放大线性上升,响应时间随之劣化。
诊断合并积压的起点是观察增量SSTable的数量和合并任务的排队情况。通过系统视图可以查看每个分区的SSTable数量、合并任务的执行状态和等待队列长度。如果SSTable数量持续超过阈值且合并任务队列长期非空,说明合并能力不足。
处理合并积压有两条路径。第一条是提升合并能力,增加合并线程数、提高合并并行度、或者将合并调度到资源更充裕的时段。第二条是降低增量数据的产生速度,调整freeze_trigger_percentage减少转储频率、或者通过限流控制写入速率。两条路径的选择取决于系统的瓶颈是在合并侧还是在写入侧。
合并积压的预防比处理更重要。在业务规划阶段,应当根据预期的写入速率和查询延迟要求,预先估算合并能力的需求,并为合并预留足够的CPU和IO资源。等到积压出现再处理,往往需要付出更大的代价。
第二章 SQL层的调优
2.1 执行计划的缓存与软解析
一条SQL语句的执行经过编译和执行两个阶段。编译阶段,优化器进行词法分析、语法分析、语法优化和代码生成,生成执行计划后提交给执行器执行。这个过程称为硬解析,消耗资源并增加SQL延迟。为了避免每次请求都进行硬解析,执行计划被缓存在计划缓存中,相同类型的后续SQL语句首先在计划缓存中查找匹配的计划,找到后直接提交执行器执行,这个过程称为软解析。
计划缓存的存在意味着,一条SQL在第一次执行时生成的计划,可能在后续数千次执行中被反复复用。如果第一次生成的计划是非最优的,那么这个非最优计划的影响会被放大数千倍。优化器生成非最优计划的原因包括缺少索引、统计信息过时和账号权限限制。非最优计划的直接代价是逻辑读次数高、扫描数据量大,最终表现为延迟上升和吞吐下降。
2.2 用SQL Audit定位需要优化的语句
定位SQL层面的性能问题,起点是V$OB_SQL_AUDIT视图。通过按平均执行时间乘以执行次数降序排列,可以找出消耗租户资源最多的TopSQL语句。
一个典型的查询会聚合每个SQL_ID的平均耗时、平均排队时间、平均逻辑读、执行次数、平均CPU时间和平均等待时间,并按平均执行时间乘以执行次数排序取前十。逻辑读高的SQL是优化的重点,因为逻辑读直接反映了查询在存储层扫描的数据量,而扫描量是可以通过索引调整和SQL改写来控制的。
找到可疑SQL之后,下一步是查看它的执行计划。如果执行计划显示全表扫描、索引未命中或者连接顺序不合理,就需要判断是统计信息过时、索引缺失还是SQL写法导致优化器做出了错误的选择。
SQL Audit的价值不仅在于定位单条SQL,还在于识别SQL的模式。如果多条SQL都因为相同的原因而性能不佳,例如都缺少相同的索引或者都因为统计信息过时而选择了错误的连接顺序,那么一次系统性的调整可能解决一批SQL的问题,而不是逐个修复。
2.3 统计信息的收集时机与方式
统计信息是优化器决策的依据。OceanBase支持通过DBMS_STATS包或ANALYZE语句手动收集统计信息。V4.x版本支持主表和索引表的统计信息收集,MySQL模式下可以使用dbms_stats.gather_table_stats或dbms_stats.gather_index_stats,后者支持指定并行度。
统计信息的收集时机需要与数据变化节奏匹配。对于写入量大的表,统计信息在大量数据变更后会迅速过时,导致优化器基于旧的分布信息估算行数,进而选择错误的连接算法或索引。在批量导入、大规模数据迁移或分区变更之后,应该主动收集相关表的统计信息,而不是依赖自动收集的周期。
统计信息的收集本身也消耗资源。全表扫描和直方图计算在高并发时段执行会与业务查询争抢IO和CPU。收集动作应该安排在业务低峰期,或者针对变化显著的分区做增量收集,而不是每次都对全表做完整的收集。
统计信息的粒度也需要考虑。对于分区表,可以收集全局统计信息和分区统计信息。全局统计信息用于跨分区的查询规划,分区统计信息用于分区裁剪后的查询规划。如果只收集了全局统计信息而忽略了分区统计信息,优化器在分区裁剪后可能低估了分区内的数据量,选择了不适合的并行度或连接算法。
2.4 Outline与计划绑定
当统计信息调整之后优化器仍然选择非最优计划,或者业务希望固定某个经过验证的最优计划时,可以使用Outline进行计划绑定。CREATE OUTLINE语句将指定的执行计划绑定到特定的SQL语句上,使该SQL的后续执行强制走绑定的计划。
Outline的使用场景需要区分清楚。如果优化器选择一个索引是因为它在当前数据分布下确实更优,那么强制绑定另一个索引可能在未来数据分布变化后变得次优。Outline适合的是那些优化器因为统计信息无法准确反映业务语义而做出错误选择的场景。例如,某条查询的业务语义决定了它总是只返回极少数行,但优化器因为统计信息中的平均值估算而认为它需要扫描大量数据,此时绑定一个索引扫描的Outline是合理的。
计划缓存的另一个问题是计划可能因为各种原因失效,比如统计信息更新、Schema变更或者内存压力导致的缓存淘汰。Outline在计划失效后仍然能够将SQL重新引导到绑定的计划上,这是它相对于单纯依赖计划缓存的价值。
Outline的管理也需要流程化。绑定的计划需要被记录和定期审查,当数据分布发生变化或者索引结构发生变化时,原有的绑定计划可能不再是优计划。Outline的清理应当与索引变更和统计信息更新同步进行。
2.5 并行执行的适配条件
并行执行通过将一个查询拆分为多个子任务在多核或多节点上同时执行来加速大查询,但它的收益依赖于一系列前提条件。
分区策略是第一个前提。在大表JOIN的场景中,如果参与JOIN的表使用相同的分区方式并绑定到同一个表组,可以实现partition wise join,这是性能最佳的并行JOIN形式。如果分区方式不匹配,数据需要做重分布,网络传输和内存开销会抵消并行度的收益。
并行度与分区数的比例关系是第二个前提。并行度与分区数符合整比例关系时性能较好,比如分区数为8,并行度设为4、8或16。如果并行度远高于分区数,多余的并行任务会因为无数据可处理而空转;如果并行度远低于分区数,单个并行任务需要处理多个分区的数据,并行加速比受限。
索引的合理性是第三个前提。创建合适的索引减少数据扫描量,才能让并行执行的每个子任务处理更少的数据,并行加速比才不会被单任务的IO瓶颈所限制。在并行执行中,如果某个子任务因为缺少索引而退化为全表扫描,整个查询的完成时间由最慢的子任务决定,并行度再高也无法弥补这个短板。
复制表是第四个可以借用的能力。V4.2及之后版本支持创建复制表,复制表在每个节点上都有完整的数据副本,JOIN时不需要做数据重分布,可以提升并行执行的效率。复制表适合小维表与大事实表的JOIN场景,不适合写入频繁的大表。
2.6 SQL改写的常见模式
除了索引和统计信息,SQL的写法本身也直接影响执行计划的优劣。一些常见的改写模式可以在不改动数据模型的前提下获得显著的性能提升。
子查询展开是最常见的改写之一。IN子查询和EXISTS子查询在某些场景下会被优化器转换为半连接或反连接,但在另一些场景下可能被逐行执行。将子查询改写为JOIN,或者在子查询中增加过滤条件以减少中间结果集,可以避免逐行执行的开销。
分页查询的深分页问题也是一个典型场景。LIMIT加OFFSET的写法在OFFSET很大时需要扫描并丢弃大量行,将分页改写为基于上一页最后一条记录的主键范围查询,可以避免无效扫描。
聚合查询的预计算是另一个方向。对于频繁执行的聚合查询,可以通过物化视图预先计算并存储聚合结果,查询时直接读取物化视图而不是扫描原始数据。物化视图的刷新策略需要与数据的更新频率匹配,过于频繁的刷新会抵消预计算的收益。
第三章 锁与等待事件
3.1 等待事件的分层
等待事件是分析并发性能瓶颈的核心工具。在任何时间点,OceanBase的工作线程要么在计算CPU利用率,要么在等待资源,例如IO资源、网络带宽或临界区。通过等待事件发生的位置和统计信息,可以识别系统性能瓶颈,尤其是高并发小查询的性能。
OceanBase目前支持超过200个等待事件,分为11个大类,包括网络、调度、临界区、latch、集群、事务提交、用户IO和系统IO等。用户IO等待事件由用户请求直接触发,系统IO等待事件由后台任务触发。
典型的诊断场景有三类。第一类是CPU利用率持续偏低而系统负载上升,这通常是因为并发瓶颈,例如频繁访问的临界区出现资源争用,通过定位等待时间最长的TopN等待事件可以找到争用的临界区。第二类是业务SQL的响应时间偶尔从不到1毫秒飙升到100毫秒以上,这通常是因为SQL的执行在某处被卡住,通过查看SQL执行期间记录的等待事件详情,尤其是耗时最长的等待事件的细节,可以定位卡住的位置。第三类是系统突然停止执行某条SQL,这通常是因为会话被锁竞争或未知原因的死锁阻塞,通过查看会话当前等待事件的详情可以定位原因。
3.2 行锁冲突的排查路径
行锁冲突是最常见的并发问题之一。排查的思路是把锁抽象为两个对象:行锁的持有者和行锁的等待者,两者都是事务的一部分,因此在监控活跃事务的同时监控锁的持有者和等待者,可以对锁冲突问题提供很大的帮助。
OceanBase提供了三张核心的虚拟表。__all_virtual_trans_stat用于监控活跃事务,包含事务的唯一标识、所在分区、参与者列表、创建时间、状态和锁冲突重试次数等信息。__all_virtual_trans_lock_stat记录当前集群中所有活跃事务持有行锁的信息,包括事务标识、持有锁的行的rowkey和所属会话。__all_virtual_lock_wait_stat统计当前集群中所有等待行锁的请求或语句的信息,包括等待的行的rowkey、开始等待的时间、绝对超时时间、重试加锁的次数、等待锁的会话ID和该行第一个等待事务的会话ID。
一个典型的排查场景是:一个会话中的未知长事务持有锁,阻塞了其他事务的执行,需要找到并停止这个长事务。方案是通过无法加锁的事务的session_id,在__all_virtual_lock_wait_stat中找到它等待锁的行的rowkey,然后通过rowkey在__all_virtual_trans_lock_stat中找到持有该行锁的事务和会话,最终定位到阻塞源。
3.3 锁的可观测性与死锁检测
一个更棘手的问题是,通过锁视图查到持有锁的会话ID之后,在会话列表中看到该会话处于SLEEP状态,无法判断这个会话是否可以安全终止,也无法确定它属于哪个业务请求。这种场景需要把锁信息与SQL审计关联起来。GVOBLOCKS视图可以查到持有锁的会话,GVOBLOCKS视图可以查到持有锁的会话,GVOB_SQL_AUDIT可以查到该会话最近执行的SQL,GV$OB_TRANSACTION_PARTICIPANTS可以查到事务的参与者信息。通过事务ID把三者串联,可以从锁的持有者追溯到上锁的具体SQL和业务请求。
死锁的检测依赖于等待图。当事务持有的锁与其他事务冲突时,会形成wait-for链,系统定期检测这条链,如果检测到闭环,就判定为死锁并回滚其中一个事务。死锁检测的周期和重试次数可以在OCP层面通过deadlock_check_period和deadlock_retries参数调整。缩短检测间隔可以更快地发现和解除死锁,但增加了检测本身的开销。
锁的粒度也影响并发性能。行锁的并发度高于表锁,但行锁的管理开销更大。OceanBase默认使用行锁,在需要批量操作大量行时,可以考虑使用LOCK TABLE语句显式加表锁,减少行锁的管理开销。但这会降低并发度,需要根据操作的规模和并发要求来权衡。
3.4 临界区与latch的争用
除了行锁,临界区和latch的争用也是并发性能的常见瓶颈。临界区是内核中不允许并发访问的代码段,latch是保护临界区的轻量级同步原语。当多个线程同时请求进入同一个临界区时,只有一个线程能获得latch,其他线程排队等待。
临界区争用的表现是CPU利用率不高但QPS上不去,等待事件中latch相关的等待占比高。诊断的方法是查看等待事件的TopN,找到争用最严重的latch类型,然后分析该latch保护的数据结构和访问模式。
缓解临界区争用的方法包括减少临界区的持有时间、拆分热点数据结构、以及通过调优减少对热点数据结构的访问频率。在某些情况下,增加并发度反而会加剧争用,此时降低并发度、让请求排队,反而能获得更高的吞吐。这需要根据实际的等待事件数据来判断,而不是凭直觉调整。
第四章 资源隔离与路由策略
4.1 集群级内存的分配逻辑
内存是OceanBase调优中最关键的资源,因为内存的分配直接决定了写入缓冲、合并触发和查询缓存的可用空间。
memory_limit配置OBServer最大可使用的总内存大小,当前该值无上限,但如果设置的上限超过实际物理内存总和,会导致系统中其他按总内存计算的值偏大。memory_limit需要与memory_limit_percentage配合设置。
system_memory设置系统预留给非实体租户的内存容量,默认值为30G。这部分内存不分配给业务租户,用于系统内部的元数据管理、RootService运行和内部保留。rootservice_memory_limit设置RootService的最大内存容量限制,默认值为2G。
memstore_limit_percentage设置租户使用MemStore的内存占其总可用内存的百分比,默认值为50。这个参数决定了租户内存中用于写入缓冲的比例。写入密集的业务需要更高的MemStore比例来缓冲写入峰值,但过高的比例会挤压查询缓存和Block Cache的空间,影响读取性能。
内存分配的一个常见误区是只关注总量而忽略分配比例。总内存足够但MemStore比例过低时,写入峰值会触发频繁的冻结和转储,影响写入性能。MemStore比例足够但查询缓存比例过低时,高频查询会因为缓存命中率下降而退化。内存分配需要根据业务的读写比例来校准,而不是简单地增加总量。
4.2 CPU配额与并发控制
cpu_quota_concurrency设置租户的每个CPU配额所允许的最大并发数,默认值为4,取值范围为1到10。这个参数影响租户内工作线程的并发度。对于以短查询为主的高并发业务,适当的并发度可以提升CPU利用率;对于以长查询为主的业务,过高的并发度可能导致线程争用和上下文切换开销。
租户资源的定义还包括IOPS和会话数限制。在资源配置时,MAX_CPU必须大于或等于MIN_CPU,单元配置的CPU、内存和日志盘容量在修改后必须大于或等于租户已使用的容量。资源超配是租户创建失败的常见原因,CPU或内存的超额分配会导致创建过程在资源检查阶段被拒绝。
obdiag的巡检能力可以快速发现租户资源配置的问题,例如租户线程利用率超过95%、副本数不一致、宏块利用率过低等。这些指标如果不在上线前检查,可能在业务高峰期暴露为不可用或性能骤降。
4.3 读写分离与LDC路由
读写分离是通过将弱一致性读请求路由到备副本或只读副本,来分担主副本的读取压力。OceanBase的读写分离依赖于OBProxy的路由策略。
弱一致性读有两种路由策略:读写分离和LDC路由。LDC路由的优先级高于读写分离,ODP在选择副本时先检查LDC路由,再走读写分离路由。proxy_route_policy参数控制ODP的弱读请求路由策略,默认策略允许弱读请求被路由到备副本和只读副本,备副本的类型由route_target_replica_type决定。
LDC路由的核心是利用proxy_idc_name配置指定ODP在逻辑数据中心中的位置,使弱读请求只路由到特定区域的副本。这种方式适合跨地域部署的场景,可以避免弱读请求跨地域访问带来的延迟。读写分离策略则通过ob_route_policy系统变量控制,默认取值为readonly_zone_first,即优先读只读型副本。
读写分离的前提是业务能够容忍弱一致性读的数据延迟。对于需要读到最新数据的查询,比如刚写入后立即读取的场景,弱读会导致读不到刚提交的数据。这类查询应该走强一致性读,路由到主副本,而不是为了分担压力而牺牲一致性。
4.4 资源组与租户隔离
OceanBase支持通过资源组和租户来隔离不同业务的资源。租户是资源隔离的基本单位,每个租户拥有独立的CPU、内存和存储资源。资源组用于在租户内部进一步细分资源的分配。
租户隔离的价值在于防止业务之间的相互干扰。一个写入密集的业务不会因为其合并操作而影响另一个读密集业务的查询延迟,因为合并操作消耗的是写入密集租户的资源。租户隔离的代价是资源利用率可能下降,因为每个租户的资源配置需要按照其峰值需求来预留,而不同租户的峰值可能不在同一时间出现。
租户隔离的粒度需要权衡。过细的租户划分会导致资源碎片化和配置管理的复杂度上升,过粗的划分则可能让不同业务在同一租户内相互干扰。通常的建议是按照业务线或按照SLA等级来划分租户,同一业务线内共享租户,不同SLA等级的业务分开租户。
第五章 诊断工具链
5.1 obdiag的定位与部署
obdiag是专为OceanBase设计的黑屏诊断工具,功能覆盖对日志、SQL审计以及进程堆栈等信息的扫描、收集和分析。它的价值在于把原本分散在多个视图、日志文件和命令行工具中的诊断动作,收敛为一条命令的采集和分析。
obdiag支持独立部署和通过obd部署两种方式。独立部署时,通过yum安装oceanbase-diagnostic-tool包,执行init.sh初始化环境。部署完成后,通过obdiag config命令配置待诊断集群的连接信息。obdiag采用集中式收集,只需要部署在任意一台能连接到集群各节点的服务器上,不需要在每台OBServer节点上部署。
5.2 一键收集SQL诊断信息
慢查询诊断的核心是trace_id。可以通过GV$OB_SQL_AUDIT按query_sql模糊匹配获取近期SQL的trace_id,也可以在当前会话执行SELECT last_trace_id获取前一条SQL的trace_id。
获取trace_id之后,执行obdiag gather plan_monitor命令,一键收集该SQL的执行计划、运行时统计、等待事件和相关日志。这个命令把原本需要手工在多个视图之间比对的信息聚合为一个结构化的诊断包,使问题定位从凭经验猜变成按证据查。
perf_sql场景是另一个针对慢查询的采集能力,通过trace_id一键采集所有相关诊断信息,适合SQL超时这类业务最常见的问题。
5.3 系统巡检与配置核查
性能问题的一个常见根源是系统配置与硬件规格不匹配,或者多个参数之间存在未被察觉的冲突。obdiag的system巡检项可以一次性检查所有关键配置,包括操作系统层面的参数和OceanBase层面的参数,找出可能导致瓶颈的配置项。
巡检的价值在于把上线前的检查从人工清单变成自动化流程。人工检查容易遗漏,尤其是在集群规模大、参数多的情况下。自动化巡检能够以一致的标准覆盖所有配置项,并在发现异常时给出具体的修改建议。租户资源配置的巡检可以检查线程利用率、副本一致性和宏块利用率等指标,这些指标如果在上线前被忽略,会在业务运行中以难以定位的性能异常形式暴露。
巡检的另一个用途是变更前后的对比。在参数调整或扩容操作之前执行一次巡检,操作之后再执行一次,对比两次巡检的差异,可以快速确认变更是否达到了预期效果,是否引入了新的配置问题。这种对比比单纯看某个指标的变化更全面,因为它覆盖了所有关键配置项,而不是只看被调整的那一个。
第六章 调优的判断框架
6.1 从症状到层次的映射
调优的起点是症状,但症状本身不指向具体的层次。同样表现为响应时间上升,可能是SQL执行计划变差,可能是合并积压导致读放大,可能是锁等待,也可能是租户内存不足导致频繁冻结。有效的诊断需要把症状映射到最可能出问题的层次。
如果延迟上升同时伴随逻辑读显著增加,优先看SQL层。逻辑读的增加通常意味着执行计划选择了扫描更多数据的路径,原因可能是统计信息过时或索引失效。
如果延迟上升伴随CPU利用率下降、IO等待上升,优先看存储层。合并积压或转储不及时会导致MemTable中的数据在查询时需要与更多的SSTable归并,读放大上升。检查freeze_trigger_percentage、minor_freeze_times和合并线程的积压情况。
如果延迟上升表现为间歇性的尖峰,且尖峰时间与某些操作的时间窗口重合,优先看并发层。锁等待或latch争用会造成间歇性的阻塞,通过等待事件的TopN视图可以定位争用的临界区。
如果延迟上升在特定租户上表现明显而其他租户正常,优先看资源层。租户的CPU配额、内存配额或连接数可能已经接近上限,检查租户的资源使用率与配额的比例。
6.2 调优动作的传导路径
任何一个调优动作都会沿着依赖链传导,在解决一个问题的同时可能引入新的问题。在调整参数之前,理解这个参数的传导路径,比参数本身更重要。
调小freeze_trigger_percentage会减少MemTable的内存占用,但转储频率上升,转储消耗的IO和CPU增加。如果系统的IO已经接近饱和,这个调整可能缓解内存压力却加剧IO争用。
调大minor_freeze_times会降低合并频率,写入吞吐可能提升,但查询时需要归并的SSTable数量增加,读放大上升。如果业务以读为主,这个调整会让查询延迟恶化。
增加并行度可以加速大查询,但如果并行度超过分区数的合理比例,或者数据分布不均匀,并行度的收益会被空转和跨节点数据传输所抵消。
增加索引可以改善特定查询的执行计划,但每个新增索引都会增加写入路径的维护开销和合并时的索引重建成本。索引的收益应该在查询性能的提升和写入路径的代价之间做权衡。
6.3 调优的验证方法
调优动作的效果需要通过数据来验证,而不是通过主观感受来判断。
SQL层的调优,验证指标是目标SQL的平均执行时间、逻辑读次数和CPU时间的变化。V$OB_SQL_AUDIT视图可以按SQL_ID做前后对比。
存储层的调优,验证指标是合并的完成时间、增量SSTable的数量、MemTable的冻结频率和查询的平均IO等待时间。如果合并的完成时间没有缩短,或者读放大没有下降,调整可能没有触及真正的瓶颈。
资源层的调优,验证指标是租户的CPU和内存使用率是否回到了配额的安全区间,以及租户内查询的排队时间是否下降。
验证的周期需要覆盖业务负载的完整波动。在低谷期表现良好的调整,可能在高峰期因为并发上升而失效。调整后的观察窗口应该至少覆盖一个完整的业务峰值。
第七章 典型故障场景的调优实录
7.1 场景一 合并积压导致的查询延迟飙升
某交易系统的查询延迟在业务高峰期从平均五毫秒上升到八十毫秒,CPU利用率没有明显变化,但IO等待显著上升。
诊断路径:查看增量SSTable的数量,发现多个核心分区的增量SSTable数量超过阈值,合并任务队列积压。查询这些分区的执行计划,发现原本使用索引扫描的查询因为需要归并过多SSTable而退化。
根因分析:业务的写入速率在高峰期显著上升,freeze_trigger_percentage的默认值导致MemTable频繁冻结,增量SSTable快速积累。合并线程数不足以处理积压,读放大持续恶化。
调优动作:增加merge_thread_count,将合并线程数从四增加到八。将minor_freeze_times从五调整为三,让合并更频繁地触发,避免增量SSTable过度积累。同时将合并窗口的调度调整到业务低谷期,避免合并与业务争抢IO。
效果验证:调整后增量SSTable的数量回到阈值以下,查询延迟恢复到六毫秒,IO等待下降到可接受水平。
7.2 场景二 执行计划退化导致的单条SQL性能骤降
某报表查询的执行时间从两百毫秒突然上升到十五秒,执行计划从索引范围扫描变为全表扫描。
诊断路径:通过V$OB_SQL_AUDIT定位到该SQL的trace_id,执行obdiag gather plan_monitor获取执行计划的详细信息。对比历史执行计划,发现优化器选择的索引发生了变化。
根因分析:表的统计信息在最近一次批量导入后没有更新,优化器基于过时的统计信息估算行数,错误地判断索引扫描的成本高于全表扫描。
调优动作:执行DBMS_STATS.GATHER_TABLE_STATS更新统计信息,重新执行查询,执行计划恢复为索引范围扫描。同时为该SQL创建Outline,固定索引范围扫描的计划,防止未来统计信息波动导致计划漂移。
效果验证:SQL执行时间恢复到两百毫秒以内,在后续两周的观察中没有再次出现计划漂移。
7.3 场景三 租户内存不足导致的写入阻塞
某写入密集的业务在高峰期出现写入超时,租户的内存使用率持续接近百分之百。
诊断路径:查看租户的内存分配,MemStore的使用率在高峰期达到上限,触发频繁的冻结和转储。转储速度跟不上写入速度,MemTable的内存无法及时释放,写入请求排队等待。
根因分析:memstore_limit_percentage设置为百分之五十,在写入高峰期不足以缓冲峰值。租户的总内存配额也偏低,无法通过调整分配比例来解决。
调优动作:将租户的内存配额从三十二GB增加到六十四GB,将memstore_limit_percentage从百分之五十调整到百分之六十。同时将freeze_trigger_percentage从七十调整到六十,让冻结更早发生,避免MemTable逼近上限时才触发。
效果验证:租户内存使用率在高峰期回到百分之八十五的安全区间,写入超时消失,转储和合并的节奏恢复到正常水平。
结语
OceanBase的高效运维,本质上是在LSM-Tree的写入节奏、SQL优化器的决策质量、并发控制的争用水平和资源分配的隔离程度之间,持续寻找与业务负载匹配的平衡点。合并频率、计划稳定性、锁争用和内存配额,这四个变量相互牵制,任何一个的调整都会在另外三个上产生回响。
一个可复现的调优流程应该遵循这样的顺序:先用等待事件和SQL审计定位症状所在的层次,再理解该层次的机制与参数之间的传导关系,然后以最小的变更幅度做调整,最后用数据验证效果并观察传导路径上的副作用。这个流程不依赖于对某个特定版本默认值的记忆,而依赖于对机制的理解和对验证的坚持。
当合并节奏与写入负载匹配时,读放大被控制在可接受的范围;当执行计划在统计信息更新后保持稳定时,查询性能不会因为一次数据变化而突然劣化;当锁争用被可观测性工具定位到具体的会话和SQL时,并发问题从不可捉摸变为可处理;当资源配额与业务峰值对齐时,租户之间不会因为资源争抢而互相拖累。这些状态的达成,是调优工作真正的成果,也是运维从被动救火走向主动治理的标志。