深入分析运行中应用程序的一些工具,例如 strace、dtrace 等,可以让我们深入了解内部差异和异常行为。容器技术如 Docker 大大方便了统一环境的创建,从而消除了许多微小的差异。
我曾经调试过一个仅在客户现场出现故障的系统,最终发现他们的网络连接速度非常快,以至于与服务器之间的通信时间甚至比本地代码的执行时间还要短。我通过远程登录到客户的机器上并在那里复现了问题,了解到有些问题的表现与地理位置有关。
网络差异、数据源的不同和规模等因素可能会对环境产生重要影响。例如,在每秒收到 1,000 个请求的大型集群中如何重现问题呢?可观察性工具在这些情况的管理中可能极为有效。正如我在《针对故障构建 - 轻松进行生产调试的最佳实践》中所讨论的那样,在这种情况下,调试的重点不在于重现,而在于理解环境中的可观察信息。
理论上,我们不应遇到这些问题,因为测试应有适当的覆盖范围。然而,现实总是复杂的。许多公司进行“长时间运行”的测试,一整夜持续运行,将系统压力推至极限以便在现场问题出现前找出并发问题。故障通常由存储不足造成(例如,日志将磁盘打满),但这样的问题通常很难复现。多次循环运行失败的代码常是有效的解决方案。还有一些值得使用的工具,例如我曾讨论过 “强制抛出异常”的 功能,它使我们能够更灵活地模拟失败情况。
日志记录日志记录是许多应用程序的核心特性,它是我们调试此类边缘情况的关键工具。我曾经写过关于日志记录的文章,讨论了它的价值。
日志记录的工作需要像可观察性一样经过深入的思考和规划。如果没有适当的日志记录,我们将无法有效地调试现有 bug 。合理地开始日志记录并采用最佳实践是一个好习惯。
并发并发问题是软件开发中极为棘手的 bug 之一。当遇到表现不一致的问题时,首先需要确定涉及的线程,并确保每个线程都按预期执行操作。
在调试过程中,可以使用单线程断点,从而只暂停特定线程,以检查特定方法中是否存在竞态条件。建议使用跟踪点代替断点,因为阻塞可能会掩盖或修改与并发有关的 bug 。
通过审查所有线程,并尝试通过让其他线程休眠来为每个线程提供“优势”,我们可以偶然发现引发并发问题的特定条件。
尝试使用自动化流程来重现问题非常有用。例如,当遇到这种情况时,可以创建一个循环来数百或数千次地运行测试用例,并通过日志记录来寻找问题的根源。
值得注意的是,如果问题确实源自并发代码中,额外的日志记录可能会显著改变结果。有一次,我将字符串列表存储在内存中,然后在执行完成后一次性转储完整列表,以取代直接写入日志。虽然使用内存进行记录的调试方式并不理想,但这样做可以避免记录器或控制台输出的开销。特别是在没有适当的过滤和管道的情况下,控制台输出通常比记录器更慢。
何时选择“放弃”尽管我们不主张轻易放弃,但有时必须承认某些问题在你的机器上可能始终无法重现。当遇到这种情况时,应该进入调试过程的下一个阶段,对潜在原因进行假设并创建测试用例以验证这些假设。
在难以解决 bug 的情况下,将日志和断言集成到代码中至关重要。这样做可以确保当问题再次出现时,有更多的信息可供分析和参考。