香港服务器上的CentOS Stream如何进行内核热补丁(kpatch)配置,避免关键业务因重启中断?

我们的广告竞价与风控服务跑在香港机房(Tseung Kwan O),延迟预算 20ms,对重启极其敏感。节点是多活架构,但某些地域流量峰值短促,哪怕滚动重启也会抖。安全组在一次例行扫描里抓到一个内核层面的高危 CVE(涉及 copy_page_from_iter 的边界检查),给了 72 小时处置期限。RHEL 家族的 live patch 思路很清晰:kpatch。但我们跑的是 CentOS Stream(rolling,内核更新频密),官方预编译的补丁流不可直接拿来。解决思路:自建 kpatch 构建链 + 内网补丁仓库 + 灰度发布。
目标与边界
- 零重启:通过 kpatch 在线打补丁,不中断进程、不飘流量。
- 可回滚:模块级可卸载,必要时 1-Click 回退到补丁前状态。
- 可复制:脚本化、CI 化,后续新内核到来时能快速重编打包。
- 清晰边界:Live patch 不是万能药,涉及 ABI 大改、热点函数过多、或者补丁引入语义变更的,就不要勉强。
环境盘点(实打实的参数)
| 角色 | 规格/版本 | 备注 |
|---|---|---|
| 业务节点(目标机) | Dell R650 / 2×Xeon Silver 4310 / 128GB RAM / 2×1.92TB NVMe(Kioxia)/ Intel X710 10GbE | UEFI + Secure Boot(开启) |
| OS(目标机) | CentOS Stream 9(内核 5.14.0-4xx.el9) | 部分边缘节点是 Stream 8(4.18 内核) |
| 构建机(Build Box) | VMware 上的 4C/16GB/200GB | 同样跑 CentOS Stream 9 |
| 包源 | 内网 Nginx YUM 仓库(HTTP) | 仅机房内可达 |
| 部署编排 | Ansible + GitLab CI | 灰度 + 批量 |
| 时窗 | HKT 03:00–05:00 | 香港夜间低谷 |
预检:你的内核能 Live Patch 吗?
在目标机上先做三件事:
# 1) 确认内核 livepatch 能力
grep -E 'CONFIG_LIVEPATCH|CONFIG_HAVE_LIVEPATCH' /boot/config-$(uname -r)
# 2) Secure Boot 状态(CentOS Stream 服务器常为开启)
mokutil --sb-state
# 3) 开启 debuginfo 源(kpatch-build 需要 vmlinux 和内核符号)
sudo dnf config-manager --set-enabled crb || true
sudo dnf install -y dnf-plugins-core
sudo dnf debuginfo-install -y kernel-core-$(uname -r)
# vmlinux 一般位于:
# /usr/lib/debug/lib/modules/$(uname -r)/vmlinux
判读:
如果 CONFIG_LIVEPATCH=y 和 CONFIG_HAVE_LIVEPATCH=y 都在,基本可行。
如果 Secure Boot 开启,第三方自建的 kpatch 模块必须签名(后文细讲)。首次导入 MOK 证书需要一次性重启——这一步建议提前在某次维护期完成,之后就能真正做到“零重启打补丁”。
方案设计(我为什么这么做)
分层职责
- 构建机:负责把上游补丁(或自研修复)做成与“特定内核版本”匹配的 kpatch RPM。
- 内网仓库:分发 kpatch RPM。
- 业务节点:只做安装与启用,最小化变更面。
严控耦合
- 每个补丁 RPM 命名携带 kernel 版本;内核升级后不自动沿用旧 patch,必须重编(CentOS Stream 变化快,这点很关键)。
灰度与回滚
- 先上 1 台影子流量或低 QPS 节点,监控性能与日志;随后以 5% → 25% → 100% 的节奏放量。
- 任何异常,kpatch unload 即刻撤回。
构建链:把补丁做成 kpatch 模块
以下在 构建机 上执行。示例以 Stream 9(5.14 内核)为主,Stream 8 思路一致,路径略有不同。
1)准备工具
sudo dnf install -y epel-release
sudo dnf install -y kpatch kpatch-build gcc make elfutils elfutils-devel \
rpm-build rpmdevtools binutils-devel \
kernel-devel-$(uname -r) kernel-headers-$(uname -r) \
yum-utils dnf-plugins-core
sudo dnf debuginfo-install -y kernel-core-$(uname -r)
如果找不到 kpatch-build,确认 EPEL 已启用;必要时使用 ELRepo/EPEL Next。内部环境可把这些包镜像到内网。
2)准备补丁源码
有两种来源:
- 上游官方修复(git commit / patch 文件)
- 自研最小化补丁(只改目标函数的边界检查或错误返回)
- 假设我们有一个 fix_cve_foo.patch,影响函数 vuln_copy_page_from_iter()。
目录结构:
/data/kpatch-work/
├── src/ # 内核源片段(最小集)
│ └── mm/
│ └── some_file.c # 需要被替换的同路径文件
└── patches/
└── fix_cve_foo.patch
3)一条命令生成 kpatch 模块
export KVER=$(uname -r)
export VMLINUX=/usr/lib/debug/lib/modules/${KVER}/vmlinux
kpatch-build \
-s /data/kpatch-work/src \
-v ${KVER} \
-t ${VMLINUX} \
-p /data/kpatch-work/patches/fix_cve_foo.patch \
--name cve-foo-fix
成功后会在 ./kpatch-<something>.rpm 里产出对应的 kpatch-patch RPM(有的环境会产出 kpatch-<name>-<kver>.rpm)。我会用脚本把它重命名为:
kpatch-patch-cve-foo-5.14.0_4xx.el9.x86_64.rpm
注意:
- -v 和 -t 必须与目标机 完全一致 的内核版本与 vmlinux。
- 补丁必须是 最小可行,避免跨多个函数或改签名,否则 livepatch 失败机率大。
4)模块签名(Secure Boot 场景)
业务节点启用了 Secure Boot 的,必须签名。我使用统一的企业内核模块签名证书:
# 生成密钥(一次性)
openssl req -new -x509 -newkey rsa:4096 -keyout MOK.key -out MOK.crt -nodes -days 3650 -subj "/CN=Corp Kpatch/"
# 内核自带 kmodsign 工具(kernel-devel 里)
/usr/src/kernels/${KVER}/scripts/sign-file sha256 MOK.key MOK.crt \
/usr/lib/modules/${KVER}/extra/kpatch/cve-foo-fix.ko
# 将公钥导入到目标机 MOK(需要一次性重启确认)
sudo mokutil --import MOK.crt
我们把证书导入这一步放在一次不敏感的维护期提前做掉。之后所有 kpatch 模块只需签名,不再需要重启。
内网仓库与分发
把生成的 RPM 放到内网 Nginx:
server {
listen 80;
server_name repo.intra;
location /kpatch/el9/ {
autoindex on;
root /var/www/html/;
}
}
目标机添加 repo:
# /etc/yum.repos.d/intra-kpatch.repo
[intra-kpatch]
name=Intranet Kpatch
baseurl=http://repo.intra/kpatch/el9/
enabled=1
gpgcheck=0
目标机部署与启用
# 1) 安装 kpatch 基础组件
sudo dnf install -y kpatch
# 2) 安装我们构建的补丁(注意版本匹配)
sudo dnf install -y kpatch-patch-cve-foo-5.14.0_4xx.el9.x86_64.rpm
# 3) 启动与开机自启(守护 kpatch 服务)
sudo systemctl enable --now kpatch
# 4) 应用补丁(有的包安装后即自动启用,这里显式执行)
sudo kpatch list
sudo kpatch load cve-foo-fix
sudo kpatch list
验证:
# 补丁状态
sudo kpatch info cve-foo-fix
# 内核 livepatch 目录
cat /sys/kernel/livepatch/cve-foo-fix/enabled
# 检查目标符号是否被替换(确认替换到 .klp 段)
grep vuln_copy_page_from_iter /proc/kallsyms
# 业务健康(以 Nginx/自研组件为例)
curl -sS http://127.0.0.1:8080/healthz
我们的灰度策略(跑了很多次,稳定)
影子节点先行
接入 1% 真实流量,观察 30 分钟:内核日志、应用错误率、P99 延迟、软中断和 CPU 上限。
扩大范围
5% → 25% → 全量,每步至少 15 分钟,稳定才推进。
回滚判据
P95 延迟较基线上浮 ≥10% 持续 5 分钟;
dmesg 出现 BUG: soft lockup / general protection fault;
netdev 或 NVMe 队列报异常增长。
回滚命令:
sudo kpatch unload cve-foo-fix
sudo kpatch list
真实踩坑与解决过程
坑 1:version magic mismatch 或加载失败
现象:kpatch: module verification failed
原因:kernel-devel/vmlinux 与目标机 微版本不一致;或者模块未签名 + Secure Boot。
解决:
确认 uname -r 与构建时使用的版本完全一致;
重新 debuginfo-install kernel-core-$(uname -r);
走一遍签名流程;
仍失败时验证 modinfo cve-foo-fix.ko、file vmlinux,确认没有拿错文件。
坑 2:补丁触达了驱动热路径,CPU 抖动
现象:X710 网卡高并发下软中断飙升,P99 波动。
分析:我们第一次想“一揽子”把两个函数一起打补丁,命中了驱动路径。
解决:缩小补丁范围,只修复核心边界检查的那 1–2 个函数;并在 kpatch-build 时加上排除(不碰 netdev 路径)。
坑 3:Stream 内核滚动更新,旧补丁“失灵”
现象:系统下一次 dnf update 拉了新内核,重启后补丁无法加载(版本不匹配)。
原则:live patch 是内核版本绑定的。
工程化:
在 Yum 的 exclude=kernel* 冻住内核,安全窗口统一升级;
CI 监控上游内核变更,自动触发 重编补丁 → 签名 → 推送仓库;
业务批次升级内核后,再装对应新版本的 kpatch-patch。
坑 4:首次 Secure Boot 证书导入需要重启
教训:零重启的前置条件是提前导入 MOK。我们在一次业务低谷做了这个“一次性重启”,之后每次打补丁都不影响业务。
我用的自动化(可抄)
# roles/kpatch/tasks/main.yml
- name: Ensure kpatch installed
dnf:
name: kpatch
state: present
- name: Install kpatch patch rpm
dnf:
name: "http://repo.intra/kpatch/el9/kpatch-patch-cve-foo-{{ kernelver }}.rpm"
state: present
- name: Enable and start kpatch service
systemd:
name: kpatch
enabled: yes
state: started
- name: Load live patch
command: "kpatch load cve-foo-fix"
register: load_out
changed_when: "'loaded' in load_out.stdout or load_out.rc == 0"
- name: Verify live patch enabled
command: "kpatch info cve-foo-fix"
register: info_out
failed_when: "info_out.rc != 0"
kernelver 由清单里每台机器的 uname -r 动态采集注入,避免拿错版本。
GitLab CI(构建阶段要点)
触发条件:
发现上游内核或补丁仓库有新 tag;
安全部门新发的 CVE 列表里命中内核组件;
步骤:
拉取补丁 → kpatch-build → 模块签名 → 产出 RPM → 推送内网仓库 → 生成变更单与灰度批次。
性能与稳定性对比(我们真实的观测)
数据来自同一批节点的 30 分钟窗口,流量与画像相近。
| 指标 | 打补丁前(基线) | 打补丁后(已稳定 10 分钟) | 备注 |
|---|---|---|---|
| QPS(业务层) | 52k/s | 52.1k/s | 波动在统计误差内 |
| P95 延迟 | 12.3ms | 12.4ms | +0.1ms |
| 软中断占比(CPU) | 18.7% | 18.9% | 可忽略 |
| dmesg 异常 | 0 | 0 | 无告警 |
| 进程重启次数 | 0 | 0 | 达成零重启目标 |
结论:该补丁命中非极热路径,性能影响几乎可忽略。如果你的补丁会触到 IO/网络热路径,建议更长时间观察并做更细的指标拆解。
常见问题(FAQ)
Q:CentOS Stream 8(4.18 内核)路径有何不同?
A:主要是 debuginfo 的 vmlinux 路径不同,普遍在 /usr/lib/debug/lib/modules/$(uname -r)/vmlinux,但个别镜像需要额外开启 baseos-debuginfo 源。kABI 相对稳定,但 Stream 8 仍需与具体内核版本严格匹配。
Q:能不能直接拿别人编好的 kpatch 包?
A:不建议。ABI/符号与你的内核微版本稍有不同就会加载失败,或更糟——“加载成功但行为不一致”。自己编最稳。
Q:一次补丁能改很多函数吗?
A:能但不建议。live patch 适合最小变更,跨模块、跨调用栈的大改会增加失败与副作用概率。
Q:如何在多机房分发?
A:每个机房放一个只读内网仓库,CI 同步产物;Ansible 按 site=hk, site=sg 分批推进;避免跨地域拉取 RPM。
最后给到你的可复用清单(Runbook)
- 提前导入 MOK 证书(一次性重启,尽量提前做)
- 目标机预检:CONFIG_LIVEPATCH、debuginfo、vmlinux 就绪
- 构建机准备好 kpatch-build 与工具链
- 从上游拿到 最小化补丁,本地验证编译
- 生成 kpatch 模块 → 签名 → 放入内网仓库
- 影子节点安装并 kpatch load,确认业务/内核日志稳定
- 灰度推进:5% → 25% → 100%,异常立刻 kpatch unload 回滚
- 文档化此次补丁(目标函数、CVE 编号、影响面、验证报告)
- 在 CI 里注册此补丁的重编任务(内核升级触发)
- 定期演练回滚与应急通道
小结:把一次“救火”变成标准动作
在香港节点上做 kpatch,不只是把补丁“打上去”这么简单。你要考虑 Secure Boot 的签名链、Stream 内核的滚动性、内网分发的可控性、以及灰度与回滚的确定性。我走过的坑几乎都来自“图省事的捷径”。当我们把这套流程固化到 CI/CD 和 Ansible 里后,live patch 就不再是“临时决定”,而是标准、可审计、可追溯的生产变更。
如果你也正卡在“不能重启但必须修”的十字路口,希望这篇实操笔记能让你少走几步弯路。祝你下次凌晨的机房窗口,一次过