从忽视再到复现,我是如何一步步发现SQLite Bug

1c12b9c7a4e04369b7232c9564b838b2.gif

【CSDN 编者按】程序出现错误,你的第一反应是什么呢?“奇怪,明明代码没问题啊?”、“怎么会返回一个硬件错误呢?”本文作者跟大家的反应或许类似,但当从迷雾中拨清头绪,再到复现、解决,你或许会有不一样的收获与感悟。

作者 | Philip O'Toole      翻译 | 王雪迎

出品 | CSDN(ID:CSDNnews)

rqlite 是一款用 Go 语言编写的轻量级、开源、分布式关系数据库,它使用 SQLite 作为存储引擎。最近我向 rqlite 中引入了一个高性能写入方法,大吃一惊的是,我还发现了 SQLite 中的一 个bug。

SQLite 团队很快解决了这个问题,但在此也记录一下发现过程与复现经历。

0dfb3433aea96da7c88e382c76e2692e.png

问题总是始于新特性

rqlite 7.5.0 引入了排队写入功能,它让用户能以高性能方式将大量数据写入 rqlite。当然,这意味着 SQLite INSERT 的执行频率也会增加,这才是重点。

在对排队写入进行压力测试期间,我有时会运行 rqlite 命令行,并通过定期发出以下命令来查看测试进度:

127.0.0.1:4001> SELECT COUNT(*) FROM logs

想象一下当我偶尔收到以下返回结果时的“惊喜”:

ERR! database disk image is malformed

这确实很奇怪。rqlite 默认运行内存数据库,那么磁盘映像怎么会损坏?我把它归因于 SQLite 中的错误代码重用,这是编程中的一种常见做法。但后来才意识到自己犯了真正的错误,我没有认真对待该问题,并认为它可能是暂时的,永远不会再现。

6e4889c65cb1cb677bf93d402f6f13d5.png

真实情况

11 月 1 日,rqlite 用户 João Arruda  在 GitHub 上提出了以下问题:

从忽视再到复现,我是如何一步步发现SQLite Bug_第1张图片

现在不可否认,这是一个真正的问题,而且可能是一个严重的问题。谁想在使用数据库时看到“磁盘映像格式错误”?

首先要做的是考虑能否重现 João 所遇到的情况,看看到底发生了什么。我再次启动了压力测试,提高了 INSERT 速率,直到磁盘 IO 达到最大值,然后开始通过 CLI 查询数据库——问题再次出现了。

ERR! database disk image is malformed

经测试表明,这只是一个查询时间问题。一旦 INSERT 流量停止,数据库中总是有正确数量的记录,而且这些记录看起来总是很正常。所以这是个好消息——发生的一切实际上并没有破坏数据库。

我是否可以写一个单元测试来复现此问题?想想发生的情况,我认为写这个测试并不比在一个数据库连接上执行查询,同时在另一个连接上插入行复杂多少。复现应该不会太难。

但事实并非如此。我用 Go 语言编写了一个简单的单元测试,其中不涉及任何 rqlite 代码,并运行了它。

$ go test -run Test_TableCreationInMemoryLoadRaw
--- FAIL: Test_TableCreationInMemoryLoadRaw (2.30s)
malformed_test.go:59: rows had error after Next() (1203 loops): database disk image is malformed
FAIL
exit status 1
FAIL  github.com/mattn/go-sqlite3  2.303s

下一步是与 rqlite 使用的 Go-SQLite 驱动程序的维护者进行核实。他们确认 Go 代码没有问题, 而是在 SQLite 内部存在一些问题。

如果不用 C 语言编写代码,就不会出问题

现在该在 SQLite 论坛上发帖了。在 11 月初我第一次发帖寻求帮助,询问为什么在一个内存数据库中,我的查询会返回一个磁盘映像损坏错误。虽然问题得到了一些回应,但没有真正的进展。我认识到仅用 Go 的示例对 SQLite 社区来说不太有说服力。我的代码和 SQLite 实现之间的层次太多,有太多对 SQLite 的使用原因造成该问题,而不是 SQLite 本身。

所以是时候编写一个简单的 C 程序了。问题能重现吗?还是会消失?不管哪种情况,我都希望学到一些东西。

我很容易地拼凑了一个 C 程序——SQLite C AP 非常容易编写代码。然后我编译并执行了它:

$ gcc in-memory.c -pthread -l sqlite3
$ ./a.out
Running SQLite version 3.39.4
THREADSAFE=1
Failed to step data: database disk image is malformed

就是这样,对 SQLite 的简单使用和我完美的查询遇到了数据库损坏的错误。是再次在 SQLite 论坛上发帖的时候了。很快,SQLite 团队确认这是 SQLite 中的一个 bug,并修补了他们的源代码。

从补丁中可以看到,我的查询连接似乎是在不应该访问数据库的时候访问了数据库,然后看到一个正在更改中的数据库。查询代码将此解释为损坏的数据库,并向我的代码返回了一个错误。

这一切都非常令人满意。我意识到发现这个 bug 可能是我整个软件职业生涯中做的最重要的事情。没等新的 SQLite 发布,我就马上给自己的 SQLite 打了补丁。 重构 rqlite 后没有再出现这个问题。在发布的 rqlite 7.12.1 中包括对该问题的修复。

最后 João 确认,在他的生产设置中也不再出现该问题。

从忽视再到复现,我是如何一步步发现SQLite Bug_第2张图片

c2cda5724b8f8d32a0755116f441fe2a.png

经验总结

这里汲取到的经验和往常一样。计算机会严格按照你的指示去做。它们不会在系统中随机插入错误,也不会在运行过程中自己编造错误。当我第一次看到这个问题时,忽视它是一个很大的错误,而且在理智上不够诚实。

非常感谢 João 在 GitHub 上提交了这个问题,rittneje@rqlite 中心维护 GoSQLite 驱动程序的工作人员,并帮助证明这是一个 SQLite 问题,以及 SQLite 团队如此迅速地修复了这个 bug。

ad9b41428a809006230b7914edf6cdf5.png

网友:我发现过同样的 Bug,但被解雇了!

Euphorbium:

我在 django 中遇到了同样的bug,他们花了 5 年时间才修复。Django 的标语是“有期限的完美主义者的网络框架”。因为这个 bug,我被解雇了。

Hans:

修复方法适用于 memdb。我怀疑你在django上使用了内存数据库…

HacKan:

作为一名维护人员和软件开发者,我可以 100% 理解你一开始“忽略它”的决定,就像“也许这是我的环境或什么”。

作为从中吸取的教训,下次你遇到类似的事情时,面对现实可能是一种有趣的方法:对你所看到的情况添加一个“已知问题”免责声明,说明它很可能发生。如果其他人也设法重现了问题,那就专注解决它,而不是忽略它然后提交问题。

总之干得不错,多棒的成就

dkf:

计算机真的可能在代码中插入随机错误!它们发生在辐射或宇宙射线导致随机位翻转的时候……是的,这确实发生过。但只有在大规模情况下出现,我们在硬件(内存和磁盘)中进行了纠错,以将这些(未纠正的)随机位翻转的比例降至你在整个职业生涯中都看不到的水平之下。

deets:

我曾经编译过一个大型C++代码库,在编译“klass Something”或类似的东西时出错了。源代码中不会包含如此严重的错误。更深研究发现在class关键字中出现了位翻转。重新编译显然解决了这个问题,但还是有点吓人。

Mikko Rantalainen:

我想说,任何软件问题都可能存在正在随机性,直到你可以在另一个不共享任何硬件的系统上复现该问题为止。

这是确保你当下系统不被简单破坏的唯一真正方法。崩溃可能以各种方式发生:存储、电源、内存控制器损坏、L1/L2/L3 缓存损坏、CPU 内核损坏、 CPU 核心中的微操作执行单元损坏等。

只有当你可以用两个完全独立的系统来重现问题时(最好情况下,所有的东西都是不同的,如英特尔 vs AMD CPU,三星 vs ADATA 存储等等)才可能确认。这将需要排除给定硬件组件中的设计问题。

如果你确定问题是由软件引起的,那么是时候投入资源找出确切原因了。

 
   

f88232ac77e82d18241e10493655e32d.gif

☞国产软件将往何处去:20年剧变、产品化之路和工程师战力
☞月费 19 美元,GitHub Copilot 企业版上线,你乐意买单吗?
☞“游戏发布 20 年之后,开发者喜提百万富翁!”

从忽视再到复现,我是如何一步步发现SQLite Bug_第3张图片

你可能感兴趣的:(sqlite,bug,数据库)