每秒10万次事务处理,覆盖十亿行数据:SQLite的惊人效能

本文将阐述为何嵌入式架构和单写入器并非缺陷,反而让SQLite实现了如此不可思议的扩展性

 💬 110 条评论 |  SQLite/数据库 | 

SQLite没有多版本并发控制!它只有单写入器!SQLite只适合手机和移动应用(偶尔也用于客机)!Web服务器请用Postgres这类正规数据库!本文将阐述为何嵌入式架构和单写入器并非缺陷,反而让SQLite实现了如此不可思议的扩展性。

前奏§

本文代码示例将使用Clojure语言编写,但其原理适用于大多数编程语言。

本次基准测试的运行环境配置如下:

  • MacBook Pro (2021)
  • 芯片:Apple M1 Pro
  • 内存:16 GB

这些基准测试并非追求完美或最优,仅旨在说明使用SQLite实现良好的写入吞吐量相对容易。常规基准测试免责声明同样适用。

元素周期表

TPS定义§

本文所称TPS并非指每秒写入/更新次数,而是特指每秒事务处理量——即构建Web应用时常见的交互式事务。所谓交互式事务,是指执行若干查询、运行应用程序代码后再执行更多查询的流程。例如:

BEGIN;
UPDATE accounts SET balance = balance - 100.00
    WHERE name = 'Alice';
-- some application code runs
UPDATE accounts SET balance = balance + 100.00
    WHERE name = 'Bob';
COMMIT;

事务机制的价值在于:当应用程序出现问题时,可回滚所有变更状态。

基准测试框架§

为模拟请求,我们启动 n 个虚拟线程(绿线程),每个线程执行函数 f。这类似于Web服务器的处理程序,能产生相似的竞争场景。需注意此为高突发模式——即系统将以最大虚拟线程创建速度达到 n 级并发请求。

(defmacro tx-per-second [n & body]
  `(let [ids#   (range 0 ~n)
         start# (. System (nanoTime))]
     (->> ids#
       ;; Futures are using virtual threads so blocking is not slow
       (mapv (fn [_#] (future ~@body)))
       (run! deref))
     (int (/ ~n (/ (double (- (. System (nanoTime)) start#)) 1000000000.0)))))

对于Clojure开发者,future已适配虚拟线程机制。因此必要时可创建数百万个线程。

;; Make futures use virtual threads
(set-agent-send-executor!
  (Executors/newVirtualThreadPerTaskExecutor))
(set-agent-send-off-executor!
  (Executors/newVirtualThreadPerTaskExecutor))

我们将采用Postgres作为网络数据库(本文使用Postgres,但原理同样适用于MySQL等),并配置针对核心数优化的高性能连接池。

(defonce pg-db
  (jdbc/with-options
    (connection/->pool
      HikariDataSource
      {:dbtype          "postgres"
       :dbname          "thedb"
       :username        (System/getProperty "user.name")
       :password        ""
       :minimumIdle     8
       :maximumPoolSize 8})
    {}))

SQLite将采用单写入连接模式,读取连接数等于核心数量。

(defonce lite-db
  (d/init-db! "database.db"
    {:pool-size 8
     :pragma {:cache_size         15625
              :page_size          4096
              :journal_mode       "WAL"
              :synchronous        "NORMAL"
              :temp_store         "MEMORY"
              :busy_timeout       5000}}))

数据库采用简化模式:

(jdbc/execute! pg-db
  ["CREATE TABLE IF NOT EXISTS account(id INT PRIMARY KEY, balance INT)"])
(d/q (lite-db :writer)
  ["CREATE TABLE IF NOT EXISTS account(id PRIMARY KEY, balance INT)"])

每表包含十亿行数据:

(->> (range 0 (* 1000 1000 1000))
  (partition-all 32000)
  (run!
    (fn [batch]
      (jdbc-sql/insert-multi! pg-db :account
        (mapv (fn [id] {:id id :balance 1000000000}) batch)))))
        
(->> (range 0 (* 1000 1000 1000))
  (partition-all 100000)
  (run!
    (fn [batch]
      (d/with-write-tx [tx (lite-db :writer)]
        (run!
          (fn [id]
            (d/q tx
              ["INSERT INTO account(id, balance) VALUES (?,?)" id 1000000000]))
          batch)))))

用户分布遵循幂律分布。即前X%用户将产生绝大多数交易。在十亿用户基数下,实际绝大多数用户处于非活跃或低频活跃状态。0.9995意味着99.95%的交易由0.05%的用户完成。这仍意味着任意时刻约有100,000名独立活跃用户。

采用幂律分布的原因在于其广泛存在于现实产品中。以零售领域的信用卡支付系统为例,绝大多数交易很可能集中于少数大型零售商(亚马逊、沃尔玛等)。

(defn pareto-user []
  (rand-pareto (* 1000 1000 1000) 0.9995))

rand-pareto可将随机分布转换为幂律分布。

(defn rand-pareto [r p]
  (let [a (/ (Math/log (- 1.0 p)) (Math/log p))
        x (rand)
        y (/ (- (+ (Math/pow x a) 1.0)
               (Math/pow (- 1.0 x) (/ 1.0 a)))
            2.0)]
    (long (* r y))))

网络数据库§

让我们从网络数据库开始。

(tx-per-second 100000
  (jdbc/with-transaction [tx pg-db]
    (jdbc/execute! tx (credit-random-account))
    (jdbc/execute! tx (debit-random-account))))
    
;; => 13756 TPS

可观的13756 TPS。

但通常网络数据库不会部署在应用服务器上。因此需模拟网络延迟。假设应用服务器与数据库间存在5毫秒延迟。

(tx-per-second 10000
  (jdbc/with-transaction [tx pg-db]
    (jdbc/execute! tx (credit-random-account))
    (Thread/sleep 5)
    (jdbc/execute! tx (debit-random-account))))
    
;; => 1214 TPS

注:虚拟线程不会真正休眠真实线程,而是通过挂起机制让底层载体线程恢复处理其他虚拟线程。

若将延迟提升至10毫秒会怎样?

(tx-per-second 10000
  (jdbc/with-transaction [tx pg-db]
    (jdbc/execute! tx (credit-random-account))
    (Thread/sleep 10)
    (jdbc/execute! tx (debit-random-account))))
    
;; => 702 TPS

但请注意:当前事务并非可串行化事务(若需一致性事务处理则必须如此,SQLite设计上具备隔离串行化特性)。我们必须修复此问题并处理重试机制。

(tx-per-second 10000
  (loop []
    (let [result
          (try
            (jdbc/with-transaction [tx pg-db {:isolation :serializable}]
              (jdbc/execute! tx (credit-random-account))
              (Thread/sleep 10)
              (jdbc/execute! tx  (debit-random-account)))
            (catch Exception _ nil))]
      (when-not result (recur)))))

;; => 660 TPS

若交互式事务包含额外查询(即增加网络跳转次数)会怎样?

(tx-per-second 10000
  (loop []
    (let [result
          (try
            (jdbc/with-transaction [tx pg-db {:isolation :serializable}]
              (jdbc/execute! tx (credit-random-account))
              (Thread/sleep 10)
              (jdbc/execute! tx  (debit-random-account))
              (Thread/sleep 10)
              (jdbc/execute! tx  (debit-random-account)))
            (catch Exception _ nil))]
      (when-not result (recur)))))

;; => 348 TPS

348 TPS!这是怎么回事?阿姆达尔定律生效了!

优化系统单一部件所获得的整体性能提升,受限于该部件实际使用时间所占的比例。

由于幂律分布的影响,我们在高竞争网络中使用行锁进行事务处理。最可怕的是,无论增加多少资源(CPU/服务器/内存)都无济于事——这是网络造成的硬性限制。更糟的是,任何意外的延迟增加都会加剧问题。这也意味着应用服务器不能部署在与数据库不同的数据中心(因延迟会随之增加)。

我在为Discord开发基于表情符号的小费机器人时,曾为此付出惨痛代价。当时完全不明白为何交易吞吐量会遭遇如此严苛的瓶颈。最终我们不得不放弃交互式事务的便利性,将所有操作迁移至存储过程(即消除跨网络锁定)。但在许多领域,这种方案根本不可行。

嵌入式即无网络§

让我们看看SQLite的表现。

(tx-per-second 1000000
  (d/with-write-tx [tx (lite-db :writer)]
    (d/q tx (credit-random-account))
    (d/q tx (debit-random-account))))

;; => 44096 TPS

44096 TPS!通过消除网络开销,SQLite极大降低了阿姆达尔定律的影响。

单写入器支持批量处理§

但我们不必止步于此。由于SQLite采用单写入机制,我们可实现批量操作。sqlite4clj提供了便捷的动态批处理函数。批处理规模会随工作负载动态调整,生产者在消费者繁忙时无需阻塞。该机制能有效实现延迟与吞吐量的自我优化。

(defn batch-fn [db batch]
  @(on-pool! lite-write-pool
     (d/with-write-tx [tx db]
       (run! (fn [thunk] (thunk tx)) batch))))
       
(defonce tx!
  (b/async-batcher-init! lite-db
    {:batch-fn #'batch-fn}))

注:对于Clojure/Java开发者,我们采用线程池机制——应将SQLite视为CPU而非IO操作,避免其耗尽虚拟线程(IO绿线程)资源。

(tx-per-second 1000000
  @(tx!
     (fn [tx]
       (d/q tx (credit-random-account))
       (d/q tx (debit-random-account)))))
       
;; => 186157 TPS

但等等!你可能会质疑:这算作弊吧?批量写入牺牲了精细化事务控制,导致事务失败无法隔离。说得对!让我们来修复这个问题。

(tx-per-second 1000000
  @(tx!
     (fn  [tx]
       (d/q tx ["SAVEPOINT inner_tx"])
       (try
         (d/q tx (credit-random-account))
         (d/q tx (debit-random-account))
         (catch Throwable _
           (d/q tx ["ROLLBACK inner_tx"])))
       (d/q tx ["RELEASE inner_tx"]))))
       
;; => 121922 TPS

SQLite通过SAVEPOINT支持嵌套事务,这使我们能在保持批量写入的同时实现精细化的事务回滚。单个事务失败不会导致整个批次失败,仅在断电或系统崩溃时才会触发批次整体失败。

并发读取如何处理?§

实际系统中读写操作通常混合存在,比例约为75%读取与25%写入。现在我们增加写入操作。

(tx-per-second 1000000
  (on-pool! lite-read-pool
    (d/q (lite-db :reader)
      ["select * from account where id = ? limit 1" (pareto-user)]))
  (on-pool! lite-read-pool
    (d/q (lite-db :reader)
      ["select * from account where id = ? limit 1" (pareto-user)]))
  (on-pool! lite-read-pool
    (d/q (lite-db :reader)
      ["select * from account where id = ? limit 1" (pareto-user)]))
  @(tx!
     (fn  [tx]
       (d/q tx ["SAVEPOINT inner_tx"])
       (try
         (d/q tx (credit-random-account))
         (d/q tx (debit-random-account))
         (catch Throwable _
           (d/q tx ["ROLLBACK inner_tx"])))
       (d/q tx ["RELEASE inner_tx"]))))
       
;; => 102545 TPS

102545 TPS!

注:针对Clojure/Java开发者说明,我们使用独立读取线程池以避免读取操作阻塞写入操作。

TPS报告§

Postgres SQLite
无网络延迟 13756 44096
5ms延迟 1214 无数据
10ms延迟 702 无数据
10ms可串行化事务 660 不适用
批处理事务 不适用 186157
批处理+保存点 不适用 121922
批处理+保存点+读取 不适用 102545

结论§

希望本文能说明SQLite的超常效能,以及在应用阿姆达尔定律和Postgres等网络数据库时可能遇到的挑战。

完整基准测试代码可在此处查看

本文文字及图片出自 100000 TPS over a billion rows: the unreasonable effectiveness of SQLite

共有 110 条讨论

  1. 我完全认同SQLite确实非常出色。不过作为重度用户,我也逐渐产生了一些疑虑:

    – 数据库锁死其实并不难发生。通常终止导致死锁的进程就能解决问题——但前提是你得识别出该进程/进行监控。没错,WAL机制下同样会发生死锁

    – 但一旦发生,后果相当可怕。所有操作数据库的进程会突然停止工作——既无法读取也无法写入。

    – 某些情况下WAL不会执行检查点。这会导致WAL文件剧烈膨胀,进而引发灾难性查询延迟——原本10毫秒的操作可能耗时10秒。在我遇到的具体案例中,调整SQLite参数都无济于事。我只能持续监控,并定期强制将WAL文件滚动到主数据库中。

    – 在Windows环境下这些操作更困难,例如你无法直接用'lsof'命令查看文件状态。

    – 在云端环境中,那些看似本地实则非本地驱动器的性能统计数据会明显恶化。当然这并非SQLite的过错,但其惊人的速度优势并不适用于所有看似真实本地驱动器的常见环境。

    我并非贬低SQLite,尽管存在这些缺陷我仍在使用它。同样,我也乐于采用Postgres这类数据库——至少它至今未曾让我失望。

    1. 根据您的经验,这些行为该归类为漏洞,还是更像是SQLite特定实现方式导致的已知问题?发生这类问题时您施加了何种工作负载?作为一名真心欣赏并尊重SQLite却未曾遭遇这些具体行为的人,我在此请教。

  2. 我正在开发基于SQLite的混合式protobuf ORM/通用CRUD服务器
    (代码见https://github.com/accretional/collector——文档尚不完善敬请谅解)。我正在开发基于容器的代理项目,并尝试大量使用代理实现具体功能。整体运行良好,但代理们记录文档的热情有点过头了哈哈)。

    这是我首次在实际项目中使用SQLite,我们也取得了类似的酷炫基准数据:

    * 备份真实规模的CRUD数据库时,SQLite实机数据库停机时间仅5-15毫秒
    * 备份导致临时不可用时,可正确队列数百次读写操作
    * 包含Protobuf序列化/反序列化的CRUD操作端到端延迟基本为1毫秒
    * WAL机制支持持续流式分块备份!

    此前我只接触过Postgres和Spanner。目前对sqlite简直爱不释手——若预算无限仍会用Spanner处理某些任务,但等Collector实现分区功能后,我可能永远不会再碰Postgres了。

    1. > * 备份包含实际CRUD数据库数据量的实时sqlite数据库,停机时间仅需5-15毫秒

      是否考虑过使用支持原子快照的文件系统?例如在BTRFS上启用WAL的SQLite。据我所知,这种方案应能实现良好的机械协同性。

      编辑:我之前没说清楚。这是为实现零停机备份设计的方案:创建快照→按需备份→删除快照。

      1. 若当前停机时间已达5-15毫秒,那么“零停机”文件系统反而可能增加实际停机时间。除了快照创建时的暂停,还需精确衡量快照存在期间(产生COW开销)及后台垃圾回收过程中的性能损耗。

        另据我上次查证,Linux调度量约为10毫秒,因此在系统健康状态下,备份操作未必会成为最大停机时长。

        1. 恕我直言,你似乎并不清楚自己在说什么。欢迎提供相关文献供我学习。

          调度器时钟频率为何在此讨论中重要?即便在单CPU/核心/线程系统中亦然。值得一提的是,默认调度器时钟频率自2005年起便固定为2.5毫秒。今年早些时候有人提议恢复至1毫秒。

          https://btrfs.readthedocs.io/en/latest/dev/dev-btrfs-design…. https://docs.kernel.org/admin-guide/pm/cpuidle.html https://docs.redhat.com/en/documentation/red_hat_enterprise_https://sqlite.org/wal.html#ckpt https://www.phoronix.com/news/Linux-2025-Proposal-1000Hz

  3. 唯一的注意事项是,这假设所有数据都能容纳在一台机器上,所有处理任务也能在一台机器上完成。你可以租用一台u-24tb1.112xlarge实例,配备448个虚拟核心、24TB内存,每小时255美元,并附加64TB的EBS存储——这提供了相当大的操作空间。

    1. 或者租用Hetzner的裸机服务器,单核性能提升2-3倍,成本却降低90%[1]。

      [1] 多篇HN帖子对比了Hetzner与AWS在成本和性能方面的表现。

      1. 根据我的经验,管理得当的数据库扩展非常困难。

        3台运行Patroni+PostgreSQL的EX44实例可提供64GB工作内存,至少512GB NVMe数据集存储(可通过一次性付费扩容),并配备高可用性+1维护节点。实际应用中,这套方案足以轻松支撑我所在公司前5-10年的生产需求,每月硬件成本120欧元,另配一名合格系统管理员。

        我还知道不少企业每隔几年就向戴尔投入3-4台20k-30k级服务器构建本地数据库集群,从而彻底解决数据库性能问题 (除非应用程序查询设计有问题)。

      2. 赫兹纳没有配备24TB内存的服务器

      3. 就直接金钱成本而言或许如此。

        我本想喜欢赫兹纳,但与他们打交道的官僚主义流程实在令人窒息。

        倒不是说其他云服务商没有自己的官僚体系,所以这方面算是平手。

        我只是想找个放任自流的服务商,让我能直接砸钱解决问题。

        否则的话,即便当前硬件价格离谱地高昂,我宁愿在机房自建超规格服务器。

        编辑:刚写完这条评论就看到:“微软不让我支付24美元账单,导致数千美元Azure消费被冻结”https://news.ycombinator.com/item?id=46124930

        1. 看到Reddit用户抱怨Hetzner这方面的问题很奇怪,因为我的经历完全相反。作为企业客户,我已从Hetzner租用每月约40欧元的专用服务器两年,从未遇到任何问题。他们根本没要求提供营业执照或个人身份证件,我仅提交了增值税号、公司名称和地址——这些信息与我向Migadu或Porkbun等平台提供的材料并无二致。

          或许他们对欧盟以外的个人账户有更严格的KYC审核流程?除此之外我实在想不出其他原因。

          1. 我在美国尝试过两次注册Hetzner账户(个人账户和公司/初创账户),均被拒绝。实在不明白他们的商业模式 🙂

            1. 我也遇到类似情况,不确定Hetzner究竟怎么回事。

        2. 能否具体说明你遇到的官僚主义问题?我上个月成为Hetzner客户,目前体验极佳,尚未遇到任何官僚主义障碍。

          1. 即便我在评论中提到其他供应商也不完美,我认为自己当时的批评还是有些过火了。

            但基本情况是:完成初始手续后,我的账户竟被标记异常——尽管99.999%时间都没在使用。对我来说不算大问题,毕竟没做重要用途。只是好奇若真要严肃使用,这种情况会频繁发生吗?重新验证所有资料又会带来多少麻烦?

            据我所知,若一切顺利,他们的服务确实出色:服务器性能优异,定价合理等等。

            1. 租用整套基础设施时,适度的KYC审核合情合理。

              我申请AWS SES时遭遇更多麻烦,整个流程简直像在乞求服务。后来干脆放弃,转而采用自建方案 (还是在裸机服务器上)

          2. 注册账户时他们直接要求提供护照照片。就在填写完注册表单后立刻索要护照照片。什么鬼?简直不敢相信自己的眼睛,这是我见过最离谱的事。

        3. > 我想要的是那种放任我自生自灭、只要我砸钱就不管事的供应商。

          这正是我使用Hetzner的体验。

          很多人对企业敢于核实客户身份感到不满,认为这是在筛选最糟糕的客户(廉价服务商总是吸引这类人),但只要你接受合理的要求——比如验证身份证/护照——他们就不会再干涉你的其他行为。

          1. 这很合理,我对此没有太大意见。

            我对官僚作风的担忧在于,若不幸被误判为虚假警报,那将是一段令人恼火的经历。不过考虑到恶意行为者横行的环境,我也不好过分苛责他们不得不采取这种运作方式。

            你说的没错,低价服务商确实会吸引那些企图作恶或钻空子的人。

        4. 我欣赏他们的定价策略和简洁性,但他们并不给人技术精湛的印象。他们完全不提供托管服务,连K8管理服务都没有。其S3服务(现已相当成熟的技术)在推出一年后依然糟糕透顶。

          而你提到的官僚作风,恰恰反映了他们内部运作的本质。

    2. 噗,文档里写得清清楚楚。数据库最大(理论)容量是281TB:https://sqlite.org/limits.html

      由于开发者无法接触达到该极限的硬件,此上限值尚未经过测试。

      > 但测试已验证:当数据库达到底层文件系统最大文件容量(通常远低于理论上限)或因磁盘空间耗尽无法扩展时,SQLite仍能保持正确且合理的运行状态。

    3. 纵向扩展能解决稳定工作负载的诸多问题。但其弹性较差,用户要么忍受过量配置的容量(倍数而非百分比),要么在峰值负载时(如病毒式流量、黑色星期五等关键时刻)遭遇故障。

      目前尚无完美解决方案。横向扩展通常更具弹性,至少在读取方面如此。

      1. 此言甚是,但当一台笔记本就能实现每秒102545笔交易时,相较于当年需靠Amdahl大型机才能达到每秒100笔交易的时代,超额配置反而更合理。

        1. 随着计算成本降低,你的观点确实越来越成立。

          但这仅在工作负载保持稳定时有效。若负载以相似速率增长,问题将重现。

          1. 新增加的工作负载确实无法应对。但多数情况下,我们只是用更低效的方式处理相同的工作量。

      2. 我偏爱Hetzner作为内部资源,因其负载平缓。对外服务则倾向协同处理,可通过容器/Run/App Runner等服务实现Cloudflare/AWS/GCP的负载均衡。

      3. 我怀疑对多数组织而言,接受过度配置比采用复杂方案所需的人力成本低得多,同时因整体复杂度降低而能更快推进。

    4. > 唯一前提是假设所有数据都能装进单台机器

      我的数据能装进内存吗?https://yourdatafitsinram.net/

      不确定EC2/AWS/Amazon是否适合作例。若追求单节点高性能,必然选择专用服务器,至少要避开vCPU如避瘟疫。

      1. 该网站可信度存疑。我输入“64TB”作为答案,结果它欢天喜地展示了一堆最大仅6或8TB的服务器。即使那台标称64TB内存的服务器也值得怀疑——毕竟没给操作系统和应用程序留空间。话说回来64TB已是海量数据,所以我不太在意它装不下内存。但若遭遇断电还得从磁盘重新加载数据,那可就惨了。

      2. 当机器最大容量仅24TB时,25TB数据如何塞进内存?

    5. 前提是你的应用无需具备主机或网络故障容错能力

  4. 希望本文能说明SQLite的超常效能,以及使用Postgres等网络数据库时可能遭遇的阿姆达尔定律困境。

    不,并非如此。本文先声称通常会将应用程序和数据库部署在不同服务器上,随后却开始测量本地嵌入式数据库的性能。若必须维持软件的初始要求,SQLite便完全不符合条件。若能调整要求,通过调优本地PGSQL实例即可获得类似性能——此时更应权衡功能价值而非单纯追求吞吐量。我并非否定SQLite的适用性,但本文混淆了两个不同问题/解决方案的对比,这令人困惑。

    1. 没错——但SQLite在Postgres同机运行的场景下仍轻松胜出。在实际运行环境中测试技术完全合理。

      作为一个行业,我们似乎已形成了一些效率低下的惯例。根本不存在必须采用低效方案来解决的问题,仅仅因为有人声称数据库应该运行在不同的主机上。

      1. 若要在多台硬件上运行,必然存在某些组件会与单写数据库处于远程状态。

        整个行业普遍排斥“单一大型服务器”方案,其原因未必与性能相关。

    2. > 若必须保留软件的初始需求,那么SQLite完全不适用。

      并非如此?你完全可以在另一台服务器上运行轻量级的SQLite封装进程。归根结底,所有数据库服务(包括PostgreSQL)本质上都是请求处理器与存储处理器的组合。SQLite仅是存储处理器,但同样能轻松部署在请求处理器之后。

      通过串行请求队列访问SQLite曾是实现多线程写入的标准方案。这与通过TCP协议访问的差异微乎其微。

      1. 这样做可行,但会遭遇作者描述的远程Postgres实例相同瓶颈。工作负载暴露了热门行数据的高竞争性。由于客户端与数据库服务器间的远程网络调用,若事务因延迟而保持数毫秒开放状态,即使使用SQLite,吞吐量同样会受到限制。

      2. 完全正确。人们常忽略“SQLite无法实现X功能”的本质是“SQLite未内置X功能”。若通过轻量级请求处理器或队列进行封装,本质上就能复现其他数据库的实现模式。PostgreSQL自带协调器的事实,并不意味着SQLite天生无能,只是取决于你选择集成还是外部化该层功能。

      3. 只要未启用WAL模式,通过NFS/SMB或其他文件共享协议的连接都能正常工作。

        我并非认为这是好主意,且可能以惨烈方式失败,但确实可行。在此基础上执行DML操作纯属自找麻烦。

        1. > 这只是dqlite而已。

          差远了——现在你不仅要处理网络传输,还得应对Raft共识机制…因此每次写入不仅涉及网络往返,还需两次确认。读取操作需通过领头节点,这意味着当用户访问节点1的应用时,若节点2是领头节点,那么…

          读写速度比PostgreSQL的简单复制机制更慢。我指的不仅是异步场景,即便是同步PostgreSQL也会更快。

          dqlite的诞生源于Canonical需要同步其虚拟化集群(lxd),且需要具备Raft共识的数据库——它本质是库文件(而非像PostgreSQL那样的完整服务器安装包)。性能并非其核心诉求,其使用场景也与多数人的需求截然不同。

        2. Dqlite和Rqlite主要用于构建容错集群。若仅关注网络访问功能,那当然可行,但这又如何?

          1. 我是rqlite[1]的创建者。

            技术细节:dqlite是库文件,并非像rqlite那样的网络暴露型数据库。虽然它需要通过网络连接其他节点,但本地访问是通过进程内方式实现的。而rqlite则是通过网络连接——具体来说是HTTP协议。

            [1] https://rqlite.io

    3. 矛盾的是,若要在单台服务器上扩展,原始吞吐量反而更为关键。本例中SQLite在单服务器环境下的吞吐量是PG的10倍。考虑到数据库往往是性能瓶颈,这种差距足以带来数量级的提升。此外,当事务复杂度增加时,同一服务器上的PG性能也会下降——因为UNIX套接字通信始终比函数调用慢得多。

      本文还需指出:PG网络示例因幂律限制无法实现水平扩展。即便部署超级集群,其基础吞吐量仍仅约1000 TPS。

      1. 更重要的是廉价硬件的性能飞跃,这使得垂直扩展极具成效。单台搭载NVMe硬盘的服务器运行SQLite的WAL模式,其性能提升远超多数人预期。当前硬件的物质现实已远超我们的认知直觉。

        随着可靠的流式备份系统出现,真正的挑战已从扩展性转向冗余性。

      2. > 矛盾的是,若要在单台服务器上扩展,原始吞吐量反而更为关键。

        这其中毫无矛盾之处。

    4. > 若必须维持软件的初始要求,SQLite则完全不适用。

      如果这样写的话,文章会非常短,你觉得呢?完整文章大概是这样的:“通常你会建立到数据库的远程连接,而既然我们要测试SQLite的性能,且SQLite是嵌入式的,所以无法比较。完。”

      1. 文章末尾的数据表有7行,只有一行同时包含两个数据库的数据。既然没有进行比较,设置这个对比的意义何在?

        1. 因为这说明网络关系型数据库也无法解决这个困境。

    5. 开篇就说:“SQLite适用于手机和移动应用(偶尔也用于客机)!Web服务器请用Postgres这类正规数据库!”

      不过我认为它的适用场景更广(嵌入式应用、桌面应用、低并发服务器应用等)。

      手机和移动应用的安装量自然远超网页应用部署,且原文并未提及你转述的服务器相关论点。

  5. 真正的洞见在于识别网络延迟何时成为瓶颈。对于多数工作负载而言,即使是平庸的本地数据库也胜过卓越的远程数据库。关键不在于“哪种数据库最优”,而在于“我的架构是否必须跨越网络边界?”

    1. 深表赞同。每当看到那些将数据库部署在距应用服务器数百英里之外的“现代”架构,我内心的性能工程师都会不寒而栗。

    2. (本文作者)完全赞同。这篇文章本就不是SQLite与Postgres的对比,而是探讨网络数据库在特定场景下的根本性局限。坦白说,我在行文时确实难以充分表达这个观点。

    3. 当然。现在把所有数据都存进内存,用redis或memcache缓存。只要改变规则,性能提升轻而易举。

      1. 你可以用SQLite做持久化存储,哈希表当缓存。或者直接选Mongo,毕竟它天生适合Web级规模。

        1. 确实,关键在于需求。若需持久化存储,就得考虑冗余和备份方案,此时性能指标的重要性就大幅降低了。

  6. 你是否将Postgres连接数限制在8个?这是否不必要地限制了吞吐量?这似乎是个相当明显的瓶颈…连接池适用于应用程序压垮数据库的情况,但在此场景下,你 应该 尝试增加Postgres的负载…我担心整个实验因这个瓶颈点而失真。我强烈建议重新测试时采用更大规模的连接池。(同时建议分享Postgres端CPU和线程使用率数据)。(附注:我虽是sqlite拥趸,但曾多次遭遇因连接池设置过低导致的性能瓶颈)

    1. 补充说明

      您提到将连接池设为8以匹配核心数。若事务内部无睡眠操作本无问题…但一旦在事务中加入睡眠,8个连接的上限便会彻底阻塞吞吐量——因为当8个线程抢占连接并启动总计20毫秒的睡眠时,其他线程将无法访问数据库。试想若使用64个连接…吞吐量将提升8倍…若进一步增加呢?某一临界点可能导致数据库过载…此时可考虑调优数据库以接纳更多连接…或者…或许你已触及数据库的性能极限。

      我认为8个连接数并不代表极限…你需要竭尽所能放宽客户端配置,直到触及PostgreSQL的性能边界。

    2. 我完全不认同这篇文章。数据有问题。我在网络环境下用极简配置的MySQL实例就能实现远超这个的TPS。

      编辑:继续给我差评吧。我他妈讨厌这个网站。

  7. 安德斯,一如既往的精彩。运行网络化数据库的好处之一,就是让应用程序的重新部署变得更简单。你可以启动新的EC2实例或其他设备,一旦上线就关闭旧的实例。这样几乎可以实现零停机时间。若数据库部署在同一实例上,替换时需将数据库迁移至新实例,这似乎比直接在原实例重启应用更易出错。但根据我的经验,后者通常会导致停机或需要复杂的交接逻辑。你在生产环境运行sqlite时遇到过类似情况吗?

    1. 哦,这涉及诸多考量。

      服务器需要持久化存储而非临时存储,还需配备NVMe。多数情况下最终仍会选择裸机运行单服务器。

      除非在模式设计和/或副本配置上极具巧思,否则迁移过程必然伴随停机。

      对我而言,Litestream才是让SQLite适用于Web应用的关键——此前它缺乏完善的复制方案。

      借助Litestream,建立备用备份变得简单许多。不过我在生产环境使用时,仍需接受一定停机时间,实际效果可能因人而异。

      1. > 至少对我而言,正是Litestream让SQLite成为Web应用的可行选择——此前它缺乏完善的复制方案。

        难道SQLite现在没有内置用于副本的rsync功能?

        搜索结果显示确实有… https://sqlite.org/rsync.html

      2. > 除非你在模式设计和/或副本配置上极具巧思,否则迁移期间必然存在停机时间。

        这类设计考量/假设或许值得提前说明

        我确信许多应用能接受“[数据库]迁移导致的停机”,但更多应用绝对无法容忍,尤其是关注合成指标(如每秒事务量)的应用

        1. 我持相反观点:多数应用每月承受一小时停机时间完全可行,实际容忍度可能更高。近期AWS和Cloudflare的故障就是明证。

          若真有必要,使用SQLite可实现零停机。

          当系统受限于阿姆达尔定律和用户分布式计算能力而只能达到100 TPS时,TPS就不再是合成指标。

          1. 每月1小时停机意味着最多只能提供99.9%的可用性。虽然对许多应用而言这已足够,但这只是微不足道的规模,远低于AWS和Cloudflare提供的服务水平。

            退一步说,若你的应用数据库需求能通过sqlite[+复制]满足,那固然理想,但这类需求范围远比postgres的适用场景狭窄,也更容易解决。

    2. SQLite支持多进程访问,只要配置允许,同样能实现“旧进程退出前启动新进程”的操作模式。

  8. 此说法极具误导性。SQLite的安全默认设置已被修改,提交操作实际并未写入磁盘。如此运行SQLite将导致操作系统崩溃或断电时数据丢失。

  9. 作者设置了PRAGMA synchronous="normal",意味着fsync不会在每次写入事务时触发,而是延迟执行。为确保对比公平性,应将其设为“full”。

    1. 若处于WAL模式,PRAGMA synchronous="normal"即可满足需求。与日志模式不同,此模式下数据库不会因断电而损坏。

      > synchronous=NORMAL设置为运行在WAL模式的大多数应用程序提供了性能与安全性的最佳平衡。虽然在WAL模式下使用NORMAL同步会导致断电时持久性丢失,但这对多数应用并不重要。事务仍保持原子性、一致性和隔离性——这些才是绝大多数使用场景中最关键的特性。

      1. fsync是写入过程中最耗费资源的操作。NORMAL模式意味着您不关心进程崩溃/虚拟机重启前最后约100毫秒的交易是否会被持久化。建议采用synchronous=“full”或禁用Postgres的`synchronous_commit`,避免不同方案的横向比较。

        编辑补充:示例涉及金融交易场景。能否解释为何需要可串行性但不需要持久性?

  10. SQLite如何处理高可用性架构?我最低要求是面向用户的服务能在合理时间内实现可靠的自动故障转移,理想状态是主动-主动架构。

    1. sqlite只是一个库(用C语言编写)

      几个项目示例:

        * https://github.com/rqlite/rqlite 分布式容错集群
        * https://litestream.io/ 复制到S3(或兼容存储)——侧重灾难恢复而非故障转移
        * https://fly.io/docs/litefs/ 与litestream同作者。分布式复制。需将写入重定向至主节点。
      

      我正在为一个项目权衡Postgres和sqlite(可能搭配litestream)的选择。

      除了硬件冗余,通过单台服务器垂直扩展就能满足大部分需求。对于我的应用(以及用户!),偶尔的停机时间尚可接受(只要数据有复制/备份)。

      若能达到2-5万用户规模,项目就算成功了。因此当前阶段无需过度投入,采用SQLite方案更经济高效。

      1. 我是rqlite创建者,欢迎随时提问。

  11. 是否有组织实际应用SQLite并分享经验的案例?近期我研读了大量理论与基准测试,其表现令人惊叹,但想了解是否有人记录过在生产环境中将其性能推向极限的实践?

      1. 感谢分享!我很久以前读过这篇文章却始终记不住,还以为是Shopify写的。

      2. 他们或许该坚持传统技术,把才华用在别处。股价已暴跌96%

  12. 有人掌握SQLite与PostgreSQL在典型用户端Web应用或电商场景中的可行性数据吗(如最大日活跃用户数等)?

    我知道SQLite最近更新支持并发读取,但仍仅限单写入。这种限制会在哪些场景引发问题?

    有人建议若考虑未来扩展,最好直接从PostgreSQL起步……

  13. SQLite很酷。你可以创建内存数据库,速度极快。不过最好坚持只读模式

    只需几行代码就能将任意数据库从磁盘复制到内存。

  14. 这套方案很棒,直到你遇到客户提出0容忍的RPO要求。SQLite虽有复制方案,但在高风险场景下我绝不会信任它胜过PGSQL/MSSQL/DB2/Oracle等数据库。

    1. 我很好奇,RPO为0的要求是否真实存在?某些场景确实可能“需要”这种精度。

      究竟何种数据至关重要到必须保存灾难发生前四分之一秒的数据?

      武器测试或许算一个…但绝大多数时间里,流式数据根本不具备这种级别的关键性。

      1. 我的经验是:有钱客户永远觉得自己至关重要,重要到丢失一个比特都会天崩地裂。

        因此面对挥舞巨额支票的客户,你未必该说服他们数据没那么重要。但当他们意识到:自己财力有限,且完全能接受部分数据丢失时,提供备份方案才是正解。

  15. 当前项目中sqlite-vec表现不错。接下来要研究地理空间扩展功能。

  16. 更倾向用“出人意料”而非“不合情理”。

  17. SQLite数据库无法可靠地跨多设备同步。在实现可靠同步前,我始终在寻找能做到这一点的数据库设计。

  18. 很好。我感觉人们总在重新发现这个原理:“引擎处理的事务越少,运行就越快”,但我绝不会贬低任何人学习这个原理的过程,无论他们如何领悟。

    1. 如相关讨论所述,若磁盘及文件系统正常运作,SQLite WAL 不会发生损坏。反之则无法保证——SQLite 对此无法提供保护,多数其他数据库亦然。嵌套事务(SAVEPOINT)对此毫无影响——此类操作仅能减少事务数量。

      1. > 正常运作的磁盘与文件系统

        还需正常运作的ECC或非ECC内存总线,以及[…]等条件。

        [在SQLite环境下][以10万TPS批处理]时,WAL校验和/日志损坏的恢复难度有多大?

        是否应在SQLite上“附加”分布式复制功能时使用WAL校验和?

        >> (如何)为确保一致性应向SQLite添加默克尔哈希?SQLite中的默克尔哈希与WAL校验和有何区别?

        即使采用正确的默克尔化方案,SQLite在网络传输中仍可能保持更快的速度

  19. 此前我始终使用Postgres作为数据库,后端采用Rust或NodeJS开发。在新建网站(https://limereader.com/)时,我改用Swift构建后端,SQLite作为数据库,在Swift应用中部署Vapor作为Web服务器,并通过旧款Mac mini自主托管站点。

    遇到一个SQLite相关问题:涉及多线程访问SQLite数据库。很快找到解决方案:多线程使用时需使用SQLITE_OPEN_FULLMUTEX标志初始化SQLite。此后网站已稳定运行约三周。

    1. 这会阻塞线程等待其他线程写入。虽然这种模式可能很适合你的线程模型,但我通常会将写入操作集中在一个线程中,其他线程只负责向写入线程发送数据。

      1. 我确实会打开两个连接:

        第一个用于带标志的写入:

            SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX
        

        第二个用于带标志的读取:

            SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX
        

        如你所见,两者都包含SQLITE_OPEN_FULLMUTEX标志。是否应仅在写入操作中使用该标志?

    2. 建议改用线程级连接。跨线程共享连接会限制并发性,且事务行为将偏离预期。SQLite连接开销较小。

      另建议启用WAL模式并开启mmap功能。

    3. 若能在应用层正确管理连接,则无需全锁。即确保每个连接每次仅被单线程使用。强烈建议为批量/写入操作设置多线程并行队列,并通过单一连接处理,这样就无需处理SQLITE_BUSY或SQLITE_LOCKED异常。

  20. 多年来我一直用SQLite作为TxtAI的内容存储引擎,表现非常出色。它还有许多优秀的扩展库,比如用于存储向量的sqlite-vec。SQLite功能强大,在许多场景下完全能满足需求。

  21. 这些数字组合就叫数据库。

  22. HN上对SQLite的崇拜帖子已经失控了。接下来该发帖说文件追加比Kafka更快了吗?

    对于特定工作负载而言,SQLite确实是理想选择。但更常见的场景是RDS等托管数据库能解决的问题:你需要持久存储一定规模的数据,通过网络供各类应用访问,同时要求自动备份、升级和访问控制等功能。

    我热爱SQLite,常在业余项目中使用它,但作为通用商业应用产品,它属于小众领域。它具备所有热门HN产品的特质:出色的入门体验,以及复杂的维护运维体验。

    1. 坦白说,我认为这是对当前过度优化的反响——人们总急于投入AWS、Kubernetes和微服务。绝大多数项目根本不会触及SQLite单服务器架构的性能极限。

      我并非否定AWS及集群方案的合理性,但不应将其视为默认起点。

  23. SQLite确实很棒。但刻意降低Postgres性能有何意义?

    > 等等,我们的事务并非可串行化,而一致性事务处理需要这个特性

    要么你根本不懂可串行化机制,要么就是故意误导读者。此处根本无需使用可串行化。

    > 假设应用服务器与数据库间存在5毫秒延迟。

    5毫秒延迟根本不现实。除非你用WiFi,或数据库在其他数据中心。

    > 我指的是每秒事务量,特别是构建Web应用时常见的交互式事务

    根本不常见。你八成是编造出来诋毁Postgres的。

    1. > 这种场景根本不常见。你很可能只是为了贬低pg才杜撰出来的。

      交互式事务是数据库技术中明确的概念,其语义恰如作者所述:包含多个查询的事务,中间穿插应用逻辑——例如从一个查询获取结果,进行处理后,再将处理结果作为输入执行第二个查询。

      话虽如此,博客文章中的示例在我看来有些牵强。关键问题在于:能否将该事务设计为在事务尾部连续执行两次更新操作,从而大幅缩短行锁的持有时间(这并非否认文章所述普遍问题不存在,该问题确实存在)。

      1. 我希望示例保持简单易懂,因此采用了接近Postgres官网事务示例的方案(坦白说那个示例也略显刻意)。

        实际工作中我绝不会采用这种账本设计,更倾向于使用带触发器的只追加模型。但正如你所说,交互式事务确实存在。

        出于好奇,你是否有更贴切/更自然的示例?

    2. > 你要么不理解串行化机制,要么在误导读者。此处完全没有使用串行化场景的必要。

      处理金融交易时,隔离级别必须设置为可串行化。因为事务处理顺序至关重要。

      > 5毫秒延迟不切实际。除非你用WiFi或数据库在其他数据中心。

      即便延迟仅1毫秒,在100%行锁竞争的情况下,阿姆达尔定律仍会使理论TPS上限锁定在1000。

      > 这些案例根本不常见。你很可能为了贬低PostgreSQL才杜撰出来。

      我有点困惑。是我杜撰了事务吗?你是说使用PostgreSQL时不采用可回滚的事务?

      1. > 处理金融交易时,必须采用可串行化隔离级别。因为事务处理顺序至关重要。

        所以你根本不懂可串行化级别?

        > 即使延迟仅1毫秒,若存在100%行锁竞争,阿姆达尔定律仍会使理论吞吐量封顶在1000 TPS。

        那为何要举5毫秒和10毫秒的例子?

        > 我很困惑。是我发明了事务?

        “交互式”事务。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

你也许感兴趣的: