Series · Linux · Chapter 4

Linux 软件包管理

跨发行版的包管理实战:Debian/Ubuntu 的 dpkg/apt、RHEL/Rocky 的 rpm/dnf、Arch 的 pacman,依赖排障、版本锁定、镜像源、源码编译与 Snap/Flatpak/AppImage 等现代方案。

很多人是从「装、删、升」三个动词开始学包管理的,平时也够用,直到出问题——依赖冲突装不上、升级以后服务起不来、内核换完机器进不去系统、国内拉镜像慢得想哭。这时候你需要的不是再背几条命令,而是一个心智模型:一个里到底装了什么、包管理器在背后到底求解什么、状态记录在哪里、Debian 系的 apt/dpkg 和 Red Hat 系的 dnf/rpm 在哪儿一致、又在哪儿分叉,凌晨两点登录线上机器才不至于慌。

这篇文章既给模型,也给配方。先讲一个 .deb.rpm 文件里装了什么、为什么需要包管理器;再把 aptdnfpacman 摊开横向对比——不只是「等价命令表」,还包括它们在依赖求解、版本锁定、降级、仓库信任链上各自的脾气。然后是只能踩坑学到的事:把国内镜像源真正配对、源码编译 Nginx(顺便讲清楚 configure/make/make install 三步到底干了什么)、用二进制压缩包扔下一个 JDK、以及一组能让生产机器维持「永远可升级」状态的运维习惯。

主流 Linux 包管理工具链

一个包里到底有什么

软件包是一个文件(.deb.rpm.pkg.tar.zst),里面打包了一份软件运行所需的全部内容,外加描述如何安装的元数据。元数据才是它和普通压缩包的区别——没有元数据,它就只是个 tarball。

具体到一个典型的服务端包,里面通常有六类东西:

1. 可执行文件。 编译好的二进制,按惯例放在 /usr/bin(普通用户命令)或 /usr/sbin(系统管理命令)。比如 /usr/bin/vim/usr/sbin/nginx/usr/bin/gcc

2. 配置文件。 软件自带的默认配置,几乎全在 /etc 下。包管理器对它们有特殊待遇:升级时不会覆盖你改过的配置apt 会弹出 Y/I/N/O/D/Z 提示,并把新版本另存为 .dpkg-distdnf 直接把新默认值存为 .rpmnew,你的文件原样不动。这条规则救过的线上事故比想象中多得多。

1
2
3
/etc/nginx/nginx.conf       # nginx 配置
/etc/mysql/my.cnf           # mysql 配置
/etc/ssh/sshd_config        # SSH 守护进程配置

3. 共享库。 即可执行文件运行时动态链接的 .so,相当于 Windows 的 DLL,放在 /usr/lib/lib 或发行版特有的路径下,比如 /lib/x86_64-linux-gnu/。共享库的好处是省盘省内存(一份库被所有进程共用),更重要的是——升级一次 libssl3 就能把全机所有 TLS 客户端一起打补丁。

1
2
/usr/lib/x86_64-linux-gnu/libssl.so.3   # OpenSSL
/lib/x86_64-linux-gnu/libc.so.6         # GNU C 库

4. 数据文件。 程序运行需要、但既不是代码也不是配置的东西:/var/lib/mysql 下的空数据库模板、/var/www/html 下的默认站点、locale 数据、示例证书等等。

5. 文档。 /usr/share/man/ 下的 man 手册,/usr/share/doc/<pkg>/ 下的 README 与 changelog。出问题时第一时间应该翻的不是搜索引擎,而是这里。

6. systemd 服务单元。 如果包带守护进程,会顺手把 .service 文件丢到 /usr/lib/systemd/system/,所以你装完立刻 systemctl enable nginx 就能用。这些文件归包所有;管理员要覆盖的话写到 /etc/systemd/system/ 下,免得升级被冲掉。

除了这些「文件载荷」,包元数据还记录了:包名与版本、依赖(.deb 里叫 Depends:.rpm 里叫 Requires:)、冲突关系、安装后脚本(比如 useradd nginx),以及每个文件的 SHA256。最后这条是后来 dpkg -Vrpm -V 能告诉你「哪些文件被动过」的依据。

没有包管理器会怎样

得这样过日子:每装一个软件都要

  1. 自己找下载链接(顺便祈祷源站是干净的);
  2. 自己解依赖——A 要 B 1.1+,B 要 C,C 又跟你装过的 D 冲突;
  3. 自己决定每个文件放在哪;
  4. 自己记下你拷过哪些文件,以后才能卸干净;
  5. 自己定期上游网查安全更新。

包管理器把这堆活换成了一个数据库/var/lib/dpkg/rpmdb)加一个求解器。数据库知道每一个已安装包占了哪些文件,求解器知道仓库里有什么、依赖怎么连,所有装、升、删都被规约成对这个数据库的一致变更。这就是包管理器的全部价值——而它的价值大得惊人。

主流的几个工具链

Linux 包管理几十年前就分成了若干派系,之后非常稳定。下面这张表是最该记的:

派系代表发行版包格式底层工具高层工具
DebianDebian / Ubuntu / Mint.debdpkgaptapt-get
Red HatRHEL / CentOS / Rocky / Alma / Fedora.rpmrpmdnf(EL7 上是 yum
ArchArch / Manjaro / EndeavourOS.pkg.tar.zstpacmanpacmanyay
SUSEopenSUSE / SLES.rpmrpmzypper
GentooGentoo源码 ebuild-portageemerge

实战中真正要分清的不是派系,而是底层 vs 高层。底层工具(dpkgrpm、裸 pacman -U)只对一个文件操作:解压、跑脚本、更新本地数据库——但不会主动去拉缺失依赖。高层工具(aptdnfpacman -Szypper)是同样的动作外面套了一层依赖求解器和远程仓库通信。绝大多数时候你应该用高层工具;只有当你要做的事情正好是高层工具不让你做的——比如硬装一个理论上冲突的包,或者把一个 .deb 拆开看里面有什么——才轮到底层工具上场。

后面主要讲 apt(Debian/Ubuntu)、dnf(RHEL/Rocky/Fedora)和 pacman(Arch),中间穿插底层 dpkg / rpm 在哪些场景下要用。

依赖求解:这才是真正难的部分

依赖求解图

包管理器最反直觉的地方是:它的代码大头不在「安装」,而在求解器apt install nginx 实际要做的事情是:

  1. 从本地缓存的 Packages 索引里读出 nginx 的元数据
  2. 遍历依赖图——nginx 依赖 libssl3libpcre2-8zlib1glibc6……每个又有自己的依赖,遇到「已安装且版本满足」就停;
  3. 统一版本。如果两条路径要的 libc6 版本不一样,求解器要么找一个能同时满足的版本,要么报冲突。共享库没有「两份都装」的选项——动态链接器按名字解析,不按路径,最终每个进程只会绑定到一份;
  4. 决定安装顺序,让「必须先装的依赖」真的先装,这样安装后脚本运行时不会找不到东西;
  5. 找不到一致解就大声失败,绝不留下半装不装的烂摊子。

绕开高层工具,求解就没了:

1
2
3
4
5
sudo dpkg -i nginx_1.24.0-1_amd64.deb
# dpkg: 处理 nginx 时出错: 依赖问题:
#  nginx 依赖 libssl3 (>= 3.0.0); 然而:
#   未安装 libssl3。
sudo apt install -f         # 让 apt 来收拾 dpkg 的烂摊子

apt install -f 的作用就是事后把求解器跑一遍来修复部分安装的状态。其实你完全可以一开始就让高层工具来处理本地文件:

1
2
sudo apt install ./nginx_1.24.0-1_amd64.deb     # apt 会从仓库里解依赖
sudo dnf install ./nginx-1.24.0-1.x86_64.rpm    # Red Hat 系同理

卸载也一样。如果你装了 nginx 和它的 libcache-extra,单独卸 libcache-extra 没问题;但卸 nginx 时,原本只是为了 nginx 才装上的 libpcre2-8,如果没人再依赖它,应该顺手清理掉。apt autoremovednf autoremove 干的就是这件事。

Debian / Ubuntu:apt 和 dpkg

三个工具怎么选

工具是什么用在哪儿
apt现代用户友好前端:彩色输出、进度条、合理默认值。交互式 shell、日常使用。
apt-get老前端,引擎相同,输出稳定、可被脚本解析。Shell 脚本、Ansible、Dockerfile。
dpkg底层工具,一次只处理一个 .deb,不解依赖。检查包内容、强制状态、应急修复。

记忆方法:apt = apt-get + 交互优化;apt-get = 依赖求解器包着 dpkgdpkg 才是真正解压文件、跑维护脚本的那一层。

日常命令

1
2
3
4
5
6
7
8
sudo apt update                       # 刷新仓库索引(不装任何东西)
sudo apt upgrade                      # 应用所有可升级包,绝不删包
sudo apt full-upgrade                 # 升级时如有必要可以删旧包
sudo apt install nginx                # 装 nginx(含依赖)
sudo apt install nginx=1.24.0-2ubuntu1   # 安装指定版本
sudo apt remove nginx                 # 卸载,但保留 /etc/nginx
sudo apt purge nginx                  # 卸载并清掉配置
sudo apt autoremove                   # 清理不再被依赖的包

新人最常踩的两条命令:

  • apt update 不装任何东西,它只是把镜像源上最新的 Packages 索引拉到本地,下一条 apt install 才能看到最新版本。「明明仓库有这个包却装不上」十有八九是没跑过 update
  • apt remove 保留 /etc。绝大多数场景这正是你要的(重装时还能恢复配置);但如果你是真要彻底下线一个服务,应该用 apt purge

搜索、查看、定位

1
2
3
4
5
6
7
apt search nginx                  # 在已配置的源里搜索
apt show nginx                    # 版本、依赖、描述、大小
apt-cache policy nginx            # 可装的候选版本 + 来自哪个源
dpkg -l                           # 列出所有已装包
dpkg -l | grep nginx              # nginx 装了吗
dpkg -L nginx                     # nginx 装了哪些文件
dpkg -S /usr/sbin/nginx           # 这个文件属于哪个包

后两条是排障神器。「这个包到底往磁盘上扔了什么文件?」「PATH 里这个不知道哪来的二进制是哪个包带的?」是事故调查的高频问题,而 Linux 上没有别的命令能像 dpkg -S 这样反查文件归属。

锁定、降级、冻结

1
2
3
4
5
6
7
8
sudo apt-mark hold nginx          # 不让 nginx 自动升级
apt-mark showhold                 # 当前哪些包被冻结
sudo apt-mark unhold nginx        # 解除冻结

# 降级三步走
apt-cache policy nginx                       # 1) 看可选版本
sudo apt install nginx=1.22.1-1ubuntu1       # 2) 装回老版本
sudo apt-mark hold nginx                     # 3) 冻结,下次升级才不会被冲

hold 是你应对「上游发了一个有问题的版本」的武器:钉住老版本,找个维护窗口再升级,不让自动化在半夜替你做决定。

清理

1
2
3
sudo apt clean        # 删 /var/cache/apt/archives 下所有缓存的 .deb
sudo apt autoclean    # 只删已经不在任何源里的过期 .deb
sudo apt autoremove   # 清理已不被依赖的孤儿包

我自己肌肉记忆里常用的一条组合:

1
sudo apt update && sudo apt upgrade -y && sudo apt autoremove -y && sudo apt autoclean

每周对每台机器跑一次,/var 不会平白被撑爆。

RHEL / Rocky / Fedora:dnf(顺带 yum 和 rpm)

dnf 在 CentOS 8 / RHEL 8 和 Fedora 22 替代了 yum,命令名几乎一一对应(这是有意保留的兼容性),但底层用 Python 重写,并换上了真正的 SAT 求解器(libsolv)。所以 dnf 在大事务上明显更快。CentOS 7 还在用 yum,其他地方都按 dnf 来。

日常命令

1
2
3
4
5
6
7
8
sudo dnf makecache                 # 刷新元数据
sudo dnf check-update              # 列出能升级的包
sudo dnf upgrade                   # 升级一切
sudo dnf install nginx             # 装
sudo dnf install nginx-1.24.0      # 装指定版本
sudo dnf remove nginx              # 卸
sudo dnf autoremove                # 清孤儿
sudo dnf downgrade nginx-1.22.1    # 原生降级(apt 没有等价命令)

dnf downgradednfapt 体验好的地方之一——apt 世界里降级是「装回老版本祈祷不冲突」,dnf 里它是一等公民。

搜索、查看、定位

1
2
3
4
5
6
7
8
9
dnf search nginx                   # 搜索
dnf info nginx                     # 详情
dnf list --showduplicates nginx    # 所有源里所有版本
rpm -qa                            # 所有已装包
rpm -qa | grep nginx               # 装了吗
rpm -ql nginx                      # nginx 装了哪些文件
rpm -qf /usr/sbin/nginx            # 这文件属于哪个包
rpm -qc nginx                      # 只看配置文件
rpm -V nginx                       # 校验文件有没有被改过

rpm -V 是被低估的工具。输出 S.5....T. c /etc/nginx/nginx.conf 的解读是:大小变了(S)、MD5 变了(5)、修改时间变了(T),且这是一个配置文件(c)。配置文件被改是正常的;但如果 /usr/sbin/ 下的二进制被改了——这就是排查入侵痕迹的入口。

锁定与历史

1
2
3
4
5
6
7
8
sudo dnf install 'dnf-command(versionlock)'   # 一次性安装插件
sudo dnf versionlock add nginx                # 锁定 nginx
dnf versionlock list                          # 查看
sudo dnf versionlock delete nginx             # 解锁

dnf history                                   # 这台机器上每一次事务
dnf history info 42                           # 第 42 号事务到底装了/删了什么
sudo dnf history undo 42                      # 把它整个回滚

dnf history 没有真正的 apt 等价物。它把每一次事务的时间戳、命令行、完整 diff 都记下来,并且任何一次都能回滚。在被多任管理员折腾过的服务器上,这是你还原「到底什么时候发生了什么」的唯一办法。

Arch:pacman

Arch 的 pacman 既是底层也是高层工具。Arch 是滚动发行,不存在「Arch 22.04」,只有「现在仓库长什么样」,所以工作流天然偏向「永远整体升级」:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
sudo pacman -Syu                  # 同步源 + 升级一切(Arch 的金科玉律)
sudo pacman -S nginx              # 装
sudo pacman -Ss nginx             # 搜
sudo pacman -Si nginx             # 仓库里这个包的信息
sudo pacman -Qi nginx             # 已装包的信息
sudo pacman -Ql nginx             # 文件列表
sudo pacman -Qo /usr/bin/nginx    # 文件归属
sudo pacman -R nginx              # 卸(保留依赖)
sudo pacman -Rns nginx            # 卸 + 顺便清孤儿依赖 + 清配置
sudo pacman -Sc                   # 清缓存

Arch 的铁律:永远不要部分升级。隔几周没跑 pacman -Syu 直接来一句 pacman -S nginx,你装的可能是依赖了新版本库的 nginx,而你机器上还是老库——结果就是 nginx 起不来。要么先 -Syu,要么干脆 pacman -Syu nginx 一步到位。

Arch User Repository(AUR)由社区维护构建脚本,前端工具如 yayparupacman 加上 makepkg 一锅端:拉源码、构建、安装。挺好用,但 AUR 的东西要按「我会读完 PKGBUILD 再装」的态度来对待。

一台机器上的包生命周期

包生命周期

抛开具体工具,每个包在你机器上都走同样的循环:搜 → 装 → 升(或冻结)→ 卸。包管理器把每个包的当前状态记在数据库里(Debian 是 /var/lib/dpkg/status,Red Hat 是 /var/lib/rpm/ 下的 rpmdb)。你跑的每一条命令,本质上都是对这个数据库的一笔事务,加上对磁盘的对应操作。

正因如此,「我手动 rm 掉那个二进制了」永远是错的:数据库里那个包还显示装着,下次升级会悄悄把文件放回来,dpkg -V / rpm -V 又会报「文件丢失」。要么把包正经卸掉(apt purge / dnf remove),要么——如果你确实需要保留包但屏蔽某个文件——用 dpkg-divert 这种正经手段。

仓库到底长什么样

仓库结构与信任链

apt update 时,机器走 HTTPS 连镜像,按一套很具体的目录结构去拉索引文件。理解这个结构,调试镜像问题就只是 curl 几条 URL 的事。

Debian 风格的 apt 仓库提供两棵树:

  • dists/<suite>/:每个 suite 的元数据。顶层文件是 Release(或者它的 inline-签名版本 InRelease),里面列出每个组件(mainuniverse 等)和架构,附上每个 Packages.gz 索引的 SHA256。Release.gpg 是分离的 GPG 签名。这棵树是信任的根。
  • pool/:真正的 .deb 文件,按源码包名分目录(pool/main/n/nginx/nginx_1.24.0-1_amd64.deb)。

apt install nginx 的信任链是:

  1. 你机器上 /etc/apt/trusted.gpg.d/ 里有一组你信任的 GPG 公钥(通过源里的 signed-by= 字段或老的 apt-key 放进去);
  2. apt updateInRelease,用上面那些公钥验签——验不过就直接拒绝使用整个 suite(NO_PUBKEY 错误就来自这一步);
  3. InRelease 拿到每个组件 Packages.gz 的 SHA256,拉下来再校验;
  4. Packages.gz 拿到 nginx_1.24.0-1_amd64.deb 的 SHA256,从 pool/ 拉下来校验,然后才交给 dpkg

链上任何一环断了——公钥缺失、Release 被改、.deb 校验对不上——apt 都会拒绝继续。这就是它的安全保证:哪怕镜像被人替换,能给你的也只是合法的字节流,要伪造内容必须同时拿到上游签名密钥。

Red Hat 这边结构类似:repodata/repomd.xml 是签名清单,里面是 primary.xml.gz 索引和各个 .rpm 的校验信息。dnf 默认强制走这条链,除非你显式关掉 gpgcheck(请别关)。

配国内镜像源

国内拉默认 Ubuntu / CentOS 源很慢,办法是改成国内镜像。两个最大的是阿里云(mirrors.aliyun.com)和清华(mirrors.tuna.tsinghua.edu.cn),都覆盖主流发行版。

Ubuntu(apt):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 1. 先备份
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak

# 2. 原地改 URL(Ubuntu 22.04 'jammy')
sudo sed -i.aliyun \
    -e 's|http://.*archive.ubuntu.com|http://mirrors.aliyun.com|g' \
    -e 's|http://.*security.ubuntu.com|http://mirrors.aliyun.com|g' \
    /etc/apt/sources.list

# 3. 刷新 + 验证
sudo apt update
grep mirrors.aliyun.com /etc/apt/sources.list   # 看一眼确认

常见 Ubuntu LTS 的代号:bionic(18.04)、focal(20.04)、jammy(22.04)、noble(24.04),写到源里别写错。

Ubuntu 24.04 起把 /etc/apt/sources.list 移到了 /etc/apt/sources.list.d/ubuntu.sources,格式也换成了 deb822。同样的 sed 在新文件上一样能跑。

CentOS / Rocky(dnf):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
sudo cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak

# CentOS 7
sudo curl -o /etc/yum.repos.d/CentOS-Base.repo \
    http://mirrors.aliyun.com/repo/Centos-7.repo

# Rocky Linux 9(CentOS 8/Stream 已 EOL,生产请用 Rocky 或 Alma)
sudo sed -e 's|^mirrorlist=|#mirrorlist=|g' \
         -e 's|^#baseurl=http://dl.rockylinux.org/$contentdir|baseurl=https://mirrors.aliyun.com/rockylinux|g' \
         -i.bak /etc/yum.repos.d/rocky-*.repo

sudo dnf clean all && sudo dnf makecache
dnf repolist                                # 确认能看到 aliyun

清华源把上面命令里的域名换成 mirrors.tuna.tsinghua.edu.cn 即可,路径完全一样,按你网络情况选快的那个。配完一定要真的跑一次升级(apt upgrade -y / dnf upgrade -y),有问题立刻就能发现,省得真要装东西时才意识到镜像根本没生效。

源码编译:configure / make / make install

有时候仓库里的版本太老,或者你需要一个发行版包没有的编译选项(一个 Nginx 模块、一个特定 OpenSSL 版本、一个调优开关)。源码编译就是后路。经典的 autotools 三步曲

  1. ./configure:探测系统——什么编译器、有哪些库、有哪些头文件,再根据你给的参数生成 Makefile
  2. make:按这个 Makefile 调编译器把代码编出来;
  3. sudo make install:把编译产物拷到 Makefile 里记录的目录(通常就是你给 configure--prefix)。

CMake 和 Meson 的命令不一样(cmake -B build / cmake --build build / cmake --install build),但形状一致:先 configure,再 build,再 install。

实战:在 Ubuntu 上编译 Tengine

Tengine 是淘宝维护的 Nginx 分支,多了不少模块。构建流程跟上游 Nginx 完全一致——会编 Tengine 就会编 Nginx。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1. 装编译依赖
sudo apt install -y build-essential libpcre3 libpcre3-dev libssl-dev zlib1g-dev
# Red Hat 系等价命令:
# sudo dnf install -y gcc make pcre-devel openssl-devel zlib-devel

# 2. 拉源码
wget http://tengine.taobao.org/download/tengine-2.3.3.tar.gz
tar -zxvf tengine-2.3.3.tar.gz
cd tengine-2.3.3

# 3. configure:所有构建选择都在这步定下来
./configure \
    --prefix=/usr/local/nginx \
    --with-http_ssl_module \
    --with-http_v2_module \
    --with-http_realip_module \
    --with-http_gzip_static_module

# 4. 编译,所有核心一起上
make -j"$(nproc)"

# 5. 安装:把二进制、默认配置、html 拷到 --prefix 下
sudo make install

每一步实际干了什么:

  • ./configure 检查你有没有 C 编译器,有没有 OpenSSL/PCRE/zlib 的开发头文件。哪个缺了它会清楚地报错——补上对应的 -dev 包再来一次。然后它会生成一个 Makefile,把 /usr/local/nginx 写进去作为安装路径,并启用你要的四个 HTTP 模块。
  • makeMakefile 一路编下去,产出 objs/nginx 和几个辅助文件。
  • sudo make install 创建 /usr/local/nginx/{sbin,conf,logs,html},把二进制、默认配置和示例 HTML 拷进去。它不会给你写 systemd 单元、建用户、开防火墙——这些都得你自己来。

跑起来:

1
2
3
/usr/local/nginx/sbin/nginx                    # 启动
/usr/local/nginx/sbin/nginx -s reload          # 重载配置
/usr/local/nginx/sbin/nginx -s stop            # 停止

接到 systemd 下管的话,写个 /etc/systemd/system/nginx.service

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[Unit]
Description=Nginx HTTP Server (built from source)
After=network.target

[Service]
Type=forking
PIDFile=/usr/local/nginx/logs/nginx.pid
ExecStart=/usr/local/nginx/sbin/nginx
ExecReload=/usr/local/nginx/sbin/nginx -s reload
ExecStop=/usr/local/nginx/sbin/nginx -s stop
PrivateTmp=true

[Install]
WantedBy=multi-user.target

然后:

1
2
sudo systemctl daemon-reload
sudo systemctl enable --now nginx

源码编译要付的代价是:没有 apt upgrade 替你管它。升级路径、安全跟踪、OpenSSL 出问题时重编整个软件,都是你自己的工作。大多数生产场景里正确答案是「除非有具体理由,否则用发行版包」。源码编译是用来解决那个「具体理由」的。

二进制压缩包:以 JDK 为例

有些软件以「解压即用」的二进制压缩包发布——JDK、Go、Node.js、大部分数据库引擎都有这种形式。没有安装程序好商量,文件放哪、环境变量怎么设,都是你的决定。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 1. 下载(Adoptium / Oracle / 内网镜像)
wget https://download.example.com/jdk-17_linux-x64_bin.tar.gz

# 2. 解压到 /opt——「第三方独立软件」按惯例放这里
sudo tar -zxf jdk-17_linux-x64_bin.tar.gz -C /opt
sudo mv /opt/jdk-17* /opt/jdk-17                # 改成短而稳定的路径

# 3. 全局生效
sudo tee /etc/profile.d/jdk17.sh > /dev/null <<'EOF'
export JAVA_HOME=/opt/jdk-17
export PATH=$JAVA_HOME/bin:$PATH
EOF

# 4. 让当前 shell 也立刻生效
source /etc/profile.d/jdk17.sh

# 5. 验证
java -version
javac -version

/etc/profile.d/*.sh 会被所有用户的登录 shell source 到,这正是 JDK 想要的作用域。只想给当前用户用,写到 ~/.bashrc~/.zshrc 即可。要并行多个 JDK,统一放在 /opt/jdk-XX 下,靠切 JAVA_HOME 切换(或者上 sdkmanjenv)。

同样的套路适用于 Go(/opt/go)、Node.js(/opt/node-vXX)等等。它们之所以流行,正是因为绕开了发行版的发布周期——上游版本和你急用的版本不匹配时这一招特别管用。

现代方案:Snap、Flatpak、AppImage

Snap、Flatpak、AppImage 与发行版包对比

发行版包模型有个长期痛点:它把软件牢牢绑在发行版的其它部分上。一个针对 Ubuntu 22.04 的 glibcgtk 编出来的 Firefox,不能轻易塞到 RHEL 8 上跑。三种方案分别从不同角度解决这件事:

  • Snap(Canonical):把依赖打包进一个 .snap 文件(其实是 SquashFS 镜像),用 AppArmor 沙箱跑,snapd 自动更新。Ubuntu 上一些应用(Firefox、Chromium 的 snap 版)默认就是 snap,其他发行版上少见。
  • Flatpak(freedesktop.org / RHEL 社区):基于共享 runtime(比如 org.freedesktop.Platform//22.08)打包,用 bubblewrap 沙箱,主要走 Flathub 分发。Linux 桌面 GUI 应用的事实标准。
  • AppImage:单个自包含可执行文件,不装、无守护进程、默认无沙箱。双击就跑,「我只想试一下这个软件」或者内部工具分发的好选择。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 装
sudo apt install snapd                                  # 一次性
sudo snap install firefox

sudo dnf install flatpak                                # 一次性
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install flathub org.gimp.GIMP

# AppImage:下载、chmod +x、跑
wget https://example.com/foo.AppImage
chmod +x foo.AppImage
./foo.AppImage

什么时候用哪个:

  • 服务器端的东西nginxmysqlpythonredis)一律走发行版包:体积小、跟 systemd 集成好、安全更新由发行版安全团队推送。
  • 桌面 GUI 应用且发行版包跟不上上游的:上 Flatpak(Flathub 选择最广)或 Snap。
  • 临时跑一下试试的工具:AppImage。

四种可以同台共存,安装根目录、更新机制都是分开的,互不干扰。

让生产机器一直可升级的几条习惯

这是我修过太多别人留下的烂摊子之后总结出来的:

1. 按计划升级,不是出事再升。 每周一次 apt upgrade -y / dnf upgrade -y,走你那套变更管理流程。一台机器越久没升级,下一次升级就越疼。

2. 边用边清。

1
2
sudo apt autoremove -y && sudo apt autoclean      # Debian/Ubuntu
sudo dnf autoremove -y && sudo dnf clean all      # Red Hat 系

/var 莫名其妙满了,第一时间查包缓存:

1
2
3
du -sh /var/cache/apt/archives    # Debian/Ubuntu
du -sh /var/cache/dnf             # Red Hat 系
du -sh /var/lib/snapd/cache       # snap 的旧版本特别能涨

3. 升不起的就钉死。 数据库、带自定义模块的内核、被你压测过特定版本的中间件——apt-mark hold / dnf versionlock add 锁住,留到运维窗口里手动升。

4. 包管理器和源码编译的同一软件不要并存。 apt install nginxmake install 都往 PATH 里塞了 nginx,迟早你会启错那个、改错配置,然后浪费一小时排查。只用其中一种。

5. 源码装的东西放 /opt/usr/local 包管理器拥有 /usr(除了 /usr/local),你别去抢它的地盘。

6. 改 /etc 之前先备份。

1
sudo cp /etc/nginx/nginx.conf{,.bak.$(date +%F)}

凌晨三点你把配置改坏了,包管理器救不了你,备份能。

7. 语言生态用语言生态自己的隔离。 Python 用 python3 -m venvuv,Node.js 版本用 nvm、项目依赖用 npm/pnpm。别让 pip install --user 污染了系统 Python——其它发行版包还指望它呢。

8. 看清楚你在做什么。 apt/dnf 在执行前会把整个计划打印出来。如果它要删的包数量明显异常,先停下来搞清楚原因再确认。「我用 apt 把系统弄废了」的故事,绝大多数都开头于:有人在「即将删除 200 个包」的提示里直接按了 y

总结

希望你带走的心智地图:

  • 是文件加元数据;包管理器存在是为了让这个映射在数据库里始终一致并跨重启保留。
  • 高层工具aptdnfpacman)跑求解器、对接仓库;底层工具dpkgrpm)一次只处理一个文件。
  • 包管理器真正难的地方是依赖求解,绕开高层工具基本就是给自己找事。
  • 仓库是签名过的文件树,信任从你机器上的 GPG 公钥,沿 Release / repomd.xml 一路传到每一个 .deb / .rpm
  • 源码编译是仓库版本不顶用时的后路;自愿背上升级税。
  • Snap / Flatpak / AppImage 用磁盘和集成度换「跨发行版分发」,桌面应用合适,服务端少见。

扩展阅读:

系列下一篇:

  • 《Linux 进程与资源管理》—— cgroups、pstopnice、OOM killer。
  • 《Linux 用户管理》—— 用户、组、sudoers、PAM。

当你能配镜像源、能锁版本、能调试依赖冲突、能在「发行版包 / 源码编译 / Flatpak」三者之间按理由选择时,你就从「跟包管理器较劲」升级到「让它替你干活」了。

Liked this piece?

Follow on GitHub for the next one — usually one a week.

GitHub