本篇内容是根据2021年11月份Debugging Go音频录制内容的整理与翻译,
Natalie 和 Ian 欢迎 Liran Haimovitch 和 Tiago Queiroz 来到节目,重点讨论调试 Go 程序。它们涵盖了好的和坏的调试实践、在云中调试的难度、错误日志和指标的价值、在生产中(或不在生产中)调试的实践等等!
过程中为符合中文惯用表达有适当删改, 版权归原作者所有.
Natalie Pistunovich: 今天我们要讨论的是如何调试 Go,我要分享一下,其实这期节目是重新录制的。尽管我们尽了最大努力做了冗余备份,并使用了 Babel 本地录音和其他工具,但由于网络问题,音频质量非常差,一切都出了问题,所以我们重新录制这期节目。希望这次不会有我们无法解决的“黑盒子”问题……
我今天的联合主持人是 Ian。嘿,Ian,你好吗?
Ian Lopshire: 我很好。这次对我来说会很有趣,因为我仍然是那种依赖打印调试的人……让我们看看我能学到什么。
Natalie Pistunovich: 我们还邀请了 Liran Haimovitch 和 Tiago Queiroz。两位先生,你们能自我介绍一下吗?Liran,你比我们早一个小时,你在柏林的东边---
你在特拉维夫。
Liran Haimovitch: 是的,我能完全理解打印调试的优势,因为调试器有时非常繁琐,在很多情况下难以使用。而直接在代码中插入打印语句、重新编译、重建、并查看发生了什么要简单得多。然而,重新编译和部署软件并不总是那么容易,这正是我们 Rookout (译者注: Rookout: Cloud-Native Debugging Tools
)的用武之地。
我叫 Liran Haimovitch,是 Rookout 的首席技术官。我在网络安全领域工作了大约十年,而在过去的---
天啊,已经六年了---
我主要专注于可观测性,研究如何利用动态插桩和其他网络安全中的概念来使可观测性和调试在生产环境中更加敏捷和易用。
Natalie Pistunovich: 感谢你的加入。Tiago,你也在柏林。
Tiago Queiroz: 是的。大家好,我是 Tiago。我住在柏林,来自巴西。我是一名软件工程师,已经使用 Go 超过五年了,目前在 Elastic 工作,负责 Elastic Agent 和 Beats 的开发。有趣的是,我们的调试方式主要依赖日志,因为我们基本上是构建二进制文件,然后用户运行它们。有时会出现问题,因为没有软件是完美的……所以日志和打印调试仍然是发现问题的主要方式,尤其是当问题不在我们自己的机器上时。
是的,我之前在 Elastic 之前的大部分工作经验是在初创公司和微服务领域。我也做了很多关于如何提高系统可观测性的工作,始终倡导更好的可观测性和调试工具。所以这是我非常喜欢的一个话题,并且我总是试图在我的工作场所提高人们的意识;我们需要让系统变得可观测,并且在出问题时更容易理解……系统应该能告诉我们:“嘿,我因为这个原因崩溃了。”
Natalie Pistunovich: 人工智能……人工智能会告诉你:“你因为这个原因把系统搞崩了。”
Tiago Queiroz: 哈哈,是的。
Natalie Pistunovich: 那我们来聊聊目前的一些做法吧,暂时不讨论人工智能。有哪些调试的好习惯和坏习惯,特别是和 Go 相关的?
Tiago Queiroz: 我认为一个好的习惯肯定是确保你的应用程序有日志。确保你在编写应用程序时加上合适的日志语句。
“合适的日志”是一个非常宽泛的概念,具体多少合适你得通过实际操作和观察来学习。但无论如何,你应该确保有足够的日志,尤其是在写 Web 服务器时,理想情况下 HTTP 服务一启动就应该输出类似“嘿,我正在这个主机和端口上运行”的信息。它应该展示一些启动时的关键信息,比如配置,这些配置可能会因为不同的环境而有所变化,而这些正是解决问题的关键。
最好的例子是:有时候你在预发布环境中运行服务时使用一个端口,而在生产环境中使用另一个端口,但如果没有正确设置配置,事情就无法正常工作。如果你没有任何日志语句,你可能会觉得“嘿,我的 Web 服务器是在 3000 端口启动的,而不是 8000”,这可能会让你花费几个小时来调试。
所以当应用程序启动时,真的要考虑清楚,一个工程师或使用你应用程序的用户在出现问题时,可能需要知道哪些信息,或者他们需要了解应用程序当前的状态。
Liran Haimovitch: 调试任何编程语言的棘手之处在于,你可能是在调试不可预测的问题……因为如果问题是可以预测的,那你可能已经有一条日志语句告诉你“系统崩溃了,这是为什么。去修复它吧。” 系统可能会非常清楚地写出问题所在,或者你对领域和应用程序的了解足够深入,能够快速找到线索,知道问题在哪里。
但如果你需要调试,那说明问题对你来说是不可预测的。可能是因为调试过程非常复杂,可能是因为有很多依赖项,或者你对 Go 或应用程序本身不太熟悉……这都没关系,没人是完美的,每个人都有知识盲点。无论如何,你必须记住,如果某件事很难调试,那它在某种程度上是不可预测的,你需要考虑如何应对这种不可预测性。无论是通过插入打印语句来获取更多信息,还是使用调试器、实时调试器,或者查看其他可观测性数据……不管什么方式,你都需要解决这种不可预测性,从已知的信息出发,逐步推理。
特别是在 Go 中,我发现一个经常让人困惑的地方是,Go 应该没有异常处理机制,但实际上它有点类似。我并不是想和谁争论什么,但 panic 和 defer 的行为与异常抛出和捕获非常相似。如果你是 Go 的新手,或者对代码库不太熟悉,panic 经常会被忽视。有些库甚至故意使用 panic 作为一种异常处理机制,用来在栈非常深的时候回溯栈。
无论如何,当你在处理一个错误流程或不可预测的结果时,始终要考虑“这里有没有可能发生 panic?如果有,我该怎么测试,或者怎么确保没有 panic 发生?” 因为你会发现,无论是有意还是无意,可能某处确实有 panic,导致你的代码进入了一种不可预测的状态,给你带来了麻烦。
Natalie Pistunovich: 那你觉得本地调试和在云端调试有什么区别呢?
Tiago Queiroz: 我认为最大的区别在于,本地调试你可以让调试过程“慢动作”进行。实际上,我在 GopherCon 上参加了 Bill Kennedy 的一个工作坊,他有一句很有趣的名言:“调试器的功能就是让你的 bug 以慢动作运行。” 基本上,这就是它的全部功能。
Natalie Pistunovich: 这句话真不错。
Tiago Queiroz: 这其实非常有趣……归根结底,当你将调试器附加到应用程序上时,通常你只是在本地运行。你可以字面上让代码以“慢动作”运行,停下来并检查一些东西,对吧?我觉得当你不完全理解程序的工作流程或代码路径时,调试器非常有用,或者当有太多动态的事情发生时。如果我非常了解程序,通常我只会使用打印语句。
当然,这时我会想,“好吧,[[听不清 00:12:04.23]] 我通常对发生的事情有个大致的猜测。当我完全没有头绪时,[[听不清 00:12:11.28]]。我觉得这是本地调试和云端调试的主要区别之一---
你可以附加一个调试器,可以以慢动作运行代码。因为如果你是在云端,即使是在 Kubernetes 部署的预发布环境中,通常你的应用程序前面可能有一些代理,可能有一些超时设置之类的东西。当你使用调试器,比如说“嘿,让我的 HTTP 请求停留五分钟(或者是 30 秒),我在思考”,那时可能会超时。特别是在 Go 中,当你进行到某个点时,[[听不清 00:12:45.06]],请求的上下文会超时。我觉得这就是主要的区别。
Liran Haimovitch: 是的,云端调试真的很糟糕……它真的很糟糕,因为我们习惯用来调试的两种技术在云端都不太好用。99%的情况下你无法使用调试器;当你在云端运行时,你无法附加传统调试器,无论是因为你没有访问权限,还是因为调试器会像 Tiago 提到的那样阻塞进程,这会导致健康检查失败。进程 [[听不清 00:13:23.14]],然后 AWS 或 Kubernetes 或其他编排工具会终止你的进程,并启动一个没有附加调试器的新进程。
当然还有很多其他技术问题,但 99%的情况下你无法附加调试器,无法享受那种让代码以“慢动作”运行的乐趣,从而能够在代码飞速执行时发现 bug。
此外,日志虽然很好---
比调试器好---
但你不能总是使用它们。而且,当你进入不可预测的领域时,日志往往---
我不能说它们够不够用,因为那样说有点严苛,但它们有时确实很难读取。它们可能难以筛选,难以抓住全局,基本上是大海捞针。而且问题不仅在于你有多少日志,还在于它们相对于 bug 的位置,以及它们揭示了多少软件内部的运作信息。
正如 Ian 一开始说的那样,“我喜欢添加打印语句来更好地了解情况”,当你的代码在云端运行时,添加打印语句会花费一段时间。你必须添加代码,然后要么在本地构建,要么推送到远程,然后你还得 [[听不清 00:14:49.13]] CI/CD,进行部署……如果是个人环境,添加一行日志可能需要 20 分钟,如果是共享环境甚至生产环境,可能需要几个小时甚至几天。
所以你仍然可以用打印语句调试,但修改它们会变得非常痛苦和缓慢……这也是新型调试器---
实时调试器---
兴起的部分原因,它们非常有用……我们实际上去年发布了对 Go 的支持。我认为这是让云端调试变得更简单的一种方式,当你可以像传统调试器那样设置断点并查看数据,同时仍与负载均衡器、API 网关、Kubernetes 等配合使用时,这非常有帮助。
Ian Lopshire: 是的,我想听更多关于云端调试的内容……我以前没听说过这个。这让我有点惊讶……你能详细说明一下吗?
Liran Haimovitch: 当然。在 Rookout,我们的目标是为你提供开发者可观测性,首要的是提供类似调试器的体验,不过实际上没有使用任何调试器。我们要求你安装我们的代理或 SDK。对于 Go 来说只是一个 Go 模块。这个 Go 模块在你运行时会连接到一个编排服务。然后我们提供你一个“如果我在这里设置了断点会发生什么”的体验。你进入我们的平台,在线上代码中放置一个断点(一个不会中断的断点),下一次这行代码被执行时---
可能是一次,可能是上百次,甚至更多次---
我们收集快照。这些快照看起来就像断点命中时调试器显示的内容。你会看到堆栈跟踪、局部变量的值等等……只不过这些快照是异步的。所以应用程序不会停止,不会等你慢慢阅读调试过程。
我们在捕获快照时会让应用程序稍微变慢一两毫秒,然后你可以离线查看它,随时阅读。你可以在不同的快照之间来回切换,以便查看代码的运行情况,表面上看是在“慢动作”运行,但实际上代码执行得非常快。
Natalie Pistunovich: 你提到可以通过这些快照在云端进行调试,当你在云端放置断点时,查看这些快照是非常容易的,或者说更容易。那么,有哪些好的实践可以让调试变得更简单呢?你可以做些什么来帮助自己、未来的自己、以及你的同事呢?有哪些你在调试时遵循的实践?
Liran Haimovitch: 当然,日志是必不可少的。正如 Tiago 提到的,日志非常棒。你不应该吝啬日志。不过你也要记住,日志太多也会有问题,特别是当你从日志聚合器那里收到账单时……所以要注意这一点。但这是一个关于如何优化日志成本和数量的完整话题,我们不会深入讨论。据我的经验,大多数人在早期往往会犯日志太少的错误。
另一件事,也是我之前提到的,就是在写代码时,重点关注错误情况。我记得在某个地方读到过,当你写代码时,大约 20% 的代码是处理正常流程的,处理应该发生的事情,而 80% 的代码是处理不应该发生的事情;处理输入验证、错误响应、网络断开、数据库错误,或其他不可预测的情况。80% 的代码会在这里,而且复杂度远远超过 80%。尽量在写代码时多思考这些错误情况。
虽然写一行日志,比如说你进入了某个函数---
也许这是个重要的函数,某天可能会有用---
这行日志很可能只是噪音,最终会被丢进那些花费过高的日志堆里,最后你不得不关闭它们。另一方面,我认为错误状态的日志要有用得多。如果出现了错误,务必写一行日志。
我记得我们在 Rookout 的一个早期客户,当时他们用 Rookout 在一个 Node 应用中调试一个追了六个月的 bug。这个 bug 非常复杂,他们在做 OAuth 三方认证,拿到一个 cookie……他们其实有一个错误验证代码,假设如果 cookie 太大,他们会截断它。然而,cookie 里存有 JWT 令牌;显然,它是一个签名的工件。如果你截断签名的工件,签名就会失效,导致各种痛苦。他们在那个 if 语句上写了个注释:“这里应该添加一行日志。”注释里写着“这永远不应该发生。添加一行日志。”但他们没有添加日志。结果他们花了六个月才搞清楚为什么有些用户无法登录系统……原因是他们的 cookie 太大了,因为某些地方的配置。
Tiago Queiroz: 是的,确实要关注你的错误日志。我认为这是最有帮助的东西之一。但不仅仅是日志本身,还要有好的错误消息。不要只说“嘿,我崩溃了”或者 [[听不清 00:22:31.07]]。尝试提供一些上下文,解释为什么发生了错误。比如一个简单的例子,如果是插入操作,至少要打印表的名称。也许你不能打印整个 SQL 语句,但你可以打印表名,或者说“插入用户失败”之类的。
所以尽量解释清楚,给出上下文,说明为什么会进入错误路径。正如 Liran 所说,关注那些错误处理代码;通常很多 bug 和问题都会出现在那里。错误没有得到妥善处理,甚至更糟的是---
它们被处理了,但却被隐藏了,导致你在日志中看不到错误。或者应用程序从错误处理路径中退出,但没有告诉你为什么。明明有错误,却不告诉你原因。
我最近有个很好的例子。最近我们在应用程序中做了一些 PR,添加了更多日志,给错误日志更多上下文信息。比如“哦,我无法启动,因为日志文件已经存在了。所以可能有另一个实例在运行。”但它没有告诉你日志文件在哪里;它只说“日志文件”。它存在,但你不知道文件在哪里,怎么找到它?……而且你需要找出哪个进程占用了这个文件。
有时候如果你查看大量日志,在应用程序启动的最开始,它可能会说“哦,我的配置路径在这里”,然后你得知道需要将它与其他信息结合起来才能找到日志文件。但为什么不直接打印出来呢?比如“日志文件,完整路径已经存在。我无法启动。”
有时这些简单的事情就能产生巨大的差别……所以,也许找到所有错误并恢复,但请给出错误的详细信息,给出具体情况。尝试展示导致错误的代码路径,并在错误信息中尽可能多地提供上下文,这样读日志的人,或者使用任何可观测性工具的人,都能更好地理解为什么会发生这种情况。
Ian Lopshire: 我们已经讨论了很多关于日志的问题,听起来对于错误来说,日志是首选。那么你觉得统计数据的作用是什么呢?像统计数据、计数、延迟的直方图……这些在调试中处于什么位置?
Liran Haimovitch: 这是个棘手的问题……指标是---
它们是一种非常复杂且强大的软件监控方式。你可以通过它们完成很多事情,尤其是当你在大规模系统、生产系统中使用它们时。但你也需要对指标有一些基本的了解,知道如何使用它们。你还需要对你的具体应用有一些实际经验,因为对于一个应用来说可能很好的数字,对另一个应用来说可能很糟糕……其中一些取决于业务环境和你所处的领域,另一些则取决于你的应用程序特性,以及你在开发过程中做出的各种决策。
当然,有一些非常好的起点,比如查看每秒请求数,查看应用程序每秒处理多少请求,查看延迟,处理请求需要多长时间,查看错误率,成功请求与失败请求的比率……但数据的查看方式有很多种变化。我要看中位数、平均数,还是 p90、p95、p99?这取决于上下文。有些应用程序适合看 p95,而另一些则需要看 p99.9……还有的可能看平均值就够了。
另外,你是从哪些维度查看这些指标?你如何细分它们?你有多个区域吗?你想按区域监控吗?你有多个端点吗?你想按端点监控吗?不同的端点对延迟或错误的容忍度不同……类似的问题还有很多很多。
所以一方面,如果你想在生产中监控应用程序,开始引入一些指标是一个很好的起点……但这也是一个双重学习曲线,既要学习如何使用指标,也要学习对你的具体应用来说,什么是正常的,什么是好的,什么是坏的。
Tiago Queiroz: 是的,我认为指标并不能---
我不会把它们完全称为调试工具。它们更多是帮助你理解---
因为指标是聚合的,对吧?通常我们会得到每秒请求的平均数,或者平均---
比如在给定时间内的请求、指令、执行次数,通常这些指标用于请求延迟、吞吐量、数据库连接等。
所以通常你无法通过这些聚合指标定位某个特定事务中的某个问题。但它们可以帮助你理解另一个问题,比如哪些 bug 现在值得优先处理。如果你有错误率指标,显示有 50% 的请求总是失败---
那么你肯定有一个大问题。很多用户受到这个 bug 的影响。但如果只有 0.1% 的请求失败或太慢,而这对用户数量来说基本上可以忽略不计,也许实际上还有其他更大的 bug 需要优先解决。
所以我觉得指标可以帮助你确定优先级,尤其是性能问题。如果你想提升性能,或者你确实有性能问题,那么这些指标会在那里……即使你可以将性能问题视为 bug,当有人说“哦,有个 bug。我们来调试一下”时,我通常想到的是代码层面的怪异行为,而不是“每当请求过多时,数据库就崩溃了”。通常我不认为那是一个 bug。也许是代码中的 bug,可能是你对数据库发出了太多请求,或者 SQL 查询没有优化;可能是。但至少在我脑海中,当我听到“bug”或“调试”这个词时,我想到的是这些。
Natalie Pistunovich: 我们讨论了调试的好习惯和坏习惯,讨论了云端调试和本地调试的区别;我们还讨论了统计数据和错误日志的区别。接下来我们来聊聊生产环境调试和非生产环境调试。你会在生产环境中调试吗?如果会,怎么做?
Tiago Queiroz: 是的,我觉得在 Elastic 工作时,我们遇到了一个非常有趣的生产环境调试案例……我们无法访问用户的环境。所以基本上我们只能依赖日志,问用户“你做了什么?”所以在这种情况下你能看到两件事---
一是拥有好的日志的力量,二是日志也可能过于冗长。比如你有些应用程序,启用了调试日志,并且有很高的吞吐量,有时候你可能会得到 100 MB 的日志,而这只记录了三秒钟的执行,因为某些部分的日志过于冗长。
所以,是的,我觉得生产环境调试有一个特点,就是有时你根本无法访问用户的环境,很多时候你只能依赖日志和指标。这意味着你基本上是在“回顾过去”。好吧,事情发生了,我有一些线索指向过去发生的事情,而不是现在。这与本地调试不同,甚至与预发布环境调试也不同……通常在预发布环境中你对环境有更多控制权。
Natalie Pistunovich: 而且你不能真正重现报告问题的用户的环境,因为这不像是“把你的代码发给我,我试着运行看看……”
Tiago Queiroz: 是的,有时你确实做不到,或者你尽力做到尽可能接近……但这让我想起了一个事情---
有时你无法访问生产环境或者实际环境,但如果你有足够的信息,你可以重现一个足够接近的实验。比如“好吧,每当这些情况发生时,就会有 bug。”然后你在开发或预发布环境中重现它,这样你就可以运行调试器,运行你的工具。
Liran Haimovitch: 担任 Rookout 的 CTO 有趣的一点是,我不仅能看到我们工程师如何在生产环境中调试,还能和我们的许多客户一起在生产环境中调试。许多客户已经---
我不会说他们放弃了,但他们意识到他们需要比传统调试方法更好的解决方案。
监控工具并不是为调试设计的。监控工具的目的是告诉你有问题发生了。你的应用程序宕机了,某些端点有问题,部分用户没有得到最佳服务……但这些工具从来都不是为了精确定位问题所在,特别是当许多问题都是代码相关的时。如今,随着云原生应用程序的普及,你将大量基础设施交给了其他人---
AWS、Google Cloud、Azure,或其他你选择的服务商,大多数问题都出在代码中,而你需要非常细致的观察。
我们的很多客户已经意识到,无论是为了提高工程师的生产力,还是为了更快地解决客户问题,或者两者兼而有之,他们更倾向于能够真正地在生产环境中进行调试。而在实现这一目标的过程中,我们看到了一些需要考虑的问题和障碍。安全性显然是一个大问题。你必须确保无论你使用什么工具,你都知道谁能访问系统,知道系统如何被访问,并确保一切都是安全的……世界各地,特别是在欧洲,有很多隐私法规,所以你必须考虑这些问题,你必须知道哪些私人数据正在被收集,是否在处理过程中被移除,它是否被存储,谁在存储这些数据……
显然,你还需要考虑性能和可用性。我提供给工程师的访问和工具会不会影响我的应用程序的稳定性?会不会影响应用程序的性能?
每当你考虑在生产环境中调试时---
无论你是在给他们提供像 Rookout 这样的高级工具,还是让他们通过 SSH 访问系统进行操作---
你都需要考虑这些方面的影响,特别是系统的非功能性需求,这可能因行业、公司,甚至同一公司中的不同应用而千差万别。你必须在处理它们时考虑这些问题。但除了这些显著的额外风险外,你仍然会回到我们在这次对话中讨论的调试基本原则。
偶尔,我会遇到某些人决定在生产环境中附加传统调试器,因为问题严重到他们别无选择,只能这样做,或者因为某种特殊原因,这不会对他们的系统产生太大干扰,他们可以承受……但最终你会使用相同的技术,只是风险规避的程度不同。
Ian Lopshire: 我想回到日志记录这个话题上,虽然我们已经讨论了很多……但我们之前提到过,有时候日志太少了,有时候又太多了……你觉得日志级别在这其中起到了什么作用吗?我们应该如何使用它?在 Go 社区,有很多人争论“只用一个日志级别就够了”,而另一些人喜欢使用不同的级别……你们对此有什么看法?
Tiago Queiroz: 我认为这取决于情况。我在不同的观点之间摇摆过,有时觉得“我们只需要一个级别”,有时觉得“我们应该有多个级别”。我认为这很大程度上取决于你开发的应用类型。如果你在自己的基础设施上部署微服务,那么你有更多的控制权,也许你可以用更少的日志级别,并且认真编写好的日志消息。但如果你实际上是提供二进制文件给别人下载和运行,那么至少有一个调试级别会很有帮助,因为你可以说“好的,这些是大多数人不需要的东西。”它们只是浪费磁盘空间,或者如果日志被发送到别处,也会浪费日志存储……所以 99% 的时间我们不会需要它们。但当出现问题时,我们确实需要看到这些日志。
在我们的一些应用程序中---
在 Elastic,我们实际上采用了不同的方法。我们确实有日志级别,但我们也可以特定地过滤日志记录器或应用程序的不同部分 [无法听清 00:36:15.17]。我们可以说“哦,我只想查看来自输入、解析器或输出的调试日志。”这几乎像是为应用程序的不同组件设置不同的日志级别。
所以这也是我觉得相当有趣的一种方法,尤其是在日志可能会过于详细的情况下,它可以非常有用。你可以过滤掉那些详细的部分,甚至不生成它们,然后专注于你真正需要的部分。
Liran Haimovitch: 日志级别非常重要。你的系统规模达到一定程度后,肯定离不开它们……尤其是在处理流量时,因为你必须以某种方式减少日志量,而使用日志级别是最简单的入门方法。显然,正如 Tiago 所说的,你以后可以变得更加复杂。
但在考虑日志级别时,有一点你必须记住---
在大多数当今的 SaaS 运营中,同一个团队既构建软件又运营软件,日志级别,特别是在长期使用中,往往会被“操纵”。
举个例子……假设你第一次运行应用程序,可能会输出大量的调试日志。你没有任何流量,打印出所有内容……你简直渴望有人能访问你的网站。所以为什么不呢,干脆打印所有内容。随后,应用程序成长了,你获得了更多流量,移动到了基础设施上;这时仍然没大问题。但某个时刻,应用程序爆发了,你有了大量用户,你为某个日志聚合器支付了太多费用,突然某个运维人员走过来说:“我们得减少日志量。我们需要只保留某个级别以上的日志,因为我们浪费了太多钱,而没人真正查看这些日志。”
现在,你可能定义了非常明确的日志级别,也可能没有,但下次某个软件工程师需要某条日志来解决问题或完成工作时,我可以保证他们使用的日志级别将是警告,而不是信息或调试,不管那条日志有多重要……因为人性如此,他们会利用系统;如果他们需要一条日志行,就会不惜一切代价添加那条日志并完成工作。长期来看,这意味着每次你提高日志级别的最低标准时,你会丢失一些日志,但新的日志会被添加,很多其它日志也会被移动、重构到新的级别……而这并不一定是基于日志的重要性,而是最近被你的工程师使用的内容,这并不总能提供关于哪些日志仍然重要的良好反馈……更不用说有些工程师可能比其他人更加直截了当,在这种情况下,情况可能会更糟,因为有些工程师会利用系统,而其他人不会。
总之,日志级别不会解决你所有的问题,但它们确实可以在一定程度上提供帮助。
Ian Lopshire: 我非常喜欢你关于“操纵系统”的观点,因为我绝对做过这样的事情,毫无疑问。
Liran Haimovitch: 你知道我们在录音吧……
Ian Lopshire: 哦,是的,我会承认这一点,因为我需要看到这些日志 [笑声]。我可以承认一整天。这没关系。
Tiago Queiroz: 我完全同意这一点。我觉得有些日志不是 [无法听清 00:40:00.10],因为如果出了问题,我需要知道发生了什么,所以我不会将它们标记为调试日志。我知道有些人会称其为调试日志;但我称其为 [无法听清 00:40:09.13],因为我希望无论如何都能看到这条日志。我知道你在运行日志,对吧?这就是问题所在。我知道你在运行的是信息级别的日志,所以我将级别设置为我们正在运行的级别,是的。所以我可能也犯了同样的错误……但我认为这确实是一个问题。
我认为这又回到了一个问题---
随着系统的扩展,你真的需要格外小心。Liran 提到了一个很好的观点---
随着系统越来越大,日志量会成为一个问题,那时你真的需要仔细思考你在记录什么以及如何记录,以确保你始终拥有所需的日志……因为日志是那种你平时不会看,除非你遇到了问题的东西;如果你遇到了问题,却没有日志,那你基本上只能提交一个 PR 增加日志,部署……20 分钟算你非常幸运了,可能还需要几周才能搞定……尤其是如果你提供的应用程序是供人们下载和安装的。那么,是的,你确实需要非常小心地编写日志 [无法听清 00:41:13.24]。
Natalie Pistunovich: 最后一个问题,然后我们进入不受欢迎的观点环节……当你必须调试某个问题时,你会从哪里开始?
Tiago Queiroz: 我总是尽量重现问题发生时的场景。通常,我们会听到类似“好吧,这个东西出错了……”然后我会尝试重现它。这很大程度上取决于具体情况,但我会尽量在我的机器上让问题发生,如果我已有的日志还不足以让我理解问题所在的话。有时候,日志已经足够了。那么工作就完成了,只需“好吧,修复错误信息……”通常事情并没有那么简单。所以,是的,通常需要更多的实验,我会让 bug 变得很容易重现,这样我可以反复运行,直到我理解是什么导致了它……在这个过程中,我认为调试的艺术开始展现出来。一旦你能重现它,那么“好吧,为什么会发生呢?”接下来就是打印语句、日志语句、调试器、改变环境、改变应用程序的状态,直到你理解为什么会发生这种情况。
Liran Haimovitch: 我经常做的事情是查看代码,特别是如果我有一个描述性强的 bug [无法听清 00:42:35.18],它能指引我走向正确的方向。很多时候,如果你用全新的眼光看代码,不管是你自己的代码还是别人的代码,只要你对整个应用有不错的熟悉度,并且你正在查看可能存在 bug 的可疑代码段,并且有一些关于问题的描述---
你经常能看到 bug。可能是一个简单的 if 语句,也可能是比较时检查了错误的属性……我前几天就遇到了这样的问题……你可能会发现其他问题,比如某个调用缺失了,或者多了一个调用……但如果你有一定经验,并且知道自己在找什么,你经常只需盯着代码看,就会发现“嘿,这看起来不对”,然后你突然间就理解了问题所在。
除此之外,调试是一个非常联想的过程。我是说,如果事情变得非常复杂,你可以开始采取更标准化的流程,写下你知道的一切,记录各种 [无法听清 00:43:43.11] 关于可能出错的原因,然后开始讨论如何收集你需要的额外数据,以验证或排除这些假设,可能通过添加大量打印语句来实现。这通常就是调试的常规流程。
但在早期阶段,我发现联想式思维在大多数情况下能带来更快的结果……这也是我建议查看代码的另一个原因,因为一旦你看过代码,了解了基本情况,你就更有条件去查看额外的数据,无论是日志、指标,还是我们讨论过的 bug 报告或其他内容,这样你可以在上下文中理解问题,而不是从零开始拼凑一切。
Ian Lopshire: 很好。下一部分应该是大家最喜欢的环节……不受欢迎的观点。我希望你们准备好了……
Ian Lopshire: 好的,谁想先来?
Liran Haimovitch: 我来吧……我不受欢迎的观点是关于 OpenTelemetry 的。我知道 OpenTelemetry 被吹捧为“统一所有”的解决方案,但我发现它在很多情况下过于自以为是,太复杂了,而且在大多数情况下,它并没有比结构化日志提供更多的好处。当然,有一些例外;有些特别复杂的场景,比如 Uber 有 2000 个微服务,直到他们意识到这是错误的方法,在这种情况下,你确实需要分布式追踪和 OpenTelemetry,恭喜你发明了它。
但对于我们大多数凡人来说,最好不要过度复杂化我们的系统和可观测性解决方案。简单在工程中总是更好,尤其是在软件工程中。日志非常棒。自从计算机工程诞生以来,它们就一直陪伴着我们。指标也非常不错。如果你想再高级一点,结构化日志完全可以胜任,并且它们可以非常简单;你只需要在日志记录器的配置上写几行简单的配置……另外,OpenTelemetry 在 Go 中是一大堆样板代码,涉及大量配置。你必须自己设置所有中间件……所以不要对它抱有太高的期望;你选择的是一个非常复杂、非常昂贵的解决方案,在大多数情况下,它的价值不如传统方法;在某些罕见的情况下,它可能会有更高的价值,但你可能永远也遇不到这些情况。
Ian Lopshire: 我非常同意它的样板代码太多,过于复杂……我们曾经尝试过迁移到它,最后放弃了,因为……实在是负担太大了。
Natalie Pistunovich: 听起来这个“不受欢迎的观点”其实很受欢迎啊……
Tiago Queiroz: 是的,我也同意。我没有太多使用 OpenTelemetry 的经验,但我用过其他基本上是可观测性和指标相关的解决方案……是的,你可以通过结构化良好的日志走得很远。甚至 [无法听清 00:47:09.12],它非常简单,因为你可以轻松地解析它们,或者将它们放入 Elasticsearch 之类的系统中,然后你可以一起查看日志,聚合它们……是的,你可以在不复杂化的情况下走得很远很远。
所以我的不受欢迎的观点是……我仍然认为标准库中的 testing 包是你需要的最好的测试框架;它是最好的。它非常简单,非常强大,尤其是现在模糊测试已经内置在语言中了,还有其他功能……而且它不引入新的概念。基本上,你还是要写 if 语句。如果某件事情发生了,那么我就调用我的 T 对象上的一个方法,对吧?或者调用 B 对象,用于基准测试之类的功能。
它让开发者、编写代码的工程师更多地思考他们在做什么,并且可能还会写出更好的测试失败信息。所以与其简单地说“A 不等于 B”,不如说“我在尝试做某件事,但因为某个原因失败了。”你仍然可能陷入“ A 不等于 B”的陷阱,但我认为这种情况会减少---
有一些测试库有巨大的公共 API,它们很难使用……这里那里有一些非常小的库,我看到的几个例子是---
它们只是放错了地方,比如 [无法听清 00:48:31.11] 被比较的实际错误,好在测试并没有非常错误……但这种情况确实会发生。而使用 testing 包,这种情况不会发生。它基本上只会打印出一条信息说“好的,你可以让测试失败,然后你可以写信息解释为什么。”
当然,构建一门语言与构建其他类型的应用程序非常不同,但据我所知,在 Go 语言中,编写代码时只使用 testing 包,所以它强大到足以编写 Go 代码……因此我认为它也应该适用于大多数(如果不是全部的话)使用场景。如果你需要特定的功能,你总是可以编写自己的辅助函数;而 t.helper 也是一个很棒的帮助工具。
Ian Lopshire: 我担心这个观点会变得很受欢迎。
Natalie Pistunovich: 是的。我也带来了一个不受欢迎的观点。
Ian Lopshire: 哇,好吧……
Natalie Pistunovich: 我认为作为一个经理、团队负责人、产品经理,或者其他角色,在你的跟踪系统中(比如 JIRA、Trello 等)发一轮消息:“这里的进展如何?这里的进展如何?这里的进展如何?”---
然后开发者第二天到来时看到十条看起来一模一样的消息,“这里的进展如何?”---
这是你能做的最糟糕的事情之一。
Liran Haimovitch: 完全同意。
Natalie Pistunovich: 不,这应该是不受欢迎的观点。不要同意。
Liran Haimovitch: 埃隆·马斯克要好得多。他会告诉所有人,如果他们下周没有交付他一夜之间想出来的东西,就会被解雇。 [笑声]
Natalie Pistunovich: 这才是不受欢迎的观点。
Ian Lopshire: 是的,那些状态更新消息总是让我感到焦虑。就像,“等等,我应该把这些都完成吗?啊……!”
Natalie Pistunovich: 即使答案是“是”,也有比十条“这里的进展如何”的消息更好的方式。
Tiago Queiroz: 是的,完全同意。
Natalie Pistunovich: 也许你们有一个约定,比如每天结束时更新,每周更新,或者隔天更新,我不知道……如果你们没有这样的约定,你们应该制定一个。如果团队对这个约定不满意,你需要找到其他的解决方案,但打开收件箱看到十条这样的消息时,我只想关掉电脑,说“再见”。
Tiago Queiroz: 是的,我同意。
Liran Haimovitch: 如果你希望别人花时间写下答案,那么你至少应该花时间写下证明你知道每个任务内容的问题,并且更加专注于你在问什么,而不是泛泛地问“你的进展如何?”因为我也可以回答“它正在进行中。”
Natalie Pistunovich: 完美!天哪,采纳这个观点吧…… [笑声]
Tiago Queiroz: 我甚至会说一些状态更新消息就像调试日志一样,只是说“是的,进展顺利。事情在进行中”,但大多数时候这不是你需要看到的,也不是你需要听到的。
Natalie Pistunovich: 确实。这就像信息级别的日志。
Tiago Queiroz: 是的,你需要的是错误级别的日志。比如,“嘿,进展延迟了,因为这个原因”,或者“嘿,我遇到了这个问题,被它卡住了,所以任务 X、Y 或 Z 没有进展,甚至可能倒退了。”
Natalie Pistunovich: 我觉得我们正在触及一些核心问题……我们还有很多问题想要讨论,还有很多话题想要探讨,看来我们没有其他选择,只能再做一期节目。我想对两位表示感谢,感谢你们的参与;希望你们也能参与我们下一期关于调试的节目。祝大家度过愉快的一天,无论你们身处何地,处于哪个时区。再见,大家!