1. 分区策略牵扯的底层逻辑:性能与顺序性在这里分岔
做大数据开发这几年,我见过不少刚接触 Kafka 的人,一上来就问“分区策略怎么写”,结果被反问一句“为什么要分区”反而答不上来。说实话,分区策略不是孤立的面试知识点,它背后是 Kafka 的并行度模型、顺序性边界、可用性设计,以及生产集群里最常遇到的倾斜问题。不把底层逻辑理顺,照抄一段自定义 Partitioner 代码,多半会在扩容或消费端堆积时暴雷。
先建立一个基础认知:一个 Kafka Topic 可以分成多个分区(Partition),消息实际是追加到某个分区的日志文件里。每个分区内部会维护一个单调递增的 Offset,新消息永远写到尾部,消费者记录自己已经读到的 Offset。这样设计最直接的好处是:不同分区可以独立地分散到不同 Broker,读写也能并行展开,整体吞吐量才有提升空间。换句话说,分区是 Kafka 执行并行操作的最小单元,也是消息顺序性的作用边界。
这一点常常被理解错。有人以为“同一个 Topic 内消息全部有序”,实际只有“同一个分区内消息有序”;有人以为“有几个分区消费者就能起几个线程消费”,实际情况还要受消费组分配逻辑影响。消息根本不会因为 Topic 本身而自动均匀落到所有分区,它具体进哪个分区,完全取决于生产端的分区器(Partitioner)。分区器一旦设计不好,就可能出现三分区里只有一个分区被疯狂写入,剩下两个分区闲得发慌,消费端 Lag 却一直降不下去。
为什么要单独把分区策略拎出来研究?因为它同时影响三个层面:
- 生产层面:决定消息能否均匀分布、批次是否有效利用、Broker 磁盘和带宽压力是否均衡。
- 消费层面:决定同一个 Key 的消息顺序是否保留、消费组扩容后能不能吃到对应分区、某个分区积压时能否快速定位倾斜。
- 架构层面:决定当 Topic 扩容、Broker 宕机、分区 Leader 切换之后,系统是否能保持稳定,而不至于出现大量重复消费或乱序消费。
这类问题在真实环境里不是“能不能跑”,而是“能跑多久不出事”。比如很多业务喜欢把用户 ID 当作 Key 来保证同一个用户的操作有序,这本身没什么问题,可一旦某个超级大 V 用户的消息量占了全量流量的 30%,这个 Key 会固定路由到一个分区,其他分区再怎么扩容也救不了你。这就涉及选择分区维度时的取舍,我在后面第五节会用一个具体案例展开讲。
在往下深入之前,再用一个生活化的类比帮助建立直觉。把 Topic 想象成一家快递公司,分区就是若干条分拣通道,每条通道只有一个分拣员在看。你投快递时如果完全不管渠道,快递会被塞进当前最空的那条通道,这样通道利用率高;但如果某类包裹必须按顺序处理,比如同一个订单的多个退换货动作不能乱,就必须把同一个订单的所有包裹丢进同一条通道。这个“怎么决定丢进哪条通道”的动作,就是消息分区策略。丢错通道的后果,轻则处理负载不均,重则业务顺序被破坏。
所以我的建议是:动手配置 partitioner.class 之前,先花时间把分区的容量模型、顺序性要求和消费扩容路径画清楚,哪怕只用最朴素的表格列出来都行。想不清楚这些,策略写得再花哨也只是把问题往后推迟。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 默认分区器并不“随机”:消息落盘的完整决策链路
Kafka Producer 在发送消息时,并不会真的把每条消息立刻发给 Broker。它会先把消息放进内存里的 RecordAccumulator 中,等待攒成一个批次(Batch),再由后台 Sender 线程统一拉取发送。这样做的目的是减少网络小包,提升吞吐量。分区器的执行时机就是在消息进入 RecordAccumulator 之前,它决定这条消息到底放入哪个分区的批次队列。
很多资料把默认分区器形容成“有 Key 就按 Key 哈希,没 Key 就轮询”。这个说法在 Kafka 2.4 之前大致成立——旧逻辑里无 Key 消息会在每个批次到达后轮询切换分区,尽量打散。但从 Kafka 2.4 开始,社区改成了 Sticky Partition(粘性分区) 策略,含义是:在批次没有达到发送条件前,所有无 Key 消息会先被“粘”到同一个分区,尽量把该分区的批次填满,等这个批次被 sender 线程取走或者当前分区不可用后,再切换到下一个分区继续粘。
为什么默认逻辑会往这个方向演进?核心原因就是批次利用率。如果无 Key 消息频繁轮询切换分区,每条小消息都会被拆散到不同分区的队列,导致每个分区都有零散数据但都填不满一个批次,Sender 只能发出很多小批次。小批次意味着每次都要承担网络往返和磁盘顺序写成本,批量压缩也失去意义;粘性分区通过减少分区切换频率,让短时间内的小消息尽可能集中写入同一分区,批次可以更快填满,压缩效率和吞吐量都明显提升。因此,如果用较新版本的 Kafka,看到无 Key 消息长期集中在一个分区是完全正常的,不是负载均衡坏了。
带 Key 的逻辑又是另一套。默认分区器会对 Key 的字节数组计算 Murmur2 哈希,再对分区总数取模,公式大致等价于:
text复制partition = positive(murmur2(keyBytes)) % numPartitions
这里的重点有两个。
第一,为什么不用 Java 自带的 hashCode?因为 Java hashCode 对不同版本、不同 JVM 实现并不能保证完全一致的算法,而且 Kafka 的客户端不只 Java 一种语言。如果分区器用 Java hashCode,那么 Java 客户端算出来的结果和 Go 客户端、Python 客户端各自计算的结果可能不一样,同一个 Key 在不同语言客户端下会被路由到不同分区,顺序性就无从谈起。Murmur2 是一个跨语言、跨版本都稳定的哈希算法,Kafka 协议族都能复现同样结果,所以被选为默认实现。
第二,取模的结果和分区总数强相关。如果你用公司 ID 当 Key,默认会把同一公司的消息固定到一个分区,理论上确实保证了该公司内部的消息顺序。可一旦 Topic 分区数从 5 扩到 10,同一个 Key 取模的余数会变化,原来路由到分区 0 的公司消息可能跑到分区 6,之前消费端关于该公司消息的顺序记录就被打断了。这是一个特别容易在扩容后被忽略的坑,后面第五部分还会回到这个问题上。
另外还要理解默认分区器和“轮询”的区别。无 Key 消息的粘性分区策略,本质上追求的是堆积效率,而不是均衡。它不能保证消息均匀分布在各个分区,尤其当发送端低吞吐、批次长期填不满时,消息会一直停留在第一个粘住的分区上,直到达到 linger.ms 或 batch.size 的发送条件。如果你需要的是规模较大的统计类无 Key 流量均匀拆分,或者某些下游需要近似均匀分片,那就要考虑自定义分区器,或者给消息统一生成一个分散的 Key。
从排查经验看,有两种情况经常把默认策略“误伤”:一是测试环境只有两三个分区,消息量又小,发现所有消息都奔着分区 0 去了,以为是集群坏了;二是消息没有业务 Key,但下游从“某个分区”读数据做聚合,随后发现聚合结果不完整。这里要明确结论:不关心顺序、不关心聚合维度的无 Key 消息,用默认策略就可以;一旦下游对数据有分片粒度的业务诉求,就不能完全依赖默认分区器了。
3. 写一个生产可用的自定义分区器:从需求分析到落地代码
自定义分区器的使用场景通常不是“因为默认不好”,而是“默认分区器的分区维度不符合业务语义”。
举一个我实际做过的例子。某套 IoT 数据采集系统里,消息由设备 ID 和消息类型组成,其中有少量“告警消息”要求第一时间被下游实时处理,普通采集消息则可以稍微积压。如果全按设备 ID 分区,结果可能是告警消息被分配到某个消费 Lag 高的分区,实时性完全没法保证。我们希望把告警消息集中路由到一个独立分区,该分区对应的消费组单独分配处理资源,其他普通消息再走默认的 Key 哈希逻辑。这时候就需要在 Producer 端实现自定义 Partitioner。
在 Java 客户端里,实现分区器只需要继承 org.apache.kafka.clients.producer.Partitioner 接口,核心方法一共三个:
java复制public class AlertAwarePartitioner implements Partitioner {
private int alertPartition = 0;
private int fallbackHashPartitions = 3;
@Override
public void configure(Map<String, ?> configs) {
Object alertPartitionConfig = configs.get("alert.partition");
if (alertPartitionConfig != null) {
alertPartition = Integer.parseInt(alertPartitionConfig.toString());
}
}
@Override
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster) {
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
int numPartitions = partitions.size();
if (numPartitions <= alertPartition) {
return 0;
}
// 如果 key 以 alert: 开头,说明是告警消息,强制走告警分区
if (key != null && key.toString().startsWith("alert:")) {
return alertPartition;
}
// 其他消息继续走 Key 哈希,保证设备顺序
if (keyBytes != null && keyBytes.length > 0) {
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
// 完全没有 key 的消息用一个最简单的轮询
return (int) (ThreadLocalRandom.current().nextLong(numPartitions));
}
@Override
public void close() {
// 清理资源,通常什么都不用做
}
}
使用配置只需要在 Producer 构造时设置分区器和自定义参数:
properties复制partitioner.class=com.example.AlertAwarePartitioner
alert.partition=0
在生产环境写这类代码时,有几个容易被忽略的地方。
第一,分区器里拿到的 Cluster 对象是客户端内部的元数据缓存,不代表实时状态。不要在 partition 方法里频繁调用 cluster.partitionsForTopic(topic) 并打印完整分区列表,那样会引入不必要的开销。Kafka 元数据会由后台线程定期刷新,分区器每次执行都会传入当前的最新状态,直接用即可。
第二,不要只根据返回分区号做逻辑判断。如果 Topic 后来扩容,分区总数增多,你原来预留的“告警分区”可能已经满了或者被其他消息占用。代码里一定要对 numPartitions <= alertPartition 做兜底,否则消息会落到一个不存在的分区,直接出现 IllegalStateException。
第三,不要在分区器里做任何阻塞操作。分区器在客户端发送路径上执行,阻塞等待 Redis 或者远程接口会直接拖垮生产吞吐。所有路由判断应该基于消息本身的 Key、Value 或本地缓存完成。如果确实需要动态判断,也要在 configure 阶段提前加载,并且考虑用弱一致性缓存。
第四,如果保留“普通消息走 Key 哈希”的逻辑,建议直接复用 Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions,而不要自己重新实现哈希。这样能保证普通消息和其他没替换分区器的客户端行为一致,排查问题时也少一层变量。
自定义分区器看起来简单,但它的真正价值往往不在代码,而在写代码前的需求拆解。我通常会先把问题定义成一个二维表:
| 消息类型 | 顺序性要求 | 实时性要求 | 目标分区策略 |
|---|---|---|---|
| 告警事件 | 不要求跨消息顺序 | 高 | 固定路由到告警分区 |
| 设备采集流 | 同一设备内有序 | 中 | 设备 ID 哈希 |
| 全量统计日志 | 无要求 | 低 | 随机/均匀分片 |
把表填出来,代码长什么样基本就一目了然了。反过来,如果不用表格,而是直接上手写一个看起来逻辑“很复杂”的分区器,多半会把不必要的业务判断塞进去,最后既影响性能又难维护。
4. 消费端分区分配策略:别让下游拖累整个消息链路
分区策略不只有生产端的写入路由,消费端同样存在“分区如何分给消费者”的分配问题。很多人调通了 Producer,却发现消费组的某个消费者特别忙,其他消费者几乎空闲。原因大概率不在消息总量,而在于消费端使用哪个分区分配策略,以及消费组的消费者数量和各 Topic 分区数的关系。
Kafka 消费者使用消费组(Consumer Group)进行协作消费,同一个消费组内,一个分区最多只能被一个消费者线程消费。消费组里的消费者数量一旦变化——新实例加入、实例退出、心跳超时或订阅 Topic 发生变化——就会触发一次 Rebalance,也就是重新决定当前 Topic 的分区由组内哪些消费者来拉取。Rebalance 期间,部分消费者会暂停消费并停止提交 Offset,所以如果分区数量多、消费组成员多,Rebalance 的开销会被放大。
Java 客户端里常见的分区分配策略有这么几种:
| 分配策略 | 核心逻辑 | 适合场景 | 典型问题 |
|---|---|---|---|
| RangeAssignor(范围) | 按 Topic 独立处理,把分区按消费者名称字典序分段分配 | 老版本默认,单 Topic 且分区数能被消费者数整除时可以接受 | 多 Topic 场景下消费者容易负载不均 |
| RoundRobinAssignor(轮询) | 把全部订阅 Topic 的分区作为一个整体轮流分配 | 多 Topic 订阅、希望整体拉平负载 | 每个分区换手较多,连续变动时不够稳定 |
| StickyAssignor(粘性) | 尽量保留上一次分配结果,只调整需要变动的分区 | 分区较多、消费组成员频繁变化 | 对 Rebalance 感知敏感 |
| CooperativeStickyAssignor(协作粘性) | 在 Sticky 基础上支持增量协作式 Rebalance | 高版本 Kafka、大规模消费组 | 需要所有客户端版本支持 |
RangeAssignor 是最容易引发“倾斜”的默认选择。假设消费组有两个消费者订阅 Topic A(8 个分区,分区编号 0-7)和 Topic B(2 个分区),Range 策略按 Topic 分别计算:A 的 8 个分区按字典序切成两半,消费者 0 拿到 0-3,消费者 1 拿到 4-7;B 的 2 个分区按字典序排序后,消费者 0 拿到 0,消费者 1 拿到 1。乍一看每个 Topic 内部还算均衡,但整体来看消费者 0 持有了 5 个分区,消费者 1 只持有 3 个分区。如果 Topic 数更多或者分区数不能整除,这种差异会变得非常明显,某台机器负载明显偏高。
从 Kafka 2.4 起,新消费组的默认策略已经调整为 CooperativeStickyAssignor。这个策略最大的改进是引入了“增量 Rebalance”,每次分配变动不必让所有消费者全部停下,而是只撤销需要迁移的部分分区,其他分区继续正常消费。如果你的集群版本已经升级到 2.4 以上,且所有客户端版本都支持,建议优先使用 CooperativeStickyAssignor。如果项目里有很多老版本客户端混用,就只能保守一点,把策略协调到所有客户端都能识别的配置上。
在 Spring Boot 项目中,配置方式通常是:
properties复制spring.kafka.consumer.properties.partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor
如果你的项目是纯 Java 客户端手动创建消费者,则设置:
java复制props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
CooperativeStickyAssignor.class.getName());
再往下要理解一个核心约束:同一个分区在同一时刻只能被同组内一个消费者消费。换句话说,就算你把分配策略配得再均衡,一个 Topic 只有 3 个分区时,消费组最多只会有 3 个消费者真正拉取消息。超过 3 个消费者后,多出来的实例会一直空闲,直到有消费者退出或分区扩容。很多测试团队在这个环节误判:“我起了 10 个消费者,为什么只有 3 个在工作?”不用查代码,先数 Topic 分区数就明白了。
实际项目里,我会在确认消费端负载时按下面几步走:
- 用
kafka-consumer-groups.sh --describe --group <group>查看每个分区对应的 CURRENT-OFFSET、LOG-END-OFFSET 和 LAG。如果某个分区的 Lag 持续增长,说明该分区的消费速率跟不上写入速率。 - 用工具查看该消费组此刻正在消费该分区的消费者节点,确认是否是某个固定节点始终承担重负载。
- 如果重负载分区相对固定,而消费者总数不变,检查消费组是否在 Range 策略下把大分区分给了同一个消费者。
- 如果分区负载本身不均衡,回到生产端看是不是分区器把大量消息写进了少数分区。
还有一个容易出现且不好排查的情况:消费端设置了手动提交 Offset,但提交粒度太粗或提交延迟太大。Rebalance 发生时,消费者还没来得及提交已完成消息的 Offset,等新的消费者接管原有分区后,可能从更早位置重新消费,造成重复数据。这不是分区分配策略本身的问题,而是与 Rebalance 相互作用的经典故障,尤其在高版本默认异步提交时需要额外注意。通常解决办法是缩短 max.poll.interval.ms 和提交间隔,让每个消费线程处理完一批消息后及时提交,同时把处理逻辑里的不可控阻塞尽量转移到独立线程池中。
换句话说,分区分配策略的目的是让同一时刻某个分区只有一个消费者在消费,而不是让一个消费者能够消费所有分区。 消费能力不足时,正确的思路是增加 Topic 分区数并保持消费者数量小于或等于分区数,而不是一味增加消费者。扩容之后要继续观察分配效果,确认新加入的消费者确实拿到了分区,才算真正生效。
5. 热点倾斜与扩容的代价:三个真实生产案例复盘
分区策略相关的多数问题,不是第一天就显现的,而是当流量模型发生变化或者执行扩容操作时集中爆发。我选三个自己实际处理过的场景复盘一下,希望能帮大家提前踩坑。
5.1 单 Key 流量倾斜:超级商户把分区打满
某支付风控系统按“商户 ID”作为 Key 发送交易消息到 Kafka,用于后续按商户维度做风险检测,同一个商户的交易必须有序处理。早期商户量小,流量均匀,3 个分区完全够用。后来接入了一个电商大促渠道,该渠道的峰值流量占全系统 40%,而这个渠道在业务侧恰好只有一个大的商户 ID。结果这个商户的所有交易消息全部哈希到同一个分区,该分区每秒写入量直接冲破磁盘带宽,消费 Lag 从几百条涨到几十万条。
排查时先通过监控确认了各分区消息量,发现分区 2 的写入速率是分区 0 和分区 1 的十几倍。此时加消费者没有任何意义,因为单个分区只能被一个消费者消费;加分区同样没有意义,因为原始 Key 取模后仍然固定在那一个分区。核心解法是改变分区维度。
由于该业务的核心要求是“同一个商户的交易有序”,不能用随机 Key 代替。我们采用的方案是:把原始商户 ID 拆成商户 ID + 渠道内子分片序号,比如 merchant_001_shard_0、merchant_001_shard_1,在业务层给同一个商户的交易叠加一个轮询的 shard 后缀。这样,同一个商户的某一笔交易消息,可能去了 shard_0 对应的分区,另一笔可能去了 shard_1 对应的分区,单分区压力就降了下来。
代价也很明显:同一个商户的交易不再全局有序,而是只能保证“同一个 shard 内的局部顺序”。如果风控检测对某几笔交易有强顺序依赖,就必须在检测端按“商户 ID + 交易序号”做一个极轻量的重排,或者把强有序的子集再单独走 Kafka Streams 的分区逻辑。这个取舍我们当时讨论了很久。结论是:当业务允许局部有序时,尽量用高基数的复合 Key 去规避热点;当业务必须严格全局有序时,单个热点 Key 会把分区打到极限,这时需要引入分层聚合或旁路处理,而不是单纯调分区器。
5.2 Topic 分区扩容引发的顺序风险
另一个项目里,数据同步任务用“订单 ID”做 Key,把变更消息写入同一分区以保证变更顺序。因为单日订单量预估翻倍,运维直接执行了 kafka-topics.sh --alter --partitions 8 把分区数从 4 扩到 8。扩容后第二天就收到下游对账异常。
问题出在扩容改变了取模基数。原来 4 个分区时,订单 ID 的 murmur2 哈希值对 4 取模;扩容后变成对 8 取模。同一个订单的哈希值原本可能落在分区 3,现在则落到分区 7,消费者侧虽然能消费到新消息,但同一个订单的旧消息还在分区 3,新消息到了分区 7,两边无法保证顺序拼接。如果有下游把同一订单的不同阶段状态当成乱序事件处理,结果就会错误。
这类问题的通用解法有几种:业务可以接受的话,在扩容前停写,等存量消息全部消费完再扩容,然后再放量写;不能停写的话,需要在下游增加“分区版本号”或者“全局单调序号”,按序号判断是否来自同一个上游分片。最省事的经验是,如果 Topic 明确承载强顺序业务且会把订单 ID 或用户 ID 当 Key,就要在设计之初估算未来三年的业务量,把分区数一次性规划到位,避免后续频繁扩容。根据经验,这类有序 Topic 扩容的代价远高于新建一个 Topic 并重新导流。
5.3 Broker 分区的 Leader 分布不均
这个案例和生产端分区器没有直接关系,但因为经常和分区倾斜混在一起,所以放在一起提醒。某集群在业务高峰期经常出现个别 Broker CPU 高、磁盘 IO 等待大,但其他 Broker 空闲。查分区元数据后发现,新扩容的 Broker 上几乎没有创建过分区,很多热门 Topic 的 Leader 集中在几台老 Broker 上。
Kafka 默认在创建 Topic 时会把分区尽量分散到不同 Broker,但后续如果新增了 Broker,原有分区并不会自动迁移过去。如果不做 kafka-reassign-partitions.sh 操作,新机器只能等后续新建 Topic 才能被利用,现有流量还是压在老机器上。排查时很容易被误判为“分区倾斜导致消费者 Lag”,其实根源在集群层面的分区副本分布。
处理方法是定期检查集群分区分布,尤其是 Broker 数量变化后。可以用可视化工具直接看 Topic 分区的 Leader 分布,也可以执行:
bash复制kafka-reassign-partitions.sh --generate --topics-to-move-json-file topics.json --broker-list "1,2,3,4" --zookeeper zk_host:2181
生成迁移方案后,确认没有流量风险再执行。注意这里推荐在业务低峰期操作,避免 Leader 切换对在线业务造成可见抖动。
6. 生产环境中配套的监控、常见错误与排查清单
分区策略相关的很多故障,其实可以靠监控更早发现。许多团队接手 Kafka 集群后第一个动作就是看消费 Lag,但 Lag 只是一个结果指标,它不会告诉你“为什么某个分区的 Lag 在涨”。依赖 Lag 反推问题,往往等到积压已经非常严重了。适合分区模型的监控指标应该包含三层:
- 生产端各分区写入速率:能够直接看到消息是否倾斜到某个分区。Kafka Producer 的指标按 topic-partition 维度已经能覆盖,如果使用 Micrometer 或 JMX 暴露指标,就按
records-per-second和byte-rate拆分出来观察。 - 消费端各分区消费速率和 Offset:脚本查看
kafka-consumer-groups.sh外的另一层,建议将每个消费组的 LAG 指标按照 partition-id 上报到监控系统。重点观察是否只有个别分区的 LAG 增长,如果是,多半是分区写入倾斜或消费能力倾斜。 - Broker 层分区 Leader 分布与磁盘使用率:热点 Topic 的分区是否集中在同一台 Broker,磁盘使用率是否呈现明显的长尾。
开源生态里可以直接使用 Kafbat UI、Offset Explorer 这类可视化工具快速查看分区偏移量和分发情况。部署在 Docker 环境时,比如用 Docker Compose 起 Kafka 时如果不希望引入 Zookeeper,可以选用 Kafka 的 KRaft 模式镜像。这里稍微提醒一句:很多连接报错如 Error while fetching metadata with correlation id,本质是客户端无法从 broker 获取元数据,常见原因是地址配置错误、网络不通或者 broker 还没完成启动;一旦客户端能正常拿到元数据,后续如果遇到 Cluster authorization failed,则多半是 ACL 权限没有把当前用户授予该 Topic 的相应操作权限。这类错误和分区策略本身关系不大,但在集群升级或迁移后非常容易同时出现,先确认网络和 ACL,再排查分区器逻辑,能少走很多弯路。
以下是我在排查“分区相关问题”时常用的一个检查清单,基本按优先级排序:
- 确认各分区写入速率是否均匀。
- 确认消费组订阅的分区数和消费者数量关系。
- 确认消费组当前使用的分区分配策略是否为预期策略。
- 确认所有生产端客户端的分区器配置一致,避免新旧版本混用导致相同 Key 路由到不同分区。
- 确认 Topic 最近是否扩容过,扩容后 Key 哈希的路由变化是否对下游有影响。
- 确认是否存在单个 Key 独占大流量,必要时用复合 Key 拆分。
- 确认集群 Broker 节点数变化后,分区副本分布是否重新调整,而不是靠新建 Topic 慢慢填平。
- 确认监控指标能在分钟级别反映单分区写入速率,不要只盯全 Topic 总量。
分区策略归根结底做的是一道分流的题目:流量均匀、顺序可控、消费可扩展,三者之间经常互相冲突。任何一个优秀的设计都必须在具体业务约束下做权衡。我自己的体感是,很多踩坑并非因为开发者没能力写分区器,而是因为对“分区是顺序与并行的边界”理解不够深,一旦流量的形状发生变化,最初选择的策略就不再适用。少写一点炫技逻辑,多留一点扩容和倾斜的预案,比任何花哨的动态路由都可靠。
