Jsonb:分享几个关于性能的故事

原文:Jsonb: few more stories about the performance
作者:Dmitry Dolgov
译者:roy

译者注: 作者用轻松诙谐的语气结合详细的步骤和图表,生动地比较了PostgreSQL,MySQL和MongoDB在处理不同文档时候的性能。

因此, 实际上并没有什么 “标准” 基准能告诉你哪种技术对你的应用程序是最佳的。只有你自己的需求、数据和基础架构才能告诉你哪些是你需要了解的。

有一段时间,我无法停止做一些有趣的,或者有用的,甚至怪异的(每次做一个)基准测试, 用来揭示在关系数据库的世界中如何应用面向文档的一些方法细节。最后我决定用我的 博客文章来分享这些大量的细节。所以欢迎来到基准俱乐部, 在这里我们将讨论公平地比较不同数据库性能需要做些什么。你可能猜到了, 基准俱乐部的第一条准则是永远不要共享一个可重现的基准。但我们认定自己是坏蛋工程师, 所以今天我们要打破这个规则。

目标

比较所有的存储和处理文档形式的数据库解决方案是不可能的(虽然人们通常期望这样),所以我将范围限制在PostgreSQL,MySQL和MongoDB:

  • PostgreSQL - 因为它是一个领先的数据库,而且是我生命中的一部分(译者注:作者 Dmitry Dolgov 是PostgreSQL 开源项目贡献者)。

  • MySQL - 对二进制json也有相当不错的实现,所以将PostgreSQL与其最接近的竞争对手进行比较是很有趣的。

  • MongoDB - 最流行的NoSQL数据库之一。对于我来说,它是“面向文档”的同义词。

环境

不幸的是,我没有任何裸机来测试性能。所以我所有的测试都是使用AWS EC2进行的,这样的测试有其特有的优点和缺点:

  • 你可以非常容易地重现相同的结果。
  • 许多公司使用AWS作为主要平台来运行他们的基础设施,所以我的结果可能更加相关。
  • 但是使用EC2进行性能测试有一些潜在影响。

我的典型基准环境有两个EC2实例,一个用于工作负载生成器,另一个用于数据库。我使用的是 m4.large 实例类型和一个gp2 ELB卷存储,使用默认的Ubuntu 16.04 AMI映像,因此可以使用HVM虚拟化。两个实例都在同一个可用区域,VPC和安置组,所以我们消除了重要的网络问题。

但对结果充满信心是不够的。还有一些有趣的和性能相关的EC2的使用 细节(例如,通常建议禁用超线程以获得更好的延迟),并且通常基准测试是相当危险的区域(只有疯狂的人才加入Benchmark Club,因为每个人都认为他比你聪明,而且没人愿意帮忙)。为了至少能部分地缓解这些可能的问题,并确保我的测试更具可重复性,我尽可能地小心 - 几乎每个用例都至少做了四轮相同的基准测试(通常甚至更多)。

我使用了YCSB作为测试工具,这是一个非常有名的测试NoSQL数据库性能的工具。它提供了许多有趣的工作负载类型,但不幸的是只涉及结构相当简单的文档。更确切地说,YCSB使用简单文档,仅有一定数量的关键字和相应的值,这与我的观点并不相近。这就是为什么我也创建了这个工具的一个分支,在那里我介绍了创建具有一些复杂结构的文档的可能性,以及PostgreSQL(jsonb)和MySQL(二进制json)的驱动程序(警告:我应该指出,它不是让我感到自豪的代码,但你懂的 - “它对我很有用”)。

为了创造一个环境,我有一个 ansible手册(对于那些在各种会议上看到我介绍这个主题的人来说,这本手册总是公开的,但因为没有真正的反馈,过了一段时间之后,我就没把它放到幻灯片里),它接受一些参数,如EC2的键值,可用区等,然后创建所需要的实例并配置它们,最后开始数据加载和实际测试。你需要手动做的唯一事情就是为你的数据库创建包含所有安全组的子集(我只是懒得自动化)。不幸的是,Ansible自身在某个时候成了一个问题,因为它仍然使用Python2,而最新版本的Ubuntu并不提供Python2,所以需要一些技巧来使用它。

除了运行数据库本身之外,每个实例还利用sar工具收集系统度量值(以及来自pgviewmongotop的一些与数据库相关的度量值),以便测试后使用。

几点说明。我不确定现在的情况,但是当我编写所​​有这些脚本的时候,在最新版本的MongoDB的Ubuntu的最新版本中没有提供适当的服务。这意味着我必须再添加一个模板并自己创建这个服务。

另一件让我度过数个不眠之夜的好事就是所谓的“无人值守升级”。这个Ubuntu服务有时可能会唤醒并开始更新系统。我不需要这个额外的开销(虽然很小),但有时软件包会因为安装被锁死从而导致测试失败,所以我禁用了它。一般来说,为了防止碎片化的测试,添加Ansible重试来更新软件包部分是完全有意义的,例如:

until: update_result.stderr == ""
retries: 10
delay: 1
ignore_errors: yes

还有一件让你的生活更轻松的事情就是添加

host_key_checking = False # 我们知道我们的主机
timeout = 60  # 或更多
pipelining = True # 减少SSH操作次数

到你主机上的 ansible.cfg 文件中
(理论上 pipelining 可能破坏 sudoers 配置的兼容性,但是在我的测试中,我从来没有经历过类似的事情)。

为了节省一些金钱和时间,在几乎所有的测试里,我使用已经准备好所需要一切的AMI镜像。但是,如果将测试数据预加载到此镜像中,则必须明白,在测试开始时,缓存中显然没有数据。

配置

在我的测试中,我使用了各种数据库版本:

  • PostgreSQL 9.6.3/10
  • MongoDB 3.2.5/3.4.4
  • MySQL 5.7.9/8.0.3

为了简单起见,你可以假设我们正在使用最新的稳定版本,我只会在一个特定的版本有一些性能差异的时候才会提到它。

在处理这些数据库的配置时,我试图只调整那些与实例参数或工作负载性质直接相关的选项。其它需要更详细的配置选项则留给我的下一个研究。这让我有以下重要的选项:

  • PostgreSQL

    • shared_buffers
    • effective_cache_size
    • max_wal_size
    • checkpoint_completion_target
  • MySQL

    • innodb_buffer_pool_size
    • innodb_log_file_size
  • MongoDB

    • write concern level
    • checkpoints
    • eviction
    • transaction_sync(只是出于好奇,并不真的推荐使用)

几个重要的注意事项:

  • 所有的测试都使用正确的数据一致性(这意味着MongoDB要使用 write concern 级别的journal)
  • SSL被禁用(这对PostgreSQL和MongoDB很容易,而对MySQL驱动程序,我不得不手动禁用它)
  • 在PostgreSQL和MySQL使用了Prepared Statement
  • 所有数据库都以单一实例的形式使用。对于MongoDB来说,这实际上是非常不自然的,通常你想使用复制来获得最终的一致性。但同时这也允许我们在相似的条件下测试所有涉及到的数据库。现在我正在开发测试套件的第二部分来测试数据库集群,所以你可以将本文作为第一步来考虑,尽管如此,它本身仍然非常有趣。

另外对于所涉及的文档类型,使用默认的YCSB,我可以定义:

  • “简单”或“小”文档 - 10个键和值,每个值是100个随机字符
  • “大”文档 - 100个键和值,每个值是200个随机字符

而使用我的分支,我也可以定义:

  • “复杂”文档 - 100个键和值,组成3个嵌套级别的树,每个值为100个随机字符

读负载

YCSB提供了几种有趣的工作负载类型来模拟真实世界的情况。我们从简单的 WorkloadC 开始,它由100%的读取查询组成。每个读取查询通过其ID来获取单个文档,所以我们需要讨论如何索引文档:

  • PostgreSQL - 我们可以在文档中索引单个路径/多个路径(使用常规功能索引)或所有路径(使用GIN索引)。

  • MongoDB - 我们可以在文档中索引路径或多个路径。

  • MySQL - 没有直接支持索引二进制JSON,但我们可以创建一个虚拟列,并像往常一样索引它。所以文档内的单个或多个路径是可以支持的。

让我们尝试使用“默认”索引方法进行简单的性能测试,这意味着我们将为PostgreSQL索引整个文档,在MongoDB中只索引ID,而在MySQL中只有一个独立的虚拟列索引ID。

此图表示所有数据库的 WorkloadC 下的吞吐量。x轴表示正在查询我们的数据库的客户数目, 所以基本上它是一个并发级别。y轴表示吞吐量值。

在此图上,你可以看到相同测试的第99百分位(99%)的延迟。

我敢打赌,你已经对这个数据有很多疑问。既然我们努力想得到一个公平的比较,我们需要讨论和解释它们:

  • 为什么所有数据库在大约有20个客户的时候会出现峰值?
  • 为什么当客户数量增长时,MongoDB性能下降?
  • 为什么MySQL和MongoDB之间存在性能差距?
  • 为什么PostgreSQL和MongoDB之间存在性能差距?

20客户时的峰值

这可能是最简单的一个,它涉及到一个实例配置。下一个图表显示了PostgreSQL在20个客户时候的测试中的CPU消耗。

正如你所看到的,CPU资源几乎被完全消耗掉,而从20个客户之前的相同度量来看,我们拥有同样的容量。所以这就是这个峰值的解释。

MangoDB 的性能下降

要了解这里发生的事情是非常棘手的,因为我从 sar 里获得的所有指标对于20个和100个客户来说都处于同一水平。所以,作为我调查的一部分,我开始用 perf 来跟踪MongoDB。事实证明,只有两个显著增长的指标: CPU迁移事件的数量和 sched_yield 系统调用的数量。

这让我得出的结论是,MongoDB在螺旋锁中出现了一些问题,因为包含 sched_yield 系统调用的唯一模块是 spin_lock.cpp 。有趣的是,这种情况在版本更新后变得越来越好(但它仍然相当显著),例如这里比较3.2和3.4的吞吐量:

MySQL 对比 MongoDB

实际上,MySQL在第一次测试中的表现更糟。事实证明,由于在MySQL中我们使用的是虚拟列,所以我们必须明确地告诉它应该使用 STORED 语句来存储:

CREATE TABLE usertable (
data JSON,
ycsb_key VARCHAR(255)
    GENERATED ALWAYS AS (JSON_EXTRACT(data, '$.YCSB_KEY'))
    STORED PRIMARY KEY
);

但是即使有了这个改进,这个测试的吞吐量也是很低的。来自MySQL方面的同事声称,这主要是因为 information_schema 比其他数据库收集更多的数据并涉及更多类似的操作,这显然导致更大的开销。但是同时在生产环境中没有人真正地禁用它,所以到目前为止,我还是决定为我的性能测试做同样的事情。

PostgreSQL 对比 MangoDB

我认为这是现在对你最重要的问题。为什么PostgreSQL表现如此不佳呢?答案很简单,我们正在做一个不公平的基准。你可能会注意到,我们仅在MySQL和MongoDB的文档中索引了一个ID,而对PostgreSQL索引了一个完整的文档。所以,我们试着解决这个问题。

在这里,可以看到与我们正在讨论的变化完全相同的测试,即为PostgreSQL仅索引一个ID。现在很明显,PostgreSQL和MongoDB的吞吐量在MongoDB遇到螺旋锁导致性能下降之前是几乎相同的。 PostgreSQL和MySQL之间还有差距,这主要是有两个因素:

  • 如前所述的 information_schema
  • prepared statements,因为在MySQL中它们比在PostgreSQL中缓存的信息少,因此它们不是那么有效

现在我们来讨论一下结果。到目前为止,PostgreSQL和MongoDB的吞吐量与读取工作负载相同。这令人惊讶吗?一点都不 - 我要告诉你,所有的数据库都或多或少地使用相同的数据结构和相同的方法来存储文档。考虑到这一点,以及相同类型的索引和相同的环境,很自然地我们得到的数字是相同的。

数据结构

当我们谈论如何在数据库中存储文档时,我们通常有两个关注点 - 我们的文档应占用尽可能小的空间,但同时我们应该能够有效地使用它。要做到这些我们唯一能做的事情就是把一些额外的信息放到文档中,例如对于其中的每个键,我们可以存储相应值的类型和值。我们所有的目标数据库都采用这种或那种类似的形式。

下面是一个表示PostgreSQL中 jsonb 内部结构的图表:

在这里我们可以看到的是:

  • 整体文件大小
  • 一棵树,每个节点都有一个标题头和实际的内容

让我们放大一下节点结构:

基本上,节点头包含 值类型值大小偏移量 和一个神秘的标识来决定我们是否存储 大小偏移量 。这样做是为了找到平衡点 - 使用 偏移量,我们可以通过压缩成本获得更多的访问速度,使用 尺寸 可以获得更好的可压缩性。

下面是MongoDB中的 bson 类似图:

毫不奇怪,我们看到几乎相同的图片,包含文档大小,树形结构的节点,每个节点都有自己的头部和实际的内容。再来看一下节点头结构:

和以前一样(没有那么多的选择),我们在这里有 值类型值的大小键名 ,这是一个小而有趣的区别。问题是,对于 jsonb 我们将所有的键和值存储在一个可变大小的内容部分,并隐式地为它们应用一些顺序。在 bson 里,我们直接在节点头中存储一个键。

现在让我们来探索MySQL中二进制json的内部结构:

正如你猜测的那样,树形结构的节点,但是这次没有明确的头部 - 所有的信息都存储在一个可变大小的内容块中。让我们深入一下:

在这里,与大小,键和值一起,我们发现一个有趣的区别 - 指针,这是从文档的开始处的偏移量,你可以找到这个特定的键或值。据我了解的MySQL源代码,这样做是为了能够以一种懒惰的方式处理文档。

有关文档如何存储的另一个有趣的事情是键/值顺序。

对于 jsonb 和二进制json,我们实际上将同一级别的键和值组合在一起。原因很简单,通过这种方式我们可以更快地访问键,很容易缓存它们等等。

我们总结一下。所有的数据库都或多或少地使用相同的方法来存储文件,只有很小的差异。为了好玩,这里有一些简单文档的二进制级别差异的例子:

{"a": 3"b": "xyz"}

以下是PostgreSQL中 jsonb 文档的样子:

我们可以清楚地看到其结构的一些部分,并将键和值组合在一起。

bson 也一样:

正如你所看到的,这里的情况是相反的,所有的键和值都是按照自然的顺序排列的,包括 值类型值的大小 等等。

最后是二进制json:

又一次看到相当长的标题头中有一些指针,然后是分组的键和值。

另一个有趣的事情是,在 jsonb 中,可变大小的内容和头按4个字节对齐。很简单,但会导致有趣的后果,例如这里有两个文档:

{"a": "aa""b": 1}
{"a": 1"b": "aa"}

这两个文档的大小相同, 因为我们只是交换了值,仅此而已。但是,如果你尝试将它们存储在 PostgreSQL 中, 你将看到第二个将增加2个字节的磁盘空间:

INSERT INTO test VALUES('{"a": "aa", "b": 1}')

INSERT INTO test VALUES('{"a": 1, "b": "aa"}')

在第二个文档中,我们可以清楚地看到在键 a 和键 b 前后有4个额外的字节。因为第一个有2字节长的头,最终我们看到这个2字节的差异。当然, 这不是那么多,但在某些情况下,也许值得一提。

复杂文档

我们之前所做的测试涉及相当简单的文档,没有任何内部结构。正如我之前说过的,在现实生活中我们要使用更复杂的东西,所以让我们试着用相同的环境来测试相同的读工作负载, 但是现在让我们使用带有一些嵌套信息的文档 (“复杂” 文档):

我们在这里看到,数字是不同的,但这只是因为我们在处理更复杂和更大的文档, 但是所有的模式都是一样的。

文档大小

另一个有趣的问题是处理较大的和较小的 jsonb 文档有什么不同。让我们复制与以前相同的测试,但现在我们将固定客户的数量为 40,而在x轴上变化文档的大小。

我们可以看到在大约2kB的地方,性能有趣的下降,在图中其他地方则几乎线性退化。你能猜到原因吗?

当我们将文档存储在 PostgreSQL 中时,首先将其压缩。即使在压缩后文件依然太大(是的, 你猜对了,超过2kB),PostgreSQL 将其分裂成多个区块,并储存在toast表中。

解析时间

当你使用 jsonb时候,可以尝试一个有趣的实验。使用这样的查询是完全可以的:

SELECT * FROM test_table WHERE data @> '{"key": "123"}'::jsonb;

但正如你可能看到的那样,为这个条件构建一个 jsonb 对象 PostgreSQL 需要解析一个字符串,其中包含 jsonb 定义。这是绝对没有必要的, 因为你有很多其他的选择来创建一个 jsonb 对象, 例如函数 jsonb_build_object:

SELECT * FROM test_table WHERE data @> jsonb_build_object('key''123');

这不是一个顶级优化,我没有期望它做任何事情,但事实证明,对很多查询, 你可以使用它来提升吞吐率。例如,让我们看一下对带有 GIN 索引的只读工作负载的第一个测试, 但现在我们要使用 jsonb_build_object:

你可以看到,与原始图相比,PostgreSQL每秒多了数千个额外的操作。一点说明-这是一个有点老的测试,所以它对比的是MongoDB 3.2.5。

可扩展性

到目前为止,我们正在使用相当普通的硬件进行测试,这当然是有价值的。但我不得不提一下,当使用性能更强大的硬件时测试情况可能会有某种程度上不同。例如,让我们在不同的机器( m4.largem4.xlargem4.2xlarge)上比较对整个文档使用 GIN 索引的PostgreSQL只读测试:

第一个图形与我们之前看到的是一模一样的, 但是在下面的两个图中有一个有趣的模式。你可以看到,在几乎线性地获得更多吞吐量的同时, PostgreSQL 和 MongoDB 之间的索引相关的差别在减少。我们将在以后讨论其他类型的工作负载会有怎样的不同。

插入负载

既然 YCSB 提供了这么多有趣的工作负载,让我们来探索一下。下一个是只插入的工作负载。最开始我在这里犯了一个错误(我必须说我犯错是很专业的,其中一些错误几乎就像是一门艺术), 我试图使用和之前测试几乎相同的设置,即对所有数据库的文档建一个 ID 索引。问题是, 我还必须正确配置检查点,因为所有数据库的默认配置都不适合这类工作负载。让我们来看看:

你可以看到数据具有振荡模式。例如,如果我们希望看到一些度量值,我们可以看到这个测试是受 IO 限制的:

在最后一个图中,我们可以看到 IO 操作队列大小的时序变化。这些峰值与检查点相关, 数据库将数据刷新到磁盘的执行频率太高。因此,考虑到这一点,让我们调整检查点配置 (我在这里谈论的是 max_wal_sizecheckpoint_completion_targetinnodb_log_file_sizecheckpoint + eviction。另外,请注意,MySQL 在默认情况下有两个 journal 文件, 而 innodb_log_file_size 每一个都要配置的):

这一次的数据更具代表性,我们可以看到有趣的东西,PostgreSQL 在吞吐量方面更具竞争力。MySQL 和 MongoDB 是几乎相同的水平,但有一点 - 实际上, 为了得到更好的 MySQL 性能,当一个文档 id 对应的虚拟列没有 * PRIMARY KEY* 的时候, 我使用了”错误”的设置。在这种情况下,我们可以得到更好的插入工作负载的数字 (但其他一切都是很可怕的),没有它的话MySQL将会有更少的吞吐量。据我所知,MySQL 处理插入工作负载不是那么好,这是一个众所周知的情况,当然有很多MySQL分支用来解决这个问题。

更新负载

在谈论更新工作负载之前,让我们讨论另一个相关的主题 - 在更新 jsonb 时, 我们会遇到什么样的问题? 尽管我们认为它是一个具有某种结构的文档,但对于 PostgreSQL 来说,它仍然是一个常规的数据类型。这意味着有时我们只触及文档的很小一部分, 但数据库需要遍历整个文档。以下是我所说的几个例子:

  • 更新文档的一个字段
  • DETOAST 文档
  • 重新索引文档

重新索引

这里的故事很简单。到目前为止,在 PostgreSQL 里,如果你为 jsonb 列创建了一个函数索引, 然后更新了文档的某些部分(根本不涉及索引),而数据库仍将重新索引此文档。为了检测这是否重要,让我们在两个设置里做一个测试 - 一个是我们有一个文档的id 当索引,另一个是我们对一个单独的列做索引,该列包含相同类型的 id,但与文档分开。我使用 WorkloadA 做测试, 它包含50% 的更新和50% 的读取, 其他一切都类似于以前的测试。

从这个图中我们可以看出,吞吐量有差异。它可能没有那么显著,但请记住, 这严重依赖于文档类型,我预计对于大型和更复杂的文档(索引可能带来更多的开销),这种差异会更大。我很高兴地指出,这方面的工作正在进行中。

DETOAST

让我们比较以下两个查询:

SELECT data->'key1'->'key2' FROM table;
SELECT data->'key1'data->'key2' FROM table;

乍一看,他们之间没有明显的差别,但这种印象是错误的。在第一种情况下,data 只 detoasted 一次,一切都很好。不幸的是,在第二种情况下,data 的两个键都 detoasted 两次。这不仅对 jsonb,而且对 PostgreSQL 中的所有复合数据类型都是如此。总的来说,这不是一个大问题,但是如果我们的文档很大,而且我们要提取太多的键,那么就会有巨大的开销。

为了证明这个说法,让我们做一个实验,重复之前的只读测试(PostgreSQL 使用 ID 索引)。但这次我们只获取一个大文档的 10%:

我们可以清楚地看到,在这种情况下,PostgreSQL 和 MySQL 的开销是巨大的。 对此问题有两种可能的解决方案:

  • 当使用 ALTER TABLE table_name ALTER COLUMN column SET STORAGE EXTERNAL 时候,禁用 jsonb 列压缩。在这种情况下,你可以或多或少地避免 detoasting 开销的磁盘空间成本。
  • 更改查询

我想详细阐述一下第二个方案。由于我们在一个关系数据库中工作,我们可以将我们的文档转换成一个像记录一样的 SQL 结构,然后做任何我们想做的事情:

CREATE TYPE test AS ("a" TEXT, "b" TEXT);
INSERT INTO test_jsonb VALUES('{"a": 1, "b": 2, "c": 3}');
SELECT q.* FROM test_jsonb, jsonb_populate_record(NULL::test, data) AS q;

实际上这是在 PostgreSQL 中使用文档的很常见的模式,因为没有对 Json 路径的全面支持。当然还有一些 扩展包,提供了类似的 Json 的路径,甚至还有 SQL/JSON 这样的巨大扩展包。但是,如果Json 路径是真正必要的,至少在目前我们可以使用这种 “打开” 模式。

让我们设想一下,我们有一个文档的集合,我们想从每一个文档中提取所有 value = “aaa” 的项目:

[{
    "items": [
        {"id": 1"value": "aaa"},
        {"id": 2"value": "bbb"}
    ]
}, {
    "items": [
        {"id": 3"value": "aaa"},
        {"id": 4"value": "bbb"}
    ]
}]

为此,我们可以”打开”文档使用函数 jsonb_array_elements 做这样的事情:

WITH items AS (
    SELECT jsonb_array_elements(data->'items')
    AS item FROM test
)
SELECT * FROM items
WHERE item->>'value' = 'aaa';

类似的,如果我们希望得到所有项目并包含 status = true:

{
    "items": {
        "item1": {"status": true},
        "item2": {"status": true},
        "item3": {"status": false}
    }
}

我们可以使用 jsonb_each 函数打开我们的文档:

WITH items AS (
    SELECT jsonb_each(data->'items')
    AS item FROM test
)
SELECT (item).key FROM items
WHERE (item).value->>'status' = 'true';

当然,这种方法可能会导致相当冗长的查询(试想一下,如果你想以这种方式更新文档中的内容, 你必须”拆开”并再次”包装”回来),这就是为什么我们有 SQL/JSON 这样的补丁。但是不要把这看成是丑陋的技巧,因为实际上这是非常重要的事情 - 当面向文档方式遇到关系型数据库的时候,你可以使用 SQL 的全部功能并应用到你的文档。

更新

现在,让我们最后来谈谈更新工作负载。这里的问题是,当我们更新 jsonb 文档的一个字段时, PostgreSQL 将在journal 中写入整个文档,你可以想象它的开销。问题是这一开销会有多大?让我们使用 WorkloadA 并使用默认检查点配置和小尺寸的文档来做个测试 :

有趣的是,我们有几乎稳定的数据,前面提到的开销是微不足道的。现在让我们在正确调整后的检查点上再测一次:

这里我们可以看到另一个有趣的事情 - 在检查点调整后,PostgreSQL 和 MySQL 在吞吐量方面处于相同的水平,然而不幸的是,MongoDB 的配置并没有多大帮助。起初, 我认为我配置错了检查点,并且 MongoDB 仍然在频繁地执行它们(这是对的,因为我使用 log_size 选项来调整 journal 大小,但事实证明,WiredTiger 有最大值2GB的限制)。但是, 即使我打开 wait 选项,db. serverStatus() 显示 transaction checkpoints 是 1,吞吐量仍然处于同一级别。

在一天结束的时候,我还在调查这个性能差距,但是预期的结果是 MongoDB 的吞吐量低于 PostgreSQL 和 MySQL,只是没有那么多。这一次我们可以说,我前面提到的开销是微不足道的。现在,让我们使用相同的配置进行相同的测试,但是针对大型文档:

最后,开销很大。为了证明这一点,让我们看看如果我们想更新这个小文档会发生什么:

{
    "aaa": "aaa""bbb": "bbb""ccc": "ccc"
}

我们要更改第一个字段:

{
    "aaa": "ddd""bbb": "bbb""ccc": "ccc"
}

下面是我们将在不同的数据库journal中看到的:

PostgreSQL

MySQL

MongoDB

我们可以清楚地看到,MongoDB 只写入了新旧文件的区别。

但有一件有趣的事情 - 实际上,当我们更新一个文档时,如果它的新旧版本都在同一页上, PostgreSQL 将只写我们想要的差异。问题是,文件往往是相当大,因此, 在同一页上的可能性会很低(即使在我笔记本电脑上面的一个小文件,它也不是所有时间都在同一页上)。明显的改善方法是 增大页的容量。快速提醒一下,你可以使用此指令此指令 来预估分配多少页是有意义的:

sysctl -w vm.nr_hugepages=1000

然后在 PostgreSQL 中启用大容量的页:

huge_pages = on # 默认是 try

不幸的是,对此我还没有任何基准测试,但从手工实验我可以看到,针对大容量页(2048kB) 和简单文档类型,大多数时候更新保存是有差异的。

另外值得一提的是,这个问题在最近的 MySQL 开发版本(即8.0.3)中也得到了解决,此版本有一个称为”JSON 值的部分更新”的 功能,但我还没有测试它。

在我们完成这一节之前,我不得不提一下,所有的测试都用的是不算强大的 EC2 实例, 实际结果会依不同设置(如更多的CPU)而不同。我正在研究这个问题, 同时你可以查看我同事使用更强大硬件的结果()。它们实际上涵盖了 YCSB 可以提供的所有工作负载,但使用了一些不同的设置,主要是 synchronous_commit = off

Jsonb 对比 Json 对比 Table 表

到目前为止,我们在一直比较三种不同的数据库,但问题是,即使在PostgreSQL里也有 jsonb 的替代品。如果你必须存储一些文档,也许把它们作为一个纯 json 来存储是有道理的,即便只是从性能的角度?或者,基于相同的原因, 甚至可以将文档转化到适当的 sql 关系中。如果你不太在意 jsonb 提供的灵活性, 你可以考虑这些选项。让我们看看从 jsonbjson 和普通表之间的相同测试中得到什么结果:

  • 关系 对比 Jsonb,只读工作负载

嗯,没那么大的差别。

  • 关系 对比 Jsonb,只插入工作负载

有一些性能差距,但没有那么大。基本上,我们可以看到,因为关系是PostgreSQL里的一等 公民,它们的内部结构相对于jsonb更容易保持。同时,在性能方面,提取一个关系型记录和 jsonb文档中的一行是几乎是相同的。

  • 纯 Json 对比 Jsonb,只读工作负载

我们又一次看到一些性能差距,纯 json 快一点。解释很简单 - 我们在 jsonb 中有一些内部结构在维护结构和磁盘空间方面引入了一些开销。但是正如我们从上面的图中看到的那样,开销并不大。

  • 纯 Json 对比 Jsonb,只插入工作负载

由于上面提到的相同原因,json 快一点,但也不是很显著。

你可能会猜到,我没有谈论更新工作负载,因为它没有意义- jsonb 将完胜 json,但是与 sql 关系相比,它会比较慢。在这一点上,你可以认为 jsonb 介于纯 json 和 sql 关系之间, 它具有一些内部结构,你可以高效地使用它,同时它还提供了一定程度的灵活性。

结论

主要的结论是,这是一个非常有趣的研究课题,我准备继续这样做。我真的希望能得到一些积极的反馈并提升我的测试。

另一个有趣的结论是,我上面展示的所有性能差异基本上都是由数据库本身造成的,而不是它们处理文档的方式。这意味着这完全不是一个灰色的区域,而是我们熟知的东西。在大多数情况下,你可以在关系数据库中使用文档,如果有必要的话,你甚至可以从 NoSQL 的解决方案中迁移过来 ,而且无需担心性能会显著下降。

致谢

非常感谢 Alexey Kopytov,他帮了我很多 MySQL 的设置,感谢Wei Shan Ang,他审查了这篇文章并分享了他配置 MongoDB 的经验,感谢 Oleg Bartunov 提出了最初的设想。

1月13日,SDCC 2017之数据库线上峰会即将强势来袭,秉承干货实料(案例)的内容原则,邀请了来自阿里巴巴腾讯微博网易等多家企业的数据库专家及高校研究学者,围绕Oracle、MySQL、PostgreSQL、Redis等热点数据库技术展开,从核心技术的深挖到高可用实践的剖析,打造精华压缩式分享,举一反三,思辨互搏,报名及更多详情可点击此处查看。
这里写图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值