无人售货机场景下RocketMQ监控运维与稳定性实战
2026/9/16 1:54:18 网站建设 项目流程

做无人售货机这几年,我最深的体会是:机器本身反而很少出大问题,真正让工程师半夜爬起来解决问题的,往往是背后那条看不见的消息链路。售货机每卖出一瓶水、每完成一次支付回调、每触发一次补货提醒,背后都是一条消息在Broker、Consumer和嵌入式设备之间来回穿梭。当设备量从几十台涨到几千台,消息量从每天几百条涨到每天几百万条,RocketMQ的监控、告警、运维和稳定性就从一个“加分项”变成了“救命项”。

这篇内容就围绕我们团队在无人售货机线上环境里,如何一步步把RocketMQ从“能跑”做到“跑得稳”来展开。适合正在做IoT设备接入、消息中间件运维,或者准备把RocketMQ投到生产环境的朋友参考,内容偏实战,不绕弯子。

1. 无人售货机的消息场景决定了选型和维护策略

1.1 先从场景画像说起:这不是一个常规的互联网业务

无人售货机看起来简单,但把它抽象成消息系统之后,你会发现它和典型的互联网电商业务有很大区别。设备端是嵌入式Linux或者Android系统,通过4G网络连接云端,网络质量不稳定,尤其是地下室、商场角落、地铁站这些信号被遮挡严重的点位,断网重连是家常便饭。单台设备QPS很低,可能一天才几十笔交易,但设备总量大,几千台设备同时上报心跳、交易、货道状态、温控数据,汇聚到云端之后,消息量就非常可观了。

同步出现的还有跨机房部署问题。售货机分布在不同城市,消息入口需要多地域接入,但核心的订单服务和出货指令服务又必须集中管理。这意味着消息不能只在单一地域流转,需要跨城同步、容灾切换。RocketMQ在这个场景下的优势很明显:它的消息轨迹完整、支持事务消息和延迟消息、Consumer可以精确控制消费位点,而且社区活跃度高,踩坑有人陪。这几个特性对我们这种“设备端弱网+云端强一致需求”的组合非常重要。

1.2 为什么是RocketMQ而不是别的消息队列

选型阶段我们其实对比过多个方案。Kafka主打超高吞吐和日志场景,但事务消息和消息重试机制相对弱,而且在小消息体高频场景下,Topic和Partition的管理成本不低。RabbitMQ轻量灵活,但集群模式下的消息堆积处理能力一般,对于需要大量堆积的场景支撑不够好。RocketMQ好在它把“可靠性”放在核心位置,事务消息能保证支付和出货指令的一致性,定时消息天然适配“超时未支付自动关单”“出货超时自动退款”这类售货机业务逻辑。

我们最终统一采用RocketMQ 4.9.x版本集群,主从模式部署,数据盘用SSD,Broker配置了异步刷盘但开启了主从同步复制。这个组合在吞吐和可靠性之间取了平衡点:既不会因为同步刷盘导致性能瓶颈,又能在主机宕机时从节点快速顶上,消息不丢。

注意:选型没有绝对的对错,关键看你的业务容忍度。无人售货机涉及资金交易,消息丢失是不可接受的,所以我们在一致性上的投入比普通日志类业务大得多。

2. 监控体系搭建:没有可观测性,稳定性就是空谈

2.1 从“能看见”到“看得清”:监控指标的分层设计

接手线上RocketMQ集群之后,我们第一件事不是写告警规则,而是先梳理监控指标。这个顺序很重要,没有指标数据,告警规则就是空中楼阁。我习惯把所有指标分成三层:第一层是Broker硬件层,包括CPU、内存、磁盘IO、磁盘使用率、网络带宽和TCP连接数;第二层是RocketMQ内核层,包括消息生产TPS、消费TPS、消息堆积量、拉取请求耗时、消费耗时、Broker的FileChannel读耗时等;第三层是业务链路层,也就是从售货机支付成功到出货指令下发的完整链路耗时,以及各环节的消息转化率。

第三层最容易忽略,但恰恰是无人售货机场景最需要的。Broker的TPS正常不代表业务就正常,可能出现支付回调消息到了,但订单服务消费时因为设备离线等原因迟迟无法下发指令。所以我们额外为每台售货机的设备ID设置了一个业务维度的监控项,追踪关键消息从生产到消费的端到端耗时,并落到Grafana的Panel里。

2.2 监控部署实践:Prometheus、Grafana和自研Exporter的组合

监控技术栈上我们选的是Prometheus + Grafana,配合RocketMQ官方自带的metrics-exporter。官方exporter会暴露Broker的各项运行时指标,包括Consumer的消费位置、堆积数、消息平均耗时等,Prometheus周期抓取,Grafana负责可视化。这里有个细节:metrics-exporter默认只暴露本机指标,所以在多Broker部署时,需要为每个Broker节点单独配置一个抓取任务,并且在Grafana中用标签区分实例。

除了官方Exporter,我们还写了一个小的自研Exporter,专门采集业务层面的数据。它做的事情很简单:通过RocketMQ的OpenMessaging协议周期性查询每个ConsumerGroup的消费位点,计算消息堆积量,再额外探测消费组是否“假死”。这些东西官方Exporter也能给一部分,但自研的这个可以加上业务标签,比如按照设备所属城市、代理商维度聚合堆积数据,排查问题时一眼就能定位到是某个区域的消息链路出了问题。

# prometheus.yml 摘录,注意每个broker/nameserver独立job - job_name: 'rocketmq-exporter' static_configs: - targets: ['10.0.1.11:5557','10.0.1.12:5557','10.0.2.11:5557']

Grafana侧的Dashboard我们直接基于官方模板改,重点加了两个视图:一个是消息堆积量的热力图,按照ConsumerGroup和Topic两个维度交叉展示;另一个是消息生产消费速率差值曲线,这个值一旦长期大于0,就意味着生产速度超过了消费速度,堆积只是时间问题。

2.3 告警规则设置:宁可少而准,不要多而滥

告警规则是监控体系里最难的部分。开发阶段大家都喜欢把阈值调得很敏感,结果上线第一天就被告警淹没了,最后所有人看到告警都默认“没大事”,狼来了效应极其严重。我们后来痛定思痛,整理了三条核心原则:告警必须可执行、告警必须分优先级、告警必须走聚合。

可执行的意思是,收到告警的人必须知道要做什么。比如“磁盘使用率超过80%”可以执行,“生产TPS异常波动”就没什么用,收到之后只能干瞪眼。所以我们的核心告警规则只有五条:磁盘使用率超过85%、消息堆积量超过5万条并持续10分钟、消费组消息消费延迟超过5分钟、Broker进程宕机、消息生产失败率超过1%。其他指标一律放到Dashboard上看趋势,不单独配告警。

优先级分成P0、P1、P2三级。P0只包含消息丢失、集群不可用、核心Topic堆积导致支付回调无法消费,这类告警直接通过企业微信机器人推送到技术负责人,并且电话语音告警;P1是磁盘容量预警、消费延迟升高等,在工作时间推送;P2是比如Broker GC时间变长等性能劣化趋势,周报汇总即可。

告警聚合方面,我们使用Alertmanager处理分组,把同一时间窗口内同一规则触发的告警合并成一条,避免几千台售货机的网络抖动触发几千条“设备离线消息”。这个在设备数量上来之后特别重要,否则没有人能在告警洪流中看到真正的问题。

3. 线上运维实战:从日常巡检到故障恢复

3.1 批量集群运维:一台台手动敲命令是效率黑洞

无人售货机业务有个特点,Broker集群的节点数量不像互联网大厂那么多,但Topic数量会随着业务迭代越来越多,而且每个Topic都可能有不同的消费组,这就导致运维操作频繁且琐碎。今天新增一个订单消息Topic,明天调整一个消费组阈值,后天又要新建重试队列。这些操作如果都靠人肉ssh到服务器上去执行,效率极低,而且容易出错。

我们做了两件事来改善:第一,把所有RocketMQ的运维命令脚本化,统一放到一台跳板机上,用Ansible分发执行。基本上常见的创建Topic、修改Broker配置、查看消费位点等操作都封装成脚本,入参只有环境、集群名、Topic名这些基础信息。第二,把RocketMQ的Dashboard(rocketmq-console-ng)接入了统一登录认证,并限制在生产内网访问。Dashboard虽然功能不算最强,但胜在直观,查看Topic的收发消息曲线、Consumer消费位置、重新消费等操作都能在页面上完成,比命令行友好得多。

# 创建topic脚本示例 sh mqadmin updateTopic -n 10.0.1.11:9876 -b 10.0.1.11:10911 -t Order_Pay_Success -p 6

这里有个坑要提醒:Dashboard的“重新消费”功能一定要慎用。它的实现本质是重置ConsumerGroup位点到某个时间点,如果业务代码没有做幂等,重置之后大量消息会重新进入业务逻辑,轻则重复发指令,重则把下游服务打挂。我们后来专门在代码里给所有消费逻辑都加了幂等保护,才敢把这个功能开放给运营同学使用。

3.2 消息堆积排查:从看到告警到定位根因的三板斧

消息堆积是RocketMQ运维里最高频的问题。我们总结了一套排查流程,按顺序执行基本能定位到90%的问题。第一板斧是看堆积曲线和消费TPS,如果消费TPS是0,说明消费端挂掉了或者Consumer被阻塞了;如果消费TPS接近0但线程还活着,大概率是消费线程池满了,业务代码里的某个远程调用卡住了。第二板斧是看消费链路耗时,如果单条消息消费平均耗时从几十毫秒涨到几秒,优先怀疑业务依赖的下游接口变慢了,比如出货指令服务依赖的售货机连接池满了。

第三板斧就是看日志,我们对核心消费逻辑加了结构化日志,包含Topic、消息ID、设备ID、处理耗时、抛出异常类型等字段。排查问题时直接按消息ID去检索整条链路的日志,从生产者发出、Broker存储、Consumer拉取、业务处理一直到设备确认,每一步的时间点都清晰可见。有一次线上告警显示出货指令消息堆积,排查下来是某个区域服务商后台导入了大量商品数据,导致商品服务数据库连接数被打满,间接影响了消费线程,如果没有链路日志,这个响应至少要慢几个小时。

3.3 版本升级与灰度发布:RocketMQ自身的运维也有讲究

很多人以为RocketMQ部署上线后就不用动了,实际上中间件本身也需要持续升级,修复bug、性能优化、新特性引入都靠升级。但线上集群不能直接升级,尤其是无人售货机业务7×24小时在跑,停机时间非常有限。我们采用的是灰度升级策略:先在预发环境验证,再到生产环境挑一台低流量Broker升级,观察一天再逐步扩展到全集群。

升级过程中有两个容易踩的坑。第一个坑是RocketMQ的CommitLog文件格式不兼容。从低版本升到高版本时,如果版本跨度太大,老CommitLog可能无法被新版本Broker正确加载。所以我们制定了规矩,升级必须逐版本跳,不允许跨大版本。第二个坑是FlushDiskType配置在升级后可能被重置。有次升级后我们发现磁盘IO异常高,排查后是升级脚本覆盖了配置文件里的异步刷盘设置,导致所有Broker都变成了同步刷盘,吞吐直接掉了一半。从那以后,升级手册里专门加了一条:升级前备份原配置文件,升级后逐项对比确认,不允许直接copy包覆盖。

4. 量产稳定性优化:站在生产视角的调优与设计

4.1 生产端优化:小消息成倍数是无人售货机场景的大敌

无人售货机有一个很典型的消息特征:单条消息很小,可能就几十个字节,但条数特别多。设备心跳每30秒上报一次,加上交易消息、补货消息、故障消息,一台机器一天产生的消息量在百条以上,几千台设备就是几十万条。小消息高并发场景下,Broker的主要瓶颈不是CPU,而是内存和GC。大量消息对象频繁创建销毁,Young GC非常频繁,严重的时候会看到GC暂停导致生产端发送RT飙升。

我们生产端做了两个核心优化。一是批量发送。把服务端收集到的同一台设备的多条消息聚合到一个批次里,调用SendResult时使用批量发送接口,大幅减少了网络往返和Broker端的单条消息处理开销。二是在业务允许的场景下降低消息堆积粒度,比如心跳类消息直接使用普通Topic且不设置重试,因为心跳消息的本质是最新的状态才有效,重试旧心跳没有意义,反而会加重系统负担。

客户端参数方面也有不少讲究。sendMsgTimeout默认是3000ms,在4G弱网场景下远远不够,我们调整到了5000ms,并且开启了retryAnotherBrokerWhenNotStoreOK,配合主从同步复制,确保单台Broker写入异常时消息能自动转发到其他Broker,不丢消息。

4.2 消费端稳定性:幂等、批量消费与限流

消费端是无人售货机消息链路里最容易出问题的环节。核心原因在于消费逻辑往往涉及多次外部IO:查订单库、调出货指令服务、更新设备状态、写消息轨迹库,任何一个依赖抖动都会拖慢消费速度。而且消费端一旦出现异常消息,默认的重试机制是抛异常后回到Broker重试,重试次数多了会进入死信队列,如果没人处理,消息就悄悄丢了。

我们对消费端做了系统性加固。首先所有消费逻辑都做幂等,以订单号或者消息ID作为唯一键,消费前先查Redis或者数据库去重,保证同一条消息无论被投递多少次,业务最终只执行一次。其次是开启批量消费,consumeMessageBatchMaxSize设为32,一次拉取32条消息批量处理,吞吐提升明显。但要注意批量消费必须自己做部分失败的重试控制,RocketMQ批量消费如果批量中有一条处理失败,默认整个批次都重试,会导致旁边明明处理成功的消息也跟着重复消费,所以我们的做法是把批量拆开逐条处理,出错的那条单独记录并抛错,其他消息照常提交。

限流方面,我们在下游出货指令服务里做了信号量限流,核心时刻只允许一定数量的并发出货指令下发到设备端。原因很简单,出货指令的消费速度受限于售货机连接通道的并发数,消费太快但设备连不上,只会白白堆积无效指令。配合RocketMQ的消费限流参数,在高峰期动态调整消费线程数,保证系统在极端流量下依然平稳。

4.3 延迟消息和定时任务:无人售货机业务里的隐形依赖

无人售货机业务里延迟消息无处不在。典型的例子有:支付单超过15分钟未支付自动关单、出货指令发送后60秒未收到设备回执自动触发补偿、售货机离线超过30分钟自动通知运营人员。我们使用RocketMQ的定时消息机制来实现这些功能。RocketMQ支持18个延迟级别,从1秒到2小时,基本覆盖了我们的业务需求。

这里有一个非常容易踩的坑:RocketMQ的定时消息在到达延迟时间之前,消息会先写入一个内部的定时Topic(SCHEDULE_TOPIC_XXXX),到达时间后再由Broker转发到真实Topic。如果定时消息量特别大,比如几十万台设备同时设置了定时任务,这个内部Topic就成了瓶颈,而且肉眼看不到堆积,只能通过监控SCHEDULE_TOPIC_XXXX的消费速度来发现。我们曾经就因为定时消息量暴增导致Broker日志疯狂报错,最后是通过单独监控这个内部Topic才定位到问题。

另外,定时消息的延迟时间精度不是绝对的,可能有一定误差,千万不能把它当成精确计时器用。涉及到资金类的超时操作,我们仍然建议以数据库里的时间字段为准,RocketMQ的定时消息只充当触发信号,消费端要做二次时间校验,发现时间未到就重新投递。

4.4 事务消息:支付与出货一致性问题的解药

售货机业务里最需要保证一致性的环节是支付和出货。用户在机器上扫码支付成功后,云端必须确保出货指令最终被下发到设备端,整个链路不能出现“钱扣了但货没出”的情况,否则客诉和退款会让人崩溃。我们使用RocketMQ事务消息来解决这个问题。

事务消息的流程是:订单服务先发送一条半消息到Broker,此时消息对消费者不可见;订单服务本地执行支付回调数据的写库操作,事务成功后向Broker发送commit,半消息才变为可见消息,消费者才能收到;如果本地事务执行失败则发送rollback,消息被删除。还有一个关键环节是事务反查,如果本地事务操作成功但Broker没有收到commit指令(比如网络抖动),Broker会定时回调生产者端的checkLocalTransaction方法,询问本地事务的状态,以此保证最终一致性。

这里我的经验是:事务反查回调接口必须有幂等设计。因为网络原因或者生产端重启,同一个事务可能被反查多次,如果checkLocalTransaction实现得不好,可能会重复查询或产生脏数据。我们的实现是统一查一张事务状态表,表里以业务单号为主键,状态有“处理中、成功、失败”,每次反查只读状态,不回写,从源头避免并发写造成的数据错乱。

5. 常见问题与排查技巧实录

5.1 高发问题速查表:先照表排查,再深入细节

和监控告警一样,故障排查也讲究“先易后难”。我整理了最近一年线上RocketMQ故障的高发点和对应处理方案,分享给大家参考。

问题现象可能原因优先排查项处理建议
消费TPS为0但消费线程正常消费逻辑被长时间阻塞线程dump,看线程状态检查下游依赖接口连接池
消息堆积持续上涨消费速度跟不上生产速度消费耗时曲线和消费TPS曲线对比优化消费逻辑、扩容消费者
生产端发送RT突增Broker GC或磁盘IO抖动Broker监控面板GC曲线、IO等待检查是否触发同步刷盘、清理日志
定时消息不触发内部SCHEDULE_TOPIC_XXXX堆积监控内部Topic消费速度降低定时任务量,改用时间戳校验
消费重复执行客户端重试导致消息重复投递消息轨迹查看投递次数消费逻辑必须幂等
Broker磁盘写满CommitLog增长过快磁盘使用率监控定期清理过期消息,扩容磁盘

这个表格是我们内部运维文档里的一个精简版。真正排查时重要的不是记住每行内容,而是建立“先看指标、再对现象、后动手操作”的习惯。不要一上来就翻日志,日志只是用来验证猜想,不是用来找线索的。

5.2 一次真实的故障复盘:凌晨三点的消息堆积战

分享一个我们真实遇到的线上故障,凌晨三点钟,告警机器人推送了一条P0消息,核心Topic“出货指令”消息堆积量在10分钟内涨到了8万条。当时值班的同学第一反应是查看消费TPS,发现消费TPS从正常的2000降到了0,而且ConsumerGroup处于在线状态,说明消费者还连着Broker,但处理线程全部卡死了。

紧接着做了线程dump,发现所有消费线程都阻塞在调用出货指令服务的HTTP接口上,连接池全部耗尽。再看下游出货服务,发现它的数据库连接数也满了。进一步定位发现,前一天运营同学批量导入了大量商品数据,导致商品服务的某个SQL出现了慢查询,这个慢查询连带把数据库连接池占满了。消费线程拿不到数据库连接,自然全部阻塞,消息只能堆积在Broker上。

当时的处理分三步:第一步,临时关闭商品服务的那条异常SQL任务,数据库连接数恢复,消费TPS迅速回升;第二步,由于堆积的消息大多是需要及时下发的出货指令,我们用Dashboard把消费位点重置到一个小时之前,让堆积消息重新消费;第三步,后续做了两个整改,一是给所有下游依赖的数据库配置了连接池保护和慢查询告警,二是给消息消费线程配置了线程池饱和策略,绝对不能因为下游抖动导致消费线程无限阻塞。

这次故障让我记住了最重要的一条经验:RocketMQ消费端稳定性不取决于消费代码写得多好,而取决于所有下游依赖的抗抖动能力。连接池、线程池、信号量、超时时间,每一个都要有兜底,否则一个慢查询就能让整个消息链路瘫痪。

5.3 嵌入式设备场景下的独特维护心得

最后分享一点无人售货机这种嵌入式设备场景下特有的维护心得。设备端的消息生产客户端和云端不同,它运行在资源受限的硬件上,内存通常只有几百MB,CPU也是低功耗型号。这就意味着设备端不能直接引入完整的RocketMQ客户端,内存根本扛不住。我们的方案是设备端使用轻量级HTTP上报,由云端一个消息接入网关统一转换后写入RocketMQ。这样RocketMQ客户端只在服务端运行,设备端逻辑保持轻量简单。

设备断网重连时有一个特殊问题:网络恢复瞬间,几千台设备会同时上报积压了很久的状态数据,造成消息洪峰。我们为此专门设置了一个网关层缓冲,在设备重连高峰期对上报请求做排队和限速,避免瞬间消息量打满Broker。同时,设备端上报的消息大多是可以“合并去重”的状态类数据,网关在落库前会做一次简单的合并,同一台设备短时间内重复上报的状态只保留最新一条,既减轻了网络压力,也降低了消息总量。

设备升级过程中还有一个容易忽略的问题:设备端的消息确认机制要够简单。一些售货机的主控板逻辑很简单,发送消息后如果没收到确认就会重发,而云端如果处理不当,很容易出现重复消息风暴。我们让设备端的消息自带唯一序号,云端网关用Redis做去重,重复消息直接丢弃,从源头控制了下游的重复消费压力。这些经验可能在一些纯互联网业务团队看来很基础,但在嵌入式与云端结合的IoT场景里,每一条都是血泪换来的。

做了这么久的无人售货机RocketMQ运维,我个人最深的感触是:消息中间件的稳定性不是靠某个单点优化就能搞定的,它考验的是从设备端到云端的全链路设计能力。监控让你发现问题,告警让你及时知道问题,但真正让系统在量产环境下扛住压力的,还是那些平时不起眼的细节——批量发送、消费幂等、连接池保护、开关限流、重试退避。把这些细节打磨好了,消息链路自然就稳了。希望这篇内容能给正在做类似IoT+消息中间件项目的朋友一些参考。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询