V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
zzxgz
V2EX  ›  问与答

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

  •  
  •   zzxgz · 2022-03-24 02:12:17 +08:00 · 3431 次点击
    这是一个创建于 983 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我最近在测试用 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 吗?应该怎样实现呢?

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

    第 1 条附言  ·  2022-03-25 02:15:18 +08:00
    发现了测试里面的一个失误:

    在运行[这个测试]( https://www.cnblogs.com/liughost/p/6698205.html)的时候,我想不使用内存模式的时候,只删除了`:memory:`,而没有删除后面的`mode=memory`。经修正并重新测试之后,得出如下结果:

    ```
    ./main
    SQLite start
    insert span= 60 read span= 217 avg read= 0.0217
    ```

    结果慢了一倍左右。
    13 条回复    2022-03-26 00:37:30 +08:00
    tinkerer
        1
    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
        2
    tinkerer  
       2022-03-24 05:30:17 +08:00
    关于问题 1 ,因为 sqlite 的一切都是直接与文件的交互,不存在真正意义上的多线程操作,而是排队干活。
    xupefei
        3
    xupefei  
       2022-03-24 07:40:06 +08:00 via iPhone
    这测试真的靠谱?磁盘 IO 在 ACID 的前提下真的能达到这种水平吗?
    lloovve
        4
    lloovve  
       2022-03-24 08:59:40 +08:00
    最近也在研究 go sqlite ,我感觉不是 golang 的问题,golang 和 c 差距不可能这么大,感觉可能是驱动的问题,从下面链接看也不是 cgo 的问题

    https://golangexample.com/sqlite-http-server-performance-benchmark/
    wtfdsy
        5
    wtfdsy  
       2022-03-24 09:51:28 +08:00
    不太懂 GO ,提供个思路,我用 C++用 sqlite 追求极限性能的时候都是把数据库往内存弄一份用 inmemory 模式的
    zzxgz
        6
    zzxgz  
    OP
       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
        7
    zzxgz  
    OP
       2022-03-25 02:17:12 +08:00
    刚刚发现了一个测试的错误,并在 append 里补上了重新测试的结果。看来用 Go 现在最多也只有 5 万 qps 了,离一百万 qps 差得太远了。
    xupefei
        8
    xupefei  
       2022-03-25 02:52:17 +08:00 via iPhone
    @zzxgz 你想想硬盘的 IOPS 能上百万吗?你的测试里可是有写操作的。在 ACID 的前提下数据库理论写速度不可能超过硬盘 IO 。
    那如果百万 IOPS 指的是关掉 ACID 的内存数据库,那你的测试方法就没意义,结果自然也没意义了。
    zzxgz
        9
    zzxgz  
    OP
       2022-03-25 06:42:38 +08:00
    @xupefei 你说的对,包含写操作的话,的确不可以。

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

    @tinkerer 你说得有道理,但我认为 Scan 也是必要的,因为不用 Scan 的话,我不知道应从数据库里拿到的 rows 得到我需要查找的信息。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2659 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 09:56 · PVG 17:56 · LAX 01:56 · JFK 04:56
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.