“麻辣鲜”火锅最初只有一家店,所有食材堆在一个后厨仓库里,老板一人管账,门店自己决定从仓库取什么菜。后来生意火爆,连锁店开到全国,一个仓库再也塞不下每天从各地涌来的毛肚、牛肉和冻虾——进货堵在门口,取货绕晕店员。 老板想出了一个办法:把食材按类别分到不同城市的中央仓库——成都仓管毛肚、内蒙仓管牛肉、沿海仓管海鲜。每家门店的前台负责接单(路由),总部设一个“食材目录处”记录每种食材在哪个仓库(元数据),各城市仓库各自存储、各自出货(数据分片)。这样一来,仓库再多也不乱,门店再忙也不挤。这就是 MongoDB 分片集群的雏形:能水平扩展的分布式数据库架构。 本文用这套“连锁火锅店”的故事,带你从零搞懂 MongoDB 分片集群的三层架构、分片键选型、chunk 与 balancer 机制、运维扩容与问题排查,让你真正理解“数据该怎么分、分了怎么管、出了问题怎么救”。
掌握 MongoDB 分片集群架构与运维,是构建互联网级高并发、海量存储数据库系统的核心能力,也是资深 DBA 和架构师岗位的必备技能。
目录
可视化演示
动态演示MongoDB分片集群架构与的核心机制,通过概览/流程/对比/趋势四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
MongoDB分片集群架构与:分片集群 分片集群三层架构与组件职责 分片键选择策略与数据分布 核心机制可视化 MongoDB分片集群架构与整体架构与关键组件
第一章:分片架构总览——前台、目录与仓库
“麻辣鲜”在全国开了几十家分店之后,老板发现单靠“一个超大总仓库”已经行不通:进货通道堵死,取货路线绕晕,最要命的是稍微一扩容就要停业整顿。于是他把仓库按食材类别拆到了不同城市——这就是分片(sharding)的初衷:把数据水平切分到多个存储单元,让容量和并发可以“加仓库”而不是“换仓库”。
对应到 MongoDB,一个分片集群由三个角色组成。mongos 是门店前台:应用只连它,它不存任何数据,只负责接单并转发;config server 是总部食材目录处:保存集群的元数据,比如每个 chunk(货架)归哪个 shard(仓库),mongos 每次路由都要问它;shard 是城市仓库:真正存放用户数据的单元,每个 shard 本身又是一个副本集,保证数据多副本不丢。
三者的关系可以用一条链路描述:应用 → mongos →(查 config 拿到映射)→ 目标 shard。集群的存储天花板就是所有 shard 容量之和:
一句话总结:mongos 不存货、config 不存货、只有 shard 存货。三个角色各司其职,才能让“前台、目录、仓库”这套体系在不停业的情况下越开越大。
- 累加符号 Σ把从 i=1 到 n 的所有项逐一相加,相当于“把全国每个仓库的容量全部盘点加总”。
- C_i(第 i 个 shard 的容量)第 i 个仓库能装多少食材,单位是 GB 或 TB。
- n(shard 总数)仓库的个数,水平扩展的本质就是让 n 变大。
第二章:分片键选型——决定命运的分仓维度
分仓库的维度决定了生意格局。如果按“口味”分(辣/不辣),全国人民都吃辣,不辣仓就空置;按“食材类别”分,几百种食材刚好均匀散开。在 MongoDB 里,“按什么分”就是分片键(shard key),它是集合中一个或几个字段,MongoDB 按它的值把数据切到不同 chunk。
选分片键有四个硬指标:基数高、写入分散、查询常带、不可变。基数(cardinality)指分片键不同取值的个数,基数太低(比如只有两个值的“性别”)会导致数据最多只能切成两块,后面的 shard 白加。写入分散最容易被忽略:如果分片键是自增 _id,新插入永远落在最大键值所在的 chunk,所有写流量都打在同一 shard,这就是“写热点”。
解决办法是 hashed 分片(把键值哈希后再分布)或选业务上天然分散的字段。查询方面,带分片键的查询能精确路由到单个 shard,不带则要广播所有 shard(scatter-gather)。此外,分片键一经选定很难修改(MongoDB 4.4 起可 refine,但成本仍高),所以上线前必须想清楚。复合分片键的基数可以用乘法估算:
分片键就像火锅店的“分仓规则”,规则定错了,后面再好的车队、再多的仓库也救不回来。选键是分片集群里唯一“事后极难反悔”的设计决策。
- N(复合键总基数)组合键一共有多少种不同取值,决定理论上最多能切多少个 chunk。
- C_1(第一个字段的基数)第一个字段有多少种不同取值,比如城市字段有 300 个城市。
- ×(取值组合)两个字段的取值两两搭配,就像“菜系 × 食材”能组合出几十种新菜品。
- C_m(最后一个字段的基数)所有字段基数连乘,得出组合键的最终基数。
第三章:Chunk机制——仓库里的货架切分
食材进仓库后不能胡乱堆,得按分片键的值域分成一段段“货架区”,每段就是一个 chunk。比如把用户 ID 哈希后的 0-99 键值域切成 [0,20)、[20,40)……每个区间就是一块货架,写进来的数据按分片键值落在对应的货架上。
chunk 是数据的逻辑组织单位,范围用 [minKey, maxKey) 表示,默认目标大小是 64MB(maxChunkSize)。数据落在哪个 chunk 由分片键值决定,而 chunk 归哪个 shard 由 config server 记录。写入量上来后,chunk 会“长大”:当一个 chunk 的文档数或大小超过阈值,MongoDB 就会尝试 split——在它的键值范围中间找一个分界点,切成两个更小的 chunk。
注意 split 只是“把账本上的一个区间拆成两行”,数据物理位置没动;真正搬数据的是后面的迁移(migration)。需要规划的碎片数量,可以用总数据量除以目标 chunk 大小来估算:
货架满了就拆成两半——split 让数据能继续“长个”,也让均衡器有更多可搬的小块。理解了 chunk,再看 balancer 就顺理成章了。
- D_total(总数据量)整个集合要存放的数据总量,比如 10TB。
- S_chunk(目标 chunk 大小)默认 64MB,相当于每个货架的“标称容量”。
- ⌈⌉(向上取整)算出 10.2 也算 11 个货架,最后那个装不满也得存在。
第四章:均衡器原理——总部的调度车队
前台的目录再准,仓库之间也可能“旱的旱死、涝的涝死”。总部为此养了一支“调度车队”——balancer,它常驻后台,发现某个仓库的货架明显多于其他仓库时,就把多余的货架搬到空一点的仓库。
balancer 是运行在 mongos 上的协调者(新版本里协调组件在 config server 主节点上运行,原理一致)。它周期性检查各 shard 的 chunk 数量:当最大值和最小值的差值超过阈值(chunk 总数较少时阈值是 8 个),就进入均衡流程。迁移一个 chunk 分为四步:把源 chunk 标记为 draining;把文档逐个拷贝到目标 shard;拷贝完成后在源 shard 删除;最后更新 config 里的 chunk 映射。
迁移不是原子的,但业务读写不会被阻塞——读写可以打到正在迁移的 chunk 上。为了避免迁移流量挤占业务 IO,可以设置 balance window(比如凌晨 2-6 点),也可以临时停掉 balancer 做运维操作。触发条件可以用差值公式表达:
调度车队不会一次搬空,而是搬一块、核实一块、再搬下一块,还要避开城市高峰——这就是 balancer 的“慢工出细活”。看清它的节奏,运维时就不会误判“为什么数据还不均衡”。
- C_i(各 shard 的 chunk 数)每个仓库当前拥有的货架数量。
- max_i / min_i(找最大与最小)在所有 shard 里挑出 chunk 最多的和最少的那两个仓库。
- T(迁移阈值)小集群默认 8 个,差值不够大不值得折腾车队。
第五章:部署与运维——开分店的硬规矩
开分店有硬规矩:仓库不能只有一个值班员,总目录更不能交给“临时工”。部署 MongoDB 分片集群,第一原则是每个角色都高可用:config server 必须用副本集,生产环境 3 节点起步——它保存着全集群的 chunk 路由映射,一旦丢失,mongos 就变成“没有目录的前台”,数据明明在却找不到。
每个 shard 也建议 3 节点副本集(可加仲裁节点),mongos 无状态,可以部署多台并用负载均衡器对外。日常运维离不开三件事:备份、监控、巡检。备份要“先停 balancer 再做快照”,对每个 shard 和 config 分别备份并保证一致性;监控聚焦 chunk 分布、迁移速率、各 shard 的 QPS 与容量;巡检脚本要定期检查 config.chunks 里有没有异常块。
副本集的高可用可以用独立节点的可用性推导出来:
规矩越硬,营业越稳。把 config 副本集、shard 副本集、备份调度当成开店执照一样对待,分片集群才能扛住生产流量。
- a(单节点可用性)单个值班员不撂挑子的概率,比如 99%。
- (1-a)³(三节点同时故障)三个节点都挂掉整体才不可用,所以三个“挂的概率”相乘。
- 1 - ……(整体可用性)用 1 减去“全挂”的概率,就是至少有一台活着的概率。
第六章:扩容与容灾——再开一个仓库
生意继续涨,仓库又不够了。这时候老板只需说一句“再开一个仓”,剩下的交给车队:MongoDB 新增 shard 后用一条 addShard 命令完成挂载,balancer 会自动把部分 chunk 迁入新 shard,业务全程不停。
扩容后每个 shard 的平均数据量可以用总数据量除以 shard 个数来估算。但要注意:迁移需要时间,扩容瞬间新 shard 是“空仓”,热点还留在老 shard 上;而且迁移过程会产生额外 IO,所以扩容动作最好放在低峰期,并盯住迁移进度。除了横向加 shard,还有两种手段:垂直扩容(给单个 shard 加 CPU/内存/磁盘)——相当于把仓库盖高,简单但有硬件上限;zone 分片——通过 zone 和 tag 把特定键值范围的数据“钉”在指定的 shard 上,适合数据属地化或读写隔离需求。
扩容后的容量规划用下面的公式:
扩容不是“加个仓库马上统统均匀”,而是“挂好仓库,给车队一点时间”。把期望容量、迁移成本、zone 策略放在一起规划,集群才能优雅地长大。
- D_total(总数据量)整个集群要承载的全部数据。
- n_shard(分片个数)扩容后一共有多少个仓库。
- ⌈⌉(向上取整)每片至少要能装下平均值,实际生产还要预留 30% 以上余量。
第七章:问题排查——仓库异常三查法
仓库出问题,老司机的排查顺序是“先看键,再看块,最后查车”:键是分片键选型,块是 chunk 状态,车是 balancer 迁移。三个环节对应三类典型故障。
热点问题:某个 chunk 的访问量远高于平均,常见原因是分片键基数低或取值倾斜(比如“城市”字段里上海占 80%)。热点表现为单个 shard 的 QPS/CPU 飙高,排查时看 config.chunks 里的键值分布,解法是换复合分片键或对热点键做二次哈希。jumbo chunk:chunk 超过 maxChunkSize 且无法再 split(键值域已经没有中间值可切),会被打上 jumbo 标记,balancer 拒绝迁移它,导致均衡卡死;排查 config.chunks 里的 jumbo: true,处理手段是把块内数据拆细,或临时调大 maxChunkSize 清理标记。
迁移慢则要检查 balance window 是否太短、目标 shard 是否缺索引、网络是否被限速。热点程度可以量化:
排查如看病:“键”是体质、“块”是病灶、“车”是药效。按这个顺序定位,大部分分片集群问题都能在十分钟内找到方向。
- QPS_chunk(单 chunk 请求量)这个货架每秒被取货的次数。
- QPS(集群平均请求量)所有 chunk 每秒请求数的平均值。
- H(热点度)大于 2 说明明显偏热,大于 5 就要紧急干预。
AI 应用场景
场景一:大规模推荐系统的用户行为特征存储
阶段:特征存储与数据服务层
解决的问题:推荐模型需要近线存储海量用户行为日志供特征工程读取。传统单机 MongoDB 在几十亿条记录、每秒数万写入面前很快触顶;用分片集群按 user_id 做哈希分片,写入均匀分散到所有 shard,特征查询带上 user_id 后由 mongos 精确路由,不再广播全表。
具体案例:某互联网公司日活 5000 万,行为特征表每天新增约 20 亿条记录,把集合按 user_id hashed 分片到 24 个 shard,成功支撑 10 万级 QPS 写入,同时把特征读取 P99 延迟稳定在 20ms 内。
场景二:RAG 知识库的文档切片元数据管理
阶段:RAG 数据工程管道
解决的问题:大模型 RAG 应用要把海量文档切块、向量化后入库,向量存在向量数据库,但文档原文、切片元数据、切片之间的父子关系需要一个可靠且可扩展的数据库来管。MongoDB 分片集群按 doc_id 分片,支持知识库元数据的海量存储与按文档维度聚合查询。
具体案例:某企业级知识库平台存放 1 亿篇文档的切片元数据,8 个 shard 承载知识库的每日刷新与在线检索,管理端按 kb_id 过滤时能精确路由,避免跨 shard 广播。
场景三:模型训练指标与日志的可观测性平台
阶段:MLOps 训练运维
解决的问题:大模型训练任务每秒产生大量指标日志,需要持久化供调参与复盘分析。日志按 run_id 分片,天然把同一个训练任务的日志聚在同一批 shard 上,查询某次实验只需路由到少量分片。
具体案例:某 MLOps 平台累计管理 10 万次训练实验,每次实验产生约 500GB 指标日志,分片集群按 run_id 哈希分片到 12 个 shard,支持按实验维度快速切片分析,日志留存 180 天。
Python 演示
# 模拟 MongoDB 分片集群: mongos 路由 + config 元数据 + chunk 迁移均衡import hashlib def hash_key(user_id): # 把 user_id 哈希到 0-99, 模拟 hashed 分片键 return int(hashlib.md5(str(user_id).encode()).hexdigest(), 16) % 100 class Shard: # 一个分片, 相当于 replica set 的主节点 def __init__(self, name): self.name = name self.docs = {} def insert(self, key, value): self.docs[key] = value def query(self, key): return self.docs.get(key) def count(self): return len(self.docs) def take_range(self, lo, hi): # 把落在 [lo, hi) 的文档取走, 用于 chunk 迁移 moved = {} for k in [k for k in self.docs if lo <= hash_key(k) < hi]: moved[k] = self.docs.pop(k) return moved class ConfigServer: # 记录每个 chunk 区间属于哪个 shard def __init__(self, boundaries, shards): self.boundaries = boundaries self.shards = shards self.chunk_owner = [0] * (len(boundaries) - 1) def route(self, key): for i in range(len(self.boundaries) - 1): if self.boundaries[i] <= key < self.boundaries[i + 1]: return self.shards[self.chunk_owner[i]] return self.shards[-1] def migrate(self, chunk_idx, dst): # 把一个 chunk 的数据和归属一起迁到 dst lo, hi = self.boundaries[chunk_idx], self.boundaries[chunk_idx + 1] src = self.shards[self.chunk_owner[chunk_idx]] for k, v in src.take_range(lo, hi).items(): dst.insert(k, v) self.chunk_owner[chunk_idx] = self.shards.index(dst) class MongosRouter: # 应用入口: 先问 config 找 shard, 再读写数据 def __init__(self, config): self.config = config def write(self, uid, doc): self.config.route(hash_key(uid)).insert(uid, doc) def read(self, uid): return self.config.route(hash_key(uid)).query(uid) def scatter_gather(self, city): # 不带分片键的查询: 广播到所有 shard hits = [] for s in self.config.shards: for doc in s.docs.values(): if doc["city"] == city: hits.append(doc) return hits def balance(config): # 极简 balancer: 从 chunk 最多的 shard 迁移一个 chunk 到最少的 for _ in range(10): counts = [sum(1 for o in config.chunk_owner if o == i) for i in range(len(config.shards))] if max(counts) - min(counts) <= 1: break src = max(range(len(counts)), key=lambda i: counts[i]) dst = min(range(len(counts)), key=lambda i: counts[i]) for ci in range(len(config.chunk_owner)): if config.chunk_owner[ci] == src: config.migrate(ci, config.shards[dst]) break return [sum(1 for o in config.chunk_owner if o == i) for i in range(len(config.shards))] def main(): shards = [Shard("shardA"), Shard("shardB"), Shard("shardC")] boundaries = list(range(0, 100, 20)) + [100] config = ConfigServer(boundaries, shards) router = MongosRouter(config) # 模拟 300 个用户写入, 初始所有 chunk 都在 shardA for uid in range(300): router.write(uid, {"user_id": uid, "city": "city_" + str(uid % 10)}) print("docs per shard before:", [s.count() for s in shards]) # 带分片键的查询: 精确路由到单个 shard print("read user 42:", router.read(42)) # 不带分片键的查询: mongos 广播所有 shard print("scatter_gather city_7:", len(router.scatter_gather("city_7"))) print("chunk owners before:", config.chunk_owner) print("chunk owners after:", balance(config)) print("docs per shard after:", [s.count() for s in shards]) if __name__ == "__main__": main()
self.chunk_owner = [0] * (len(self.boundaries) - 1)初始把所有 chunk 都登记到第一个 shard,模拟“部署初期数据全挤在一个仓库”。def route(self, key):config server 的核心方法:给定分片键哈希值,用边界表找到它属于哪个 chunk,进而返回所属 shard。def write(self, uid, doc):mongos 写路径:先算 hash_key(uid) 走 route 找到 shard,再 insert,一次写入只打一个 shard。def scatter_gather(self, city):不带分片键的查询:mongos 必须广播所有 shard 再合并结果,代价很高。def migrate(self, chunk_idx, dst):balancer 迁移:把 chunk 对应的文档从源 shard 搬到目标 shard,并更新 chunk_owner。if max(counts) - min(counts) <= 1:均衡终止条件:各 shard 的 chunk 数差异不超过 1,类似 MongoDB 的阈值判定。print("docs per shard before:", [s.count() for s in shards])输出均衡前的文档分布,可以清楚看到热点集中在 shardA。
常见误区
误区“反正 MongoDB 会自动分片,我随便挑个字段当分片键,数据多了自然就均衡了。”
正解分片键决定数据怎么切,选错会直接造出“热点仓库”。它必须基数高、写入分散、查询常带。好比连锁火锅店按“辣度”分仓,全国人民都吃辣,不辣仓就一直空转。分片键是架构第一决策,且后期极难修改。
为何常见:新手容易把“分片”误解成数据库自动魔法,忽略了切分维度本身的物理意义。
误区“chunk 超过 64MB 就会自动 split,所以我只管写入就行。”
正解split 需要有中间值可切。如果分片键单调递增,新数据总落在最边缘的 chunk,而这个 chunk 的区间里已经没有可分界的键值,就会变成 jumbo,既不 split 也不迁移。自动拆分的前提是“区间内还有中间值”。
为何常见:只看到文档里写的“自动拆分”,没理解拆分的数学前提是键值域可继续二分。
误区“balancer 一直在跑,我大量写入后,应该几分钟就均匀了吧?”
正解balancer 有触发阈值(chunk 数差异超过 8 才动手)、有运行窗口、每次迁移要逐文档拷贝数据并耗 IO。一个大 size 的迁移 cycle 跑几小时很正常。“自动”不等于“立即”。
为何常见:把“后台异步任务”等同于“实时同步”,忽略了异步任务的调度周期与迁移成本。
误区“扩容嘛,跑一条 addShard 命令,睡一觉数据就均匀了。”
正解addShard 只是“挂上了仓库”,真正搬货的是 balancer,它逐 chunk 迁移,迁移期间源和目标 shard 都有额外 IO 与网络开销。所以扩容要选低峰期、提前评估迁移量,必要时手动调大 balance window。
为何常见:只看到“一条命令扩容”的表象,忽略了背后真实的数据搬迁工程。
误区“config server 才几十 MB 元数据,丢了大不了重新分片。”
正解config 保存 chunk 到 shard 的映射,丢了等于总部目录被烧毁,mongos 无法路由,数据虽然还在但“找不到”。所以 config 必须 3 节点副本集,并且纳入备份体系。
为何常见:元数据体积小,但它是整个路由系统的“寻址表”,重要性被严重低估。
面试题
Q1. MongoDB 分片集群中负责路由客户端请求的是哪个组件?
- A.shard 存储数据的分片
- B.mongos 集群路由入口← 正确答案
- C.config server 配置中心
- D.mongodump 备份工具
答案:B。mongos 就像火锅店的前台,应用只跟它打交道,它查总目录(config)后把请求转到正确的仓库(shard),本身不存数据,所以能水平部署多台。shard 只管存,config 只存元数据,mongodump 是备份工具。
Q2. 关于分片键“基数”的说明,正确的有?
- A.基数越大越利于分布均匀← 正确答案
- B.基数越小越可能造成热点← 正确答案
- C.基数与数据分布没有关系
- D.基数只影响查询速度
答案:AB。基数就是分片键不同取值的个数。基数大,才有足够多的 chunk 摊到各个 shard;基数小(比如只有“辣/不辣”两档),数据最多切成两块,必然形成热点。C 和 D 把基数的作用完全说反了。
Q3. MongoDB 默认 chunk 的目标大小是多少?
- A.16MB 上限文档大小
- B.32MB 这个常见数值
- C.64MB 默认分片大小← 正确答案
- D.128MB 大文档友好
答案:C。默认 maxChunkSize 是 64MB。注意 16MB 是单个文档大小上限,两者是不同概念;chunk 超过 64MB 会触发 split 尝试,拆不动就会变成 jumbo。
Q4. 关于 chunk 迁移流程,下列说法正确的是?
- A.迁移原子且瞬间完成
- B.先拷贝再删除更新元数据← 正确答案
- C.迁移期间暂停业务写入
- D.迁移只更新元数据记录
答案:B。balancer 迁移像搬家车队:先把货架上的货一件件搬到新仓库(拷贝),搬完确认无误后旧仓库才清空,最后更新总部目录(config)。整个过程业务读写不中断,也绝不是只改账本。
Q5. 以下哪种分片键最可能导致“写热点”?
- A.按 user_id 哈希分片
- B.按城市名均匀分布分片
- C.按组合字段城市加用户分片
- D.按自增字段单调递增分片← 正确答案
答案:D。自增字段是“永远在增长”的编号,新写入永远落在值域最右端的 chunk,等于所有新货都堆在同一个仓库门口;哈希分片能把连续编号打散到全部 chunk。
Q6. 关于分片集群组件,以下说法正确的有?
- A.每个 shard 本身建议使用副本集保证高可用← 正确答案
- B.config server 推荐 3 节点副本集← 正确答案
- C.mongos 是无状态的,可以部署多台← 正确答案
- D.shard 必须单节点才能完成分片
答案:ABC。mongos/前台无状态可以多台;shard/仓库必须副本集避免单点;config/总目录必须副本集,因为它挂了没人知道食材在哪。D 错在 shard 单节点恰恰是运维大忌。
Q7. jumbo chunk 会带来什么后果?
- A.无法被 balancer 迁移卡住← 正确答案
- B.数据会自动丢失找不回
- C.整个集群会直接宕机
- D.分片键被清零重新分配
答案:A。jumbo 是“大到拆不动的货架”,因为键值域只剩单值无法再 split,balancer 拒绝搬它,均衡被卡住。它不是致命故障,但会拖累数据分布与后续扩容。
Q8. 在分片集群中对不带分片键的字段做查询时,会发生什么?
- A.mongos 会把查询广播到所有 shard← 正确答案
- B.返回结果由 mongos 合并输出← 正确答案
- C.只路由到第一个 shard 节点
- D.性能较慢属于 scatter-gather← 正确答案
答案:ABD。查询不带分片键时,mongos 不知道去哪找,只能像全城寻人一样每个仓库都问一遍,再合并结果。这正是广播查询(scatter-gather),是分片集群查询的大忌,不会只查第一个 shard。
Q9. 集群新增一个 shard 后,数据分布会怎样变化?
- A.数据立刻全部重新均匀分布
- B.需要手工导出再导入才能分散
- C.balancer 后台逐步迁移服务不停← 正确答案
- D.新 shard 只会接收新写入数据
答案:C。新增仓库后,调度车队会在后台把部分货架逐步搬去新仓库,这是渐进的自动过程,期间业务照常服务;迁移受窗口与 IO 影响,所以扩容建议在低峰期做,而不是立刻均匀或手工搬家。
Q10. 分片键为“城市”字段且上海占 80% 数据时,最合理的优化是?
- A.使用复合分片键并对热点哈希← 正确答案
- B.直接把城市字段删掉不用
- C.加大 chunk 大小到 1GB 容量
- D.只保留一个 shard 节点
答案:A。单值占比过高的键叫“倾斜键”,会让上海对应的 chunk 变成超级热点。用“城市+用户ID”复合键或对热点城市做二次哈希,能把上海的货摊到多个货架甚至多个仓库。删字段、加 chunk 大小、减 shard 都是反向操作。