车间里的西门子PLC数据怎么进Zabbix这是最近一个项目里客户抛给我的问题。三台S7-1200、两台S7-1500分布在两条产线上设备状态、产量、报警这些数据全在PLC里。IT运维那边统一用Zabbix盯网络设备顺带想把产线设备也接进去。问题在于Zabbix对SNMP的支持最顺而PLC这边默认走的是S7协议两边语言不通。这个“PLC数据转SNMP”的项目做完之后我踩了不少坑也理清了一套可复用的思路。这篇文章就把整个过程完整拆一遍从方案选型、OID规划到Agent开发、Zabbix对接再到实际遇到的问题一步步讲清楚给正在做类似IT/OT数据联动的朋友一个参考。1. 项目需求与方案选型1.1 项目背景与需求拆解这个项目的数据源是5台西门子PLC其中3台S7-1200、2台S7-1500。PLC侧需要上报的数据点不算复杂主要分三类设备运行状态运行、停止、故障、维护四种状态生产数据当前产量、目标产量、累计运行时间过程参数关键温度、当前报警码。统计下来大约30个数据点刷新要求也不苛刻状态类5秒内可见产量类放宽到30秒也能接受。按说这个量级的数据采集用SCADA最合适但客户并不想在IT侧再部署一套上位机系统因为运维团队日常只认Zabbix这套监控体系。所以核心问题变成了怎么把PLC里的数据用Zabbix最熟悉、最稳定的方式送上去这里其实牵涉到一个常见的认知差PLC工程师熟悉S7协议、Profinet、博图而IT运维熟悉SNMP、OID、Zabbix模板。两边都不愿意为了对方改变自己的工具栈最务实的办法就是找一个中间层把OT侧的数据翻译成IT侧听得懂的语言。SNMP作为网络设备监控的事实标准自然是Zabbix侧最没有磨合成本的协议。网络拓扑上还有个细节PLC都在车间工业网段Zabbix Server在办公网段两个网段之间有防火墙隔离。这意味着中间采集节点需要同时能访问两个网段而且要在防火墙上显式放行特定端口。所以在方案设计阶段网络规划就得一并考虑否则后面调试的时候光是通不通的问题就能耗掉半天。1.2 方案对比与选型做技术方案时我先把市面上可行的路径列了一遍主要有下面四种方案优点缺点适用场景硬件协议网关实时性好、不占服务器资源、稳定性高成本高、单台网关几千上万元、配置繁琐大规模产线、数据点数多、实时性要求极高OPC UA 中间件转发标准化程度高、数据模型丰富需要部署OPC UA Server、配置厚重、License成本不确定已有OPC UA基础的企业自研Agentsnmpd 扩展脚本成本几乎为零、灵活、可完全掌控需要自己维护开发脚本中小规模、数据点数不多、预算有限的场景PLC侧直接支持SNMP不需要额外设备西门子PLC原生不支持SNMP基本不可行不适用最终我选了自研Agent这条路。主要原因有三点第一30个数据点、秒级刷新这个压力很小一台普通的Linux虚拟机就能扛住完全没有必要花几万块买硬件网关。第二zabbix_snmp的采集方式非常成熟只要Agent侧把OID树挂好Zabbix侧几乎零成本接入。第三脚本化方案的扩展性好后期如果新增一台PLC或者新增几个数据点只需要改配置文件和OID映射表不需要重新采购设备或者改网络架构。当然这个方案也有代价所有的高可用和异常处理逻辑都得自己写不像硬件网关那样开箱即用。但对于这个项目规模来说这个代价完全可控。1.3 整体架构设计整个数据链路是这样的PLCS7协议→ 采集脚本Snap7库读取→ 本地缓存 → snmpd pass_persist扩展 →SNMP协议→ Zabbix ServerAgent部署在一台Ubuntu 22.04的Linux服务器上IP规划是PLC网段192.168.10.0/245台PLC的IP为192.168.10.10~192.168.10.14Agent服务器192.168.10.20双网卡配置一张网卡接PLC网段一张接办公网段Zabbix Server办公网段通过SNMP协议访问Agent服务器的161端口。安全策略上PLC网段到Agent只放行TCP 102端口S7协议专用端口办公网段到Agent只放行UDP 161端口SNMP协议专用端口。这样做的好处是即使办公网段被入侵攻击面也只停留在Agent服务器不会直接暴露PLC。这里插一句架构设计时最容易忽略的是Agent服务器的物理位置。它必须同时能访问两个网段如果现场网络条件不允许就得考虑用路由器策略或者部署在DMZ区。这个项目里客户机房刚好有一台空闲的虚拟机可以做这个跳板省去了不少事。2. 关键环节拆解2.1 PLC侧的准备与S7通信细节要从S7-1200/1500里读数据第一步是在博图TIA Portal里开启PUT/GET通信访问。具体操作是PLC属性 → 保护与安全 → 连接机制 → 勾选“允许从远程伙伴进行PUT/GET通信访问”。这一步不做后面Snap7连上来会被直接拒绝。Snap7是开源的S7通信库支持S7-300/400/1200/1500Python环境下安装非常方便。连接时最常遇到的坑就是Rack和Slot参数。S7-1500和S7-1200一般填(0,0)就能通但S7-300的CPU通常是Rack 0、Slot 2S7-400可能要看具体的机架组态。如果连不上可以用Snap7提供的connect_tsap方法手动指定TSAP比如S7-1200的本地TSAP通常是0x0301远端TSAP是0x0300这个后面排查问题时还会提到。读取DB块是另一个关键点。DB块本质上是一段连续的内存区域Snap7的db_read方法按字节读取所以需要预先知道目标数据在DB块里的偏移量和长度。这要求PLC工程师在博图里把DB块的结构整理清楚最好形成一张数据点表标注每个变量的偏移地址、数据类型、字节长度。没有这张表数据采集脚本根本无从下手。另外西门子PLC是big-endian字节序而x86服务器是小端。解析REAL类型时要用struct.unpack(f, ...)解析INT要用struct.unpack(h, ...)解析WORD用HDWORD用I。如果字节序搞反读出来的数值会非常离谱比如温度显示成几千万度。2.2 SNMP Agent扩展方案对比SNMP Agent本身有几种实现思路我对比后选了最省事的一种复用Linux系统自带的net-snmp再用pass_persist机制挂一个外部脚本。先解释一下pass_persist。net-snmp允许在snmpd.conf里声明某个OID子树交给外部程序处理有两种模式pass模式每次收到SNMP请求系统就fork一个子进程来执行脚本脚本处理完就退出。好处是简单坏处是频繁fork对性能有影响而且脚本无法维护长连接状态比如PLC连接。pass_persist模式脚本启动后常驻内存通过stdin/stdout和snmpd通信。snmpd发PING脚本回PONGsnmpd发GET加OID脚本返回对应值。因为脚本是常驻的所以可以提前建立好和PLC的长连接性能好很多。我选的自然是pass_persist模式。还有一个更底层的做法是直接用pysnmp库自己实现一个完整的SNMP Agent自由度最高但需要自己处理SNMP协议细节、OID树的遍历逻辑开发量至少翻一倍。对于这个项目来说net-snmp已经做了大部分协议层面的工作自己只需要关注一件事怎么把PLC数据翻译成SNMP能理解的格式。所以性价比最高的方案就是“snmpd pass_persist脚本”。2.3 OID规划与数据类型映射OID规划是这个小项目里容易被忽视但实际很重要的设计环节。规划得好后期扩展设备时不用推倒重来规划得乱后面加数据点就是一场灾难。我用的是企业私有OID根节点1.3.6.1.4.1.5432154321是示例的企业号实际项目中应该向IANA申请或者用自己企业已有的企业号。在这个根节点下面按设备和数据类型分区1.3.6.1.4.1.54321.1.x.x —— 1号PLC的数据 1.3.6.1.4.1.54321.2.x.x —— 2号PLC的数据 1.3.6.1.4.1.54321.3.x.x —— 3号PLC的数据每台PLC内部再按数据点顺序编号.x.1.0 —— 设备状态 .x.2.0 —— 运行模式 .x.3.0 —— 当前产量 .x.4.0 —— 目标产量 .x.5.0 —— 温度 .x.6.0 —— 报警码 .x.7.0 —— 累计运行时间数据类型映射上SNMP有自己的一套类型体系和PLC的数据类型不完全对应。我的处理方式如下PLC数据类型SNMP类型说明BOOLINTEGER0/1状态位直接转整数INT / WORDINTEGER原样映射REAL浮点数Gauge32乘10或100后取整避免浮点精度问题DWORD累计值Counter32 或 Gauge32累计值用Counter瞬时值用Gauge这里有个经验REAL型数据直接传给SNMP是很麻烦的因为标准SNMP类型里没有浮点数Counter64、Gauge32都是整数。所以实践中普遍的做法是放大10倍或100倍再传Zabbix端拿到后用预处理功能除以10或100还原。比如温度25.6℃SNMP侧传256Zabbix里再除以10显示为25.6。如果不做放大处理浮点转整数的精度丢失会让温度显示长期停留在25℃。3. 实操过程与实现细节3.1 环境准备Agent服务器操作系统是Ubuntu 22.04需要安装net-snmp、snmpd和python3-snap7sudo apt update sudo apt install snmpd snmp python3-snap7 -y安装完成后先确认snmpd服务正常启动systemctl status snmpd如果服务没有启动一般是因为snmpd的配置有问题可以查看/var/log/syslog里的报错信息。默认配置下snmpd会监听所有接口的161端口但只允许本机和localhost访问后面要修改配置才能让Zabbix远程访问。另外python3-snap7依赖libsnap7安装时会自动拉取。需要注意的是Snap7对新版固件的S7-1500支持情况偶尔会有版本兼容问题如果连接不上优先确认python3-snap7的版本并在官方仓库查看最新的支持说明。3.2 PLC数据采集实现PLC数据采集是这个项目的核心逻辑。我写了一个完整的pass_persist脚本下面这段是主要实现重点看三个部分PLC连接管理、数据缓存、GET/GETNEXT处理。#!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys import time import struct import snap7 PLC_IP 192.168.10.10 RACK, SLOT 0, 0 DB_NUM 1 BASE_OID 1.3.6.1.4.1.54321.1 # OID - (偏移, 类型) OID_MAP { f{BASE_OID}.1.0: (0, byte), # 设备状态 f{BASE_OID}.2.0: (1, byte), # 运行模式 f{BASE_OID}.3.0: (2, int), # 当前产量 f{BASE_OID}.4.0: (4, int), # 目标产量 f{BASE_OID}.5.0: (6, real), # 温度放大10倍 f{BASE_OID}.6.0: (10, word), # 报警码 f{BASE_OID}.7.0: (12, dword), # 累计运行秒数 } _cache {} _last_read 0.0 _plc None CACHE_TTL 2 def ensure_connection(): global _plc if _plc is not None: return _plc _plc snap7.client.Client() _plc.connect(PLC_IP, RACK, SLOT) return _plc def _reconnect(): global _plc try: if _plc is not None: _plc.disconnect() except Exception: pass _plc None def read_plc_data(forceFalse): global _cache, _last_read now time.time() if not force and _cache and (now - _last_read) CACHE_TTL: return _cache try: plc ensure_connection() data plc.db_read(DB_NUM, 0, 20) if len(data) 20: _cache { f{BASE_OID}.1.0: str(data[0]), f{BASE_OID}.2.0: str(data[1]), f{BASE_OID}.3.0: str(struct.unpack(h, data[2:4])[0]), f{BASE_OID}.4.0: str(struct.unpack(h, data[4:6])[0]), f{BASE_OID}.5.0: str(int(struct.unpack(f, data[6:10])[0] * 10)), f{BASE_OID}.6.0: str(struct.unpack(H, data[10:12])[0]), f{BASE_OID}.7.0: str(struct.unpack(I, data[12:16])[0]), } _last_read now except Exception as e: _reconnect() return _cache def handle_get(oid): data read_plc_data() if oid in data: return oid, data[oid] return None def handle_getnext(oid): data read_plc_data() keys sorted(data.keys()) for k in keys: if oid or k oid: return k, data[k] return None def main(): read_plc_data(forceTrue) while True: line sys.stdin.readline() if not line: break cmd line.strip() if cmd PING: sys.stdout.write(PONG\n) sys.stdout.flush() elif cmd in (GET, GETNEXT): oid sys.stdin.readline().strip() result handle_get(oid) if cmd GET else handle_getnext(oid) if result: sys.stdout.write(f{result[0]}\ninteger\n{result[1]}\n) else: sys.stdout.write(NONE\n) sys.stdout.flush() if __name__ __main__: main()脚本的逻辑其实不复杂但有几个细节值得多说一句第一数据缓存。SNMP轮询的节奏往往很密集Zabbix默认可能每30秒轮询一次但如果你用snmpwalk手动测试会在几秒内发出几十个请求。如果不加缓存每个请求都去读一次PLCPLC的连接资源会被快速消耗严重时会导致PLC侧通信故障。我设置了2秒的缓存TTL实测下来对Zabbix的秒级监控要求完全够用又不会对PLC造成压力。第二异常重连。read_plc_data里捕获了所有异常一旦捕获就调用_reconnect()断开连接下次请求时重新连。这样即使PLC短暂掉线比如PLC重启、网线松动脚本也能自动恢复不需要人工干预。第三持久连接还是每次重连。我最终选择了保持长连接ensure_connection维护全局_plc因为S7协议本身是有状态的频繁建连/断连会增加PLC的通信负担。3.3 snmpd pass_persist配置脚本写好后需要告诉snmpd哪些OID交给这个脚本处理。编辑/etc/snmp/snmpd.conf在文件末尾追加一行pass_persist .1.3.6.1.4.1.54321 /usr/bin/python3 /opt/plc_snmp_bridge.py这里有个小坑需要注意pass_persist后面跟的OID不需要写成.1.3.6.1.4.1.54321的完整形式但net-snmp对OID的匹配机制是前缀匹配所以1.3.6.1.4.1.54321开头的所有OID请求都会转发给这个脚本。如果脚本内部无法处理某个OID需要返回NONE否则snmpd会陷入死循环。还要改一下snmpd的监听配置让Zabbix能远程访问。默认配置里agentAddress参数是udp:127.0.0.1:161只监听本机。改成监听所有接口agentAddress udp:161然后重启snmpdsudo systemctl restart snmpd在服务器本机验证一下snmpwalk -v2c -c public 127.0.0.1 .1.3.6.1.4.1.54321如果脚本工作正常会输出类似下面的内容SNMPv2-SMI::enterprises.54321.1.1.0 INTEGER: 1 SNMPv2-SMI::enterprises.54321.1.2.0 INTEGER: 1 SNMPv2-SMI::enterprises.54321.1.3.0 INTEGER: 128 SNMPv2-SMI::enterprises.54321.1.4.0 INTEGER: 500 SNMPv2-SMI::enterprises.54321.1.5.0 INTEGER: 256 SNMPv2-SMI::enterprises.54321.1.6.0 INTEGER: 0看到这个输出说明PLC数据已经通过SNMP协议暴露出来了。3.4 Zabbix对接配置Zabbix侧配置相对简单但有几个容易踩的细节。打开Zabbix前端界面按照下面的步骤操作创建主机数据采集 → 主机 → 创建主机主机名称填“PLC-01”群组选“生产设备”配置SNMP接口在“接口”区域选择“添加”类型选“SNMP”IP地址填Agent服务器的IP192.168.10.20端口保持161设置SNMP community主机宏里添加{$SNMP_COMMUNITY}值填snmpd配置的community名称默认public强烈建议修改成强口令等待主机发现成功Zabbix会对SNMP接口做可用性检查状态变成“已启用”就说明Agent侧没有问题添加监控项在主机上添加监控项类型选“SNMP agent”OID填1.3.6.1.4.1.54321.1.1.0数据类型选“Numeric (unsigned)”更新间隔设30秒。这里提醒一句Zabbix对SNMP的可用性检测默认用的是sysDescr.0这个OID1.3.6.1.2.1.1.1.0。因为我们的snmpd是系统自带的这个OID会被net-snmp自己处理所以可用性检查没问题。但如果哪天你自己实现了一个单独的SNMP Agent记得一定要实现sysDescr.0否则Zabbix会一直报告主机不可用。数据点添加完后再创建图形和触发器。比如设备状态的触发器表达式last(/PLC-01/state) 3这里的3对应前面OID映射里的“故障”状态。当PLC运行状态变为故障时Zabbix会自动触发告警。对于温度数据因为我们在Agent侧已经乘了10所以Zabbix监控项里要做预处理添加“自定义倍数”步骤值除以10。这样Zabbix展示出来的温度就是正常的25.6℃。4. 常见问题与排查技巧4.1 常见问题速查表项目交付前后我整理了一批高频问题基本都是实操中一定会遇到的现象可能原因解决方式snmpwalk超时无响应防火墙未放行UDP 161端口在Agent服务器和Zabbix侧防火墙放行UDP 161snmpwalk返回超时但本机测试正常snmpd只监听了127.0.0.1修改agentAddress为udp:161脚本不返回任何值脚本缺少执行权限或Python依赖缺失手动执行脚本测试PING/GET确认python3-snap7已安装PLC数据一直读不到PLC侧未开启PUT/GET访问在博图中勾选“允许从远程伙伴进行PUT/GET通信访问”PLC连接偶尔断开S7连接数超限检查PLC连接资源增加脚本缓存时间降低轮询频率读出的数值特别大或为负数字节序或数据类型解析错误确认用f/h/H等大端解析核对DB偏移Zabbix显示主机不可用未实现sysDescr.0或者community不匹配用snmpget验证sysDescr.0检查宏值4.2 性能与稳定性优化项目上线前我做了几轮压力测试。用snmpwalk连续遍历整个OID树模拟Zabbix高频轮询的场景。刚开始确实发现PLC连接不稳定后来通过加缓存和长连接解决。这里有几个实际有效的优化思路缓存策略一定要有。上面脚本里设置了2秒TTL整个OID树的数据在这2秒内是快照无论Zabbix怎么高频遍历PLC侧只会每2秒被读一次。用time命令测试单个GET响应时间基本在1毫秒以内比直连PLC快了一个数量级。用snmptrap做主动告警。SNMP除了轮询还有主动上报的机制。像“设备故障”这种关键事件轮询最快也要等一个采集周期比如30秒才能发现。如果现场对故障响应时间要求高可以考虑在脚本里监听PLC的故障信号一旦触发就主动发送SNMP Trap给Zabbix。Zabbix的SNMP Trap监控项可以做到秒级告警。这个方案我只是验证过可行性实际项目里因为客户接受30秒延迟所以没上但值得作为改进方向。systemd守护进程。pass_persist脚本是常驻进程如果脚本本身崩溃snmpd会标记这个子树不可用导致所有数据点都读不到。建议给脚本配置systemd服务设置Restartalways并在脚本里加日志输出方便排查崩溃原因。安全加固。SNMP v1/v2c的community是明文传输的相当于共享密码。现场环境里如果监控网络和办公网络没有严格隔离建议直接用SNMP v3或者至少在防火墙配置中限制只有Zabbix Server的IP能访问161端口。这个项目里客户网络整改过一轮我最终给Agent配置了源IP白名单只允许Zabbix Server访问。4.3 踩坑记录与经验心得讲几个印象比较深的坑都是花了不少时间才排查出来的。第一个是S7-1200的TSAP问题。项目里有一台S7-1200固件版本比较老用默认的(0,0)连接参数怎么都连不上报错信息看半天也没头绪。后来查Snap7的官方文档才知道S7-1200/1500的TSAP和S7-300/400不一样老固件对TSAP校验更严格。解决方案是用Snap7的connect_tsap方法手动指定本地和远端的TSAP值比如0x0301和0x0300。这里特别提醒如果你负责的PLC型号比较杂最好在采集脚本里把连接参数做成可配置的不然每换一台PLC都要改代码。第二个是关于REAL类型的精度问题。SNMP侧传浮点数天然不方便我一开始图省事直接把REAL转成整数取整结果温度值一直是25小数点后面的内容全丢了。后来改成乘10再取整Zabbix端拿到256预处理后显示25.6℃精度完全够用。这个做法在工程上很常见但一定要在文档里写清楚所有REAL数据都放大了10倍避免后续接手的人误读。第三个是字节序导致的“离谱数据”。第一次联调时读上来的温度显示为-123456789.0。当时第一反应是PLC侧的地址映射错了折腾半天最后才意识到是大小端问题。PLC的DB块里存的是big-endian我的采集脚本跑在x86服务器上默认按小端解析所以数值全乱了。加了struct.unpack(f, ...)之后数据立刻正常。这个坑对写过工业协议的老手来说不算什么但对刚接触这个领域的人来说非常容易踩。第四个是关于Zabbix的轮询频率。我遇到过客户反馈“Zabbix里数据很久不更新”排查下来发现不是Agent的问题而是监控项的更新间隔设成了5分钟再加上触发器依赖最新值看起来就像“卡住”了。后来把监控项更新间隔统一调整为30秒问题解决。所以对接Zabbix时除了关注Agent侧也要把所有监控项的采集间隔梳理一遍。最后再分享一点跨部门协作的经验。这种OT/IT联动的项目最容易出问题的地方往往不是技术而是双方对“数据点”的理解不一致。PLC工程师手里的数据点表写的是“DB1.DBD6”IT运维那边关心的是“温度这个OID到底对应哪个值”。每次联调都要来回核对很浪费时间。这个项目里我提了个建议把所有参与采集的数据点先在Excel里画一张映射表包含PLC地址、偏移、数据类型、OID、SNMP类型、Zabbix监控项名称、报警阈值一次对清楚再动手开发。这张表后来成了项目的核心交付文档比任何代码都有价值。如果你也在做类似项目建议第一步就先拉这张表能省掉后面80%的沟通成本。