刚过完年,工作室群里好几个朋友都在问同一个问题:消息队列到底选Kafka还是RocketMQ?其中有个朋友正处在评估从Kafka迁到RocketMQ的阶段,天天拿着监控图找我商量。这个问题我被问过太多次,因为这两个名字虽然经常被放在一起比较,但底层设计哲学其实差得很远,拿同一套标准去卡,很容易得出错误结论。
这篇文章不打算贴官方文档复述,我按自己在业务里实际用过的经验,把两个中间件从架构、消息模型、可靠性、性能到迁移踩坑完整梳理一遍。适合正在做技术选型的团队、打算从Kafka迁移到RocketMQ的中间件负责人,以及想真正搞懂两套消息体系差异的开发同学。你能看到的不只是对比清单,还有我自己的取舍逻辑和实操流程。
1. 先搞清楚定位:两种消息队列的“出身”决定了很多事
1.1 Kafka的基因:日志管道与高吞吐流式场景
Kafka最初的设计目标是大数据场景下的日志收集和事件流处理。它的核心模型是一条消息就是一个不可变的事件,写入之后基本不会再改动,消费者反复读都可以。因为面向日志,所以它天生追求“顺序写磁盘”“批量发送”“横向扩展吞吐”,这些无一例外都指向了高吞吐、低延迟的流式管道场景。
Kafka里每个分区(Partition)本质上就是一个持久化的顺序日志,消息按写入顺序追加,消费者只需要记住自己消费到了哪个位置(Offset),就可以持续读取。为了保证顺序写,它把数据追加到log文件尾部,依赖操作系统的PageCache加速,发送端和消费端又做了批量读写。高性能不是靠什么黑魔法,而是因为它把“顺序”“批量”两个词做到了极致。
这也决定了它的一些边界:Kafka提供的语义是中性的,它不主动帮你做延迟消息、不过滤标签、不做复杂的重试与死信管理。如果需要这些能力,通常要自己在外围实现,或者干脆换成业务侧更顺手的中间件。
1.2 RocketMQ的基因:业务削峰与可靠性优先
RocketMQ诞生于电商业务场景,最早是为了解决交易链路里的削峰填谷、异步解耦、最终一致性这类问题。所以它一开始就把可靠性、事务性、业务可追踪性放在了很高的位置。后来的功能演进也一直围绕业务开发者的痛点:延迟消息、事务消息、Tag过滤、消息轨迹、死信队列,几乎每个功能都是业务程序员叫得出来的名字。
RocketMQ的存储模型跟Kafka也不太一样,它做了两层索引:所有Topic的数据先顺序写进同一个CommitLog,再通过轻量级的ConsumeQueue建立起“Queue -> 消息位置”的索引。这样做的好处是,一个Broker上哪怕有几千上万个Queue,写入依然能保持很好的顺序性,不会因为Queue数量增加而快速退化。代价是读取一条消息需要多一次索引定位,整体吞吐上限通常没有Kafka那么夸张,但满足绝大多数业务系统绰绰有余。
1.3 两种定位对选型的影响
很多选型文章喜欢讨论“哪个更快”,我反而觉得先看画像更重要。如果你的系统就是做数据管道、日志聚合、接入Flink做流式计算,那用RocketMQ会经常觉得缺东西。反过来,如果你的订单、支付、库存流程需要事务消息、延迟关单、按业务标签过滤事件,那么用Kafka就要自己造一堆轮子,还要处理消费重平衡带来的抖动问题。
这不是谁替代谁的关系,而是“数据通道”和“业务消息网关”的差异。理解这一点,后面所有对比都会顺理成章。
2. 核心模型对比:Topic、分区/队列、Consumer与Offset
2.1 存储模型与索引设计
Kafka的物理存储是每个Partition独立目录,目录里是一组Segment文件和对应的索引文件。写入消息时,追加到当前活跃Segment的尾部;查找时通过Offset索引快速定位。因为每个Partition都有自己的顺序流,所以Partition数量越多,理论上并行度越高,但实际单台Broker上如果分区数过多,文件句柄、索引、PageCache压力都会上来,甚至变成随机写。
RocketMQ的存储则更集中。所有Topic的Queue都共享一套CommitLog,消息进来先写到CommitLog,然后分发建立ConsumeQueue索引。ConsumeQueue很小的,存的是CommitLog偏移量、消息大小、Tag哈希。这样设计让RocketMQ可以轻松支撑大量Topic和Queue,写盘几乎一直是顺序写,不会因为“队列数量多”而明显掉性能。
有一个实践差异必须说清楚:Kafka的消息是按Partition粒度过期删除,你可以很灵活地按Partition管理数据,而RocketMQ的消息过期是整文件维度,默认过期时间是72小时。也就是说RocketMQ通常不适合长时间保留大量历史消息,如果你要让消息保留7天甚至更久,成本会很高,而Kafka只要调大分区留存时间就可以。做选型时,这条往往被忽视。
2.2 消费模型与负载均衡
Kafka消费模型是“Topic – Partition – Offset”。一个消费组里的多个消费者共同消费一个Topic,每个分区在同一时间只能被该组内的一个消费者线程持有。这个约束保证了分区内消息串行处理,但也是重平衡问题的根源。当消费者上下线、分区数量调整时,Kafka会触发Rebalance机制,让整个消费组的人停下来重新分配分区,期间消费是停滞的。如果一个消费组下面挂了几百个分区,消费者数量频繁变动,那处理不好就会出现明显的消费中断和重复消费。
RocketMQ的消费模型是“Topic – Queue – Offset”,本质逻辑跟Kafka很像,一批Queue被分配到消费组内的消费者上,一个Queue只能被一个消费者处理。但RocketMQ在Broker端维护了消费组的队列分配逻辑,Broker感知到消费者列表变化后,会把一些Queue重新分配,消费者尽量不动。相比Kafka的全量Rebalance,RocketMQ的负载均衡过程更温和,消费者侧的中断时间更短。
另外,RocketMQ原生支持集群消费和广播消费两种模式。广播模式是指一条消息能被同一个消费组的每个消费者都消费一份,这个能力在Kafka里需要靠不同GroupId模拟,用起来比较绕。如果你的业务里经常有“配置下发、全量订阅”这种需求,RocketMQ会更顺手。
2.3 功能维度对比
我习惯把两个中间件的功能能力拉一张表,选型时直接看:
| 能力项 | Kafka | RocketMQ |
|---|---|---|
| 顺序消息 | 分区内有序,需要生产者按Key路由 | 队列内有序,提供MessageQueueSelector和顺序消费Listener |
| 延迟/定时消息 | 原生不支持,需要自己实现 | 原生支持延迟级别,5.x支持任意时间定时消息 |
| 事务消息 | 支持流处理事务,偏写幂等 | 支持半消息+事务回查,业务事务表达更直接 |
| 消息过滤 | 无内置Tag过滤,只能客户端过滤 | 支持Tag和SQL92表达式过滤 |
| 死信队列 | 无原生DLQ,需自行处理 | 消费失败自动进入%DLQ%开头的死信队列 |
| 消息轨迹 | 无内置,需要外部链路追踪 | 支持消息轨迹查询,能看到全链路 |
| 重置消费位点 | 支持按时间/offset重置 | 支持按时间重置,命令也很方便 |
| 数据保留 | 按分区和保留时间策略清理 | 按CommitLog文件过期清理,默认72小时 |
这张表不是告诉你哪个“更能打”,而是让你意识到:Kafka更偏基础设施,RocketMQ更偏业务中间件。功能丰富度上RocketMQ明显占优,这点在业务型团队里特别重要。
3. 可靠性、顺序性与性能的实践权衡
3.1 可靠投递语义与端到端配置
消息队列都绕不开三个语义:最多一次(At Most Once)、至少一次(At Least Once)、精确一次(Exactly Once)。Kafka和RocketMQ默认情况下给的都是“至少一次”,也就是说消息可能会重复消费,但一般不会丢。为什么?因为生产者发送成功后要等确认,消费者处理完业务后要提交Offset。如果消费者处理完但还没来得及提交Offset就宕机,复Consumer重新分配后会从之前的位置再次消费,这就产生了重复。
想做到“不丢”而不是“不重”,关键是把生产者和Broker配置到可靠模式。
Kafka生产者侧建议:acks=all,retries>0,enable.idempotence=true。acks=all代表Leader确认副本都写入成功才返回,min.insync.replicas建议设置为2,配合replication.factor=3,这样即使一台机器挂了,也能保证至少有一个同步副本。注意unclean.leader.election.enable要设为false,避免副本落后太多的节点被选举为Leader导致丢数据。
RocketMQ生产者侧建议:发送消息用DefaultMQProducer的同步发送,设置sendMsgTimeout=3000,retryTimesWhenSendFailed=2。Broker层面如果想压榨可靠性,把flushDiskType调成SYNC_FLUSH,brokerRole调成SYNC_MASTER。但说实话日常大部分业务用ASYNC_FLUSH加主从异步复制就够了,因为同步刷盘和同步复制的性能开销非常大,而业务系统里下游消费失败才是消息丢失的重灾区,与其在Broker上追求极致,不如把消费幂等做好。
我自己的习惯是:生产者必须同步发送并捕获异常,发送不成功就落到本地补偿表;消费者消费前先根据业务唯一键去重,能跑幂等就绝不依赖消息队列的精确一次语义。这个习惯虽然在两边通用,但在RocketMQ上维护起来更顺手,因为它有现成的消息轨迹和死信队列。
3.2 顺序消息怎么实现
顺序消息的业务场景很典型:订单状态流转、库存流水、账户账单。无论是Kafka还是RocketMQ,全局顺序都意味着“只有一个分区/队列”,吞吐会直线下滑。实际业务里也不需要所有消息全局有序,只需要同一个业务Key的消息有序。
Kafka实现方式:生产者在发送时,按照业务Key(比如订单ID)做Hash,把同一个订单的消息发到同一个Partition。消费者为了保证分区内顺序,通常给每个分区分配单独线程或串行处理。有一个容易踩的坑:生产者如果开启了重试,消息顺序可能被打乱,所以要在幂等开启的前提下设置max.in.flight.requests.per.connection为5,或者干脆设成1来保顺序。
RocketMQ实现方式:生产者用MessageQueueSelector选择消息发到哪个Queue,例如select((mqs, msg, arg) -> mqs.get(Math.abs(("orderId:" + arg).hashCode()) % mqs.size()), orderId),消费者使用MessageListenerOrderly监听。MessageListenerOrderly机制上会锁住消息处理队列,确保同一个Queue的消息被串行消费。
顺带提醒:顺序消息和消费并发度是矛盾的,同一个队列只允许一个消费者线程消费,如果某个订单对应的大量消息都走一个队列,那么吞吐会偏低。解决思路是不要把所有消息都塞进一个队列,而是让不同订单足够分散,让尽可能多的Queue分摊压力。
3.3 性能基线与实践调参
网上有很多性能对比数据,但脱离环境谈性能都是耍流氓。我这里给一个相对靠谱的经验区间(磁盘是SSD、消息大小1KB、副本数3、生产环境压力测试):
| 场景 | Kafka | RocketMQ |
|---|---|---|
| 生产者同步发送单Broker | 10万~30万条/秒 | 5万~15万条/秒 |
| 开启压缩+批量异步发送 | 会明显更高 | 有一定提升,但不建议过度调大批量 |
| 消费者吞吐 | 依赖分区数和消费并发,通常消费端容易成为瓶颈 | 依赖Queue数量和消费线程池,回调模式方便并发 |
看到这个数据,有人会直接下结论说Kafka更快。但我觉得要看你付出的成本:Kafka为了追求吞吐,客户端参数需要细致调优,比如batch.size设到32KB~64KB,linger.ms设到5~20,用lz4或zstd压缩。而RocketMQ默认配置就适合业务场景,发送同步、消费回调,开发人员不需要花费大量精力在调优上。如果你每天消息量只有几百万甚至几十万,那性能差异根本感知不到,业务功能是否顺手才是第一位的。
4. 实战场景与选型卡位:什么时候该换、什么时候别换
4.1 场景画像对照
| 典型场景 | 更适合谁 | 我的理由 |
|---|---|---|
| 日志采集、大数据管道、数据接入湖仓 | Kafka | 数据量大、保留时间长、生态与Flink/Spark无缝集成 |
| 流式计算、事件驱动架构 | Kafka | 多消费组、分区重放、配合流处理引擎方便 |
| 订单、支付、库存异步化 | RocketMQ | 需要事务消息、延迟关单、Tag过滤,业务语义齐全 |
| 消息削峰填谷,下游容易抖动 | RocketMQ | 消费失败重投和死信队列更明确,业务排查方便 |
| 配置下发、全量订阅 | RocketMQ | 广播消息原生支持,Kafka要用多GroupId模拟 |
| 需要按消息内容精确过滤 | RocketMQ | Tag和SQL92过滤,Kafka只能在客户端做,浪费带宽 |
多数业务系统用RocketMQ确实更省心,但如果你做得是实时数仓,那大概率还是离不了Kafka。最怕的是团队听到“某大厂都在用XX”就强行切换,不考虑自身场景。
4.2 迁移决策清单
已经使用Kafka,但心里在摇摆的团队,建议先做一次评估。可以拿下面五条做个自测:
- 峰值流量是不是真的需要每秒百万级吞吐?
- 消息要不要保留超过7天甚至更久?
- 下游是否大量使用Flink、Spark Streaming做实时计算?
- 业务里有没有大量的延迟消息、事务消息、标签过滤需求?
- 消费端是不是经常因为Kafka重平衡出现几分钟的消费中断?
如果你的业务明显偏向4、5,但1、2、3都回答“不”,那从Kafka迁到RocketMQ大概率是值得的。反过来,如果3是核心诉求,或者数据量动不动就上亿千万,那RocketMQ的长期运行成本和存储模型会让你头疼,建议不要轻易动。
5. 从Kafka到RocketMQ迁移实战:一次可以落地的操作流程
5.1 迁移前准备和双写方案设计
我以前接手过一个中间状态的项目,Kafka上堆了业务消息、日志消息、事件消息,业务方每天投诉延迟消息不可用,消费重平衡频繁。当时定了迁移到RocketMQ的目标,但执行时没有做“一刀切”,而是先做了双写。
双写方案核心思路:
- 在业务发送层抽象一个消息发送接口,通过开关控制三种模式:只发Kafka、只发RocketMQ、同时发两边。
- 先开启双写,但消费端仍然只消费Kafka,让RocketMQ这边作为备份和校验,确认两边消息数、消息内容对齐。
- 观察一段时间后,开启一小部分灰度的消费组消费RocketMQ,比较处理结果和Kafka消费结果是否一致。
- 等所有校验通过,再按业务板块逐步切换,把接收方的消费组全部切到RocketMQ,发送端的开关始终保留可回退能力。
这个过程中最容易忽视的是历史数据。Kafka本身保留的消息如果时间不长,其实不建议做全量搬迁,因为消息没有数据库那样的行级更新能力,历史数据复制过去也未必有消费价值。更稳妥的做法是:让老Kafka消费组继续跑一会儿,只处理尚未消费完的积压数据,新业务从切换时间点开始走RocketMQ。
5.2 集群搭建和常用命令参考
迁移的前提是新集群要跑得稳。RocketMQ部署没有Kafka那么依赖ZooKeeper,但NameServer同样建议至少两台。Broker推荐主从结构,刷盘模式平时用ASYNC_FLUSH就可以,给核心Topic单独开同步复制也行。搭建时留意存储目录的空间和文件秒级清理策略,默认72小时没跑几次就可能把磁盘写满。
几个常用的操作命令可以收藏:
# RocketMQ 创建Topic,p/r/w分别表示读队列、写队列数 sh mqadmin updateTopic -n localhost:9876 -t ORDER_TOPIC -c DefaultCluster -p 8 -r 8 -w 8 # 查看消费组进度,Diff就是堆积量 sh mqadmin consumerProgress -n localhost:9876 -g ORDER_CONSUMER # 按时间重置消费位点,时间戳是毫秒 sh mqadmin resetOffsetByTime -n localhost:9876 -g ORDER_CONSUMER -t ORDER_TOPIC -s 1576636800000 # 查看消费者连接和消费状态 sh mqadmin consumerStatus -n localhost:9876 -g ORDER_CONSUMERKafka侧排查时常用:
# 创建Kafka topic kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 3 --partitions 8 --topic ORDER_TOPIC # 查看消费组延迟 kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group ORDER_CONSUMER # 查看Topic当前最新位点 kafka-run-class.sh kafka.tools.GetOffsetShell --bootstrap-server localhost:9092 --topic ORDER_TOPIC --time -1注意不同版本命令参数有差异,尤其是RocketMQ 5.x和4.x,配置项和命令风格都有调整,最好以你部署版本的官方命令为准。
5.3 客户端适配层与关键代码
迁移阶段强烈建议做一个消息发送抽象层,不要直接在业务代码里调用某个中间件的Producer,否则后面切来切去会疯掉。最简化的接口大概是这样:
public interface BizMessageProducer { SendResult sendOrderly(String topic, String tag, String key, byte[] body); SendResult sendDelay(String topic, String tag, String key, byte[] body, long delayMs); }然后分别实现KafkaBizMessageProducer和RocketMQBizMessageProducer,通过配置文件决定当前注入哪个实现。双写阶段可以做一个组合实现:
public class DualBizMessageProducer implements BizMessageProducer { private final KafkaBizMessageProducer kafkaProducer; private final RocketMQBizMessageProducer rocketMQProducer; private final boolean rocketEnable = config.get("switch.rocket.enable", false); public SendResult sendOrderly(...) { SendResult kafkaResult = kafkaProducer.sendOrderly(...); if (rocketEnable) { rocketMQProducer.sendOrderly(...); } return kafkaResult; } }关键点是不要为了双写把一条消息的发送链路变成强依赖,否则某一边故障会影响主链路。双写阶段可以用异步方式把新MQ作为影子系统,等确认稳定后再调成同步发送。消费端也一样,可以保留旧Kafka消费者,只把新的RocketMQ消费者作为一个影子消费组去校验数据,跑通了再切流量。
5.4 回滚预案与风险控制
迁移过程中一定会遇到各种意外,我的经验是回滚开关一定要留,而且要早留。比较实用的风险控制手段:
- 发送端的开关要在配置中心里,不要埋在代码里要求重启,否则紧急回滚会把人急死。
- 双写阶段消息可能两边都收到,如果消费端没有做幂等,就会产生重复业务操作。解决思路是建立统一的
message_id,在业务落库时做唯一索引或Redis去重。 - 从Kafka切到RocketMQ后,消费进度体系要重新熟悉,RocketMQ的消费位点是存在Broker端的,重置位点比Kafka简单,但也意味着如果你误操作,影响面会很大的。
- 迁移期间不建议立刻下线Kafka集群,至少保留一个业务周期,比如7天,保证一旦发现RocketMQ方向性问题,还能重新消费Kafka上的原消息。
6. 常见问题与排查技巧实录
6.1 消费堆积排查思路
不管是Kafka还是RocketMQ,消费堆积不能一上来就怪中间件。我通常按这个顺序查:
- 先看堆积量在哪一级:Kafka用
kafka-consumer-groups.sh --describe看LAG,RocketMQ用mqadmin consumerProgress看Diff。 - 再看消费者处理耗时:把消费者处理单条消息的耗时打点,看P99。如果耗时从10ms涨到500ms,那下游数据库、远程调用大概率是瓶颈。
- 排查消费者线程数设置:Kafka的消费线程能力和分区数强相关,消费者数量小于分区数时,一部分分区是闲置的;RocketMQ消费线程池默认20个线程,如果Queue数小于线程数也没关系,但Queue数小于消费者个数时会有空闲消费者。
- 看客户端是否有频繁Rebalance:Kafka消费组如果频繁协调,日志里能看到
JoinGroup、SyncGroup反复执行,这时候堆积不是因为下游慢,而是暂停消费让出来的。
堆积不是病,快速消除堆积需要的是扩容消费者、批量消费、关掉无用过滤器。
6.2 消息丢失与重复消费
消息“丢了”大多是配置不当或使用方式不对。Kafka常见丢消息场景:生产端用了acks=0或acks=1,Broker发生主从切换时未同步数据直接丢;消费端把auto.offset.reset设成了latest,新消费组上线就跳过老消息。RocketMQ常见丢消息场景:同步发送失败但业务没感知,或者使用了sendOneway;Broker异步刷盘,机器断电刚好丢内存页。
重复消费则更普遍。Kafka和RocketMQ都是“消费成功后提交位置”模式,只要业务处理成功瞬间客户端宕机,重启后就会再消费一次。靠消息队列本身去重几乎不可能,最好在业务侧建立幂等机制。比如消费端根据业务唯一键查库,存在则跳过;或者用Redis的SETNX判重。老生常谈,但实在重要:所有交易类场景,消费端都必须做幂等。
6.3 顺序消息被打乱
很多人以为用了顺序发送+顺序消费就高枕无忧了,其实还是有很多窗口期。比如Kafka生产者发送第一条消息失败后重试,如果max.in.flight.requests.per.connection大于1,后续消息可能先到;再比如RocketMQ使用MessageQueueSelector时,如果发送失败后自动重选了队列,那就等于跳到了另一个队列。
我的经验是:避免在消息队列层追求强顺序,一旦吞吐上不去,排障很难。更好的方案是消息体里带一个版本号,消费端做“旧版本丢弃”;或者把顺序状态放到Redis/数据库里,真正业务状态机一旦发现当前消息版本小于已处理版本就丢弃。这样即使队列短暂乱序,也能兜住。
6.4 运维避坑记录
最后聊几个只有长期维护才会发现的坑。
Kafka这条:分区数一旦增加千万不要随便减少,减少分区会导致数据重新分配和顺序语义彻底错乱。消息保留时间不能只靠“log.retention.hours”这一个参数去调,因为还有log.retention.bytes在作用,两个条件只要满足一个就会清理。
RocketMQ这条:主从同步复制模式下,如果从节点磁盘很慢,写入会被拖垮。我遇到过一次因为从节点和主节点不同机房、内网延迟又高,导致正常的异步写主瞬间堆积,最后把主从复制模式降级才恢复。另外Tag过滤虽然好用,但Tag数量不要搞得太多,否则思路不清晰,甚至出现同Topic下发大量不同Tag导致ConsumeQueue索引膨胀。
另外,不管是哪一套,磁盘告警都要前置。消息队列最怕的不是CPU高,而是磁盘被日志文件撑爆。Kafka有大量Segment文件,RocketMQ也有CommitLog。如果磁盘剩余少于20%,要提前处理过期日志和保留策略,千万别指望默认清理会在磁盘满的时候救你,它经常是“来不及”。
我自己这些年用下来最大的体会是:真正难的不是选型,而是把业务对消息的语义模型想清楚。如果你一开始就把消费幂等、顺序边界、延迟消息的粒度定好,Kafka和RocketMQ之间的迁移会轻松不少。有一段时间我同时维护着两套:Kafka承接所有日志采集和事件流,RocketMQ只负责交易链路相关的业务消息。刚开始觉得双栈维护成本高,后来发现这反而是最舒服的形态,因为每一类消息都放在了自己最匹配的位置上。
最后再分享一个小技巧:无论是Kafka还是RocketMQ,都要在测试环境把“消费端重启”“Broker宕机”“网络闪断”这三个演练做一遍。我每次带着团队做故障演练,都能揪出一些平时监控根本看不出来的问题。消息队列这东西,平时看着稳,真正出问题的时候,留给你反应的时间往往只有几分钟,提前把预案做扎实,比什么都值钱。