小Cの已经记不起来的博客

MongoDB 分片键选择对读写分布和查询性能的影响

先把结论说在前面

分片键不是建集合时随手填的一个参数。它基本决定了三件事:写入会落到哪些分片、查询是只找一两个分片还是把整个集群扫一遍、后面 chunk 怎么分裂和迁移。选得好,集群加机器就能跟着扛;选得不好,数据是分散了,但热点、广播查询、jumbo chunk 一个都不会少。

我一开始也把分片键当成“能分就行”的东西,后来被监控图教育了。下面这些坑,基本都是我自己或者周围项目里真实遇到过的。

我踩过的那个坑

前阵子给一个订单集合做分片,数据量大概几千万,写入还在涨。当时想得很简单:订单不是按时间查得多吗,那就拿 createdAt 做范围分片。命令大概是这样:

sh.enableSharding("shop")
sh.shardCollection("shop.orders", { createdAt: 1 })

上线第一天看着还挺正常,第二天开始不对劲。监控里最新时间段对应的那个分片写入一直顶在高位,其他分片闲得发慌。原因也不复杂:订单时间一直往前走,新写入永远落在最右边那个 chunk,范围分片对这种单调递增字段天生就容易出写热点。

更难受的是查询。业务里大量请求是“按用户查订单”:

db.orders.find({ userId: "u_12345" }).sort({ createdAt: -1 }).limit(20)

这个查询没带 createdAt 的范围条件,也没带分片键,mongos 只能把请求发到所有分片,再合并结果。explain 里能看到 SHARD_MERGE,每个分片都跑了一遍。数据量小的时候没感觉,数据一多,P99 直接起飞。你给 userId 单独加索引也没用,因为问题不在单个分片内的索引,而在请求根本没被路由到目标分片。

这就是分片键最容易被低估的地方:它不只影响“数据怎么分”,还影响“查询怎么走”。

写分布:单调递增和低基数最容易被低估

范围分片适合范围查询,但它对单调递增字段不友好。createdAt、自增 ID、雪花 ID 这类值,新数据总是集中在最后一个 chunk。你可以预分裂一些 chunk 缓一缓,但只要写入持续增长,热点还是会往尾部跑。balancer 后面虽然会搬 chunk,但搬的是已经写进去的数据,当前写入热点没那么快消失。

哈希分片能把写入打散,比如:

sh.shardCollection("shop.orders", { userId: "hashed" })

这样同一个用户的数据会落到同一个分片,写入分布通常也均匀很多。但代价是范围查询变难受:按 createdAt 做区间统计,还是得广播到所有分片。哈希不是银弹,它只是把问题从“写热点”换成了“范围查询要扫更多分片”。

低基数分片键也挺坑。比如拿“状态”字段分片,值就那么几个,chunk 数量受限制,同一个值的数据没法继续拆,很容易搞出 jumbo chunk。状态还会变,分片键又不可变,后面想改都麻烦。分片键字段的取值最好足够多,而且别是那种频繁更新的字段。

读分布:查询带不带分片键,是两条路

MongoDB 的读路由很直接:查询条件里带了分片键,或者带了复合分片键的前缀,mongos 就能定向到目标分片;没带,就大概率广播。广播查询不是每次都慢,但它会把压力摊到所有分片,数据量上来后很难控。

所以选分片键时,我会先翻一遍真实查询,而不是先看字段基数。比如订单系统里,如果 70% 的查询是“按用户查”,那分片键就应该围绕 userId 做文章;如果后台运营大量按时间范围拉数据,那范围分片更合适,但得接受写入热点,并想办法做预分裂和监控。如果两种查询都很多,就考虑复合分片键,比如 { tenantId: 1, createdAt: 1 },但要注意查询必须带 tenantId 才能用上前缀,只按 createdAt 查还是会广播。

复合分片键不是把两个字段一拼就完事。顺序很重要,查询模式也很重要。{ tenantId: 1, createdAt: 1 } 和 { createdAt: 1, tenantId: 1 } 对路由的意义完全不一样。前者适合“先定租户再按时间查”,后者对单纯按时间范围查更友好,但租户隔离和写入分布又得另算。

上线前我会做的几件事

先把最常跑的查询拿出来,在分片集群上跑 explain("executionStats"),看它到底是定向还是走了 SHARD_MERGE。然后看 chunk 分布:

sh.status()
db.orders.getShardDistribution()

如果发现某个分片 chunk 特别多、写入特别集中,或者某个分片键值占了大量文档,就得警惕。低基数分片键还要注意 jumbo chunk,别等 balancer 搬不动了才发现。唯一索引也要提前想清楚,分片集合里的唯一索引通常得把分片键包含进去,不然后面加约束会很难受。

还有一点,分片键字段的值一旦写入就改不了。要换分片键,要么重建集合迁数据,要么走 reshardCollection,后者也不是点一下按钮就完事,期间照样要占资源、要看业务低峰。所以每次有人问我分片键怎么选,我都先问:你们最常跑的查询长什么样,写峰值落在哪。回答不清楚,选啥都是赌。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放