
做Kafka运维的朋友大概率都遇到过这类场景新建一个topic分区数和副本数明明都是按规范填的但broker上具体怎么分布往往只有翻到“分区副本分配规则”这个地方才算真正搞明白。等集群规模上来这个规则就会成为绕不过去的坎——它直接决定了数据能不能扛住单机故障、读写流量会不会凑到同一批机器上、扩缩容之后需不需要手动搬数据。这篇文章我想把Kafka分区副本分配规则这件事讲透。不光是给结论还会把默认分配器的计算逻辑、机架感知分配的原理、手动重分配的实操流程以及我在实际运维里踩过的坑一起整理出来。无论你是刚接触Kafka的新人还是正在为leader倾斜、磁盘水位不均头疼的维护者按这个思路排查和操作基本都能找到方向。1. 分区与副本先搞懂分配规则到底在分配什么1.1 分区Partition并行、有序与故障恢复的基座在聊分配规则之前得先把Kafka的底层模型捋清楚。一个topic在逻辑上是一个主题但在物理上会被切成多个分区Partition。每个分区是一个有序的消息日志新消息不断追加到这个日志的末尾消费者读取的时候可以按偏移量定位。分区与分区之间相互独立所以同一个topic里的消息可以分散到多台broker上并行读写——这正是Kafka能撑住海量吞吐的核心原因。你可以把分区理解成一个大仓库被拆成了若干个储物间储物间分布在不同的楼层broker。如果所有东西都堆在同一个储物间搬运工干活再快也有限拆成多个储物间并且分散在不同楼层就能让多个搬运工同时干活。Kafka里的“多个搬运工”就是多个消费者线程或消费者实例它们通过分区把负载分摊开来。分区还有一层不太起眼但很关键的作用它决定了故障恢复的粒度。假设一个topic有12个分区每台broker上放了3个某台broker宕机时只需要把这3个分区在其他broker上重建或切换主副本剩下的9个分区完全不受影响。分配规则如果不合理比如把某个topic的全部分区都塞到同一台broker上那这台机器一挂整个topic就彻底不可用了。所以分区数量以及分区在broker间的分布是Kafka可用性设计的第一块地基。1.2 副本ReplicaLeader与Follower的分工分区解决了并行问题但不解决数据安全问题。裸奔的单分区架构一旦broker磁盘损坏消息就永久丢失。所以Kafka给每个分区设计了多个副本Replica每个副本的内容保持一致。副本之间有明确分工其中一个副本是Leader负责接收生产者的写入请求和消费者的读取请求其余的副本是Follower只从Leader那里异步拉取消息保持数据追上最新的进度。这个“一主多从”的模型乍一看有单点风险实际上恰恰是它的容错设计。假设一个topic配置了3个副本分布在3台broker上任何一台broker宕机只要剩下两台里有一台的数据是完整的集群就能从Follower中选出一个新的Leader继续对外提供服务。你损失的只是那台宕机机器的资源而不是整个分区的可用性。这里有个经常被忽略的点副本的分布位置和数量比副本本身更重要。3个副本放在同一台broker上和1个副本没有本质区别照样是单点故障。所以Kafka的分配规则在生成副本列表时会尽量避免同一分区的多个副本落在同一台broker上。这是副本分配的第一条铁律后面讲分配算法时你会反复看到这个原则。1.3 ISR机制分配规则里看不见的数据安全阀副本之间是异步同步的异步就意味着Follower可能暂时落后。Kafka没有要求所有副本都必须和Leader完全一致才能算“健康”而是维护了一个动态集合ISRIn-Sync Replicas同步副本集合。只有跟Leader保持同步的副本会留在ISR里落后太多的会被踢出去等追上进度后再加回来。ISR这个概念和副本分配的关系很微妙。分配规则决定了一个分区的副本初始落在哪些broker上但ISR决定了这个分区的数据安全底线。生产环境里常见的配置组合是副本因子为3、min.insync.replicas2意思是写入必须至少被ISR里的2个副本确认才返回成功。这样设计之后即使1台broker彻底宕机ISR里仍然还有至少2个副本不会出现“只有Leader一个副本确认数据随机器一起丢掉”的惨剧。从分配规则的角度看ISR还影响另一个操作副本重分配。你在执行kafka-reassign-partitions时新副本要加入、旧副本要退出这个过程本质上就是把分区从一个ISR迁移到另一个ISR。如果迁移过程中网络抖动或者负载过高ISR会频繁收缩、扩容严重时会导致集群出现UnderReplicatedPartitions告警。所以后面讲重分配时ISR的状态也是你判断操作是否成功的核心依据。2. 默认分配规则到底是怎么算的2.1 创建Topic时副本是怎么轮询分配的Kafka创建topic的时候默认分配器会按照一套固定的轮询策略为每个分区生成一个broker列表。这里以最经典的分配逻辑为例假设集群有4个broker编号是0、1、2、3你创建一个6分区、2副本的topic分配结果大致是分区ID副本所在broker0[0, 1]1[1, 2]2[2, 3]3[3, 0]4[0, 1]5[1, 2]看出来规律了吗起始broker的编号基本就是分区ID对broker数量取模的结果分区0从broker0开始分区1从broker1开始分区2从broker2开始分区3从broker3开始分区4又回到broker0。确认副本时从起始broker往后依次加1第2个副本落在起始broker的下一个broker上。这样整个集群看下来每个分区的副本都分散在不同broker上不会出现两个副本挤在同一台机器的情况。这里的“轮询取模”是默认分配规则的核心思想但实际代码比这复杂一点。默认分配器会做两轮分配第一轮先保证每台broker都能拿到至少一个分区的副本第二轮把剩余分区继续轮询补齐尽量让各broker上的分区副本总数相差不超过1个。最终的效果就是所有broker上的总副本数基本均衡单个分区的多副本又彼此错开。这也是为什么你在一个配置合理的集群里看kafka-topics.sh --describe的输出会发现每个broker上的分区副本数量大体平衡。2.2 Leader副本为什么要占第一个位置在默认分配结果里副本列表的第一个位置是Leader副本剩下的才是Follower副本。这个细节看起来不起眼实际上决定了leader分布在哪些broker上。再回到上面的例子6分区2副本的topicleader分别落在broker0、1、2、3、0、1。如果你只创建了一个topic这个分布没问题。但现实中你会创建几十个甚至上百个topic每个topic的分区数还各不相同。如果大量topic的分区数都恰好和broker数量形成某种整除关系leader就可能集中到固定的几台broker上。举个例子集群有5台broker你连续创建了5个topic每个topic都是10个分区。默认规则下分区ID从0开始leader按0、1、2、3、4循环看起来是均匀的。但如果你其中几个topic的分区数设置成了能被5整除的数且分区数是5的偶数倍情况就会微妙起来。更常见的问题是某台broker宕机后它上面的大量leader被其他broker接管等它恢复后除非Kafka重新进行leader平衡默认不会自动做否则这些leader就一直压在其他broker上形成持久性的leader倾斜。理解了“副本列表第一个位置是leader”你就知道手动重分配的时候可以通过调整副本列表的顺序来让leader分布更均匀。这个操作我在第3部分会详细讲。2.3 机架感知分配从单机故障到机架故障默认的轮询规则只考虑了broker之间的分散没有考虑物理机架或者云上的可用区这一层。如果你的broker分布在多个机架上而某个分区的两个副本刚好落在同一个机架的两台机器上一旦这个机架断电或交换机故障这两个副本就同时挂了分区可用性直接归零。开启机架感知Rack Awareness之后分配器的目标就变了它不仅要让副本分散到不同broker还要让副本分散到不同机架。实现方式是在每台broker的配置文件里设置broker.rack创建topic时分配器会按“机架-副本”的strided方式分配。具体逻辑不展开源码但核心行为可以概括为如果有3个机架每个机架2台broker你创建一个3副本的topic3个副本会被分别放到3个不同的机架上每台机架只承担其中一个副本。如果副本数大于机架数怎么办比如3个机架、5个副本分配器会优先保证每个机架至少有一个副本然后多余的副本在同一机架内选择不同的broker摆放。换句话说机架感知能避免“机架级”单点但无法保证机架内一定只放一个副本。所以分配规则的实际效果取决于你集群里机架数量和副本因子的匹配情况。开启机架感知之后默认分配算法的起始broker选择逻辑也会变化它不再简单按分区ID取模而是先按机架排序再在机架之间轮询。这意味着你在计算分区分布时不能套用上一节的简单公式否则会算错。最简单的方式是创建topic后用kafka-topics.sh --describe直接查看分配结果而不是自己拍脑袋推。2.4 分区数与副本因子分配规则的两个输入参数分配规则的所有计算都建立在两个输入上分区数和副本因子。这两个值一旦定下来分配结果的理论形态也就基本定了。分区数决定了topic的并行度上限。分区越多生产者可以并行写入的通道越多消费者也能开更多线程去消费。但分区不是越多越好每个分区在集群里都有对应的元数据和日志文件分区太多会拉高broker的内存占用、文件句柄数还会让leader切换的耗时变长。分区副本的分布越散协调器做管理操作的代价也越大。经验上单个broker上的分区总数控制在2000以内是比较稳的区间跨过这个门槛各种异常会开始冒头。副本因子决定的是数据冗余度和可用性。副本因子为2在磁盘利用率和安全性上是个尴尬的数字——它能容忍一台broker宕机但如果这台宕机的broker恰好是某个分区的唯一同步副本ISR里只剩1个数据就有丢失风险。生产环境的核心业务数据建议副本因子为3内部topic如__consumer_offsets也建议保持3测试环境可以用2或者1但别把测试环境的配置直接照搬到生产。这两个值的组合还直接影响分配均匀性。比如副本因子等于broker数量时每个分区的副本会覆盖所有broker这是最理想的拓扑副本因子大于broker数量时同一broker上会出现同一个分区的多个副本这属于配置错误要尽量避免。3. 什么时候需要手动重分配3.1 什么场景需要手动重分配副本默认分配规则只在创建topic时生效它不会感知后续集群的变化。所以下面几类场景几乎必须手动介入。第一种是broker扩容或缩容。集群新增了一台机器已有topic的分区不会自己迁移过去新topic的分配会用到新broker但老topic仍然挤在旧机器上。执行缩容时目标broker上的分区必须提前搬走否则下线操作会直接把数据风险暴露出来。第二种是副本因子调整。业务上线之初副本数配少了现在想把副本因子从2提到3只靠默认分配器不会给你补副本必须走重分配流程。第三种是leader倾斜或磁盘水位不均。默认分配算法在创建时能做到均匀但经过迭代发布、节点故障之后实际分布会被打乱。比如某台broker上总副本数明明不多但磁盘使用率明显偏高这时候就值得看一下是不是个别大分区堆在上面。第四种是数据搬移。有些topic访问量大想把它们统一放到SSD盘的broker上或者把冷数据挪到大容量机械盘节点这同样要依赖重分配完成。手动重分配的核心工具是官方命令行脚本kafka-reassign-partitions.sh它做的事情就是生成一份“分区副本目标分布方案”然后指挥broker把副本一个个搬过去。下面我按步骤拆开讲。3.2 重分配文件JSON结构搬家清单怎么列在执行重分配之前你需要准备一份JSON文件它描述“每个分区的副本最终应该在哪几台broker上”。这是整个操作的蓝图格式如下{ version: 1, partitions: [ { topic: user-topic, partition: 0, replicas: [2, 1, 0] }, { topic: user-topic, partition: 1, replicas: [3, 2, 1] } ] }字段很好理解topic是topic名partition是分区IDreplicas是目标broker ID列表顺序就是leader在前的副本顺序。这份文件描述的是“最终状态”而不是“增量变化”。执行重分配时Kafka会比较当前分布和目标分布自动计算出需要新增哪些副本、删除哪些副本然后逐步搬移。这里有个特别重要的规则replicas列表里的broker不要重复而且副本数要和当前副本因子保持一致。如果你把目标副本数写少了Kafka会删除多余副本写多了则会新增副本并同步数据。重新生成副本因子的时候目标副本数就是你想要的新副本数。手工写这个JSON容易出错尤其是topic数量多的时候。所以实际操作中一般先用脚本生成方案再人工微调不要从零手写。3.3 kafka-reassign-partitions三步走生成、执行、验证完整流程可以分成三步。第一步是用--generate生成候选方案。假设我要把user-topic迁移到broker 0、1、2、3上先准备一个topics-to-move.json{ topics: [ {topic: user-topic} ], version: 1 }然后执行kafka-reassign-partitions.sh \ --bootstrap-server localhost:9092 \ --topics-to-move-json-file topics-to-move.json \ --broker-list 0,1,2,3 \ --generate脚本会输出两个JSON块一个是Current partition replica assignment也就是当前的实际分布另一个是Proposed partition reassignment configuration这就是它建议的目标分布。把建议的那份保存成reassignment.json不要直接用先人工检查一遍。检查的重点是leader分布是否均匀、是否把副本放到了你真正想放的broker上。如果机器配置有差异比如某台broker磁盘较小你可以调整JSON里对应的replicas顺序手动把副本从大盘机器挪到小盘机器。调整完之后执行第二步kafka-reassign-partitions.sh \ --bootstrap-server localhost:9092 \ --reassignment-json-file reassignment.json \ --execute执行命令会返回Successfully started reassignment of partitions之类的结果但它不代表迁移已经完成。副本同步是异步的可能需要几分钟甚至几小时取决于分区大小和网络带宽。第三步就是轮询验证kafka-reassign-partitions.sh \ --bootstrap-server localhost:9092 \ --reassignment-json-file reassignment.json \ --verifyverify输出里会显示每个分区的迁移状态。只有当所有分区都显示Completed successfully整个操作才算结束。期间如果某个分区一直卡在同步中需要结合ISR状态和网络负载找原因而不是反复--execute那样容易造成重复调度。3.4 重分配期间的限流与风险控制分区副本迁移本质上就是在broker之间复制数据。一个分区几十GB甚至几百GB同步过程会占用大量网络带宽和磁盘IO很可能拖垮线上业务。官方工具提供了--throttle参数可以限制每个broker重分配期间的复制流量上限单位是字节/秒。比如限到50MB/s可以这样执行kafka-reassign-partitions.sh \ --bootstrap-server localhost:9092 \ --reassignment-json-file reassignment.json \ --execute --throttle 52428800限流的效果是复制速度降下来但verify的结果不会立刻变成完成甚至会显示还在进行中。这是正常现象不需要慌张。限流太大的时候你还能在broker的JMX指标里看到复制流量曲线。我的习惯是把限流值设成集群峰值带宽的20%左右等业务低峰再跑到50%而不是一口气拉满。还需要注意一点如果执行过程中某个分区一直处于复制状态检查源broker和目标broker之间是否有网络瓶颈、磁盘是否已经接近写满。磁盘写满是最容易被忽略的原因——目标broker上日志目录剩余空间不足复制任务会一直卡住ISR也迟迟不能扩展。所以动手重分配之前先给目标broker留出足够的磁盘余量容量评估时按“源分区大小的1.2倍”做预留比较保守。4. 常见问题与排查实录4.1 Leader分配不均默认算法也救不了leader集中在个别broker上是我在运维里遇到最多的“假均匀”问题。从分配结果看每台broker上的总副本数确实差不多但leader副本全压在固定的两三台上读写压力就集中在它们身上。原因往往不在于创建之初的分配算法而在于后续的故障切换。某台broker宕机后它上面的leader会被其他broker接管等它恢复回来follower副本虽然同步完了但角色不会再自动切换回去。如果没有开启自动leader均衡那么时间越长leader分布越发散。排查时先看每台broker上的leader数量。新版Kafka可以用kafka-leader-election.sh手动触发优先副本选举也可以先通过重分配把一些分区的副本顺序调一下让leader均匀分散。调整副本顺序的经验做法是把目标broker列表里的第一个位置leader位按轮询方式分配给不同的broker而不仅仅是把broker ID从小到大排。默认生成的JSON方案经常是升序排列这会导致leader也顺着broker ID的规律分布未必是你想要的。4.2 机架感知没生效的排查思路有的集群明明配置了broker.rack创建topic之后用describe一看同一个分区的副本还是落在了同一个机架里。这种情况通常不是分配算法失效而是配置没被正确加载。先确认broker配置文件里broker.rack的值是否真的写进去了最好在每台broker上分别执行grep broker.rack检查一遍不要用管理的“我以为配过了”代替实际验证。接着确认topic创建时有没有显式指定副本分配。如果用--replica-assignment参数或者直接传了一个分配JSON默认分配器会被完全绕过机架感知自然失效。这是非常隐蔽的坑一旦你在创建命令里加了--replica-assignmentKafka认为你“自己已经决定好了”不会再做任何机架优化。还有一个容易被忽略的点机架感知只在副本数大于1时才有意义。副本因子为1的topic怎么分配都无所谓机架因为它本身就没有跨机架容错能力。这类topic要么补副本要么接受它的风险而不是指望开启机架感知来解决。4.3 新Broker加入后分区不迁移集群扩容完成后很多人会顺手创建一批新topic然后惊讶地发现老topic的分区还是全在旧broker上。这不奇怪默认分配规则是“创建时生效”的策略不是“自动平衡器”。新建topic时新broker会参与分配但存量topic不会因为集群里多了机器就主动搬过去。想让老数据利用上新broker的资源只能手动重分配。做法和第3部分一样用--generate把目标broker列表改成包含新broker观察建议方案后再执行。如果你不想让所有topic都动那就只把负载高的那批topic做重分配控制操作范围。这里有个经验建议扩容之后不要急着把所有分区都重分配一遍先看新broker加入后集群的负载情况把真正高吞吐的topic迁过去就好全量迁移对集群的冲击往往大于收益。4.4 磁盘水位不均该看哪个指标磁盘使用率不均衡不一定只和分区副本数量有关。两个broker上分区副本数量一样但磁盘占用差距很大很可能是分区大小差异造成的。Kafka的分区大小不是均匀的有的分区积压了大量消息没被清理有的分区消费速度快所以日志文件很小。排查时可以用kafka-log-dirs.sh查看每个broker上各分区的磁盘占用kafka-log-dirs.sh \ --bootstrap-server localhost:9092 \ --describe --topic-list user-topic输出会列出每个分区的总大小和各broker上的副本大小。对比一下就能看出大分区堆在哪几台机器上。解决思路通常是把大分区重分配到磁盘余量大的broker或调整日志清理策略让消息及时过期。分配规则只能保证分布均匀保证不了磁盘倾斜后者更多要靠数据生命周期管理。4.5 分区数扩容后新分区分配不均线上topic分区数不够用执行kafka-topics.sh --alter --partitions 20之后发现新增的分区倒是分配上去了但老分区还是原地不动整体分布自然不均衡。这是预期行为不是bug。扩容操作只增加分区不重排已有分区。如果只是临时扛流量可以先扩容等业务高峰过了再针对这个topic做一次完整重分配把新旧分区的分布理顺。如果你希望扩容后所有分区重排一次那就需要自己构造一份包含所有分区目标位置的JSON交给重分配工具执行。扩分区之前最好先想清楚分区数大概率是只增不减的少了可以补但补完之后的老分区不会自动变位置所以首次创建时把分区数估得宽裕一点能省掉后续很多麻烦。5. 规划与运维建议5.1 分区数与副本因子的经验值参考给个可以直接抄作业的参考核心业务topic副本因子配3min.insync.replicas配2普通业务topic副本因子配3就够用别轻易降到2测试环境的topic副本因子可以设1但不要在测试环境验证“副本为1还高可用”这种伪命题。分区数方面没有万能公式但可以按两步估算。第一步用压测工具测出单分区在你们硬件条件下的生产吞吐上限比如单分区能达到10MB/s。第二步用业务峰值吞吐除以单分区吞吐再乘以1.5到2的冗余系数得到分区数。算出结果后再对照broker数量和单broker分区上限做校验。如果总分区数除以broker数量远大于2000就该考虑是不是集群规模不够先把broker数量提上来再说。5.2 自定义副本分配器的适用场景默认分配器在大多数场景下够用但它不感知磁盘容量、不感知broker负载、不感知网络拓扑差异。如果你有明确的需求比如希望某些topic的leader专门落在高性能机器上或者想按业务线物理隔离broker就需要自定义副本分配器。Kafka的副本分配器接口允许你继承默认实现并覆写分配逻辑。简单思路是创建topic时从AdminClient传入自定义分配器在当前broker列表基础上优先选择磁盘剩余空间大、leader数量少的broker作为leader再按机架感知规则安排follower。实现并不复杂但要注意这个分配器只对创建时生效运行中的topic不会自动调用它。另外自定义分配器如果写得不规范比如没有检查broker列表是否包含重复、没有处理机架信息缺失的情况创建topic就可能直接失败或生成不可用的副本列表。没有足够把握之前优先用默认分配器加手动重分配的组合比一上来就写插件稳得多。5.3 日常运维该盯哪几个分配相关指标分配规则相关的运维重点盯四个指标。第一个是UnderReplicatedPartitions它表示有多少分区的副本数低于配置值一旦持续大于0说明有的副本一直没同步到位。第二个是IsrShrinksPerSec和IsrExpandsPerSecISR频繁收缩扩张通常意味着副本同步在抖动可能是网络问题也可能是磁盘IO瓶颈。第三个是每台broker上的LeaderCount配合前面说的kafka-leader-election使用如果某台机器长期偏高就在重分配时把它的leader副本主动往前挪。第四个是broker的磁盘使用率和最大分区占用通过kafka-log-dirs.sh定期检查。这四项看下来基本能覆盖大部分分配规则引发的隐患。真正出问题时先看ISR状态再看网络和磁盘指标最后结合describe输出定位具体分区排查路径就能清晰很多。分区和副本的分配不是一个“配完就结束”的事集群会一直变化规则只是给了你一个起点后续的维护动作才是保障可用性的重点。