搞运维的人多少都经历过这种场面:凌晨两点手机震动,说某栋办公楼的网络延迟骤升,跑到机房一看,那台接入层交换机的CPU利用率已经烧到95%,风扇声音响得跟飞机起飞一样。设备重启之后网络是通了,但问题到底什么时候开始的、哪台设备还有隐患,全靠翻日志和拍脑袋。后来我花了大概三周时间,用Flask这个Python框架做了一套专注于交换机故障预警和处理的运维管理系统,把所有核心交换机、汇聚交换机的运行状态统一纳管起来,被动的夜间救火才慢慢变成了主动的预警和处理。
简单说这套系统的价值就是三句话:提前发现隐患、自动通知到位、处理留痕可查。对单人运维或两三个人的小团队来说,它比动不动就大几十万的商业网管平台更贴近真实需求,也比自己拿脚本拼凑的监控方案更完整、更规范。如果你正好也在做网络运维,或者想用Flask搭一套带数据采集、告警、工单处理的内部系统,这篇文章里的架构思路、核心代码、踩坑记录,基本上可以照着抄。
1. 为什么会做这个系统:不是所有监控都适合你的场景
先说背景。当时我所在的某公司大概有四十多台交换机,分布在办公区、机房、生产车间几个不同的VLAN里,品牌还不统一,有常见的两类主流厂商设备。之前一直靠每周人工登一次设备查看状态,CPU、内存、端口流量这些指标全靠肉眼判断,问题是设备多的时候一台台登录巡检基本不现实,而且故障发生往往集中在晚上和节假日,等人发现再处理,影响早就扩散了。
市面上不是没有商业监控产品,我也评估过。那些产品功能确实全,但问题在于:第一,价格不便宜,小团队很难说服管理层批预算;第二,很多重型平台对硬件和部署环境有要求,运维成本本身就不低;第三,也是最关键的,网络拓扑和告警策略每个企业都不一样,商业系统的规则引擎再灵活,配置起来也很费劲,想加一个“某台设备CPU连续三次超过阈值才告警”这种贴合自己场景的逻辑,往往要找原厂支持。
因此我决定自己动手。选型阶段考虑过几个方向:写纯Shell脚本加Crontab定时跑,采集数据存文件,但这套方案最大的问题是数据无法统一查询、没有Web界面、告警通知逻辑写起来非常痛苦;也考虑过用其他更重的框架,比如Django或者FastAPI,后来权衡了一下还是Flask。原因很简单:Flask足够轻量、灵活,路由和视图函数清晰,对于这种以内网工具定位的系统来说完全够用,而且作为一个Python开发者,基于Flask去组织数据模型、ORM、定时任务这些组件,生态非常成熟,不需要重复造轮子。
这个决策背后其实有一个很朴素的逻辑:选型不是跟风选重的、选大而全的,而是先算清楚整个系统的核心任务是什么。我的核心任务只有两个,一个是定时采集交换机关键性能指标,另一个是对指标做阈值判断并触发告警与处理流程。想明白了这一点,技术选型就变得简单了——Flask加一个任务调度器加一个数据库,足够支撑这两件事。
2. 整体架构和选型思路:Flask并不是随便选的
整套系统的架构可以分成四层:采集层、数据层、业务逻辑层、展现层,各层之间通过明确的功能边界解耦。部署形态上,系统跑在一台内网服务器上,通过管理VLAN访问所有交换机的SNMP服务,整个系统对外的入口就是Flask提供的若干HTTP路由。
在这里我想先把每一项选型背后的理由说透,因为这些东西在项目做完之后回头看,比代码本身更值得沉淀。
2.1 数据采集:为什么所有指标都走SNMP
交换机故障预警的前提是拿到可靠的指标数据。关于采集方式,我评估过两种:一种是通过SSH登录设备执行巡检命令,然后用正则解析输出;另一种是走SNMP协议主动拉取MIB库里的OID节点值。最开始我想的是SSH方式,因为很多运维同行习惯用命令行,感觉更直接,但仔细对比之后发现SNMP优势明显。
用SSH采集有两个致命问题:一是不同厂商甚至不同型号的交换机,同一指标的命令输出格式都不一样,解析规则可能要写成千行代码,维护成本极高;二是SSH登录需要在系统里保存明文密码或者密钥,对交换机的安全边界是个破坏。而SNMP是网络设备普遍支持的标准协议,通过统一的OID节点来获取CPU利用率、内存使用率、端口状态、接口流量这些指标,不存在格式兼容问题,而且社区版SNMP只开UDP 161端口、用只读团体字符串,风险面小很多。
当时用的是Python的pysnmp库,这个库功能强,但API风格偏硬核,不少人第一次用会觉得别扭。后来实践发现,封装一个简洁的采集函数很有必要,把所有交换机设备的社区字符串、SNMP版本、OID映射表统一管理,主逻辑里只关心“哪台设备、采集什么指标、拿到了什么值”,这也是我在第4节里会展示的代码思路。采集周期设了两种:核心设备每60秒轮询一次,接入层和汇聚层设备每300秒轮询一次。这个参数不是拍脑袋定的,核心交换机承载的链路故障直接影响所有业务,需要更细的采样粒度来捕捉瞬时飙升;而接入层压力相对可控,拉取太频繁反而会增加交换机CPU负担。
2.2 数据存储:SQLite起步,预留升级路径
存储层我一开始就选了SQLite。很多人可能觉得SQLite是小打小闹,但在这个场景下它非常合适:系统部署单机,写并发并不高,定时采集任务往数据库插入的数据量大概每秒几十条,SQLite对这种负载绰绰有余。而且SQLite的零配置文件特性,让系统的部署成本降到最低,拷贝一个文件就能跑起来。
不过设计数据模型的时候,我特意把表结构按照后续迁移到MySQL/PG的要求来做,字段类型尽量避开SQLite特有的写法,外键关系和索引也都提前规划好。这么做的好处是,如果将来设备数量增长到几百台,或者需要多个运维人员同时在线处理告警,我可以平滑地切换数据库后端,不至于重写整个数据访问层。
设备表和告警记录表是核心。设备表除了存IP地址、设备名、型号、位置、SNMP团体串这些基本信息外,我还预留了分组字段,方便把同楼宇或同业务线的设备归组,告警阈值和通知策略会按组下发。告警表记录每次触发的类型、级别、状态、触发时间、恢复时间、处理人、处理备注,这样每一条告警从出生到闭环都有完整审计链路,出了问题也方便复盘。
2.3 后台任务:定时采集不是用Crontab硬顶的
采集任务要周期性地在后台运行,一开始我确实考虑过直接用系统Crontab每分钟调用采集脚本,后来放弃了。原因很简单,Crontab方式把任务的调度逻辑和系统本身割裂了,任务执行的状态、失败重试、并发控制都很难管理,比如某一次采集因为网络抖动超时了,Crontab不会自动补偿下一次就会执行,丢数据的问题没法通过简单的配置解决。
我用的是APScheduler来做任务调度,把采集任务以一个后台守护进程的形式运行在Flask应用内部。APScheduler支持按固定间隔触发、Cron表达式触发,还可以对任务做持久化和错过任务的处理策略。实际配置中,我用的是BackgroundScheduler,配合自定义的作业存储器,这样即使应用进程重启,已注册的任务也不会丢失。这一步看起来小,但直接决定了系统在无人值守时的可靠性。
3. 核心模块拆解:从设备录入到告警闭环
系统的整体功能可以拆成五个模块:设备管理、数据采集、故障预警、告警通知、告警处理。这些模块不是各自孤立的,而是串成了一条完整的闭环链路。下面逐个说明设计意图和关键细节。
3.1 设备管理模块:给所有交换机建一个统一档案
设备管理模块是整个系统的基础。录入设备时,除了基础信息以外,有两项设置非常关键,一个是设备的采集方式参数,另一个是阈值策略。
采集方式参数包括SNMP版本(V2C还是V3)、端口、团体字符串、超时时间和重试次数。这些参数直接影响后续采集的成功率。举个例子,很多老设备默认SNMP超时时间是2秒,但在高负载或者管理网络不畅时,2秒根本不够,如果不给设备单独配置更长的超时时间,采集失败率会很高,误报的概率也会上升。所以我在设备表里设计了这些字段,而不是做一个全局统一配置。
阈值策略做成可分组配置的。核心交换机CPU利用率超过75%预警,超过90%告警;接入层则相对宽松,80%预警、95%告警。内存阈值的判断逻辑类似。端口状态则更特殊,它不是一个连续变化的数值,而是一个离散的状态量,所以在策略设计上,我针对“端口Down”这种瞬时状态做了单独的逻辑:单条端口Down只记录,不去触发高等级告警,但如果同一台设备同一时刻多条端口Down,则极有可能是链路聚合断开或者设备重启,这种需要立刻升级告警级别。
3.2 数据采集模块:耐心处理每一个OID返回值
采集模块解决的问题就是“怎么拿到值”和“拿到的值怎么处理”。SNMP的OID是设备MIB库的节点标识,不同的OID对应不同指标,拿交换机CPU举例,主流厂商常见的CPU利用率OID虽然路径不一样,但返回值的语义是一致的,都是表示设备CPU使用百分比。
采集过程中比较容易被忽略的是返回值类型的处理。SNMP返回的大整数、Counter32、Gauge32、TimeTicks这些类型,直接拿来用会有陷阱。就以接口流量为例,SNMP返回的字节计数是Counter类型,它虽然表示累计值,但在计数器归零回绕之后,直接做差会得到负数。我在系统里对这类计数器做了专门处理,每次采集时记录上一次的值和上一次的时间戳,通过“当前值减去上次值除以时间差”来换算成每秒速率,同时判断计数器是否发生过回绕,如果回绕了就按“当前值加上最大值再减去上次值”来修正。
采集模块还需要关注故障态。超时无响应、SNMP认证失败、OID不存在,这三种错误状态都要能区分开记录到日志里。如果某台设备连续三次采集失败,系统会将它标记为“设备不可达”,这个状态本身就会触发一条故障告警——设备没数据本身就是最大的故障信号。
3.3 故障预警引擎:不是简单的阈值比较
预警引擎是整个系统的判断中枢。它接收采集模块写入原始指标表的数据,按照策略表中的规则来判断是否产生告警,并决定告警的级别。
我做了三级告警级别:警告、严重、紧急。级别判断不是只看单次数据是否越界,而是同时考虑持续时间和连续次数。
中心思想就是一个“去抖动”的过程。采集数据不可避免会有偶发的波动,比如某台交换机在业务高峰期CPU瞬间跳到90%,但一秒钟之后就恢复到正常,这种瞬时的尖峰如果直接触发告警,值班人员很快就会被大量无效告警淹没,产生告警疲劳。我的规则是:只有当连续三次采集都超过阈值时,才产生一次真正的告警事件,其中任何一次回到阈值以下,计次就清零。时间维度上,超过警告阈值持续时间超过5分钟,自动升级为严重告警。这一层设计让告警数量至少降低了70%,接收告警的人终于愿意认真看待每一条消息了。
3.4 告警通知与处理流程:让告警真正被响应
告警触发了却没人知道,那这套系统搭建就没有意义了。通知模块最初只发邮件,后来用了企业内部IM的Webhook机器人,接入之后效果立竿见影——第一时间推送到手机端就能看到。每条通知消息会带上设备IP、告警类型、当前值、阈值、持续时间,让接收人不用打开Web界面就能判断问题的严重程度。
告警处理流程走典型的工单闭环:告警生成后状态是“待处理”,运维人员可以到Web界面上确认告警、填写处理备注、标记告警为“处理中”,故障恢复后系统会自动把告警状态改为“已恢复”,同时记录恢复时间。如果一条告警在生成两小时后仍没有人确认,系统会自动再发一条催促通知。这一套逻辑写起来不难,但它在真实运维中的价值是巨大的——每一条告警都有了责任人,再也不会出现“通知了但不知道谁在处理”的模糊局面。
4. 关键代码实现:把思路落成能跑的代码
上面讲的是设计层面的事,落地的时候会遇到很多具体问题。下面分享几个核心模块的关键实现,每段代码里都注释了我当时踩过的坑和解决思路。
4.1 SNMP采集核心封装
我基于pysnmp封装了一个采集函数,简化调用方的复杂度。封装的重点是处理超时、重试和返回值解析,把底层的SNMP细节屏蔽掉。
from pysnmp.hlapi import * import time # OID映射表,不同厂商设备建议单独维护 OID_CPU_USAGE = '1.3.6.1.4.1.9.9.109.1.1.1.1.3' # 某厂商CPU使用率OID OID_MEM_USAGE = '1.3.6.1.4.1.9.9.48.1.1.1.1.5' # 内存池使用量OID OID_PORT_STATUS = '1.3.6.1.2.1.2.2.1.7' # 接口状态表OID OID_IF_OCTETS = '1.3.6.1.2.1.2.2.1.16' # 接口接收字节计数OID def snmp_get_value(ip, community, oid, port=161, timeout=3): error_indication, error_status, error_index, var_binds = next( getCmd(SnmpEngine(), CommunityData(community, mpModel=0), # mpModel=0表示SNMPv1/v2c UdpTransportTarget((ip, port), timeout=timeout, retries=1), ContextData(), ObjectType(ObjectIdentity(oid))) ) if error_indication: raise TimeoutError(f'SNMP请求失败: {error_indication}') if error_status: raise RuntimeError(f'SNMP返回错误: {error_status.prettyPrint()}') return var_binds[0][1].prettyPrint()这段代码有几个细节值得注意。第一,超时时间设了3秒,重试次数设了1次,加上重试的耗时,单次采集最坏情况要6秒左右;如果同时采集几十台设备,串行跑一遍会非常慢,所以实际项目里我用了线程池并发采集,控制并发数不超过10,避免阻塞事件循环。第二,mpModel参数很多人会忽略,它表示SNMP协议的版本模式,对于老设备只能走v2c的情况非常关键。第三,返回值统一用prettyPrint转成字符串,再在调用方按字段转成整数或布尔值,避免类型处理侵入采集逻辑。
4.2 定时采集任务:并发控制与失败重试
采集任务注册到APScheduler里,每轮调度处理所有在线设备。这里给出线程池并发和采集失败标记的逻辑。
from concurrent.futures import ThreadPoolExecutor from apscheduler.schedulers.background import BackgroundScheduler MAX_WORKERS = 10 def collect_all_devices(): devices = get_all_devices() # 从数据库读取设备列表 with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: futures = {executor.submit(collect_single_device, dev): dev for dev in devices} for future in futures: dev = futures[future] try: future.result() # 采集成功,清除设备连续失败计数 clear_device_failed_count(dev['id']) except Exception as e: # 采集失败,失败次数累加,连续3次则触发设备不可达告警 mark_device_failed(dev['id'], str(e))这里有一个在实际运行中踩出来的教训:线程池不是越大越好。最初我把最大并发设到20,结果发现交换机响应SNMP请求本身就会消耗CPU资源,采集并发过高的时候反而导致部分设备响应超时。后来调低到10,并启用了指数退避重试策略,也就是第一次失败等5秒,第二次等10秒,第三次等20秒,超过三次就不再等本轮,整体采集稳定性明显提升。
4.3 预警逻辑:去抖与升级的实现要点
预警判断不复杂,核心是状态机式的连续计数。
def check_cpu_alert(device_id, cpu_usage): config = get_threshold_config(device_id) threshold_alert = config['cpu_warning'] # 例如 75 threshold_critical = config['cpu_critical'] # 例如 90 # 连续计数和当前告警状态从数据库或Redis读取 sequence_count = get_sequence_count(device_id, 'cpu_over_threshold') if cpu_usage > threshold_critical: sequence_count += 1 if sequence_count >= 3: # 连续3次超过紧急阈值 trigger_alert(device_id, 'cpu_usage', 'critical', cpu_usage) elif cpu_usage > threshold_alert: sequence_count += 1 if sequence_count >= 3: trigger_alert(device_id, 'cpu_usage', 'warning', cpu_usage) else: sequence_count = 0 # 回到正常范围,计数清零 save_sequence_count(device_id, 'cpu_over_threshold', sequence_count)这段逻辑可以概括为“三次确认制”。为什么要三次?因为采集周期如果是60秒,连续三次就意味着该状态至少持续了3分钟,基本上可以排除瞬时波动。加上阈值分级,系统不会因为一次CPU突发而把所有告警通道打满。实际运行中,这个策略让CPU类告警的准确率提升到接近100%,几乎没有再出现误报。
4.4 Flask路由与前端页面:给运维同学一个能用的界面
Flask在这一环节承担的是Web控制台的角色。路由设计遵循一个原则:每个页面只做一件事,页面之间通过告警状态和设备ID关联。
from flask import Flask, render_template, request, jsonify app = Flask(__name__) @app.route('/') def index(): # 仪表盘:核心指标汇总、最近告警列表、设备在线状态 stats = get_dashboard_stats() return render_template('index.html', stats=stats) @app.route('/devices') def device_list(): devices = get_all_devices() return render_template('devices.html', devices=devices) @app.route('/alerts') def alert_list(): # 支持按状态筛选:pending / processing / recovered status = request.args.get('status', 'pending') alerts = get_alerts_by_status(status) return render_template('alerts.html', alerts=alerts) @app.route('/alerts/<int:alert_id>/handle', methods=['POST']) def handle_alert(alert_id): # 确认告警、登记处理人、填写备注 handler = request.form.get('handler') remark = request.form.get('remark') update_alert_handler(alert_id, handler, remark) return redirect('/alerts')前端我用了简单的服务端渲染加一点JavaScript轮询,页面每10秒向后端拉一次最新的告警数据,实现了一个轻量级的实时刷新效果。没有引入复杂的前端框架,因为这套系统的使用者就是我司的运维同行,浏览器的兼容性和加载速度比酷炫的动态效果更重要。不过要给设备列表页加搜索和过滤,我建议还是引入一个轻量级的表格库,纯手写这些功能性价比不高。
5. 上线后的问题与排查实录
任何系统上线之后都会冒出一堆你写代码时想不到的问题,这套交换机预警系统也不例外。下面挑几个最有代表性的问题,把我当时的排查思路和处理方法记录在这里,希望给做类似系统的人省点弯路。
5.1 SNMP OID和厂商差异的适配问题
第一台设备采集成功之后,我以为所有品牌都大同小异,结果换了一个品牌设备,CPU利用率OID完全对不上,SNMP返回的数据明显是错的。这里要特别提醒的是,不同厂商的MIB库节点差异比想象中要大得多,而且同一厂商的不同型号设备,某些OID的返回值含义也可能不一致。
解决办法是建立一张“厂商型号与OID适配表”。每个新设备接入系统时,先通过设备信息的型号字段匹配到对应的OID映射配置,如果没有匹配项,就先用命令行工具手动验证该型号的OID,确认返回值语义正确后再录入系统。这个过程看起来琐碎,但精确的OID映射是后续所有告警判断的前提,马虎不得。
5.2 误告警导致告警疲劳
系统上线第一周,告警通知的频率非常高,最多的一天产生了两百多条告警。一开始我还觉得是网络真的问题多,后来一查发现相当一部分告警来自同一台设备,端口状态和CPU数据来回抖动。问题根源有两个,一个是我之前提到的阈值判断缺少连续次数确认,另一个是部分端口的物理状态本身就不稳定,比如某个端口连接着不常用的终端,终端休眠时端口会周期性Down/Up。
针对这个问题做了两件事:第一,所有告警规则引入了连续确认机制,即第3节里的“三次确认制”;第二,给端口类告警增加了“排除列表”配置,对于一些已知的、不影响业务的端口,可以手动加入排除名单,不再触发告警。同时对于短期无法根治的端口问题,把告警通知改为聚合通知,也就是同一个设备五分钟内的同类告警合并成一条消息推送,而不是一条一条轰炸。
5.3 任务积压与数据量增长
系统连续运行一个月后,数据表里累计了几十万条指标记录,一些带条件查询的页面开始变慢,采集任务偶尔也会出现积压。排查下来原因有两个:一是采集任务线程池中的任务超过设备数,部分设备被重复排队;二是告警条件查询语句没有走索引。
处理方式:给设备表和告警表的查询字段加上索引,尤其是设备ID、告警状态、触发时间这三个字段的组合索引,查询速度恢复到了毫秒级。对于指标数据,我加入了定期归档策略,超过30天的原始指标数据自动迁移到归档表,保留统计摘要即可,因为预警关注的是实时状态,历史明细更多是复盘和报表时才用得到。这个归档策略配合定时任务,保证系统长期运行也不会因为数据膨胀而拖垮性能。
5.4 告警通知可靠性
有一个月某台核心交换机确实发生了CPU持续飙升,但推送系统当时有一条告警通知延迟了将近五分钟才到达。排查发现是通知模块在处理历史积压的告警消息,队列里堆积了大量已恢复状态的旧告警,来不及发送。
优化措施很直接:通知队列里只放入“待通知”状态的实时告警,已恢复、已确认的告警从发送队列中移除;其次,通过备用通道保障送达率。这也让我意识到,告警通知本身要设计降级机制,正常情况走IM机器人,机器人不可达时自动转向邮件通知,保证极端情况下信息还是能传递出去。
6. 这个系统还能怎么演进:边界与扩展空间
现在这套系统在我这边已经稳定运行了相当长一段时间,每日新增告警从最初的上百条下降到现在平均几条,而且基本都是真实故障。聊聊后续可以扩展的方向,也算给对这类系统感兴趣的同行一些参考。
第一个方向是配置备份和定期巡检。交换机故障预警只是网络运维的一部分,很多故障其实源于配置变更,比如某台设备被人误改配置导致网络异常,如果能引入配置备份模块,定时抓取设备running-config并与上一次备份做差异比对,异常变更时同样触发告警,就能把问题挡在更早的环节。
第二个方向是知识库和自动处理脚本联动。目前告警处理主要是人工确认和登记,后续可以在告警类型上挂载处理预案,比如“CPU超过90%”关联一个重启指定端口的脚本入口,运维人员确认告警后一键执行预处理操作。当然,这个能力要谨慎控制权限,避免误操作造成更大范围的影响。
第三个方向是接入更多的监控维度。目前主要采集交换机本身的运行指标,图形化展示也比较朴素,后续可以引入流量趋势图、环比分析、容量预测这些能力。对于规模更大的环境,还可以把系统拆成采集端和服务端两部分,采集端部署在多个网络分区里,服务端统一汇聚数据,这样的分布式演进路径也能让系统的生命周期更长。
写在最后:一个系统真正的价值在于被日常使用
如果让我总结这套系统最核心的收获,我不会说“我用了Flask做了采集和告警”这种空心话。真正的收获是,我完整地走了一遍“以具体场景为起点,设计数据模型、写逻辑、再在真实应用里反复打磨”的过程。比如告警去抖、计数器回绕修正、SNMP超时重试这些问题,真的只有把系统放到生产环境里跑起来,才会逼着你去想明白。
最后再给准备做类似系统的朋友一个建议:第一版不要追求功能的全,把“一台设备的数据能采集上来、一条告警能从触达到恢复形成一个完整闭环”做到位,就已经能产生价值了。功能可以在运行过程中持续补强,但闭环的逻辑一开始就要立住,否则再多的页面和按钮也只是摆设。希望这篇分享能给同样在运维道路上自己动手解决问题的你一点参考。