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 8676ms2020-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.19
、4.0.20
、4.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
即可。