ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

气人Bug排查指南:从复现到定位的系统方法

气人Bug排查指南:从复现到定位的系统方法 写 Bug 排查这件事我已经写过很多次了。但每次遇到“这Bug太气人了”这种标题我还是会点进去看。不是想围观别人倒霉而是因为所有气人的 Bug 背后通常都藏着一条可以复用的排查经验。这篇文章就是想把“气人”拆开看Bug 到底为什么难查、卡在哪一层、用什么顺序能更快定位以及修复之后怎么判断真的没事了。适合谁看刚入行还在被报错追着跑的开发者写了好几年代码但看到偶现 Bug 就头疼的同行还有带测试团队、天天被“这功能刚才还好好的”这句话折磨的人。最值得关注的不是某个具体 Bug 的修复代码而是一套能套用到多数问题上的排查思路。1. 先承认一个事实气人的 Bug 往往不是“难”而是“藏得太深”先说一个容易被忽略的点。很多 Bug 让人生气不是因为原理多复杂而是因为信息不对称。你盯着代码看了半小时觉得逻辑没问题。日志也在正常打印数据库连接也没报错前端传过来的参数看着也对。但功能就是不对。这时候你会觉得不是 Bug 的问题是整个世界的问题。这类遭遇有一个共性我们缺少一条足够清晰的排查链路。1.1 气人 Bug 最常见的四个特征以我多年看报错和写代码的经验气人 Bug 通常有这些特征第一复现不稳定。跑十次可能只出一次问题而且触发条件不明确。你刚想截图它又好了。这种情况最消耗耐心因为你没法确认修复到底有没有生效。第二报错信息本身有误导性。报错指向 A 文件但问题实际出在 B 模块。比如前端提示接口超时后端日志却显示接口根本没有收到请求那可能是网关配置或者网络代理的问题。第三只在特定环境出现。开发环境一切正常测试环境偶尔报错生产环境必现。这种环境相关的问题往往和依赖版本、系统权限、内存分配有关而不是代码逻辑本身的毛病。第四依赖链太长。一个功能后面串了前端页面、接口网关、业务服务、消息队列、数据库、文件存储任何一环出问题最后表现出来的现象都差不多结果不对。但定位成本会成倍上升。1.2 把“好气”变成“好查”切换成 Bug 观察员视角如果每次遇到 Bug第一反应是烦躁那排查效率一定低。因为人一生气就容易跳过步骤直接猜。我建议换个心态把自己当成 Bug 观察员。Bug 不是冲着你来的它只是一个状态异常的系统表现。你要做的不是和它斗气而是观察它在什么条件下出现、在什么条件下消失、报错信息里哪些词是真正有用的。这个视角不只是心理安慰它直接影响排查方式。观察员会记录现场会保留第一现场的证据会先确认现象再下结论。而一个带着情绪的开发者在干嘛大概率是打开代码一顿改改完发现 Bug 还在然后更生气了。所以这篇文章第一个结论是遇到气人的 Bug先不要急着证明自己写得没错。先接受“系统里确实有个我们还没看透的状态”然后按照固定顺序去查。2. 把“气人”拆开Bug 到底卡在哪一层排查 Bug最怕的就是在错误层级上使劲。前端问题去后端查日志查了半天当然没有结果。接口问题去改数据库累死也修不好。所以遇到问题第一步不是看代码而是先分层。2.1 如何区分前后端 Bug先看请求进出很多合作场景里前后端同事会互相认为问题在对方那边。怎么快速分清楚我的顺序很简单。先打开浏览器开发者工具看 Network 面板。如果请求发出去了但响应结果是错的说明后端返回的数据有问题或者前端对数据的解析有问题。这时要看响应体本身是数据结构不对还是状态码不对。如果请求根本没有发出去问题大概率在前端。可能是按钮事件没绑定上、表单校验没通过、接口地址配置错了。如果请求发出去了但状态一直是 pending最后超时那问题可能在后端处理时间过长也可能在网络层。比如后端在等锁、在重试外部接口、在同步下载大文件都会导致响应超时。这里有个很容易误判的情况前端显示报错不是后端直接拒绝而是网关超时。这种问题不能只盯着业务代码还要看超时阈值和日志时间线。2.2 再分环境是代码逻辑问题还是环境和依赖问题如果问题只在特定环境出现就不要急着改代码逻辑。先对比正常环境和异常环境的差异。需要检查的点包括依赖版本是否一致、配置文件是否区分了环境、系统时区是否一致、磁盘空间和内存是否充足、文件夹读写权限是否正确、有没有杀毒软件或安全策略拦截、数据库版本和编码格式是否一致。我见过太多例子本地跑得好好的部署到服务器就报错。最后发现是服务器上某个 Python 包版本和本地不一致。这种事情看起来像代码问题实际上就是环境问题。所以我的建议是遇到环境相关 Bug第一时间把两边的版本信息拉个清单逐项对比。不要靠记忆一定要把实际命令的输出贴出来看。2.3 用一张表格梳理排查起点下面这张表是我自己排查时常用的分层判断表。遇到 Bug 先按这个思路归类能省掉很多无用功疑似层次先看什么确认方法常见误判前端交互浏览器 Console、Network看请求是否发出、响应状态码把前端解析问题当后端报错后端接口接口日志、状态码、耗时看请求是否到达、返回结构网关超时被当成业务逻辑错误数据处理输入参数、输出结果用最小样例单步验证数据结构不对当成接口 Bug环境依赖版本、权限、资源占用对比正常环境包版本不一致当成功能 Bug系统资源内存、CPU、磁盘top、free、df 命令内存溢出被当成代码逻辑错误这一层判断做对了后面排查就会非常顺。做错了可能折腾一整天都找不到方向。3. 从“来 Bug 了”到“验证修复”看懂 Bug 生命周期游戏测试里有个概念叫 Bug 生命周期大意是一个 Bug 从被发现、提交、确认、修复、验证到关闭中间会经过多个状态。很多人只在“发现”和“修复”两个状态里打转忽略了其他环节导致问题反复出现。其实自己排查 Bug 时也应该走完整条生命周期。表面看是流程实际上是逼自己把每个环节的证据补齐。3.1 复现先确认“稳定复现”还是“偶现”排查第一步永远是复现。如果连复现都做不到后面所有判断都有风险。稳定复现的问题最好查。只要找到固定的操作路径就能用二分法逐步缩小范围。偶现的问题麻烦一些我一般会先尝试提高复现概率。比如并发类问题就压测大数据量问题就灌入更多数据时序问题就反复快速点击或连续提交。如果实在无法复现就尽量保留现场。业界的通用做法是收集日志、抓包、记录操作时间线、导出当时的输入数据。没有现场修 Bug 就像盲猜。3.2 定位用“二分法”和“最小复现”定位时最常用的思路是二分法把出问题的那条链路切成两半看哪一半正常、哪一半异常然后继续切。比如一个接口返回数据不对。你可以先看数据库里的原始数据对不对。如果对说明问题在数据处理逻辑如果不对说明数据写入阶段就有问题。这样每查一次就能把范围缩小一半。另一个配套手段是构造最小复现。把输入数据从几千条减到几条把操作步骤从五个减到两个直到能用一个最简单的样例触发问题。很多时候你能在缩样例的过程中直接看出问题根本不需要继续查。这里要特别提醒一点不要把“最小复现”和“模拟生产”混为一谈。最小复现是为了定位模拟生产是为了验证修复是否完整。两者目的不同不要用同一个脚本做这两个阶段的事。3.3 修复改代码之前先回答三个问题很多 Bug 修了又犯不是因为代码没改对而是因为根本没有回答修复前的三个问题第一这个 Bug 产生的直接原因是什么。不能只说“这里有空指针”要说出“为什么这个变量在这里会是 null”。第二这个修复会不会影响相邻功能。改一个公共函数时尤其要注意。第三这个问题只在这个入口出现还是其他入口也会触发。如果是后者必须把所有相关调用点都查一遍。回答完这三个问题再动手改代码。改完之后先跑针对性的测试再跑回归测试。不要觉得回归测试浪费时间实际生产里大量“修复了一个 Bug带出了两个新 Bug”的悲剧就是缺少回归这一步。3.4 验证不报错不等于修好了验证环节是最容易被糊弄过去的。很多人看到 Bug 不再复现就觉得已经修复了。但对于偶现问题这远远不够。正确做法是至少跑三次以上确认把触发条件反复执行。如果之前是压测时出现的修复后要重新压测如果之前是特殊数据导致的修复后要把那批数据再灌进去。还要关注副作用。比如你为了修慢查询给一个表加了索引结果数据写入变慢了这算是一个新问题。所以修复验证一定要同时关注性能和稳定性。3.5 沉淀把当天的“气人”变成以后的经验Bug 关闭之后花十分钟写一条记录。不用写长文就记五件事现象是什么、根因是什么、用什么方法定位的、修复方案是什么、这类问题以后再出现先查哪里。三个月后你会发现很多让你抓狂过的问题其实都有共同模式。有了记录下次遇到类似问题可能十分钟就能定位。没记录下次还要从头追一遍。4. 几个真实存在的气人 Bug 类型附排查顺序热搜里那些 Bug 关键词看着零零散散但归类之后其实就那么几类。这里挑几个典型的展开说一下每类我都会给出通用排查顺序。注意具体的修复代码要看你的项目环境但排查思路是通用的。4.1 依赖和构建类npm 的 native binding 报错有一种报错很长里面带一句cannot find native binding看起来像是某个原生模块没有装好实际原因可能有好几种。我遇到过的可能性包括 Node 版本和原生模块不匹配、npm 缓存脏数据、安装过程中网络中断、平台特定的二进制包缺失、node_modules 目录权限不对。排查顺序建议这样走删除 node_modules 和 lock 文件重新安装。确认 Node.js 版本符合项目要求。查看安装日志确认有没有后半段失败的记录。单独执行该模块的构建脚本看具体报错。对比能正常运行的其他机器环境。这种问题最不能做的就是反复删除重装但不变更环境。如果重装两次都没解决一定要把注意力从“装不上”转移到“环境哪里不对劲”。4.2 模型推理和长文本类重复输出、chunk 设置异常近两年大模型相关 Bug 明显变多。比如同一个对话超过一定长度后就出现重复回答或者在推理框架里设置 chunk_size 后结果异常。这类问题有一个共同特征问题不在业务代码而在输入长度和推理参数之间的配合。排查时先看输入长度和上下文窗口的关系。如果输入接近模型长度上限又采用了比较激进的截断策略输出出现重复内容并不奇怪。再看推理框架的配置参数比如 chunk_size 这类影响批量切分的参数它不能只是“设一个值就完事”要确认它和数据输入长度、显存大小匹配。我一般会在小数据量上测试多组参数记录显存占用和输出质量而不是直接用生产环境的大参数验证。遇到这种 Bug最忌讳的是反复修改模型相关的提示词而忽略了下游推理参数和数据处理逻辑。4.3 存储和任务状态类卷分离失败、存储 Bug存储类 Bug 特别容易气人因为它们往往在运维操作或者任务执行的中段才出现。比如 OpenStack 环境里 Cinder 卷在分离时失败或者某个存储工具在写入量到一定规模后报错。这类问题的排查顺序应该是先确认操作的完整日志尤其是超时时间和重试次数。检查存储所在主机的资源状态磁盘、内存、文件系统占用。查看锁机制确认是否有其他任务占用了同一个卷或资源。检查存储驱动版本和云平台版本是否兼容。最后才考虑是不是存储服务的代码缺陷。存储类问题的难点在于它可能当时没有立刻报错而是延迟到下一次操作才失败。所以排查时不能只看当前日志要把操作前一段时间内的系统事件、内核日志一起拉出来对照。4.4 嵌入式外设类DMA 通道和缓冲区溢出嵌入式开发里有一种 Bug 特别难查DMA 通道配置看起来没问题外设也能初始化但数据传输总是偶尔出错。这类问题的特点是对时序敏感和普通软件 Bug 的排查方式有很大差异。我的建议是先确认 DMA 通道的分配关系看是否和中断优先级有冲突再核对缓冲区大小和传输长度是否匹配。如果配置的是循环模式还要检查边界处理逻辑。缓冲区溢出也是嵌入式里的常客。像systemsetting检测到基于堆栈的缓冲区溢出这类提示本质上说明有函数访问了超出栈范围的内存。排查时要先看违规发生的函数栈再往上找哪个调用方的数据长度和处理逻辑不匹配。这类 Bug 不建议靠猜最好能在调试器里跑出具体的溢出点然后顺着调用链找到源头。4.5 界面和平台兼容类滑动异常、页面退出还有一种 Bug 和具体技术栈无关纯粹是平台兼容问题。比如在某个品牌的手机上访问数据平台页面上下滑动时出现异常退出。这类问题排查时要优先确认是不是 WebView 内核版本和页面代码不兼容再看页面里是否有特定 CSS 属性或手势事件触发了浏览器 Bug。还可以在浏览器开发者工具里通过设备模拟方式复现但要注意模拟并不能百分之百还原真实硬件上的行为。如果复现不了就在真机上抓日志重点看页面崩溃时的崩溃堆栈。没有崩溃堆栈就不要急着改页面代码。4.6 一个必要的提醒在线测评环境的 Bug像“头歌测评遇到的 Bug”这类问题很多不是代码逻辑错了而是测评环境的规则和本地不一致。比如输入输出格式的细微差别、判题脚本的换行处理、编译器版本差异。遇到这种情况先不要怀疑题目有问题而是仔细核对自己的输出和题目要求特别注意空格、换行、浮点数精度。把输出在本地和在线环境各跑一次逐字节对比一般都能发现问题。5. 测试中的 Bug怎么用工具辅助定位而不是背锅测试人员发现 Bug开发人员第一反应经常是“复现一下”。但有些 Bug 操作步骤很长手动复现效率很低。现在有很多自动化工具可以做辅助定位可以用它们把“偶现”变成“可复现”。5.1 用浏览器自动化工具复现前端问题比如 Playwright 这类浏览器自动化工具可以记录用户操作步骤然后反复回放。如果 Bug 在特定操作路径下出现写一个自动化脚本就能在几分钟内跑几十遍比手工点击要可靠得多。关键是要带着“复现路径”去写脚本而不是随便打开一个页面就点一下。比如 Bug 是某个弹窗关闭后再打开会出现样式错乱脚本里就要完整执行“打开、关闭、再打开”这个路径。脚本跑起来之后可以配合截图、录屏、收集控制台日志等功能把现场证据保存下来。这样在给开发提 Bug 时就能直接附上完整复现脚本和异常日志沟通效率会高很多。5.2 区分测试脚本问题还是产品 Bug用自动化工具测出来的异常也不一定就是产品 Bug。可能是测试脚本本身定位元素不稳定、等待时间不够、测试数据和环境冲突。判断方法很简单把自动化脚本里做的每一步手动执行一遍。如果手动执行正常问题大概率在脚本稳定性如果手动执行也有问题那才是产品 Bug。这个区分非常重要。不然你花了半天定位最后发现是脚本没等页面加载完就白折腾了。5.3 偶现问题怎么用日志和时间线定位偶现问题最怕没有时间线。不同系统的日志时间如果不同步你很难把前端请求和后端日志对应起来。所以在排查这类问题前先确认所有机器的时间是同步的或者至少在日志中记录了完整的调用链路 ID。有了统一的时间线再把前端报错时间点、接口请求时间点、后端异常时间点放在一起看。你会发现很多“偶现”其实是某个中间件在特定时间触发了一次清理任务或者某个服务的线程池在高峰期被占满。这类问题靠口头沟通很难发现但看时间线一目了然。6. 把“气人 Bug”变成“普通 Bug”的几个排查习惯说实话排查经验积累到一定程度后大部分 Bug 就都不气人了。不是因为 Bug 变简单了而是因为你会自动按优先级去查不再被表象带着跑。我一直保留着几个习惯这里直接列出来可以参考。6.1 先看日志再改参数很多时候问题还没查清楚人就已经开始调参数了。比如程序变慢了下意识就把并发数调大。这其实很危险。正确顺序是先看慢在哪一步。如果慢在数据库查询调并发没有意义如果慢在外部接口等待加线程数反而可能拖垮下游服务。先看日志找到时间消耗主要集中在哪里再决定动什么参数。6.2 一次只改一个变量排查时最忌讳同时改多个东西。比如你既升级了依赖版本又改了代码逻辑还换了配置文件。如果 Bug 消失了你根本不知道是哪一步起的作用。如果 Bug 还在你也不知道是哪一步没起作用。所以我坚持一次只改一个变量。改完跑测试记录结果。再改下一个再跑测试。这样每一步的因果都很清楚。虽然看起来慢但总比在多个方案之间来回试要快得多。6.3 不要被“玄学”带偏程序员圈子里流行“神兽保佑·代码无 Bug”更多是一种自嘲和一时的心理安慰。真到了排查阶段还是要靠证据。如果一段代码“有时候正常有时候不正常”我第一反应不是系统有问题而是某个资源没有被正确释放或者某个状态在特定条件下没有复位。这种间歇性异常通常都能在资源使用和状态管理上找到根因。排查思路是先看这个模块期间有没有共享全局状态再看有没有并发写入接着看有没有用到定时器或异步回调最后看有没有依赖外部缓存。大多数“玄学 Bug”都是这三类原因之一。6.4 低配环境先跑通再谈压测优化有些人一拿到项目就想着压测高并发结果环境配置不够直接各种超时和报错还以为代码有问题。正确做法是在低配环境上先把流程跑通确认核心逻辑正确再逐步提高并发和负载。低配环境能跑通至少说明基本逻辑没问题。高并发下出的问题往往就是资源竞争、连接池、锁和超时这些在低负载下暴露不了的边界问题。分阶段压测才能精确定位是逻辑问题还是容量问题。6.5 维护一份自己的“气人 Bug”清单最后我强烈建议维护一份属于自己的 Bug 排查笔记。不用很正式按日期记录就行。内容包括问题现象、根因、定位方法、修复方案、可复用的排查命令。比如我自己的笔记里就有一条记录“接口偶发超时先看网关日志再看业务日志里有没有等待数据库连接池的迹象。到底是连接池耗尽还是慢查询要有数据支撑。”这条记录就是从一次排查了三个小时的 Bug 里沉淀下来的。以后再遇到类似问题先翻自己的笔记基本上十分钟就能进入状态。有时候你以为遇到了新 Bug其实是旧问题的变体。有笔记的人永远比没笔记的人淡定。说回“这 Bug 太气人了”。我自己也有过很多次想砸键盘的时刻但冷静下来复盘发现真正花时间的从来不是修 Bug 本身而是定位 Bug 阶段的反复试探。文章里这些方法和排查顺序都是从那些试探中总结出来的。下次再遇到 Bug别急着生气。先拉日志先复现先分层再动手。你会发现很多问题其实并没有想象中那么难缠。
返回列表