作者 | Shai Almog 译者 | 明明如月
责编 | 夏萌
出品 | CSDN(ID:CSDNnews)
“在我的机器上可以运行”这一说法虽然听起来有趣,但实际上反映了开发领域中一种普遍存在的态度,也就是只有当用户证明了 bug 的存在,我们才会对其给予关注。实际上,我们必须主动承担责任,深入调查问题,不论它会引导我们走向何方。
双管齐下解决 bug
解决 bug 需要采用双管齐下的方法。首先,我们要设法在与用户相同的环境中复现出可能只在用户电脑上才出现的问题。此外,我们还可能需要远程调试或利用用户计算机的日志,甚至可能请求他们替我们执行某些操作。
几年前,我曾尝试复现用户报告的一个 bug ,但即便我采用了相同的 JVM 版本、操作系统、网络连接等,仍未能找到该 bug。最终,用户发来一段展示缺陷的视频,我这才注意到了他在用户界面中特殊的点击方式。这揭示了一点:出现 bug 的过程不仅与计算机硬件有关,有可能会涉及到用户的具体操作。
用户行为与沟通在解决 bug 中的作用
我们需要尽可能地还原用户的操作行为,才能重现 bug 。通过视频来记录操作可以有助于我们实现这个目标。此外,理解复现环境中的细微差异及与能够重现问题的人进行开放、明确沟通都是不可或缺的部分。
然而,沟通的过程可能会遇到障碍。有时候,报告问题的人可能是支持部门的员工,而我们却是研发部门的。有时候,客户的不满情绪可能会导致沟通中断。因此,我认为将研发部门与支持部门整合是确保问题顺利解决的关键所在。
解决 bug 的工具与方法
深入分析运行中应用程序的一些工具,例如 strace、dtrace 等,可以让我们深入了解内部差异和异常行为。容器技术如 Docker 大大方便了统一环境的创建,从而消除了许多微小的差异。
我曾经调试过一个仅在客户现场出现故障的系统,最终发现他们的网络连接速度非常快,以至于与服务器之间的通信时间甚至比本地代码的执行时间还要短。我通过远程登录到客户的机器上并在那里复现了问题,了解到有些问题的表现与地理位置有关。
网络差异、数据源的不同和规模等因素可能会对环境产生重要影响。例如,在每秒收到 1,000 个请求的大型集群中如何重现问题呢?可观察性工具在这些情况的管理中可能极为有效。正如我在《针对故障构建 - 轻松进行生产调试的最佳实践》中所讨论的那样,在这种情况下,调试的重点不在于重现,而在于理解环境中的可观察信息。
理论上,我们不应遇到这些问题,因为测试应有适当的覆盖范围。然而,现实总是复杂的。许多公司进行“长时间运行”的测试,一整夜持续运行,将系统压力推至极限以便在现场问题出现前找出并发问题。故障通常由存储不足造成(例如,日志将磁盘打满),但这样的问题通常很难复现。多次循环运行失败的代码常是有效的解决方案。还有一些值得使用的工具,例如我曾讨论过 “强制抛出异常”的 功能,它使我们能够更灵活地模拟失败情况。
日志记录
日志记录是许多应用程序的核心特性,它是我们调试此类边缘情况的关键工具。我曾经写过关于日志记录的文章,讨论了它的价值。
日志记录的工作需要像可观察性一样经过深入的思考和规划。如果没有适当的日志记录,我们将无法有效地调试现有 bug 。合理地开始日志记录并采用最佳实践是一个好习惯。
并发
并发问题是软件开发中极为棘手的 bug 之一。当遇到表现不一致的问题时,首先需要确定涉及的线程,并确保每个线程都按预期执行操作。
在调试过程中,可以使用单线程断点,从而只暂停特定线程,以检查特定方法中是否存在竞态条件。建议使用跟踪点代替断点,因为阻塞可能会掩盖或修改与并发有关的 bug 。
通过审查所有线程,并尝试通过让其他线程休眠来为每个线程提供“优势”,我们可以偶然发现引发并发问题的特定条件。
尝试使用自动化流程来重现问题非常有用。例如,当遇到这种情况时,可以创建一个循环来数百或数千次地运行测试用例,并通过日志记录来寻找问题的根源。
值得注意的是,如果问题确实源自并发代码中,额外的日志记录可能会显著改变结果。有一次,我将字符串列表存储在内存中,然后在执行完成后一次性转储完整列表,以取代直接写入日志。虽然使用内存进行记录的调试方式并不理想,但这样做可以避免记录器或控制台输出的开销。特别是在没有适当的过滤和管道的情况下,控制台输出通常比记录器更慢。
何时选择“放弃”
尽管我们不主张轻易放弃,但有时必须承认某些问题在你的机器上可能始终无法重现。当遇到这种情况时,应该进入调试过程的下一个阶段,对潜在原因进行假设并创建测试用例以验证这些假设。
在难以解决 bug 的情况下,将日志和断言集成到代码中至关重要。这样做可以确保当问题再次出现时,有更多的信息可供分析和参考。
真实的案例
Codename One 就遇到过一个难以排查的问题,使用 App Engine 成本突然从几美元飙升到几百美元。这一突然的成本增加甚至有可能导致我们一个月内破产。虽然我们竭尽全力进行了分析和修复,但我们还不能精确定位具体原因 ,只能采用蛮力的方式解决问题。
最终,解决 bug 的过程是一个充满坚持和不断学习的挑战。这不仅仅意味着把 bug 看作是开发过程中的一部分,同时也包含了理解如何从每次调试经验中提高和成长。
总结
“ 在我的机器上可以运行”这一说法在软件开发领域常常显得没有说服力。我们必须对 bug 负责,并尽可能准确地复现用户的环境和行为。透明的沟通和研发支持部门的协同合作至关重要。
我们可以使用现代工具深入分析正在运行的应用程序,精确地定位问题所在。尽管容器技术(例如 Docker)简化了统一环境的创建,但网络、数据源和规模的差异仍可能对调试产生影响。
有时,即使付出了极大努力,某些 bug 也可能始终无法在我们的机器上重现。在这种情况下,我们需要根据潜在原因制定合理的假设,创建能验证这些假设的测试用例,并在代码中添加日志和断言,以便未来的调试工作。
最后,调试不仅是一项坚持和适应的学习过程,更是对任何开发人员职业成长和技能提升的关键环节。
参考链接
《针对故障构建 - 轻松进行生产调试的最佳实践》:https://debugagent.com/building-for-failure-best-practices-for-easy-production-debugging
曾讨论过:https://debugagent.com/debugging-program-control-flow
写过关于日志记录的文章:https://debugagent.com/logging-best-practices-revisited
Codename One:https://www.codenameone.com/
领取专属 10元无门槛券
私享最新 技术干货