Linux-02 包管理器:从 apt、yum、dnf 的使用到离线安装与事务原理
上一篇建立了 Linux 内核、发行版和命令来源的坐标系。接下来马上会遇到一个最实际的问题:软件怎样进入系统?
apt install nginx 看起来只有一行,背后却不是“从网站下载压缩包再解压”这么简单。包管理器必须决定下载哪个版本,计算完整依赖,验证文件来自可信仓库,处理已有配置,按顺序执行安装脚本,更新本地数据库,还可能创建用户、刷新动态链接缓存、注册甚至启动 systemd 服务。
离线环境会把这些隐藏步骤全部暴露出来:为什么只带走 nginx.deb 仍然装不上?为什么同为 x86_64,Ubuntu 24.04 的包不一定能装到 22.04?为什么复制一个仓库目录后客户端仍说元数据损坏?
先回答五个问题:
dpkg与apt、rpm与dnf/yum分别负责什么?apt update为什么不安装任何软件,却必须经常执行?- 软件包怎样证明“它来自可信仓库,且传输后没有被替换”?
- 无网络机器怎样获得一个软件及其完整依赖闭包?
- 一次安装到底改了哪些文件、执行了哪些脚本,失败到一半又怎样恢复?
1. 先建立分层:包格式不等于包管理器
主流 Linux 有两套常见生态:
| 生态 | 包文件 | 底层包工具/数据库 | 高层依赖管理器 | 常见发行版 |
|---|---|---|---|---|
| Debian | .deb |
dpkg |
apt、apt-get |
Debian、Ubuntu |
| RPM | .rpm |
RPM | dnf,旧系统常用 yum |
RHEL、Rocky、Alma、Fedora、CentOS |
| Alpine | .apk |
apk-tools | apk |
Alpine |
| Arch | .pkg.tar.zst |
pacman 数据库 | pacman |
Arch Linux |
| SUSE | .rpm |
RPM | zypper |
openSUSE、SLES |
最容易混淆的是“底层安装”和“高层求解”:
apt / dnf
-> 知道有哪些仓库和版本
-> 选择候选版本
-> 求解依赖、冲突、替代关系
-> 下载并校验包
-> 组织安装事务
|
v
dpkg / RPM library
-> 读取本地包
-> 解包文件
-> 运行维护脚本
-> 更新本地包数据库
可以这样记:
dpkg、RPM 回答“这个本地包怎样落到机器上”;apt、dnf回答“为了得到目标软件,需要哪些包、选哪个版本、从哪里安全下载、按什么顺序安装”。
所以直接执行:
sudo dpkg -i app.deb
sudo rpm -Uvh app.rpm
底层工具可以发现依赖缺失,却通常不会像高层工具那样从仓库求解并下载完整依赖。更合适的本地包安装方式是:
sudo apt install ./app.deb
sudo dnf install ./app.rpm
注意前面的 ./:它告诉 apt/dnf 这是本地路径,而不是仓库中的包名。
1.1 yum 到 dnf 发生了什么
yum 长期是 RHEL/CentOS 用户熟悉的高层工具。后来 Fedora/RHEL 生态以 dnf 替代它,主要改善依赖求解、API、性能和可维护性。现代 RHEL 系中,yum 命令常只是兼容入口,底层实际调用 DNF。
command -V yum
readlink -f "$(command -v yum)"
yum --version
dnf --version
但不能据此假设所有系统都一样:CentOS 7 的经典 Yum 与 RHEL 9 的兼容命令并非同一实现。写自动化脚本时先读取 /etc/os-release,再确认工具能力,而不是只凭命令名判断。
1.2 apt 与 apt-get 为什么同时存在
apt 面向交互使用,输出友好、会显示进度;apt-get 和 apt-cache 的接口更稳定,传统上更适合脚本。
# 人工操作
sudo apt update
sudo apt install nginx
apt search nginx
# 自动化中常见
sudo apt-get update
sudo apt-get install -y --no-install-recommends nginx
apt-cache policy nginx
脚本不能只加 -y 就算可靠,还要固定源、处理错误、避免交互式配置,并验证最终状态。
2. 先识别系统、版本和架构
在复制命令前先确认环境:
cat /etc/os-release
uname -m
# Debian 包架构
dpkg --print-architecture 2>/dev/null
# RPM 架构
rpm --eval '%{_arch}' 2>/dev/null
三个维度都影响能否安装:
发行版家族:Debian 生态还是 RPM 生态
发行版版本:Ubuntu 22.04、24.04;RHEL 8、9
CPU 架构:amd64/x86_64、arm64/aarch64
“都是 Linux”和“都是 x86_64”远远不够。一个包还可能依赖:
- 特定 glibc、OpenSSL 或其他共享库 ABI;
- 特定 Python/Perl 运行时;
- systemd、内核功能或文件系统布局;
- 发行版提供的虚拟包名和默认配置;
- 同仓库中精确版本的其他包。
# 看 ELF 二进制需要哪些共享库
readelf -d /usr/bin/curl | grep NEEDED
ldd /usr/bin/curl
# 看某个符号版本需求
readelf --version-info /usr/bin/curl | less
Alpine 使用 musl,Debian/RHEL 通常使用 glibc。即使 CPU 架构一致,在 glibc 环境构建的动态链接程序也不一定能直接放进 Alpine。
3. 日常使用:搜索、安装、升级和卸载
3.1 Debian/Ubuntu:apt 与 dpkg
# 刷新仓库元数据,不安装软件
sudo apt update
# 搜索并查看候选包
apt search '^nginx$'
apt show nginx
apt-cache policy nginx
# 安装;显式指定版本时便于复现
sudo apt install nginx
sudo apt install nginx=1.24.0-2ubuntu7.1
# 仅下载,不安装
apt download nginx
# 卸载程序,通常保留系统级配置
sudo apt remove nginx
# 连同由包管理器跟踪的配置文件一起删除
sudo apt purge nginx
# 删除自动安装且不再被需要的依赖
sudo apt autoremove
# 清理下载的 deb 缓存
sudo apt clean
remove 与 purge 的区别不是“删得干不干净”这么简单。purge 只知道包元数据标记为 conffile 的配置;程序运行后生成的数据、日志和用户文件通常不会自动删除。删除数据库包更不应该期待包管理器替你判断哪些业务数据可丢。
3.2 RHEL/Rocky/Alma/Fedora:dnf 与 rpm
# 建立或刷新缓存
sudo dnf makecache
# 查询
sudo dnf search nginx
dnf info nginx
dnf list --showduplicates nginx
# 安装、升级、卸载
sudo dnf install nginx
sudo dnf upgrade nginx
sudo dnf remove nginx
sudo dnf autoremove
# 仅下载,download 子命令通常来自 dnf-plugins-core
sudo dnf download nginx
sudo dnf clean all
旧系统可能使用:
sudo yum makecache
sudo yum install nginx
sudo yum update nginx
sudo yum remove nginx
生产脚本中不要把 yum 和 dnf 的所有选项想当然地视为完全相同,应以目标发行版的手册和测试结果为准。
3.3 update、upgrade 到底更新什么
APT 中两个动作必须分开理解:
apt update
下载仓库索引,让本机知道“当前有哪些包和版本”
apt upgrade
根据刚获得的元数据,升级本机已安装的软件包
因此下面这个 Dockerfile 有竞态:
RUN apt-get update
RUN apt-get install -y curl
第一层可能被构建缓存复用,而镜像中的旧索引对应的包已经从镜像站移走,第二层就会 404。应把相关动作放在同一层:
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
apt upgrade 通常不主动删除已安装包;apt full-upgrade 为完成依赖变化可以安装或删除包,影响更大。DNF 中 upgrade 是现代推荐表达,update 常作为兼容别名,但仍应以发行版版本为准。
不要在生产机器上无审查地执行全量升级:
# 先看计划,而不是立刻确认
apt list --upgradable
dnf check-update || rc=$? # dnf 用 100 表示“有更新”,不等于执行失败
内核、glibc、OpenSSL、数据库客户端库和 systemd 的更新可能需要重启进程甚至主机。文件已经升级,不代表正在运行的进程已经加载新库。
4. 查询能力:线上排查比安装更常用
4.1 这个文件属于哪个包
# Debian
dpkg -S /usr/bin/ssh
# openssh-client: /usr/bin/ssh
# RPM
rpm -qf /usr/bin/ssh
# openssh-clients-...
如果查不到,可能是:
- 管理员手工复制到了
/usr/bin; - 程序安装在
/usr/local或/opt; - 文件已经被删除但进程仍持有;
- 查询的是符号链接解析前/后的不同路径;
- 包数据库损坏或目标位于容器、chroot 的另一根目录。
这正是为什么不要手动往 /usr/bin、/usr/lib 塞文件。它们属于发行版包管理器;手工覆盖后,升级可能无提示替换文件,也无法可靠卸载。
4.2 一个包安装了哪些文件
# 已安装的 Debian 包
dpkg -L openssh-client
dpkg -s openssh-client
# 查看本地 deb,不安装
dpkg-deb -c ./app.deb
dpkg-deb -I ./app.deb
# 已安装的 RPM 包
rpm -ql openssh-clients
rpm -qi openssh-clients
# 查看本地 rpm,不安装
rpm -qpl ./app.rpm
rpm -qpi ./app.rpm
4.3 哪个包能提供尚未安装的文件
# Debian:apt-file 需要先建立自己的索引
sudo apt install apt-file
sudo apt-file update
apt-file search 'bin/htpasswd'
# DNF
dnf provides '*/htpasswd'
dnf repoquery --whatprovides '/usr/bin/htpasswd'
4.4 文件有没有被修改
# Debian:检查有 md5sums 记录的普通文件
sudo dpkg -V openssh-client
# RPM:校验大小、权限、摘要、属主等
sudo rpm -V openssh-clients
RPM 验证输出不是简单的“正常/异常”,每个字符对应 size、mode、digest、owner 等属性。配置文件被管理员有意修改也会报告差异,必须结合文件类型和变更记录解释。
包校验能发现与包数据库记录不同,却不能证明机器安全:攻击者可能同时修改数据库,也可能只改变运行时状态、内核或未被包跟踪的数据。
5. 仓库和镜像源到底是什么
仓库不是只有一堆 .deb 或 .rpm 的 HTTP 目录。一个可用仓库至少包含:
包文件
包名、版本、架构和依赖元数据
文件路径或能力索引
哈希值
仓库级签名及其信任信息
组件、发行版或模块信息
客户端通常先下载体积较小的元数据,在本地完成查询和依赖求解,最后只下载选中的包。
5.1 APT 软件源
现代 Debian/Ubuntu 可使用传统单行格式,也可使用 deb822 格式。传统示例:
deb [arch=amd64 signed-by=/usr/share/keyrings/company-archive-keyring.gpg] https://mirror.example.com/ubuntu jammy main universe
各字段含义:
deb 二进制包仓库
aarch/amd64 限定架构
signed-by 只让此仓库信任指定 keyring
URL 仓库基址
jammy suite/codename
main universe component
常见位置:
/etc/apt/sources.list
/etc/apt/sources.list.d/*.list
/etc/apt/sources.list.d/*.sources
/usr/share/keyrings/
/etc/apt/keyrings/
查看最终版本选择:
apt-cache policy
apt-cache policy nginx
apt-cache madison nginx
apt-cache depends nginx
apt-cache rdepends nginx
apt-cache policy 很重要:它能解释同一个包为什么从某个源选中某个版本,而不是另一个源的版本。
5.2 DNF/YUM 软件源
仓库配置通常位于 /etc/yum.repos.d/*.repo:
[company-appstream]
name=Company AppStream
baseurl=https://mirror.example.com/rhel/9/appstream/$basearch/os/
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-company
常用命令:
dnf repolist --all
dnf repoinfo company-appstream
dnf list --showduplicates nginx
dnf repoquery --requires --resolve nginx
dnf repoquery --whatprovides 'libssl.so.3()(64bit)'
gpgcheck=1 通常检查 RPM 包签名;repo_gpgcheck=1 控制仓库元数据签名检查,具体支持和默认值依发行版而异。企业镜像不应为了“先装上再说”统一关闭验证。
5.3 换镜像源不是越近越好
评估镜像应关注:
- 是否确实对应当前发行版和版本;
- 同步延迟和完整性;
- HTTPS、仓库签名和 key 分发方式;
- 是否提供所需架构、组件、debug/source 包;
- 是否有 SLA、审计、快照和版本保留;
- 上游删除旧版本时,构建能否复现。
随机执行网上的“换源脚本”可能覆盖所有源、导入过宽的信任 key,甚至把测试仓库和正式仓库混在一起。修改前先保存原配置并检查差异:
sudo cp -a /etc/apt/sources.list.d /etc/apt/sources.list.d.bak
sudo cp -a /etc/yum.repos.d /etc/yum.repos.d.bak
备份只是示意;配置管理系统中更应该以版本化模板和审查流程管理。
6. 信任链:HTTPS 不能替代包签名
HTTPS 保护客户端到当前服务器之间的传输,但仓库内容可能经过镜像同步、对象存储、CDN 和缓存。包管理还需要验证“发布者认可了这组元数据”。
APT 的简化信任链:
发行版/仓库签名密钥
-> 签名 Release/InRelease
-> Release 记录 Packages 索引哈希
-> Packages 记录每个 deb 的哈希
-> 下载 deb 后逐级校验
RPM 生态通常对 RPM 包签名,仓库还可对 repodata 进行签名:
受信任 GPG key
-> 验证 repository metadata 和/或 RPM signature
-> metadata 给出依赖与包校验信息
-> 下载后验证再进入事务
查看本地包签名:
# RPM
rpm -Kv ./app.rpm
# Debian 包自身常见的完整性信息不等同于 APT 仓库发布签名;
# 正常情况下应让 apt 从已签名仓库获取和验证。
dpkg-deb -I ./app.deb
现代 APT 应为每个第三方源使用独立 signed-by keyring,避免把某个第三方 key 变成对所有仓库都有效的全局信任。旧教程常见的 apt-key add 已不再是推荐设计。
不要这样“修复”签名错误:
trusted=yes
--allow-unauthenticated
gpgcheck=0
签名失败可能意味着系统时间错误、key 过期、源配置错、镜像半同步或真实的内容篡改。正确做法是确定失败层级和 key 来源,而不是关闭防线。
7. 依赖解析:安装目标不是一个包,而是一组约束
一个包可以声明:
Depends/Requires 必须满足
Pre-Depends 必须在解包/配置前满足
Recommends/Suggests 推荐或可选
Conflicts 不能共存
Breaks/Obsoletes 与旧包版本不兼容或替代
Provides 提供某个虚拟能力
例如用户请求安装 A:
A requires B >= 2
A requires TLS implementation
B requires C = 3
D provides TLS implementation
E conflicts with B < 2.5
求解器需要从多个仓库、多个架构和多个版本中找到一组同时满足约束的解,而不是递归下载文件名。
7.1 什么是依赖闭包
若:
A -> B, C
B -> D
C -> D, E
A 的依赖闭包是 {A, B, C, D, E}。但“在联网机器上查看闭包”仍不等于得到离线目标所需的包,因为联网机器可能已经安装 D,于是下载工具认为无需再下载;目标机却没有 D。
离线准备必须基于:
- 与目标机相同的发行版、版本和架构;
- 目标机已安装包清单,或一套干净且等价的根文件系统;
- 固定时间点的仓库快照;
- 安装时相同的推荐依赖策略和模块流配置。
7.2 版本选择、hold 和排除
Debian 系:
apt-cache policy nginx
sudo apt-mark hold nginx
apt-mark showhold
sudo apt-mark unhold nginx
DNF 系:
# versionlock 通常由插件提供
sudo dnf versionlock add nginx
sudo dnf versionlock list
版本锁定能减少无意升级,却也可能阻塞安全修复。任何 hold/versionlock 都应有负责人、原因和到期条件。
8. 一次 apt install 到底发生了什么
以 apt install nginx 为例,概念流程是:
1. 读取 sources、preferences 和本地状态
2. 加载 /var/lib/apt/lists 中的仓库索引
3. 选择候选版本并求解依赖、冲突
4. 向用户展示将安装/升级/删除的集合
5. 下载 deb 到缓存并校验
6. 获取 dpkg 前端锁,防止并发事务
7. 调用 dpkg 按依赖顺序 preconfigure、unpack、configure
8. 处理 conffile 和维护脚本
9. 执行 trigger,如刷新共享缓存或手册索引
10. 更新 dpkg 状态数据库并释放锁
关键目录:
/etc/apt/sources.list* 仓库配置
/etc/apt/preferences* pinning 规则
/var/lib/apt/lists/ 仓库索引
/var/cache/apt/archives/ 已下载 deb
/var/lib/dpkg/status 已安装包状态
/var/lib/dpkg/info/ 文件清单、脚本、校验等
查看某个已安装包的维护信息:
ls -l /var/lib/dpkg/info/nginx*
sudo less /var/lib/dpkg/info/nginx-common.postinst
不要在不理解影响时手工运行这些脚本,它们假设自己处于 dpkg 事务的特定状态。
8.1 deb 不是普通 tar 包
.deb 是 ar 容器,通常包含:
debian-binary
control.tar.* 元数据、依赖、维护脚本、conffiles 等
data.tar.* 真正要落盘的文件
只读检查:
ar t ./app.deb
dpkg-deb -I ./app.deb
dpkg-deb -c ./app.deb
mkdir inspect-deb
dpkg-deb -e ./app.deb inspect-deb/DEBIAN
dpkg-deb -x ./app.deb inspect-deb/root
-e 提取控制信息,-x 提取数据。检查陌生包时可以先在普通目录展开,不要直接以 root 安装。
8.2 unpack 与 configure 是两个阶段
Debian 包可能处于:
not-installed
unpacked
half-configured
installed
config-files
“文件已经出现在 /usr/bin”不代表包配置完成。postinst 失败后,文件可能已落盘,但用户创建、配置生成、trigger 或服务处理尚未完成。这就是半安装状态存在的原因。
维护脚本常见阶段:
preinst 解包前
postinst 解包后配置时
prerm 删除前
postrm 删除后
脚本可能创建系统用户、迁移配置、调用 ldconfig、更新 initramfs、通知 systemd 或启动服务。安装包因此是带 root 副作用的代码执行,不是单纯复制文件。
9. 一次 dnf install 到底发生了什么
DNF 的概念流程类似:
1. 读取 /etc/dnf 与 /etc/yum.repos.d
2. 加载缓存或下载 repodata
3. 由依赖求解器选择包集合
4. 下载 RPM 并验证 checksum/signature
5. 建立 RPM transaction
6. 检查文件冲突、磁盘空间和依赖
7. 按事务顺序执行 scriptlet、解包与升级
8. 处理 trigger,更新 RPM 数据库
9. 记录 DNF history
常见位置:
/etc/yum.repos.d/*.repo 仓库配置
/etc/dnf/ DNF 配置
/var/cache/dnf/ 元数据和包缓存
/var/lib/rpm/ RPM 数据库(具体布局依版本变化)
查看 RPM 内脚本:
rpm -qp --scripts ./app.rpm
rpm -q --scripts nginx
RPM scriptlet 常见类型包括 %pre、%post、%preun、%postun 和 trigger。升级不是机械的“先删旧包再装新包”,脚本收到的参数和执行顺序有明确语义;自己制作包时必须测试首次安装、升级、降级、删除和失败恢复。
9.1 事务不等于业务级原子性
包管理器会尽力维护包数据库与文件状态,但不能把所有外部副作用变成数据库事务:
- 脚本已经创建用户后失败;
- 已执行数据库格式迁移;
- 服务启动后向外部系统注册;
- initramfs 已生成到一半;
- 配置脚本访问网络超时;
- 磁盘在解包过程中耗尽。
因此 dnf history undo 或重新执行 dpkg --configure -a 是恢复工具,不是万能时光机。关键系统升级仍需要快照、备份、可重建镜像和应用级回滚方案。
10. 配置文件为什么升级时会询问
包管理器既要发布新的默认配置,又不能悄悄覆盖管理员修改。
Debian 的 conffile 机制会记录配置文件摘要。新版本提供的新文件与本机修改发生冲突时,可能询问:保留本地版本还是安装维护者版本。非交互自动化若没有明确策略,就可能卡住或意外覆盖配置。
RPM 常通过 %config、%config(noreplace) 表达配置语义,升级时可能产生:
.rpmnew 新配置另存,继续保留本地文件
.rpmsave 本地旧配置另存,新配置占据正式路径
升级后检查:
sudo find /etc -type f \( -name '*.dpkg-*' -o -name '*.rpmnew' -o -name '*.rpmsave' \) -print
更稳妥的生产模式是:
软件包提供默认配置
配置管理系统管理环境配置
升级前做差异检查
升级后做语法验证和冒烟测试
不要直接修改 /usr/lib/systemd/system/*.service;它通常属于软件包,下次升级会覆盖。使用:
sudo systemctl edit myapp
systemctl cat myapp
创建 /etc/systemd/system/myapp.service.d/*.conf drop-in。
11. 本地安装:手里只有一个 deb 或 rpm
11.1 Debian
# 先检查,不安装
dpkg-deb -I ./app_1.2.3_amd64.deb
dpkg-deb -c ./app_1.2.3_amd64.deb
# 让 apt 把本地包放进依赖求解
sudo apt install ./app_1.2.3_amd64.deb
若之前用了 dpkg -i 而留下缺失依赖:
sudo dpkg --audit
sudo apt --fix-broken install
sudo dpkg --configure -a
这些命令可能继续安装、配置或删除包,执行前先阅读计划。联网时 --fix-broken 可能下载依赖;真正离线时必须事先把依赖放入可用仓库或一并提供。
11.2 RPM
# 检查包信息、依赖和签名
rpm -qpi ./app-1.2.3-1.x86_64.rpm
rpm -qpR ./app-1.2.3-1.x86_64.rpm
rpm -Kv ./app-1.2.3-1.x86_64.rpm
# 推荐让 dnf 处理本地包和依赖
sudo dnf install ./app-1.2.3-1.x86_64.rpm
rpm -i 是首次安装,rpm -U 支持安装或升级。直接使用 --nodeps、--force 绕过依赖和文件冲突,常会把“当前装不上”变成“系统以后无法一致升级”。除非正在受控修复且能解释每项不变量,否则不要用这些开关。
12. 离线安装不是复制顶层包
离线场景分三档:
一次安装一个简单程序
-> 下载本地包和完整依赖闭包
少量机器、固定软件集合
-> 制作离线 bundle,并生成校验清单
大量机器或长期隔离环境
-> 建立签名的内网仓库/镜像快照
12.1 准备环境必须与目标一致
推荐在与目标机匹配的虚拟机、容器根文件系统或 chroot 中求解:
同发行版
同大版本/小版本策略
同架构
同仓库集合和优先级
同模块流(若适用)
已安装包基线可控
不能在一台已经装满开发依赖的机器上运行“只下载缺少依赖”,再假设干净目标机也能安装。
收集目标基线:
# Debian
dpkg-query -W -f='${binary:Package}\t${Version}\n' | sort > packages.tsv
# RPM
rpm -qa --qf '%{NAME}\t%{EPOCHNUM}:%{VERSION}-%{RELEASE}\t%{ARCH}\n' | sort > packages.tsv
12.2 Debian 离线下载
下载单包:
mkdir -p bundle/debs
cd bundle/debs
apt download nginx
这只下载目标包,不自动保证闭包完整。可在干净、等价环境中让 APT 下载一次真实安装事务:
sudo apt-get update
sudo apt-get install --download-only nginx
ls /var/cache/apt/archives/*.deb
但缓存里可能混有旧包,且“已安装依赖”不会被重新下载。更可靠的方法是使用专门的仓库快照/镜像工具,或者建立一套与目标基线一致的隔离 root,让求解器输出确定事务,再复制事务所需的全部包。
可以先观察安装计划:
apt-get -s install nginx
apt-cache depends --recurse --no-recommends nginx
apt-cache depends 是分析工具,不是完整离线证明:虚拟包、版本选择、已安装状态、推荐依赖和多架构都会影响真实事务。
传输后生成本地索引:
cd bundle/debs
dpkg-scanpackages . /dev/null | gzip -9c > Packages.gz
然后用 HTTP 提供,或临时配置可信的本地 file: 源。正式环境还应生成 Release 元数据并签名,不能因“内网”就长期使用不验证的源。
12.3 RPM 离线下载
安装插件后,可在匹配环境中下载依赖:
sudo dnf install dnf-plugins-core
mkdir -p bundle/rpms
sudo dnf download \
--resolve \
--alldeps \
--destdir bundle/rpms \
nginx
--resolve 解析依赖,--alldeps 避免仅因准备机已安装某依赖就跳过下载;具体行为仍应在目标发行版验证。
建立本地 RPM 仓库:
createrepo_c bundle/rpms
示例 repo:
[offline-bundle]
name=Offline Bundle
baseurl=file:///opt/offline-bundle/rpms
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-company
如果 bundle 中保留的是发行版原签名包,可继续验证包签名;如果企业自行重打包,则必须建立受控的签名和 key 分发流程。
12.4 离线 bundle 应包含什么
一个可审计 bundle 不应只有包文件:
packages/ 所有包
repository metadata Packages/repodata
manifest 包名、版本、架构、来源
SHA256SUMS 传输完整性
signature 对清单或仓库元数据签名
public key procedure 如何可信导入 key
install plan 预计新增、升级、删除什么
rollback plan 快照/备份/重建路径
validation script 安装后验证
生成传输校验清单:
find packages -type f -print0 \
| sort -z \
| xargs -0 shasum -a 256 > SHA256SUMS
shasum -a 256 -c SHA256SUMS
哈希只能发现内容变化;若攻击者可同时替换包和 SHA256SUMS,它不提供身份认证,所以还需要可信渠道传递的签名或公钥。
13. 长期离线环境:做仓库镜像,而不是反复搬文件
13.1 三种企业模式
公共上游直连
优点:简单、更新快
风险:外部可用性、限流、上游变化
代理缓存
第一次从上游取,后续由内网复用
优点:省带宽、部署轻
风险:不能保证从未请求过的包离线可用
完整镜像/受选仓库快照
同步需要的 suite/component/arch 和元数据
优点:可离线、可复现、可审计
代价:容量、同步、签名和生命周期治理
“当前最新镜像”不等于“可复现”。今天构建成功,明天上游替换元数据或移除旧包,完全相同的 Dockerfile 仍可能得到不同结果。生产环境更需要按时间或发布批次固定仓库快照。
13.2 安全发布镜像
同步不能直接覆盖客户端正在读取的目录:
1. 同步到 staging/2026-08-12
2. 校验元数据、包数量、签名和抽样安装
3. 生成不可变版本目录
4. 原子切换 current 指针或 Web 路由
5. 保留上一版本供回滚
6. 客户端按批准窗口刷新元数据
如果先发布新元数据、包文件还没同步完,客户端会按索引请求不存在的包;如果先覆盖包、索引仍旧,也可能出现校验不一致。仓库发布本身也是一个需要原子边界的系统设计问题。
13.3 不要无限镜像所有内容
镜像策略要明确:
- 哪些发行版和仍受支持的版本;
- 哪些架构;
- binary、source、debug 包是否都需要;
- 保留多少快照;
- 安全更新同步时延;
- EOL 仓库如何隔离;
- 谁能把包从测试推广到生产;
- 漏洞紧急修复如何走快速通道。
内网镜像如果永远不更新,会把网络隔离变成长期漏洞冻结。
14. 包安装怎样影响 systemd
安装服务包通常会放置:
/usr/bin 或 /usr/sbin 的程序
/usr/lib/systemd/system/*.service
/etc 下默认配置
/var/lib 下状态目录
system user/group
tmpfiles.d、sysusers.d、logrotate 等规则
但以下三个动作必须区分:
install 文件和元数据进入系统
start 当前立即运行服务
enable 建立开机启动关系
检查状态:
systemctl is-enabled nginx
systemctl is-active nginx
systemctl cat nginx
rpm -qf /usr/lib/systemd/system/nginx.service 2>/dev/null
dpkg -S /lib/systemd/system/nginx.service 2>/dev/null
不同发行版包策略可能在 postinst/%post 中启动或重启服务。制作基础镜像、chroot 或离线 root 时,通常不希望安装阶段启动服务,必须使用发行版支持的抑制机制,而不是靠“反正 systemd 没运行”碰运气。
升级共享库后,已有进程仍映射旧 inode:
sudo lsof +L1
sudo grep -l '(deleted)' /proc/[0-9]*/maps 2>/dev/null | head
因此包升级后的验证应包括哪些服务需要重启,而不只是“apt/dnf 返回 0”。
15. 与 Go、cgo 和动态链接的关系
纯 Go 程序常能构建为较独立的静态二进制,但以下情况会重新依赖系统包:
- 使用 cgo;
- 链接 OpenSSL、SQLite、Kerberos、ODBC 等 C 库;
- DNS、用户查询走 libc/NSS;
- 依赖 CA 证书、时区数据库或系统配置;
- 调用外部命令。
编译期包和运行期包常不同:
# Debian 示例,名称因版本而异
sudo apt install build-essential libsqlite3-dev
# RPM 示例
sudo dnf install gcc sqlite-devel
*-dev/*-devel 通常提供头文件、未版本化链接名和 pkg-config 元数据;运行镜像只需实际共享库。多阶段构建可避免把编译器带进生产镜像:
FROM golang:1.24-bookworm AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -o /out/app ./cmd/app
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates tzdata \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /out/app /usr/local/bin/app
USER 65532:65532
ENTRYPOINT ["/usr/local/bin/app"]
若 CGO_ENABLED=1,检查真正的运行依赖:
file /out/app
ldd /out/app
readelf -d /out/app | grep NEEDED
不要简单把构建机上的 .so 复制到镜像。共享库还涉及 soname、加载器、ABI、许可证和安全更新,应优先让目标发行版的包管理器管理。
16. 容器里的包管理原则
容器并没有取消包管理,只是把安装时间从“服务器运行期”前移到“镜像构建期”。
推荐:
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
RPM 基础镜像类似:
RUN dnf install -y --setopt=install_weak_deps=False curl \
&& dnf clean all \
&& rm -rf /var/cache/dnf
原则:
update与install放在同一层;- 不安装不需要的推荐/弱依赖;
- 删除索引和缓存只为减小镜像,不影响已安装文件;
- 固定基础镜像 digest 和仓库快照才能提高可复现性;
- 不在容器启动脚本中临时
apt install; - 调试工具放入单独 debug 镜像,不让生产镜像无限膨胀;
- 升级依赖应重建、测试并重新发布镜像,而非进入运行容器手改。
需要注意 Docker 层:先在一层下载,下一层再删除,旧层仍保存文件,镜像不会真正变小。这就是安装和清理应在同一个 RUN 中完成的原因。
17. 安装失败和半配置状态怎么排查
先保留完整错误,不要一看到 lock 或 dependency 就删除数据库文件。
17.1 Debian 系
sudo dpkg --audit
sudo dpkg --configure -a
sudo apt-get check
sudo apt --fix-broken install
# 查看近期操作历史
less /var/log/apt/history.log
less /var/log/dpkg.log
常见原因:
仓库索引过期,包 URL 已变化
依赖被 hold 或版本 pin 阻塞
磁盘空间/inode 耗尽
维护脚本失败
配置脚本需要交互输入
文件系统只读
包架构不匹配
多个前端争用锁
镜像源处于半同步状态
17.2 RPM/DNF 系
sudo dnf check
sudo dnf history
sudo dnf history info last
rpm -qa | sort
sudo rpm -Va
rpm -Va 在大系统上输出可能很多,且配置差异未必是故障。先围绕受影响包验证:
rpm -V nginx
rpm -q --requires nginx
dnf repoquery --unsatisfied
17.3 不要随便删除锁文件
锁的意义是避免两个事务同时更新包数据库和系统文件。看到锁错误先找持有者:
ps aux | grep -E '[a]pt|[d]pkg|[d]nf|[r]pm'
sudo lsof /var/lib/dpkg/lock-frontend 2>/dev/null
如果另一个事务确实在运行,等待它结束。若进程已经异常退出,应根据工具状态做恢复,而不是机械删除 /var/lib/dpkg/lock* 或 RPM 数据库。删除仍被活跃进程使用的锁会允许并发写,可能把可恢复问题升级成数据库损坏。
17.4 磁盘满时为什么容易失败
安装需要同时容纳:
仓库索引
下载缓存
新包解压后的文件
旧文件或备份
数据库临时文件
initramfs 等 trigger 产物
检查空间和 inode:
df -h /
df -ih /
du -xhd1 /var | sort -h
清缓存:
sudo apt clean
sudo dnf clean all
缓存清理不能修复已经处于半配置状态的包;释放空间后仍要重新运行审计、配置或检查命令。
18. 生产升级:从命令变成变更流程
一条可靠升级链路应是:
固定仓库快照
-> 在同基线测试环境求解事务
-> 审查新增/升级/删除列表
-> 检查安全公告与不兼容变更
-> 做快照/备份并定义回滚条件
-> 小批量灰度
-> 验证服务、配置、端口、指标和日志
-> 扩大批次
-> 记录最终版本与证据
升级前保存计划:
# Debian:模拟执行
apt-get -s upgrade
apt-get -s install nginx=VERSION
# DNF:先下载/查看事务,实际能力依版本与插件
sudo dnf upgrade --assumeno
sudo dnf history
验证不应只有“包版本对了”:
包数据库一致
配置语法通过
systemd unit active 且未重启循环
端口和健康检查正常
动态链接库为预期版本
错误率、P99、资源指标无回归
重启后仍能正常启动
回滚路径真实可用
18.1 为什么降级很危险
sudo apt install package=OLD_VERSION
sudo dnf downgrade package
命令存在不代表数据兼容。新版服务可能已经迁移数据库、重写缓存格式或生成旧版无法读取的配置。包管理器能降级文件,不会自动逆转业务数据。降级前必须阅读应用的兼容与迁移说明。
19. 自制包比 make install 好在哪里
直接执行:
./configure
make
sudo make install
通常把文件写入 /usr/local,但系统未必有完整清单,升级、查询、验证和卸载都依赖项目自身规则。对于需要分发到多台机器的软件,制作 deb/rpm 的价值在于:
- 明确文件清单、权限、属主;
- 声明版本、架构和依赖;
- 可查询、验证、升级和卸载;
- 用仓库签名和推广流程控制来源;
- 与 systemd、sysusers、tmpfiles 等发行版机制协作。
但自制包也意味着要承担维护责任:
版本规则
依赖边界
升级脚本幂等性
配置兼容
签名密钥安全
多个发行版版本的测试矩阵
漏洞修复与仓库保留策略
对于单个 Go 服务,也可以采用“一个二进制 + unit + 配置管理”的发布模式;是否制作系统包取决于组织部署标准,而不是技术上的绝对优劣。
20. 常见误区
20.1 apt update 会升级系统
不会。它更新可用包索引。真正改变已安装包的是 install、upgrade、full-upgrade 等事务。
20.2 rpm -i 和 dpkg -i 会自动联网补齐依赖
底层工具主要处理本地包。使用 dnf install ./x.rpm 或 apt install ./x.deb 才能让高层求解器结合仓库处理依赖。
20.3 复制目标包就是离线安装
目标包只是依赖图的根。必须准备与目标基线匹配的完整闭包、元数据、签名和安装计划。
20.4 内网仓库不需要签名
内网仍有误发布、账号泄漏、代理污染、存储篡改和供应链风险。TLS、访问控制、仓库签名和不可变快照解决的是不同问题。
20.5 包管理事务成功就代表服务升级成功
包数据库成功只证明包管理层完成。服务可能无法解析新配置、未重启到新库、健康检查失败或发生数据格式不兼容。
20.6 删除 lock 文件能修复安装
活跃事务仍持锁时删除路径不会安全终止它,反而可能让第二个事务并发进入。先识别进程和状态,再使用发行版支持的恢复流程。
20.7 curl URL | sudo sh 和包管理器一样
远程脚本可以任意执行且通常缺少文件清单、依赖事务、版本数据库和卸载路径。即使来源可信,也应先下载、固定版本、检查内容与校验信息,再在受控环境执行。
21. 实战:构建一个可审计的离线交付流程
假设要把 myagent 部署到不能联网的 50 台服务器。
21.1 准备阶段
1. 确认目标:Rocky 9.4 x86_64
2. 固定批准的 BaseOS/AppStream 快照
3. 在干净的同版本环境求解 myagent 事务
4. 下载 myagent 与全部依赖
5. 验证上游签名
6. 生成本地 repodata
7. 生成 manifest 与 SHA256SUMS
8. 对仓库元数据/交付清单签名
9. 用全新离线 VM 做首次安装、升级、卸载测试
21.2 发布阶段
1. 通过受控介质导入隔离区
2. 在入口重新校验介质和签名
3. 发布为不可变版本目录
4. 原子切换测试仓库地址
5. 小批量安装并检查 DNF transaction
6. 验证 systemd、日志、端口和指标
7. 批准后推广到生产仓库
21.3 回滚设计
包文件回滚:仓库保留上一版本
系统状态回滚:虚拟机/LVM/云盘快照
配置回滚:配置仓库版本
应用状态回滚:由应用迁移方案保证
发布回滚:仓库 current 原子切回
这套流程的重点不是“会执行 dnf install”,而是让任意一台机器都能回答:安装了什么、为什么选这个版本、文件从哪来、由谁批准、如何验证、失败后怎样恢复。
22. 排查速查表
| 现象 | 先看什么 | 常见原因 |
|---|---|---|
| 包不存在 | apt-cache policy、dnf repolist |
源未启用、架构/版本不匹配、索引未刷新 |
| 404 | apt update、仓库同步状态 |
本地索引旧、镜像半同步、上游已删除版本 |
| 签名失败 | 系统时间、key、仓库 Release/repodata | key 过期、源配错、内容损坏或遭篡改 |
| 依赖冲突 | 候选版本、hold/pin、模块流 | 混源、版本锁、跨发行版安装 |
| 锁被占用 | 持锁进程、自动更新服务 | 另一个事务正在运行或异常中断 |
| 安装后服务没起 | systemctl status、journal |
未 start、配置错、脚本失败、端口冲突 |
| 升级后仍显示旧行为 | 进程启动时间、maps、服务版本 | 老进程仍映射旧库或未重启 |
| 离线缺依赖 | manifest、目标基线、真实求解计划 | 只下载顶层包、准备机已有依赖 |
| 配置被改名 | .dpkg-*、.rpmnew/.rpmsave |
本地配置与维护者新版本冲突 |
| 安装到一半失败 | dpkg --audit、dnf history、磁盘 |
scriptlet、空间、只读文件系统、依赖问题 |
23. 面试题
23.1 apt 与 dpkg 有什么区别
dpkg 是 Debian 包的底层管理器,负责检查和解包本地 deb、执行维护脚本、维护已安装状态;apt 是高层前端,读取仓库元数据、选择候选版本、求解依赖、下载和校验,再组织 dpkg 事务。dpkg -i 遇到缺依赖会留下未配置状态,apt install ./x.deb 则可以结合已配置仓库补齐依赖。
23.2 dnf、yum 与 rpm 有什么区别
RPM 定义包格式并提供本地包数据库和事务能力;DNF/Yum 在其上管理仓库、依赖求解和下载。Yum 是历史上常见的高层管理器,现代 RHEL/Fedora 生态主要使用 DNF,很多新系统保留 yum 兼容命令,但不同发行版版本的具体实现不可混为一谈。
23.3 apt update 为什么不等于 apt upgrade
update 刷新 /var/lib/apt/lists 中的仓库索引,不修改已安装包;upgrade 根据索引和本地状态计算并执行升级事务。两者分离让管理员可以先看候选版本和计划,再决定何时变更系统。
23.4 软件仓库为什么需要元数据
客户端必须在下载大包前知道包名、版本、架构、依赖、冲突、能力和哈希,才能查询和求解。仓库元数据还建立签名到包哈希的信任链。只有包文件的目录既无法高效求解,也难以保证一致发布。
23.5 为什么 HTTPS 之外还需要 GPG 签名
HTTPS 只认证当前连接端点并保护传输,不能天然证明经过多级镜像、缓存和存储后的内容仍是发行者认可的发布集合。仓库签名让客户端依据预置的发行者 key 验证元数据,包哈希再验证具体下载内容。两者应共同使用。
23.6 为什么离线安装经常缺依赖
因为准备者只下载了顶层包,或者下载工具以准备机“已经安装某依赖”为由跳过了它。真实依赖还受版本、架构、虚拟能力、仓库优先级和目标基线影响。正确方法是在等价干净环境中求解完整事务,或同步带元数据和签名的仓库快照。
23.7 包安装为什么可能执行任意 root 代码
Deb/RPM 包包含 pre/post install、remove、trigger 等维护脚本。它们为创建用户、更新缓存、迁移配置和管理服务而设计,通常由 root 执行。因此签名验证和仓库信任极其重要,审计陌生包不能只看 data 文件,还要看控制信息和 scriptlet。
23.8 包管理事务是原子的吗
它对包数据库、依赖顺序和文件替换提供事务组织,但不能使所有外部副作用原子。脚本可能创建用户、迁移业务数据、启动服务或访问外部系统后失败。恢复需要包工具、系统快照和应用回滚共同完成。
23.9 为什么升级共享库后需要重启服务
运行进程通过 mmap 映射的是打开时的库 inode。包升级替换路径上的文件后,旧进程仍可继续执行已映射的旧代码;只有重启或重新 exec 才会加载新库。验证安全更新不能只看磁盘包版本,还要检查运行进程。
23.10 remove、purge 和删除数据有什么区别
APT remove 删除程序文件但通常保留 conffile,purge 还删除被包管理器标记的配置;两者通常都不会擅自删除应用运行后生成的业务数据。数据删除属于更高风险的生命周期操作,需要应用和管理员明确决定。
23.11 为什么不能直接删除包管理锁
锁保护包数据库和系统文件免受并发事务写入。错误可能表示另一个自动更新或人工安装仍在运行。直接删除锁路径不能停止持锁进程,反而可能允许第二个事务进入。应先识别持锁者,确认异常退出后再按工具支持的流程修复状态。
23.12 怎样设计企业内网镜像源
至少要有受控上游、按发行版/版本/架构的同步策略、签名验证、不可变快照、staging 校验、原子发布、旧版本保留、权限审计和安全更新 SLA。客户端从测试仓库逐级推广到生产仓库,而不是全部追随一个不断变化的 latest 目录。
24. 本篇小结
包管理器解决的不是“下载并解压”,而是 Linux 软件的完整供应和生命周期问题:
发行版与架构
-> 仓库元数据和候选版本
-> 依赖约束求解
-> 下载、哈希和签名验证
-> dpkg/RPM 安装事务
-> 配置与维护脚本
-> systemd、动态链接器和运行进程
-> 审计、升级、恢复与回滚
日常操作时记住四条主线:
- 先查询再修改:确认候选版本、来源、事务计划和文件归属;
- 高层工具解依赖,底层工具管本地包:不要靠
--force、--nodeps掩盖状态问题; - 离线交付的是依赖闭包和可信仓库,不是单个文件;
- 安装成功不等于服务成功:继续验证配置、进程、动态库、指标和重启行为。
下一篇进入 目录结构与路径解析:包管理器为什么把程序放到 /usr/bin、配置放到 /etc、状态放到 /var/lib,管理员手工安装的软件又为什么应该进入 /usr/local 或 /opt。
xingliuhua