MongoDB 插入第二个索引的性能
MongoDB insert performance with 2nd index
我正在尝试使用 WiredTiger 将大约 2.5 亿个文档插入 MongoDB 3.0,每个文档大约 400 个字节。我只需要搜索一个短字符串键 _user_lower
。虽然我现在用的是 WiredTiger,比 MMAPv1 好很多,但我确实先用了 MMAPv1,也有类似的问题。
我的服务器(非常便宜VPS)有:
- 250 GB 磁盘
- 1 GB 内存
- 2 GB 交换
- 2.1 GHz 单核 CPU
我知道这台机器真的很慢,我要求它做一些有点不切实际的事情。但我很困惑它是如何用一个索引启动得如此之快,而第二个索引却毁了性能:
我插入了我当时拥有的所有数据(大约 250M 行)除了 _id
之外没有任何索引。考虑到我糟糕的硬件,这表现非常好:
- 大约 每秒 5000 次插入(完全可以接受)
- 在完成所需的 14 小时内,此速率几乎保持不变
- 完成后
_id
上的索引大小接近 2.5GB。请注意,这是我物理 RAM 的两倍多。
- 根据 mongostat,进程的 RES 没有超过 450 MB。
- 无交换
top
似乎表明 CPU 时间并没有全部花在等待磁盘上(因此在用户空间中花费了大量时间,大概是 snappy
中的 WiredTiger代码)
然后我在我需要查询的唯一字段 _user_lower
上建立了一个(非唯一)索引。这花了 7.7 个小时,这很好,因为这是一次性交易。索引最终为 1.6 GB,与 _id
索引相比,这对我来说似乎真的很低。 RES 上升到大约 750 MB。
然后,我下载了一个新的数据集来加载。它只有 102 MB(238 K 文档)。我以相同的方式加载它,使用 mongoimport
,但这次:
- 每秒仅 80 次插入(有时较慢)
- RES 保持在 750 MB 左右
top
表示几乎 100% 的 CPU 都花在等待 IO
- 当然,负载已经过顶了。
我可以理解相当大的性能损失,因为必须更新该索引。但是没想到这么多。我已经阅读了所有关于我的索引应该适合 RAM 的地方,但是在初始插入期间性能非常好,索引很快就超出了我的记忆。
我完全可以优化 _user_index
索引吗?我什至不知道这意味着什么,但也许只索引前几个字符?我绝对愿意将查询性能减半以换取三倍的插入性能。
是什么导致了巨大的性能下降? 在没有新硬件的情况下如何修复它?我并不是很喜欢 MongoDB,所以没有这些性能特征的替代品也可以。我有一个想法,只使用平面文件,这可能会起作用,但我不想编写所有代码。
向集合中添加新项目时,数据库必须使索引保持最新。由于默认情况下 MongoDB 中的索引是 B 树,这意味着它必须在树中插入一个项目。虽然在最好的情况下这不是一个特别昂贵的操作,但它有两个潜在的性能问题:
- 性能抖动:有时,B-Tree 桶可能已满,需要拆分桶,因此比 'simple' 插入
需要更多操作
- 插入目标必须随时可用
在这种情况下,后者可能会引起麻烦:因为名称的插入会命中树中的随机节点(即名称插入不遵循模式)并且您的 RAM 小于索引,很有可能必须从磁盘中获取目标。不幸的是,磁盘寻道的性能是 orders of magnitude lower than main memory references。如果你不走运,第一个 ref 位置需要另一个磁盘搜索,这样在 MongoDB 甚至 开始 写入之前,对于单个插入需要多次磁盘读取。这可能需要数百毫秒,旋转磁盘或典型 IaaS 基础设施上的一些争用甚至几秒钟。
因为 ObjectIds 是单调生成的(时间戳是最重要的部分),插入总是发生在最后,并且可以将目标大部分保留在 RAM 中。性能抖动,即问题 1 可能仍然是一个问题,因为存储桶拆分可能需要磁盘寻道,但与第一种情况相比,它很少发生,不会破坏平均性能,这应该可以解释观察到的行为。
另外,当桶被单调递增的值填满时,MongoDB会在桶满90%时拆分桶;通过随机插入,分裂会发生得更早,达到 50%,所以在这种情况下树会多一点 'dense'。
我正在尝试使用 WiredTiger 将大约 2.5 亿个文档插入 MongoDB 3.0,每个文档大约 400 个字节。我只需要搜索一个短字符串键 _user_lower
。虽然我现在用的是 WiredTiger,比 MMAPv1 好很多,但我确实先用了 MMAPv1,也有类似的问题。
我的服务器(非常便宜VPS)有:
- 250 GB 磁盘
- 1 GB 内存
- 2 GB 交换
- 2.1 GHz 单核 CPU
我知道这台机器真的很慢,我要求它做一些有点不切实际的事情。但我很困惑它是如何用一个索引启动得如此之快,而第二个索引却毁了性能:
我插入了我当时拥有的所有数据(大约 250M 行)除了 _id
之外没有任何索引。考虑到我糟糕的硬件,这表现非常好:
- 大约 每秒 5000 次插入(完全可以接受)
- 在完成所需的 14 小时内,此速率几乎保持不变
- 完成后
_id
上的索引大小接近 2.5GB。请注意,这是我物理 RAM 的两倍多。 - 根据 mongostat,进程的 RES 没有超过 450 MB。
- 无交换
top
似乎表明 CPU 时间并没有全部花在等待磁盘上(因此在用户空间中花费了大量时间,大概是snappy
中的 WiredTiger代码)
然后我在我需要查询的唯一字段 _user_lower
上建立了一个(非唯一)索引。这花了 7.7 个小时,这很好,因为这是一次性交易。索引最终为 1.6 GB,与 _id
索引相比,这对我来说似乎真的很低。 RES 上升到大约 750 MB。
然后,我下载了一个新的数据集来加载。它只有 102 MB(238 K 文档)。我以相同的方式加载它,使用 mongoimport
,但这次:
- 每秒仅 80 次插入(有时较慢)
- RES 保持在 750 MB 左右
top
表示几乎 100% 的 CPU 都花在等待 IO- 当然,负载已经过顶了。
我可以理解相当大的性能损失,因为必须更新该索引。但是没想到这么多。我已经阅读了所有关于我的索引应该适合 RAM 的地方,但是在初始插入期间性能非常好,索引很快就超出了我的记忆。
我完全可以优化 _user_index
索引吗?我什至不知道这意味着什么,但也许只索引前几个字符?我绝对愿意将查询性能减半以换取三倍的插入性能。
是什么导致了巨大的性能下降? 在没有新硬件的情况下如何修复它?我并不是很喜欢 MongoDB,所以没有这些性能特征的替代品也可以。我有一个想法,只使用平面文件,这可能会起作用,但我不想编写所有代码。
向集合中添加新项目时,数据库必须使索引保持最新。由于默认情况下 MongoDB 中的索引是 B 树,这意味着它必须在树中插入一个项目。虽然在最好的情况下这不是一个特别昂贵的操作,但它有两个潜在的性能问题:
- 性能抖动:有时,B-Tree 桶可能已满,需要拆分桶,因此比 'simple' 插入 需要更多操作
- 插入目标必须随时可用
在这种情况下,后者可能会引起麻烦:因为名称的插入会命中树中的随机节点(即名称插入不遵循模式)并且您的 RAM 小于索引,很有可能必须从磁盘中获取目标。不幸的是,磁盘寻道的性能是 orders of magnitude lower than main memory references。如果你不走运,第一个 ref 位置需要另一个磁盘搜索,这样在 MongoDB 甚至 开始 写入之前,对于单个插入需要多次磁盘读取。这可能需要数百毫秒,旋转磁盘或典型 IaaS 基础设施上的一些争用甚至几秒钟。
因为 ObjectIds 是单调生成的(时间戳是最重要的部分),插入总是发生在最后,并且可以将目标大部分保留在 RAM 中。性能抖动,即问题 1 可能仍然是一个问题,因为存储桶拆分可能需要磁盘寻道,但与第一种情况相比,它很少发生,不会破坏平均性能,这应该可以解释观察到的行为。
另外,当桶被单调递增的值填满时,MongoDB会在桶满90%时拆分桶;通过随机插入,分裂会发生得更早,达到 50%,所以在这种情况下树会多一点 'dense'。