读 Kafka 源码才知道,你写的观察者模式就是个玩具

12 阅读1分钟

前阵子排查一个消费者 rebalance 的问题,翻到 Kafka 源码里去了。本来只想看看协调器怎么工作的,结果发现 Kafka 的设计模式用得相当精妙——不是教科书式的"定义接口→实现类→调用",而是把多个模式揉在一起解决真实问题。

很多人学设计模式停留在"观察者就是 EventBus"这个层面,但 Kafka 的观察者模式跟教科书完全不是一个物种。今天拆几个 Kafka 源码里最有意思的设计模式,看完你可能得重新审视自己写的代码。

观察者模式:Consumer Group 的协调机制

教科书里的观察者模式:一个 Subject,N 个 Observer,状态变了通知一下。完事。

Kafka 的消费者组协调机制也是观察者,但它解决的问题复杂得多:

  • 不是简单的"状态变了通知一下",而是"状态变了之后,所有人要协商出一个新的一致状态"
  • 观察者(消费者)可以随时加入和离开
  • 通知不是一次性的,而是一个多轮的 rebalance 协议
// Kafka 源码:ConsumerCoordinator 的协调逻辑
private void onJoinComplete(int generation, String memberId, String assignmentStrategy, ByteBuffer assignment) {
    // rebalance 完成后的回调——每个消费者拿到自己的分区分配
    // 这不是简单的 notify,而是一个分布式协商的终点
}

关键点:Kafka 的协调器(GroupCoordinator)不是简单广播状态,而是通过 JoinGroupRequestSyncGroupRequest 两轮交互完成一次 rebalance。第一轮选 leader,第二轮由 leader 做分区分配,再把结果分发下去。

你写的观察者模式是"通知一下",Kafka 的观察者模式是"通知→协商→确认→生效"。这个差距不是代码量的差距,是问题域的差距。

策略模式:分区分配策略的可插拔设计

Kafka 的分区分配策略是个教科书级的策略模式,但它有一个很多人忽略的设计决策:默认不是 RoundRobin

// PartitionAssignor 接口——策略模式的抽象
public interface PartitionAssignor {
    // 每个策略实现自己的分配算法
    Map<String, Assignment> assign(Cluster metadata, Map<String, Subscription> subscriptions);
}

// 三种内置策略
class RangeAssignor implements PartitionAssignor { ... }  // 默认策略
class RoundRobinAssignor implements PartitionAssignor { ... }
class StickyAssignor implements PartitionAssignor { ... }

踩过一个坑:用 RoundRobin 分配策略时,消费者数变化会导致几乎所有分区重新分配。换成 StickyAssignor 后,只有必要的分区会移动,消费者缓存命中率直接上去。

这个案例说明一个策略模式的选型问题:策略不能只看"对不对",要看"切换代价大不大"。RoundRobin 逻辑没错,但在消费者频繁上下线的场景下,每次 rebalance 的代价太高。

责任链模式:Producer 的 Interceptor 链

很多人不知道 Kafka Producer 有拦截器机制,因为它默认是空的。但这个设计非常典型:

// Producer 拦截器接口
public interface ProducerInterceptor<K, V> extends Configurable {
    ProducerRecord<K, V> onSend(ProducerRecord<K, V> record);      // 发送前
    void onAcknowledgement(RecordMetadata metadata, Exception e);   // 收到 ack 后
    void close();
}

// KafkaProducer 中的调用
private RecordAccumulator recordAccumulator;
private List<ProducerInterceptor<K, V>> interceptors;

// 发送时经过拦截器链
private Future<RecordMetadata> doSend(ProducerRecord<K, V> record) {
    // 拦截器链处理
    ProducerRecord<K, V> interceptedRecord = interceptors == null ?
        record : this.interceptors.onSend(record);
    // ... 后续发送逻辑
}

跟 Servlet Filter 的区别在于:Kafka 的拦截器只在两个钩子点生效(onSend 和 onAcknowledgement),不像 Filter 可以拦截整个请求链。这个取舍是有意的——Kafka 追求吞吐量,拦截器链越短越好。

实际用法:我们用拦截器做了两件事:

  1. 在 onSend 里给消息打时间戳和链路追踪 ID
  2. 在 onAcknowledgement 里统计发送成功率和延迟

这两个功能加起来只增加了不到 1% 的延迟,但监控粒度提升了几个量级。

模板方法模式:KafkaProducer 的发送流程

KafkaProducer.send() 方法是模板方法模式的经典实现,只是它没用抽象类,而是用一个固定流程 + 多个可替换组件:

public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
    // 1. 拦截器处理(可选)
    ProducerRecord<K, V> interceptedRecord = interceptors.onSend(record);
    // 2. 序列化(Serializer 可替换)
    byte[] serializedKey = keySerializer.serialize(topic, interceptedRecord.headers(), interceptedRecord.key());
    // 3. 分区选择(Partitioner 可替换)
    int partition = partition(record, serializedKey, serializedValue, cluster);
    // 4. 追加到 RecordAccumulator
    RecordAccumulator.RecordAppendResult result = accumulator.append(...);
    // 5. 唤醒 Sender 线程
    if (result.batchIsFull || result.newBatchCreated) {
        this.sender.wakeup();
    }
    return result.future;
}

整个流程是固定的(序列化→分区→攒批→发送),但每一步的具体实现都可以替换:Serializer、Partitioner、Interceptor 都是可插拔的。这比你在代码里写个 abstract class 再搞几个子类要灵活得多。

工厂模式 + 门面模式:Kafka 的客户端创建

创建一个 KafkaProducer 要做的事很多:配 NetworkClient、配 Serializer、配 RecordAccumulator、配 Sender 线程……如果让用户自己组装,90% 的人会配错。

// 用户只需要这样
KafkaProducer<String, String> producer = new KafkaProducer<>(props);

// KafkaProducer 构造方法里做了大量组装工作
KafkaProducer(Map<String, Object> configs, Serializer<K> keySerializer, Serializer<V> valueSerializer) {
    this.producerConfig = new ProducerConfig(configs);
    // ... 组装几十个组件
    this.metrics = new Metrics();
    this.serializer = ...;
    this.partitioner = ...;
    this.recordAccumulator = new RecordAccumulator(...);
    this.sender = new Sender(...);
    this.ioThread = new KafkaThread("kafka-producer-network-thread", this.sender, true);
    this.ioThread.start();
}

这就是门面模式:用户只需要传一个 Properties,Kafka 在内部组装好所有组件。同时,Serializer、Partitioner 这些组件通过配置动态创建,这就是工厂模式的变体——配置驱动创建。

总结一下关键洞察

  1. 观察者模式不只是通知,还可以是协商。Kafka 的 rebalance 协议比简单的 notify 复杂一个量级,但也解决了一个量级更难的问题。

  2. 策略模式的选型要看切换代价。RoundRobin vs Sticky 不是一个"对不对"的问题,而是"改一次的代价大不大"的问题。

  3. 责任链不是越灵活越好。Kafka 的拦截器只有两个钩子,刻意限制了灵活性,换取了低延迟。设计模式的运用要跟性能目标对齐。

  4. 模板方法不用抽象类也能做。KafkaProducer 的 send() 是固定流程 + 可插拔组件,比继承体系更灵活。

  5. 工厂 + 门面是复杂对象创建的标准解法。当你有几十个组件要组装时,别让用户自己 new。

这些模式在 GoF 书里都有,但 Kafka 的实现方式跟教科书不一样——教科书教你的是结构,Kafka 展示的是取舍。结构容易学,取舍需要踩坑。


我在做一个用卡皮巴拉讲设计模式的微信小程序「爪爪代码冒险记」,23 个设计模式用漫画 + 答题的方式讲,感兴趣可以搜一下,后续每篇文章我也会同步对应的小程序内容。