How To Report A Bug So It Actually Gets Fixed
How To Report A Bug So It Actually Gets Fixed
如何提交一份能被真正修复的 Bug 报告
I wanted to make a blog post like this for a long time, because it’s something that I wish I could find more of myself. I think one of the things that helps us most in our careers as software engineers is knowing how to debug problems, how to reproduce them, and how to report them. 我一直想写一篇这样的博客文章,因为我希望能看到更多类似的内容。我认为,作为软件工程师,职业生涯中最有帮助的能力之一,就是知道如何调试问题、如何复现问题以及如何报告问题。
What prompted me to write this was watching this awesome video from Kovarex, the founder of Factorio, where he goes through a bug report and tries to fix it. I thought the bug report was written pretty well, and I thought it might be helpful to show how I went about writing a bug report like this myself, and what the thought process was. 促使我写这篇文章的原因是,我观看了《异星工厂》(Factorio)创始人 Kovarex 的一段精彩视频,他在视频中处理并尝试修复了一个 Bug 报告。我觉得那份 Bug 报告写得非常好,因此我想分享一下我是如何撰写此类报告的,以及我背后的思考过程。
Last year the logs of a production service I was running started filling up with LEAK: ByteBuf.release() was not called before it's garbage-collected. Thousands of times. You instantly think it’s something to do with some sort of memory leak, considering it’s spammed by the GC, but let me explain how I got from here to Microsoft shipping a fix in a few weeks.
去年,我运行的一个生产服务日志中开始充斥着 LEAK: ByteBuf.release() was not called before it's garbage-collected 的报错,出现了成千上万次。考虑到这是由垃圾回收器(GC)频繁触发的,你第一反应肯定是某种内存泄漏。但请允许我解释一下,我是如何从这里出发,最终促使微软在几周内发布修复补丁的。
Own the bug
对 Bug 负责
1. Always assume you are wrong.
1. 永远假设是你的错。
We all write code. Sometimes the same code changes every day, especially if it’s a feature we are actively working on. Netty, on the other hand (the networking library that the Azure OpenAI SDK my service uses is built on, and where that LEAK line comes from), is run by thousands of companies. Surely the issue is your code. 我们都会写代码。有时同一段代码每天都在变,尤其是当我们正在积极开发某个功能时。而 Netty(我的服务所使用的 Azure OpenAI SDK 构建于其上,且上述 LEAK 报错正是源自于此)则被成千上万家公司使用。所以,问题很大概率出在你自己的代码上。
This is where I spent most of my time on this issue, trying to see if it was. That’s not wasted time, because if it ends up being your issue, you fix it and go on with your day. If it’s not your issue, well, then you get to participate in what I think is the most wholesome part of software development, which is bug reporting. 我在这个问题上花费了大部分时间,试图确认是否是我的代码有问题。这并不算浪费时间,因为如果最终发现确实是你的问题,你修复它并继续工作即可;如果不是你的问题,那么恭喜你,你将参与到我认为软件开发中最有意义的部分——Bug 报告。
2. Make it deterministic.
2. 让问题变得可确定。
If something is random, it usually means you haven’t really found what the issue is. The thing with Netty is that it only reports a leaked buffer when the garbage collector collects it. So even if the actual leak is steady, in the logs it looks like random bursts, because you need to wait for the garbage collector to actually get to it. 如果一个问题是随机出现的,通常意味着你还没有真正找到问题的根源。Netty 的特性在于,它只有在垃圾回收器回收缓冲区时才会报告泄漏。因此,即使实际的泄漏是持续发生的,在日志中看起来也像是随机爆发的,因为你必须等待垃圾回收器真正执行回收操作。
So my reproduction runs a hundred concurrent requests and calls System.gc() every single time one fails. Normally this is something you would never, ever do in production. But for a reproduction of a bug, it’s basically the spotlight that shows you where something happens.
因此,我的复现代码会运行一百个并发请求,并且每次请求失败时都会调用 System.gc()。通常情况下,你绝不会在生产环境中这样做,但为了复现 Bug,这基本上就是照亮问题发生点的聚光灯。
3. Bisect versions, just like you do with commits.
3. 像二分查找提交一样,二分查找版本。
If you don’t know about git bisect, well, stop reading this blog post. I think that would be a much better use of your time. It lets you quickly find where a commit has gone awry by always jumping midway between commits, so you can narrow down where the issue appears.
如果你不知道 git bisect,那么请停止阅读这篇文章,去学习它,那会是你时间更好的利用方式。它能让你通过在提交记录之间不断折半跳转,快速定位到出错的提交,从而缩小问题出现的范围。
What I wanted to do here was something similar, but instead of jumping between commits, I wanted to jump between versions. So I had my reproduction code, something super simple, and then, using the exact same app, the same SDK and everything, I jumped between versions until I got to the border between one of them working and one of them not working. In my case, reactor-netty-http version 1.1.23 was clean and working, and 1.1.24 leaked. That was the issue.
我想做的事情类似,但不是在提交之间跳转,而是在版本之间跳转。我准备了一段非常简单的复现代码,使用完全相同的应用、SDK 等环境,在版本之间不断切换,直到找到“正常工作”与“出现问题”的临界点。在我的案例中,reactor-netty-http 1.1.23 版本是正常的,而 1.1.24 版本出现了泄漏。问题就此锁定。
4. Shrink it into a public repository.
4. 将其精简并放入公共仓库。
There are two reasons you want to do this. Usually when you have an issue in production, there’s a lot of private data sitting next to it that you wouldn’t necessarily want to share with everybody online. That’s the first reason. The second is that the more complex your reproduction environment is, the harder it becomes to actually pinpoint the bug. So you want to create a repository that you can share with the maintainers of the project, one that is as small as possible but still reproduces the bug. 你这样做有两个原因。首先,生产环境的问题通常伴随着大量私有数据,你肯定不想将其公开。其次,复现环境越复杂,就越难精准定位 Bug。因此,你需要创建一个尽可能精简、但仍能复现 Bug 的仓库,以便与项目维护者共享。
In my case I created a Gradle project with a single test, with exact pins of the versions that started having the issue. That proves the bug without you having to trust me, and it proves that it’s not my app that is actually bugged, but rather something upstream. It later became the test that the fix was verified against, because if you have this reproduction inside of a public repository, you can always check whether it still fails after the fix has been deployed. 在我的案例中,我创建了一个包含单个测试的 Gradle 项目,并精确锁定了出现问题的版本。这证明了 Bug 的存在,无需对方盲目信任我,也证明了问题不在我的应用,而是源于上游。后来,这成为了验证修复方案的测试用例,因为只要将复现代码放在公共仓库中,你随时可以检查修复部署后问题是否依然存在。
Report it
提交报告
5. File where the evidence points.
5. 向证据指向的地方提交。
Now comes the fun part. In my case I filed it with reactor-netty, because the bisecting between reactor-netty versions is what showed me where the bug was. I made an issue with them, but it was closed, because it wasn’t actually their fault.
现在到了有趣的部分。在我的案例中,我向 reactor-netty 提交了报告,因为通过版本二分查找,证据指向了那里。我提交了一个 Issue,但被关闭了,因为那实际上并不是他们的错。
They explained it to me pretty clearly, and that to me was a huge boon. I could then take that and go to the actual source of the issue, which in this case was the Azure SDK for Java. The connection between the two is that the Azure SDK is built on top of reactor-netty.
他们向我解释得非常清楚,这对我有很大帮助。我随后可以根据这些信息找到问题的真正源头——在本例中是 Azure SDK for Java。两者之间的联系在于,Azure SDK 是构建在 reactor-netty 之上的。
6. Do the archaeology.
6. 进行“考古”工作。
This part is sort of optional, but I think it adds a whole lot to a bug report. I started looking through the Azure SDK for Java tracker, wanting to see if there were any issues that referenced anything very similar to this one. The best thread turned out to be one from five months earlier, from when the 1.1.24 connection lifecycle change happened, which supposedly fixed this. To be fair, I didn’t find that one just by searching: violetagg, the reactor-netty maintainer, pointed me to it. But in a lot of cases nobody points you to it, and you need to find it yourself. Azure added a guard in a PR, so the exception went away, but the buffer kept leaking. The fix for that issue actually made the bug quieter, but it didn’t make it go away.
这部分虽然是可选的,但我认为它能为 Bug 报告增色不少。我开始翻阅 Azure SDK for Java 的 Issue 追踪器,想看看是否有类似的问题。结果发现了一个五个月前的讨论帖,当时 1.1.24 版本进行了连接生命周期的变更,本意是修复这个问题。平心而论,我并不是自己搜到的,而是 reactor-netty 的维护者 violetagg 指向了它。但在很多情况下,没人会指引你,你必须自己去寻找。Azure 在一个 PR 中增加了一个防护措施,异常消失了,但缓冲区仍在泄漏。那个修复方案只是让 Bug 变得“安静”了,并没有真正解决它。
This gave me more information about the history of my bug, and I think that made everything move much quicker: I could make correlations between what happened in the past and what was happening currently. 这让我掌握了更多关于 Bug 历史的信息,我认为这加快了后续的进展:我能够将过去发生的事情与当前的情况联系起来。
7. Write the report you’d want to receive.
7. 写一份你自己想收到的报告。
I don’t know if you’ve ever been on the other side, receiving a report, but a lot of them are very simplistic. They don’t contain all the information that you need, and they actually… 我不知道你是否曾站在另一端处理过报告,但很多报告都过于简单。它们没有包含你所需要的所有信息,而且实际上……