前阵子给一个中型数据中心做环境监控改造,甲方审计时提了一个让人头疼的要求:机房每个机柜的温度湿度曲线,必须能追溯至少半年,而且要能随时导出报表。市面上大多数温湿度传感器都支持SNMP上报,可一旦网络抖动、交换机重启,云端或监控平台的数据就断了,审计时根本解释不清那段空白。后来我干脆自己做了一台带本地存储的POE温湿度记录仪,把历史数据留在设备内部,再通过SNMP和Web两种方式把曲线和报表拿出来。这篇文章就把整个设计思路、关键代码和踩过的坑完整记录下来,给正在做机房监控或嵌入式网络设备的朋友一个参考。
1. 机房环境监控的真实痛点:为什么既要联网采集又要本地存储?
1.1 审计场景里最怕的一句话:"当时网络卡了一下"
做机房运维的都知道,温度湿度数据平时看起来很好测,一个传感器接上网络、配置好SNMP,监控平台就能画出实时的温度曲线。但真正到了年度审计、故障回溯的时候,问题就全暴露出来了:SNMP走的是UDP,采集平台如果恰好在那几分钟里断连,数据就永远丢了;就算采集平台有重传机制,传感器端没有缓存,重传也拿不到老数据。审计人员看到曲线图中间有一条几小时的空白,想让他相信"那段时间温度其实是正常的",基本等于空口无凭。
所以这次项目里我把设计重心放在了"数据必须落在设备本地"这层上。无论上层平台怎么断连,记录仪自己按照固定周期采样、存盘,网络恢复之后再去补读。这个思路很像行车记录仪——摄像头loop写入SD卡,出事之后拔卡就能还原现场。机房温湿度记录仪也一样,本地存储才是审计追溯的最后一道保险。
1.2 POE供电解决的不只是"少一个电源适配器"
机房机柜里最拥挤的不是网线,而是电源插排。每个传感器都要占一个220V插座,再加上密密麻麻的电源适配器,理线理到头大。POE供电在这时候的优势就非常明显了,一根网线同时搞定数据传输和电源供应,传感器本身不再需要独立的电源适配器,直接从交换机或POE供电模块取电就行。
但这台记录仪对POE的需求比普通IP摄像头更苛刻一点。摄像头断一次电最多是画面黑一阵,温湿度记录仪如果因为供电不稳反复重启,本地存储的数据文件很可能损坏,之前的记录就全废了。所以设计上必须考虑POE的启动电流、分级协商和稳定供电策略,我在第6章会单独展开讲。
1.3 需求边界:这个设备到底替谁干活
把这台记录仪的使用场景拆开看,主要有三类角色在依赖它:
- 机房运维人员:日常用监控平台看实时温度湿度,收到越界告警后快速定位是哪个机柜出问题。
- 审计人员:需要导出某个时间段的温湿度报表,字段要包含时间戳、温度、湿度、告警事件,格式最好是CSV或Excel能直接打开的。
- 设备本身:要在断网、断电、存储异常的情况下仍然尽可能保住数据,至少保证重启之后历史数据不丢。
这三类需求对应到技术实现上,就是三个核心模块:SNMP协议栈用于网络上报与告警、文件化本地存储用于数据持久化、Web页面和报表导出用于审计查询。后面章节就围绕这三块展开。
2. 硬件方案选型:主控、传感器、PoE模块与本地存储的组合
2.1 主控为什么选STM32而不是高集成度的Linux小板
最开始我也考虑过用树莓派Zero W或者类似的Linux开发板,毕竟跑完整SNMP Agent、配一个SQLite数据库都很方便。但仔细掂量之后放弃了,原因有三点:
第一是电源环境。Linux开发板对供电质量更敏感,POE模块输出的一点点纹波都可能导致内核不稳定,而在机房强电环境里这种干扰恰恰不好控制。STM32的电源域设计相对简单,抗干扰能力在同等条件下更好调。
第二是启动时间和异常恢复。Linux板子的文件系统如果突然断电,容易损坏SD卡,轻则需要fsck,重则系统起不来。STM32配合裸机或RTOS,上电几百毫秒就能进入工作状态,没有复杂的文件系统挂载过程。
第三是成本。机房里温湿度传感器一装就是几十个,每个都上Linux板子,单台成本会翻好几倍,甲方预算也扛不住。STM32F407ZET6这颗芯片,有足够的RAM和Flash跑lwIP协议栈,还带硬件CRC和加密单元,性价比高得多。
最终的主控方案是STM32F407ZET6,配合DP83848以太网PHY芯片、PoE PD模块和一颗SPI接口的NOR Flash。整个系统的资源分配是:
| 资源 | 型号/容量 | 用途 |
|---|---|---|
| 主控 | STM32F407ZET6(192KB RAM,512KB Flash) | 跑lwIP协议栈、SNMP Agent、存储管理 |
| 网络PHY | DP83848 | 100M以太网物理层,和PoE模块共用网口 |
| 传感器 | SHT30(I2C接口) | 采集温度和湿度,精度0.3℃/2%RH |
| 本地存储 | W25Q128(16MB SPI NOR Flash) | 存储历史温湿度记录和日志 |
| 供电 | PoE PD模块(IEEE 802.3af Class 2) | 从网线取电,输出5V给主控和传感器 |
2.2 POE供电链路的两种落地方式
POE供电的实现有两条路线,我在设计时都试过:
一条是直接用标准的PoE分离器。这种分离器一头插网线,一头输出5V/9V/12V电源和一个RJ45数据口,设备端完全不需要知道POE的存在。优点是开发快,成本低,缺点是分离器本身作为一个外置模块,故障点多一个,而且在机柜里多了一个小盒子,布线不够利落。
另一条是把POE PD模块直接画到设备板子上。比如用TI的SI3402或者MP8007这类芯片,把网线上的48V电压拉出来,通过DC-DC转换成本地供电电压。这个方案的集成度更高,但需要注意的是POE分级电阻的选择。IEEE 802.3af协议里分0到4级,不同级别对应不同的最大功率,同时交换机会根据分级结果决定是否给这个网口供电。选Class 2还是Class 3,要提前计算整板功耗,留出1.5倍余量,否则强启的时候容易供不上电。
我最终用的是板载PoE模块方案。实测下来整板功耗在2.8W左右,因为带了一颗本地Flash和100M PHY,所以选的是Class 3,PSE端能供到6.49W,余量比较健康。如果要挂外部设备,直接把5V输出引到排针即可。
2.3 传感器选型:别只看精度参数
温度湿度传感器看起来是整套系统里最简单的部分,实际坑不少。选SHT30而不是更便宜的DHT11,原因有几个:
- DHT11的单总线协议对时序要求非常苛刻,在RTOS环境下容易受任务调度影响,偶尔会读到错误数据。
- SHT30是I2C接口,支持CRC校验,每个字节都能验一下数据完整性,对于本地存储来说,坏数据混进去会影响整个报表的可信度。
- SHT30的测量范围是-40℃到125℃,湿度0到100%RH,覆盖机房所有场景;DHT11的高湿区精度太差,机房空调除湿工况下测出来完全不准。
还有一个容易被忽略的细节是传感器的刷新频率。SHT30本身支持每秒测量几次,但记录仪不需要那么高频,我设置的是每10秒读一次实时值,每分钟把当前值落盘一次。采样周期太长,曲线会丢失细小的波动;太短,Flash写入压力大,而且温湿度本身是惯性量,每分钟一个点已经足够反映机柜的温升趋势。
2.4 本地存储介质:NOR Flash、SD卡还是铁电RAM
本地存储介质的选择直接影响使用寿命和数据安全性,我梳理下三种方案的对比:
| 存储介质 | 容量 | 写入寿命 | 掉电安全性 | 成本 |
|---|---|---|---|---|
| SPI NOR Flash | 16MB | 约10万次擦除/扇区 | 高,单字节写入原子性强 | 中 |
| SD卡 | 数GB | 视Flash芯片而定,垃圾回收逻辑不可控 | 低,突然断电易坏文件系统 | 低 |
| FRAM铁电存储器 | 几十KB到几MB | 几乎无限 | 极高 | 高 |
我最终选了SPI NOR Flash,容量16MB,单条记录按照16字节设计,每分钟一条,一天86400秒除以60等于1440条,再乘以16字节大约是23KB一天,一年不到10MB。16MB的Flash哪怕是满打满算,也足够存一年多的数据,配合循环覆盖的策略,完全满足审计半年的要求。
SD卡的容量虽然大,但文件系统的掉电保护是个老大难。裸写扇区不现实,上FATFS的话一旦突然断电,文件分配表很容易坏,恢复起来费劲。NOR Flash配合自研的环形日志结构,反而能控制在简单可靠的范围内。FRAM是个好东西,但容量做不了这么大,用来存系统配置和关键报警状态还差不多,不适合做历史曲线的大容量存储。
3. SNMP v2c代理与OID树设计:让数据能被标准网管系统读到
3.1 MIB文件先行:先把OID设计清楚再写代码
很多嵌入式开发者写SNMP Agent的时候习惯先把代码跑通,再回头补MIB文件,我觉得这个顺序反了。MIB文件不仅仅是给网管软件看的文档,它本身就是SNMP Agent的数据字典。先把OID树设计清楚,后面的代码实现、请求分发、数据结构的组织都会顺很多。
我这台记录仪的MIB设计如下,企业私有号段用1.3.6.1.4.1.xxxxx占位(正式产品需要申请自己的企业号):
SNMPv2-SMI::enterprises.xxxxx |-- systemInfo (1) | |-- deviceName (1) // 设备名称,字符串 | |-- firmwareVersion (2) // 固件版本 | |-- uptime (3) // 开机时间,单位秒 |-- environment (2) | |-- temperatureCurrent (1) // 当前温度,单位0.1℃ | |-- humidityCurrent (2) // 当前湿度,单位0.1%RH | |-- temperatureMax (3) // 当日最高温度 | |-- temperatureMin (4) // 当日最低温度 | |-- humidityMax (5) | |-- humidityMin (6) | |-- alarmStatus (7) // 当前告警状态,bitmap |-- history (3) | |-- historyTable (1) // 历史数据表 | | |-- historyEntry (1) | | |-- index (1) // 行序号 | | |-- timestamp (2) // Unix时间戳 | | |-- temperature (3) // 温度值 | | |-- humidity (4) // 湿度值 | | |-- eventFlag (5) // 事件标志,上电/越界/恢复正常 |-- export (4) | |-- exportRequest (1) // 写1开始导出CSV | |-- exportStatus (2) // 导出状态,0=空闲 1=生成中 2=完成 | |-- exportResult (3) // 导出结果文件长度 | |-- exportData (4) // 分块读取导出数据(OCTET STRING)这个MIB设计的时候有一个关键考虑:历史数据表必须支持GETNEXT和GETBULK操作。如果只支持GET精确读取,网管系统要拿到一年的曲线数据,就得一个OID一个OID地遍历,效率极低。通过把历史数据设计成表格结构,并实现GETNEXT,外部系统可以用GETBULK请求一次拉回大量记录,速度快得多。
3.2 lwIP自带SNMP代理的取舍与改造
STM32上最常用的网络协议栈是lwIP,它自带了一个轻量级的SNMP Agent实现,支持SNMP v1和v2c的GET、SET、GETNEXT、GETBULK这些基本操作。但lwIP自带的Agent有个局限:它的MIB对象是用宏定义静态注册的,数据访问的逻辑和lwIP内部模块耦合在一起。如果只是上报几个系统状态变量还好,要做一张有几千行数据的历史表,直接改lwIP源码里的mib2.c会改到怀疑人生。
我的做法是保留lwIP的UDP协议栈,SNMP Agent的编解码部分自己实现。SNMP v2c的核心格式并不复杂,BER编码(Basic Encoding Rules)跑一遍,PDU的类型就那么几种,对于温湿度记录仪这种小数据量的设备,手写编码器反而灵活。关键数据结构就两个:一个负责把SNMP请求的OID解析成内部的对象定位符,一个负责把内部的数据组装成SNMP响应报文。
后面第4章我会把GETBULK处理历史表的核心逻辑画一下。
3.3 Trap告警:断电、越界、存储异常都要上报
SNMP Trap是一条主动上报的通道,设备一旦发现问题,不等监控平台来轮询,直接发UDP包到配置好的Trap接收端。温湿度记录仪里我设置了四类Trap:
| Trap类型 | 触发条件 | 严重程度 |
|---|---|---|
| coldStart | 设备上电初始化完成 | 通知 |
| temperatureAlarm | 温度超过设定上限或低于下限 | 严重 |
| storageError | Flash写入失败或校验错误 | 严重 |
| memoryAlmostFull | 剩余存储空间不足10% | 警告 |
Trap发送用的是SNMP v2c的Inform格式,比v1的Trap-PDU多了一个请求ID,接收端收到之后理论上要回一个Response包确认。不过在嵌入式设备上,UDP本身不可靠,我们一般只负责把Inform包发出去,收不到确认就重发三次,不做更复杂的可靠传输。真要保证告警不丢,得靠Zabbix这类平台的告警确认机制去兜底。
Trap配置放到设备Web页面里,可以设置最多两个接收端IP和端口号。这个功能看着不起眼,实际使用的时候特别重要,因为机房环境比较恶劣,有时候某个机柜突然升温,平台轮询周期是5分钟,等发现问题已经晚了,Trap可以在秒级把这个信息推给值班人员。
3.4 编码示例:SNMP GET返回温湿度数据的处理路径
下面这段代码展示SNMP Agent收到一个GET请求后,从OID解析到返回数据的基本处理流程。这里用简化的伪代码描述,实际项目中按这个逻辑展开即可:
// SNMP PDU解析入口 void snmp_agent_process(uint8_t *buf, uint16_t len) { snmp_message_t msg; ber_decode_pdu(buf, len, &msg); // 1. BER解码,取出PDU类型和OID列表 if (msg.pdu_type == SNMP_GET) { for (int i = 0; i < msg.oid_count; i++) { oid_entry_t *entry = &msg.oids[i]; snmp_varbind_t result; // 2. 根据OID前缀判断走哪个模块 if (oid_match(entry, PATH_ENV_TEMP_CURRENT)) { result.type = INTEGER; result.value.int_val = sensor_get_temperature(); } else if (oid_match(entry, PATH_ENV_HUMI_CURRENT)) { result.type = INTEGER; result.value.int_val = sensor_get_humidity(); } else if (oid_match(entry, PATH_SYS_UPTIME)) { result.type = TIMETICKS; result.value.int_val = system_get_uptime_ms() / 10; } else { // 3. 找不到OID则返回NO_SUCH_NAME result.type = SNMP_NOSUCHNAME; } snmp_add_varbind_to_response(&msg, entry, &result); } } // 4. 编码响应包并通过UDP发送 uint8_t outbuf[1500]; uint16_t outlen = ber_encode_pdu(outbuf, &msg, SNMP_GET_RESPONSE); udp_sendto(snmp_pcb, outbuf, outlen, &msg.src_ip, msg.src_port); }这里有三个容易出问题的地方:
- OID匹配不能用字符串比较,要按子标识符逐级拆开比对,否则性能会很差。
- 温湿度数值用整数表示,单位是0.1℃和0.1%RH。SNMP的INTEGER没有浮点类型,要么用INTEGER带小数位,要么用OCTET STRING存字符串,我优先用整数,因为Zabbix里做触发器表达式更直观。
- 响应包长度必须严格限制在UDP payload里,超过1500字节的包会被分片,很多监控平台的SNMP模块对分片包处理不好,宁可分多次返回。
4. 历史曲线查询:从SNMP批量读到WEB页面的完整链路
4.1 GETBULK批量读数据的OID组织技巧
历史曲线的数据来源是本地Flash里存的那张循环表。外部系统通过SNMP访问这张表的时候,最核心的操作就是GETBULK(批量读取)。GETBULK的工作方式是一次请求带上一个起点OID和一个最大返回条目数,Agent会从起点OID开始,自动往后遍历若干条记录。
实际操作中有一个技巧:GETBULK的max-repetitions参数不能设置太大,否则一次要返回几十KB数据,UDP包会被分片。我一般控制在100条记录以内。假设历史表每条记录16字节,SNMP响应包头占用60字节左右,100条记录大概1600字节,加上编码开销会超过MTU,所以实际设置50~80条比较稳。
如果监控平台想要获取某一天的数据,它只需要把起点OID设置成这一天的0点0分对应的表格行位置,然后反复GETBULK,直到拿到当日最后一行的数据。这里的行位置和时间的关系由Agent内部维护,外部系统不需要知道行号,直接遍历即可。
4.2 设备内置Web页面的历史曲线实现
即便有了SNMP,运维人员还是需要一个更直观的看图入口。监控平台画出的历史曲线通常是以天或周为单位的聚合,想要看具体某个机柜某一次波动的原始细节,还是直接打开设备内置的Web页面最方便。
Web页面基于lwIP自带的HTTP服务器改造,前端只用了最简单的Canvas画图,没有引第三方图表库。页面结构很简单:
- 首页显示当前温度、湿度、设备状态和最近一个小时的曲线。
- 历史查询页面可以选起始时间和结束时间,查询结果以折线图展示。
- 导出页面提供两个按钮,一个是导出CSV文件,一个是导出PDF报表(PDF报告这块,嵌入式设备上做重页面容易卡,简单场景用CSV足够了,PDF方案是在上位机完成,后面再讲)。
Web页面的历史曲线请求,本质上是向设备发一个HTTP GET请求,带start_timestamp和end_timestamp参数,设备在本地Flash里检索对应的数据段,按JSON格式返回。前端拿到JSON数组后渲染Canvas。由于一年十万多个点全部画在Canvas里会导致浏览器卡死,所以Web页面只展示采样间隔在5分钟以上的聚合曲线,要看原始明细就下载CSV。
4.3 时间戳精度与查询窗口划分
历史查询最关键的是时间对齐。嵌入式设备没有RTC电池备份的话,断电重启后时间会回到编译固件的默认时间,历史数据的时间戳就会错乱。所以我的设计里加入了一个辅助机制:
- 设备正常运行时,通过NTP协议和局域网时间服务器同步时钟,每小时校准一次。
- 如果设备曾经断电,RTC使用后备电池维持计时,NTP恢复后重新校准。
- 每条历史记录的timestamp字段都存Unix时间戳,所有查询和报表导出都基于这个绝对时间值,不依赖设备当前的系统时间。
这个设计在审计追溯时非常重要。审计人员要求的是"某个真实时刻的温湿度数据",而不是"设备运行了多久之后的第N个采样点",时间戳必须可靠可验证。如果设备时间发生过跳变,导出的报表里也要能看出来,所以事件标志字段里专门有一个"timeAdjust"标志位,记录时间被NTP校准的时刻。
5. 审计报表导出的两条路线:直出CSV与接入现有监控平台
5.1 设备直出CSV报表的字段设计
审计报表最刚性的需求是CSV,因为它能被Excel直接打开、可以筛选、可以做透视。导出的字段我经过和甲方审计人员几轮沟通,最终定成下面这个格式:
| 字段 | 示例值 | 说明 |
|---|---|---|
| timestamp | 2024-06-15 14:30:00 | 本地时间,格式固定为YYYY-MM-DD HH:MM:SS |
| temperature | 23.4 | 数值,单位℃ |
| humidity | 45.2 | 数值,单位%RH |
| event_flag | 0 | 0=正常 1=上电 2=温度越界 3=湿度越界 4=恢复 5=时间校准 |
| device_name | rack-A12 | 设备名称,方便多台设备数据合并时区分来源 |
字段定这么细是有原因的。机房审计不能只看温度湿度两头,还要看是否有过停电、是否有过告警、是否发生过时间跳变。event_flag把关键事件融进同一条数据流里,审计人员筛一下"event_flag != 0"的行,就能快速找到所有异常节点。
导出CSV的方式有两种:一种是通过Web页面的导出按钮直接下载;另一种是通过SNMP的export模块触发设备在本地生成CSV文件,然后外部系统用FTP或HTTP的方式把文件拉走。如果只是偶尔导一次,Web页面下载就够了;如果要做每周例行审计归档,建议用后一种自动化方案,脚本定时触发导出并归档到审计服务器。
5.2 通过SNMP拉取数据喂给现有监控平台
很多机房在用的监控平台是Zabbix,它天生支持SNMP协议,但默认的模板对私有MIB支持得不好。要让Zabbix直接读取这台设备的温湿度数据,需要把MIB文件编译进Zabbix模板,创建对应的监控项和图形。
如果是历史报表需求,更通用的做法是用Python脚本通过SNMP把设备数据拉出来,写入InfluxDB并用Grafana做可视化。下面这个简化的脚本演示了用pysnmp读取历史表的逻辑:
from pysnmp.hlapi import * def get_history(host, start_oid, max_records=100): results = [] error_indication, error_status, error_index, var_binds = next( get_bulk_cmd( SnmpEngine(), CommunityData('public', mpModel=1), UdpTransportTarget((host, 161), timeout=2, retries=2), ContextData(), 0, # non-repeaters max_records, # max-repetitions ObjectType(ObjectIdentity(start_oid)), lookupMib=False ) ) for oid, value in var_binds: results.append((str(oid), str(value))) return resultspysnmp的GETBULK返回顺序是按OID字典序排列的,所以外部脚本直接循环将返回的OID和时间戳字段映射即可。对于审计报表,我建议在拉取完成后,把数据转存到PostgreSQL或MySQL里,方便之后按时间窗口做二次聚合。
5.3 报表合并与机房审计周期
单台设备导出的CSV是孤立的,审计通常需要把整层机房或整个数据中心的所有记录仪数据合并成一张总表。这一步我建议做个简单的ETL流程:每台设备一个固定IP,脚本按设备清单逐个拉取CSV,合并成一个按照时间戳排序的大表。
合并的时候有几个注意事项:
- 所有设备的时钟必须先对齐,NTP服务器用同一个。
- 报表文件名带上设备序列号和日期范围,防止同名文件互相覆盖。
- 如果在审计周期内有设备掉线几个小时,掉线期间的数据在报表里不能凭空消失,要么留空行,要么在备注列里标注"设备离线无记录"。诚实的缺测标注比用插值补出来的假数据更符合审计要求。
之前遇到过一个甲方的审计人员,要求报表必须能追溯到"每一次空调启动和停机"对机柜温度的影响。这种分析光靠温湿度数据还不够,还需要空调的功耗数据。所以我又在设备上加了一路485接口,接空调的RS485通讯线,把空调的运行状态和设定温度也一并记录到本地Flash里。这样导出的报表就能同时看到空调动作和温度波动的关系,审计说服力直接上一个档次。
6. 实测踩坑记录:PoE启动电流、SNMP超时与Flash磨损
6.1 PoE分级与启动瞬间的电压跌落
第一次板子打样回来后,上电测试就出了问题:记录仪插到POE交换机上,设备指示灯闪了一下就灭了,反复试了几次都这样。用示波器抓POE模块的输出电压,发现每次上电会有一个将近200ms的电压跌落,直接从5V掉到3V以下,STM32早就复位了。
这个问题的根源太典型了:POE分级电阻选得偏小,PSE上电阶段按Class 2的功率上限供电,5V输出的驱动能力不足以同时扛过主控启动、Flash初始化和以太网PHY的link-up过程。PHY的link-up瞬间会有一个瞬间电流尖峰,整板功耗瞬时冲到5W以上,超出Class 2的6.49W上限其实不远了,但POE供电链路本身的纹波和压降把余量吃掉了。
解决办法有两个方向:第一个是改分级电阻到Class 3,让PSE承诺更大的功率上限;第二个是加缓启动电路,在5V输出上串联一个小一点的电感或者用电子开关限流,避免PHY和主控同一个瞬间同时满负荷。我两个都做了,效果非常明显,之后再没复现过启动掉电的问题。
这里提醒一下:POE交换机的供电能力不是无限的,如果整柜设备都是Class 3,还要核算交换机的总PoE功率预算。一台24口的POE交换机按30W每口算,不是所有口都能满载的,实际部署时要把设备的实际功耗列个表,避免过载。
6.2 SNMP请求超时与重试策略
SNMP协议本身是基于UDP的,请求超时后重复是家常便饭。但不同监控平台的超时设置差异很大,Zabbix默认的超时是3秒,如果设备响应慢一点,就会被标记为不可达。实测发现,设备在Flash擦除期间处理SNMP请求时,响应延迟会从几毫秒飙升到几百毫秒,因为Flash擦除操作会阻塞主控的总线访问。
为了解决这个问题,我把Flash擦除操作从主循环里拆出去,放到一个低优先级的后台任务里执行,每次擦除一个扇区就让出CPU。同时,SNMP Agent模块里的数据读取全部走缓存,实时温度湿度变量是每次采集后直接更新到内存中的,SNMP请求查询的时候不需要访问Flash,只从内存里取,响应速度就稳定了。
如果按照这个思路设计,建议在设备文档里明确标注:SNMP读操作超时建议设置为5秒,重试次数建议2次。超时设太短会频繁误报设备离线,设太长又会影响平台对告警的响应速度。5秒是一个比较平衡的值。
6.3 Flash存储寿命估算与磨损均衡
16MB的NOR Flash,按每分钟一条记录,一年也才写不到10MB数据,看起来毫无压力。但这里要算一笔更细的账:NOR Flash是按扇区擦除的,W25Q128每个扇区4KB,每次擦除的寿命是10万次左右。如果不做磨损均衡,一直往同一个扇区写,那这个扇区很快就会被写穿。
我的环形存储实现是:整个Flash分区划分为N个扇区,维护一个写指针和一个擦除指针,写满一个扇区就顺移到下一个,待写入扇区如果之前有数据,先擦除再写。通过这种循环写入的方式,16MB的Flash拆成4096个4KB扇区,理论上可以保证每个扇区的擦写次数相对均匀。按每天1440条记录、每条16字节计算,每分钟数据量23KB,大概每秒写完一个4KB扇区需要3.5分钟,一年下来累计擦写次数是(365天×1440分钟)/(每扇区可写4KB/16B=256条)≈2053次,距离10万次的寿命上限还远得很。
即便这样,我还是在存储模块里加了一个"坏扇区跳过"机制。如果在写入过程中发现某次擦除或写入失败,设备会把这个扇区标记为异常,自动迁移到下一个扇区,并上报一条storageError Trap。这个机制在项目部署后期很管用,因为机房环境温度高,Flash在高温下的数据保持能力会下降,提前发现问题总比审计的时候拿不出数据强。
6.4 部署细节:探头位置比什么都重要
最后一个坑不是电子的,是物理部署的。有次现场调试,一台记录仪装在机柜顶部,温度曲线在白天经常出现异常尖峰,查来查去发现是探头正好对着空调出风口,空调启动瞬间温度被吹低了三四度,不是真实的环境温度。
正确做法是传感器探头装在机柜前门或后门的回风口位置,远离空调出风直吹,同时避开服务器风扇排风的强对流区域。在机柜上下三层各装一台设备是更专业的方案,因为机房里的温度分层现象严重,只测一个点位很容易出现"顶部28℃、底部22℃"这种偏差,审计报表里如果只有单点数据,说服力会大打折扣。
另外,传感器探头不要贴在金属机柜表面。机柜的铁皮是热的良导体,贴上去会测得比实际环境温度偏高。用双面泡棉胶垫高1厘米左右,让探头悬空,测出来的才是真正的空气温度。这些小细节看似不起眼,在审计的时候往往就是决定报表能不能过关的关键。
写在最后的一点经验
这项目做完之后的感受是:温湿度记录仪这种东西,单看每个功能点,SNMP、POE、Flash存储、CSV导出,都不是什么新技术,但把它们组合在一起,再加一个"审计级可靠性"的要求,难度就上来了。尤其是本地存储和SNMP的结合,既要在嵌入式资源里跑通协议栈,又要保证数据落盘的完整性和可追溯性,这两个需求本身是有冲突的。我的建议是,如果你也在做类似设备,先把审计需求问清楚,到底要保留多久的数据、允许多大的缺测率、报表要包含什么字段,这些确定之后再定硬件方案和存储架构,不然等设备量产了才发现存储容量不够或者导出格式不对,改动成本会非常高。
如果后续有时间,我打算给设备加一个基于Web的远程固件升级功能,这样部署在几十个机柜里的设备就不用一个个捅串口线升级了。固件升级过程中对历史数据的分区保护也要设计好,毕竟设备可以重启几百次,历史数据却不能丢一次。