尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

软件调试实战:从bug分类到最小复现与日志分析

软件调试实战:从bug分类到最小复现与日志分析 最近在看一条跑团熟肉标题是【coc跑团熟肉】神话与科学 part2 馒馒来克苏鲁神话副标题直接写着“bug一样鬼畜的克苏鲁神话三部曲 第二部”。这里面的“熟肉”是指已经翻译好字幕的视频不是吃的。我点进去本来是想看跑团结果发现真正吸引人的反而是那些规则漏洞、逻辑冲突和鬼畜展开弹幕几乎全在刷“bug”。同样是“bug”放到软件开发里就完全不好笑了。一个小问题可以让人从下午两点折腾到晚上十点日志看了一遍又一遍AI 也帮忙分析了一轮又一轮最后发现只是依赖版本没对上。这篇文章不聊具体剧情只借“bug一样鬼畜”这个由头把排 bug 的顺序、方法、边界和工具链问题从头到尾整理一遍。如果你最近正好被某个 bug 卡住可以直接按下面的顺序过一遍。1. 排 bug 之前先把问题分成三类不要急着改代码大部分人拿到 bug 的第一反应是打开代码开始猜。比如看到报错里提到某个函数就立刻去改那个函数或者看到日志里有一个异常就马上搜索异常怎么解决。这种做法偶尔能蒙对但大多数时候会浪费时间因为报错位置往往不是问题根源。我更建议先做一次分类。根据复现特征bug 大致可以分为三类稳定复现型、偶发型、环境相关型。分类不同处理方式完全不同。1.1 能稳定复现的问题等于白送如果一个 bug 只要执行固定的操作路径就必然出现那它其实是最好处理的。你可以放心地在复现环境里打断点、加日志、逐步跟踪甚至不用猜直接看每一步的中间结果就能定位。处理这类问题时唯一要小心的是不要把“环境问题”误判成“业务问题”。比如同样的代码在 Windows 上运行正常在 Linux 上每次必现那基本不是代码逻辑的问题而是大小写敏感、路径分隔符、换行符、动态库加载路径或权限模型不同造成的。这时候应该先去对比环境差异而不是改业务代码。实操中可以列一个简单的复现记录项目内容触发步骤从哪个页面进入到哪个按钮结束输入数据文件、文本、参数、账号运行环境操作系统、语言版本、浏览器或运行环境版本报错信息完整堆栈不要只记最后一行是否必现连续三次执行是否都出现这张表填完之后你至少能判断出它是不是稳定复现型。如果是直接单步调试或加详细日志多半很快能找到问题。1.2 偶发问题先收集现场再怀疑代码偶发问题最烦人。有时候一天出现一两次有时候一周才出现一次。你盯着日志看只看到一段无关紧要的普通错误没有任何明显规律。这类问题通常和时序、并发、资源占用、缓存清理、随机初始化顺序有关。比如多个线程同时读写同一个全局变量某次调度顺序不对就会在极端情况下触发问题再比如对象被提前回收但只有在内存压力大的时候才会表现出来。对待偶发问题不要急着找原因。先把“现场”留全出现时间、系统负载、日志前后 30 秒的内容、当前版本、运行时长、网络状态、磁盘剩余空间全部记录下来。很多偶发问题不是逻辑错误而是资源竞争或生命周期问题必须通过现场数据还原发生顺序。我有一个笨办法但很有效如果问题一周出现一次就提前在关键模块里埋好可持续输出的日志包括函数入口、函数出口、耗时、关键变量值。等下一次问题出现时把这段日志拉出来按时间线对齐往往就能看到是哪一步先出了问题。1.3 环境相关的问题最容易伪装成业务 bug这种 bug 更隐蔽。它不会每次都触发只会在特定系统、特定语言版本、特定区域设置或特定显示分辨率下出现。表面上看像是业务逻辑写错了实际上换一台机器就完全正常。典型的例子包括某段代码在 Windows 路径下用反斜杠在 Linux 下就找不到文件正则表达式在 Python 不同版本里对中文和 Unicode 的处理不一致时区不同导致日期计算差几个小时DPI 缩放导致界面控件错位。遇到这种情况最有效的做法是做一个“干净环境对照”。在一台没有安装额外依赖、使用默认语言、默认区域的机器上跑同一个用例看是否还能复现。如果干净环境不复现大概率就是环境变量或系统设置引起的。这里可以顺带提一下 PyCharm 这类工具。很多人说“PyCharm 工具栏出 bug 了”界面按钮消失或错乱。很多时候这不是代码 bug而是缓存和索引损坏或者是第三方插件和主题不兼容。这时候先执行 File Invalidate Caches 清理缓存或者把配置目录迁移到默认位置再启动通常比翻代码更解决问题。2. 看日志不是看最后一行而是按时间线找因果链很多人打开日志文件第一件事就是搜索“error”“exception”然后盯着报错出现的那段一直看。这里有个误区报错位置只是结果原因通常都在它之前几秒甚至几分钟就已经发生了。日志的真正价值是按时间线还原操作过程。排查时要学会从报错点往前倒推而不是从报错点往后看。2.1 从报错往前倒推先看发生前的状态举个例子假设日志里出现一个异常说某个文件找不到。如果只看这一条可能会认为文件被误删了。但如果你往回翻 10 行可能发现是前面某个函数把路径参数里的目录名处理成了空字符串或者用户上传时没有带扩展名最终拼出了一个不存在的路径。看日志的正确姿势是这样先定位报错的精确时间点。再往前翻 30 行左右看发生前的调用链。关注前几条日志的时间间隔判断是否有卡顿、重试、超时。把报错前后的输入参数、输出结果、资源占用做一次对齐。如果日志量很大可以用关键字做过滤。但注意过滤不要只过滤 error还要过滤当前会话的请求 ID、用户 ID、订单号或任务编号。分布式系统里尤其如此没有 trace ID 的话把多个服务日志串起来会非常痛苦。2.2 CANoe 如何通过看日志查 bugCANoe 是汽车电子开发测试中比较常见的总线分析工具。很多人第一次接触时只会在 Trace 窗口里看到一排排报文不知道该怎么定位问题。尤其是出现偶发故障时要么是某条报文没发要么是发出来了但对端没响应。用 CANoe 查 bug我的顺序是这样的先停掉抓包把 Trace 窗口的报文按时间排序。找到故障发生的时间点看前后 1 秒内的报文是否连续。如果某个节点没发周期报文先检查它的电源、CAN 收发器、波特率配置。如果周期报文在发但对方没有 ACK再看错误帧和错误计数器。如果涉及多个 ECU 协同先用 filter 只保留相关 ID再做信号级别的对比。这里最常被忽略的是错误帧。error frame 在 Trace 里很容易混在正常报文中需要专门过滤。一旦发现错误帧频繁出现基本可以判断是物理层问题比如终端电阻、线路屏蔽、接地或波特率不一致而不是应用逻辑问题。2.3 内核日志里的 soft lockup 怎么读有些报错看起来和业务完全无关但它们是系统层面的重要线索。比如内核日志里有这样一条kernel: watchdog: BUG: soft lockup - CPU#2 stuck for 23s! [kworker/u32:3:2196]翻译一下内核看门狗检测到 CPU 2 在连续 23 秒内没有正常调度当前卡住的是一个 worker 线程。这种情况通常不是某一行 C 代码出错那么简单而是下面几类原因之一某段代码进入了死循环把 CPU 长时间占住。中断被长时间屏蔽调度器无法抢占。驱动在等待某个硬件寄存器但硬件一直没响应。锁竞争太严重某个内核线程一直拿不到锁。排查顺序我建议这样先看卡住的线程名和堆栈再查它对应哪个驱动或内核模块如果是 kworker就找它关联的 workqueue 任务再看有没有加载异常驱动最后排查硬件故障。不要一上来就去改业务代码因为这条日志大概率是系统底层引起的。3. 找到能稳定复现的最小样例等于解决了一半我观察到一种现象AI 修改一个小 bug 用时很久一直分析却迟迟给不出来。这个问题的根源往往不是模型不够聪明而是问题描述本身太模糊。你给 AI 看一个 500 行的报错堆栈再附上两个服务之间的完整调用链它可以分析出很多可能性。但当它每给一个原因你都说“不是”它就会继续扩大猜测范围。问题出在缺少一个关键输入最小复现样例。3.1 为什么要先最小化而不是继续堆日志最小复现样例的意思是把无关条件全部去掉只保留执行到 bug 的最小输入、最小代码路径和最小运行环境。一旦你做出了这个样例代码里的每一步都可以被检查不再依赖“概率”和“运气”。举个例子某个批量处理任务在运行到第 1000 条记录时崩溃。如果你直接拿 1000 条记录调试每次都要跑很久。正确做法是先用第 998 到 1002 条记录做一个小样本看能不能复现。如果小样本不复现就继续缩小范围或者从第 1000 条记录本身寻找特征。能做最小复现样例还意味着你能够给同事、给 AI、给开源社区提一个足够明确的 issue。很多人提 issue 时贴了一堆截图但没说输入数据长什么样、运行环境是什么、哪个版本、哪个模块。维护者看到这种问题大概率不会认真跟进。3.2 一个通用的精简流程固定、隔离、二分我自己常用的流程是三步固定输入。把触发问题的原始数据保存下来测试时只喂这一份数据不做随机变动。隔离模块。把和问题无关的日志、第三方调用、缓存、消息队列全部关掉。二分定位。把代码路径分成前后两半先跑前半看问题是否出现再逐步收窄到具体函数和具体参数。用这个方法最理想的结果是最后得到一段 10 行以内的代码和一组固定参数只要执行就会崩溃。到了这一步即使你不知道根因也可以把样例丢给静态分析工具或 AI让它们帮你判断。3.3 实在复现不了就做增强观测有些问题只在生产环境出现本地和测试环境都稳定。比如并发环境下偶发的死锁或者内存耗尽后触发的一连串异常。这种情况下不要硬复现而是增强观测。可以在关键业务方法里加耗时统计、线程数统计、队列长度统计把结果输出到独立日志文件。如果涉及外部接口可以增加参数序列化输出方便事后重放。如果进程会崩溃要保证核心 dump 能正常落地否则问题发生的一瞬间现场就丢了。这里还要提一个观念不能稳定复现不代表不能修。很多偶发问题在增强观测后都能找到稳定的前置条件。比如“只有队列长度超过 10000 时才出现”这已经是一个可以模拟的条件了。只要把条件构造出来它就从偶发变成了可复现。4. 依赖、环境和工具链往往比业务逻辑更容易藏 bug业务代码的问题通常一眼就能看出来反而是依赖冲突、环境变量、工具链内部的坑会让人在同一个地方反复摔倒。下面的几个例子都是真实场景中常见的我按现象和排查顺序拆开说。4.1 npm optional dependencies 与 native binding 的问题错误信息类似这样Cannot find native binding. npm has a bug related to optional dependencies这个报错很容易让人误判。第一眼看上去像模块缺失于是去 npm install结果装完还是报一样的错。实际上这种问题经常和 optional dependencies 安装失败有关。npm 在处理可选依赖时如果某个依赖安装失败默认可能不会让整个安装流程失败。但后续运行时软件需要加载对应的 native binding比如 node-sass、sharp、bcrypt 等模块这时就会因为找不到二进制文件而崩溃。排查步骤我建议这样来先清空 node_modules 和 package-lock.json重新完整安装一次。看看安装日志里有没有 optional、prebuild、postinstall 相关的警告或失败。如果是 native binding 缺失执行 npm rebuild 重新编译。确认编译工具链齐全。Windows 下需要 Visual Studio Build ToolsLinux 下需要 python、make、g。最后再检查 registry 镜像地址有些私有镜像源的 optional 包并不完整。关键判断是这样的如果重新安装后依然找不到 binding大概率不是 npm 本身的“已知 bug”而是你的环境和原生模块没有匹配。先检查 Node 版本再检查编译工具最后再考虑换包版本。4.2 U 盘安装 ISO 时遇到“已知 bug”怎么办有说法是“U 盘 ISO 安装程序确实有一个已知的官方 bug”。具体细节不同环境差异很大但这类问题有一个共同特点你按照正常方式做了启动盘结果系统还是起不来或者在安装过程中直接失败。处理系统安装类问题我建议按下面的顺序排查先校验下载的镜像文件。计算 SHA256 或 MD5和官方发布值对比。如果校验值不对后面全是白做。在虚拟机里先挂载 ISO 试一次引导确认镜像本身能不能启动。制作 U 盘后再拿另一台电脑测试。如果虚拟机正常但真机不正常重点检查 UEFI/Legacy 启动模式。有的 U 盘主控比较特殊量产工具写出来的 ISO 引导方式不标准这时候换一个写入模式或用更通用的写入工具反而能解决问题。不要把“官方 bug”当作唯一的解释。官方 bug 会有版本号、复现条件、修复版本如果你没有确认这些信息那很可能只是镜像损坏、启动模式不对或 U 盘兼容性问题。4.3 工具和数据库的“已知 bug”要分情况看待开发工具也会出 bug。比如 PyCharm 的工具栏偶尔显示异常很多人以为是代码问题实际上清理缓存、禁用第三方插件、重置窗口布局就能解决。数据库同样如此。像达梦数据库的 listagg 有 bug具体现象可能包括聚合结果顺序不稳定、空值处理不正确、字符截断等。遇到这种情况不要只想着升级版本还要先确认当前数据库版本是多少官方修复了吗。业务依赖的是不是 Oracle 兼容模式下的行为。改用另一种写法能不能绕开比如用 XML 聚合或窗口函数替代。有没有稳定的最小 SQL 可以把问题复现出来。这类问题的处理原则是先区分是工具自身问题还是你的用法问题。如果目标环境版本就是有 bug可以先做 SQL 改写或规避如果官方已修复再考虑升级。5. 有些 bug 很贵所以要拦截在发布前我见过不少团队上线后才发现一个很低级的 bug导致线上数据错乱。修复本身可能只要 5 分钟但数据回滚、用户投诉、客服解释却要花几天。很多 bug 真正的成本不在代码层而在“上线时间点”。5.1 “史上最贵 bug”的教训是提前暴露关于“史上最贵 bug”的说法有很多版本比如某个数值估算错误导致航天器坠毁或者某个精度问题导致大规模损失。不管具体钱数是多少共性的教训其实是同一个bug 越晚被发现修复成本越高。开发阶段发现的问题改一行代码就行测试阶段发现问题需要重新跑用例发布后发现的问题可能还要重启服务、回滚数据、写事故报告。这个成本是指数级上升的。所以不应该只靠“仔细点”来避免 bug。更可靠的方式是通过检查清单和自动化测试在进入发布流程前就拦截掉一批常见问题。5.2 安全关键场景可以用静态分析工具兜底在一些对安全性要求极高的领域比如航空、医疗、汽车电子光靠代码评审和单元测试可能不够。这时候可以引入专业的静态分析工具比如 Polyspace Bug Finder 和 Polyspace Code Prover。Polyspace Bug Finder 主要作用是在编译前找运行时错误比如除零、数组越界、空指针、非法类型转换。它不会真正执行代码而是通过静态分析扫描所有可能的路径。Polyspace Code Prover 更进一步会对代码做形式化验证证明某类运行时错误不存在。这类工具适合安全等级要求高的项目普通业务项目不一定需要。但思路可以借鉴在 CI 流程中加入静态分析、覆盖率约束、编译警告级别和关键路径的强制代码评审能明显减少低级错误流出。5.3 提交前的检查单比事后复盘更值我整理了一份能长期复用的提交前检查单内容不算复杂但每次改动上线前都值得过一遍检查项说明最小复现样例这个改动对应的 bug是否保留了一份可复现的测试用例日志是否齐全异常路径有没有记录入参、堆栈和时间戳失败重试批量任务中某一条失败能否跳过并继续执行输出命名批量输出的文件或记录是否有重复命名和覆盖风险并发安全是否涉及共享状态、全局变量、缓存一致性问题回归测试本次改动会不会影响上一个正常功能环境差异Windows、Linux、不同版本之间是否有明显行为差异如果一个修复方案没法通过最小复现样例来验证那它只能算“理论修复”不算是真正完成。这里还要再强调一次“失败重试”。很多线上问题不是逻辑写错而是面对超时、断网、磁盘满等临时故障时系统没有任何保护任务直接崩掉。提前设计好重试次数、退避策略和失败队列比上线后再补救稳妥得多。回到开头那部跑团熟肉。跑团里的 bug 可以当鬼畜看规则越乱越搞笑开发里的 bug 不行因为代价是真实的时间、精力和交付质量。我个人最想强调的还是三件事先把现场留好再找最小复现最后再看日志和依赖。这个顺序能帮你避开大部分无谓的返工。
返回列表