上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-08-16 10:44 阅读量:743

 


我们的广告竞价与风控服务跑在香港机房(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)

  1.  提前导入 MOK 证书(一次性重启,尽量提前做)
  2.  目标机预检:CONFIG_LIVEPATCH、debuginfo、vmlinux 就绪
  3.  构建机准备好 kpatch-build 与工具链
  4.  从上游拿到 最小化补丁,本地验证编译
  5.  生成 kpatch 模块 → 签名 → 放入内网仓库
  6.  影子节点安装并 kpatch load,确认业务/内核日志稳定
  7.  灰度推进:5% → 25% → 100%,异常立刻 kpatch unload 回滚
  8.  文档化此次补丁(目标函数、CVE 编号、影响面、验证报告)
  9.  在 CI 里注册此补丁的重编任务(内核升级触发)
  10.  定期演练回滚与应急通道

小结:把一次“救火”变成标准动作

在香港节点上做 kpatch,不只是把补丁“打上去”这么简单。你要考虑 Secure Boot 的签名链、Stream 内核的滚动性、内网分发的可控性、以及灰度与回滚的确定性。我走过的坑几乎都来自“图省事的捷径”。当我们把这套流程固化到 CI/CD 和 Ansible 里后,live patch 就不再是“临时决定”,而是标准、可审计、可追溯的生产变更。

如果你也正卡在“不能重启但必须修”的十字路口,希望这篇实操笔记能让你少走几步弯路。祝你下次凌晨的机房窗口,一次过

目录结构
全文