· 1 分钟阅读

Kafka 在消息推送系统里的削峰填谷实践

这篇结合消息推送项目,记录我为什么用 Kafka 做消息中转站,以及在消费者组、幂等、积压和优先级消费上遇到的设计取舍。

消息推送系统这个项目刚开始看起来不复杂:业务方传一条消息,系统帮它发邮件、短信或者飞书通知。但真正往下设计时,我发现它其实很适合练后端系统设计,因为里面有高并发、削峰填谷、消息可靠性、限流、延时消息、多厂商切换这些问题。

我这篇主要写 Kafka 这一块。不是讲 Kafka 的所有原理,而是从项目角度整理:为什么我需要一个消息中转站,Kafka 在这里解决了什么问题,又有哪些问题不是 Kafka 自己能解决的。

为什么不能同步发送

最直接的方案是业务方调用推送接口,推送系统马上调用短信、邮件、飞书这些第三方平台,然后把结果返回。这个方案很好理解,但问题也很明显。

第一,第三方平台不稳定。短信厂商可能限流,邮件服务可能响应慢,飞书接口也可能偶发超时。如果业务请求一直等第三方返回,业务方的接口耗时就会被下游拖住。

第二,流量不是均匀的。比如活动通知、批量告警、定时任务,可能某一分钟突然打进来大量消息。如果全部同步处理,推送系统和第三方平台都会被打满。

第三,不同消息的重要程度不一样。告警消息应该尽快发,普通营销消息可以慢一点。如果同步发送,很难统一做优先级调度。

所以我引入消息中转站,入口只负责校验、记录、投递,真正发送交给消费者异步处理。

Kafka 在这里的作用

Kafka 的作用可以理解成一层缓冲。生产者写入消息,消费者按自己的处理能力消费。业务高峰时,消息可以先积压在 Kafka 里,不会直接把压力打到短信厂商。

我设计消息时会保留这些字段:

{
  "messageId": "msg-10001",
  "channel": "SMS",
  "priority": "HIGH",
  "receiver": "153****5683",
  "templateId": "alarm_template",
  "params": {},
  "traceId": "trace-xxx"
}

messageId 用于幂等,channel 用于路由,priority 用于优先级调度,traceId 用于链路追踪。刚开始我只想着把消息发出去,后来才意识到这些字段如果一开始不设计好,后面排查问题会很难。

Topic 和分区怎么考虑

这里有两种思路:

一种是按渠道拆 Topic,比如 push_sms_topicpush_email_topicpush_feishu_topic。这样隔离性比较好,某个渠道积压不会直接影响其他渠道。

另一种是统一 Topic,通过消息里的 channel 字段区分。这样管理简单,但消费者侧要自己做更多路由。

我更倾向于按渠道或者业务重要程度拆 Topic,至少高优先级告警不要和普通消息完全混在一起。分区数要结合 QPS 和消费者处理能力来估。比如目标是 3000+ QPS,如果单线程处理能力有限,就需要更多分区和消费者并行。

不过分区也不是越多越好。分区太多会增加 broker 管理成本,也会让 rebalance 更重。所以这块需要根据压测结果调整。

消费者组解决扩容问题

Kafka 消费者组适合做横向扩容。同一个消费者组里,多个实例共同消费 Topic 的不同分区。这样推送系统部署多个实例时,不会每个实例都消费同一批消息。

但这里有个容易误解的点:Kafka 能减少重复消费,不代表业务上完全不会重复。

比如消费者已经调用短信厂商成功了,但还没提交 offset 就宕机。重启后这条消息可能会再次消费。如果没有幂等,就可能重复发短信。

所以我会在数据库里维护消息发送状态:

INIT -> SENDING -> SUCCESS
              -> FAILED
              -> RETRYING

发送前先根据 messageId 查状态。如果已经是 SUCCESS,就直接跳过。如果是 SENDING 但更新时间很久以前,说明可能有异常,需要补偿处理。

优先级消费不是只靠 Kafka

项目里有三优先级通道:高、中、低。高优先级消息比如系统告警,低优先级可能是普通通知。

一开始我想是不是直接建三个 Topic 就行。后来觉得还不够,因为消费者资源也要控制。如果低优先级消息很多,不能把所有线程都占住。

可以做一个简单的调度策略:

高优先级:每轮消费 6 次
中优先级:每轮消费 3 次
低优先级:每轮消费 1 次

这样低优先级不会完全饿死,高优先级也能更快处理。当然这只是一个基础策略,实际还要结合消息积压和发送通道限流动态调整。

积压不是一定坏事

以前我看到 MQ lag 上升会觉得系统出问题了。后来理解到,积压本身不一定坏,它有时候说明 Kafka 正在帮系统挡住峰值。

真正要看的是:

  • 积压是否持续增长。
  • 高优先级消息是否也在积压。
  • 消费者是否异常。
  • 下游厂商是否限流。
  • 重试队列是否变多。

如果只是活动高峰导致短时间积压,消费者后面能追上,那是可以接受的。如果 lag 一直升,说明消费能力或者下游能力不够。

Kafka 解决不了所有可靠性问题

Kafka 提供的是消息中间件能力,但消息推送系统的可靠性还要靠很多补充机制:

  • 数据库记录消息状态。
  • 消费端幂等。
  • 失败重试和退避。
  • 死信队列。
  • 第三方厂商容灾切换。
  • 监控告警。
  • 定时补偿任务。

比如短信厂商 A 持续失败时,系统要自动切到厂商 B。Kafka 只负责消息流转,不知道哪个厂商可用。所以厂商健康检测和路由策略要在业务层做。

我觉得这个项目比较能聊的点

如果面试里聊这个项目,我不会只说“用了 Kafka 提高吞吐量”。这样太空了。

我会重点讲:

  1. 为什么同步发送不适合高并发推送。
  2. Kafka 如何削峰填谷。
  3. 消费者组如何支持集群部署。
  4. 为什么还需要业务幂等。
  5. 优先级消息怎么调度。
  6. 积压怎么监控和处理。
  7. Kafka 和 MySQL 两种中转站各自适合什么场景。

这些问题能体现一点系统设计思考,而不是只会调 API。

小结

Kafka 在这个消息推送项目里,最重要的价值是把入口流量和发送能力解耦。它让系统能扛住瞬时高峰,也方便消费者横向扩容。

但我也越来越觉得,消息队列只是中间一环。真正让系统可靠的,是 Kafka、数据库状态、幂等、限流、重试、监控和补偿一起配合。单独拿出 Kafka 来讲并不完整,把它放回业务链路里,才能看出设计的意义。