T7、RHEL alternatives
文档版本:2.0
适用对象:系统管理员、运维工程师、配置管理(Ansible/Puppet/Chef)、包维护者
参考来源:Debian man update-alternatives(1)、Debian Policy、Fedora Wiki、RHEL 文档、SUSE 管理指南
1. 文档说明与快速定位
| 需求 | 跳转章节 |
|---|---|
| 快速会用 | 2. 快速入门 |
| 本质与边界(为何存在、解决什么、不解决什么) | 3. 本质与要解决的问题 |
| 设计(两层链接、link group、auto/manual、与包管理契约) | 4. 设计动机与约束 |
| 实现与机制(状态存储、各操作触发的行为、包脚本契约) | 5. 实现与机制 |
| 术语与概念 | 6. 术语与概念 |
| 发行版差异 | 7. 发行版与路径差异 |
| 命令语法 | 8. 命令参考、13. 命令速查 |
| 深入实践案例(Java 全族、MTA、自建、Python、批量、故障) | 10. 实践案例 |
| 排错与最佳实践 | 11. 排错与常见问题、12. 最佳实践 |
2. 快速入门
- 一句话:Alternatives 是系统级的符号链接管理框架,让多个“可互换”实现(如多版本 Java、多种编辑器)并存时,由一个通用命令名通过符号链接指向当前选中的实现,并由优先级或管理员选择决定指向谁。
- 最常用(Red Hat 系,Debian 系把
alternatives换成update-alternatives):- 查看:
alternatives --display java - 交互切换:
sudo alternatives --config java - 脚本固定:
sudo alternatives --set java /path/to/java-17/bin/java
- 查看:
- 链接链:
/usr/bin/java→/etc/alternatives/java→ 实际二进制;切换时只改中间一层。
3. 本质与要解决的问题
3.1 问题背景
- 多个包可提供同一“逻辑接口”的不同实现(如多种 vi、多种 Java、多种 MTA),且允许同时安装。
- 系统与脚本需要一个稳定的通用名(如
vi、java、editor)来调用“当前默认”的实现,而不必写死路径或版本号。 - 若没有统一机制:要么每个包抢占同一路径(互相覆盖),要么由管理员手写符号链接,包升级或安装/卸载会破坏或覆盖这些链接,且无法区分“包提供的候选”与“管理员的选择”。
3.2 本质定义
Alternatives 的本质是:在“多实现并存”的前提下,用“状态 + 符号链接”维护“当前默认实现”的单一真相源,并约定包管理与管理员谁有权在何时改写该状态。
- 状态:每个链接组有 mode(auto/manual) 和 current selection(或“由优先级推导的 best”);状态存放在 administrative directory,与
/etc/alternatives的符号链接一起构成完整事实。 - 符号链接:通用名(如
/usr/bin/java)不直接指向具体实现,而是指向/etc/alternatives/<name>,再由后者指向真实路径。这样“谁被选为默认”的变更只体现在 /etc 下的链接,符合 FHS 对“管理员可改配置”的归属。 - 契约:包在 postinst(configure) 注册、在 prerm/postrm(remove) 注销;不在 upgrade/disappear 等阶段调用 alternatives,以免破坏 manual 或造成反复切换。
因此,理解 alternatives 的核心是:谁在什么时机可以改“当前默认”和“模式”,以及链接如何与状态一致。
3.3 边界:什么不用 alternatives
| 场景 | 更合适的做法 | 说明 |
|---|---|---|
| 用户级“默认”(如 A 用 vim、B 用 nano) | 环境变量 EDITOR、VISUAL 或 shell 配置 |
alternatives 是系统级;per-user 用 env 或 pyenv/jenv/nvm 等 |
| 编译时选编译器 | 环境变量 CC、CXX 或构建系统配置 |
不要用 alternatives 代替环境变量 |
| 替换某包提供的唯一文件(无多实现) | diversions(dpkg 的 divert) | alternatives 用于“多选一”,diversions 用于“替换单文件” |
| systemd 配置等 | 不用 alternatives | Debian Policy 明确不用于 systemd 配置文件 |
4. 设计动机与约束
4.1 为什么是“两层”符号链接(generic → alternatives 目录 → real)
- FHS:
/usr属于“不变的系统文件”,/etc属于“主机相关、管理员可改”的配置。若把/usr/bin/java直接链到/usr/lib/jvm/...,则“当前默认”的语义会写入/usr,与 FHS 相悖;且包管理在安装/卸载时若改写/usr/bin/java,容易与管理员意愿冲突。 - 设计选择:让
/usr/bin/java只指向/etc/alternatives/java,而“当前选中的实现”由/etc/alternatives/java再指到具体路径。这样:- 包可以只安装自己的二进制到
/usr或/opt,并注册到 alternatives; - 真正“改默认”的操作只改
/etc/alternatives下的链接(及状态),管理员改动被限定在 /etc,符合 FHS,且包升级不直接覆盖“谁被选中”。
- 包可以只安装自己的二进制到
(出处:Debian man — “so that the system administrator's changes can be confined within the /etc directory”.)
4.2 为什么需要 link group(master + slave)
- 一个“实现”往往不止一个文件:例如选 JDK 17,则
java、javac、keytool、以及对应 man 页都应一起指向 17,否则会出现java是 17 而javac仍是 11 的不一致。 - link group:由一个 master link(如
java)和若干 slave links(如javac、java.1.gz)组成;切换“当前实现”时,只改 master 的选择,所有 slave 随 master 一起切换,保证一组链接始终对应同一实现。
4.3 为什么有 auto 与 manual 两种模式
- auto:包安装/卸载时,由系统根据优先级自动决定“当前默认”(通常选最高优先级且仍存在的实现)。适合“随包进退、自动选最优”的场景。
- manual:当前指向由管理员显式选择(
--config或--set);包安装/卸载不会改变该组的指向。适合生产上“固定版本、禁止包升级悄悄换默认”的场景。 - 一旦管理员通过
--config/--set做出选择,该组会变为 manual,以尊重“人”的决策;若要恢复“自动选最高优先级”,需显式执行--auto <name>。
4.4 优先级的角色
- 每个候选有一个整数 priority;数值越大越优先。
- 仅在 auto 模式下:系统用 priority 决定“当前默认”应是哪一个(通常为已安装中 priority 最高者);manual 模式下 priority 仅作展示或
--display中的“best”参考,不驱动切换。
4.5 与包管理的契约(Debian Policy 与 man 的硬性约定)
- 应当调用:
- postinst (configure):注册本包提供的 alternative(
--install); - prerm (remove):在删除文件前从 alternatives 中移除本包提供的 path(
--remove name path); - postrm (remove):也可参与移除,但若在文件已删后调用
--remove,该 path 已不存在,可能被标为“站点本地”等,故推荐在 prerm 中做 --remove。
- postinst (configure):注册本包提供的 alternative(
- 不应调用(否则易丢 manual 或反复切换):
- upgrade、disappear 等阶段不要调用 update-alternatives 来改链接;仅在 configure/remove 时注册/注销候选即可。
- 维护脚本中避免
--set:在包脚本里用--set会把链接组切成 manual,无法区分“用户选的”和“包强制的”,且会阻止后续自动管理(Debian Bug #643602)。
5. 实现与机制
5.1 目录布局与职责
| 路径 | 职责 |
|---|---|
| /usr/bin/ |
用户可见的入口;必须是指向 /etc/alternatives/<name> 的符号链接(由 alternatives 创建/更新)。 |
| /etc/alternatives/ |
由 alternatives 管理的“中间层”符号链接;指向当前选中的实现(真实二进制或文件)。 |
| /var/lib/alternatives/(RHEL)或 /var/lib/dpkg/alternatives/(Debian) | 状态库:每个链接组对应一个文件(通常与 <name> 同名),记录 mode、master/slave 的 generic 路径、所有候选的 path+priority+slave 列表、以及当前选中 path(manual 时)或“由 priority 推导”的 best。 |
注意:不要手动用 ln -sf 改 /etc/alternatives 或 generic name;所有改动必须通过 --set / --config / --install / --remove 等,否则状态与链接不一致,后续行为不可预期。
5.2 状态存储(概念化,实现可略有差异)
每个链接组在 administrative directory 下有一个状态文件(名称即 <name>)。其逻辑内容大致包括:
- 模式:auto 或 manual。
- master:generic 路径(如
/usr/bin/java)、以及“当前指向的 path”(manual 时显式存;auto 时可为“best”或等价信息)。 - 候选列表:每个候选一条记录 —— path、priority、以及该候选下各 slave 的 (slave generic path, slave name, slave path)。
- slave 列表:与 master 同组的各 slave 的 generic 路径与名称,用于在切换时一起更新。
(具体磁盘格式各实现不同,此处仅说明“状态里有什么”;运维只需通过 --display、--get-selections 等观察,勿直接编辑状态文件。)
5.3 各操作触发的行为(机制要点)
| 操作 | 对状态的影响 | 对链接的影响 |
|---|---|---|
| --install | 若 <name> 不存在则新建链接组(mode=auto);将 (path, priority, slaves) 加入候选。若已存在则仅新增/更新该候选。 |
若 mode=auto 且新候选 priority 为当前最高,则把 master 与所有 slave 的链接更新为指向该候选;否则若为新建组,则指向该唯一候选。可能创建 generic name → /etc/alternatives/ |
| --remove name path | 从该组的候选列表中删除该 path(及对应 slave 信息)。 | 若当前指向的就是该 path,则把当前指向改为其他候选(或删链);并可能把组改回 auto。否则只删记录,不改链接。 |
| --set name path | 将“当前选中”设为 path,并将组设为 manual。 | 立即把 /etc/alternatives/<name> 及该组所有 slave 的链接更新为 path 对应的目标。 |
| --config name | 用户选择后,将当前选中设为所选 path,并将组设为 manual。 | 同 --set,按选择更新所有链接。 |
| --auto name | 将组设为 auto;当前选中逻辑改为“best”(最高 priority 且存在)。 | 将 master 与 slave 链接更新为指向 best。 |
| --remove-all name | 删除该链接组的全部状态与候选。 | 删除 /etc/alternatives/<name> 及该组涉及的 generic 链接(视实现而定)。 |
要点:
- auto 下:每次执行
--install/--remove时,若当前选中的实现被删或出现更高 priority 的候选,链接会随之更新。 - manual 下:
--install/--remove不改变当前指向,只增删候选记录;只有--set/--config/--auto会改链接。
5.4 包维护脚本何时调用(小结)
- postinst configure:
--install注册本包提供的 alternative。 - prerm remove:
--remove <name> <path>在文件被删之前注销,避免状态仍指向已删 path。 - 不要在 upgrade、disappear 等阶段调用以“切换”或“重选”链接,否则会丢失 manual 或产生 flip-flop(Debian man 明确警告)。
6. 术语与概念
| 英文术语 | 含义 |
|---|---|
| generic name / alternative link | 用户调用的路径,如 /usr/bin/editor、/usr/bin/java |
| alternative name | 链接组名,即 /etc/alternatives/<name> 中的 name,如 editor、java |
| alternative (path) | 某个具体实现路径,如 /usr/bin/vim、/opt/jdk-17/bin/java |
| alternatives directory | 默认 /etc/alternatives,存“中间层”符号链接 |
| administrative directory | RHEL:/var/lib/alternatives;Debian:/var/lib/dpkg/alternatives,存状态 |
| link group | 一个 master + 多个 slave,一起切换 |
| master link | 决定整组指向哪个实现的主链接 |
| slave link | 随 master 同步切换的从属链接 |
7. 发行版与路径差异
| 项目 | RHEL / CentOS / Fedora | Debian / Ubuntu |
|---|---|---|
| 命令名 | alternatives |
update-alternatives |
| 可执行路径 | /usr/sbin/alternatives |
随 dpkg |
| 状态目录 | /var/lib/alternatives |
/var/lib/dpkg/alternatives |
| 中间层链接 | /etc/alternatives |
/etc/alternatives |
| 提供包 | 常为 chkconfig 包 | dpkg 包 |
| 扩展 | --initscript(chkconfig) |
--get-selections / --set-selections |
8. 命令参考
8.1 --install
alternatives --install <link> <name> <path> <priority> [--slave <link> <name> <path>]...
<link>:主链接的通用路径(如/usr/bin/java),必须绝对路径。<name>:链接组名(如java)。<path>:本候选的真实路径,必须绝对路径。<priority>:整数,越大在 auto 下越优先。--slave link name path:从属链接的 (用户路径, 组内名, 实际路径),可多组。
8.2 --remove / --remove-all
alternatives --remove <name> <path>
alternatives --remove-all <name>
8.3 切换与模式
alternatives --config <name> # 交互,选后设为 manual
alternatives --set <name> <path> # 非交互,设为 manual
alternatives --auto <name> # 恢复 auto,指向最高 priority
8.4 查询
alternatives --list
alternatives --display <name>
Debian 额外:--get-selections、--set-selections、--list <name>(列出该组所有候选 path)。
8.5 可选参数
--altdir、--admindir:覆盖默认的 alternatives 目录与状态目录。--verbose/--quiet。
退出码:0 成功,2 错误(与 man 一致)。
9. 实践案例(概要)
- Java 多版本:用
--install注册多个 JDK,并用--slave绑定javac、keytool、man 等;生产用--set固定版本。 - 默认编辑器:
alternatives --config editor(或 update-alternatives)。 - MTA(RHEL):sendmail 与 postfix 通过 mta 链接组切换。
- 自建多版本:如
/usr/local/bin/foo在 foo-2 与 foo-3 间切换,并带 slave(man、config);先--install两个候选再--config或--set。 - RHEL 8 无版本号 python:用
alternatives --set python /usr/bin/python3.11提供/usr/bin/python;注意不要为系统自带的python3乱建 alternative(SUSE 文档警告:不要为 python3 做自定义 alternative,会破坏依赖)。 - 批量部署(Debian):
--get-selections导出,--set-selections在它机或重装后导入。
10. 实践案例(深入)
10.1 Java 完整链接组(master + 多个 slave)
目标:java、javac、keytool、man 等随同一 JDK 一起切换,且生产固定版本。
# 注册 JDK 11
alternatives --install /usr/bin/java java /usr/lib/jvm/java-11-openjdk/bin/java 1100 \
--slave /usr/bin/javac javac /usr/lib/jvm/java-11-openjdk/bin/javac \
--slave /usr/share/man/man1/java.1.gz java.1.gz /usr/share/man/man1/java-11.1.gz
# 注册 JDK 17(priority 更高,auto 下会成默认)
alternatives --install /usr/bin/java java /usr/lib/jvm/java-17-openjdk/bin/java 1700 \
--slave /usr/bin/javac javac /usr/lib/jvm/java-17-openjdk/bin/javac \
--slave /usr/share/man/man1/java.1.gz java.1.gz /usr/share/man/man1/java-17.1.gz
# 生产固定为 17
alternatives --set java /usr/lib/jvm/java-17-openjdk/bin/java
理解要点:每个 --install 的 slave 列表必须与该候选的 path 一一对应;切换时整组一起变,避免 java 与 javac 版本不一致。
10.2 MTA 组(sendmail vs postfix)
RHEL/Fedora 上常见“mta”链接组:/usr/sbin/sendmail 等由 alternatives 指向 sendmail 或 postfix 的实现。切换方式同上:alternatives --config mta 或 --set mta <path>。理解:同一“逻辑接口”(MTA)的多实现,由一组链接统一切换。
10.3 自建多版本命令(带 man、config 的 slave)
场景:自建脚本 foo-2、foo-3,希望 /usr/local/bin/foo 可切换,且 man、配置文件随版本一起换。
# 两套实现:/usr/local/bin/foo-{2,3},/usr/local/man/man1/foo-{2,3}.1.gz,/etc/foo-{2,3}.conf
sudo alternatives --install /usr/local/bin/foo foo /usr/local/bin/foo-2 200 \
--slave /usr/local/man/man1/foo.1.gz foo.1.gz /usr/local/man/man1/foo-2.1.gz \
--slave /etc/foo.conf foo.conf /etc/foo-2.conf
sudo alternatives --install /usr/local/bin/foo foo /usr/local/bin/foo-3 300 \
--slave /usr/local/man/man1/foo.1.gz foo.1.gz /usr/local/man/man1/foo-3.1.gz \
--slave /etc/foo.conf foo.conf /etc/foo-3.conf
sudo alternatives --config foo # 或 --set foo /usr/local/bin/foo-3
要点:slave 的 generic 路径(如 /usr/local/man/man1/foo.1.gz)在两组中相同,但各自指向不同文件,保证 man foo 与当前 foo 版本一致。
10.4 RHEL 8 的 /usr/bin/python(无版本号)
RHEL 8 不默认提供未版本化的 python;若需要,用 alternatives 指向某个 python3.x:
sudo alternatives --set python /usr/bin/python3.11
注意:不要为系统自带的 /usr/bin/python3 再建一个“覆盖型”的 alternative(某些文档警告会破坏依赖);仅当发行版明确支持“python”这个 generic 名时再设。
10.5 批量部署与配置管理(Debian)
# 导出当前所有选择(备份或做黄金镜像)
update-alternatives --get-selections > alternatives-selections.txt
# 在新机或重装后批量恢复
sudo update-alternatives --set-selections < alternatives-selections.txt
在 Ansible 等工具中:可把 --get-selections 输出作为“事实”或模板,用 --set-selections 做幂等配置;生产建议对关键组(如 java)再用 --set 明确一次,防止后续包操作影响。
10.6 故障根因与对应手段
| 现象 | 本质原因 | 做法 |
|---|---|---|
| 切换后命令行版本没变 | 实际执行的是 PATH 里别的路径或 shell alias | readlink -f $(which java) 看真实链;检查 alias、$PATH |
| 包升级后默认被改 | 组处于 auto,新包 priority 更高 | 生产用 --set 固定(manual) |
| 不同用户看到不同默认 | 用户层 PATH/alias 不同 | alternatives 是系统级;查 ~/.profile、~/.bashrc、/etc/profile.d |
| 删除某版本后报错或断链 | 状态仍指向已删 path | --remove <name> <path> 从候选中移除该 path |
| 手动改过 /etc/alternatives 或 ln -sf | 状态与链接不一致 | 仅用 --set/--config 改;若已乱,可用 --install 重新注册再 --set 纠正 |
| 包升级后“默认”来回变 | 维护脚本在 upgrade/disappear 里调用了 alternatives | 包侧应在 postinst configure / prerm remove 才调用;本地只能 manual 固定 |
11. 排错与常见问题
(与上节 10.6 一致,此处仅列命令级排查。)
- 看链接链:
ls -l /usr/bin/java /etc/alternatives/java;readlink -f /usr/bin/java。 - 看组状态与候选:
alternatives --display java(或update-alternatives --display java)。 - 确认“当前”与模式:
--display首行会标 auto/manual 及当前指向。
12. 最佳实践
- 生产固定版本:对关键接口(如 java、python)用
--set设为 manual,避免包升级自动换默认。 - 脚本化与幂等:把
--install、--set或--set-selections纳入 Ansible/Puppet/Chef,保证可重复执行。 - 成组一致:Java、GCC 等一族命令用
--slave绑定,避免 java/javac 或 gcc/g++ 版本不一致。 - 巡检与审计:定期
alternatives --display <name>或--get-selections留存结果,便于比对变更。 - 区分用途:alternatives 管系统级默认;per-user 用环境变量或 pyenv/jenv/nvm 等。
- 环境变量:
EDITOR、CC等用环境变量或配置设置,不用 alternatives 代替。
13. 命令速查
| 操作 | 命令 |
|---|---|
| 列出所有组 | alternatives --list |
| 某组详情 | alternatives --display <name> |
| 交互切换 | alternatives --config <name> |
| 非交互指定 | alternatives --set <name> <path> |
| 恢复自动 | alternatives --auto <name> |
| 注册 | alternatives --install <link> <name> <path> <priority> [--slave ...] |
| 移除候选 | alternatives --remove <name> <path> |
| 移除整组 | alternatives --remove-all <name> |
| 批量导出/导入(Debian) | update-alternatives --get-selections / --set-selections |
14. 常见误区与纠正
| 错误 | 正确 |
|---|---|
--install 用相对路径 |
所有 link、path 必须绝对路径 |
| 只注册 java 不配 javac 等 slave | 用 --slave 成组注册,保证一致 |
| 手改 /etc/alternatives 或 ln -sf | 仅用 --set / --config 修改 |
| 新候选 priority 低于现有却期望成默认 | auto 下只有 priority 最高者会被选;要么提高 priority,要么用 --set |
15. 参考与引用
- Debian: update-alternatives(1)、DebianAlternatives (Wiki)
- Debian Policy: Alternative versions of an interface
- Fedora: Alternatives system
- SUSE: update-alternatives: Managing multiple versions
- Red Hat: Introduction to the alternatives command
- Man:
man alternatives(RHEL/CentOS)、man update-alternatives(Debian/Ubuntu)
修订时请更新文首版本号;行为描述以各发行版 man 与 Policy 为准。
