工业现场的设备管理有个很尴尬的现实:车间里跑的PLC、电表、水表、传感器,十台里有八台只认SNMP或者Modbus这类"老派"协议,而老板和运维团队又希望数据能实时推到手机、大屏、云端看板。这两拨需求中间隔着一道鸿沟——传统工业协议擅长"管设备",现代物联网协议擅长"传数据"。我做过好几个工厂的采集项目,最后跑通的那套方案,基本都是MQTT加SNMP双协议组合:SNMP负责把设备底层的状态、告警、流量、温度这些指标捞出来,MQTT负责把这些数据高效、可靠地送到上层平台。这套组合不是拍脑袋想出来的,是被现场逼出来的。
1. 为什么工业设备管理绕不开SNMP和MQTT这两条腿
1.1 SNMP在工业现场的不可替代性
先说SNMP。很多人觉得这协议老掉牙了,但你去任何一个机房、变电站、水厂泵站看一眼,交换机、路由器、UPS、工业网关、部分PLC,出厂就带SNMP Agent。它最大的价值在于标准化——MIB(管理信息库)是一套全球统一的命名体系,OID(对象标识符)就像设备的"身份证号",1.3.6.1.2.1.1.1.0这个OID在华为交换机上代表系统描述,在思科交换机上也代表系统描述。这意味着你写一套采集逻辑,换品牌换型号,大部分OID还能复用。
SNMP有三个核心操作:GET(读单个值)、GETNEXT/GETBULK(遍历表格)、TRAP/INFORM(设备主动上报告警)。工业场景里最常用的是轮询GET和接收TRAP。轮询适合采集周期性指标,比如CPU利用率、端口流量、温度;TRAP适合捕捉突发事件,比如端口down、电源故障、阈值越限。我见过不少项目只做轮询不做TRAP,结果设备都烧了半小时了,平台还在等下一个采集周期,这就是没吃透SNMP的典型表现。
SNMP目前主流是v2c和v3两个版本。v2c用community字符串做认证,配置简单但安全性弱;v3支持用户认证和加密,适合对安全有要求的场景。工业内网里v2c用得最多,因为设备老、配置省事,但如果你的采集网关要跨网段或者上云,v3是必须的。这里有个坑:很多老设备的SNMP v3实现不完整,只支持认证不支持加密,对接的时候要先用snmpwalk测一遍,别上来就写代码。
1.2 MQTT解决的是"最后一公里"的传输问题
SNMP采集到的数据,怎么送到云端或者中心平台?传统做法是采集程序直接写数据库,或者用Modbus TCP转发,但这两种方式在分布式、弱网、多节点场景下都很吃力。MQTT的优势就体现出来了:发布订阅模型、轻量级头部、支持QoS等级、断线重连、遗嘱消息。一个采集网关可以同时向多个主题发布数据,云端、边缘节点、手机App各自订阅自己关心的主题,互不干扰。
MQTT的QoS等级是工业场景必须搞清楚的:QoS 0是"最多一次",发出去就不管了,适合高频但允许丢的指标;QoS 1是"至少一次",有确认机制但可能重复,适合大多数设备状态数据;QoS 2是"恰好一次",四次握手开销大,工业场景用得少。我一般建议设备状态和告警用QoS 1,高频传感器数据用QoS 0加本地缓存,别一股脑全上QoS 2,broker扛不住。
还有一个容易被忽略的点是遗嘱消息(Last Will and Testament)。采集网关连上broker时预先注册一条遗嘱,一旦网关异常断线,broker会自动发布这条消息,云端立刻知道"这个网关掉线了"。这比等心跳超时再判断要快得多,在无人值守的泵站、基站场景里特别有用。
1.3 双协议组合的分工逻辑
把这两个协议放在一起,分工其实很清晰:
| 层次 | 协议 | 职责 | 典型数据 |
|---|---|---|---|
| 设备层 | SNMP | 采集设备指标、接收告警 | CPU、内存、端口流量、温度、电源状态 |
| 传输层 | MQTT | 数据上报、指令下发 | JSON/二进制 payload、主题消息 |
| 平台层 | MQTT Broker | 消息路由、订阅分发 | 主题树、QoS策略、保留消息 |
采集网关是这两层的粘合剂:它作为SNMP Manager去轮询或接收设备数据,同时作为MQTT Client把数据发布到broker。这个网关可以是一台工控机、一个树莓派、一台ARM边缘盒子,甚至是一台跑Linux的旧服务器。关键是它要能同时跑SNMP采集进程和MQTT客户端进程,并且有本地缓存能力,断网时不丢数据。
2. 采集网关的选型与SNMP采集链路搭建
2.1 网关硬件和系统的实际选择
采集网关的选型直接决定项目能不能落地。我按场景分三类说:
轻量场景(几十个SNMP节点、数据频率低):树莓派4B或者类似的ARM开发板就够了,跑Debian或者Ubuntu Server,装net-snmp工具和Python的pysnmp库。成本低,功耗小,适合水表采集器、小型泵站这类场景。注意ARM板子的网口数量,很多只有一个,要接多个网段得加USB网卡或者用VLAN。
中等场景(几百个节点、需要本地存储和边缘计算):工控机或者ARM边缘计算盒子,比如瑞芯微、全志的方案,跑Ubuntu或者麒麟系统。这类设备通常有多个网口、RS485、DI/DO,能同时接SNMP设备和Modbus设备。麒麟V10在ARM上的离线安装是个常见需求,后面单独说。
重载场景(上千节点、高频率采集):x86工控机或者虚拟机,跑CentOS/Ubuntu,用Go或者Java写采集程序,配合时序数据库做本地缓存。这种场景下SNMP轮询的并发调度是瓶颈,需要做分片和错峰。
系统层面,我强烈建议用Linux而不是Windows。net-snmp在Linux上的稳定性和工具链完整度远超Windows,snmpwalk、snmpget、snmptrap这些命令行工具调试起来非常方便。Windows上虽然也有SNMP服务,但配置繁琐,而且很多工业网关默认就是Linux。
2.2 SNMP采集的核心参数配置
SNMP采集有几个参数必须调对,否则要么采不到,要么把设备采挂:
- 超时时间(timeout):默认1秒,工业设备响应慢,建议设2-3秒。太短会大量超时,太长会拖慢轮询周期。
- 重试次数(retries):默认5次,建议设1-2次。重试太多会在设备繁忙时雪上加霜。
- 轮询间隔:状态类指标30-60秒,流量类指标10-30秒,别低于5秒,很多老设备扛不住。
- 并发数:单进程并发别超过50,超过就要分片。SNMP是UDP,丢包是常态,并发太高丢包率飙升。
用snmpwalk测试的时候,命令大概是这样:
# SNMP v2c 遍历系统信息 snmpwalk -v 2c -c public -t 3 -r 2 192.168.1.100 1.3.6.1.2.1.1 # SNMP v3 带认证 snmpwalk -v 3 -l authNoPriv -u monitor -a MD5 -A "authpass" -t 3 -r 2 192.168.1.100 1.3.6.1.2.1.1 # 获取单个OID snmpget -v 2c -c public -t 3 -r 2 192.168.1.100 1.3.6.1.2.1.1.3.0这里有个实操经验:先用snmpwalk把设备的MIB树完整走一遍,把关心的OID记下来,再写采集程序。别对着厂商文档抄OID,文档和实际固件经常对不上。我遇到过一个交换机,文档说端口流量在1.3.6.1.2.1.2.2.1.10,实际固件里这个OID返回的是空值,真正的流量在厂商私有MIB里。
2.3 用Python搭建SNMP采集端
Python做SNMP采集,pysnmp是主流库。下面是一个最小可用的采集示例,采集系统描述和运行时间:
from pysnmp.hlapi import * def snmp_get(ip, community, oid): iterator = getCmd( SnmpEngine(), CommunityData(community, mpModel=1), # mpModel=1 表示 v2c UdpTransportTarget((ip, 161), timeout=3, retries=2), ContextData(), ObjectType(ObjectIdentity(oid)) ) errorIndication, errorStatus, errorIndex, varBinds = next(iterator) if errorIndication: return None elif errorStatus: return None else: for varBind in varBinds: return varBind[1].prettyPrint() # 采集系统描述 sys_descr = snmp_get('192.168.1.100', 'public', '1.3.6.1.2.1.1.1.0') print(sys_descr)这个写法适合低频采集。如果要高频轮询多个OID,建议用bulkCmd批量获取,减少往返次数。另外pysnmp是纯Python实现,性能一般,节点多了要上多进程或者换net-snmp的Python绑定。
对于TRAP接收,需要起一个SNMP Trap Receiver进程,监听162端口。pysnmp也支持,但生产环境我更推荐用snmptrapd(net-snmp自带的守护进程),配置好之后把TRAP转成脚本调用或者写入队列,稳定性更好。
3. MQTT主题设计与消息可靠性保障
3.1 主题树的设计原则
MQTT主题设计是双协议组合里最容易被做烂的部分。我见过有人把所有数据往一个主题里塞,结果订阅端要处理几十种payload格式,维护成本爆炸。好的主题树应该满足三个条件:层次清晰、可通配订阅、与设备拓扑对应。
推荐的主题结构:
factory/{厂区}/{车间}/{设备类型}/{设备ID}/{数据类型}举个例子:
factory/A/workshop1/switch/SW001/status factory/A/workshop1/switch/SW001/traffic factory/A/workshop1/ups/UPS003/alarm factory/A/workshop2/plc/PLC007/temperature这样设计的好处是,云端可以按厂区订阅factory/A/#,按设备类型订阅factory/+/+/switch/#,按告警订阅factory/+/+/+/+/alarm。通配符+匹配单层,#匹配多层,用好了能省很多订阅逻辑。
注意:主题里不要用中文、空格、特殊字符,虽然MQTT协议允许,但很多broker和客户端处理起来会出问题。设备ID用英文数字组合,分隔符统一用斜杠。
3.2 QoS和保留消息的取舍
QoS的选择前面提过,这里补充一个实际场景的决策表:
| 数据类型 | 推荐QoS | 是否保留消息 | 理由 |
|---|---|---|---|
| 设备在线状态 | 1 | 是 | 新订阅者需要立刻知道当前状态 |
| 实时告警 | 1 | 否 | 告警有时效性,历史告警走数据库 |
| 周期性指标 | 0 | 否 | 高频数据,丢一两个点可接受 |
| 配置下发 | 1 | 否 | 需要确认送达 |
| 设备心跳 | 0 | 是 | 保留最后心跳,判断在线状态 |
保留消息(Retained Message)是个双刃剑。它让新订阅者立刻拿到最后一条消息,但如果设备状态变了而保留消息没更新,就会误导订阅者。我的做法是:状态类主题用保留消息,数据类主题不用。
3.3 消息不丢失的完整链路
"MQTT怎么保证不丢失消息至少一次"是个高频问题。光靠QoS 1不够,要从采集端到broker到订阅端全链路考虑:
采集端:SNMP采集到的数据先写本地队列(比如Redis、SQLite、或者内存队列),MQTT发布成功后再出队。断网时数据堆在队列里,恢复后补发。队列要有上限和淘汰策略,别把磁盘写满。
传输端:QoS 1保证broker收到消息,但broker到订阅端的投递也要QoS 1。如果订阅端处理慢,broker的消息队列会堆积,要设置合理的队列长度和丢弃策略。
订阅端:收到消息后先落库再确认,别先确认再处理。处理失败要有重试和死信队列。
Broker端:选型很关键。EMQX、Mosquitto、HiveMQ、VerneMQ各有特点。中小规模用Mosquitto够用,大规模集群用EMQX。Broker要开启持久化,防止重启丢消息。
这里有个我踩过的坑:Mosquitto默认的max_queued_messages是1000,超过就丢。工业场景下订阅端偶尔卡顿,1000条很快就满了。要改成10000以上,并且监控队列长度。
4. 双协议网关的代码实现与联调
4.1 采集与发布的整体架构
一个完整的双协议网关,内部结构大概是这样:
[SNMP设备] --SNMP--> [采集模块] --队列--> [MQTT发布模块] --MQTT--> [Broker] | [本地缓存] | [TRAP接收]采集模块负责轮询和TRAP接收,把数据标准化成统一格式(推荐JSON),写入本地队列。发布模块从队列取数据,按主题规则发布到broker。本地缓存用于断网续传和TRAP暂存。
数据格式建议统一成:
{ "device_id": "SW001", "device_type": "switch", "timestamp": 1700000000, "metrics": { "cpu_usage": 45, "mem_usage": 62, "port1_in": 1024000, "port1_out": 2048000 }, "alarm": null }告警数据单独一个格式:
{ "device_id": "UPS003", "device_type": "ups", "timestamp": 1700000000, "alarm": { "level": "critical", "code": "POWER_LOSS", "message": "市电中断,切换电池供电" } }4.2 用Python把SNMP数据推到MQTT
下面是一个简化但可运行的示例,把SNMP采集和MQTT发布串起来:
import json import time import paho.mqtt.client as mqtt from pysnmp.hlapi import * # MQTT配置 MQTT_BROKER = "192.168.1.200" MQTT_PORT = 1883 MQTT_USER = "gateway" MQTT_PASS = "gateway123" TOPIC_PREFIX = "factory/A/workshop1" # SNMP设备列表 DEVICES = [ {"ip": "192.168.1.100", "community": "public", "id": "SW001", "type": "switch"}, {"ip": "192.168.1.101", "community": "public", "id": "UPS003", "type": "ups"}, ] # 关心的OID OIDS = { "sys_descr": "1.3.6.1.2.1.1.1.0", "sys_uptime": "1.3.6.1.2.1.1.3.0", "cpu_usage": "1.3.6.1.4.1.2021.11.11.0", } def snmp_get(ip, community, oid): iterator = getCmd( SnmpEngine(), CommunityData(community, mpModel=1), UdpTransportTarget((ip, 161), timeout=3, retries=2), ContextData(), ObjectType(ObjectIdentity(oid)) ) errorIndication, errorStatus, errorIndex, varBinds = next(iterator) if errorIndication or errorStatus: return None for varBind in varBinds: return varBind[1].prettyPrint() def on_connect(client, userdata, flags, rc): print("MQTT connected with result code", rc) client = mqtt.Client(client_id="gateway_001") client.username_pw_set(MQTT_USER, MQTT_PASS) client.will_set(f"{TOPIC_PREFIX}/gateway/gateway_001/status", "offline", qos=1, retain=True) client.on_connect = on_connect client.connect(MQTT_BROKER, MQTT_PORT, 60) client.loop_start() client.publish(f"{TOPIC_PREFIX}/gateway/gateway_001/status", "online", qos=1, retain=True) while True: for dev in DEVICES: metrics = {} for name, oid in OIDS.items(): val = snmp_get(dev["ip"], dev["community"], oid) if val is not None: metrics[name] = val if metrics: payload = { "device_id": dev["id"], "device_type": dev["type"], "timestamp": int(time.time()), "metrics": metrics } topic = f"{TOPIC_PREFIX}/{dev['type']}/{dev['id']}/status" client.publish(topic, json.dumps(payload), qos=1) time.sleep(30)这个代码有几个地方值得说:will_set注册了遗嘱消息,网关掉线时broker会自动发offline;loop_start启动了后台网络线程,主线程只管采集;发布用QoS 1保证送达。生产环境还要加异常处理、本地队列、日志、配置热加载,但骨架就是这样。
4.3 联调时最容易卡住的几个点
第一个坑:SNMP community不对或者被ACL拦了。现象是snmpwalk超时,但ping得通。先确认community字符串,再确认设备有没有配SNMP访问控制列表。很多交换机默认只允许特定网段访问SNMP。
第二个坑:MQTT broker认证失败但客户端不报错。paho-mqtt的connect是异步的,认证失败在on_connect回调的rc里。要打印rc,rc=5表示未授权。别以为connect没抛异常就是成功了。
第三个坑:主题发布成功但订阅端收不到。检查订阅端的主题过滤器,factory/A/#能匹配factory/A/workshop1/switch/SW001/status,但factory/A/+只能匹配一层。还有broker的ACL,有些broker配置了主题级别的发布订阅权限。
第四个坑:中文payload乱码。MQTT payload是二进制的,中文要UTF-8编码。paho-mqtt的publish如果传str会自动编码,但有些客户端不会。统一用json.dumps(..., ensure_ascii=False).encode('utf-8')。
5. 工业现场的部署细节与踩坑记录
5.1 麒麟V10 ARM离线安装MQTT客户端
国产化项目里,麒麟V10 ARM系统上离线装MQTT客户端是个高频需求。现场往往没有外网,只能用离线包。以paho-mqtt为例,步骤是:
- 在一台能联网的同架构机器上下载wheel包:
pip download paho-mqtt -d ./pkgs --platform manylinux2014_aarch64 --only-binary=:all: - 把pkgs目录拷到目标机器
- 目标机器上执行:
pip install --no-index --find-links=./pkgs paho-mqtt
如果目标机器连pip都没有,就要用系统包管理器。麒麟V10基于CentOS/RHEL系,可以用rpm -ivh装python3-paho-mqtt的rpm包。但很多情况下没有现成rpm,就得从源码装。源码装paho-mqtt其实不需要编译,纯Python包,解压后python3 setup.py install就行。
net-snmp在麒麟上的离线安装稍微麻烦点,依赖libssl、libcrypto等。建议用yumdownloader把依赖一起下下来,做成离线repo,用createrepo建本地源,这样装起来最省事。
5.2 水表采集器的双协议适配
水表采集器是个典型场景:采集器通过Modbus 645或者Modbus RTU读水表数据,然后通过MQTT上报。但有些采集器本身也支持SNMP,用于被网管平台管理。这种设备就是双协议组合的天然载体。
对接水表采集器时要注意:Modbus 645和Modbus RTU的寄存器地址不一样,645是表号+数据标识,RTU是寄存器地址。采集器一般会做转换,但转换规则要跟厂商确认。MQTT上报的payload格式也要确认,有些采集器用私有格式,有些用JSON。
如果采集器只支持Modbus不支持MQTT,那就在网关上做转换:网关用Modbus读采集器,转成MQTT发布。这就是"协议转换网关"的典型用法。
5.3 现场部署的几条硬经验
网络隔离:SNMP采集网段和MQTT上报网段最好分开,或者用VLAN隔离。SNMP轮询的广播包和MQTT的TCP连接混在一起,网络质量差的时候互相影响。
时间同步:所有设备、网关、broker都要NTP对时。SNMP的sysUptime是相对时间,MQTT的timestamp是绝对时间,时间不同步会导致数据对不上。我遇到过一次,网关时间慢了8小时,告警时间全错,排查了半天。
日志和监控:网关要记录SNMP采集成功率、MQTT发布成功率、队列长度。这些指标本身也可以通过MQTT上报,形成自监控。采集成功率低于95%就要查网络或设备。
电源和看门狗:工业现场电压不稳,网关要配UPS或者宽压电源。软件层面加看门狗,进程挂了自动重启。systemd的Restart=always是最简单的方案。
固件和MIB版本:设备固件升级后MIB可能变,OID可能失效。升级前要备份MIB,升级后要重新验证OID。这个坑我在一个变电站项目里踩过,升级后一半的OID返回空值,最后是厂商改了私有MIB。
6. 从单点采集到规模化管理的演进思路
6.1 采集节点的分片与调度
设备数量上去之后,单网关扛不住,要做分片。分片策略有几种:按网段分、按设备类型分、按地理区域分。我一般按网段分,因为SNMP轮询的瓶颈在网络IO,同网段的设备放一个网关,减少跨网段流量。
分片之后要有统一的配置管理。每个网关的采集配置(设备列表、OID映射、主题前缀)不能硬编码,要从配置中心拉取。配置中心可以用MQTT的保留消息实现:网关订阅config/gateway/{gateway_id},配置变更时发布新配置,网关收到后热加载。
6.2 数据质量与告警收敛
双协议组合跑起来之后,数据量会很大,告警也会很多。要做告警收敛:同一设备的同类告警在时间窗口内只发一次,恢复时发恢复消息。SNMP TRAP本身有重复上报的问题,要在网关侧去重。
数据质量方面,SNMP采集失败要标记为null而不是0,否则会污染统计。MQTT发布失败要重试,重试失败要落盘。这些细节决定了数据能不能用于生产决策。
6.3 边缘计算与云端协同
网关不只是转发,还可以做边缘计算。比如在网关侧计算设备的健康度、做阈值判断、聚合多个设备的数据。这样云端收到的就是加工过的数据,减少传输量和云端计算压力。
云端则负责全局视图:跨厂区对比、历史趋势、机器学习预测。MQTT的主题设计要支持这种分层:边缘算完的结果发到factory/A/aggregated/#,原始数据发到factory/A/raw/#,云端按需订阅。
这套双协议组合我用了几年,从几十个设备的小项目到上千个设备的厂区都跑过。核心经验就一条:SNMP负责把设备管好,MQTT负责把数据传好,中间的网关要足够健壮。网关的健壮性体现在队列、重试、缓存、监控这些"不性感"的地方,但这些地方做不好,再漂亮的架构都是空中楼阁。