Linux 基础体系 · 第 84/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux 补丁与发行版升级:安全更新、内核、重启、灰度和回滚

Linux 的“更新”不是一个动作,而是一组不同风险的状态变化:

  • 安全更新:在不改变发行版主版本的前提下,修复已知漏洞或安全缺陷。
  • 软件包更新:替换某个用户态软件包及其依赖,可能改变行为,但通常保持发行版兼容性。
  • 内核更新:安装新的内核、模块和相关启动文件;新内核通常要在重启后才运行。
  • 发行版升级:跨越发行版版本边界,例如 Ubuntu 22.04 到 24.04、RHEL 8 到 9,涉及仓库、ABI、默认组件和系统迁移工具的整体变化。
  • 重启:使新内核以及部分需要重新启动的用户态服务真正接管运行。
  • 灰度:先在少量、具有代表性的节点上应用变更,通过观测结果决定是否扩大范围。
  • 回滚:把系统恢复到变更前可接受的状态。软件包回退、启动旧内核、文件系统快照恢复和备份恢复并不是同一种回滚。

补丁管理的核心不是“把所有包更新到最新”,而是控制如下状态转换:

flowchart LR
    A[基线确认] --> B[仓库与签名校验]
    B --> C[解析依赖与变更集合]
    C --> D[预演与备份]
    D --> E[灰度安装]
    E --> F[服务与安全验证]
    F --> G{是否通过}
    G -- 是 --> H[扩大范围]
    G -- 否 --> I[停止扩散]
    I --> J[回滚或切换备用节点]
    H --> K[确认是否需要重启]
    K --> L[计划重启]
    L --> M[重启后验证]

每一步都产生证据:机器身份、当前版本、候选版本、安装事务、服务状态、监控结果和回滚结果。没有这些证据,事后很难判断“补丁是否安装”“新内核是否运行”以及“故障是否由本次变更引起”。


一、先区分安全更新、软件包更新与发行版升级

1. 安全更新不等于所有最新版本

安全更新通常由发行版维护者从上游修复中构建,并回移植到当前发行版版本。一个老版本号可能已经包含补丁,因此不能只根据上游版本号判断是否安全。

例如,发行版可能将某个漏洞修复回移植到:

openssl-3.0.x-某发行版修订号

上游最新版本可能已经是 3.0.y,但发行版的修订号已经包含对应修复。判断依据应优先使用:

  1. 发行版安全公告;
  2. 软件包的发行版修订号;
  3. 包管理器提供的安全分类;
  4. 漏洞扫描器与发行版 OVAL、CVE 数据的对应关系。

CVE 表示漏洞标识,不直接表示某台机器一定受影响。是否受影响还取决于:

  • 软件包是否安装;
  • 漏洞代码路径是否被编译启用;
  • 当前版本是否已包含回移植修复;
  • 服务是否实际运行;
  • 配置是否允许触发漏洞;
  • 该漏洞是否只影响特定架构或特定功能。

因此,“存在 CVE”与“需要在此刻安装某个包”之间还存在适用性判断。

2. 软件包更新的边界

软件包更新通常发生在同一发行版大版本内,例如 Ubuntu 22.04 的普通更新,或 RHEL 9 内的维护更新。它可能同时更新:

  • ELF 二进制文件;
  • 动态库;
  • systemd unit;
  • 配置文件模板;
  • 内核模块;
  • 数据库客户端;
  • Python、Ruby 等语言运行时;
  • 依赖关系和触发器。

包管理器负责文件、依赖和安装脚本的一致性,但不负责证明业务兼容。一个包成功安装,只说明包事务完成,不说明应用行为正确。

3. 发行版升级是迁移,而不是“大号补丁”

发行版升级通常包含:

  • 仓库地址和发行版代号变化;
  • 大量包的版本和 ABI 变化;
  • 编译器、Python、OpenSSL、systemd 等基础组件变化;
  • 配置格式变化;
  • 服务默认行为变化;
  • 引导加载器和内核变化;
  • 旧包淘汰或替换;
  • 可能的数据库、容器运行时和第三方驱动兼容问题。

因此,发行版升级的回滚难度远高于普通包更新。即使能把部分包降级,也可能无法恢复已经执行的配置迁移、数据库 schema 迁移或数据格式转换。


二、包管理器真正保证什么

APT、DNF 等包管理器主要处理四类问题:

  1. 仓库元数据:有哪些包、版本、依赖、校验和、更新公告;
  2. 签名与完整性:仓库元数据是否由受信任密钥签名,下载文件是否匹配校验和;
  3. 依赖求解:安装或升级后,包之间的版本约束能否同时满足;
  4. 事务执行:解包、配置、触发器和安装脚本按顺序执行。

它们不自动保证:

  • 新版本没有回归;
  • 服务配置正确;
  • 业务协议兼容;
  • 数据库迁移可逆;
  • 重启后仍能启动;
  • 远程 SSH 连接一定不会中断;
  • 旧版本的所有文件都能恢复。

1. 仓库、签名与包文件的数据流

典型数据流如下:

仓库地址
  ↓
下载 Release/repomd 元数据
  ↓
验证发行版信任的签名密钥
  ↓
解析包索引、依赖和安全公告
  ↓
下载具体 RPM/DEB
  ↓
校验包哈希
  ↓
解包与执行维护脚本
  ↓
更新本地包数据库

签名通常保护“元数据确实由受信任仓库发布”,哈希通常保护“下载的包与元数据声明的一致”。这两者都不能证明包没有逻辑缺陷,也不能替代来源治理。生产环境还应限制仓库来源,避免把不受控的第三方仓库混入基础系统。

2. APT 的基本检查与更新

以下命令适用于 Debian、Ubuntu 等使用 APT 的系统。需要 root 或 sudo 权限。

# 查看发行版和内核
cat /etc/os-release
uname -r

# 刷新仓库元数据,不安装软件
sudo apt-get update

# 查看可升级包
apt list --upgradable

# 预演普通升级,不改变系统
sudo apt-get -s upgrade

apt-get update 只刷新索引,不会安装补丁。apt-get -s upgrade 中的 -s 表示模拟,输出中的安装、升级、删除数量是依赖求解结果,不是最终业务风险评估。

普通升级可以使用:

sudo apt-get upgrade

它通常不会为了满足新依赖而删除现有包或安装大量新包。需要允许依赖集合发生更大变化时,常见命令是:

sudo apt-get full-upgrade

full-upgrade 可能删除包,因此必须阅读事务摘要。删除一个看似无关的元包,可能导致后续内核或桌面组件不再随更新安装。

安全更新并没有跨发行版统一的 APT 命令。可以先检查仓库是否提供安全来源:

grep -R --line-number --no-messages \
  -E 'security|jammy|bookworm|stable' \
  /etc/apt/sources.list /etc/apt/sources.list.d/

Ubuntu 常见的自动更新由 unattended-upgrades 实现;Debian、Ubuntu 的具体配置文件和默认来源可能不同。不能仅凭安装了该包就断定所有安全更新都会自动安装,应检查:

dpkg -l unattended-upgrades
systemctl status unattended-upgrades --no-pager
grep -R --line-number --no-messages \
  'Unattended-Upgrade' /etc/apt/apt.conf.d/

3. DNF 的基本检查与安全分类

Fedora、RHEL、Rocky Linux、AlmaLinux 等系统常使用 DNF 体系,但命令、插件和仓库权限会因发行版版本不同而不同。

# 查看系统信息
cat /etc/os-release
uname -r

# 刷新并显示可用更新
sudo dnf check-update

dnf check-update 在存在可用更新时可能返回退出码 100,这不是命令执行失败。脚本中必须区分“有更新”和“出错”,不能简单使用:

dnf check-update && echo OK

查询安全公告:

sudo dnf updateinfo summary
sudo dnf updateinfo list --security
sudo dnf updateinfo info --security

在支持该功能和相应仓库元数据的系统上,可以只应用标记为安全更新的包:

sudo dnf upgrade --security

但“安全更新”依赖发行版发布的 updateinfo 元数据。如果仓库没有完整安全分类,命令可能无法覆盖所有安全修复。生产判断应以发行版公告和仓库内容为准,而不是把 --security 当成漏洞扫描器。

预演普通升级:

sudo dnf upgrade --assumeno

不同 DNF 版本对模拟、下载和插件选项的支持存在差异;执行前可查看:

dnf --help
dnf upgrade --help

4. 为什么不能混用包管理器

不要直接用 rpm -Uvhdpkg -i 替代 DNF、APT 处理常规升级。底层工具可以安装单个文件,但通常不负责从仓库求解完整依赖闭包。

例如,一个 DEB 包依赖:

A → B (>= 2.0)
A → C (= 1.4)

直接 dpkg -i A.deb 可能先解包 A,再因为 B 或 C 不满足而留下“已解包但未配置”的中间状态。APT 能基于仓库求解依赖并完成配置,底层工具更适合包构建、救援或由高级包管理器调用。


三、依赖求解与“可重复安装”

1. 包管理器求解的是依赖约束,不是业务约束

设安装集合为 SS,每个包的版本为 viv_i。包管理器需要找到一个版本选择,使得:

dDependencies(S),d 的版本约束都满足\forall d \in Dependencies(S),\quad d \text{ 的版本约束都满足}

例如:

应用 A 需要 libssl >= 3.0
插件 B 需要 libssl < 3.2

那么 libssl=3.1 可能满足两者;如果仓库只提供 3.3,依赖求解可能失败,或者在允许替换包时移除 B。包管理器只知道这些形式化约束,不知道 B 是否是生产关键插件。

2. 依赖闭包扩大了变更范围

假设业务服务 app 依赖:

app
├── libhttp
│   └── openssl
└── systemd-libs

升级 openssl 可能触发 libhttpapp 或其他服务重启。一个安全库补丁因此可能影响多个进程。更新前应查看事务摘要,而不是只看最初的目标包:

# APT 模拟
sudo apt-get -s install openssl

# DNF 模拟
sudo dnf upgrade --assumeno openssl

如果计划只修复一个 CVE,却看到大量核心包被替换或删除,应先查明原因:仓库混用、模块流切换、发行版版本不一致、锁定包或依赖冲突都可能造成范围扩大。

3. 可重复安装需要固定的不只是包名

“执行 apt install nginx”不等于可重复安装,因为仓库候选版本会随时间变化。更可重复的方案至少记录:

  • 发行版版本和架构;
  • 仓库 URL、发行版代号和组件;
  • 仓库签名密钥;
  • 包名和精确版本;
  • 配置管理代码;
  • 第三方包来源;
  • 内核与启动参数;
  • 数据库 schema 版本。

APT 可以查询已安装版本:

dpkg-query -W -f='${Package}\t${Version}\n' > packages-debian.tsv

RPM 系统可以使用:

rpm -qa --qf '%{NAME}\t%{EPOCHNUM}:%{VERSION}-%{RELEASE}.%{ARCH}\n' \
  > packages-rpm.tsv

这些清单是证据和重建输入,但不是完整系统备份。它们不包含所有配置、运行时数据、密钥、用户生成文件和外部服务状态。


四、内核更新:安装、运行与重启是三个不同状态

1. 内核包已安装,不代表新内核正在运行

Linux 内核通常由引导加载器在启动阶段加载。一次内核更新至少有三个状态:

旧内核正在运行
  ↓ 安装新内核包
新内核文件存在,但旧内核仍在运行
  ↓ 重启并由 bootloader 选择新内核
新内核正在运行

因此:

uname -r

显示的是当前运行中的内核,不是“系统上安装的最高版本内核”。

Debian/Ubuntu 可以查看已安装内核:

dpkg-query -W -f='${Package}\t${Version}\n' 'linux-image*' 2>/dev/null

RPM 系统可以查看:

rpm -qa | grep -E '^kernel(-core|-modules)?'

重启后必须再次执行 uname -r,并与预期版本比较。

2. 内核更新涉及哪些组件

一个内核更新可能包含:

  • 内核镜像;
  • initramfs;
  • 内核模块;
  • 固件;
  • linux-imagekernel-core 等包;
  • DKMS 或第三方内核模块;
  • GRUB 配置;
  • 早期启动所需的存储、网络和加密模块。

如果根文件系统位于 RAID、LVM、iSCSI、NVMe、加密卷或特殊存储上,initramfs 中缺少对应模块可能导致“包安装成功但重启无法挂载根文件系统”。

常见检查:

# 查看 initramfs 文件
ls -lh /boot/initr* /boot/initramfs* 2>/dev/null

# 查看当前启动参数
cat /proc/cmdline

# 查看已加载模块
lsmod

不同发行版生成 initramfs 的工具可能是 initramfs-toolsdracut,不要跨发行版照抄参数。

3. DKMS 和第三方模块是高风险点

显卡、存储、虚拟化、安全代理等第三方模块可能需要针对每个内核重新编译。新内核安装后,模块编译失败不一定阻止包事务完成,但重启后设备可能消失或服务无法启动。

检查 DKMS 状态:

dkms status 2>/dev/null || true

检查新内核对应模块是否存在,需要先知道目标内核版本:

find /lib/modules -maxdepth 2 -type f -name '*.ko*' | head

在生产环境中,内核灰度节点必须覆盖真实硬件、存储、网卡、加密、监控和安全代理组合;只在普通云主机上通过,不能证明物理机或特殊设备通过。


五、重启:为什么需要、如何判断、如何验证

1. 用户态服务也可能继续使用旧代码

替换磁盘上的共享库后,已经运行的进程通常仍持有旧库的文件描述符和内存映射。新启动的进程才会加载新库。因此:

  • 包文件可能已经是新版本;
  • 旧进程仍执行旧代码;
  • 重启服务可以让该服务加载新库;
  • 重启主机才能切换内核,并处理更多早期启动组件。

可使用 lsof 检查已删除但仍被进程打开的文件:

sudo lsof +L1

输出中的 DEL 文件不一定表示故障,但说明磁盘上的文件已替换,旧进程仍持有旧 inode。对关键服务,应根据发行版维护脚本和服务管理器结果决定是否重启,而不是盲目重启所有服务。

2. “需要重启”的判断是发行版相关的

Debian/Ubuntu 常见提示文件:

test -f /var/run/reboot-required && echo "reboot required"
cat /var/run/reboot-required.pkgs 2>/dev/null || true

RHEL 系列常见检查工具:

command -v needs-restarting >/dev/null && sudo needs-restarting -r

这些机制属于发行版实现或工具提供的判断,不是 Linux 内核统一规范。没有提示也不代表绝对不需要重启;有提示也应核对具体包和业务计划。

3. 重启前后的验证闭环

重启前记录:

date -Is
hostnamectl
uname -a
systemctl --failed
df -hT
df -ih

重点是保存:

  • 主机名和实例身份;
  • 当前内核;
  • 失败的 systemd 单元;
  • /boot、根分区和 inode 使用率;
  • 变更事务日志;
  • 远程访问路径和带外控制台入口。

重启可以使用:

sudo systemctl reboot

重启后验证:

uptime
uname -r
systemctl --failed
systemctl is-system-running
journalctl -b -p warning..alert --no-pager

systemctl is-system-running 返回 degraded 时,不一定表示本次补丁失败,但必须检查失败单元。还应验证实际业务:

curl --fail --silent --show-error https://service.example/health
ss -lntp

健康检查应包含依赖,例如数据库连接、队列连接、证书加载和关键读写,而不是只检查 TCP 端口是否监听。


六、SSH 远程更新的特殊风险

OpenSSH 客户端、服务端和相关库更新时,存在两类不同风险:

  1. 当前连接风险:当前 sshd 进程可能仍在使用旧库,更新文件本身通常不会立即断开已有会话;
  2. 新连接风险:服务重启或主机重启后,新配置、主机密钥、算法和认证模块可能导致新连接失败。

更新 SSH 前应确认:

# 检查配置语法
sudo sshd -t

# 查看实际生效配置;不同版本支持的选项可能不同
sudo sshd -T | less

# 保留一个已验证的现有会话,并准备第二个会话测试
ssh -o BatchMode=yes user@host 'hostname; uname -r'

不要只依赖 SSH 作为唯一恢复通道。应准备云厂商串口、虚拟控制台、远程管理卡或现场带外入口。

OpenSSH 官方手册对 sshd_config、密钥、认证和命令行为的定义,应优先于博客中的参数示例。参考:OpenSSH Manual Pages


七、普通补丁、内核 Live Patching 与重启不能混为一谈

Live patching 是在内核运行期间替换部分内核代码,以减少因安全漏洞而重启的次数。它通常只覆盖特定内核修复和受支持的发行版、架构及订阅范围。

它不能替代普通重启,因为以下内容仍可能需要重启:

  • 未被 live patch 覆盖的内核组件;
  • 新硬件驱动;
  • 引导加载器和 initramfs;
  • 用户态库和服务;
  • 需要重新初始化的内核状态;
  • 发行版升级后的整体组件变化。

因此,安全策略应记录两个状态:

漏洞修复已应用:是/否
运行中的内核与用户态进程已切换:是/否

“补丁已安装”与“正在运行的代码已修复”不是同一个布尔值。Linux 内核安全文档对内核安全机制、威胁模型和实现边界的说明可参考:Linux Security Documentation


八、生产变更前的预检查

补丁风险往往不是下载失败,而是系统在不适合变更时被改变。至少应检查以下项目。

1. 身份、版本和仓库

hostnamectl
cat /etc/os-release
uname -r

# APT
apt-cache policy 2>/dev/null | head -50

# DNF
sudo dnf repolist

确认主机确实属于目标环境,避免把测试命令执行在生产节点。仓库应满足:

  • 来源明确;
  • 签名校验启用;
  • 镜像同步完成;
  • 与发行版版本匹配;
  • 第三方仓库已评估;
  • 代理、离线镜像和证书有效。

2. 容量和 inode

df -hT
df -ih
sudo du -xhd1 /var /boot 2>/dev/null | sort -h

/boot 空间不足可能阻止新内核和 initramfs 写入;inode 耗尽则可能在磁盘还有空间时仍无法创建文件。日志、缓存、旧内核和包下载目录都可能影响事务。

3. 当前健康状态和服务依赖

systemctl --failed
systemctl list-units --type=service --state=running
systemctl list-dependencies --reverse network-online.target

如果系统在补丁前已经存在失败单元,变更后出现同样故障将难以归因。应先记录并处理基线故障。

4. 备份的可恢复性

“有备份”不等于“可回滚”。至少需要知道:

  • 备份时间点;
  • 是否包含配置和密钥;
  • 是否包含应用数据;
  • 是否与当前版本兼容;
  • 恢复需要多长时间;
  • 是否做过恢复演练;
  • 恢复后如何避免旧节点重新接收流量。

对数据库而言,备份恢复和事务日志恢复通常比单纯保存软件包更重要。软件包回滚无法撤销已经提交的业务数据。


九、灰度发布:不是“先更新一台”这么简单

1. 灰度节点必须具有代表性

如果生产集群中存在以下差异:

  • 不同 CPU、网卡或存储;
  • 不同内核模块;
  • 不同流量角色;
  • 不同配置开关;
  • 不同数据库分片;
  • 不同可用区;
  • 不同容器运行时;

那么灰度集合必须覆盖这些风险维度。只挑一台“最空闲”的机器,可能恰好避开了最重要的故障路径。

2. 灰度的最小闭环

对每个灰度节点,应按顺序执行:

  1. 从负载均衡或服务发现中摘除;
  2. 等待连接和任务排空;
  3. 记录补丁前版本与业务指标;
  4. 进行模拟和实际安装;
  5. 按需重启服务或主机;
  6. 验证新版本、服务和业务;
  7. 观察足够长的窗口;
  8. 通过后重新接流;
  9. 再选择下一批节点。

摘流不能只停止新连接。如果存在长连接、异步任务、消息消费或定时任务,还应确认排空和重复执行语义。

3. 用可观测信号决定扩散

灰度期间应比较变更前后的:

  • 错误率;
  • 延迟分位数;
  • 吞吐;
  • CPU、内存、IO、连接数;
  • OOM、内核日志和磁盘错误;
  • 服务重启次数;
  • TLS、认证、DNS 和数据库失败;
  • 队列积压和任务重试。

可以定义一个简单的扩散条件。设基线错误率为 e0e_0,灰度错误率为 e1e_1,允许增量为 Δe\Delta e;只有满足:

e1e0Δee_1 - e_0 \leq \Delta e

并且关键业务探针、日志和资源指标均未越界,才允许扩大范围。这里的阈值不是 Linux 规范,而是业务变更策略;关键在于事先定义、自动采集和明确停止条件。


十、回滚的四种层次

1. 服务回滚

如果问题只是新配置或服务行为,可以恢复配置并重启服务:

sudo cp /etc/example/service.conf.backup /etc/example/service.conf
sudo systemctl restart example.service
sudo systemctl status example.service --no-pager

配置备份必须与变更关联,且不能把包含密钥的备份文件暴露给普通用户。

2. 软件包版本回退

APT 可以查询版本并尝试安装指定版本:

apt-cache policy nginx
sudo apt-get install nginx=目标版本

前提是仓库仍保留该版本,且依赖允许回退。可能还需要同时指定相关依赖的版本。apt-mark hold 可以阻止某些包继续自动升级,但不是完整回滚机制:

sudo apt-mark hold nginx
sudo apt-mark unhold nginx

RPM 系统可以查看 DNF 事务历史:

sudo dnf history
sudo dnf history info 事务ID

某些情况下可以使用:

sudo dnf history undo 事务ID

这只能在事务可逆、旧包仍可获取、依赖关系允许的情况下使用。它不是时间机器,不能撤销安装脚本产生的任意副作用,也不能恢复数据库数据。

3. 启动旧内核

如果新内核无法启动,GRUB 通常会保留旧内核条目。应通过控制台选择旧内核启动,启动后检查:

uname -r
systemctl --failed
journalctl -b -1 -p err --no-pager

旧内核不要在灰度验证完成前删除。删除旧内核前应至少保留一个已验证可启动的回退内核,并确认引导配置已更新。

4. 文件系统快照或整机恢复

LVM、Btrfs、ZFS、云盘快照和虚拟机镜像可以提供更大范围的恢复点,但语义取决于实现:

  • 快照是否包含 /boot
  • 快照是否跨越多个独立磁盘;
  • 数据库是否处于一致状态;
  • 快照是否复制到故障域之外;
  • 恢复后网络身份、挂载点和密钥是否正确;
  • 是否能在规定时间内完成恢复。

跨发行版升级时,整机镜像、可替换节点和蓝绿环境通常比“逐包降级”更可靠。


十一、为什么回滚经常失败:数据库迁移反例

设旧版本应用使用表:

CREATE TABLE users (
    id BIGINT PRIMARY KEY,
    name TEXT NOT NULL
);

新版本执行了不可逆迁移:

ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT '';

之后新版本开始向 email 写入数据。即使把应用包降级回旧版本,数据库仍然包含新 schema 和新数据。旧版本可能:

  • 忽略新列,功能看似正常;
  • 因为 schema 检查失败而拒绝启动;
  • 读取新格式后产生错误;
  • 覆盖或丢失新版本写入的数据。

因此,应用发布必须区分:

  1. 向前兼容:旧代码能处理新 schema;
  2. 向后兼容:新代码能处理旧 schema;
  3. 回滚兼容:发生降级时数据仍能被旧代码安全使用。

如果不具备回滚兼容性,正确的回滚单位通常是“切换到旧的完整环境并恢复或隔离数据”,而不是只降级 RPM/DEB。


十二、发行版升级的典型路径与差异

发行版升级不能使用一个跨发行版通用命令。常见路径如下:

1. Ubuntu

Ubuntu 的版本升级通常使用发行版提供的升级工具,例如:

sudo do-release-upgrade

是否允许从当前版本直接升级、是否需要先安装全部当前版本更新、是否支持服务器版本,取决于当前版本、目标版本和升级路径。生产环境应先阅读对应版本的发行说明,并在相同镜像、仓库和配置上演练。

2. Debian

Debian 大版本升级通常涉及:

  • 备份并审计当前仓库;
  • 将软件源从旧代号切换到新代号;
  • 先完成当前版本更新;
  • 分阶段执行最小升级和完整升级;
  • 检查配置文件冲突;
  • 重启并验证服务。

不能直接把所有源文本替换后无条件执行 full-upgrade。第三方源、混合发行版源和残留 pin 优先级可能让依赖求解结果不可预测。

3. RHEL 及兼容发行版

RHEL 主版本升级通常使用发行版指定的迁移工具和检查流程,例如 Leapp 体系,而不是仅执行 dnf upgrade。升级前的 pre-upgrade 报告、订阅状态、模块流、第三方驱动和已知 inhibitor 必须处理。

4. Fedora

Fedora 的版本升级通常使用 dnf system-upgrade 体系,但具体命令和步骤随 DNF 版本变化,应以目标版本官方文档为准。它与普通 dnf upgrade 的生命周期不同,往往会下载升级事务并在重启后的特殊目标中完成。

这些差异说明:包管理器升级同一发行版内的软件,发行版升级工具负责改变发行版生命周期和迁移状态。不要因为两个系统都使用 RPM 或 DEB,就认为升级流程等价。


十三、配置文件冲突与维护脚本

软件包升级可能同时修改配置文件。常见行为包括:

  • 保留管理员修改过的旧配置;
  • 安装发行商的新配置为 .dpkg-dist.rpmnew 等文件;
  • 将旧配置保存为 .dpkg-old.rpmsave 等文件;
  • 在交互界面要求选择;
  • 由自动化工具按预设策略处理。

升级后应搜索这些痕迹:

sudo find /etc -xdev \
  \( -name '*.dpkg-dist' -o -name '*.dpkg-old' \
     -o -name '*.rpmnew' -o -name '*.rpmsave' \) \
  -print

维护脚本可能执行用户、目录、缓存、systemd daemon reload、服务重启或 schema 操作。查看包内容和脚本有助于解释副作用:

# Debian
apt-get download 包名
dpkg-deb -I 包文件.deb
dpkg-deb -e 包文件.deb /tmp/pkg-control

# RPM
rpm -q --scripts 包名

不要在生产主机上随意手工执行从包中提取的脚本。这里的目的主要是审计和理解生命周期。


十四、失败表现与诊断路径

1. 包事务中断

常见表现:

unmet dependencies
dpkg was interrupted
transaction check error
file ... from install of ... conflicts with file ...

诊断顺序应是:

# APT
sudo dpkg --audit
sudo apt-get check

# RPM/DNF
sudo dnf check
sudo rpm -Va

rpm -Va 会验证已安装文件的元数据和校验信息,但配置文件被管理员修改并不自动表示损坏。不要在原因不明时直接删除包数据库或强制覆盖文件。

2. 服务安装成功但启动失败

systemctl status 服务名 --no-pager
journalctl -u 服务名 -b --no-pager
systemctl cat 服务名

应区分:

  • ExecStart 路径变了;
  • 配置语法不兼容;
  • 权限或 SELinux/AppArmor 策略拒绝;
  • 端口已被占用;
  • 依赖服务未启动;
  • 动态库或内核能力缺失;
  • 数据目录版本不兼容。

只看 systemctl status 的最后几行通常不够,需结合本次包变更和服务完整日志。

3. 重启后系统无法正常启动

首先通过控制台确认:

  • 是否进入了正确内核;
  • 根文件系统是否挂载;
  • initramfs 是否找不到设备;
  • 网络是否启动;
  • systemd 是否进入 emergency 或 degraded;
  • 是否因为磁盘检查、加密解锁或挂载超时阻塞。

如果旧内核可以启动,记录新内核启动日志:

journalctl -k -b -1 --no-pager

其中 -b -1 表示上一次启动,实际可用的 boot ID 应通过 journalctl --list-boots 核对。

4. SSH 断开但主机仍健康

可能只是 sshd 重启导致连接短暂中断,也可能是:

  • 新配置拒绝认证;
  • 防火墙规则改变;
  • 网络服务未启动;
  • DNS 或路由失败;
  • 安全模块阻止密钥读取;
  • 新内核驱动异常。

应立即使用备用会话或带外控制台判断“主机不可达”与“SSH 服务不可用”的区别。


十五、自动更新、手工更新与维护窗口

自动安全更新适合补丁频繁、节点可替换且有充分监控的环境,但必须明确它会不会:

  • 自动重启服务;
  • 自动重启主机;
  • 安装内核;
  • 删除或替换依赖;
  • 在业务高峰执行;
  • 受仓库镜像延迟影响。

可将策略分成三层:

自动下载安全包
  ↓
自动安装用户态安全包
  ↓
人工批准内核和发行版升级

这不是所有系统的默认行为,而是一种降低爆炸半径的策略。对于不可中断节点,自动更新并不自动等价于安全,因为安装后的“尚未重启”状态可能持续很久,形成表面已修复、实际仍运行旧代码的差异。


十六、补丁优先级:不能只按版本新旧排序

可以把补丁优先级近似写成:

Priority=Impact×Exposure×Exploitability×AssetCriticalityPriority = Impact \times Exposure \times Exploitability \times AssetCriticality

其中:

  • Impact:漏洞或回归可能造成的影响;
  • Exposure:服务是否暴露在不可信网络;
  • Exploitability:是否已有公开利用或正在被利用;
  • AssetCriticality:节点对业务的关键程度。

这个公式不是标准评分算法,而是帮助排队的决策模型。它解释了两个常见反例:

  • 内网测试机存在高危远程执行漏洞,可能比公网但未启用相关服务的机器优先;
  • 关键数据库节点的普通内核更新,也可能需要比边缘 Web 节点更严格的灰度和窗口。

CVSS、CVE、发行版安全公告和业务资产等级应结合使用,不能只按 CVSS 分数自动决定是否立即重启。


十七、一个可审计的补丁执行示例

下面示例以 APT 系统为例,展示“记录—预演—更新—验证”的最小闭环。它不是所有生产环境都适用的通用脚本,执行前必须确认节点已摘流、备份可用且有带外访问。

#!/usr/bin/env bash
set -Eeuo pipefail

log="/var/log/patch-$(date +%Y%m%d-%H%M%S).log"
exec > >(tee -a "$log") 2>&1

echo "=== baseline ==="
date -Is
hostnamectl
cat /etc/os-release
uname -r
systemctl --failed || true
df -hT
df -ih

echo "=== refresh metadata ==="
apt-get update

echo "=== simulation ==="
apt-get -s upgrade

echo "=== apply update ==="
DEBIAN_FRONTEND=noninteractive apt-get \
  -o Dpkg::Options::="--force-confold" \
  upgrade -y

echo "=== post-package check ==="
dpkg --audit || true
apt-get check
test -f /var/run/reboot-required && echo "REBOOT_REQUIRED=yes" || true
cat /var/run/reboot-required.pkgs 2>/dev/null || true

echo "=== service failures ==="
systemctl --failed || true

关键点如下:

  • set -Eeuo pipefail 让脚本更早暴露错误,但无法保证每个子命令的业务语义正确;
  • 日志保存主机身份、时间和命令结果,便于审计;
  • apt-get update 与实际升级分开,便于定位是仓库问题还是包事务问题;
  • 模拟结果必须人工或自动策略检查,尤其是删除列表;
  • --force-confold 会保留本地配置,可能导致新版本需要的配置项没有加入,不能盲目作为统一策略;
  • 脚本没有自动重启,因为重启是独立的可用性变更;
  • systemctl --failed 只是发现异常,不是业务验证。

生产系统通常还应把“摘流、健康探针、重启、重新接流、观察窗口和停止条件”纳入发布编排,而不是只封装包管理命令。


十八、容易混淆的结论

“补丁安装成功,所以漏洞已经修复”

不一定。可能安装了错误架构、安装到了备用节点、漏洞修复需要重启、服务仍持有旧库,或者扫描器识别规则与发行版回移植版本不一致。应同时确认包版本、运行进程、运行内核和服务重启状态。

“重启后有旧内核,所以一定能回滚”

不一定。旧内核可能缺少当前硬件驱动,旧用户态包和新配置也可能不兼容。旧内核主要提供内核层回退路径,不等于完整系统回滚。

“DNF history undo 或 APT 指定旧版本可以恢复”

只对包文件和部分事务有效。安装脚本、配置迁移、数据库数据、外部状态和其他节点已经发生的变化不会自动恢复。

“只更新安全包风险最小”

安全包可能更新 glibc、OpenSSL、内核、systemd、认证栈或容器运行时,依赖闭包仍可能很大。安全优先级高,不代表变更风险为零。

“灰度一台机器就够了”

只有当这台机器覆盖了目标节点的硬件、配置、流量和依赖特征时才有代表性。集群异构时,灰度应按风险维度选样本。


补丁管理最终要维护的不是“最新版本”这一数字,而是一个可解释的系统状态:

哪些漏洞已修复?
哪些包已安装?
哪些进程仍使用旧代码?
哪个内核正在运行?
哪些服务已通过业务验证?
出现故障时能恢复到哪个状态?
恢复所需时间是否满足业务目标?

当这些问题都有版本、日志、监控和演练结果作为证据时,安全更新、内核重启、发行版升级、灰度扩散和回滚才构成一个真正可控的生产流程。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。