用 Go 如何做到 SQLite 每秒读取一百万次?

2022-03-24 02:12:17 +08:00
 zzxgz

我最近在测试用 Go 来操作 SQLite 的性能,用的是这个库( https://github.com/mattn/go-sqlite3 )。

我的测试代码在这个这个仓库的代码,我参考了这个仓库的代码,其测试流程是:

  1. 创造与数据库的 3 个连接,分别用于创建表,把数据写入表,和读取表里面的记录。
  2. 创造一个有${numberOfCores} * 2个工人的 dispatcher 。
  3. 创建 People 这个表,它有 id ,firstname 和 lastname ,然后在 firstname 和 lastname 上创建 index 。
  4. 创建 1024 * 1024 * 20个 People 的实例,一共大约二千万个。
  5. 把以上的实例一个一个地插入表。
  6. 准备读取用的 statement ,然后把 statement 配上每一个 People 的 firstname 和 lastname ,传入 dispatcher ,dispatcher 会把这些 statement 分配给不同的 worker ,让 worker 来读取数据库。
  7. 等待所有的读取结束,然后程序打印出写入和读取需要的时间。

我本来以为,用的 dispatcher 会令性能提高,但是我发现读取数据的时间非常长:

root@fw0016589:/home/user/src/github.com/zzxgzgz/SQLite_Multithreading_Go# ./main
2022/03/16 14:41:39 Hello world!
2022/03/16 14:41:39 All 112 workers are running, now you may dispatch jobs.
2022/03/16 14:41:54 Gernated 20971520 people!
2022/03/16 14:50:48 Inserting 20971520 people took 8m54.187231461s
2022/03/16 16:42:39 To query 20971520 people, it took time: 1h51m50.105310237s

读取的 QPS 只有可怜的 20,971,520 / (111 * 60 + 51) = 3,124.947

后来我在网上找到了这个帖子,用帖子里面的代码(补上了帖子里面没有分享的 struct ),测试得到的结果好了很多:

./main
SQLite start
insert span= 62 read span= 107 avg read= 0.0107

这个测试与我的类似,它是插入和读取一千万条数据,然后它的 qps 是 10,000,000 / 107 = 93,457.94,比我的代码快了约 30 倍。

但是,这个离我的目标还有一些距离,这个帖子声称它可以单机(性能非常高的机器)四百万 qps 。我运行了帖子里提供的性能测试(C++代码),它在我的机器上能达到一百二十万以上的 qps:

Thu Mar 17 10:30:58 PDT 2022  Starting: -vms unix-excl -locking_mode NORMAL (./perftest, /home/user/src/github.com/Expensify/Bedrock/perftest/test.db)
./perftest -csv -numa -numastats -mmap -linear -vms unix-excl -locking_mode NORMAL -testSeconds 60 -maxNumThreads 256 -dbFilename /home/user/src/github.com/Expensify/Bedrock/perftest/test.db
Enabling NUMA awareness:
numa_available=0
numa_max_node=1
numa_pagesize=4096
numa_num_configured_cpus=56
numa_num_task_cpus=56
numa_num_task_nodes=2
numThreads, maxQPS, maxQPSpT
1, 46024, 46024
2, 91274, 45637
3, 137404, 45801.3
4, 180705, 45176.2
5, 225147, 45029.4
6, 272936, 45489.3
7, 314279, 44897
8, 364338, 45542.2
9, 405119, 45013.2
10, 448852, 44885.2
11, 494748, 44977.1
12, 537469, 44789.1
13, 591314, 45485.7
14, 637572, 45540.9
15, 678796, 45253.1
16, 730028, 45626.8
17, 770238, 45308.1
18, 815726, 45318.1
19, 858698, 45194.6
20, 907344, 45367.2
21, 951636, 45316
22, 994060, 45184.5
23, 1041419, 45279.1
24, 1083378, 45140.8
25, 1128111, 45124.4
26, 1169421, 44977.7
27, 1216605, 45059.4
28, 1257847, 44923.1
29, 1260620, 43469.7
30, 1266371, 42212.4
31, 1268080, 40905.8
32, 1266702, 39584.4
33, 1275697, 38657.5
34, 1285441, 37807.1
35, 1279162, 36547.5
36, 1285150, 35698.6

我的问题是:

  1. 为什么我有 dispatcher 的代码,比一条一条读取数据库的代码,慢了这么多呢?
  2. 在使用 Go 的情况下,可以达到像 C++代码那样的 QPS 吗?应该怎样实现呢?

这个帖子有点长了,谢谢你花时间来阅读。

3424 次点击
所在节点    问与答
13 条回复
tinkerer
2022-03-24 05:23:51 +08:00
性能差距可能并不在于你的 golang 代码优化上,而是 cgo 本身的损耗,包括 c 类型 与 golang 类型的转换和 ffi 。c++ 与 C 库的交互几乎没有中间损耗,不像 golang 有自己的 runtime 。
鉴于你好像想要近似于 c++ 版的性能,那你可能需要用 c/c++ 写你的程序,因为 golang 的性能本身就比不上 c/c++。
tinkerer
2022-03-24 05:30:17 +08:00
关于问题 1 ,因为 sqlite 的一切都是直接与文件的交互,不存在真正意义上的多线程操作,而是排队干活。
xupefei
2022-03-24 07:40:06 +08:00
这测试真的靠谱?磁盘 IO 在 ACID 的前提下真的能达到这种水平吗?
lloovve
2022-03-24 08:59:40 +08:00
最近也在研究 go sqlite ,我感觉不是 golang 的问题,golang 和 c 差距不可能这么大,感觉可能是驱动的问题,从下面链接看也不是 cgo 的问题

https://golangexample.com/sqlite-http-server-performance-benchmark/
wtfdsy
2022-03-24 09:51:28 +08:00
不太懂 GO ,提供个思路,我用 C++用 sqlite 追求极限性能的时候都是把数据库往内存弄一份用 inmemory 模式的
zzxgz
2022-03-25 01:57:25 +08:00
谢谢各位的回复!

@tinkerer #2 我也同意你的观点。[这个回答]( https://stackoverflow.com/a/4060838/8883222) 说,只要同一时间只有一个 writer ,那么应该也是没问题的(1 writer n readers)。如果真的是 cgo 的问题的话,那么这个问题在 Go 里面可能就暂时没有解决办法了,可能只能用 C++达到一百万 qps 的性能了。

@xupefei 应该是可以的,我觉得 Bedrock 的测试挺好的。想请教一下,其他的常用的数据库大约能达到怎么样的性能呢?会比一百万 qps 低很多吗?

@lloovve 谢谢你分享的链接,看起来这个人测试的结果都在 10 万 qps 以下,[这个帖子]( https://turriate.com/articles/making-sqlite-faster-in-go)也测试了`mattn/sqlite3`,得出的 qps 也是类似的水平。

@wtfdsy 谢谢你的分享![这个测试]( https://www.cnblogs.com/liughost/p/6698205.html)它用的也是`inmemory`模式(`mode=memory`),我把它用内存模式和不用内存模式都测了一次,性能上的差距不是很大,主贴里面的测试结果(`avg read = 0.0107`)是没有用内存模式的,用内存模式的结果大约是(`avg read = 0.009xxx`)。

现在看起来,用 C++(或者 C )跟用 Go 的性能差别有点大啊,但是我们是希望能用 Go 来达到这个性能目标的(百万级或者不差太远的 qps ),不知道各位还有没有其他的想法呢?

对了,其实我也在`mattn/go-sqlite3`这个仓库里面提了[差不多的的问题]( https://github.com/mattn/go-sqlite3/issues/1022),但是回复我的人好像也是没啥头绪。
zzxgz
2022-03-25 02:17:12 +08:00
刚刚发现了一个测试的错误,并在 append 里补上了重新测试的结果。看来用 Go 现在最多也只有 5 万 qps 了,离一百万 qps 差得太远了。
xupefei
2022-03-25 02:52:17 +08:00
@zzxgz 你想想硬盘的 IOPS 能上百万吗?你的测试里可是有写操作的。在 ACID 的前提下数据库理论写速度不可能超过硬盘 IO 。
那如果百万 IOPS 指的是关掉 ACID 的内存数据库,那你的测试方法就没意义,结果自然也没意义了。
zzxgz
2022-03-25 06:42:38 +08:00
@xupefei 你说的对,包含写操作的话,的确不可以。

现阶段我更关心的是,如果测试的时候只算读取数据库的 qps ,能不能做到百万呢?
xupefei
2022-03-25 06:45:43 +08:00
@zzxgz 全在内存里的话当然可以,但有啥意义呢
xupefei
2022-03-25 06:48:41 +08:00
你的实验如果全是读操作,那么数据库完全是多余的。还不如各写一个循环算次数
tinkerer
2022-03-25 11:39:47 +08:00
@zzxgz 也有可能查询性能损失来自 database/sql 的 Scan, 好像是使用 reflect 来实现的。
zzxgz
2022-03-26 00:37:30 +08:00
@xupefei 全部在内存里面的话,我也认为可以,但是我这边需要数据有持久性,所以就需要使用一个数据库了。

@tinkerer 你说得有道理,但我认为 Scan 也是必要的,因为不用 Scan 的话,我不知道应从数据库里拿到的 rows 得到我需要查找的信息。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/842496

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX