假设你已经在本地把Kafka跑起来了,往某个Topic里塞过几条消息,也能用命令行消费者把它们读出来——那我猜你现在大概率是这种状态:命令能通,心里却是虚的。Topic到底是干什么的,Partition为什么一会儿说要多少个,Replica和Consumer Group又是怎么配合的,这几个词在脑子里各说各话,串不起来。这是很多入门者共同的坎。
这一篇,我就把Topic、Partition、Replica、Consumer Group这四个核心概念拆开揉碎讲清楚。它们不是彼此孤立的四个名词,而是构成Kafka存储、复制、消费三条主线的四个齿轮。把这条链子弄明白,你再回头看那些配置文件、启动参数、生产消费的API代码,会突然觉得顺了很多。这篇文章适合三种人:刚跑通Demo、准备在自己的项目里正式用Kafka的开发者;被各种"高吞吐、可持久化、分布式消息中间件"这类宣传词绕晕的初学者;以及想把基础概念系统梳理一遍再继续深入的进阶读者。
1. 先搞清楚:Kafka到底是一个"队列",还是一本"账本"
1.1 从"取走即删"到"自己翻页"的思维转变
很多人在入门别的消息中间件时,脑子里已经形成了一个固定模型:生产者往队列里丢消息,消费者从队列里取消息,取走一条少一条。这套"点对点"模型用起来很直观,但它有两个天然的短板:第一,消息被某个消费者取走后,其他需要同一份数据的系统就再也拿不到了;第二,消费者只能被动接受"现在发过来的消息",很难回头再去读过去某段时间的数据。
Kafka偏偏把这个模型反过来了。消息被生产者发送到Kafka之后,不会因为被消费掉而立即删除,它会按照Topic里的保留策略存在一段可配置的时间。消费者也不直接从队列里"取走"消息,而是像翻一本账本一样,维护一个自己的指针,记录自己上次读到哪一行,下次接着往下翻。你翻过的内容不会消失,后面新来的人还可以从第一页开始翻。
这个"账本模型"是Kafka所有行为的根源。它带来的三个直接好处是:
- 写入路径极其简单,每条消息永远追加在文件末尾,不修改旧数据,顺序写对磁盘非常友好;
- 消费是"非破坏性"的,任何一方读数据都不会影响别人,同一份数据可以同时提供给无数个下游;
- 数据可以回溯,新加入的消费者可以把指针拉回很久以前,实现"从某个时间点开始重放"。
这就是为什么Kafka在很多场景下不只是消息队列,而是被当作一套数据管道或事件总线来用。新版本的Kafka已经支持KRaft模式,可以不再依赖独立的协调组件来管理元数据,但上面这套账本模型始终没有变。
1.2 四个核心概念在一条链路上的分工
在逐个展开之前,我们先给四个概念各定一个"职位",这样后面不会迷路:
- Topic:按业务把数据分门别类的逻辑名字;
- Partition:Topic的物理切分单位,真正存消息、被并发读写的对象;
- Replica:Partition在别的机器上的冗余拷贝,负责高可用;
- Consumer Group:消费端用来分摊这些Partition的"团队编制"。
一句话关系是:一个Topic被切成多个Partition,每个Partition配几个Replica,一个Consumer Group里的每个消费者负责其中某几个Partition。后面所有内容,都是在给这句大白话补上细节。
2. Topic:业务分类层,消息的第一道分流
2.1 Topic不是队列,更像数据库里的表
一个Kafka集群同时跑着订单数据、日志数据、指标数据和用户行为数据。如果所有消息全部混在一个大池子里,下游根本没有办法分辨哪条消息属于哪个业务。Topic就是用来解决这个问题的第一层标签。
我的建议是把Topic理解成数据库里的表,而不是队列。你建表时会考虑哪张表存什么业务数据,Topic也一样。你在代码里往Kafka发消息,第一件事永远是指定Topic名字;消费者订阅的时候,也是指定一个Topic或者一组Topic来读。Topic在逻辑上就是一个分类命名空间,它本身不直接持有数据文件。
物理上你甚至找不到一个叫"order-events"的目录。真正占磁盘的是它下面的一个个Partition子目录,通常长这样:
/data/kafka/order-events-0/ /data/kafka/order-events-1/ /data/kafka/order-events-2/Topic和Partition的关系,像是一本书和这本书被拆成的几十个分册,封面上写着书名,但翻开之后每一册是独立编页码的。
2.2 创建Topic时的那两个参数到底在说什么
创建一个Topic的典型命令是:
kafka-topics.sh --bootstrap-server localhost:9092 \ --create --topic order-events \ --partitions 3 --replication-factor 2这里的--partitions 3表示把这个Topic的数据拆成3个分片,--replication-factor 2表示每一份分片数据要存2个副本。这两个参数要一起理解,因为Partition决定的是存储和并发的颗粒度,Replica决定的是容错和可用的下限,两者独立作用于每个Partition,而不是作用于整个Topic。
我要特别提醒一个很多人踩过的坑:很多测试环境默认开了auto.create.topics.enable=true,这意味着你生产消息到一个不存在的Topic时,Kafka会静默地自动创建一个Topic,默认往往只有一个分区、一个副本。你当时发消息看着是成功的,后面真上生产了describe一看,PartitionCount是1,ReplicationFactor是1,性能上不去,Broker挂了数据还全丢。所以生产环境建议显式关闭自动创建,或者至少把自动创建的默认分区数调大。
另外一个容易忽略的点是:Topic的删除默认是关闭的(delete.topic.enable=false)。很多人在测试环境建了一堆垃圾Topic想清理,发现删不掉,就是这个默认参数在起作用。这类"写时爽、删时难"的属性,决定了建Topic之前要想清楚。
2.3 Topic命名的粒度与实操习惯
实际项目里Topic的粒度很考验划分能力。我见过一个后端团队把所有业务事件全部塞进一个叫app-events的Topic,下游想消费订单事件,就必须把其他所有类型的事件也一起拉下来,然后在消费者里自己过滤,理由很简单:图省事。短期看确实少建几个Topic,长期看每加一个下游都要重复拉取全量数据,带宽和过滤成本一直在翻倍。
反过来,有人把非常细碎的小事件也单独建Topic,比如一个系统里有几十种一两百条一天的小事件,每个都建一个Topic,结果集群里几十个Topic,大部分都是个位数Partition、几乎没流量,管理成本和元数据压力一点都不少。
比较稳妥的划分方式是"业务域+事件类型",例如order-events、payment-events、user-login-events,再配合命名规范把环境也带进去,比如prod-order-events、test-order-events。粒度上,同一个业务域内、消费团队基本一致的事件可以合在一个Topic里;消费团队不同、保留策略不同、数据隔离要求高的场景,分开建Topic。没有一个公式能套用所有情况,但"下游怎么消费"应该比"上游怎么产生"更优先决定Topic的划分。这个原则在Kafka场景下非常关键,因为Topic的粒度直接影响每个Consumer Group的拉取成本和过滤负担。
3. Partition:存储与并发的基本单位,顺序只在分区内成立
3.1 Partition在磁盘上长什么样
Partition是Kafka里真正负责存数据、承担读写并发的实体。每个Partition在逻辑上是一条不断追加的有序日志,在磁盘上对应一个目录,目录里是若干个段文件:
/data/kafka/order-events-0/ 00000000000000000000.log 00000000000000000000.index 00000000000000000500.log 00000000000000000500.index这个目录里的每一条消息都有一个从0开始不断递增的offset,也就是这条消息在这个Partition内的序号。消费者通过保存"我读到哪个offset了"来记住消费进度。每个Partition独立执行追加写、独立维护offset范围、独立做数据过期清理,相互之间完全不共享状态。
这也是Kafka能高吞吐的根本原因之一:它在一个Partition内部做到了串行顺序写,但让多个Partition之间并行工作,相当于把一个大账本拆成很多小本子,每个小本子各有一支笔在写,互不争抢。
3.2 消息怎么决定进哪个Partition
生产者发消息的时候,可以带一个key,也可以不带。
- 不指定key时:默认分区器会把消息尽量分散到各分区。老的实现是轮询,新版本引入了分区粘性机制,本质都是让消息均匀分布在各个分区,避免某几个分区过热。
- 指定key时:对key做哈希运算,再对分区数取模,得到的值就是目标分区号。
这意味着同一个key的消息,一定会进入同一个Partition。比如你不断发送某个订单ID的状态变更事件,只要key一直用同一个订单ID,这些事件就永远集中在同一个Partition里,而且是按发送顺序排列的。下游消费者如果只关注"同一订单的状态变化要有序处理",只要按Partition消费,就天然有序。
这里还藏着一个很容易错的点:Kafka的分区分配只保证"相同key落在同一分区",并不保证"同一key在分区里的位置固定不变"。一旦Topic的分区数变了,相同key取模的结果也可能变,原本聚集在同一个分区的数据就可能被拆散到不同分区。所以对一个依赖局部有序的业务来说,扩容分区是一项需要谨慎评估的操作,不是想加就加。
3.3 分区内有序,不等于Topic全局有序
Kafka对顺序性的承诺非常克制:它只保证Partition内部有序,不保证整个Topic有序。这个克制的来源是工程妥协。如果要求全局有序,等于整个Topic只能有一个Partition,所有消息都得排成一列顺序写,那并行能力和吞吐基本就废了。
很多初学Kafka的人会纠结:"我的业务要求所有消息严格按时间顺序处理,Kafka能做吗?"能做,但你需要自己设计key。比如一个订单的生命周期事件,把订单ID作为key,就能让这条订单的所有事件进入同一分区、按序到达;不同订单之间的事件本来就不需要严格排序,它们完全可以并行处理。当你要求全局有序的时候,实际上绝大多数场景你只是要求"同一个业务实体的局部有序",认清这一点能少走很多弯路。
3.4 分区数量:拍脑袋可不行
分区数不是越大越好。分区太少,并行度和吞吐上不去;分区太多,也会带来一堆副作用:
- 每个Partition在Broker上都有对应的文件句柄、内存缓冲和索引;
- 消费者扩容和rebalance时涉及的分区分配计算会变重;
- 元数据量变大,集群管理压力上升。
实际操作中,我建议用这个思路估算分区数:
峰值每秒消息数 × 峰值系数 -------------------------- = 最少分区数 单分区实际吞吐能力峰值系数一般取1.5到3倍缓冲。单分区吞吐能力跟消息大小、磁盘类型、网络带宽都有关系,不能一口咬死。经验区间上,SSD加千兆网、消息体积在几十KB以内时,单分区每秒几千条消息是常态,消息再小一点还能更高,但每个环境差异不小,最靠谱的办法是拿自己的消息体做一次压测,再回来填这个公式。
还要考虑消费者实例数。一个普遍建议是分区数不要小于你预期的消费者并发数,因为每个分区同一时刻只能被同一个消费组里的一个消费者分配。你想要灵活扩容消费端,分区数就得留出余量。总原则是:分区数要能吃掉峰值流量,还要留出消费端扩容空间,但不要盲目追求几百个分区。毕竟这是一个建完之后想减少就很麻烦的配置项。
3.5 数据什么时候被清理
Partition里存着有保留期限的数据,你消费完了它还在,那它什么时候走?这取决于保留策略。
最常用的是时间策略retention.ms,消息在Partition里存够指定时长(默认7天)后会被清理。另一个是大小策略retention.bytes,整个Partition的数据总量超过指定值后,最早的日志段会被删。还有一种不太一样的清理方式叫日志压缩(log compaction),它不是按时间删数据,而是保留同一个key的最新一条消息,把旧版本回收掉。日志压缩很适合存"用户最新状态""配置最新值"这类数据,它让Kafka不只是消息队列,还能当一套紧凑的KV存储来用。
这里要记住一个核心点:清理的单位是Partition,不是Topic。你对Topic做配置,实际作用在每个Partition上。
4. Replica:副本不是复制Topic,而是复制Partition
4.1 Leader与Follower,读写为什么都走Leader
单机存一份数据是不安全的,Kafka的应对方式是给每个Partition配多个副本,分散在不同的Broker上。注意,副本机制的单位是Partition,ReplicationFactor=3的意思是每个Partition都存3份,而不是整个Topic有3个共享副本。
每个Partition的多个副本里,通过选举产生一个Leader,其余的是Follower。生产者和消费者只跟Leader打交道,Follower无用武之地地默默做一件事:从Leader拉取消息,把自己这份数据同步到和Leader一致。
很多从关系型数据库转过来的人会疑惑:为什么不搞"读写分离",让Follower分摊读压力?Kafka设计上刻意不做这件事。因为一旦允许从多个副本读数据,不同副本之间必然存在同步延迟,读到的数据可能不一致,还会破坏"按offset顺序读"的语义。让单点写入、单点读取,换来的是清晰的分区内线性一致性,代价是Follower在正常情况下确实只承担备份功能。这是Kafka为了工程简单和高吞吐做出的重要取舍。
4.2 ISR:哪些副本算"跟得上"
同步副本不是永远固定的。Kafka引入了一个概念叫ISR(In-Sync Replicas),翻译过来就是"跟得上Leader的副本集合"。
每个Follower都在后台不停地把Leader上的数据拉过来写到自己本地,谁的进度落后太多,谁就会被踢出ISR。反过来,一个落后的Follower如果慢慢追上来了,又可以重新进入ISR。ISR的构成变化会直接影响生产者的写入确认,这一点在下一小节展开。
如果Leader所在的Broker宕机了,Kafka会从ISR里选一个新的Leader。这里有个关键参数叫unclean.leader.election.enable,翻译过来就是"是否允许从不同步的副本里选Leader"。默认是false,也就是宁可等ISR里剩余的副本恢复,也不让一个数据落后的Follower顶替当Leader,因为那样会丢数据。有些人为了可用性把它设成true,数据安全就要打折扣,这是一个明确的权衡,不是可以随便开的开关。
4.3 acks与min.insync.replicas:可靠性是一笔交易
生产者发消息时可以指定acks参数,它决定了消息写到什么程度,服务端才回复你"成功了"。整套语义可以看这张表:
| acks值 | 语义 | 风险 | 适用场景 |
|---|---|---|---|
| 0 | 发出即算成功,不等服务端任何回执 | 消息可能直接丢失 | 可丢失的高吞吐日志埋点,不推荐日常使用 |
| 1 | Leader写成功即可,不等Follower | 如果Leader在Follower同步前宕机,消息可能丢 | 大多数对丢数据不太敏感的业务,默认值 |
| all | ISR里所有副本都写成功才返回 | 写入吞吐受最慢副本拖累 | 订单、支付、账户等关键数据链路 |
很多人以为设了acks=all就万事大吉了,这是个大误区。acks=all只是保证"所有ISR副本确认写入",问题是:如果ISR里只剩Leader一个副本呢?那acks=all就退化成acks=1,数据照样可能丢。
所以生产环境的关键数据一般会再配一个min.insync.replicas,比如设为2。它的意思是:写入消息时,ISR里至少要保证有2个副本,如果当前ISR只剩一个副本了,写入就直接报错,不允许成功。这样配合起来才真正有了冗余容错。
实践中还有个反向的权衡容易被忽略:acks=all加min.insync.replicas=2让数据更安全,但也让可用性变差。因为一旦有两个Broker同时挂掉,ISR不足2个,Kafka宁可拒绝写入也不肯冒着丢数据的风险让你成功。安全和可用在分布式系统里经常是打架的,Kafka把这个选择的旋钮留给了你。
4.4 副本数设置:3副本是默认答案吗
常见的经验是:生产环境副本数设3,测试环境设2,单机体验设1。这个经验基本可靠,前提是你的Broker数量能撑得住。副本数不能超过Broker数,比如集群只有2台Broker,你非要配3副本,创建的时候就会失败或者分配出"某些副本挤在同一台机器上"的结果,那副本就失去意义了。
副本多带来的不只是磁盘翻倍,还有写入放大和同步开销。一个Topic如果流量巨大且对数据丢失不敏感,可以只配2副本或1副本,省下来的磁盘和带宽可以分给重要的业务。我在实际项目里见过把消费进度、临时计算结果这类可重建数据放在1副本Topic里的,没什么问题。副本数的取舍跟业务的重要程度直接挂钩,而不是"越高越好"。
5. Consumer Group:消费端的水平扩展与故障转移单位
5.1 一个分区同一时刻只能被组内一个消费者消费
Consumer Group是Kafka消费端最基本的组织方式。一组**相同group.id**的消费者实例聚在一起,共同订阅同一个Topic,Kafka负责把Topic下的所有分区分配给组里的每个消费者。
组内分配遵循一条铁律:一个分区在同一个时刻,只能被这个组里的一个消费者实例认领。这个"一个分区一个消费者"的机制,是Kafka消费端并发模型的地基。
由这条铁律可以推出一个直观结论:如果组里有4个消费者,Topic有4个分区,那每个消费者认领一个分区;如果Topic只有2个分区,那组里4个消费者里会有2个原地空转,什么分区都分不到;如果Topic有8个分区,那组里4个消费者每人认领2个分区。
换句话说,消费者数量一旦超过分区总数,多出来的消费者不再有任何作用。这是初学Kafka时最容易被"加消费者提速"这种普遍直觉误导的地方。
同时还要区分两种消费模式:
- 消息队列模式:一个Topic只被一个Consumer Group消费,每一条消息只被组内一个实例处理,天然实现了负载均衡;
- 发布订阅模式:同一个Topic被多个Consumer Group各自独立订阅,每个组都能拿到这份数据的完整拷贝,互不影响。
模式之间唯一的区别只是Consumer Group的数量。这也是Kafka最巧妙的设计之一。
5.2 Rebalance:组内成员变化时发生了什么
消费者组不是静悄悄的。一旦发生下面几种情况,Kafka会让组内所有消费者暂停工作,重新分配分区:
- 组内有消费者实例新增或退出(包括崩溃);
- 订阅关系变化,比如重新调用了subscribe方法;
- Topic的分区数增加或者被删除。
这个过程叫Rebalance。在Rebalance发生的瞬间,所有消费者都会停止消费,因为分区的归属权被整体重算了。等到分配完成,大家再继续工作。频繁的Rebalance是消费端最常见的性能杀手之一。
最常见的原因是消费者处理速度跟不上。max.poll.interval.ms这个参数默认是5分钟,Kafka要求消费者每次poll之后要在规定时间内再发起下一次poll。如果你的业务逻辑每批消息要处理好几分钟,超过了这个上限,Kafka就会判定这个消费者"病危",把它移出消费者组,然后触发一次Rebalance,让其他消费者接手它的分区。老版本还有更短的会话超时时间,更容易因为网络抖动或者GC停顿引发误判。处理慢不是最主要的解决方式,你既可以把max.poll.interval.ms调大,也可以调小每次poll拉回来的消息条数max.poll.records,让单批处理更快。增量式的协作式Rebalance协议也能减少全组停顿的时间,但那是更后面的优化话题了。
5.3 offset到底存了什么:"消费进度"与重复消费的由来
消费者在Partition里的进度叫做offset,Kafka会把每个Consumer Group的消费进度保存在一个内部的__consumer_offsets主题里。消费者每次poll一批消息,处理完以后,要把"下一条该从哪个offset继续读"提交上去,这就是位移提交。
默认情况下,enable.auto.commit=true,Kafka每5秒自动提交一次位移。很多人对这个"自动提交"的时机有误解,它不是"处理完一批就提交一批",而是在下次poll的时候,把上一轮已经拉取到的最大offset提交上去。这意味着,如果你的进程在处理完一批消息之后的5秒内崩溃了,Kafka记录的位移还没推进到最新,重启之后就会把上一批已经处理过的消息再读一遍。
这是Kafka最典型的"至少一次"消费语义——消息不会丢,但可能重复。如果要实现"最多一次",就需要关闭自动提交,先手动提交位移,再处理消息;代价是处理过程中一旦崩溃,消息就丢了,而且丢的不止一条。几乎所有主流业务都会接受"至少一次"并配合在消费端做幂等,这是我在这个环节最想强调的经验:把消费处理做成幂等的,比纠结能不能精准不重复消费重要得多。
5.4 消费组的常用参数与实践建议
最后给你几个实际使用中常调的参数:
group.id:一个应用服务在Kafka里的唯一消费者组标识,同一服务的所有实例必须一致;session.timeout.ms:消费者在会话超时之前必须发出心跳,否则被判定死了;max.poll.interval.ms:处理逻辑允许的最大间隔,超过会被移出组;max.poll.records:单次poll返回的最大消息数,控制单批处理时长;auto.offset.reset:新组或者offset失效时从哪里开始读,earliest表示从最早开始,latest表示从最新开始。
auto.offset.reset只在一个组没有任何已提交位移、或者已提交位移对应的消息已被清理时生效。很多人在消费者重启后希望"重新从最早读一遍",却发现它总是从上次的位置继续,原因就是你的组已经有位移记录了,这个参数根本没机会参与判断。
实践上的建议很简单:让生产环境消费组里的实例数量与分区数对齐,至少让实例数不大于分区数。这样既不会浪费实例资源,又能保留未来扩容的空间。对于流量波动大的业务,消费端扩容前先确认Topic分区数够不够,别以为起一堆消费者进程就能无限加速——分区数才是天花板。
6. 把四大概念串起来:一条订单消息的完整旅程
6.1 用一段代码看生产到消费的链路
概念说完了,我们用一段极简的代码走一遍完整链路。生产端发送订单事件:
// 订单事件,key用订单ID ProducerRecord<String, String> record = new ProducerRecord<>("order-events", orderId, orderJson); // 发送时,Kafka会按orderId哈希决定进哪个分区 producer.send(record);消费端启动一个消费者组,订阅同一个Topic:
consumer.subscribe(Arrays.asList("order-events")); while (true) { ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000)); for (ConsumerRecord<String, String> record : records) { // 这时你已经拿到一条完整消息 String orderId = record.key(); String value = record.value(); long offset = record.offset(); int partition = record.partition(); handle(orderId, value, offset, partition); } }当这条消息从生产者发出后,它首先按照orderId的哈希落进order-events这个Topic的某个Partition里。这个Partition有一个Leader副本在某个Broker上接收写入,同时还有若干Follower副本在后台同步这份数据。如果生产者配置了acks=all,那么要等ISR里所有副本都确认写入,这次发送才算成功。
消息写完后,它在每个副本的日志里被追加到了最新位置。此时,你的消费者组里的某个实例会被分配到包含这条消息的那个Partition,它从自己的offset开始一路读过来,读到这条消息,处理完业务逻辑后,再把位移提交回__consumer_offsets,标志着这条消息在这个组消费成功。
整个过程,Topic负责告诉Kafka"这是哪一类消息",Partition负责"具体存在哪个文件里"并决定并发和顺序,Replica负责"万一有一台机器挂了数据还在",Consumer Group负责"团队里具体谁去处理它"。
6.2 一张对照表看清四个概念的职责
把整篇文章压成一张表,方便你随时回来复习:
| 概念 | 作用层级 | 并发/扩展单位 | 高可用单位 | 顺序性单位 |
|---|---|---|---|---|
| Topic | 逻辑命名层 | 不承担并发能力 | 不承担容错 | 无明显顺序语义 |
| Partition | 物理存储层 | 读写并行的基本单位 | 副本机制的作用对象 | 分区内严格有序 |
| Replica | 数据冗余层 | 不直接承担读写扩展 | Leader/Follower加ISR容错 | 跟随所在Partition |
| Consumer Group | 消费组织层 | 组内负载均衡 | 组内成员故障转移 | 按分区消费进度跟踪 |
6.3 新人最容易混淆的三个点
最后,我把平时带人时几乎必问的三个混淆点再摆出来,你如果能立刻说清楚,这四个概念就真的过关了。
第一,"Topic配了3副本"这个说法是否正确?正确。但准确说法是"Topic的每个Partition都配置了3个副本"。关键区别在于是对Partition生效,而不是Topic整体共享一份拷贝。
第二,"消费者越多,消费越快"是否成立?有条件成立。分区数量是消费并发天花板,组内消费者数量一旦超过分区总数,多出来的消费者只能空转。提高吞吐前先想清楚分区数够不够。
第三,"Kafka能保证消息有序"是否正确?只说对了一半。Kafka保证的是分区内有序,不是全局有序。业务里想让同一订单、同一用户的消息有序,就把业务ID作为key发给同一个分区。真正要求全局有序的场景在分布式系统中极其罕见,如果有,你应该先反思架构。
这三点想明白了,你再看Kafka的配置、监控指标、运维文档,会发现很多曾经像黑话一样的术语都能对号入座。Partition数为什么不能随便加,副本副本为什么对延迟有影响,消费端为什么偶尔停顿——它们背后全是这一套概念在互相牵制。建议你下一步可以自己动手做一个实验:创建一个8分区的Topic,启一个消费者组,先跑一个消费者实例,再启动第二个,观察一下现有消费者的分区分配变化,再故意停掉一个进程,看组里是否会自动恢复。亲眼看到一次Rebalance的发生,比读多少遍文档都管用。