乐趣区

关于mongodb:MongoDB-splitChunk引发路由表刷新导致响应慢

MongoDB splitChunk 引发路由表刷新导致响应慢

MongoDB sharding 实例从 3.4 版本 降级到 4.0 版本 当前插入性能明显降低,察看日志发现大量的 insert 申请慢日志:

2020-08-19T16:40:46.563+0800 I COMMAND [conn1528] command sdb.sColl command: insert {insert: "sColl", xxx} ... locks: {Global: { acquireCount: { r: 6, w: 2} }, Database: {acquireCount: { r: 2, w: 2} }, Collection: {acquireCount: { r: 2, w: 2}, acquireWaitCount: {r: 1}, timeAcquiringMicros: {r: 2709634} } } protocol:op_msg 2756ms

日志中能够看到 insert 申请执行获取 collection 上的 IS 锁 2 次,其中一次产生期待,等待时间为 2.7s,这与 insert 申请执行工夫保持一致。阐明性能升高与 锁期待 有显著的相关性。

追溯日志发现 2.7s 前,零碎正在进行 collection 元数据刷新(2.7s 的时长与 collection 自身 chunk 较多相干):

2020-08-19T16:40:43.853+0800 I SH_REFR [ConfigServerCatalogCacheLoader-20] Refresh for collection sdb.sColl from version 25550573|83||5f59e113f7f9b49e704c227f to version 25550574|264||5f59e113f7f9b49e704c227f took 8676ms
2020-08-19T16:40:43.853+0800 I SHARDING [conn1527] Updating collection metadata for collection sdb.sColl from collection version: 25550573|83||5f59e113f7f9b49e704c227f, shard version: 25550573|72||5f59e113f7f9b49e704c227f to collection version: 25550574|264||5f59e113f7f9b49e704c227f, shard version: 25550574|248||5f59e113f7f9b49e704c227f

chunk 版本信息

首先,咱们来了解下上文中的版本信息。在上文日志中看到,shard version 和 collection version 模式均为「25550573|83||5f59e113f7f9b49e704c227f」,这即是一个 chunk version,通过 “|” 和 “||” 将版本信息分为三段:

  • 第一段为 major version : 整数,用于 辨识路由指向是否发生变化,以便各节点及时更新路由。比方在产生 chunk 在 shard 之间迁徙时会减少
  • 第二段为 minor version : 整数,次要用于记录 不影响路由指向的一些变动。比方 chunk 产生 split 时减少
  • 第三段为 epoch : objectID,标识汇合的惟一实例,用于 辨识汇合是否产生了变动。只有当 collection 被 drop 或者 collection 的 shardKey 产生 refined 时 会从新生成

shard version 为 sharded collection 在指标 shard 上最高的 chunk version

collection version 为 sharded collection 在所有 shard 上最高的 chunk version

下文“路由更新触发场景”–“场景一:申请触发”中就形容了应用 shard version 来触发路由更新的典型利用场景。

路由信息存储

sharded collection 的路由信息被记录在 configServer 的 config.chunks 汇合中,而 mongos&shardServer 则按需从 configServer 中加载到本地缓存 (CatalogCache) 中。

// config.chunks
{
        "_id" : "sdb.sColl-name_106.0",
        "lastmod" : Timestamp(4, 2),
        "lastmodEpoch" : ObjectId("5f3ce659e6957ccdd6a56364"),
        "ns" : "sdb.sColl",
        "min" : {"name" : 106},
        "max" : {"name" : 107},
        "shard" : "mongod8320",
        "history" : [
                {"validAfter" : Timestamp(1598001590, 84),
                        "shard" : "mongod8320"
                }
        ]
}

下面示例中记录的 document 示意该 chunk:

  • 所属的 namespace 为 “sdb.sColl”,其 epoch 为 “5f3ce659e6957ccdd6a56364”
  • chunk 区间为 {“name”: 106} ~ {“name”: 107},chunk 版本为 {major=4, minor=2},在 mongod8320 的 shard 上
  • 同时记录了一些历史信息

路由更新触发场景

路由更新采纳 “lazy” 的机制,非必须的场景下不会进行路由更新。次要有 2 种场景会进行路由刷新:

场景一:申请触发

mongos 收到客户端申请后,依据以后 CatalogCache 缓存中的路由信息,为客户端申请减少一个 「shardVersion」 的元信息。而后依照路由信息将申请散发到指标 shard 上。

{ 
  insert: "sCollName", 
  documents: [{ _id: ObjectId('5f685824c800cd1689ca3be8'), name: xxxx } ], 
  shardVersion: [Timestamp(5, 1), ObjectId('5f3ce659e6957ccdd6a56364') ], 
  $db: "sdb"
}

shardServer 收到 mongos 发来的申请后,提取其中的 「shardVersion」 字段,并与本地存储的 「shardVersion」进行比拟。比拟二者 epoch & majorVersion 是否雷同,雷同则认为能够进行写入。如果版本不匹配,则抛出一个 StaleConfigInfo 异样。对于该异样,shardServer&mongos 均会进行解决,逻辑基本一致:如果本地路由信息是低版本的,则进行路由刷新。

场景二:非凡申请

  • 一些命令执行肯定会触发路由信息变动,比方 moveChunk
  • 受其余节点行为影响,收到 forceRoutingTableRefresh 命令,强制刷新
  • 一些行为必须要获取最新的路由信息,比方 cleanupOrphaned

路由刷新行为

具体的刷新行为分为两步:

第一步:从 config 节点拉取权威的路由信息,并进行 CatalogCache 路由信息刷新。理论最终是通过 ConfigServerCatalogCacheLoader 线程来进行的,结构一个

{
    "ns": namespace,
  "lastmod": {$gte: sinceVersion}
}

申请来获取路由信息。其中如果 collection 的 epoch 发生变化或者本地没有 collection 的路由信息,那么只需增量获取路由信息,sinceVersion = 本地路由信息中最大的版本号,即 shard version;否则 sinceVersion = (0,0),全量获取路由信息。

ConfigServerCatalogCacheLoader 取得到路由信息当前,会刷新 CatalogCache 中的路由信息,此时系统日志会打印上文中看到的:

2020-08-19T16:40:43.853+0800 I SH_REFR [ConfigServerCatalogCacheLoader-20] Refresh for collection sdb.sColl from version 25550573|83||5f59e113f7f9b49e704c227f to version 25550574|264||5f59e113f7f9b49e704c227f took 8676ms

第二步:更新 MetadataManager(用于保护汇合的元信息,并提供对局部场景获取一个一致性的路由信息等性能) 中的 路由信息。更新 MetadataManager 时为了保障一致性,会给 collection 减少一个 X 锁。更新过程中,系统日志会打印上文看到的第二条日志:

2020-08-19T16:40:43.853+0800 I SHARDING [conn1527] Updating collection metadata for collection sdb.sColl from collection version: 25550573|83||5f59e113f7f9b49e704c227f, shard version: 25550573|72||5f59e113f7f9b49e704c227f to collection version: 25550574|264||5f59e113f7f9b49e704c227f, shard version: 25550574|248||5f59e113f7f9b49e704c227f

这里也即是文章开篇提到影响咱们性能的日志,根因还是因为更新元信息的 X 锁导致。

3.6+ 版本对 chunk version 治理的变动

那么,为什么 3.4 版本 没有问题,而到了 4.0 版本 却产生了性能进化呢?这里间接给出答案:3.6&4.0 的最新小版本当中,当 shard 进行 splitChunk 时,如果 shardVersion == collectionVersion,则会减少 major version,进而触发路由刷新。而 3.4 版本中只会减少 minor version。这里首先来看下 splitChunk 的根本流程,随后咱们再来详述为什么要做这样的改变

splitChunk 流程

  • 「auto-spliting 触发」:在 4.0 及以前的版本中,sharding 实例的 auto-spliting 是由 mongos 来触发的。每次有写入申请时,mongos 都会记录对应 chunk 的写入量,并判断是否要向 shardServer 下发一次 splitChunk 申请。判断规范:dataWrittenBytes >= maxChunkSize / 5(固定值)
  • 「splitVector + splitChunk」:向 chunk 所在的 shard 下发一个 splitVector 申请,获取对该 chunk 进行拆分的拆分点。该过程会依据索引进行肯定的数据扫描及计算,详见:SplitVector 命令。若 splitVector 获取到了具体的拆分点,则再次向 chunk 所在的 shard 下发一个 splitChunk 申请,进行理论的拆分。
  • 「_configsvrCommitChunkSplit」:shardServer 收到 splitChunk 申请后,首先获取一个分布式锁,而后向 configServer 下发一个 _configsvrCommitChunkSplit。configServer 收到该申请后进行数据更新,实现 splitChunk,过程中会有 chunk 版本信息的变动。
  • 「route refresh」:上述流程失常实现后,mongos 进行路由刷新。

splitChunk 时,chunk version 变动

在 SERVER-41480 中,对 splitChunk 时,chunk version 版本治理进行了调整

在 3.4 版本以及 3.6、4.0 较早的小版本中,「_configsvrCommitChunkSplit」 只会减少 chunk 的 minor version。

The original reasoning for this was to prevent unnecessary routing table refreshes on the routers, which don’t ordinarily need to know about chunk splits (since they don’t change targeting information).

根本原因是为了爱护 mongos 不做必须要的路由刷新,因为 splitChunk 并不会扭转路由指标,所以 mongos 不须要感知。

然而只进行小版本的自增,如果用户进行枯燥递增的写入,容易造成较大的性能开销。

假如 sharding 实例有 2 个 mongos:mongosA、mongosB,2 个 shard:shardA(chunkRange: MinKey ~ 0),shardB(chunkRange: 0 ~ Maxkey)。用户继续枯燥递增写入。

  • T1 时刻:mongosB 首先判断 chunk 满足「auto-spliting 触发」 条件,向 shardB 发送「splitVector + splitChunk」,在申请失常完结后,mongosB 触发路由刷新。此时,shardB 的 chunkRange 为 0 ~ 100,100 ~ Maxkey。
  • 随后在肯定工夫内(比方 T2 时刻),mongosB 无奈满足「auto-spliting 触发」 条件,而 mongosA 继续判断满足条件,向 shardB 发送 「splitVector + splitChunk」,但最终在「_configsvrCommitChunkSplit」 步骤,因为 mongosA 的路由表不是最新的,所以无奈依照其申请将 0 ~ Maxkey 进行拆分,最终无奈胜利执行。因为整个流程没有残缺完结,所以 mongosA 也无奈进行 路由表更新,则 在这段时间内继续会有这样的有效申请

而如上文形容的,splitVector 依据索引进行肯定的数据扫描、计算,splitChunk 会获取分布式锁,均为耗时较高的申请,所以这种场景对性能的影响不可漠视。

SERVER-41480中对上述问题进行修复,修复的形式是如果 shardVersion == collectionVersion (即 collection 上次的 chunk split 也产生在该 shard 上),则会减少 major version,以触发各节点路由的刷新。修复的版本为3.6.15, 4.0.13, 4.3.1, 4.2.2

而这个修复则导致了开篇咱们遇到的问题,确切些来说,任何在 shardVersion == collectionVersion 的 shard 上进行 split 操作都会导致全局路由的刷新。

官网修复

SERVER-49233 中对这个问题进行了具体的论述:

we chose a solution, which erred on the side of correctness, with the reasoning that on most systems auto-splits are happening rarely and are not happening at the same time across all shards.

咱们抉择了一个不完全正确的解决方案(SERVER-41480),理由是在大多数的零碎中,auto-split 很少产生,而且不会同时在所有 shard 上产生。

However, this logic seems to be causing more harm than good in the case of almost uniform writes across all chunks. If it is the case that all shards are doing splits almost in unison, under this fix there will constantly be a bump in the collection version, which means constant stalls due to StaleShardVersion.

然而,在对所有 chunk 进行平衡写入的状况下,这个逻辑仿佛弊大于利。如果这种场景下,所有的 shard 同时进行 split,那么在 SERVER-41480 修复下,collection version 将一直呈现平稳,也就意味着一直因为 StaleShardVersion 而导致一直暂停。

举例来具体阐明下这个问题:假如某 sharding 实例有 4 个 shard,各持有 2 个 chunk,以后时刻 major version=N。客户端对 sharding 实例的所有 chunk 进行平衡的写入,某时刻 mongosA 判断所有 chunk 合乎 split 条件,顺次对各 shard 进行间断的 split chunk 触发。为了便于阐明,假如如图所示,在 T1,T2,T3,T4 时刻,顺次在 ShardA、shardB、shardC、shardD 进行间断的 chunk split 触发,那么:

  • T1.1 时刻 chunk1 产生 split,使得 shardA 的 shardVersion == collection;T1.2 时刻 chunk2 产生 split,触发 configServer major version ++,此时最新的 major version=N+1;随后的 T1.3 时刻,shardA 感知后刷新本地 major version=N+1
  • 随后的 T2、T3、T4 时刻顺次产生上述流程。
  • 最终在 T5 时刻,mongosA 在触发完 split chunk 后被动刷新路由表,感知 major version = N+4

那么当零碎中另外一个 mongos(未产生更新,路由表中 major version=N)向 shard(比方 shardB)发送申请时

  • 在第一次申请交互后,mongosX 感知本身 major version 落后,与 configServer 交互,更新本地路由表后下发第二次申请
  • 第二次申请中,shardB 感知本身 major version 落后,通过 configServer 拉取并更新路由表
  • 在第三次申请中,单方均取得最新的路由表,而实现此次申请
  • mongos&shard 之间感知路由表落后靠申请交互时的 StaleShardVersion 来实现,而路由表更新的过程中,所有须要依赖该汇合路由表实现的申请,都须要 期待路由表更新实现 后能力持续。所以上述过程即 jira 中形容的:一直因为 StaleShardVersion 而导致一直暂停。

同时 SERVER-49233 提供了具体的解决方案,在 3.6.194.0.204.2.9 及后续的版本中,提供 incrementChunkMajorVersionOnChunkSplit 参数,默认为 false(即 splitChunk 不会减少 major version),可在配置文件或者通过启动 setParameter 的形式设置为 true。

而因为 auto-spliting 逻辑在 4.2 版本 批改为在 shardServer 上触发(SERVER-34448),也就不会再有 mongos 频繁下发有效 splitChunk 的场景。所以对于 4.4 版本,SERVER-49433 间接将减少 major version 的逻辑回滚掉,只会减少 minor version。(4.2 版本因为两头版本提供了 major version 逻辑,所以提供 incrementChunkMajorVersionOnChunkSplit 来让用户抉择)

这里对各版本行为总结如下:

  • 只会减少 minor version:3.4所有版本、3.6.15 之前的版本、4.0.13之前的版本、4.2.2之前的版本、4.4(暂未公布)
  • shardVersion == collectionVersion 会减少 major version,否则减少 minor version:3.6.15~ 3.6.18(蕴含)之间的版本、4.0.13 ~ 4.0.19(蕴含)之间的版本、4.2.2 ~ 4.2.8(蕴含)之间的版本
  • 提供 incrementChunkMajorVersionOnChunkSplit 参数,默认只减少 minor version:3.6.19及后续版本、4.0.20及后续版本、4.2.9及后续版本

应用场景与解决方案

MongoDB 版本 应用场景 修复计划
4.2 以下 数据写入固定在某些 Shard 采纳能够减少 major version 的版本(或设置 incrementChunkMajorVersionOnChunkSplit = true)
4.2 以下 数据在 shard 之间写入较平衡 采纳仅减少 minor version 的版本(或设置 incrementChunkMajorVersionOnChunkSplit = false)
4.2 所有场景 采纳仅减少 minor version 的版本(或设置 incrementChunkMajorVersionOnChunkSplit = false)

阿里云 MongoDB 4.2 版本中曾经跟进了官网修复。遇到该问题的用户能够将实例降级到 4.2 的最新小版本,而后按需配置 incrementChunkMajorVersionOnChunkSplit 即可。

退出移动版