
1. 项目概述为什么要管理“多个同名命令”1.1 update-alternatives 到底管什么用 Linux 折腾到一定阶段几乎都会遇到一个尴尬问题同一个命令在系统里装了好几份但实际执行的时候到底调起来哪一份完全看 PATH 里谁排在前面。手动去改环境变量、改 /usr/local/bin 里的软链当时解决了等包管理器一升级或者下次装个新版软件老问题又原样冒出来。update-alternatives 就是专门解决这一类“多版本同名命令管理”问题的工具。它在 Debian 系系统里几乎是内置的CentOS、Rocky、Fedora 这类 Red Hat 系的发行版也提供同名命令核心逻辑一致。简单说它把所有需要“选一个生效”的命令统一登记在一个名字下面由系统统一维护从公共命令入口到真实可执行文件之间的符号链接。你只需要告诉它哪个候选路径优先级更高或者临时手动指定用哪个剩下的事情它自己会处理好。很多人第一次看到 /etc/alternatives 这个目录会觉得陌生但把它理解成“两级软链”就通了。用户在终端敲的 /usr/bin/java其实是一个符号链接指向 /etc/alternatives/java而 /etc/alternatives/java 又是另一个链接最终指向真实 JDK 目录里的 java 可执行文件。update-alternatives 管理的就是这第二段链接的指向。多一层间接层之后切换版本时只改 /etc/alternatives 下的链接不动 /usr/bin 里的入口任何脚本、服务、shell 里已经写死的 /usr/bin/java 都不需要改非常干净。1.2 它能解决什么典型问题最典型的使用场景是 Java 开发环境。机器上既有 JDK 8又有 JDK 11、JDK 17每个项目要求的版本不一样。如果不用 alternatives就得反复改 JAVA_HOME还要同步修改 PATH更麻烦的是一些由 systemd 管理的服务在启动时读的是系统默认 java 命令改环境变量根本影响不到它们。而把 java、javac、jar、keytool 这些命令全部注册进同一个组之后一条 update-alternatives --config java弹出一个菜单选一下全系统的默认版本就切过去了服务重启后生效的就是新版本。另一个高频场景是基础工具链。比如系统自带的 openssl 版本太旧自己编了一套新的放在 /usr/local/openssl/bin 下希望 /usr/bin/openssl 直接指向新版本再比如同时装了多个 Python 解释器希望 python3 这个入口统一指向某个自定义安装版本。这类需求用 alternatives 管理都很顺手因为它不关心软件是哪来的只要候选路径存在就可以注册进去。2. 基础概念与命令速览2.1 四个核心子命令参数逐一看先用一个命令行样例建立更新认识。安装一个新的备选方案用的是 --install 参数sudo update-alternatives --install /usr/bin/node node /usr/local/node-v18/bin/node 1800这段命令里四个位置参数分别表达生成哪个命令入口/usr/bin/node、这个入口属于哪个软件组node、真实可执行文件的完整路径/usr/local/node-v18/bin/node、优先级数值1800。记住这四个参数顺序挺重要我见过不少同事把路径和名字换了位结果注册出来的组完全没法用。交互式切换某个软件组的当前版本用 --configsudo update-alternatives --config node执行后会出现一个编号菜单列出该组下所有候选路径输入数字回车就完成切换。需要在脚本或者自动化里不交互地指定版本用 --setsudo update-alternatives --set node /usr/local/node-v14/bin/node注意 --set 后面跟的路径必须是已经注册进这个组的候选路径不能随便填一个没登记过的目录否则会报错。删除某个候选用 --remove后面同时传软件组名和之前注册的路径sudo update-alternatives --remove node /usr/local/node-v14/bin/node2.2 自动模式、手动模式与优先级机制理解了参数之后真正决定行为的是“自动模式”和“手动模式”这套设计。每个 alternatives 软件组都有一个状态标记要么是 auto要么是 manual。自动模式下系统会对比组内所有候选路径的优先级数字数字大的被选中。这个逻辑很直接优先级 1800 的会自动盖过优先级 1500 的。手动模式则相反管理员用 --config 或 --set 明确指定了当前路径系统尊重选择不会因为优先级变化而自动切换。这里有一个容易被忽略的细节一旦你手动指定过路径这个组就会处于 manual 状态。之后如果包管理器升级往这个组里注册了一个更高优先级的新候选系统并不会自动切过去。对生产环境来说这是优点因为到底用哪套工具是管理员说了算而不是被依赖关系牵着跑但对一些习惯“升级即是生效”的人来说就会遇到“明明升级了命令版本还是旧”的困惑。排查时先看一眼状态就清楚了。2.3 系统里这些链接是怎么组织的在 Debian 系系统里alternatives 数据主要分布在三处。管理数据的配置文件在 /var/lib/dpkg/alternatives/ 下每个软件组对应一个文本文件公共命令入口在需要它的目录里最常见是 /usr/bin/中间层的链接统一放在 /etc/alternatives/。平时自己排查用 ls -l 看一下 /etc/alternatives/ 下的链接指向再读一下对应配置文件基本就能把整条链路捋直。配置文件里能看到的信息包括当前状态是 auto 还是 manual以及每个候选路径对应的优先级。手动去改这个配置文件是完全可以做到的但我不建议直接改理由很简单dpkg 或 update-alternatives 自己管理这些文件时会做权限检查和一致性维护手工改很容易留下脏状态遇到包管理器重写时反而更乱。正确做法是老老实实用命令操作工具内部会同步好关联链接。3. 核心实操从查看到自定义注册3.1 查看已经注册过的备选方案拿到一台新机器第一步是摸清楚系统里哪些命令被纳入了 alternatives 管理。直接跑不带参数的 update-alternatives或者用 --list会列出所有已注册的软件组名称像这样update-alternatives --list输出里可能看到 java、node、editor、pager、x-www-browser 等等。单独看某一个组的详情用 --displayupdate-alternatives --display java输出会包含这个组当前的模式auto/manual、当前链接指向、候选路径列表以及每个候选的优先级。这段信息排查问题时极有用。比如有时候明明只想让 JAVA_HOME 指向特定版本结果发现 /usr/bin/java 的链接已经指向了另一个候选看 --display 一眼就能定位是优先级自动选中还是手动指定出错。3.2 手动切换版本的两个常用姿势交互切换适合临时用一下、人工观察结果。执行 sudo update-alternatives --config java屏幕会出现一个列表There are 3 choices for the alternative java (providing /usr/bin/java). Selection Path Priority Status ------------------------------------------------------------ * 0 /usr/lib/jvm/java-17-openjdk-amd64/bin/java 1700 auto mode 1 /usr/lib/jvm/java-8-openjdk-amd64/bin/java 1081 manual mode 2 /usr/lib/jvm/java-11-openjdk-amd64/bin/java 1111 manual mode看到 * 指向当前生效的链接。如果在列出的路径里没有你想要的那一个说明它还没注册进来需要先做 --install不要以为漏了哪项交互菜单只展示已经纳入管理的候选。批量和自动化场景则适合用 --set。比如我写一个部署脚本希望每次灰度发布机器上固定用 JDK 11就直接在脚本里放sudo update-alternatives --set java /usr/lib/jvm/java-11-openjdk-amd64/bin/java sudo update-alternatives --set javac /usr/lib/jvm/java-11-openjdk-amd64/bin/javac这种写法的好处是不会弹出交互提示脚本能一口气跑完也不会因为终端里没人选菜单而卡住。3.3 把自编译软件加入 alternatives 管理自编译的软件是 alternatives 的真正发挥场景。假设我自己编了一个 nginx放在 /usr/local/nginx-1.26/ 目录下希望系统默认的 nginx 命令指向它同时保留原来的系统包版本作为备选。先确认原系统入口存在ls -l /usr/bin/nginx然后把新版本注册进去给一个高于系统包默认值的优先级sudo update-alternatives --install /usr/bin/nginx nginx /usr/local/nginx-1.26/sbin/nginx 2000这里要注意一个小坑 /usr/local/nginx-1.26/sbin/nginx 指的是二进制可执行文件不是配置目录。有些人会把整个安装目录路径当候选路径传进去结果符号链接指向了一个带子目录的路径执行 nginx -v 直接报“Is a directory”。候选路径必须是最终可执行文件哪怕它本身的依赖库文件在旁边的 lib 目录里那也无所谓因为 alternatives 只管链接不管运行时搜索路径。3.4 修改优先级与清理失效链接注册完的候选如果想要调整优先级并不是直接改一个数字那么简单。update-alternatives 没有专门提供“改优先级”的子命令常规做法是先 --remove 旧路径再用新的优先级 --install 同一路径重新注册。因为删除和注册都很快生产环境里可以连续执行不会对业务产生影响。清理失效链接也是常见操作。软件被卸载了但 alternatives 配置没清干净或者是路径被迁移了候选文件不存在却还留在列表里。这时用 --remove 把不存在的路径移除掉。如果发现整个组都已经不需要了比如某个自编译工具彻底弃用可以用 --remove-all 一次性清空该组所有候选并删除 /usr/bin 下的入口链接。4. 常见问题与排查技巧实录4.1 命令还是显示旧版本很多人在手动切换之后马上敲 java -version发现还是旧版本于是认为 alternatives 没生效。实际上有相当一部分情况是 shell 的命令哈希表在作怪。bash 会把输入过的命令路径缓存起来当你第一次敲 java 是旧的 /usr/bin/java 时它会记住这段解析结果切换链接后再敲 javabash 直接用了缓存旧路径根本没重新走环境变量查找。解决办法很简单在当前终端执行hash -r或者干脆重新开一个终端。如果是 zsh用rehash。这个现象极其容易误导排查方向我在一堆文档里看到别人把这个问题误判成链接坏了其实链接状态完全正常。另一个常见原因是 PATH 里存在一个更靠前的同名可执行文件。比如某个用户目录下装了 local/java/bin/java且 PATH 里 /usr/local/bin 排在 /usr/bin 前面那么敲 java 时实际执行的是前者。alternatives 管理的只是 /usr/bin 下的入口覆盖不了 PATH 顺序问题。遇到版本对不上先which java看看到底执行的是哪个路径如果结果都不是 /usr/bin/java说明问题根本不在 alternatives。4.2 包升级后自定义选择被重置用 --set 指定了某个版本过了一段时间发现又变回系统默认版本。这类问题通常出现在 Debian 系系统更新软件包时。包里自带的维护脚本会在安装或升级时再次调用 update-alternatives --install重新注册路径并把模式改回 auto如果新版本优先级更高自动模式下就会切换过去。规避办法是把自定义选择固化成一个小的系统脚本或者单位文件在包升级后主动重新拉回你的指定版本。我惯用的做法是在 /usr/local/sbin/ 下放一个 fix-alternatives.sh里面写好所有需要固定的 --set 命令每次跑完 apt upgrade 顺手执行一次。虽然土但足够可靠也方便排查到底主动干预了哪些组。4.3 链接变成悬空状态怎么办所谓悬空链接就是 /etc/alternatives/xxx 指向的候选文件已经被删除但链接本身还在。出现这种情况一般是因为软件卸载时没有正确触发 update-alternatives 的清理过程或者手动删除了候选目录。表现就是执行某个命令报 “No such file or directory”但 ls -l /usr/bin/xxx 看链接还在。处理时先看这个组还有没有别的有效候选update-alternatives --display xxx如果还有其他有效候选直接 --config 或 --set 切到有效路径悬空问题就解决了。如果所有候选都已失效用 --remove-all 把整个组清掉然后重新注册新候选。不建议直接 rm 掉悬空链接因为 /var/lib/dpkg/alternatives/ 下的配置数据还在下次包管理器操作时可能出现记录和实际链接不一致的状况。4.4 非 Debian 系的差异点在 RHEL、Rocky、Fedora 系列上update-alternatives 命令也是可用的基础语法高度一致但数据管理方式不同。Red Hat 系里这个命令由 chkconfig 或 alternatives 包提供配置信息以纯文本形式放在 /var/lib/alternatives/ 下并不依赖 dpkg。使用习惯上基本可以照搬但注意不要用 dpkg 相关的检查命令去查就算查了也不会有反应。Arch Linux 系则直接没有 update-alternatives 命令官方更推荐用 archlinux-java 之类的专项工具。也就是说如果你在 Arch 环境里手上只有一条 update-alternatives 命令的可参考经验需要先确认系统是否提供这个命令没有就换用发行版自己的方案。最稳妥的判断方法是执行command -v update-alternatives有输出就正常用没输出就不查了。5. 实战用 update-alternatives 管理好 Java 全家桶5.1 完整注册一套 JDK 的命令Java 的问题在于它不是只有一个命令java、javac、javadoc、jar、keytool 等等都分布在同一个 bin 目录里。只把 java 这一个命令纳入 alternatives 管理其他命令还是会混乱。完整操作先确认 JDK 路径比如ls /usr/lib/jvm/java-17-openjdk-amd64/bin/然后逐个注册关键的几个命令。手工敲太多会很累建议写一个循环。把 JDK 路径存成变量配合命令名批量生成候选JDK17/usr/lib/jvm/java-17-openjdk-amd64 for cmd in java javac javadoc jar jshell keytool; do sudo update-alternatives --install /usr/bin/$cmd $cmd $JDK17/bin/$cmd 1700 done这里 /usr/bin/$cmd 是入口$cmd 是组名第三个参数是真实路径。注册完以后切换或查看都只需要指定这一组名字里的任意一个其实每个命令单独管理查 java 的不会自动关联 javac 的所以切换版本时最好把常用命令一起切。我是手动创建switch_jdk.sh里面按完整的命令顺序写一遍 --set以后切版本跑脚本即可。因为 JDK 各个小版本之间命令集大同小异这个脚本长期维护成本很低。5.2 在脚本里自动选版本如果要发布脚本或者配置管理里自动选择 JDK推荐以优先级策略为主。假设 JDK 17 的优先级是 1700JDK 11 的优先级是 1111且组处于 auto 模式系统会自动选 17。想在某个节点临时切换成 11就跑sudo update-alternatives --set java /usr/lib/jvm/java-11-openjdk-amd64/bin/java这个操作会把组切为 manual 状态想一想这是否是你想要的。自动化这类事情我喜欢在设置前后把状态打出来留作日志update-alternatives --display java | head -n 20这样即使后面出了问题翻日志也能立刻明白当时是什么模式、谁把链接改掉的。5.3 别忘了一并管理关联命令只管理 java 不管理 javac 是 Java 环境里最常见的半吊子配置。COMMON命令行提示符 java 是 17但编译代码用的 javac 还是 8构建时到处报 version mismatch。我自己的实践是在一个 JDK 的注册循环里覆盖当前 JDK bin 目录下的所有可执行文件而不是只挑几个常用命令。必要时还可以把 jar、jlink、jdeps、serialver 这些都注册进去命令多没什么坏处真正浪费时间的反而是之后逐个排查。同一个软件组还有个专属技巧把 JAVA_HOME 也做成 alternatives 管理的链接。比如注册一个 java_home 组候选路径直接指向 /usr/lib/jvm/java-17-openjdk-amd64然后在 /etc/profile.d/java.sh 里导出 JAVA_HOME/usr/lib/jvm/alts-java-home。这样切换版本之后JAVA_HOME 也跟着切换不会再发生 JAVA_HOME 和 java 命令指向不一致的诡异问题。6. 几条个人实操体会用了几年 alternatives最想提醒的一件事是所有操作都要留痕。切换版本之前先执行一次 update-alternatives --display 把当前状态存到文本里切换之后再看一眼链接指向是否符合预期。链接问题不象服务崩溃那么显眼常常等到跑构建任务失败时才暴露而那时你已经想不起来自己改过什么了。另外不要在 /etc/alternatives 目录里手工乱动软链。我有一回图省事直接 ln -s 修改了 /etc/alternatives/nginx 的指向当时确实生效了但后来一旦有包管理操作触发 update-alternatives 重新计算系统会因为配置数据和实际链接不一致而报奇怪的错误。正确方式永远是要临时换就 --config要长期换就 --set要自动化就通过脚本控制不要绕过工具自己动手。最后一个小技巧把优先级的数值当成命名空间来规划而不是随手写个 1、2、3。我习惯把优先级设计成有一定含义的数字比如 1000 表示基线版本2000 表示优先使用的新版本3000 表示强制候选版本。这样在看 --display 输出时瞬间就能判断出一个候选在系统里的“地位”而不是看着一堆差不多的数字发呆。