bigywater.tech
MongoDB · 74

单调递增的分片键:写入热点实测

用时间戳或 ObjectId 这类单调递增的字段做分片键会怎样?balancer 能不能解决写入热点?哈希和复合键各自的代价是什么?这篇在 2 个分片上用同样的写入负载对比三种分片键。

100% 对 50%
30 秒写入中 sh0 的占比:单调 ranged 键与哈希键
0 个
开 balancer 搬完旧数据后,再写 20,000 个更大的键,落在 sh1 的个数
scatter-gather
哈希键下按时间范围查询要问所有分片

热点

实测 · 本机执行输出
  第  5 s 窗口:sh0 48,300  sh1      0   键最大的那个 chunk 在 sh0
  第 10 s 窗口:sh0 48,000  sh1      0   键最大的那个 chunk 在 sh0
  第 15 s 窗口:sh0 47,400  sh1      0   键最大的那个 chunk 在 sh0
  第 20 s 窗口:sh0 48,500  sh1      0   键最大的那个 chunk 在 sh0
  第 25 s 窗口:sh0 47,700  sh1      0   键最大的那个 chunk 在 sh0
  第 30 s 窗口:sh0 47,600  sh1      0   键最大的那个 chunk 在 sh0
  合计:sh0 287,500、sh1 0;sh0 占 100%;共写入 287,500 个文档;chunk 分布 {'sh0': 1}(balancer 关着,chunk 没有被拆分也没有被迁移:所有写入都进同一个 chunk)

单调递增的 ranged 分片键,新文档的键总是最大,总是落进键空间末端的那个 chunk,所以 100% 的写入都打在同一个分片上。

哈希

实测 · 本机执行输出
  第  5 s 窗口:sh0 22,576  sh1 22,726
  第 10 s 窗口:sh0 23,185  sh1 22,872
  第 15 s 窗口:sh0 22,923  sh1 22,817
  第 20 s 窗口:sh0 23,003  sh1 22,998
  第 25 s 窗口:sh0 22,972  sh1 22,980
  第 30 s 窗口:sh0 23,102  sh1 23,046
  合计:sh0 137,761、sh1 137,439;sh0 占 50%;共写入 275,200 个文档;chunk 分布 {'sh0': 1, 'sh1': 1}

哈希键在 shardCollection 时就在各个分片上建好 chunk,写入被均匀分开(各约 50%)。

复合键

实测 · 本机执行输出
  手工 split + moveChunk 之后的 chunk 分布 {'sh0': 1, 'sh1': 1}
  第  5 s 窗口:sh0 23,200  sh1 23,300
  第 10 s 窗口:sh0 23,052  sh1 22,948
  第 15 s 窗口:sh0 23,148  sh1 23,152
  第 20 s 窗口:sh0 22,500  sh1 22,500
  第 25 s 窗口:sh0 23,352  sh1 23,400
  第 30 s 窗口:sh0 23,048  sh1 23,048
  合计:sh0 138,300、sh1 138,348;sh0 占 50%;共写入 276,800 个文档

在单调字段前面加一个有限基数的前缀(这里是 8 个桶),写入分散到前缀对应的多个 chunk,每个桶内键仍然递增。

balancer 解决不了

实测 · 本机执行输出
  balancer 搬了 55 秒后 test.mono 的数据分布: sh1 拥有 119,535 个文档;sh0 拥有 167,965 个文档
  新写入的 20,000 个更大的键:直连 sh0 里 ts >= 1701000000000 的有 20,000 个,sh1 里有 0 个;键最大的 chunk 在 sh0

balancer 能把旧数据搬到别的分片(119,535 对 167,965),但新写入仍然追着最大 chunk 所在的分片。

查询代价

实测 · 本机执行输出
  范围查询 ts ∈ [t+1000, t+1100):ranged 涉及的分片 ['sh1'];hashed 涉及的分片 ['sh0', 'sh1'];复合键(不带 bucket)涉及的分片 ['sh0', 'sh1']
  等值查询 ts = t+1000:ranged ['sh1'];hashed ['sh0']
  带 bucket 的查询(bucket = 3 且 ts 范围):复合键涉及的分片 ['sh0'];bucket = 6 的:['sh1']

ranged 键的范围查询只问一个分片;哈希键的范围查询要问所有分片;复合键带前缀字段才能 targeted。

怎么选

实测 · 本机执行输出
  分片键                            写入文档数    sh0 占比
  ranged {ts}(单调)              287,500      100%
  hashed {ts}                  275,200       50%
  复合 {bucket, ts}(手工分两半)       276,800       50%

写入热点和范围查询的 targeted 能力要取舍:大量按时间范围查询又大量写入时,常用复合键(低基数前缀 + 时间)折中。

分步演示

单调递增的分片键
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MongoDB 9.0.2(Homebrew 的 mongod、mongos)/ mongosh 2.12.0 / pymongo 4.18.3;每个实验在临时目录里起全新的 mongod(独立 dbpath,端口 9900 以上,只监听 127.0.0.1,WiredTiger 缓存 0.25 GB),需要副本集或分片集群时在本机起多个进程;命令输出来自真实的 mongosh,批量写入、计时与多线程用 pymongo;计时用 time.perf_counter,数字每次运行会有小幅抖动,ObjectId、时间戳等每次运行都不同。2 个分片(各为单节点副本集),chunk 大小 1 MB;为排除迁移带来的 insert,写入对比时关掉了 balancer;insert 次数取自各分片 serverStatus().opcounters
    • 没有验证:真实多核多机下热点分片的 CPU / 磁盘饱和程度(本机两个分片共享一块硬盘与 CPU,吞吐数字没有比较意义);复合键桶数的选取;时间序列场景下 MongoDB 对时序集合的分片键建议;ObjectId 作为 ranged 分片键的同样现象;zone sharding 与预拆分对热点的作用
    京ICP备2026067502号-1京公网安备11010502063487号