
最近整理测试笔记翻到OB项目那一轮的“问题记录”文档突然觉得当时很多坑其实可以提前避免。OB是我参与的一个分布式数据库项目的内部代号这个标题下记录的并不是某一个Bug而是一整轮版本联调中我们踩过的各种问题环境准备、功能回归、性能压测、高可用演练全部搅在一起。写这篇文章的目的也很简单把问题还原成“当时怎么发现、怎么排查、最后怎么解决”的过程给正在折腾分布式数据库测试的同行做个参照。如果你刚接触这类系统也能从中看到问题记录的完整手法不只是一张张Bug清单。1. 环境准备阶段的三类问题资源、网络与监控底座1.1 机器资源估算偏差导致的初始化失败OB这类分布式数据库对环境的第一道考验不是安装包而是资源预算。我们最开始按“最低配置”申请测试机每台给到8核16G内存、300G普通SSD。结果部署后执行初始化脚本集群数据目录建到一半进程直接退出日志里写的是内存分配失败。当时第一反应是怀疑安装包有问题重装两遍依旧如此才想到去看宿主机负载。实际原因是两个一是操作系统的page cache根本没有预留空间数据库进程启动瞬间吃掉了几乎所有可用内存二是最初规划副本数时用了三副本每副本的memtable和日志缓冲区累积起来超出单机内存预算。后来把测试机调整成16核32G内存里给操作系统和监控各留出约4G再把数据库进程内存上限调到可分配内存的70%左右初始化才算顺利通过。这里建议所有测试环境都做一次资源清单复核不要直接套生产建议值的下限。用下面的巡查命令可以快速判断是否死在资源上free -h cat /proc/meminfo | grep -i commit dmesg | tail -200 | grep -i out of memory如果dmesg里出现OOM字样基本可以确定是内存预算问题。磁盘方面也需要检查挂载选项比如是否使用了noatime、SSD的TRIM是否开启这些会影响后续压测阶段的IO稳定性。我们后来把环境准备阶段的问题单独登记成一张表每次开新环境先过一遍再没出现过初始化失败的情况。1.2 网络隔离工具导致的心跳抖动误判高可用测试里绕不开“模拟网络故障”这一步一开始我们图省事直接在业务网卡上用iptables丢包。结果网络故障还没模拟完整个测试集群先崩了所有节点的状态都变成不确定监控里心跳曲线像心电图一样乱跳连SSH管理通道都断了。后来定位才发现问题出在“隔离范围”上。我们只想着断掉节点间的数据通信却把管理网、心跳网全部纳入了丢包规则导致集群最基础的故障检测机制也被干扰。相比直接拔线用iptables或tc工具模拟网络分区时必须明确区分网卡数据面网卡可以做丢包管理面网卡绝不能碰。而且这类测试一定要有独立的“仲裁通信路径”否则会看到主节点反复横跳根本分不清是测试预期内的切换还是工具误伤。正确做法是先规划网络拓扑把业务网络、内部通信网络、管理网络分离开。然后用tc工具只作用在指定的网卡上模拟不同比例的丢包和延迟tc qdisc add dev eth1 root netem loss 50% tc qdisc add dev eth1 root netem delay 200ms测试结束后记得立即删除规则tc qdisc del dev eth1 root。我们有一次就是因为忘记清理后续功能回归阶段所有事务都慢了几百毫秒白白浪费两天排查时间。1.3 监控采集链路的数据丢失与时间戳错乱压测还没开始我们就发现监控数据有缺失Prometheus页面上指标曲线断断续续时间戳也对不上。起初以为是采集间隔太长后来把抓取间隔从15秒调到10秒依然丢点。真正的原因是两个监控agent部署在容器里宿主机时间与容器时间不一致导致样本时间戳乱序另外就是抓取器配置了多个目标部分目标因为网络超时被标记为down。这个问题的麻烦在于监控数据一旦出错后续所有性能分析都不可信。我们在问题记录里专门加了一条“可观测性验证用例”每次测试前先确认所有监控target是up状态时间偏差不超过100ms并且用一个已知固定值的计数器指标验证落库情况。后来给所有节点统一配置了NTP定期同步并让监控agent直接读取宿主机时钟数据缺口才彻底消失。环境阶段还有一个容易忽略的点监控系统本身也会占用网络和磁盘。如果指标采集频率过高压测时会和数据库抢IO。我们最终把基础指标采集频率控制在15秒每秒请求数之类的业务指标单独用5秒高频采集保证监控不会成为压测瓶颈。2. 功能回归阶段从失败用例到根因定位的全过程2.1 并发DDL与DML的元数据锁等待功能回归第一周就撞上一个有意思的失败一边跑TPC-C负载一边执行ALTER TABLE ADD COLUMN结果业务侧大批DML报错错误信息指向“元数据版本冲突”。从应用日志看DDL执行时间并不长但它在执行期间似乎在等待某个全局锁导致后续写事务全部排起长队。排查链路先走了弯路我们一开始盯着SQL本身以为语法有问题。后来去查集群中的锁等待视图发现大量事务阻塞在“元数据引用计数”上才反应过来DDL在分布式数据库里不只是改一张表的结构它要同步元数据到所有副本这个过程如果和DML交错容易出现等待。真实生产环境一般会做DDL窗口测试环境却没这个约束于是问题每天必现。我们当时的处理办法比较务实把DDL拆成小批次窗口执行同时调小DDL并行度避免一次性创建过多索引。之后又把这类场景固化成回归用例专门验证“DDL与DML并发”下的提交成功率。这个问题的根因关键词最终被记成“元数据锁竞争”测试报告中建议产品团队后续提供更完善的在线DDL机制。其实这类问题在分布式数据库里不算罕见关键是测试时不要只验证正常路径应该主动构造并发冲突场景。测试用例里的等待超时时间阈值也需要调低比如从5分钟改成30秒这样能更快暴露问题而不是让事务一直傻等。2.2 大事务回滚拖垮写入链路的排查第二个问题更隐蔽。某个批量导入任务一次性更新了约800万行数据执行到一半因磁盘空间不足失败系统自动回滚。回滚过程没有立刻报错但接下来整个集群的写入性能断崖式下降所有新事务都被卡住。这条排查链路非常典型先看写入TPS表现是“写到一半直接不动”再看磁盘发现日志盘已经打满继续追发现大事务产生的回滚日志量和复制量都远大于预期各个副本都在忙着处理同一个事务的回放日志盘被回滚记录塞满正常写入反而排队。问题记录里我们补充了一条重要认知大事务回滚的成本可能比执行更高因为它要把所有中间状态同步到所有副本。解决办法分成三层应用侧控制单事务操作行数超过阈值强制分批数据库侧设置大事务保护执行前预估代价超过限制直接拒绝运维侧把日志盘告警阈值提前到80%避免被打满后才知道。后来我们还专门做了一次对照实验同样800万行数据一批提交分成10批提交总耗时反而更短系统稳定性也更好。测试环境里能用数据说话的结论在复盘会上最有说服力。2.3 分区裁剪失效导致的扫描放量有一类问题最容易被归咎于“数据库变慢了”实际上却是查询语句写得不好。当时一个报表查询单表有256个分区按日期分区。业务反馈查询越来越慢我们从执行计划看到它扫描了全部256个分区而不是预想中的1个分区。原因很简单分区键是字符串类型但查询条件传的是数字优化器为了匹配类型在条件上做了隐式转换导致分区裁剪失效。这种问题在单机数据库里可能只是性能下降在分布式数据库里会被放大很多倍因为它不止扫描数据还会把扫描任务下推到所有节点网络开销直接拉满。定位过程并不难难点在于养成“先看执行计划”的习惯。测试环境下我们统一要求所有慢查询必须保存执行计划快照并记录预估扫描行数和实际返回行数。这条截图可以直接作为问题记录的一部分。后来在问题记录模板里加了“执行计划是否发生变更”一栏很多性能问题靠这一项就能快速定位。排查时我们也会用EXPLAIN输出关键信息比如访问分区数、估算行数、是否使用索引。这类信息对定位分区裁剪问题非常有用比直接看耗时数字靠谱得多。3. 性能压测阶段瓶颈往往不在引擎本身3.1 连接数与线程池配置的隐性上限压测进行到并发500时TPS反而比并发200时还低而且报错集中在应用侧连接获取超时。这个问题听着像数据库扛不住实际查下来数据库压力远未到极限卡点卡在连接数上。我们拆开看整个链路客户端连接池的最大连接数配成200但压测工具模拟的业务线程数是300意味着后面100个线程一直在排队等待连接。服务端这边也没好到哪去工作线程数上限设置过小空闲连接又占了大量内存导致有连接时没人处理有人处理时连接又被占满。这类问题的排查顺序应该是“先看客户端再看接入层最后看服务端”。客户端连接池参数是否匹配压测并发数是最容易忽略的一层。服务端的最大工作线程数也要和CPU核数挂钩不能盲目调大否则线程上下文切换反而吞掉性能。我们后来把连接数相关参数整理成一张对照表配置层级容易踩的坑建议做法客户端连接池最大连接数小于压测并发连接数按峰值并发上浮20%接入层连接排队无超时提示设置快速失败和排队上限服务端线程池线程数远超CPU核数以CPU核数的2到4倍为基数空闲连接连接长时间占用资源定期心跳回收空闲连接实测调整三层配置后并发200和并发500的TPS曲线终于符合预期。这个案例也提醒我压测问题记录不能只记“数据库慢”还要记录哪个环节先出现排队否则优化方向会跑偏。3.2 热点行更新放大分布式事务开销压测模型如果设计成“所有请求打同一行”分布式数据库的性能曲线会非常有趣TPS先冲高然后迅速回落最终稳定在一个很低的值。我们第一次跑热点行更新时以为集群出了故障找到问题后发现其实是分布式事务叠加热点竞争。原理并不复杂分布式数据库的多副本同步要求事务提交时协调多个节点的状态热点行本身是全局唯一的写入目标所有写事务都在同一把锁上竞争协调开销被成倍放大。单机数据库也有热点问题但在分布式架构下每个事务都多了一层跨节点协调热点行几乎成了必然瓶颈。我们做了两组对比一组是均匀分布写入另一组是10%热点行更新。前者的TPS是后者的7到8倍这个差距就是分布式事务协调开销的直观体现。应用层改起来也比较直接把库存这类热点数据拆成多个子账户比如按尾号分桶更新时随机选一个桶最后再汇总虽然业务逻辑变复杂但吞吐量提升明显。测试层面的教训是压测模型一定要包含热点场景不要只跑均匀分布的数据。很多分布式数据库在均匀负载下表现很好一到热点就原形毕露这类结果反而比峰值TPS更有参考价值。3.3 慢SQL并非引擎慢驱动与协议开销实测性能压测中最容易“冤枉数据库”的是那些数据库内部执行很快、但整个请求却很慢的SQL。我们遇到一个报表查询应用侧耗时2秒数据库侧执行计划显示只需要20毫秒差距整整100倍。排查时先怀疑是网络延迟结果内网ping只有0.3ms。接着怀疑数据序列化问题最后在驱动层面找到了原因查询默认的fetchSize太小返回大量数据时需要来回往返多次每次往返都多一次网络开销和协议解析同时应用进程中生成结果集对象消耗了大量CPU。解决方式也很直接调大fetchSize并开启批量拉取启用结果集压缩把连接从短连接改成复用连接池。改动之后同一个查询的服务端执行时间不变但应用侧耗时从2秒降到180毫秒。这类问题在问题记录里应该被标记为“客户端优化”而不是“数据库缺陷”。复盘时我们会把慢SQL分成两类一类是执行计划确实慢需要优化SQL或索引另一类是执行计划快但端到端慢需要排查驱动、网络和序列化。分类清楚后性能优化组的工作量能省一半。4. 高可用演练阶段切换、脑裂与数据校验4.1 节点宕机后主备切换超时的参数归因高可用演练是问题记录里最刺激的部分。我们模拟主节点宕机预设的是30秒内完成切换结果实际花了90多秒期间业务完全不可用。第一反应是故障检测机制太慢翻日志发现heartbeat没有立刻中断而是等了大半个周期才意识到节点失联。不过这只是表面原因深挖之后看到两个关键点一是宕机前备节点有大量日志没同步完切换时要先把这些日志拉过来并apply拉日志和回放都耗时二是心跳超时阈值设置偏保守业务可用性要求和故障检测灵敏度没有对齐。这两点叠加起来就让RTO远大于预期。调整思路分两条线业务侧把故障检测周期调短代价是更容易受网络抖动影响所以只在独立网络环境的测试集群里这样做数据侧则是给同步通道增加并发度同时让备节点定期预apply日志避免切换时临时补课。调优后再做三次同类演练切换时间稳定在25秒左右。高可用测试最忌讳只看次数不看结果每次演练都要记录“故障注入时间、切换完成时间、恢复后数据校验时间”三段数据否则很难判断优化是否真的有效。4.2 网络分区下的选主震荡与仲裁者配置第二个高可用问题更棘手。我们用iptables模拟两个节点之间的网络分区预期是少数派停止服务多数派继续正常选主。然而实际表现是主节点频繁切换一会儿A节点当选一会儿B节点当选活像“反复横跳”。这个问题如果不记录很容易被归结为“选举算法有Bug”。追根溯源后发现选主震荡源于三个因素的叠加分区规则把仲裁通信也切断了导致部分节点收不到足够多的确认消息各节点之间时钟偏移较大选举版本号比较时出现不一致心跳超时阈值太短系统把一次正常拥塞误判成节点故障。解决方案是三管齐下物理上保证仲裁通信链路独立即使数据面分区也不能断仲裁逻辑上统一启用时钟同步偏移控制在50ms以内参数上适当调大故障检测时间避免“假死”导致的不必要切换。最终在反复演练中网络分区场景才表现得符合预期。这类测试一定要记录“发生脑裂时两边各自的数据状态”否则切换期间很容易出现双主或者数据不一致。我们每次做分区演练前都会做一次全局数据快照用来和演练后数据做对比。4.3 数据校验不一致的排查与回补策略切换演练结束后我们照例对核心表做数据校验结果发现某个业务表的行数对不上新主节点比原主节点少了一些数据。那一刻所有人都紧张起来第一反应是“数据丢了”。冷静下来后并没有急于修数据而是先复现校验过程。最终定位到问题出在“校验时机”上切换刚完成时还有部分日志在异步回放新主节点尚未完全追平此时做行数对比自然看到差异。这类“伪不一致”在分布式系统里很常见不等于真正的数据丢失。解决办法是等日志回放彻底追平后再做校验最好使用一致性快照点而不是任意时间点。为了验证没有真丢数据我们用原主节点的备份文件做日志回放把差额数据补齐然后再过一轮全量校验最终两边完全一致。整个过程下来所有人都对“切换后立即校验”这件事有了警惕问题记录里也专门写了一条高可用演练后的数据校验必须先确认数据追平状态再做对比。这个案例给我们最大的经验是数据不一致不能上来就修复先分清楚是“校验工具不一致”“日志未追平”还是“真实数据丢失”否则修复动作本身可能掩盖更多问题。5. 从问题记录到改进闭环我的复盘思路5.1 每条问题记录应该包含哪些信息测试过程中最多的时候一天记十几条问题如果只是复制粘贴错误日志过两周再看根本找不回当时上下文。我在OB项目里整理出一套问题记录模板后来一直沿用了下来每条至少要包含八个字段字段说明发现时间与环境哪个测试环境、哪轮测试、哪个版本现象描述可观测到的指标变化、报错信息、业务影响复现步骤能稳定复现的最小操作集合首次排查方向当时先查了什么、为什么这么查根因分析最终定位到的根本原因不写中间猜测临时规避手段当前版本下怎么绕开问题回归用例编号问题是否沉淀成了自动化用例关闭状态待修复、已修复、转为需求、无法复现这套模板的价值在于“问题记录不是用来证明谁出错了而是用来支持下一次决策的”。我能清晰回顾当时排查顺序为什么走了弯路下次再遇到类似问题就不会重复浪费时间。写问题记录还有一个技巧在“现象描述”里一定写客观指标比如“TPS从12000降到3000”而不是“系统变慢了”。客观数据能让其他人直接判断优先级不会因为主观描述吵来吵去。5.2 回归用例和压测基线的沉淀如果没有回归用例问题记录充其量是一堆历史事故说明。我们在每个问题收尾时都会要求填写“回归用例编号”没有编号的就当场补。大事务回滚问题对应“分批提交压力测试”DDL与DML并发问题对应“在线DDL并发回归”热点行问题对应“热点更新压测模型”。这套用例库不断积累之后每次新版本发布跑一遍就能覆盖历史踩过的坑。压测基线也一样。我们给每轮压测保存了一份基线文档包含环境参数、压测模型、数据量、并发梯度、峰值QPS和延迟分布。没有基线你说“性能变好了”或者“变差了”都缺少参照物。后来有一次新版本压测结果比上轮低了15%一开始以为是优化没生效翻基线才发现压测数据量翻了倍单位数据量的处理能力其实提升了。所以基线一定要和数据量挂钩否则横向对比没有意义。5.3 复盘会的重点根因分类 vs 责任归属项目复盘会上我们刻意把根因分成三类环境配置类、代码缺陷类、测试设计类。环境配置类数量最多占了一半以上但修复成本最低代码缺陷类虽然占比不高影响却最大测试设计类最容易被忽视比如压测模型不覆盖热点、高可用演练不校验数据追平状态这些问题会导致问题根本测不出来。复盘时不讨论“这是谁负责的模块”只讨论“我们为什么没有更早发现它”。比如监控数据丢失如果能在环境准备阶段被发现后续很多性能问题就能更快定位。用这种视角看问题最终会得出一个很扎心但有用的结论一大半问题不是被测系统的问题而是测试环境和测试方法的问题。修好这些被测系统的真实质量才会暴露出来。我把这个思路也写进了我的测试问题记录末尾每次动手前先读一遍问题记录不是给领导看的是给下一个踩坑的自己看的。所以每一步操作、每一个当时看来很蠢的判断我都会写进去。说不定过几个月回看这篇文章里的一些踩坑过程也能帮你少走一段弯路。