用时间戳或 ObjectId 这类单调递增的字段做分片键会怎样?balancer 能不能解决写入热点?哈希和复合键各自的代价是什么?这篇在 2 个分片上用同样的写入负载对比三种分片键。
第 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 搬了 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 能力要取舍:大量按时间范围查询又大量写入时,常用复合键(低基数前缀 + 时间)折中。