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

资讯详情

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

KeyarchOS下hd-idle硬盘休眠适配与systemd服务化实践

KeyarchOS下hd-idle硬盘休眠适配与systemd服务化实践 前阵子接了一个存储节点的优化需求节点上挂了一批 SATA 盘大部分时间处于低频读写状态但一直保持通电空转既费电也影响盘体寿命。项目组希望在不影响业务的前提下让空闲硬盘自动进入待机状态。我评估了几套方案之后最终选定 hd-idle-1.05 做适配目标平台是 KeyarchOS。这篇文章把整个适配过程、踩过的坑、以及最后沉淀下来的一套可直接复用的 systemd 服务化配置完整记录下来给后面要在国产服务器操作系统上做类似开源工具适配的同学做个参考。1. 为什么选 hd-idle需求判断与工具对比1.1 项目背景与需求拆解这个节点不是典型的数据库或虚拟化场景而是一台偏冷存储的归档服务器特点是容量大、并发低、读多写少。业务方给的要求很简单硬盘在没有 IO 超过一定时间后自动进入 standby来的时候能正常唤醒不能影响数据完整性。听起来就是做一个空闲盘断电策略但真正落地的时候需要考虑三个层面内核层用什么机制控制、用户态用什么工具做策略决策、以及这些策略在国产操作系统上能不能被 systemd 正常托管。在信创适配的大背景下这类工作往往不只是“装个软件”那么简单。KeyarchOS 这类服务器操作系统在硬件兼容和内核特性上有自己的构建版本第三方工具如果走源码编译依赖的 glibc 版本、内核头文件、设备节点策略都可能和原生的 CentOS 环境有细微差别。hd-idle 属于典型的老牌开源小工具代码体量小、依赖少、没有复杂的图形栈非常适合作为适配对象也很适合用来跑通一整套“源码适配 打包集成 服务化托管”的流程。1.2 hd-idle 与其他工具的区别很多人第一反应是 hdparm毕竟hdparm -S可以直接设置硬盘的休眠超时。但这里有一个容易忽视的差异hdparm 的-S参数是设置磁盘固件内部的 standby timer由磁盘自己计数超时后自行进入待机而 hd-idle 是在用户态守护进程里监控每个磁盘的真实 IO 活动超过空闲阈值后主动向磁盘发送 standby 命令。打个比方hdparm 相当于给硬盘设了个固定闹钟时间一到就断电hd-idle 则像是一个管家随时盯着硬盘有没有人访问看完没人来才决定拉闸。这带来的优势很实际hd-idle 可以按磁盘名称甚至型号做差异化策略比如-a sda -i 0让系统盘永远不休眠而-a sdb -i 3600让数据盘空闲一小时后再休眠hdparm 的-S虽然也能按盘设置但它是内核直接透传给固件的参数如果你需要在休眠前做一些外部动作或者想跳过某类磁盘hdparm 就不够灵活了。另外 hd-idle 会把监控决策逻辑放在用户态排错时直接看日志和/proc/diskstats比黑盒的固件定时器直观得多。1.3 版本选择的考量hd-idle 目前比较通用的版本就是 1.05后面带的-4一般是打包版本号或者发行版修订号。我没有选择追最新 git 代码而是直接锁定稳定版原因有三第一这类工具核心功能多年没大变稳定版经过大量生产环境验证第二1.05 源码只有几个文件编译依赖极简适配过程中可控性强第三在 KeyarchOS 这类 RHEL 兼容生态上老版本源码通常比激进的新版本更容易通过编译和打包工具链。2. 适配前的环境勘察与源码解构2.1 系统环境确认拿到一台测试机器后我没有急着解压源码而是先把系统身份和工具链摸清楚。因为有的时候适配不成功不是代码问题而是编译环境和打包工具缺失导致的。按以下顺序做了确认cat /etc/os-release uname -a gcc --version make --version rpmbuild --version dnf --version我这边实际环境是 KeyarchOS 5.3 版本内核 5.14 左右gcc 11make 4.4rpmbuild 存在。这里有个经验如果rpmbuild不存在后续想走 RPM 打包就得先dnf install rpm-build。另外uname -a看到的内核版本决定了我们能不能直接用发行版内核头文件做模块类适配但 hd-idle 是用户态程序不依赖内核头文件所以这一项只是做一个基础记录并不影响编译。检查项本机信息说明操作系统KeyarchOS 5.3基于 RHEL 生态使用 dnf/yum内核5.14用户态工具适配不依赖内核源码编译器gcc 11对 C99 代码兼容性较好包管理dnf支持 rpmbuild 本地打包架构x86_64本次适配未涉及交叉编译2.2 源码包结构分析hd-idle-1.05 的源码包解压之后核心文件其实很精简主要就集中在hd-idle.c、几个平台相关的 c 文件、configure 脚本和 Makefile 模板。这种结构对适配非常友好意味着我们不需要像适配大型项目那样去梳理几十个模块的依赖关系。我习惯先做一件事不看代码先看README和configure --help。README 会写明编译选项和运行方式configure --help会列出所有可定制项比如是否生成 init 脚本、安装路径前缀等。hdd-idle 的 configure 支持--with-init-scriptsredhat或debian这个选项在传统 sysvinit 时代很有用但在 systemd 时代往往需要我们自己改造。另外建议看下源码里main函数的启动流程。hd-idle 启动后会先解析配置文件/etc/hd-idle.conf如果没有配置文件则使用命令行参数和默认值。也就是说最终给到 systemd 的 ExecStart 参数和配置文件里的参数是等价关系理解这一点后面做服务化配置就不会被“参数是写死在 unit 里还是写在 conf 里”这个问题困住。2.3 核心工作机制解读hd-idle 的核心逻辑并不复杂但它的监控方式很多人第一次接触时会有误解。它不是通过读硬盘的温度或 SMART 信息来判断而是定期读取内核暴露的/proc/diskstats统计每个块设备上的读写扇区数或 IO 次数是否发生了变化。如果在设定的空闲窗口内某个磁盘的统计数字一直没有增长就认为这个盘是空闲的随后通过通用 SCSI 接口发送 ATA standby 命令让盘体进入待机。这里涉及到两个内核机制/proc/diskstats是内核块设备层的统计输出用户态可直接读取SG_IO 是 SCSI generic 接口通过/dev/sg*设备节点往磁盘发送控制命令。在适配过程中真正容易出问题的不在编译而在运行系统里必须存在对应的 sg 设备节点且启动服务的用户要有访问权限。这个坑后面会专门讲。3. 编译安装与打包集成3.1 configure 与编译命令hd-idle-1.05 的编译过程相当传统三段式走下来基本不会出大问题。我用的是以下这套组合./configure --prefix/usr --with-init-scriptsredhat make--prefix/usr是为了让二进制安装到/usr/sbin下符合大多数服务器发行版对系统管理命令的路径预期。--with-init-scriptsredhat会生成红帽系的 sysvinit 启动脚本虽然我们最后不一定直接用但保留它可以作为参考也可以在那份脚本的基础上反推服务运行参数。如果你希望把日志输出独立出来可以在编译前给 CFLAGS 加上一些预定义宏但我实测下来默认行为已经够用。真正需要注意的反而是不要加-O2之外的激进优化hd-idle 这种定时轮询工具对性能不敏感没必要为了一点点效率提升引入编译器改变代码语义的风险。3.2 编译期问题处理hd-idle 的代码是早年 C 风格在现代 gcc 11 下编译最常见的不是错误而是警告。比如implicit declaration of function这类通常是老代码在 C89 模式下漏声明了某些标准库函数而 gcc 默认新的标准后把警告提升得比较明确。我这边实际编译时没有遇到致命错误但如果你的环境报了类似问题可以先用make clean清掉中间产物然后手动指定 CFLAGS 再编一次make clean make CFLAGS-O2 -stdgnu99 -Wall-stdgnu99是这里的关键它让编译器采用 GNU99 标准兼容老代码里常见的隐式声明和for循环内声明变量的写法。还有一个常见的报错是缺少sg头文件也就是linux/scsi/sg.h找不到。这大概率是真的缺头文件包RHEL 系发行版上执行一下dnf install -y kernel-headers glibc-headers再重新编译一般就能解决。3.3 安装路径规划make install默认会把二进制、man 手册、init 脚本分别放到对应目录但我不建议直接无脑安装。适配过程中需要明确一件事最终这个二进制是要被 systemd 托管的安装路径最好固定且可预期。我这边选择手动拷贝避免 init 脚本和 systemd unit 混用造成启动方式冲突make install DESTDIR/tmp/hd-idle-build cp /tmp/hd-idle-build/usr/sbin/hd-idle /usr/sbin/hd-idle mkdir -p /etc/hd-idle cp /tmp/hd-idle-build/etc/hd-idle.conf /etc/hd-idle/hd-idle.conf这里的DESTDIR技巧在做 RPM 打包时很常用它可以将安装内容先落到一个临时目录方便后面统一收集文件列表。直接拷贝二进制到/usr/sbin是系统管理工具的常规位置符合 FHS 规范也不容易和现有命令冲突。3.4 制作 RPM 包纳入仓库如果只是单机使用手动拷贝就够了。但如果公司内部有几十台节点需要统一部署我强烈建议把适配结果打成 RPM 包纳入本地 yum/dnf 仓库统一管理这样版本可控、卸载干净、后续批量升级也方便。用 rpmbuild 打包时我的 spec 文件核心部分长这样Name: hd-idle Version: 1.05 Release: 4 Summary: Hard disk idle spin-down daemon for KeyarchOS License: GPLv2 Source0: hd-idle-1.05.tar.gz %description hd-idle is a utility for spinning down idle disks. This package is adapted for KeyarchOS with systemd support. %prep %setup -q %build ./configure --prefix/usr --with-init-scriptsredhat make CFLAGS-O2 -stdgnu99 -Wall %install make install DESTDIR%{buildroot} rm -rf %{buildroot}%{_sysconfdir}/init.d %files /usr/sbin/hd-idle %config(noreplace) /etc/hd-idle/hd-idle.conf /usr/share/man/man8/hd-idle.8.gz打包时我把源码里自带的 init.d 脚本删了因为服务化我们统一走 systemd避免系统里出现两套启动脚本导致服务管理混乱。%config(noreplace)这行很关键它保证后续升级 RPM 时不会覆盖管理员已经改过的配置文件这是生产环境运维最基础的要求之一。打包命令rpmbuild -ba hd-idle.spec构建完成后把生成的 RPM 放到本地仓库目录更新仓库元数据就能在目标节点上直接用dnf install hd-idle-1.05-4.el9.x86_64.rpm完成部署了。4. systemd 服务化改造4.1 为什么不用源码自带的 init 脚本hd-idle 源码通过--with-init-scriptsredhat生成的是传统的 sysvinit 脚本核心逻辑是后台启动进程、写 PID 文件、提供 start/stop/status 等命令。这套机制在 CentOS 6 时代够用但在 KeyarchOS 这种默认 systemd 的系统上直接调用 sysvinit 脚本不是不行而是会有几个实际问题进程退出管理不可控、日志散落在不同地方、开机启动顺序和依赖难以精确表达。具体到 hd-idle 这个程序它默认会 fork 到后台运行。sysvinit 脚本里通过start-stop-daemon或者直接hd-idle 拉起都很正常但 systemd 对“fork 型”服务有一套自己的期望逻辑如果进程 fork 的时机和 systemd 的等待逻辑不匹配就会出现服务状态看起来是 active主进程却已经死掉的情况。所以我最终选择用-d参数让 hd-idle 在前台运行把这个守护进程的托管权完全交给 systemd。4.2 编写可用的 systemd unit直接贴一下我最终落地的单位文件位置是/etc/systemd/system/hd-idle.service[Unit] DescriptionHD Idle Daemon for KeyarchOS Documentationman:hd-idle(8) Afterlocal-fs.target Aftermulti-user.target [Service] Typesimple ExecStart/usr/sbin/hd-idle -d -i 3600 -l /var/log/hd-idle.log ExecReload/bin/kill -HUP $MAINPID Restarton-failure RestartSec10 # 安全加固 NoNewPrivilegestrue ProtectSystemfull PrivateTmptrue [Install] WantedBymulti-user.target为什么用Typesimple而不是Typeforking因为前面提到我们用了-d参数让进程留在前台systemd 不需要等待 fork 行为直接跟踪主进程即可。-i 3600表示默认空闲 3600 秒也就是一个小时没有 IO 就进入 standby。-l /var/log/hd-idle.log将日志写到独立文件方便后续排查也可以去掉这行让日志全部进 journald看个人习惯。ExecReload里用kill -HUP让守护进程重新加载配置这样后续调整空闲时间不用重启服务执行systemctl reload hd-idle即可。4.3 开机自启与依赖顺序写好了 unit 文件需要先让 systemd 重新加载配置再设置开机自启systemctl daemon-reload systemctl enable hd-idle systemctl start hd-idle systemctl status hd-idle依赖顺序方面Afterlocal-fs.target保证本地文件系统全部挂载完成后再启动服务避免出现设备节点还没准备好就由守护进程先跑起来的情况。如果你的数据盘来自远程 SAN 或者网络挂载还需要把网络相关 target 也加进去比如Afternetwork-online.target保证盘确实可用之后才开始计时。5. 参数配置与运行验证5.1 配置文件语法与关键参数hd-idle 支持通过/etc/hd-idle/hd-idle.conf配置文件把启动参数固化下来。配置文件的语法很有意思它不是常见的 keyvalue 格式而是直接写命令行参数每行一部分。比如我这边的配置文件长这样# 默认空闲一小时 -i 3600 # 系统盘 sda 永不自动休眠 -a sda -i 0 # 数据盘 sdb 半小时无 IO 进入 standby -a sdb -i 1800 # 日志输出 -l /var/log/hd-idle.log这可能是 hd-idle 被吐槽最多的一个设计了因为初次接触很容易认为-i 0是空闲 0 秒理解成“立刻休眠”。实际上-i 0在 hd-idle 语义里是“禁用自动休眠”它代表这个匹配项不启用 standby。之前有同事就是没搞懂这个语义把系统盘配置成了-i 0结果发现系统盘一直在休眠差点酿成事故。需要关注的参数主要有这么几个-i 秒默认空闲超时时间0 表示禁用自动休眠-a 盘名按磁盘名称匹配特定盘可以反复指定多条规则-l 路径日志文件路径-d前台调试模式配合 systemd 使用非常关键-p 秒轮询周期默认值已经够用一般不需要调5.2 启动服务与日志观察服务启动后第一步是确认进程还活着第二步是看一眼日志有没有异常。hd-idle 的日志里会记录它监控到的磁盘活动信息、判定空闲后下发的 standby 命令以及系统启动时的参数解析情况。用journalctl -u hd-idle也可以看到标准输出但因为我指定了独立日志文件核心信息都在/var/log/hd-idle.log里。这里有一个经验你必须知道hd-idle 在刚开始启动的一段时间内是不会对任何磁盘做休眠操作的。原因很简单它需要先观察一轮完整的 IO 活动模式避免在启动刚完成、系统还在做初始化读写的时候就把盘拉下来。所以如果你的日志比较安静不要急着认为服务没起来先等一个轮询周期再判断。5.3 验证硬盘是否真正进入 standby配置完成之后验证“硬盘确实进入待机”这件事比想象的复杂一点。不能只看日志里说发了命令还要从系统层面确认盘的状态。最常用的两个命令hdparm -C /dev/sdb smartctl -n standby /dev/sdbhdparm -C会输出当前电源状态正常待机时显示drive state is: standby如果还在空转显示active/idle。smartctl -n standby是一个更聪明的用法它先检查磁盘状态如果盘已经在 standby就直接返回而不去真正读取 SMART 数据避免把休眠中的盘唤醒。还有一种情况是盘的状态反复在 active 和 standby 之间横跳这种一般不是 hd-idle 的问题而是有其他进程在周期性读写磁盘导致盘刚进入 standby 又被唤醒。排查这种问题需要用iostat -x 1观察磁盘是否有持续的 IO再用fatrace这类工具定位到底是谁在碰盘。6. 实测中的坑与排查经验6.1 设备节点与权限问题hd-idle 在运行中最常见的问题是找不到/dev/sg*设备节点。现在很多发行版默认没有加载sg内核模块或者磁盘没有生成对应的 SCSI generic 设备节点。如果你的环境里ls /dev/sg*看不到任何输出需要手动加载模块modprobe sg ls /dev/sg*确认节点存在之后还要关注权限。系统管理服务默认以 root 运行一般情况下没有问题但如果出于安全考虑给服务加了User或者用非 root 用户运行就必须确保该用户对/dev/sg*有读写权限。最简单的方式是修改 udev 规则把 sg 设备的组改为 disk然后给服务的 User 加入 disk 组。另外如果 KeyarchOS 上启用了 SELinux 强制模式有可能拦截进程对设备节点的访问排查的时候关注一下/var/log/audit/audit.log里的 avc 拒绝记录。6.2 持续 IO 导致无法休眠我实际调试中最头疼的一个问题是明明业务上没有读写硬盘就是进不了 standby。打开日志一看hd-idle 一直在记录该磁盘有活动。用iostat -x 1观察发现确实每秒都有零星的写入。最后定位到是一个监控代理每隔几十秒写一次心跳文件而该文件恰好落在数据盘上。这种问题有两个解决方向一是尝试把高频小 IO 迁移到系统盘或者内存盘二是在 hd-idle 里把这个盘的匹配规则设为不启用自动休眠毕竟频繁休眠反而比一直空转更伤盘。我给的建议是优先优化业务侧的写入频率因为休眠策略的意义在于“长时间真正空闲时省电”如果盘本身一直在被使用强行休眠反而得不偿失。6.3 RAID/HBA 控制器透传问题如果服务器用的是 RAID 卡或者 HBA 控制器并且 RAID 卡处于 RAID 模式下hd-idle 通过 SCSI generic 接口下发的 standby 命令可能会被控制器拦截无法直接作用到底层物理盘。这种情况下你会看到日志里命令执行成功但是hdparm -C显示磁盘状态没有变化。这不是 hd-idle 的问题而是硬件控制器挡在中间把直通命令吞掉了。解决办法只有一个大方向让物理盘以直通模式pass-through或者 JBOD 模式挂在控制器上。如果控制器必须做 RAID那么 hd-idle 这类用户态工具基本帮不上忙只能依靠控制器自带的电源管理策略。NVMe 盘也不要指望这个工具来管理hd-idle 针对的仍是传统 ATA/SCSI 设备。6.4 与内核电源管理参数的关系还有一个容易忽略的地方hd-idle 的 standby 行为和内核自带的磁盘电源管理参数可能存在交叉影响。比如有些发行版在 tuned 的性能方案里会自动配置磁盘 APM 级别hdparm -B的值会影响磁盘在空闲时是否自动降低转速。如果 tuned 设置的 APM 策略和 hd-idle 的策略打架可能出现盘体转速下来了但状态仍是 active或者 hd-idle 判定空闲下发了 standby但 APM 设置又让它快速回到 ready 状态。建议在部署 hd-idle 的同时把该节点加入一个自定义 tuned 配置或者至少在服务启动后检查一下hdparm -B /dev/sdb的输出确保 APM 的取值和我们的待机目标一致。不然你会发现 hd-idle 一直在努力工作磁盘状态却纹丝不动。6.5 常见问题速查表现象可能原因处理建议日志报 cannot open /dev/sg0sg 模块未加载modprobe sg必要时写入 /etc/modules-load.d服务 active 但进程消失fork 型服务误配为 Typesimple改用 -d 前台模式unit 配 Typesimple磁盘一直不进 standby有进程持续写盘用 iostat、fatrace 定位调大超时或排除该盘命令执行成功但盘状态不变RAID 控制器拦截命令改为直通或 JBOD 模式盘进入 standby 后很快被唤醒APM/tuned 策略冲突检查 hdparm -B调整 tuned profile升级 RPM 后配置丢失spec 未标记配置文件使用 %config(noreplace) 打包7. 最后分享一点适配老工具的经验这次 hd-idle 在 KeyarchOS 上的适配整体难度不算高但过程里踩到的坑几乎全部集中在“旧软件运行模型和新系统服务模型之间的差异”上。源码自带的 sysvinit 脚本、fork 式守护进程行为、对/dev/sg*设备节点的依赖这些在老系统上都不是问题到了 systemd 时代每一项都需要重新审视。如果你也在做类似的老牌工具适配我建议拿到代码后不要急着编译先把它自带的启动方式、依赖的设备节点、默认的配置文件全看一遍再结合目标系统的 systemd 版本去设计服务化方案。另外打包成 RPM 并统一配置其实花不了多少额外时间却能在批量部署时省下大量重复劳动这个习惯值得养成。
返回列表