Ubuntu 24.04 的 /etc/apt/sources.list 是个空壳。
这句话解释了我们上周一次初始化卡 275 秒的原因。容器开了、IP 有了、22 端口通了,sudo 却没装上,前端一直转圈,最后报「初始化超时」。查到最后,不是 PVE 的问题,不是网络的问题,是软件源的问题——我们换源的那行脚本,改的是一个已经不生效的文件。
现象:机器是活的,工具是缺的
那次交付的是一台 Ubuntu 24.04(noble)。按流程走到「初始化系统」阶段,等了四分半钟,状态卡住。到 PVE 里看:
status=running✅/interfaces返回 10.x 地址 ✅TCP:22能连 ✅- 但
curl、git、sudo一个都没有 ❌
按当时的判定逻辑,工具不全就判失败。但机器明明是好的,用户也能 SSH 进去——只是什么也干不了。这比彻底失败更尴尬:订单停在「初始化中」,用户以为我们在骗他。
第一层根因:换源改错了文件
Ubuntu 24.04 起默认使用 deb822 格式,源定义搬到了 /etc/apt/sources.list.d/ubuntu.sources:
Types: deb
URIs: http://archive.ubuntu.com/ubuntu
Suites: noble noble-updates noble-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
而 /etc/apt/sources.list 里通常只剩注释。我们旧脚本是这么换源的:
cat > /etc/apt/sources.list <<EOF
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ $CN main restricted universe multiverse
...
EOF
看起来很合理,实际上等于往一个没人读的文件里写配置。ubuntu.sources 原封不动,archive.ubuntu.com 依然是活跃源。国内容器访问海外 archive,apt-get update 在那边逐个重试超时,几百个包索引一个也下不来——这就是 275 秒的来源。
教训一句话:Debian/Ubuntu 现在有两套源文件格式并存,只改一套等于没改。
第二层根因:拿日志字样判成败
旧逻辑是抓初始化脚本的输出,看到「成功」字样就算过。问题是 apt 的失败大多是静默的:apt-get install -y -qq 返回非零,脚本里一句 || true 吞掉,日志看起来干干净净。于是「跑完了」和「装上了」被混为一谈。
改成实测探针,一个工具一个检查函数:
tool_ready() {
case "$1" in
openssh-server) [ -x /usr/sbin/sshd ] || command -v sshd >/dev/null 2>&1 ;;
ca-certificates) [ -f /etc/ssl/certs/ca-certificates.crt ] ;;
sudo) command -v sudo >/dev/null 2>&1 ;;
*) command -v "$1" >/dev/null 2>&1 ;;
esac
}
for t in $CORE_TOOLS; do tool_ready "$t" || MISSING="$MISSING $t"; done
判定标准变成:lihua 能登录 + 核心工具齐全。不看日志写了什么,只看文件在不在、命令跑不跑得起来。
三层修法
| 层面 | 旧做法 | 新做法 | 效果 |
|---|---|---|---|
| 源文件 | 只写 sources.list |
同时识别 sources.list 与 sources.list.d/*.sources |
Ubuntu 24.04 / Debian 12 都能换到清华镜像 |
| 发行版识别 | 不区分 | 读 /etc/os-release 的 ID + VERSION_CODENAME |
Debian 用 bookworm,Ubuntu 用 noble,不会串 |
| 非 Debian 系 | 继续跑,白等 | 检测不到 apt-get 立即 die |
5 秒失败,不耗 300 秒 |
| 成败判定 | 抓日志字样 | 实测探针 + 二次补装 | 「跑完了」≠「装上了」 |
| 备份 | 无 | sources.list.lihua-bak + *.sources.lihua-bak |
出问题能回滚 |
换源主体现在长这样,四条分支全铺开:
case "$OS_ID" in
debian)
if ls /etc/apt/sources.list.d/*.sources >/dev/null 2>&1; then
for f in /etc/apt/sources.list.d/*.sources; do
sed -i -E "s#URIs:.*#URIs: https://mirrors.tuna.tsinghua.edu.cn/debian#g" "$f"
done
else
printf 'deb https://mirrors.tuna.tsinghua.edu.cn/debian/ %s main ...\n' "$CN" \
> /etc/apt/sources.list
fi
;;
ubuntu) # 同上,套件名换成 noble/jammy
;;
esac
另外补了一个「二次补装」:第一次 apt-get install 之后重新探针,缺什么再单独装一遍,还是缺就直接红字退出并指明缺哪个。宁可 60 秒报错,也不要 275 秒后含糊其词。
复盘:慢失败比快失败贵
这次最贵的不是代码,是误判。系统报「初始化超时」,第一反应是 PVE 慢、网络差、模板有问题,来回查了两小时。真正的线索只有一条:机器活着但工具缺失——这排除了资源层,直指软件源。
所以我们后来加了两条硬规则:
1. 快速失败优先。不知道发行版就说不知道,然后立刻停。不许「试试看能不能跑」。
2. 判定靠观测,不靠自述。脚本说自己成功了没有意义,端口通不通、命令在不在才有意义。
同一套逻辑后来也用在容器自动上报上:不信上报脚本的返回码,服务端自己核对字段完整性。
狸花云 v7.20.5,2026-10-30