Skip to content

14 - Kafka 集成详解 ​

Watermill 通过 watermill-kafka 适配器实现 Kafka 集成。本章解析 ecommerce/pkg/kafka/client.go 的配置细节和内部机制。

Publisher 配置 ​

go
func NewPublisher(brokers []string, logger watermill.LoggerAdapter) (message.Publisher, error) {
    return kafka.NewPublisher(kafka.PublisherConfig{
        Brokers:   brokers,
        Marshaler: kafka.DefaultMarshaler{},
    }, logger)
}

PublisherConfig 核心参数:

参数默认值说明
Brokers必填Kafka broker 地址列表
MarshalerDefaultMarshaler消息序列化器(见下方)
OverwriteSaramaConfignil底层 Sarama 配置覆盖
Asyncfalsetrue 时异步发布,吞吐更高但可能丢失消息
BatchMarshalernil批量序列化器

DefaultMarshaler 将 Watermill 的 *message.Message 转换为 Kafka 的 *sarama.ProducerMessage:

  • msg.UUID → Key(用作 Kafka 分区键)
  • msg.Payload → Value
  • msg.Metadata → Headers

这是实现消息分区和顺序消费的关键——消息 UUID 作为分区键,同一 UUID 的消息落在同一个 partition。

Subscriber 配置 ​

go
func NewSubscriber(brokers []string, consumerGroup string, logger watermill.LoggerAdapter) (message.Subscriber, error) {
    return kafka.NewSubscriber(kafka.SubscriberConfig{
        Brokers:       brokers,
        ConsumerGroup: consumerGroup,
        Unmarshaler:   kafka.DefaultMarshaler{},
    }, logger)
}

SubscriberConfig 核心参数:

参数说明
ConsumerGroup消费者组名。同一组内的实例共享 partition
BrokersKafka broker 地址
Unmarshaler反序列化器
InitializeTopicDetails自动创建 topic 配置(不推荐生产使用)
NackResendSleepNACK 后重新消费的等待时间
ReconnectRetrySleep断连重试间隔

分区策略 ​

Kafka 分区决定消息的路由和消费顺序。Watermill 的 DefaultMarshaler 使用 msg.UUID 作为 Key,Sarama 默认使用 Hash 分区器:

partition = hash(key) % partition_count

要按业务键(如 order_id)分区,需要自定义 Marshaler:

go
type OrderKeyMarshaler struct{}

func (m OrderKeyMarshaler) Marshal(topic string, msg *message.Message) (*sarama.ProducerMessage, error) {
    // 从 Metadata 中提取 order_id 作为分区键
    orderID := msg.Metadata.Get("order_id")
    return &sarama.ProducerMessage{
        Topic: topic,
        Key:   sarama.StringEncoder(orderID),
        Value: sarama.ByteEncoder(msg.Payload),
    }, nil
}

Offset 管理 ​

Watermill Kafka 使用消费者组自动提交 offset:

  • 自动提交(默认)。消费消息后自动提交 offset,可能丢消息(消息收到但未处理完就 crash)
  • 手动提交。通过 SubscriberConfig.DisableAutoCommit = true 禁用,配合 message.Ack() 手动提交

在电商项目中,由于使用 Router + Retry 中间件,消息未处理成功会触发 NACK + 重试,offset 仅在 msg.Ack() 时提交。这保证了 at-least-once 语义。

消费者组重平衡(Rebalance) ​

当消费者组中的实例增减时,Kafka 触发 rebalance——重新分配 partition 给各实例。期间所有消费者暂停消费。

优化建议:

  • 增加 session.timeout.ms 和 max.poll.interval.ms 减少不必要的 rebalance
  • 缩短单条消息处理时间(30s 以内)
  • 使用 Static Membership 固定成员 ID,避免滚动重启时的 rebalance

Kafka 日志查看 ​

bash
# 查看 topic 列表
docker exec watermill-kafka kafka-topics --list --bootstrap-server localhost:9092

# 查看消费者组状态
docker exec watermill-kafka kafka-consumer-groups \
  --bootstrap-server localhost:9092 \
  --group order-service --describe

# 查看 topic 最新消息
docker exec watermill-kafka kafka-console-consumer \
  --bootstrap-server localhost:9092 \
  --topic order.events --from-beginning --max-messages 5