做游戏饰品交易这行,最怕的不是流量小,而是流量突然爆发时消息中间件先倒。我们平台“悠悠有品”从日活几千到高峰期每秒上万笔请求,中间件的选型和演进踩了不少坑。最终定下来的方案很清晰:核心交易链路用 RocketMQ 稳扛,海量行为数据和日志管道交给 Kafka 驱动。这篇文章把整套技术选型逻辑、落地细节和排查过程完整写出来,希望能给正在做高并发交易平台的朋友一些参考。
先说结论:RocketMQ 和 Kafka 不是二选一的关系,而是各管一段。RocketMQ 负责订单、支付、发货这类强一致性和事务性要求极高的核心交易链路,Kafka 负责埋点、日志、行为数据这种海量写入和吞吐优先的数据管道。两者配合下来,系统扛住了多次大促和热门饰品首发活动的流量高峰。
1. 业务画像与消息选型逻辑
1.1 游戏饰品交易平台的三个典型特征
悠悠有品做的是游戏饰品交易,比如 CS2 的皮肤、DOTA2 的饰品、各类游戏的装备和道具。这类平台和传统电商最大的区别在于商品是虚拟资产,单价高、流动性强、用户价格敏感度高,几个特征直接决定了技术架构的走向。
第一个特征是请求量脉冲式暴涨。热门饰品上新、比赛结束后的价格波动、大主播带货,都会在几分钟内把流量拉高几十倍。平时每秒一千笔请求的系统,瞬间可能要扛住每秒上万笔。这种场景下,消息中间件如果撑不住,订单就直接丢失,用户资金和饰品都会出问题。
第二个特征是业务链路长且状态多。一笔交易从用户下单、卖家确认、饰品暂扣、买家付款、平台担保、饰品发放到最终确认收货,中间涉及十几个状态变更。任何一个环节的消息丢失或乱序,都会造成资损级别的故障。
第三个特征是海量行为数据需要留存分析。用户浏览了哪些饰品、搜索了什么关键词、点击了哪个商品详情页、 在哪个页面停留了多久,这些数据每天产生几十亿条,必须全部收集起来用于推荐、风控和运营分析,任何一条都不能丢,但延迟几百毫秒完全可以接受。
这三个特征叠加在一起,单一消息中间件很难同时满足。强一致、强事务、状态机复杂的交易链路要的是“稳”,海量写入、高吞吐、允许适度延迟的数据管道要的是“快”。RocketMQ 和 Kafka 的组合,本质上是为这两个截然不同的需求分别选择最合适的工具。
1.2 为什么交易链路选 RocketMQ 而不是 Kafka
早期团队也有人提过用 Kafka 一把梭,毕竟 Kafka 吞吐高、生态好、社区活跃。但真正把交易场景的需求列出来后,RocketMQ 的优势就非常明显了。
事务消息是第一个决定性因素。交易系统里有一个非常经典的问题:本地数据库事务和消息发送如何保证原子性。比如用户下单时,需要同时写入订单表、扣减卖家库存、冻结买家资金,还要发送一条消息通知后续系统。如果先写库再发消息,消息发送失败就要回滚数据库;如果先发消息再写库,下游系统可能拿到消息时订单还不存在。RocketMQ 的事务消息机制解决了这个问题,通过 half 消息加本地事务状态回查,保证了消息和数据库操作的最终一致性。Kafka 原生没有事务消息,虽然可以通过回调接口模拟,但实现复杂度和可靠性都差很多。
顺序消息是第二个关键点。饰品交易中同一笔订单的消息必须严格有序,比如“已支付”消息不能比“已下单”消息先到,“已发货”消息不能比“已支付”先到。RocketMQ 支持队列级别的顺序消息,把同一业务 ID 的消息路由到同一个队列,消费者按顺序拉取。Kafka 虽然也能通过分区实现局部有序,但分区数量和消费者并发度的关系很微妙,一旦消费者数量超过分区数,顺序就会被打破。
延迟消息是第三个加分项。交易平台有大量超时场景:用户下单后 30 分钟未支付自动取消,卖家发货后 48 小时未确认自动完成。RocketMQ 内置 18 个延迟级别,直接设置即可使用。Kafka 本身不支持延迟消息,需要自己实现时间轮或者用 Redis 延迟队列做兜底。在已经足够复杂的交易系统里,少一个自研组件就少一份维护成本。
还有一个非常现实的考量:RocketMQ 是 Java 写的,我们团队后端主力是 Java 技术栈,遇到底层问题可以直接看源码修复,出现问题也能通过社区快速联系到核心维护者。Kafka 是 Scala 写的,虽然问题不多,但真到了需要改源码的程度,团队投入成本就高了。
1.3 数据链路为什么离不开 Kafka
交易链路选完 RocketMQ 后,数据管道这边几乎没有任何争议地选了 Kafka。原因很直接:吞吐量差距明显,生态配套成熟。
游戏饰品平台的用户行为数据量极其庞大。用户每打开一次 App,首页曝光、列表滑动、详情点击、价格曲线查看、卖家主页访问等至少产生十条以上的埋点数据。按 100 万日活、人均 200 条埋点计算,一天就是 2 亿条数据,高峰期每秒产生几万甚至十几万条写入。RocketMQ 并不是不能扛这个量级,但在同样资源消耗下,Kafka 的吞吐明显更高。Kafka 采用顺序写磁盘和零拷贝技术,单分区顺序写性能可以跑到每秒百万条级别,这是日志型数据管道的天然优势。
Kafka 的生态也是重要因素。我们的大数据链路是 Flink 做实时计算、Elasticsearch 做搜索和推荐、ClickHouse 做 OLAP 分析、HDFS 做离线数仓,这些组件基本都有成熟的 Kafka Connector 和官方集成方案。Flink 消费 Kafka 写入 Elasticsearch 是标准得不能再标准的链路,配置一下就能用。RocketMQ 虽然也有这些生态,但成熟度和坑的数量还是比 Kafka 多一些。
此外,Kafka 的消息堆积能力极强。数据管道经常出现下游短暂故障的场景,比如 Elasticsearch 集群重启、Flink 任务发布新版本,消息在 Kafka 里堆积几亿条完全没问题,恢复后继续消费即可。Kafka 的数据保留策略也灵活,可以按时间和大小双重控制,消息过期自动清理,不需要人工干预。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心交易链路:RocketMQ 的稳字诀
2.1 订单主流程的消息模型设计
先画一下订单主流程的简化模型,大家感受下消息在交易链路中是怎么流动的。用户下单时,订单服务在本地数据库写入订单记录,同时发送一条“下单成功”的 RocketMQ 消息。库存服务消费这条消息,预扣饰品库存,然后发送“库存锁定成功”消息。支付服务消费消息后,向用户发起支付请求。用户支付成功后,支付回调服务发送“支付成功”消息,订单服务消费后更新订单状态,同时发送“待发货”消息触发卖家发货流程。卖家发货后,仓库服务发送“已发货”消息,买家确认收货后,订单服务发送“已完成”消息,整个交易闭环结束。
这个模型里,每条消息都对应一个明确的状态变更事件,消费者收到消息后只做自己职责范围内的事情,服务之间完全解耦。核心设计原则有两个:消息即事件、事件驱动状态机。
所谓消息即事件,是指消息内容必须完整描述发生了什么,而不是告诉下游“去做什么”。比如“支付成功”消息里携带订单号、支付金额、支付时间、支付方式,下游服务拿到这些信息自己决定怎么处理。如果消息内容是“去发货”,那么下游服务每次都要回来查订单数据,一旦查不到就卡死,整个链路就僵硬了。
所谓事件驱动状态机,是指订单的每个状态变更都由对应的事件触发,状态之间的流转必须有明确的事件来源。这样设计的价值在于可追溯,任何一笔订单出问题,都能通过消息轨迹查到它经历了哪些状态、每个状态是谁触发的、耗时多久。RocketMQ 自带的消息轨迹功能在这里发挥了很大作用,排查线上问题时能直接看到消息的完整生命周期。
2.2 事务消息机制拆解与踩坑
事务消息是 RocketMQ 交易场景落地中最核心的机制,也是我强烈建议所有做交易系统的团队认真理解的部分。它的工作原理分三个阶段:
第一阶段,生产者发送 half 消息(半消息)到 Broker。这个消息对消费者不可见,但已经被 Broker 存储下来。此时下游系统完全感知不到任何变化,相当于“预告”。
第二阶段,生产者执行本地事务。比如订单服务在同一本地事务里写入订单记录、锁定库存、冻结资金。如果本地事务执行成功,生产者向 Broker 提交 commit 请求,half 消息变为可见,消费者可以正常消费。如果本地事务执行失败,生产者发送 rollback 请求,Broker 删除 half 消息。
第三阶段,事务回查。这里有个隐含的风险点:生产者执行第二阶段时可能宕机,比如本地事务执行到一半进程挂了,或者本地事务成功但 commit 请求没发出去。此时 Broker 会定期回查生产者,询问这条 half 消息对应的本地事务结果。生产者需要实现一个回查接口,根据本地事务的状态返回 commit 或 rollback。
实际踩过的坑主要有两个。第一个坑是回查接口的实现不幂等。回查可能被 Broker 多次调用,而且可能在你本地事务还没完成时就来查。最初我们实现回查时直接查数据库订单状态,查不到就返回未知状态,导致消息一直卡在 half 状态,下游迟迟收不到。后来改成回查接口先查本地事务执行记录表,这个表在下单事务里同步写入,查询结果只有成功、失败、未知三种。未知状态返回后 Broker 会延迟一段时间再查,直到拿到明确结果。
第二个坑是事务消息的消费端要考虑消息可能延迟到达。因为事务回查需要时间,理论上 half 消息从发送到可见可能延迟几秒甚至更久。我们最初假设“发送事务消息后下游马上能消费到”,导致部分订单的状态流转在极端情况出现几秒的延迟。后来对账逻辑里专门加了对“消息延迟到达”的兼容处理,才算彻底解决。
2.3 顺序消息:防止饰品发货乱序
交易系统的顺序消息场景比想象中多。同一款饰品的多个订单之间不需要严格有序,但同一笔订单内部的流程消息必须严格有序。举个实际案例:用户购买一个饰品后,卖家在后台操作“发货”,系统先发送“发货中”消息,再发送“已发货”消息。如果这两条消息的顺序颠倒,买家端页面会先看到“已发货”再看到“发货中”,体验极其怪异,后台状态机也可能出错。更严重的是饰品释放和冻结的操作消息如果乱序,可能导致饰品被重复出售或者用户资金被多扣。
RocketMQ 实现顺序消息的关键是 MessageQueueSelector。生产者在发送消息时需要实现这个接口,根据业务 ID 计算出固定的队列编号,保证同一业务 ID 的消息永远发到同一个队列。我们平台的实现逻辑是根据订单号取模,同一个订单号的所有消息都路由到同一个队列。消费者端使用一个固定线程池消费队列消息,或者对同一个订单号的消息加锁串行处理。
这里的坑在于消费者并发度的控制。顺序消息的队列级并发度天然有限,如果一个队列里堆积了大量消息,而这个队列的消费逻辑中还有一个慢操作,就会拖慢整个队列的消费速度。我们实际遇到的情况是,某个热门饰品的订单量暴增,同一个卖家的大量订单消息被路由到同一个队列,而卖家的发货操作依赖外部仓储接口,偶尔会超时几秒,导致该队列消费速度一度降到每秒几百条,消息堆积越来越严重。
最终解决方案是双层设计。第一层 RocketMQ 保证队列级顺序,第二层消费者内部维护一个基于业务 ID 的分组队列。消费者从 RocketMQ 拉取消息后,按业务 ID 分发到内存中的多个小队列,由多个线程并行处理分组队列。这样一来,同一订单内的消息保持顺序,不同订单之间的消息可以并行消费,把并发度提升了近十倍。
2.4 延迟消息的实际用法
延迟消息在交易平台最常见的场景是超时关单。用户创建订单后,如果 30 分钟内未完成支付,系统需要自动取消订单并释放锁定的饰品库存。最初我们用定时任务扫描数据库实现,每天凌晨跑一次全表扫描,到了高峰期全表扫描的延迟和数据量都不太理想。后来对流程做了优化:创建订单时发送一条延迟 30 分钟的消息,Broker 30 分钟后投递给消费者,消费者检查订单是否已支付,如果未支付则执行关单操作。
这里需要反复提醒的是,RocketMQ 的延迟消息只支持固定的 18 个级别,分别是 1s、5s、10s、30s、1m、2m、3m、4m、5m、6m、7m、8m、9m、10m、20m、30m、1h、2h。也就是说,你只能选择其中一个级别,不能自定义任意秒数。我们需要的 30 分钟恰好是第 16 级,所以直接用就可以了。但如果业务上需要 25 分钟、45 分钟这种自定义时间,就得换方案了。
延迟消息还有一个容易踩的坑:Broker 端的时间精度。延迟消息并不是严格精确到秒级的,实际投递时间可能会有几秒到几十秒的误差。对于超时关单这种容忍度较高的场景足够,但如果业务对时间精度要求很高,比如限时抢购的商品必须在精确时间上架,就不能依赖延迟消息,需要自己实现定时调度。
另外,延迟消息在控制台上显示的消息状态是“正在延迟”,消息轨迹能看到延迟投递的级别数,排查问题时先确认级别配置是否正确。我们曾经出现过测试环境把延迟级别配成 2(5s),导致关单消息比预期快了 6 倍的情况,查了很久才发现是配置问题。
3. 海量数据链路:Kafka 的高吞吐实践
3.1 埋点日志管道:从接入到落仓
Kafka 在悠悠有品的数据链路中承担的第一项任务是用户行为埋点收集。这个管道的数据流大概是这样:App 端和 Web 端通过 SDK 上报埋点数据到统一的接入网关,网关将数据简单清洗后以 JSON 格式写入 Kafka 的埋点主题,下游的 Flink 任务实时消费这些数据,一部分写入 Elasticsearch 供搜索和推荐使用,一部分写入 ClickHouse 用于实时报表和运营分析。
主题设计方面,我们没有按事件类型拆太细,而是统一放在一个大的埋点主题里,通过消息中携带的 event_type 字段区分曝光、点击、搜索、收藏等事件类型。这样设计的优势是写入端的逻辑简单,不需要感知业务类型,下游消费端再根据自己的需求过滤和分流。劣势是单个主题的数据量非常大,流量高峰期 Topic 的吞吐压力比较高,需要保证分区数量足够。
分区规划的原则是参考目标吞吐量和消费者并行度。我们按照单分区每秒能扛 50MB 写入的经验值来估算,高峰期每秒需要写入大约 200MB 数据,于是把埋点主题设置成 32 个分区。这样高峰期单分区写入压力在 6MB 左右,留了充足的余量。分区数也不是越大越好,分区越多,Broker 端的文件句柄和内存占用越高,消费端如果并行度跟不上,反而浪费资源。
消息内容设计上要尽量减少无效字段。早期接入时埋点数据里什么字段都往消息里塞,一条消息能达到几 KB,严重浪费 Kafka 的写入带宽。后来推行埋点规范,所有事件只保留通用字段加业务字段,通用字段包括事件 ID、用户 ID、会话 ID、时间戳、设备信息、页面信息,业务字段按事件类型最小化设计,最终单条消息平均从 3KB 降到了 800B,写入吞吐直接翻了好几倍。
3.2 Kafka 集群规划与参数调优
Kafka 集群的硬件规划和参数调优直接决定数据链路的稳定性。我们的生产集群配置是 9 台物理机,每台机器配置 32 核 CPU、128GB 内存、4 块 1.92TB NVMe SSD。9 台机器中 3 台同时担任 Controller 角色,其他 6 台是纯 Broker。每个 Topic 的副本数设置为 3,保证单台机器宕机不影响数据读写。
磁盘选择上强烈建议用 SSD,Kafka 虽然主要是顺序读写,但消费端会有大量随机读取场景,特别是消费落后需要补数据时,HDD 的随机读性能会成为瓶颈。我们用 NVMe SSD 后,消息延迟从最高的 200ms 降到了 20ms 以内,效果非常明显。
关键的 Broker 参数配置如下:log.retention.hours 设置为 24 小时,数据保留一天足够满足绝大部分业务场景的需求;num.partitions 默认值调成 8,避免创建主题时忘记指定分区数导致分区过少;default.replication.factor 默认值改成 3,防止新建主题时不带副本因子导致数据无冗余。这些都是生产环境血泪换来的教训,默认配置真的不能直接用于生产。
生产者端的参数调优同样重要。acks 设置成 all,确保消息写入所有副本才算成功,防止 Leader 副本宕机导致消息丢失。enable.idempotence 设置成 true,开启幂等性,避免网络重试造成消息重复写入。batch.size 调到 64KB,linger.ms 设置为 20ms,这两个参数控制在吞吐和延迟之间的平衡。对于离线数据管道,可以稍微牺牲一点延迟换取更高的吞吐;对于实时推荐场景,linger.ms 不能设太高,否则消息延迟过大,推荐结果就不够实时了。
消费者端最常见的坑是 auto.offset.reset 配置。如果这个参数设置成 earliest,当消费组第一次启动或者找不到 offset 时,会从主题最早的消息开始消费,可能瞬间把大量历史数据灌到下游。如果设置成 latest,又可能漏掉消费者启动期间的新消息。我们的策略是大部分场景设置成 latest,只有需要回放历史数据的场景专门设置成 earliest。
3.3 消费者端常见的三个延迟问题
Kafka 消费延迟是数据链路最常被问到的故障,说三个我们实际遇到过的典型问题。
第一个问题是消费组重平衡导致长时间消费停滞。原因是消费者的 poll 处理时间超过了 max.poll.interval.ms 默认值(5 分钟),服务端认为消费者失联,触发了重平衡。我们的一个 Flink 任务在写入 Elasticsearch 时遇到集群高峰期,批量写入超时时间设得过长,单个批次的处理时间超过了 5 分钟,导致整个消费组反复重平衡,消息延迟从秒级飙升到小时级。解决方法是用独立线程池做异步批量写入,Kafka 消费线程只负责快速拉取消息并提交到线程池,把 poll 的阻塞时间压到毫秒级。
第二个问题是消费线程数大于分区数导致部分消费者空闲。刚开始做消费端扩容时,我们直接将一个消费组的线程数从 4 个调到 16 个,以为这样消费速度就能提升 4 倍。实际上 Kafka 的消费并行度由分区数决定,一个分区同一时刻只能被同一个消费组内的一个消费者消费。如果分区数是 8,即使消费组有 16 个消费者,也只有 8 个在干活。真正的扩容方式是增加分区数,让每个消费者至少分到一个独立分区。
第三个问题是系统资源竞争导致消费者性能下降。数据管道中 Kafka 和 Flink 部署在同一批机器上,高峰期 Flink 任务的 CPU 和内存使用率拉满,Kafka 消费者的处理能力跟着下降。后来做资源隔离,Kafka 集群独占机器,计算任务单独部署,问题迎刃而解。这里想提醒大家,消息中间件是高并发系统的核心基础设施,负载隔离非常关键,不要为了省机器把消息集群和应用混部在一起。
4. 双消息引擎的部署与可视化运维
4.1 开发环境快速搭建:Docker 方案
开发环境和测试环境的消息中间件部署,我们统一用 Docker Compose 方案,简单、可复用、无污染。RocketMQ 的 Docker 部署文档踩坑比较多,我直接给出我们验证过可用的一份配置。
RocketMQ 4.9 以上版本依赖 JDK 8+,Docker 镜像官方推荐 apache/rocketmq,但这个镜像默认不包含控制台,需要额外单独运行 rocketmq-dashboard 容器。NameServer、Broker、Dashboard 三个容器之间通过自定义网络互通,注意 Broker 需要映射 10909、10911、10912 三个端口,分别对应快速通道、主通道和 HA 通道。最容易出错的是 Broker 容器启动时如果没指定 autoCreateTopicEnable=true,生产者在发送消息时连不上主题就会报错。
这里需要特别说明的是,RocketMQ 的老版本 Broker 不推荐用 Docker 部署,因为容器化部署时文件映射和内存映射文件(MappedByteBuffer)的处理比较特殊,容易出现 IO 异常。如果只是本地开发调试,建议用二进制包直接启动,几分钟就能跑起来。
Kafka 的开发环境部署,我们用的是 KRaft 模式,也就是 Kafka 新版本去掉了 ZooKeeper 依赖的模式。Kafka 3.5 之后 KRaft 模式已经生产可用,单机开发环境用 KRaft 部署只需一个容器,省去了单独部署 ZooKeeper 的麻烦。Docker 部署 Kafka 时最容易踩的坑是容器内的监听地址和宿主机监听地址不一致,客户端从宿主机访问容器内的 Kafka 时,会报出 Error while fetching metadata with correlation id 之类的错误。
这个问题的根源是 Kafka 的 ADVERTISED_LISTENERS 配置。Kafka 返回给客户端的连接地址是 listeners 中配置的地址,如果配置的是容器内 IP,客户端在宿主机上就连接不上。解决方案是把 advertised.listeners 配置成宿主机局域网 IP 或者 localhost,具体用哪种取决于客户端从哪里访问。这个坑精准地出现在所有第一次用 Docker 部署 Kafka 的开发者面前,属于必须提前掌握的知识点。
4.2 生产集群的高可用部署注意事项
生产环境的 RocketMQ 和 Kafka 集群部署,我在实操中总结出几个必须注意的点。
RocketMQ 生产集群至少需要 4 个节点:2 个 NameServer + 2 个 Broker(Master-Slave 模式)。NameServer 是无状态的,可以多部署几个,客户端会从所有 NameServer 获取 Broker 路由信息。Broker 的 Master-Slave 模式有两种同步策略:同步双写(SYNC_MASTER)和异步复制(ASYNC_MASTER)。交易场景我们选择同步双写,Master 写入成功后要等 Slave 也写入成功才返回,虽然延迟会少量增加,但保证 Master 宕机后消息不丢失。
Broker 的存储路径必须使用独立的数据盘,不要跟系统盘共用。RocketMQ 的 CommitLog 文件按 1GB 大小预分配,长时间运行后会占用大量磁盘空间,我们要提前配置好磁盘监控和清理策略。RocketMQ 的磁盘写满后会自动停止写入,不会直接崩溃,但会影响业务请求,所以磁盘使用率要设置告警阈值,建议超过 75% 就触发通知。
Kafka 集群的 Controller 节点要格外关注。KRaft 模式下,Controller 负责维护集群元数据、分区 Leader 选举等关键操作,一旦 Controller 节点故障,整个集群的元数据操作会中断。生产环境至少部署 3 个 Controller 节点,通过选举协议选出活跃 Controller,其他节点作为备用。Controller 节点的 JVM 堆内存要给足,因为集群元数据都保存在 Controller 的内存中,分区和主题越多,内存占用越大。
监控方面,两个引擎都要接入 Prometheus 和 Grafana。RocketMQ 的 exporter 能收集 Broker 的消费堆积量、生产消费 TPS、存储使用率、网络吞吐等指标。Kafka 的 exporter 更成熟一些,通过 JMX 接口暴露了非常丰富的指标,包括消息堆积量、请求延迟、网络吞吐、分区状态等。告警规则里最核心的三条:消费堆积量超过阈值、Broker 节点不可用、磁盘使用率超过阈值,这三条出了问题都是影响线上业务的大事。
4.3 可视化工具与排查窗口
消息中间件的可视化运维能节约大量排查时间,这里说两个我们实际在用的工具。
RocketMQ 的控制台使用非常高频,名为 rocketmq-dashboard(大家搜索时也会看到它的老名字 rocketmq-console-ng)。它能查看 Topic 列表、消费组消费进度、消息详情、消息轨迹,还支持发送测试消息和对死信队列做重发操作。Dashboard 的 Docker 打包时最常见的报错是 Java 下载 SSL 异常导致 Maven 构建失败,解决办法是给 Maven 配置阿里云镜像仓库,或者直接下载已经打包好的 release 版本。控制台定位线上消息问题非常方便,比如“用户反馈订单状态不对”这类问题,在 Dashboard 里输入订单号,选择对应 Topic,就能看到这条消息的发送时间、消费时间和消费结果。
Kafka 边缘侧的可视化工具推荐用 Offset Explorer(原 Kafka Tool),它支持连接本地单机 Kafka 和远程集群,界面里能直接查看主题列表、分区信息和消息内容。Offset Explorer 连接单机 Kafka 时有一个需要注意的点,如果用的是 KRaft 模式部署的 Kafka,需要在连接配置里选择正确的安全协议,如果 Borker 配置了 SASL 认证,还要填入对应的认证信息。
另外一个 Kafka 生态的常用工具是 kafka-ui,web 界面比 Offset Explorer 更友好,支持多集群管理和消息浏览,还可以直接在界面上模拟生产者发送消息,对压测和联调都非常方便。我们的数据开发团队一致推荐 kafka-ui 作为日常数据排查的标配工具。
5. 高并发场景下的排查实录与避坑清单
5.1 消费堆积与消息延迟的排查思路
消息堆积是所有消息中间件部署后必然会遇到的问题,我对这个问题的应对思路分四步走。
第一步判断堆积范围。先通过监控平台看堆积量是全局增长还是单个 Topic 或单个消费组增长。全局增长优先怀疑 Broker 节点负载过高或磁盘 IO 出现瓶颈;单点增长优先怀疑对应的消费端服务出现异常。我们曾经遇到过一次 RocketMQ 的 Broker 节点频繁 Full GC,导致所有 Topic 的消息发送延迟大幅上升,全局堆积量暴涨,当时通过监控看到 Young GC 和 Full GC 频率异常,立刻给 Broker 扩容 JVM 内存并调整 GC 参数,问题很快缓解。
第二步定位慢消费者。如果堆积只出现在某个消费组,优先查看这个消费组对应服务的日志,看是否有远程调用超时、数据库慢查询、下游接口报错之类的异常。Kafka 的消费组检测还可以看消费者的 lag 值变化趋势,如果 lag 持续上升,说明消费者的消费速度低于生产速度,需要优化消费逻辑或者增加消费者数量。
第三步看消费逻辑有没有可优化的阻塞点。常见的慢消费逻辑包括:消费消息时同步调用外部 HTTP 接口、消费消息时同步写数据库、大批量消费时一次性处理过多消息。这些场景的优化思路分别是异步化、批量化和分片化。我们有一个消费者原本在消费每条消息时都会查询一次用户信息服务,高峰期每次查询耗时 300ms 左右,消费速度被压到每秒几百条。后面改成批量查询加本地缓存,消费速度直接提升到每秒上万条。
第四步考虑临时扩容和降级兜底。如果堆积量短时间内无法消费完,而且影响到了业务,最直接的办法是临时增加消费者实例数量。RocketMQ 和 Kafka 的消费组都支持动态扩容,新消费者加入后会自动分担分区或队列的消费任务。另外,对非核心链路的消息可以做丢弃降级处理,比如一些运营统计类的日志消息,积压超过 2 小时就可以考虑跳过,不追历史数据。
5.2 幂等与重复消费的兜底方案
消息系统几乎不可能完全避免重复消息。RocketMQ 的 At Least Once 语义保证消息至少被投递一次,Kafka 的 Exactly Once 虽然可以做到端到端精确一次,但实现成本高且对下游要求苛刻,我们在实际生产中默认都是按有重复消息来设计的。幂等是消息消费者必须实现的第一优先级功能。
订单交易场景下的幂等实现方案主要是唯一键防重表。比如消费“支付成功”消息时,在本地数据库里建一张消息消费记录表,唯一键是订单号加事件类型。消费消息时先尝试插入这条记录,如果插入成功说明这条消息首次处理,继续执行业务逻辑;如果插入失败说明是重复消息,直接丢弃。这种方式简单可靠,只要数据库能保证唯一索引,幂等就一定能保证。
还有一种更轻量的方案是基于 Redis 的去重。先 SETNX 一个以消息 ID 为键的记录,设置过期时间比如 10 分钟,如果 SETNX 成功则处理消息,如果失败则跳过。这种方案适合对强一致性要求没那么高的场景,比如运营通知、积分变更这类非资损类的业务。Redis 方案的优势是性能好,劣势是如果业务处理时间超过过期时间,可能出现重复消费漏网的情况。
Kafka 场景下的幂等还需要额外注意 offset 提交的时机。很多团队处理消息时先执行业务逻辑再手动提交 offset,这是对的。但如果业务逻辑执行成功但 offset 提交失败,消费者重启后会从头消费这批消息,造成重复处理。在这种场景下,消费者端的幂等逻辑就显得更加重要,否则同样的业务操作会执行两遍。我们的兜底方案是,所有消费端的重要操作都必须实现幂等,这是硬性要求。
5.3 资源规划与压测要点
高并发系统上线前的压测必不可少,消息中间件的压测我建议关注三个指标:最大吞吐量、消息堆积能力和延迟分布。
压测时先确认集群的 Broker 配置和网络带宽是否够用。就是最简单的估算公式:单条消息平均大小乘以每秒消息数,得到每秒需要的吞吐量,再乘以副本数得到总的网络 IO 压力。如果总吞吐量已经接近集群网卡的上限,要提前做网络升级或者消息压缩。消息压缩建议在生产者端开启,RocketMQ 和 Kafka 都支持压缩算法,LZ4 在 CPU 开销和压缩率之间最为均衡,我们线上的消息压缩后体积减少了约 60%,网络压力大幅降低。
压测工具方面,RocketMQ 官方提供了 mqadmin 命令行工具,可以快速进行消息发送和消费的压测。Kafka 生态有 kafka-producer-perf-test 和 kafka-consumer-perf-test 这两个官方压测工具,使用简单,报告清晰。压测时要有意识地对 Topic 的分区数做阶梯测试,分别测 8、16、32、64 分区时的吞吐表现,找到性能和资源消耗的最优平衡点。分区数不是越多越好,分区太多时 Kafka 的元数据开销和内存占用会明显上升,可能反而拉低吞吐。
有一个压测时很容易忽略的点:消息体的大小分布。埋点和日志类消息大小分布比较均匀,但交易场景的消息大小波动可能非常大。压测产线最好同时模拟小消息(几百字节)和大消息(几十 KB)的混合流量,才能真实反映业务负载。另外,压测完的测试数据要及时清理,否则会造成磁盘空间的浪费,也可能影响后续的监控告警判断。
5.4 交易场景常见问题速查表
整理了一份我们在悠悠有品实际运维过程中总结的问题速查表,按消息引擎分类,方便大家排查时对照参考。
| 问题现象 | 消息引擎 | 常见原因 | 排查方向与解决方法 |
|---|---|---|---|
| 订单状态长时间不更新 | RocketMQ | 消息消费失败进入重试队列或死信队列 | 通过 Dashboard 查看消费组消费进度,检查死信队列中的消息,手动重发或修复消费逻辑 |
| 下游收到重复订单消息 | RocketMQ | 消费者处理慢导致消息重投,或本地事务回查机制触发重复投递 | 确认消费者执行了幂等处理,检查消费逻辑是否有耗时超过超时时间的操作 |
| 同一订单消息乱序 | RocketMQ | 生产端未正确实现 MessageQueueSelector,或者消费端并发度设置过高 | 检查生产端消息路由逻辑,确认同一业务 ID 是否路由到同一队列;降低消费端线程并发度 |
| 延迟消息提前到达 | RocketMQ | 延迟级别设置错误 | 检查消息的延迟级别配置,确认使用的是 16 级(30 分钟)而不是 15 级(20 分钟) |
| 消费组拉取消息报 cluster authorization failed | Kafka | 客户端配置的 ACL 权限不足 | 检查连接 Kafka 使用的账号是否具备对应 Topic 的读权限,在 Kafka 中配置正确的 ACL 规则 |
| Docker 部署 Kafka 客户端连不上 | Kafka | ADVERTISED_LISTENERS 配置的是容器内地址 | 将 advertised.listeners 配置为宿主机可访问的地址(如宿主机 IP 或 localhost),注意与 listeners 配置对应 |
| 消费延迟持续上升 | Kafka | 消费者处理逻辑阻塞或消费并发度不足 | 定位慢消费者,优化消费逻辑;检查消费组线程数是否小于分区数,必要时增加分区或消费者实例 |
| 消息消费者重启后重复消费大量消息 | Kafka | 关闭了自动提交 offset,但手动提交失败 | 检查 offset 提交逻辑,确保业务处理成功后可靠提交;消费端做好幂等处理 |
这个表格里的每个问题都是真实出现过的,有些甚至反复踩过好几次。最想提醒大家的是:不管选哪个消息引擎,幂等和可观测性永远是最重要的。消息中间件可以帮你扛住高并发,但最终保证系统正确性的是消费端的严谨设计和完善的监控告警。
最后分享一个从这套架构里沉淀下来的运维经验。消息中间件的高可用不只靠集群多副本,更要靠日常巡检。我们每周会固定检查 RocketMQ 和 Kafka 的消费堆积情况、Broker 磁盘使用率、集群 CPU 和内存水位,以及消息平均消费耗时,一旦发现趋势性变化就提前介入。做高并发系统时间久了会发现,大部分严重故障在爆发前都有蛛丝马迹,关键是你有没有把检查机制固化下来。
