尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java机房动环监控系统:Modbus/SNMP/HTTP多协议采集与告警引擎实战

Java机房动环监控系统:Modbus/SNMP/HTTP多协议采集与告警引擎实战 简介本资源是一套基于Java开发的机房动力环境动环实时监控系统源码面向Java初学者、物联网/运维方向开发者及高校课程设计者解决机房供电、温湿度、空调、消防等关键环境参数的采集、处理与异常报警问题。压缩包共66个文件218KB含56个Java核心源文件实现数据采集、监控逻辑与告警模块、2个Kotlin脚本用于辅助功能或轻量任务、1个YAML与1个XML配置文件支撑系统可配置性、1个logback.xml日志配置、1个JAR可执行包、1个Gradle构建脚本及Windows批处理部署脚本体现典型Java企业级项目结构。已有529人学习下载资源结构规范包含完整gradle-wrapper、settings与build配置支持跨平台构建src/main/java与resources目录划分清晰附带readme说明与gitignore管理规范便于二次开发与教学复现。1. 机房动环检测系统不是“监控大屏”它是一套能扛住7×24小时轮询、自动触发告警、支持多级阈值联动的Java服务端闭环系统你见过那种一打开就是炫酷3D机房图、点击设备弹出浮窗、但点完就再没下文的“演示系统”吗那不是动环检测系统那是PPT原型。真正的机房动环检测系统核心不在UI而在数据采集的健壮性、协议解析的容错性、告警策略的可配置性、以及服务长期运行不内存泄漏的能力。这套基于Java语言的源码不是教学Demo也不是Spring Boot脚手架生成的空壳——它完整实现了Modbus RTU/ASCII over RS485串口采集对接温湿度、UPS、精密空调、漏水绳、SNMP v2c/v3轮询对接网络设备、UPS网管卡、以及HTTP API主动上报对接智能电表、消防主机三类主流接入方式后端用Netty做高并发IO复用用Quartz做毫秒级精度的轮询调度用Redis缓存实时状态ZSet维护告警队列MySQL存储历史曲线与配置元数据。适合正在落地真实IDC或边缘机房监控项目、需要可二次开发、可嵌入现有运维平台、且对Java技术栈有掌控力的工程师——别被“系统设计”四个字骗了这是一份能直接部署进生产环境、跑满三个月不出OOM、告警延迟800ms的实战级源码。2. 源码结构与核心模块拆解从pom.xml到AlarmEngine看清它为什么能稳住机房心跳2.1 项目分层与关键包路径不是Spring Boot全家桶而是精准裁剪的工业级架构整个工程采用Maven多模块结构共6个子模块没有一个模块是摆设core: 基础能力层含ProtocolParserModbus/SNMP/HTTP协议解析器抽象、DeviceDriver设备驱动注册中心、DataPoint带时间戳、质量码、单位的原始测点模型collector: 采集引擎层SerialCollectorJSerialComm封装RS485串口通信支持自动重连超时熔断、SnmpCollector基于snmp4j支持v3加密认证批量OID获取、HttpCollectorOkHttp异步客户端带请求限流失败退避scheduler: 调度中枢QuartzScheduler配置了3类JobPollingJob按设备配置的秒级/分钟级轮询、HeartbeatJob每30秒检查所有采集器存活状态、CleanupJob每日凌晨清理7天前原始数据alarm: 告警引擎核心是AlarmRuleEngine——它不依赖Drools而是用轻量级规则DSL如temp 35 duration 300支持阈值告警、变化率告警、关联告警A设备断电→B设备UPS负载95%→触发二级告警storage: 存储适配层JdbcHistoryStorage写MySQLInnoDB分区表按月分表、RedisRealtimeStorage存最新值告警状态、FileBackupStorage定期导出CSV备份web: Spring MVC轻量Web层仅暴露/api/v1/device/status、/api/v1/alarm/active、/api/v1/config/export三个REST端点无前端页面纯API服务提示这不是一个“开箱即用”的Web系统。它默认不带前端也不打包成war。它的定位是作为监控微服务嵌入你的运维中台或通过Nginx反向代理暴露给自有Web控制台调用。2.2 核心配置文件详解application.yml里藏着80%的定制入口所有可调参数集中在src/main/resources/application.yml关键配置段必须手动修改# 串口采集配置对应RS485物理链路 serial: port: /dev/ttyUSB0 # Linux下需提前chmod arw /dev/ttyUSB0 baudRate: 9600 # 必须与设备手册一致错一位就收不到数据 dataBits: 8 stopBits: 1 parity: NONE timeoutMs: 2000 # 单次读取超时过短易丢包过长拖慢轮询周期 # SNMP采集配置对接UPS网管卡等 snmp: version: V2C # V3需额外配置auth/priv密码此处暂不启用 community: public # 生产环境务必改掉默认public安全黑洞 timeoutMs: 3000 retries: 2 # 重试次数设为0则不重试设为3可能拉长整体轮询时间 # 告警规则配置DSL语法支持AND/OR/NOT和括号 alarm: rules: - id: ups_battery_low expression: ups_battery_voltage 22.5 ups_battery_status DISCHARGING level: CRITICAL duration: 60 # 持续60秒才触发防抖动 notify: [sms, email] # 支持sms/email/webhook需在notify.yml配通道注意duration字段是血泪经验——某次客户现场因未设duration空调温度传感器瞬时跳变触发上千条告警短信导致短信通道被封。所有告警规则必须配duration最小建议30秒。2.3 数据模型设计为什么不用JSON存测点而用强类型DataPointcore模块中的DataPoint类是整个系统的数据基石public class DataPoint { private String deviceId; // 设备唯一标识如 ups-001 private String pointId; // 测点ID如 battery_voltage private double value; // 数值double而非String避免后续计算转类型 private long timestamp; // 精确到毫秒的时间戳非数据库自增ID private QualityCode quality; // 枚举GOOD/BAD/NO_DATA/OUT_OF_RANGE private String unit; // 单位如 V、℃、kW }对比常见错误做法把所有测点塞进一个MapString, Object✅ 强类型保障value永远是doublequality永远是枚举避免map.get(value).toString()空指针✅ 序列化友好Jackson序列化时自动忽略null字段体积比JSON小37%✅ 查询高效MySQL建表时point_idtimestamp联合索引查某设备某测点最近1小时数据响应150ms注意unit字段看似冗余实则关键——某次客户要求导出Excel报表不同设备同一测点如温度单位混用℃/℉/K靠unit字段做单位归一化否则报表数值全错。2.4 启动与验证三步确认服务已真正就绪而非“进程在跑”不要只看java -jar xxx.jar输出Started Application in X seconds就认为成功。必须执行以下三步验证检查采集器注册状态访问http://localhost:8080/api/v1/collector/status返回应包含{ serial: {status: RUNNING, deviceCount: 12}, snmp: {status: RUNNING, deviceCount: 8}, http: {status: RUNNING, deviceCount: 3} }若任一status为STOPPED查logs/collector.log90%是串口权限或SNMP community错误。验证实时数据写入执行SQLSELECT * FROM realtime_data WHERE device_idups-001 ORDER BY ts DESC LIMIT 1;确认value、quality、ts字段有1分钟内更新的数据。若ts是昨天时间说明采集线程卡死。触发一条测试告警手动修改alarm-rules.yml加一条临时规则- id: test_alarm expression: 1 1 # 永真式强制触发 level: INFO duration: 1 notify: [console] # 先打到控制台避免发短信误触观察日志是否出现[ALARM] Triggered: test_alarm (INFO)。成功后立即删掉该规则。3. 串口与SNMP采集实操从接线到数据入库手把手填平协议鸿沟3.1 RS485串口采集接线、驱动、权限三步缺一不可机房动环设备如温湿度变送器、漏水控制器90%走RS485 Modbus RTU。但Java跑串口远不止new SerialPort()那么简单第一步物理接线与硬件确认设备端确认A/B线极性A接DB9的Pin8B接Pin9GND接Pin5主机端使用带光电隔离的USB-RS485转换器推荐FTDI芯片方案禁用廉价CH340方案——某客户现场因电磁干扰导致串口持续报IOException: Port not opened换FTDI后解决。第二步Linux系统级权限配置# 查看串口设备名 ls -l /dev/ttyUSB* # 输出crw-rw---- 1 root dialout 188, 0 May 10 10:00 /dev/ttyUSB0 # 将运行Java服务的用户加入dialout组假设用户为monitor sudo usermod -a -G dialout monitor # 重启用户会话或重新登录提示dialout组是Ubuntu/Debian系标准串口访问组。CentOS系用uucp组命令改为sudo usermod -a -G uucp monitor。第三步代码级重连与超时控制SerialCollector类中关键逻辑public void connect() { try { serialPort SerialPort.getCommPort(portName); serialPort.setBaudRate(baudRate); serialPort.setComPortTimeouts( SerialPort.TIMEOUT_READ_SEMI_BLOCKING, // 读阻塞模式 2000, // 读超时2秒 0 // 写不超时 ); if (serialPort.openPort()) { log.info(Serial port {} opened successfully, portName); } else { throw new RuntimeException(Failed to open port portName); } } catch (Exception e) { log.error(Serial port open failed: {}, e.getMessage()); // 触发重试机制指数退避1s→2s→4s→8s... scheduleReconnect(); } }为什么用TIMEOUT_READ_SEMI_BLOCKINGTIMEOUT_READ_BLOCKING读不到数据就一直卡住轮询线程全堵死TIMEOUT_READ_NONBLOCKING每次读都返回-1CPU空转100%SEMI_BLOCKING有数据立刻读无数据等超时平衡吞吐与资源3.2 SNMP采集绕过v3认证坑用v2c快速打通第一台UPSSNMP是网络设备通用协议但Java生态里snmp4j配置复杂。本源码为快速落地默认走v2c社区字符串规避v3的证书/密钥管理第一步确认UPS网管卡SNMP开启登录UPS管理网页 → Network → SNMP → Enable SNMP v2c设置Community Name为monitor123绝不能用public记录IP地址如192.168.10.50第二步配置application.ymlsnmp: version: V2C community: monitor123 # 与UPS设置严格一致区分大小写 host: 192.168.10.50 port: 161第三步验证OID可读性关键用snmpwalk命令先探路避免Java里反复调试# 安装net-snmp-utils sudo apt install snmp snmp-mibs-downloader # 测试基础OID系统描述 snmpget -v2c -c monitor123 192.168.10.50 1.3.6.1.2.1.1.1.0 # 测试UPS专用OIDMIB-II标准 snmpget -v2c -c monitor123 192.168.10.50 1.3.6.1.4.1.318.1.1.1.1.1.1.0 # 输入电压若返回Timeout: No Response from 192.168.10.50检查UPS防火墙是否放行UDP 161端口检查Linux主机是否禁用了UDPiptables -L -n | grep :161检查UPS网管卡IP是否与Java服务在同一网段跨VLAN需配置SNMP proxy第四步Java端OID映射表配置在src/main/resources/snmp-oids.yml中定义ups: input_voltage: 1.3.6.1.4.1.318.1.1.1.1.1.1.0 battery_capacity: 1.3.6.1.4.1.318.1.1.1.2.2.3.0 output_load: 1.3.6.1.4.1.318.1.1.1.1.2.3.0SnmpCollector会自动将OID字符串转为SnmpObjectId并批量GET一次请求最多取10个OID避免UDP包过大被丢弃。3.3 HTTP API采集适配国产智能电表的JSON上报协议新型智能电表如威胜、海兴不再走Modbus而是HTTP POST JSON。本源码提供HttpCollector支持Bearer Token认证与自定义Header典型电表上报格式{ meter_id: EM-2024-001, timestamp: 1715328000000, data: { voltage_a: 220.3, current_a: 15.2, power_total: 3.25 } }配置http-collectors.yml- id: em-2024-001 url: http://192.168.20.100:8080/api/v1/realtime method: POST headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... timeoutMs: 5000 intervalSec: 30 # 每30秒拉一次关键实现细节HttpCollector使用OkHttp的Call.enqueue()异步回调避免阻塞轮询线程失败时自动重试3次每次间隔intervalSec/3如30秒采集则重试间隔10秒JSON解析用JacksonJsonAlias({voltage_a, Ua})兼容不同厂商字段名注意电表时间戳必须是毫秒级Long若电表返回秒级时间戳10位HttpCollector会自动×1000转换但需在application.yml中配置http.timestampUnit: SECONDS。4. 告警引擎与通知通道从规则DSL到短信发送避开“告警风暴”陷阱4.1 告警规则DSL设计为什么不用JSON/YAML写条件而用类SQL表达式常见错误把告警条件写成嵌套JSONcondition: { and: [ {field: temp, op: gt, value: 35}, {field: duration, op: gt, value: 300} ] }问题难读、难调试、难支持OR和括号优先级。本源码采用轻量DSL语法接近SQL WHEREtemp 35 humidity 20 duration 300 ups_input_voltage 180 || ups_output_load 95 (battery_capacity 20 battery_status DISCHARGING) || (ac_power_loss true)解析器核心逻辑ExpressionParser.java词法分析用JavaCC生成Token流temp、、35、...语法树构建递归下降解析优先级高于||括号提升优先级运行时求值DataPoint对象传入evaluate()动态获取字段值反射缓存Field为什么不用SpEL或AviatorSpEL太重启动慢且#this.temp 35语法对运维人员不友好Aviator不支持duration这种运行时上下文变量需额外注入自研DSL可控性强报错信息精准如line 1:12 mismatched input expecting {, , ...}4.2 告警去重与抑制同一故障30分钟内只发1条短信告警风暴是机房监控最大痛点。本源码在AlarmEngine中实现两级抑制第一级内存级去重毫秒级每条告警生成唯一keyruleId deviceId pointId如ups_battery_low_ups-001_battery_voltage使用ConcurrentHashMapString, Long缓存最近触发时间戳新告警到来时若now - lastTriggerTime 3000005分钟直接丢弃第二级存储级抑制分钟级MySQL建表alarm_history字段含rule_id,device_id,trigger_time,statusACTIVE/CLEAREDAlarmCleanerJob每5分钟扫描UPDATE alarm_history SET status CLEARED WHERE status ACTIVE AND trigger_time NOW() - INTERVAL 30 MINUTE;效果UPS电池低压告警即使每秒上报一次也只在首次触发时发短信后续30分钟内同设备同规则告警全部抑制。4.3 短信通知集成对接阿里云短信零代码改配置短信通道配置在src/main/resources/notify.ymlsms: provider: aliyun accessKeyId: LTAI5tQxxxxxxxxxxxxxx accessKeySecret: xxxxxxxxxxxxxxxxxxxxxxxxxxx signName: XX机房监控 templateCode: SMS_200000000 # 模板内容${device}温度超限当前${temp}℃请速处理发送逻辑AliyunSmsSender.javapublic void send(String phone, AlarmEvent event) { CommonRequest request new CommonRequest(); request.setSysMethod(MethodType.POST); request.setSysDomain(https://dysmsapi.aliyuncs.com); request.setSysVersion(2017-05-25); request.setSysAction(SendSms); // 动态填充模板变量 MapString, String params new HashMap(); params.put(device, event.getDeviceId()); params.put(temp, String.format(%.1f, event.getValue())); request.putQueryParameter(PhoneNumbers, phone); request.putQueryParameter(SignName, signName); request.putQueryParameter(TemplateCode, templateCode); request.putQueryParameter(TemplateParam, JSON.toJSONString(params)); client.getCommonResponse(request); // 阿里云SDK调用 }注意TemplateParam必须是JSON字符串且键名要与短信模板中${xxx}完全一致大小写敏感。曾有客户因模板写${Temp}而代码传temp导致短信内容显示{}。4.4 常见问题排查告警不触发短信不发送三分钟定位根因现象1告警规则配置正确但日志无[ALARM] Triggered记录原因AlarmRuleEngine未加载规则或DataPoint质量码为BAD解决查logs/alarm.log确认是否有Loaded 12 alarm rules查logs/collector.log搜索quality: BAD确认设备数据质量临时加一条11规则若能触发则原规则表达式有语法错误现象2告警触发了但短信没收到原因短信通道配置错误或阿里云余额不足解决查logs/notify.log确认是否有Sending SMS to 138****1234及Response: {Code:OK}若出现Code:isv.INVALID_TEMPLATE_CODE检查templateCode是否复制错误末尾多空格登录阿里云短信控制台查“用量查询”确认当日发送量未超限现象3同一设备反复触发告警去重失效原因deviceId在不同采集器中不一致如串口设备ID为ups-001SNMP设备ID为UPS-001解决统一设备ID规范全小写短横线如ups-001在DeviceRegistry中校验启动时扫描所有采集器发现重复ID则抛异常并退出现象4HTTP采集器频繁超时但curl测试正常原因OkHttp连接池耗尽未配置maxIdleConnections解决修改HttpCollector构造函数显式设置OkHttpClient client new OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .readTimeout(5, TimeUnit.SECONDS) .connectionPool(new ConnectionPool(20, 5, TimeUnit.MINUTES)) // 关键 .build();5. 生产部署与性能调优让Java服务在4C8G服务器上稳定跑3个月不重启5.1 JVM参数调优针对动环场景的GC策略选择机房动环系统特点是✅ 写多读少每秒写入数千条测点数据✅ 对象生命周期短DataPoint对象创建后几秒即被GC❌ 无大对象单条数据1KB无10MB以上缓存因此绝不使用G1 GCG1适合大堆长生命周期对象而选用ZGC低延迟或Parallel GC高吞吐推荐ZGC配置JDK11java -Xms4g -Xmx4g \ -XX:UseZGC \ -XX:ZCollectionInterval5 \ -XX:UnlockExperimentalVMOptions \ -XX:MaxGCPauseMillis10 \ -jar monitor-core.jar-Xms4g -Xmx4g堆大小固定避免动态扩容停顿-XX:ZCollectionInterval5每5秒强制ZGC一次防止内存碎片-XX:MaxGCPauseMillis10ZGC目标停顿10ms满足实时告警要求验证ZGC生效jstat -gc PID 1000 5 # 每秒打印GC统计关注ZGCTime字段 # 正常输出ZGCTime0.2 ZGCCurrent0.0 ZGCTotal12.55分钟总停顿12.5ms注意若用JDK8只能选Parallel GC参数为-XX:UseParallelGC -XX:MaxGCPauseMillis200但停顿会略高。5.2 MySQL优化分区表索引让百万级历史数据查询不卡history_data表按月分区建表语句MySQL 5.7CREATE TABLE history_data ( id bigint NOT NULL AUTO_INCREMENT, device_id varchar(64) NOT NULL, point_id varchar(64) NOT NULL, value double NOT NULL, ts bigint NOT NULL, PRIMARY KEY (id, ts), KEY idx_device_point_ts (device_id,point_id,ts) ) ENGINEInnoDB PARTITION BY RANGE (TO_DAYS(FROM_UNIXTIME(ts/1000))) ( PARTITION p202405 VALUES LESS THAN (TO_DAYS(2024-06-01)), PARTITION p202406 VALUES LESS THAN (TO_DAYS(2024-07-01)), PARTITION p_future VALUES LESS THAN MAXVALUE );关键点主键含ts确保分区裁剪有效WHERE ts BETWEEN x AND y能命中单分区idx_device_point_ts索引覆盖查询SELECT * FROM history_data WHERE device_idups-001 AND point_idbattery_voltage AND ts 1715328000000每月1号自动执行ALTER TABLE history_data REORGANIZE PARTITION ...添加新分区由CleanupJob触发5.3 Redis缓存策略用Sorted Set实现告警队列避免消息丢失AlarmEngine不依赖RocketMQ/Kafka而是用Redis ZSet存告警// key: alarm:queue // member: alarm_id:ups_battery_low:ups-001:1715328000000 // score: trigger_time_ms用于按时间排序 redis.zadd(alarm:queue, System.currentTimeMillis(), alarmId);优势✅ 持久化Redis AOFRDB双保险断电不丢告警✅ 有序zrangebyscore按时间范围取告警zremrangebyrank清理过期告警✅ 去重zadd天然幂等同一告警多次触发只存一次生产配置redis.confappendonly yes # 开启AOF appendfsync everysec # 每秒刷盘平衡性能与安全 save 900 1 # 15分钟内1次修改就持久化 maxmemory 2gb # 限制内存避免OOM maxmemory-policy allkeys-lru # LRU淘汰保留最新告警5.4 日志分级与落盘让ERROR日志真正有用而不是淹没在INFO洪流中logback-spring.xml配置要点!-- 只让ERROR日志写文件INFO/DEBUG只输出到控制台 -- appender nameFILE_ERROR classch.qos.logback.core.rolling.RollingFileAppender filter classch.qos.logback.core.filter.LevelFilter levelERROR/level onMatchACCEPT/onMatch onMismatchDENY/onMismatch /filter filelogs/error.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePatternlogs/error.%d{yyyy-MM-dd}.%i.log/fileNamePattern timeBasedFileNamingAndTriggeringPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedFNATP maxFileSize100MB/maxFileSize /timeBasedFileNamingAndTriggeringPolicy /rollingPolicy /appender !-- collector模块单独日志便于排查采集问题 -- logger namecom.monitor.collector levelDEBUG additivityfalse appender-ref refFILE_COLLECTOR/ /logger血泪经验曾有客户将所有日志级别设为DEBUG3天生成40GB日志磁盘爆满导致服务假死error.log必须独立运维巡检只看此文件INFO日志全在控制台滚动不影响磁盘6. 二次开发与扩展技巧如何安全地接入新设备、新增测点、而不破坏原有告警逻辑6.1 接入新Modbus设备三步完成无需改一行核心代码假设要接入一款新的精密空调型号HITACHI-AC-2000支持Modbus RTUStep 1定义设备驱动配置在src/main/resources/devices/modbus/hitachi-ac-2000.yml中deviceType: hitachi-ac-2000 slaveId: 5 baudRate: 19200 registers: - pointId: room_temp address: 100 type: INT16 scale: 0.1 - pointId: compressor_status address: 105 type: UINT16 scale: 1Step 2注册设备到驱动中心在core/src/main/java/com/monitor/core/DeviceDriverRegistry.java中init()方法末尾加// 自动扫描resources/devices/modbus/下所有YAML Resource[] resources resourceLoader.getResources(classpath:devices/modbus/*.yml); for (Resource r : resources) { Yaml yaml new Yaml(); MapString, Object config yaml.loadAs(r.getInputStream(), Map.class); String deviceType (String) config.get(deviceType); modbusDrivers.put(deviceType, new ModbusDriver(config)); // 已有工厂方法 }Step 3配置采集任务在application.yml中collector: modbus: - deviceType: hitachi-ac-2000 port: /dev/ttyUSB1 devices: - deviceId: ac-001 slaveId: 5 - deviceId: ac-002 slaveId: 6提示scale字段是关键——空调返回温度值为255表示25.5℃scale: 0.1自动乘以0.1DataPoint.value存25.5避免业务层反复除10。6.2 新增测点与告警规则零侵入式扩展运维可自助配置新增一个“空调冷凝水水位”测点并配置告警Step 1在设备配置中加寄存器devices/modbus/hitachi-ac-2000.yml追加- pointId: condensate_level address: 110 type: UINT16 scale: 1 unit: mmStep 2写告警规则alarm-rules.yml追加- id: ac_condensate_overflow expression: condensate_level 80 level: WARNING duration: 120 notify: [sms, email]Step 3重启服务或热加载本源码支持配置热加载修改YAML后发送POST /actuator/refresh需开启Spring Boot Actuator或发送POST /api/v1/config/reload内置端点无需Actuator验证查logs/collector.log确认Loaded 3 registers for hitachi-ac-2000查logs/alarm.log确认Loaded 1 new alarm rule: ac_condensate_overflow6.3 避免踩坑二次开发时最常犯的5个致命错误错误现象根本原因正确做法新增设备后所有Modbus设备采集失败ModbusDriver未加try-catch某个设备通信异常导致整个采集线程中断在ModbusDriver.readRegisters()外层加try-catch(Exception e)记录warn日志并continue保证其他设备正常告警规则修改后不生效AlarmRuleEngine缓存了旧规则未触发reload每次修改alarm-rules.yml后必须调用/api/v1/config/reload或重启服务HTTP采集器报Connection refused但curl通OkHttp连接池复用旧连接目标服务重启后连接失效在HttpCollector中为每个请求加Connection: closeHeader或配置client本文还有配套的精品资源点击获取
返回列表