分享

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

 新用户73286115 2022-12-16 发布于四川

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

作者 | Philip O'Toole      翻译 | 王雪迎
出品 | CSDN(ID:CSDNnews)

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

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

图片

问题总是始于新特性

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 中的错误代码重用,这是编程中的一种常见做法。但后来才意识到自己犯了真正的错误,我没有认真对待该问题,并认为它可能是暂时的,永远不会再现。

图片

真实情况

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

图片

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

首先要做的是考虑能否重现 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 malformedFAILexit status 1FAIL  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.outRunning SQLite version 3.39.4THREADSAFE=1Failed to step data: database disk image is malformed

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

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

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

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

图片

图片

经验总结

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

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

图片

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

Euphorbium:

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

Hans:

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

HacKan:

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

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多