IPMI SDR 和坑爹的 BMC 电源读数一例

 

最近组里新购入了一些杂牌 GPU 服务器,为了监控功耗,我部署了 Prometheus 社区的 ipmi_exporter 从 BMC 读取带外数据,并使用 Grafana 制作了 Dashboard。然而很快我就发现了奇怪的现象:

Strange power curve from IPMI

从图中可以看到,17:30 左右我向 GPU 施加了一些负载,使其温度上升到了 70 度以上。然而,整机的功耗曲线就像一潭死水,完全没有可见的变化。在 18:08 左右我又重启了 exporter,此时功耗读数有了一些变化,然而还是在此后的时间中维持不变。这是我用了这么多年的 IPMI,第一次遇到如此诡异的事情。

问题复现

我首先怀疑是 BMC 本身有故障,但从网页上能获得正确的读数和曲线(尽管采样率比较低)。于是我又怀疑是 IPMI SDR 有问题,但我用 ipmitool 能正确读取实际的功率数字。最后怀疑对象就来到了 ipmi_exporter 上,它使用的是 FreeIPMI 工具中的 ipmi-sensors 来读取数据。果然,用它就能复现问题:

root@foo# ipmi-sensors -D LAN_2_0 -h ipmi_host -u root -p FOO -r 105
Caching SDR repository information: /root/.freeipmi/sdr-cache/sdr-cache-foo.ipmi_host
Caching SDR record 157 of 157 (current record ID 352)
ID  | Name        | Type                     | Reading    | Units | Event
105 | Total_Power | Other Units Based Sensor | 670.00     | W     | 'OK'

(启动重型 GPU 负载并等待几秒钟)

root@foo# ipmitool -I lanplus -H ipmi_host -U root -P FOO sdr get 'Total_Power'
Sensor ID              : Total_Power (0x69)
 Entity ID             : 21.0 (Power Management)
 Sensor Type (Threshold)  : Other (0x0b)
 Sensor Reading        : 4500 (+/- 0) Watts
 Status                : ok

root@foo# ipmi-sensors -D LAN_2_0 -h ipmi_host -u root -p FOO -r 105
ID  | Name        | Type                     | Reading    | Units | Event
105 | Total_Power | Other Units Based Sensor | 670.00     | W     | 'OK'

root@foo# ipmi-sensors -D LAN_2_0 -h ipmi_host -u root -p FOO -r 105
ID  | Name        | Type                     | Reading    | Units | Event
105 | Total_Power | Other Units Based Sensor | 667.32     | W     | 'OK'

我拿着这个现象去问了 Claude,它提醒我这很可能和 IPMI SDR 的读取方式有问题,于是我就趁机进行了一些学习。

IPMI SDR 格式与读取命令

SDR (Sensor Data Record) 是 IPMI 1.5 标准中定义的用于描述传感器信息的数据结构,BMC 会将这些记录存储在一个 SDR Repository 中。每个记录都有一个唯一的 ID,可以通过 IPMI 命令来读取。

可以用如下的命令读取 SDR Repository:

root@foo# ipmitool -I lanplus -H ipmi_host -U root -P FOO sdr info
SDR Version                         : 0x51
Record Count                        : 157
Free Space                          : unspecified
Most recent Addition                : NA
Most recent Erase                   : NA
SDR overflow                        : no
SDR Repository Update Support       : unspecified
Delete SDR supported                : no
Partial Add SDR supported           : no
Reserve SDR repository supported    : no
SDR Repository Alloc info supported : no

这其实等价于直接发送 Get SDR Repository Info 命令:

root@foo# ipmitool -I lanplus -H ipmi_host -U root -P FOO raw 0x0a 0x20
 51 f5 00 58 13 ff ff ff ff ff ff ff ff 42

可以看到 SDR Repository 中有 157 条记录。而从上面可以看到 Total_Power 的编号是 0x69 (105)。使用 Get SDR 命令可以读取:

root@foo# ipmitool -I lanplus -H ipmi_host -U root -P FOO raw 0x0a 0x23 0x00 0x00 0x69 0x00 0x00 0xFF
 6a 00 69 00 51 01 3b 20 00 69 15 00 00 00 0b 01
 00 00 00 00 00 00 00 06 00 00 0d 40 00 00 00 e0
 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
 00 0b 54 6f 74 61 6c 5f 50 6f 77 65 72 00 00 00
 00 00

或者使用 ipmitool sdr dump,也可以获得完整的 SDR 记录。其中与上面对应的是(截掉了一些末尾的 0):

00001a40: 6900 5101 3b20 0069 1500 0000 0b01 0000  i.Q.; .i........
00001a50: 0000 0000 0006 0000 0d40 0000 00e0 0000  .........@......
00001a60: 0000 0000 0000 0000 0000 0000 0000 000b  ................
00001a70: 546f 7461 6c5f 506f 7765 7200 0000 0000  Total_Power.....

其中第一个字节 0x69 对应了 ID,第四个字节 0x01 说明这是 Full Sensor Record。各种字段的详细定义在此略去,也可参见 OpenIPMC 提供的头文件。这条记录中的重要字段包括:

偏移 长度 字段名 说明  
0x00 2 0x69 0x00 Record ID    
0x02 1 0x51 SDR Version    
0x03 1 0x01 Record Type    
0x04 1 0x3b Record Length 59  
0x15 1 0x06 Base Unit Watt  
0x17 1 0x00 Linearization 0 Linear
0x18 1 0x0d M[7:0] 系数  
0x19 1 0x40 {M[9:8], Tolerance}    
0x1a 1 0x00 B[7:0] 系数  
0x1b 1 0x00 {B[9:8], Accuracy LSB}    
0x1c 1 0x00 Accuracy    
0x1d 1 0xe0 {Rexp[3:0], Bexp[3:0]}    

也就是说,这个 SDR 定义了一个带线性系数的传感器。当读出数字是 raw,其真实值为:

y = (M * raw + B * 10^Bexp) × 10^Rexp,其中:
M = (0b01 << 8) | 0x0D = 256 + 13 = 269, B = 0
Rexp = (uint4_t) 0b1110 = -2, Bexp = 0

代入后,可知当前真实值是读出值的 2.69 倍。而是用以下命令可以读取传感器原始值:

root@foo# ipmitool -I lanplus -H ipmi_host -U root -P FOO raw 0x04 0x2d 0x69
 f9 c0 00 00

因此当前的功率是 0xf9 * 2.69 = 670W。与 ipmi-sensors 读出的值基本一致。

我多次在不同负载下进行读取测试,发现传感器的原始值几乎不发生变化,总是 250 左右,而 SDR 中的系数却总是在变化。我又让 Claude 给我 vibe 了一个脚本同时检测 SDR 记录和传感器原始值,记录如下:

Timestamp M R_exp B B_exp Raw Converted
20:40:11 291 -2 0 0 249 724.59
20:40:17 27 -1 0 0 249 672.30
20:40:22 267 -2 0 0 250 667.50
20:40:28 268 -2 0 0 250 670.00
20:40:34 265 -2 0 0 249 659.85
20:40:41 413 -2 0 0 249 1028.37
20:40:46 419 -2 0 0 249 1043.31
20:40:52 267 -2 0 0 250 667.50
20:40:58 268 -2 0 0 250 670.00

到这里,答案基本上已经水落石出。

问题根因

观察 ipmi-sensors 的代码(或者帮助文档),甚至是直接运行一下,都可以发现它维护了一个 SDR 的 cache(还记得上面的 Caching SDR repository information 吗?)。这个 cache 只在 SDR repository 的时间戳发生变化时,才会被工具认为失效并重新获取。

不巧的是,这台服务器的 BMC 选择了修改 SDR 记录中的线性系数(M、Rexp 等)来达成修改最终读数的目的,而不是直接修改传感器的原始值(这真的对吗?)。这样一来,ipmi-sensors 就一直在使用第一次缓存的系数与读出的原始值进行计算,导致最终结果几乎不变。而 ipmitool 则每次都重新读取 SDR 与原始值,得到的结果自然是正确的。

解决方案

由于 ipmi_exporter 和 FreeIPMI 耦合比较紧,而 ipmi-sensors 命令也没有选项允许跳过或者禁用 SDR cache,于是我对 exporter 的代码进行了一些 dirty fix:

--- a/collector.go
+++ b/collector.go
@@ -114,7 +114,8 @@ func (c metaCollector) Collect(ch chan<- prometheus.Metric) {
                        }
                        args := collector.Args()
                        cfg := config.GetFreeipmiConfig()
-
+                       // remove all cache
+                       freeipmi.Execute(fqcmd, []string{"--flush-cache"}, cfg, target.host, logger)
                        result = freeipmi.Execute(fqcmd, args, cfg, target.host, logger)
                }

考虑到功率读取的频率不高(每 15s 一次),这么做虽然简单粗暴,但也不至于有太大的影响。这样一来,我的 Grafana 上终于有了正确的功率曲线。