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

本文涉及的产品
云数据库 MongoDB,独享型 2核8GB
推荐场景:
构建全方位客户视图
日志服务 SLS,月写入数据量 50GB 1个月
简介: 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

路由刷新行为

MongoDB-cache-refresh.png

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

第一步:从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 流程

MongoDB-splitChunk.png

  • 「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 不需要感知。

但是只进行小版本的自增,如果用户进行单调递增的写入,容易造成较大的性能开销。

MongoDB-SERVER-41480.png

假设 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 而导致不断暂停。

MongoDB-SERVER-49233.png

举例来详细说明下这个问题:假设某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即可。

相关实践学习
MongoDB数据库入门
MongoDB数据库入门实验。
快速掌握 MongoDB 数据库
本课程主要讲解MongoDB数据库的基本知识,包括MongoDB数据库的安装、配置、服务的启动、数据的CRUD操作函数使用、MongoDB索引的使用(唯一索引、地理索引、过期索引、全文索引等)、MapReduce操作实现、用户管理、Java对MongoDB的操作支持(基于2.x驱动与3.x驱动的完全讲解)。 通过学习此课程,读者将具备MongoDB数据库的开发能力,并且能够使用MongoDB进行项目开发。   相关的阿里云产品:云数据库 MongoDB版 云数据库MongoDB版支持ReplicaSet和Sharding两种部署架构,具备安全审计,时间点备份等多项企业能力。在互联网、物联网、游戏、金融等领域被广泛采用。 云数据库MongoDB版(ApsaraDB for MongoDB)完全兼容MongoDB协议,基于飞天分布式系统和高可靠存储引擎,提供多节点高可用架构、弹性扩容、容灾、备份回滚、性能优化等解决方案。 产品详情: https://www.aliyun.com/product/mongodb
目录
相关文章
|
16天前
|
存储 缓存 网络协议
数据库执行查询请求的过程?
客户端发起TCP连接请求,服务端通过连接器验证主机信息、用户名及密码,验证通过后创建专用进程处理交互。服务端进程缓存以减少创建和销毁线程的开销。后续步骤包括缓存查询(8.0版后移除)、语法解析、查询优化及存储引擎调用,最终返回查询结果。
26 6
|
4月前
|
缓存 监控 NoSQL
【Azure Redis 缓存】Azure Redis出现了超时问题后,记录一步一步的排查出异常的客户端连接和所执行命令的步骤
【Azure Redis 缓存】Azure Redis出现了超时问题后,记录一步一步的排查出异常的客户端连接和所执行命令的步骤
|
6月前
|
监控 NoSQL MongoDB
MongoDB中的TTL索引:自动过期数据的深入解析与使用方式
MongoDB中的TTL索引:自动过期数据的深入解析与使用方式
|
6月前
|
NoSQL Redis 数据安全/隐私保护
大事件后端项目35——登录优化_redis_主动失效机制实现
大事件后端项目35——登录优化_redis_主动失效机制实现
|
6月前
|
监控 关系型数据库 MySQL
MySQL 查询数据库响应时长详解
- 启用慢查询日志(`slow_query_log`)分析超时查询,调整`long_query_time`阈值。 - 使用`EXPLAIN`检查查询执行计划,优化索引和查询结构。 - `SHOW PROFILE`揭示查询各阶段耗时,辅助性能调优。 - 开启Performance Schema监控服务器,通过`events_statements_summary_by_digest`等表分析性能。 - MySQL Workbench和Percona Toolkit等工具提供额外的性能分析和管理功能。 - 优化技巧:创建合适索引,精简查询,调整数据库配置以提升响应速度。
|
7月前
|
存储 关系型数据库 MySQL
从 MySQL 执行原理告诉你:为什么分页场景下,请求速度非常慢
从 MySQL 执行原理告诉你:为什么分页场景下,请求速度非常慢
54 0
|
自然语言处理 算法 数据库
简述数据库执行查询请求的过程
当数据库接收到查询请求后,首先需要对查询语句进行解析。这个过程包括词法分析和语法分析,将查询语句转化为内部数据结构,以便后续处理。
169 0
|
缓存 JSON NoSQL
如何处理缓存跟数据库数据不一致?
如何处理缓存跟数据库数据不一致?
如何处理缓存跟数据库数据不一致?
|
缓存 监控 关系型数据库
MySql缓存查询原理与缓存监控 And 索引监控
MySql缓存查询原理与缓存监控 And 索引监控
170 0
|
NoSQL Redis 开发者
集群-设置与获取数据|学习笔记
快速学习集群-设置与获取数据
集群-设置与获取数据|学习笔记
下一篇
DataWorks