ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

H3C设备双平台SNMP数据转换网关:架构设计与实战解析

H3C设备双平台SNMP数据转换网关:架构设计与实战解析 我直接讲这个项目的来龙去脉。上半年接了一个运维改造的活客户网络里跑着大量H3C设备原来有自己的一套网管体系做监控但今年上层要求把全网设备的状态统一汇聚到另外一个SOC平台偏偏两个平台之间用的是不同的MIB定义和告警策略。折腾了一圈之后发现两边都对SNMP有原生支持但都支持SNMP和数据能对上话是两回事最终落地成了一套SNMP数据转换SNMP的中间采集方案。这篇文章就把这个项目的来龙去脉、架构设计、关键OID映射规则、代码核心逻辑以及真正上线后踩过的坑全部摊开讲。内容适用于正在做网络设备统一纳管、多平台监控数据对接、或者需要把H3C设备质量检测NQA状态纳入统一告警体系的运维工程师也包括刚接触SNMP协议但需要上手做数据转换项目的朋友。按照这个项目的完整链路复现一遍你的平台对接问题基本就能解决大半。1. 项目背景这个SNMP转SNMP到底解决了什么问题先把这个需求讲明白。它不是那种常见的串口数据转SNMP或者Modbus转SNMP而是两个监控系统之间的事情——老平台已经能通过SNMP把H3C交换机、路由器的状态采集得很完整新平台也要通过SNMP拿数据但双方对设备状态的理解不一样。1.1 客户网络现状与监控平台的断层客户生产环境里大概有300多台网络设备以H3C为主混了一部分其他品牌的接入层交换机。老平台跑了很多年对H3C设备做了深度的定制采集包括端口流量、CPU利用率、内存池状态、单板温度还有一条很关键的数据——NQA链路质量检测的运行状态。新上线的那套SOC平台主要面向全局安全态势感知网络设备这边的数据它只认标准MIB-2接口加上有限的几个私有MIB定义。两个平台不在同一个网段甚至不在同一个安全域里头安全策略只放开了SNMP 161/UDP和162/UDP两个端口。一开始我们考虑过最笨的办法把每台H3C设备直接配置成向新平台发送并接收SNMP请求新平台自己去轮询。但试下来马上发现问题——新平台的采集器只认它自己内置的MIB库H3C设备返回来的很多扩展MIB节点它根本不认识尤其NQA那块一堆OID解析出来全是未知对象告警根本触发不了。另一个办法是让老平台把数据推给新平台但老平台对外提供的是一个私有API接口新平台对接这个接口的工作量并不小关键是老平台本身的采集周期是5分钟一次新平台安全业务要求秒级延迟接口推送这条路直接否认。最终定下来的方案就是做一层SNMP协议转换网关我们写一个独立服务从H3C设备上通过SNMP把原始数据采集下来做标准化处理后再通过SNMP协议以新平台能够识别的方式重新封装上报。相当于在两个SNMP世界中间加了一个翻译官。1.2 为什么坚持用SNMP对接SNMP而不是中间走HTTP或数据库这是客户一开始就问得最多的一个问题——既然都要写中间层了数据已经在手里了推给新平台直接用HTTP POST不好吗为什么还要再包装一层SNMP原因有三条都和实际落地条件强相关。第一新平台的接入规范和安全策略是写死的只接受SNMP协议接入设备。SOC平台那边对接的资产类型是网络设备它内部已经固化了网元建模逻辑——通过SNMP walk去发现接口表、路由表、地址转发表。如果走HTTP推送这些数据得新平台二次开发才能入库项目周期完全不可控。第二SNMP轮询是拉模式数据在采集端永远是被动的、按需的。如果走推送网关变成主动方一旦网络抖动、平台重启数据就会积压或者丢失。而SNMP自带超时重试机制在弱网环境下比我们自己写可靠性队列要省心得多。第三新平台的告警规则识别的是TRAP。它很多内置告警策略是绑定OID的我们直接转换后发送标准TRAP新平台不需要做任何规则开发告警通路当天就能打通。如果走API推送告警类型字段怎么映射要两边反复讨论最低效的就是这个。所以从实施成本上看用SNMP转换SNMP反而是最快、最稳、路径最短的方案。这个决策框架放到其他行业也一样成立——中间层不要引入新的协议栈尽量沿用两端的原生语言。2. 整体架构与转换机制的设计思路方案定下来之后第二步就是把转换链路的设计图画清楚。整个系统分三块采集层、转换层、上报层。下面把每一层承担的责任和边界讲透。2.1 采集层向H3C设备要原始数据采集层负责和真实设备打交道。每台设备配置一个只读的SNMP v2c社区字符串采集服务按设定周期发起GET/GETBULK请求把设备的关键指标拉回来。我们用Python的pysnmp库做采集最初因为pysnmp对异步的支持较好后来在实际生产环境又换成Net-SNMP命令行工具包来做批量walk原因后面在性能优化部分细说。采集对象包括系统基础信息sysDescr、sysUpTime、sysName标准MIB-2接口信息ifIndex、ifDescr、ifOperStatus、ifInOctets/ifOutOctets设备资源H3C私有MIB里的CPU利用率、内存利用率、板卡温度NQA运行状态H3C NQA测试组的状态节点采集频率上端口流量这类指标我们设置为60秒一轮NQA状态这类说过敏性指标设置为30秒一轮CPU和内存5分钟一轮就够了。SNMP轮询对设备CPU有一定开销频率太密会得不偿失。这里有一个关键细节采集层的Community和上报层的Community必须是两套完全不同的字符串。老网管用的社区字符串是强密码级别而新平台侧的社区字符串基本属于弱口令范畴平台要求简单可运维如果两者混用等于把核心设备的管理凭据直接暴露给了安全域外的系统。我们在转换网关里做了严格的密钥隔离配置从两个不同的配置文件中加载。2.2 转换层这是整个项目的灵魂转换层做的事情可以概括为三件事OID重映射、值类型转换、事件语义翻译。这也是我刚才说两边都支持SNMP但数据对不上话的病灶所在。第一件事OID重映射。同样表示接口1的当前状态老平台使用的是标准MIB-2的ifOperStatus1.3.6.1.2.1.2.2.1.8新平台对交换机接口健康度的判断却要依赖它自定义的节点。还有H3C的NQA状态H3C自己的MIB里定义的OID和通用网络管理平台里网络质量探针的OID完全不是一个树如果原封不动上报新平台解析出来就是一个未知节点。所以转换层必须维护一张OID映射表把采集到的原始OID一一对应到目标OID。第二件事值类型转换。SNMP的数据类型包括INTEGER、OCTET STRING、IPADDRESS、TimeTicks等不同平台对同一个语义的编码方式可能不同。例如sysUpTime在标准MIB里以1/100秒为单位的TimeTicks上报而新平台要求的是以秒为单位的INTEGER。有些平台的接口速率的单位是bps另外一些平台要求Kbps。转换层要把这些量纲和类型全部对齐。第三件事事件语义翻译。H3C NQA探测失败后会产生Traph3cNqaTestResultChanged新平台却不认识这条Trap它只认它的链路宕机告警。转换层收到设备原始Trap后根据映射规则翻译成新平台的告警TRAP再转发出去。值转换看上去琐碎但错一个单位监控画面上的数据就是错的。这一点在接口流量转换上特别明显实际项目中就出现过因为64位计数器转32位导致流量回绕的问题后面专门开一节讲。2.3 上报层以新平台的语言说话上报层负责把转换后的数据通过SNMP发送给目标平台。这里其实有两种模式大家做类似项目时也要先想清楚。第一种是网关模拟SNMP Agent。我们在网关服务器上开启一个虚拟Agent监听新的端口新平台把网关当成一台虚拟设备来轮询。新平台发起的GET操作网关再从真实设备上拉取最新数据返回。这种方式的好处是新平台完全无感知不需要改动平台侧任何配置坏处是网关必须实时响应对实时性要求高。第二种是网关模拟SNMP Manager主动上报。网关按照自己的节奏周期轮询真实设备把数据重新封装后作为SNMP请求主动发给新平台或者直接发送TRAP给新平台的162端口。这种方式不依赖新平台来问转换层可以自主控制频率适合告警事件这一类语义明确的场景。我们这个项目用的是混合模式普通性能指标流量、CPU、内存用第一种网关以虚拟Agent角色供新平台轮询而NQA状态变化、端口状态变更这类事件类数据用第二种转换层收到原始事件后翻译成告警TRAP立即推给新平台。设计成混合模式的原因是性能指标数据量大轮询频率由采集端控制更灵活走虚拟Agent可以让新平台自主定义采集周期但事件类数据天然具备突发性必须主动推送才有意义。3. H3C NQA状态的SNMP采集与告警归并现在重点拆一下NQA这条业务线这是整个项目里最绕也最容易被忽略的模块。简单说一下NQA是什么。H3C设备上的NQANetwork Quality Analyzer是一个主动检测工具可以定期向指定的目的IP发送ICMP请求、TCP连接探测或者HTTP请求用来衡量链路延迟、丢包率、连通性。我们客户在核心区和分支机构之间做了两条专线链路冗余靠NQA来实时判断主链路是否健康一旦探测失败就触发路由切换。NQA的运行状态原来是显示在H3C网管界面上新平台要求把每一条NQA测试组的状态纳管进统一告警。3.1 如何找到NQA状态对应的OID节点第一个拦路虎是OID定位。不同版本的H3C设备、不同产品形态NQA的MIB节点居然会有差异这一点确实容易踩坑。网上有人提到的OID一抓一大把但直接照抄基本会失败。我当时的做法是先确认设备型号和软件版本H3C的Comware V5和V7的MIB实现就有差别然后通过MIB浏览器连上设备去搜关键字把设备上实际存在的NQA节点找出来。# 在Linux机器上先通过snmpwalk确认设备上NQA相关节点 snmpwalk -v2c -c XXXXX 192.168.10.1 1.3.6.1.4.1.25506.2.6.1.9这条命令会输出一堆以该OID为根的子树节点我们需要在里面逐一识别哪个节点表示测试组编号、哪个节点表示测试例名称、哪个节点表示当前运行状态值。实际操作时在MIB管理工具里把h3cNqaMIB企业号25506整棵展开比在命令行里逐个猜要快得多。找到之后把关键OID记录进我们的映射配置# 采集NQA测试组状态的OID映射示例 nqa_oid_map { test_group_index: 1.3.6.1.4.1.25506.2.6.1.9.1.1.1.1.1, test_group_name: 1.3.6.1.4.1.25506.2.6.1.9.1.1.1.1.2, test_result_state: 1.3.6.1.4.1.25506.2.6.1.9.1.1.1.1.5, }注意生产环境里不同型号设备上这些节点索引规则可能不一样不要硬编码索引值而是由程序先通过walk获取完整表结构再动态建立索引到OID的对应关系。3.2 状态值翻译与事件归并策略H3C NQA测试结果的返回状态值是一个整数不同的设备定义略有差异但大体上0表示正常完成其他值表示探测超时、路由不可达、报文丢失等异常状态。问题在于新平台不认这一套它只认链路up/down所以转换层把设备原始状态值映射成新平台OID的两个值1表示正常2表示异常。但这里有个比枚举翻译重要得多的问题——抖动归并。NQA是周期性的假设每30秒探测一次链路抖动时30秒内探测结果可能是失败、失败、成功、失败、成功。如果每一条NQA状态变化都翻译成一条告警TRAP发给新平台新平台的告警中心会直接被打满而且会产生海量复报噪音。我们的做法是在转换层增加了一个状态缓存与去抖窗口设备上报的NQA测试组状态先进入内存缓存只有连续三次探测失败才认为链路真正故障并产生一条告警TRAP恢复时同样需要连续三次成功才发送恢复TRAP。窗口大小可以根据实际链路质量调节我们项目里主链路质量很好连续3次失败作为阈值基本不会漏报。# 去抖逻辑核心伪代码 state_cache {} def process_nqa_result(group_index, raw_state): if group_index not in state_cache: state_cache[group_index] {current: None, fail_times: 0, ok_times: 0} record state_cache[group_index] mapped_state 1 if raw_state 0 else 2 if mapped_state 2: record[fail_times] 1 record[ok_times] 0 if record[fail_times] 3 and record[current] ! 2: record[current] 2 send_trap(group_index, down) else: record[ok_times] 1 record[fail_times] 0 if record[ok_times] 3 and record[current] ! 1: record[current] 1 send_trap(group_index, up)这段逻辑看起来很简单但落地上有一个中心化问题如果转换网关是单节点部署缓存放本地没问题如果是多节点集群state_cache就必须使用分布式缓存我们用的Redis否则同一个测试组在两个节点上分别去抖告警会重复。这一点在项目初期差点翻车后面展开讲。3.3 NQA Trap的接收与翻译除了轮询NQA状态我们还接入了H3C设备主动上报的NQA Trap。H3C在NQA测试组结果发生变化时默认配置下可以向指定的Trap服务器发送Trap消息。我们的转换网关监听UDP 162端口接收Trap拿到的是设备发来的原生OID和变量绑定。由于设备Trap里携带的OID是H3C企业MIB节点新平台槽位不认识所以转换层需要把这条Trap翻译成新平台的标准告警。翻译流程分成四步第一步根据Trap的OID判断事件类型是测试组结果变化还是网络质量劣化第二步从Trap变量绑定里提取测试组索引和新的状态值第三步查映射表把状态值翻译成目标OID对应的数值第四步构造新的SNMPv2 Trap发送到新平台的162端口。这里有一个关键坑有些H3C设备的NQA Trap发送目标只支持配置一个IP而我们网关的IP和旧网管平台的IP都要接收Trap在设备侧只能配置一条Trap目标。最终方案是把旧网管的162端口流量通过交换机镜像复制一份到网关服务器或者设备上配置两个Trap目标视型号支持情况。具体哪种可行取决于设备版本建议在现场先做一次设备Trap发送验证再定方案。4. 核心代码实现与性能调优踩坑架构设计和映射规则都理顺之后进入编码和性能调优阶段。这一部分包含大量实际踩坑后的修正过程也是整个项目占工期最长的环节。4.1 采集模块的代码骨架与井喷问题采集模块第一版用的Python pysnmp库按每台设备一个协程的方式并行采集。写起来非常顺滑伪代码如下import asyncio from pysnmp.hlapi.asyncio import * async def get_device_metrics(device, oids): results {} for oid in oids: error_indication, error_status, error_index, var_binds await get_cmd( SnmpEngine(), CommunityData(device[community]), UdpTransportTarget((device[ip], 161)), ContextData(), ObjectType(ObjectIdentity(oid)) ) if not error_indication and not error_status: for name, val in var_binds: results[oid] val.prettyPrint() return results但上线第一天就直接踩了井喷定时任务一到点300台设备同时发起SNMP请求每台设备十几个OID网关的网卡瞬间被UDP包塞满设备侧CPU占用也飙高了。问题出在所有协程同时启动没有任何限速。SNMP底层是UDP无连接、不重传设备侧如果处理不过来直接丢包我们的超时重试机制反而加剧了网络拥塞。后来改用设备分片轮询的策略把300台设备分成20个批次每批次15台批次之间间隔2秒启动每台设备内部再按OID组串行请求避免对单台设备并发轰炸。改完后设备CPU平稳轮询成功率从最初的72%提升到99.6%。4.2 批量GET替代逐个GET性能立竿见影每台设备十几个OID如果逐个GET一次轮询就是十几个请求包300台设备就是几千个包。后来我们改用GETBULK批量读取一次请求可以拿回一整组OID的变量绑定。# Net-SNMP命令行示例一次批量获取多个OID值 snmpget -v2c -c XXXXX 192.168.10.1 .1.3.6.1.2.1.1.1.0 .1.3.6.1.2.1.1.3.0 .1.3.6.1.2.1.2.2.1.8.1GETBULK的优势在于repeaters机制指定一个重复次数设备端就可以把一整棵子树下的连续OID都返回。在采集H3C的接口表ifTable时一次GETBULK就能把全部接口的ifOperStatus、ifInOctets等列全部拿回来不需要逐行逐列去GET。不过GETBULK也有一个坑它默认返回的OID是按字典序连续的如果采集的OID节点在MIB树上的实际分布并不连续GETBULK就会返回一堆我们不关心的垃圾节点造成带宽浪费。解决方式是先做一次snmpwalk搞清楚目标节点的真实分布连续的段用GETBULK零散的用GET。4.3 64位计数器转32位的回绕问题这个坑非常典型必须单独讲。新平台要求采集接口流量数据标准MIB里ifInOctets是32位计数器但高速接口在百兆以上流量时32位计数器很快就回绕归零。我们最初直接采ifInOctets采回来的数经常出现流量突降为0然后又涨回来监控图完全没法看。后来仔细分析后改用64位计数器ifHCInOctets和ifHCOutOctets但问题来了——新平台有一部分老的设备模型只认32位计数器64位计数器节点它不识别。最终方案是在转换层自己维护了一个计数器跨周期增量计算器用64位计数器采集真实累计流量然后在网关内部计算两个采样周期之间的差值把差值作为周期内流量填入新平台要求的32位字段里。这样新平台拿到的就是一段时间的流量增量而不是累计值绕开了计数器回绕问题。这个方案的代价是网关必须有办法区分设备重启计数器清零和流量真的下降两种情况。我们的做法是同时监听sysUpTime如果发现设备sysUpTime比上次采集值小说明设备重启了这时候丢弃当前增量数据重新初始化基准值。这个逻辑不写的话设备一重启监控图上就会出现一个巨大的虚假流量高峰误报卡死。4.4 一张丢失数据排查链路从报文捕获到MIB对照上线第三周业务方反馈新平台上某台核心交换机的出方向流量偶尔会显示为0持续一跳就恢复。一开始怀疑是网络丢包但我们排查过程中发现一个更隐蔽的问题。第一步检查采集端日志。采集端软件记录的流量增量正常64位计数器读取到的数值符合预期说明数据采集环节没问题。第二步抓包验证上报链路。我们在网关出口和接入交换机上同时抓包抓包结果非常奇怪确实是网关发出了携带流量的SNMP响应包但新平台接收端显示的仍然是0。第三步逐字节解码报文。把一个正常的SNMP响应包和一个零流量包的BER编码逐字节对照发现网关返回的INTEGER值是正常的但新平台在解析时把我们返回的32位INTEGER当成了16位INTEGER来读高位被截断后数值直接变0。根因浮出水面新平台内置的MIB定义文件里这个流量节点的类型被错误地定义为INTEGER16和设备实际应该使用的INTEGER32不匹配。我们把这个MIB定义错误反馈给平台厂商同时转换层在封装时给这个节点增加了一个显式的OID类型强制标注从协议层告诉接收端这个字段是32位整数。问题解决后零值现象再没出现过。这个案例给我们的教训是SNMP数据看起来转发成功了不代表数据到达后没有被解析错排查转换类项目的问题一定得从报文解码层面去核对不能只看应用层日志。4.5 Trap并发与线程安全的处理Trap接收模块相对独立但我们在这里也踩了资源泄露的坑。最初用单线程阻塞式接收162端口Trap量小还没问题升级测试时模拟H3C设备批量上报Trap单线程处理不过来UDP包在socket缓冲区里大量积压最终被内核丢弃。改造方案是多线程接收队列架构接收线程只负责把UDP包文件描述符丢进内存队列处理线程从队列里取包做解析和翻译。队列长度设了5000条超过5000直接丢弃并在日志里记count防止处理不过来时内存溢出。import queue import threading import socket trap_queue queue.Queue(maxsize5000) def udp_listener(port162): sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((0.0.0.0, port)) while True: data, addr sock.recvfrom(65535) try: trap_queue.put_nowait((data, addr)) except queue.Full: logger.warning(trap queue full, message from %s dropped, addr) def trap_worker(): while True: data, addr trap_queue.get() process_trap(data, addr)队列满了宁可丢包也不能阻塞主接收线程这是UDP场景下保吞吐量的基本策略。丢掉的Trap因为后续还会通过周期性轮询补采所以对整体告警链路影响有限。理论上如果Trap量再上几个量级还得再考虑多队列分片或者接入消息中间件不过对于300台设备规模的场景单队列加12个worker已经完全够用。实际压测时我们模拟每秒2000条Trap涌入丢包率低于0.1%满足项目验收要求。5. 验收清单和一条走通全链路的实战经验项目交付前我们做了一轮比较完整的验收这里把检查清单分享出来照着逐项过比临时抓瞎测试要稳妥得多。5.1 验证数据链路是否真正打通的四步走第一步连通性验证。新平台发起SNMP GET能拿到网关虚拟Agent的响应响应时间小于1秒新平台能收到转换层主动发送的测试TRAP。第二步数据一致性验证。选取5台核心设备将网关采集到的CPU、内存、端口流量数据和新平台实际存储的最新一条数据做对比。允许存在一个采集周期内的时延差异但数值偏差不能超过1%。第三步海量数据压测。模拟设备批量重启、接口批量震荡、NQA批量失败三种场景观察新平台是否能在预期时间内看到对应的状态变化。压测过程中发现的问题有一多半是在这个阶段暴露的建议务必执行。第四步故障恢复验证。拔掉一台设备的上联光纤确认新平台在NQA连续3次失败后约90秒收到链路中断告警插回光纤确认恢复告警在90秒内上报。这步通过后项目的核心SLA才算锁定。5.2 从这次项目里沉淀下来的几条硬经验这个项目干下来最深刻的体会就是不管规划多细上线后总会遇到协议实现层面的意外。SNMP本身是一个很宽泛的协议各家设备、各版MIB对语义的解释多多少少有差异所谓的标准MIB在不同平台实现里也存在细微不同的处理方式。深究下来有几点是以后做类似项目一定会坚持的第一MIB文件必须逐厂商逐版本存档。H3C的MIB在不同Comware版本下节点定义不完全一致项目验收后如果设备升级MIB变化可能直接影响转换层解析结果。我们项目里就把设备型号-软件版本-MIB文件-映射配置捆绑存档升级设备前先核对MIB差异再决定是否要更新转换层配置。第二去抖逻辑的阈值最好不要拍脑袋定死。NQA连续失败3次告警这个阈值是结合客户链路质量和SLA要求调出来的换个网络环境可能就不合适。我建议直接把阈值参数化放配置文件里不要写死在代码中后期调整不需要重新发布服务。第三千万要重视安全策略中的端口复用问题。这个项目里新旧平台之间的网络只放开了161和162两个UDP端口看似很充足但在虚拟Agent模式下新平台轮询网关的161端口和网关轮询真实设备的161端口在网关内部是重合的。我们在设计时用不同监听地址和端口区分了两条链路这个细节如果漏了上线后SLA一定出问题。第四如果让我重新选型一次我会在采集层直接用Go重写而不是用Python加协程。Python在大量UDP包的场景下GIL和解释器开销比较明显Go的goroutine调度和内存占用更适合长时间运行的采集网关。但这是基于我们设备规模300台、每天产生几十万条性能数据的场景如果设备量小Python的灵活性反而更高选型不存在绝对最优。最后说一个项目实施中的小细节上线后一定要保留一台真实设备作为练兵设备随时可以放回流量的、随时可以封锁端口的那种。每次改完配置先用这台设备走一遍采集-转换-上报全链路确认无问题再批量推送配置到全部设备。我就是靠这个习惯在一个多月里改了好几轮映射规则但没有一次让生产链路断掉超过5分钟。希望这篇案例文章能给正在做类似平台对接、设备纳管的同行一个参考。
返回列表