写点什么

IT Hare—数据库终极“异端邪说”:单一写入数据库连接,第 1 篇:性能

2016 年 12 月 28 日

Sergey Ignatchenko 在撰写的一本有关数据库的图书,最近发布了本书的最新章节。本文属于友情转载

本文大量出现了“单一写入连接”这个概念,这个概念的定义出现在这本书的其他章节中,为了让本文更显合理,我专门向 Sergey 请教了这个概念的定义。

对于单一写入连接(Single-write-connection),我是指只使用一个应用(本文中名为“数据库服务器”),向数据库建立一个用于执行修改语句(UPDATE/INSERT/DELETE)的数据库连接。这种做法可以大幅简化架构的设计:首先,可以从根本上彻底避免所有不可测试的并发问题(如缺少 SELECT FOR UPDATE 和死锁);其次,整个系统更具确定性(对查找 Bug 很有帮助,就算简单的纯文本日志也可以让系统更可调试并让事后分析更容易);最后同样重要的是,这种“独断式”的更新可以通过非常创新的方式改善性能(尤其是维持始终井然有序的应用级缓存,可实现比直接访问数据库高 100 倍 -1000 倍的效率)。

了解这些情况后,可以开始介绍最有趣的部分了:为事务型数据库和数据库服务器实现这样的设计。本书第 7 章简要介绍了数据库服务器的实现,本文将对整个重要话题展开更详细的讨论。

首先明确一下我们的目标:处理不同业务(如证交所、银行等)的各种自动化决策任务。

这种数据库中存储了账户等信息,以及这些信息的各类持久属性,此外还存储了与支付处理有关的各类往来通信。“数据库服务器”则是指用于处理 DBMS 各类访问的应用(正如第 7 章所述,我坚定地反对通过应用服务器 / 应用逻辑直接发起 SQL 语句的做法,因此需要具备数据库服务器这样的“中间人”)。

ACID 特性对事务 / 运营数据库是至关重要的。我们肯定不想自己的钱,或在 eBay 上能卖出两万美元价格的任何东西不小心丢失或被人复制。出于一些原因的考虑,我们将介绍使用 SQL 数据库充当的事务 / 运营数据库(虽然 NoSQL 也可用于事务 / 运营数据库,但通常很难实现严格的保障,尤其是目前的大部分 NoSQL 数据库缺乏多对象 ACID 事务)。

接下来,正式开始讨论这种有趣的做法吧。

多连接数据库的访问

数据库的访问可通过两种截然不同的方法实现:多连接和单一连接。首先谈谈建立多个数据库连接这种更常见(但在我看来存在不足之处)的方法。

多连接方式访问数据库是一种常见做法,以至于很少有人会考虑是否可以通过其他不同方式实现。这样的想法其实很简单:建立到 DBMS 的多个连接,直接将所有可能需要的请求丢给数据库,由数据库负责处理这些请求。更重要的是,这种情况下不会遇到缩放性方面的问题,想要缩放,只要购买更多硬件即可(但实际情况并没有这么简单,原因见下文)。

虽然多连接数据库访问方式如此普遍,但这种方式依然存在一些不容忽视的局限:

只要涉及两个并发事务(例如写入 / 修改等事务),此时就需要考虑事务隔离问题。

事务隔离是个很麻烦的事。出于性能方面的考虑,大部分时候我们无法使用序列化(Serialized)隔离,而只要脱离了序列化隔离级别,不同事务就会开始相互交互,糟糕!

就某种意义来说,读取提交(Read Committed)是最常见的事务隔离级别,这种级别类似于编写多线程代码,并且会遇到写此类代码时类似的问题,尤其是:

可能出现本应锁的数据未能锁的隐患,这会导致数据库内出现罕见且无法测试的无效数据修改问题。更重要的是,由于这些问题转瞬即逝 / 不确定的本质,进而导致:

  • 这些问题不可重现,不可测试。
  • 这些问题倾向于长期维持潜伏状态,当负载超过某一阈值后,问题发生的概率将呈近乎指数形式增长。换句话说,可能会有某个(通常其实有很多)问题安静地隐藏在系统中,伺机在某个重要的日子(例如年度锦标赛期间)发作,给你迎头一击。
  • 当问题实际发生后对其进行追踪几乎也是不可能的(除非进行彻底的代码审阅,但这需要特别长的时间)。

此外还存在死锁的风险。需要注意,数据库的死锁不像多线程死锁(一笔事务的死锁最终将能回滚)那么让人难以接受,但依然会造成不小的麻烦。为了应对这类问题,项目通常会确定明确的获取锁顺序,只要项目中的所有人能够遵守这样的顺序,大部分情况下都可以高枕无忧 [1]。然而如果任何一个查询未能遵守这个顺序,迟早还会遇到死锁。

由于这个问题是转瞬即逝并且不确定的,因此上文提到的不可重现、不可测试、长期潜伏、不可调试等问题依然隐藏在“暗处”,时刻准备着发作并造成最糟糕的影响。

就算实现了序列化隔离级别,依然可能遇到锁(写入 / 写入锁),或由于某个事务造成写入 / 写入锁。

这些问题并非无解,其实是可以解决的。然而解决之前需要满足两个非常苛刻的前提条件:

开发团队需要包含具备娴熟经验的数据库专家。更重要的是,此人需要在高负载 OLTP 数据库方面具备丰富的经验(至少每天数百万事务,并进行事务监视等操作),这样的人其实非常难找。如果不具备这种现实环境中的高负载 OLTP 经验,可能根本无法处理高负载 OLTP 数据库可能遇到的某些(或大部分)问题(可能出现的问题远远超出了上文列出的范围),进而为整个项目带来毁灭性后果。

另外还必须要将数据库代码与应用的逻辑代码分开(正如第 7 章所述,无论使用多连接或事务隔离级别,都需要这样做)。在考虑应用逻辑的同时编写事务隔离感知的 SQL 语句,这种做法的后果无异于信贷危机级别的灾难。

正如在第 7 章中提到的,应用逻辑代码和数据库服务器代码之间的接口(也叫做数据库服务器 API)必须以应用逻辑的形式(而非 SQL 的形式)体现。更重要的是,每个请求必须对应至一个事务(不给“已更改但未提交”留下任何可趁之机,因为这会产生大量锁并维持无法确定的时长,会在影响性能的同时大幅提高发生死锁的几率)。

应用逻辑服务器和数据库服务器之间维持清晰的分隔,可以让有经验的数据库开发者专注于数据库工作本身(包括专注于实现令人敬畏的事务隔离级别),同时让其他所有人尽量远离这个“危险的领域”。

更重要的是,无论 RDBMS 的宣传材料里怎么说,实际情况远非“线性”那么简单,现实世界中,具备多连接数据库访问特性的系统,其缩放能力绝对不是线性的,实际上远比“线性”更复杂。如果你不信,可以问问在每天数百万笔写入事务的数据库方面有经验的人(如果不认识这样的人,那么你还是相信我吧,最好彻底放弃建立多连接数据库服务器这种想法)。

造成这种现象的原因在于,任何多连接 DBMS 都会因为不同的数据库连接产生大量资源争夺的情况(而数据库日志文件是无法彻底避免争夺,并且最重要的资源之一,数据库日志文件需要连续写入并执行 flush()/sync() 操作,真是麻烦!),对于资源的任何争夺都会自发地导致非线性的缩放性。

然而我们可能会将 OLTP 数据库缩放至多台服务器(如果有能够胜任此类任务的 OLTP 数据库专家的话),但毫无疑问这一点也是很难做到的。

太多的并发请求会导致整个系统速度变慢。为缓解此类问题通常会使用 TP Monitor。

虽然几乎每个数据库供应商都会说自己的 RDBMS 可以实现完美的线性缩放,但实际并非如此(这一点要铭记于心)。从大量过往经验来看,现实世界中的数据库根本无法以线性方式缩放,就算以多连接方式实现可缩放性的产品也是如此,实际上我们迟早要在应用层面上解决缩放问题(从我的经验来说,当你需要考虑使用单一写入数据库连接的时候,就该考虑这个问题了,详见下文讨论)。

平均无故障时间( MTBF )是指可系统内部失败两次之间可预测的间隔时间。— 维基百科

最后同样重要的是,从我的经验来看,相比单一连接数据库,负担繁重的多连接 RDBMS 通常崩溃的频率也更高。尤其是据我观察(使用装备了 ECC、冗余散热、RAID、运行状况监视器等各项措施的企业级服务器,运行某个大型供应商的 RDBMS 产品),在使用单一连接数据库应用 [2] 时,该 RDBMS 本身的 MTBF(即 RDBMS 自身崩溃的情况)约为每执行 5e10-1e11 次写事务出现一次,使用多连接数据库应用时约为每执行 1e10-2e10 次写事务出现一次。当然这只是坊间证据(你所使用的 RDBMS 在这方面可能有所差异),但实际观测证明了在单一连接模式下,RDBMS 内部出现“竞争”的可能性低很多,因此如果不是有非常具体的证据能够证明你所用的某个特定 DBMS 不存在这种情况,我也不会给出这样的观察结果。我们将在第 3 卷进一步讨论服务器端的 MTBF(第 31 章有关部署架构的第 2 版内容中进行了简要的介绍)。

如你所见,我本人并不太喜欢为 OLTP 数据库使用多连接的方法(基本上也是出于类似的原因,我不喜欢大规模多线程,不过对于数据库来说,这些问题的影响并不像对多线程那么大)。但必须承认,你依然可以为 OLTP 使用多连接。

话虽如此,只要具备在每天数百万写事务规模的 OLTP 数据库方面有实战经验的专家,一切都不是事儿。

如果不具备这样的人(当然,也不能想当然地找那些在数 TB 规模,但主要以读取事务为主的数据库方面有 20 年从业经验的人来代替),OLTP 数据库的多连接数据库访问这种做法会蕴含大量风险。此外情况还可能更糟(原因见上文),数据库服务器也许在测试和“Beta”阶段看起来可以正常运行,但在生产部署并且负载规模达到某一程度后,可能会招致无穷无尽的钱财和数据损失,以及用户的抱怨等。

有关 TP Monitor

如果尽可能以并行方式运行所有这些未完成的请求,由于需要进行大量线程上下文切换和资源的争夺,系统性能还将进一步大幅降低。

如果依然希望使用多连接方式访问数据库,有个重要的问题需要注意:有可能迟早你会需要使用“事务处理监视器”,即 TP Monitor。TP 监视器的想法很简单:如果需要以严格并行的方式运行所有未完成请求,由于需要进行大量线程上下文切换和资源的争夺,系统性能可能大幅降低。换句话(大致来)说,TP 监视器可以确保任何特定之间内数据库只需要执行少量请求(实际上可并行运行的请求数量取决于运行数据库的硬件,例如磁盘数量和 / 或处理器内核的数量)。

目前最著名的两个 TP 监视器是 Microsoft COM+ 和 Oracle Tuxedo(原先的 BEA Tuxedo)。

另外作为反应器(Reactor)的“铁粉”,通常我更倾向于自行构建所需的 TP 监视器。本书第 7 章将详细介绍相关做法,简单来说实际上就是使用多个数据库服务器工作反应器(与每个数据库建立一个连接),并通过一个数据库服务器代理反应器接受系统其他组件发来的所有请求,随后将请求转发至一个“空闲”(负载最小)的数据库服务器工作反应器。总的来说,TP 监视器并不涉及什么高深的技术,大部分时候我觉得自行构建也是一种很简单的方式(当然,始终应该视具体情况决定)。

数据库终极“异端邪说” – 单一写入连接的数据库访问

上文一直在吐槽多数据库连接方法 ;-),该说说单一写入连接了。这种方法最初的形式很简单:有一个数据库服务器应用,维持一个数据库连接,这个数据库服务器应用接收传入的请求,选择预先准备好的相关 SQL 语句,与配套的参数绑定并发起执行该 SQL 语句的 API 调用。收到回复后,该数据库服务器应用会将收到的结果打包成相应格式的回复并发送回请求方,然后等待接收下一个请求。

这里有个很重要的问题需要注意:我们说的是单一写入连接,与这个单一写入连接并发执行的可能有任意数量的只读连接。另外出于性能方面的考虑(并且为了避免使用锁),此时需要使用 RDBMS 所能提供的最低级别的事务隔离。换句话说,如果你使用的 RDBMS 支持基于锁的并发,这些并行的只读连接可能需要使用读取未提交(Read Uncommitted)事务隔离级别,对于基于 MVCC 的 RDBMS(但是 MySQL+InnoDB 是个例外)通常则要使用读取已提交隔离级别。这个特性会造成大量影响,但对于涉及历史数据的请求,以及 99% 的报表请求都可归于这个类别,这些请求均能在上述隔离级别下正常运行。

如上所述,单一写入数据库连接完全不存在并发的情况,任何特定时刻有且只有一个 SQL 语句正在执行。这意味着服务器端无须任何调整(且无需考虑所用隔离级别!),上文提到的与隔离级别有关的所有怪异问题都将不存在。

当然这样的简化也需要付出代价:可缩放性的缺乏。实际上缺乏可缩放性正是 99% 的人认为 OLTP 数据库使用单一写入连接是一种终极“异端邪说”的最主要原因(例如别人可能告诉你“这样根本不可行”)。我曾经不止一次见过一些高负载游戏(以及证券交易所)就是这样使用的。更重要的是,一些情况下,某一此类数据库甚至被称作“我们所知的 Windows 平台上运行的最高负载的 DB/2 实例”,说这话的人可不光来自 IBM Toronto Labs(DB/2 UDB 最初就是从这里诞生的,据我所知该实验室目前依然承担了 DB/2 的研发工作)。因此现实世界中,单一写入连接的数据库并非那么不堪,那么理论(“根本不可行”之类的言论)和实践(通过实例证明这种说法是错误的)之间为何会有这么大的差距?

单一写入数据库连接:每连接性能

首先要讨论一下单一写入数据库连接架构的性能。严格来说任何程度的性能都不足以取代可缩放性,但性能却可以影响我们开始考虑可缩放性的时机。

应用级缓存 – 有助于大幅提高性能

只要能严格限定只能通过一个数据库连接修改数据库 [3],即可增加应用级缓存,并让这个应用级缓存与数据库实现完美的相干性(Coherent)。这是因为对于单一写入连接数据库,我们可以全面掌握有关数据库的所有改动,因为只能通过一个连接做出这种改动。

实际上我发现很多情况下可以通过这种应用级缓存大幅改善性能。我曾发现通过使用应用级缓存,可让整体性能实现 5 倍 -10 倍的提升!如果研究一下具体的处理过程就会发现,这样的结果不足为奇。如果使用应用级缓存(此类缓存中最流行的做法是对 PLAYERS 表创建缓存),随后我们只需要获取必要的玩家数据(几乎所有操作都需要使用此类数据),例如计算玩家 ID 的哈希,随后通过哈希值为玩家数据建立内存计算结构。总的来说,这一过程只需要使用大约 100-200 个 CPU 时钟(约 0.1 微秒)即可完成。

然而如果要为 DBMS 执行类似的操作,我们需要:(a) 绑定一条预先准备好的语句,(b) 发起一个 API 调用,© 由 API 调用封送(Marshal)所需数据,随后 (d) 通过某些 IPC(最有可能的做法是进行用户模式 - 内核模式 - 用户模式转换,并至少产生一个线程上下文切换)进入另一个进程,随后这个请求将会 (e) 取消封送(Unmarshaled),(f) 找到一个与预先准备好的语句对应的执行计划,随后 (g) 执行该执行计划,获取并解析(!)多个索引页以及至少一个数据页,随后 (h) 解析数据页,(i) 得到有关用户的数据,(j) 封送,(k) 发回(再次产生一次用户模式 - 内核模式 - 用户模式转换,以及另一个线程上下文切换),随后还需要 (l) 取消封送,并 (m) 交付给应用。因此数据库内部的处理比应用中的处理花费更长的时间也不足为奇了,实际上对于数据库的访问,通常需要 10 微秒 -100 微秒左右的时间,相比应用级缓存内部的搜索慢了 100 倍 -1000 倍左右。为什么无法实现上文提到的 5 倍 -10 倍的提速?这是因为有很多其他任务需要通过数据库完成(尤其是为了实现耐久性,数据库事务需要经历完整的数据库同步,为解决这个问题可参阅下文的 Kinda-write-back 缓存)。

实际应用中,整体性能就算只有 5 倍 -10 倍的提升也是个不错的结果。但是需要注意,应用级缓存并不是优化数据库服务器所要采取的第一个措施,此处有关应用级缓存的讨论更多地是为了通过这种概念方便大家理解,并证明单一写入数据库连接这种做法是在进行全面缩放前可以考虑的一种有效的临时措施。数据库的整个优化过程(包括索引、数据库物理布局和 RAID 级别、规范化(Denormalisation)以及应用级缓存)将在本书第 3 卷详细介绍。

持久的 Kinda-write-back 应用级缓存

另外,上文提到的 5 倍改善是通过直写模式(Write-through)的应用级缓存实现的。此外还可使用 Sorta-write-back 类型的应用级缓存,该模式也可以实现 100% 正确的 ACID 耐久性保障。这样做可以进一步改善性能(然而我自己未曾尝试过,因此无法准确地说到底能实现多大程度的改善)。

本例中的想法主要是:

  • 数据库上大部分时间在运行某个事务(假设称之为 Larger Transaction),这并非一种真正的逻辑事务,而是一种在需要时实现耐久性的方法。
  • (假设以完美的序列化方式)接收到传入的请求后,将在 Larger Transaction 的上下文中执行该请求,但(暂时)不提交。
    • 在这一过程中几乎一切内容均可从应用级缓存中获取,尤其是我们更改了与数据库约束有关的所有数据(另外提一下,这意味着可以在数据库层面上取消很多约束,借此进一步进行优化)。更重要的是,由于应用级缓存可保证具备相干性,因此几乎可以获得需要的一切。
    • 在认为该事务成功运行后,还可以修改应用级缓存。
  • 虽然要为请求提供预先准备好的回复,但此时(暂时)还不需要实际发送,此时这些回复为“延迟的回复”。
  • 随后某一刻我们决定提交 Larger Transaction。
    • 可能是因为我们单纯不再需要延迟的回复了,或者因为我们即将执行“有潜在风险”的事务(可能在数据库层面失败的事务,例如缓存中未能包含执行操作所需的全部信息)。
  • 由于事先对应用级缓存进行了所有必要的检查,99.9999% 的时间里 Larger Transaction 都能成功运行。
    • 随后如果事务成功运行,可以将延迟的缓存发送给相应的接收方并继续运行。
    • 如果事务未能成功运行(无论原因如何,但这种情况很罕见),此时我们可以:
      • 丢其所有延迟的回复。
      • 将应用级缓存中的改动撤销至 Larger Transaction 启动时的状态,为此我们可能需要提供某种类型的应用级“回滚列表”,其中包含了自 Larger Transaction 上次启动后所修改的全部内容的“原始”值。
      • 尝试重新处理传入的消息(这些消息均为 Larger Transaction 的组成部分,并有相应的延迟回复),每次处理一笔事务的一个消息。

上述全过程提供了严格的耐久性,因为只有等到相关事务已提交并在数据库层面实现耐久性之后,我们才会发送对应的回复(在某种意义上,延迟的回复实际上只是一种“尝试性的回复”)。另外在上述处理模式中避免了大量提交事务,提交操作在延迟方面的成本非常高(见下文讨论)。上文曾经提到过,我尚未实际尝试过这种 Kinda-write-back-cache 模式,因此无法估测“能起到多大帮助”。但是粗略估算来说,(a) 有很大可能实现 1.5 倍 -2 倍的提速,并且 (b) 更可行的结果是,将超过 5-10 个请求结合在一个 Larger Transaction 中并没有多大的实际意义。

有关性能的告诫:延迟,更多的延迟

在谈到单一写入连接数据库(在我看来非常出色的)性能优势时,关于这种配置还有一个不容回避的问题需要注意:单一写入连接数据库配置对延迟极为敏感。

我们即将谈到两方面的延迟:通信延迟,以及“数据库日志 flush()/sync() 延迟”。前者很简单,是指数据库服务器应用和 DBMS 之间的通信延迟,解决这种延迟的方法也很简单,将数据库服务器应用和 DBMS 放在同一台硬件上就行了,我们可以尝试不同的连接选项确定最佳配置,搞定!

后一种延迟(“数据库日志 flush()/sync()”延迟)有必要更详细地介绍。为了理解这种延迟,首先需要明白常规的生产用 RDBMS 是如何处理事务的:

  • 在事务中发起 SQL 语句时(例如提交前),RDBMS 会将其写入两个位置:(a) 写入实际保存数据的数据库页 [4],并 (b) 写入所有重要的数据库日志文件(即事务日志)。这里需要注意的是,这些写入操作均不需要与磁盘进行同步(暂时);换句话说,这些写入都是“懒惰”的,不需要实际执行应用程序所请求的 SQL 语句,也不需要等待磁盘写入。因此不会因为磁盘同步 / 刷新(Flush)造成额外的延迟。
  • 目前为止还不错,但在提交时情况变得大为不同。数据库页的写入可能(并且通常)依然是“懒惰”的,数据库日志需要 flush()/sync() 到磁盘。除非对底层硬件进行极为慎重的选择,否则这会导致单一写入数据库连接应用的速度大受影响。
  • 尤其是可以考虑:
    • BBWC 电池支撑的写入缓存,这是一种加速磁盘写入速度的技术。— 维基百科
    • —flush()/sync() 到不包含 BBWC RAID 卡的普通机械硬盘,这一过程可能需要 10 毫秒 -15 毫秒(!),这一时间段可分为两部分:寻道时间(硬盘磁头重新定位所需的时间),以及磁头到达所需写入位置的等待时间。服务器级机械硬盘的寻道时间通常为 5 毫秒 -10 毫秒,等待时间(平均值)则可通过每分钟转速计算而来(对于每分钟 15K 转速的机械硬盘,平均等待时间为 1/(15000 转 / 分钟 /60 秒 / 分钟)/2 = 2 毫秒)。
    • flush()/sync() 到固态硬盘(不包含 BBWC RAID),这主要取决于固态硬盘的规格,但我曾见到过提交至固态硬盘可以让数据库事务的处理速度获得数量级的提升(与 steveshaw 的结论一致):低于 1 毫秒。好多了,但依然有提升空间。
    • flush()/sync() 到 BBWC RAID。BBWC(电池支撑的写入缓存)是一种非常棒的硬件功能,可以将回写缓存保存在 RAID PCIe 卡上,并可保证崩溃和断电后数据不丢失(如果服务器崩溃,将由电池为数据的维系提供支撑,通常可将数据保留 2 天 -3 天)。在我们看来,这种功能最重要的用途在于可以将 sync() 操作的延迟降至最低:BBWC RAID 卡在将数据写入到(电池支撑的)板载 RAM 后即可汇报数据已成功写入。因此实际上此时的延迟主要来自于 PCIe 本身,除此之外基本不会有其他延迟,毕竟 RAM 的速度已经非常快了,数据库事务可在数百微妙内提交至 BBWC RAID 卡,实现与使用 NVMe 时类似的性能(但我自己没有尝试过)。另外,如果使用了 BBWC RAID,在该设备之后使用固态硬盘或传统机械硬盘就没什么区别了(至少从写入延迟的角度来说)。
    • SAN 存储区域网络(SAN)是一种可提供整合的块级数据存储服务的网络— 维基百科。
    • flush()/sync() 到 SAN。遗憾的是,我自己了解的 SAN 全部不支持服务器端回写缓存这种选项,因此需要通过 SAN 光纤提交数据,这会造成延迟。因此 SAN 并不是单一写入连接数据库理想的存储选项。然而也要注意,该结论只针对使用了单一写入数据库连接的事务 / 运营数据库,大部分其他应用依然适合使用 SAN 作为存储。

因此在搭建高性能单一写入连接数据库时,我们需要重点考虑服务器内部使用机械硬盘 / 固态硬盘(或直接附加连接的 SCSI/SATA 存储)以及 BBWC RAID(或 NVMe)的配置(软件 RAID 无须考虑!)。好在这些要求都很好实现(几乎所有主要服务器制造商都提供了 BBWC RAID 卡,同时虽然并未全面普及,但很多托管 ISP 也已开始提供此类设备)。BBWC RAID 卡的成本约为 1 千美元(主要服务器制造商的报价),只需要为少数数据库服务器配备,因此预算的压力其实并没有那么大。

单一写入数据库连接:现实世界中实现的性能

理论上这个方法看起来不错,但实际使用情况如何?我有一些经验可以分享 ;-)。

我见过很多现实世界中的系统(同时运行数百个不同的 OLTP 事务,大部分事务需要修改多行数据,并出于审计需求写入更多行的数据……)采用了单一写入数据库连接这种方法。

其中一个系统的常态为通过一个数据库连接每天处理超过 3 千万笔写入事务(未使用内存中数据库),同时为大约 10 万并发玩家提供支持。

(也就是说,在对数据库进行优化并为 USERS 表增加应用级缓存之后)执行每笔事务的平均时间为 800 微秒左右(如果负载是均匀的,这就可以实现 86400 秒 / 天 *1000 毫秒 / 秒 /0.8 毫秒 / 写入事务,约等于每天 1 亿笔写入事务,但由于一天内不同时段的负载分布不均匀,并且并非所有时间都是峰值时段,实际环境中该系统每天可以处理 3 千万 -5 千万笔数据库写入事务)。SQL 语句数量数百个,每个 ACID 事务的行修改和 / 或添加操作平均数量(粗略估计)约为 10 个(其中包含非常复杂的玩家间互动以及各种审计需求)。简而言之,你也可以搭建出这种性能的系统 (真正搭建实现实用的系统,而非使用 TPC-C 等人造的测试环境进行各种无法反应实际情况的测试)。上述系统可以为成千上万的并发游戏玩家提供支持,并提供了所有竞争对手系统中最稳定的表现。

在了解了具体的实现方式以及所涉及的数据后,我觉得对于具体所要处理的数据类型也没什么好说的了。简而言之:

我很确信,你的 OLTP 数据库也可以通过这种单一写入数据库连接在性能方面实现相同的数量级提升。

当然具体问题还要具体分析(例如你最终也许只能获得半个数量级的提升 [5])。当然,实现这种程度的性能需要付出极大的努力(包括数据库级别的优化和应用级缓存),但我相信市面上大部分 OLTP 数据库都是可以完美实现的。

单一写入数据库连接:缩放性

上文曾经提到过,性能(哪怕再大的提升)也不值得让我们放弃缩放性。换句话说,某些时候哪怕每天处理 3 千万数据库事务的性能也是不够的。但我们可以通过另一种方法(现实世界中也得到了广泛应用),以无须共享的方式实现基于多个单一写入数据库连接的可缩放系统。在某种意义上,这种做法可以看作是三种与微服务有关模式的“表亲”:Database-Per-Service 模式(参阅 Fowler Richardson.DatabasePerService )、伴生(Accompanied)的 Event-Driven Architecture(参阅 Richardson.EventDrivenArchitecture ),以及 Application Publishing Events(参阅 Richardson.ApplicationEvents )。更重要的是,还可以通过一些方法(现实中的一系列应用也证明了这种做法的成功之处)从简化的单一写入数据库连接架构逐渐迁移为这种无须共享的多个单一写入数据库连接架构。随后我们会分别进行介绍。

作者 Sergey Ignatchenko 阅读英文原文 IT Hare: Ultimate DB Heresy: Single Modifying DB Connection. Part I. Performanc

  1. 取决于具体 DBMS,可能存在一些固有的锁,如执行索引扫描时锁定下一行。对于这种情况只能自求多福了,这种死锁无法真正避免。
  2. 没错,多年的经验让我知道了很多 RDBMS 都会崩溃。
  3. 额外的只读连接也可以接受,以后我将撰文讨论。
  4. 通常这些改动只会在事务提交时写入磁盘。
  5. 当然这里提到的半个数量级只是标准偏差,该范围内的置信度约为 68% 左右。

感谢陈兴璐对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2016 年 12 月 28 日 16:171181
用户头像

发布了 283 篇内容, 共 84.6 次阅读, 收获喜欢 34 次。

关注

评论

发布
暂无评论
发现更多内容

认识数据产品经理(三 成为数据产品经理)

马踏飞机747

大数据 数据中台 数据分析 产品经理

“字节”不断“跳动”,卡拉永远 OK?

无量靠谱

字节跳动 诺基亚 危机

一杯茶的时间,上手 React 框架开发

图雀社区

Reac

猿灯塔-Phaser 使用介绍

猿灯塔

Flink Weekly | 每周社区动态更新

Apache Flink

大数据 flink 流计算 实时计算 大数据处理

Spring 中不同依赖注入方式的对比与剖析

Deecyn

spring

爱是恒久忍耐,又有恩慈

泰稳@极客邦科技

身心健康 心理

反对996,但是选择996是一个怎样的矛盾心态?

顾强

职场 加班

编程的门槛 - 抄作业的得与失

顿晓

编程门槛 编程思维 动手能力 抄作业

高仿瑞幸小程序 08 创建第一个云函数

曾伟@喵先森

小程序 微信小程序 前端 移动

有了容器为什么kubernetes还需要Pod?

架构师修行之路

Kubernetes 分布式 云原生 pod

需求是被挖掘还是被创造出来的?

Neco.W

产品 互联网 需求

如何快速更改qcow2镜像文件

奔跑的菜鸟

云计算

回“疫”录(15):在家SOHO,是你想要的工作方式吗?

小天同学

疫情 回忆录 现实纪录 纪实 远程办公

从波音747学项目管理

顾强

项目管理 读书感悟 沟通

编写制度的几点实用建议

石君

制度 编写制度 安全管理

游戏夜读 | 关卡设计为什么难?

game1night

终于有一款组件可以全面超越Apache POI

Geek_Willie

前后端分离 服务端 GrapeCity Documents

油管博主路透 3080Ti 参数、黄教主烤箱中拿出 DGX A100 预热发布会

神经星星

人工智能 互联网巨头 gpu 互联网 英伟达

借助第一性原理开启中台建设

数字圣杯

数据中台 数字化转型

业务信息化操作系统(BIOS)——中台的核心产出物

孤岛旭日

中台 操作系统 企业信息化

高效阅读,成就自我-《麦肯锡精英高效阅读法》读后感

顾强

读书笔记 读书 读书方式

《硅谷革命:成就苹果公司的疯狂往事》读后感

顾强

初探Electron,从入门到实践

Geek_Willie

前端 Electron SpreadJS

21天养不成习惯,28天也不行。不要痴心妄想。

赵新龙

TGO鲲鹏会 习惯养成

回顾经典,Netflix的推荐系统架构

王喆

人工智能 学习 推荐系统 netflix

你竞争我得利之零售变革

孙苏勇

行业资讯

Dubbo集成Sentinel实现限流

Java收录阁

sentinel

面向页面的移动端架构设计

稻子

flutter ios android 前端架构 架构模式

前浪的经验:区块链软件,一定也要去中心化

Michael Yuan

比特币 区块链 智能合约 以太坊 加密货币

故障的传播方式与隔离办法

Wales Kuo

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

IT Hare—数据库终极“异端邪说”:单一写入数据库连接,第1篇:性能-InfoQ