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_topic、push_email_topic、push_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 提高吞吐量”。这样太空了。
我会重点讲:
- 为什么同步发送不适合高并发推送。
- Kafka 如何削峰填谷。
- 消费者组如何支持集群部署。
- 为什么还需要业务幂等。
- 优先级消息怎么调度。
- 积压怎么监控和处理。
- Kafka 和 MySQL 两种中转站各自适合什么场景。
这些问题能体现一点系统设计思考,而不是只会调 API。
小结
Kafka 在这个消息推送项目里,最重要的价值是把入口流量和发送能力解耦。它让系统能扛住瞬时高峰,也方便消费者横向扩容。
但我也越来越觉得,消息队列只是中间一环。真正让系统可靠的,是 Kafka、数据库状态、幂等、限流、重试、监控和补偿一起配合。单独拿出 Kafka 来讲并不完整,把它放回业务链路里,才能看出设计的意义。