fix(critical): 实例镜像 arm64 构建 OOM + 版本耦合无兜底致面板锁死(v1.4.1 连环 P0)

v1.4.1 只发布了面板、实例镜像 CI 失败,叠加版本耦合无兜底 → 用户升级指示器消失、
实例卡在坏的 1.4.0、浏览器仍黑屏。两处修复:

① Dockerfile arm64 OOM:CI 的 QEMU arm64 下第二个 apt 层 OOM(两次 apt-get update +
snapshot 源被后续层反复拉取)。合并为单层一次 update+install、装完即移除 snapshot 源、
Acquire::Languages=none + Retries=3,砍掉内存/网络峰值。本机原生 arm64 构建 + 全套探针
(Chromium 149 / DPI 98304 / CEF 连跑两次 / 30s 零崩溃 / 窗口可见)已验证通过。

② 版本耦合安全兜底:面板偏好「与自身同版本」的实例镜像 tag,但该 tag 在本地和 registry
都不存在时(CI 部分失败/发布时序),原先会指向不存在的 tag → 检测恒空、升级拉不到。
新增 resolveInstanceImage():不可达则回退 :latest,功能永不因版本错配而瘫痪(已运行时验证
9.9.9→latest)。启动时与 ensureImage 前各解析一次。

P0 复盘记录连环成因;待办:改用原生 arm64 runner 替代 QEMU、matrix 失败联动。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gloridust
2026-07-07 12:11:16 +08:00
parent d523204c6b
commit fda9522b78
4 changed files with 64 additions and 33 deletions

View File

@@ -3,6 +3,22 @@
> 每次 P0 修完在这里记一笔:症状 → 根因 → 为什么会发生(流程层面) → 对应守则。
> 目的不是追责,是让同一类错误只交一次学费。新条目追加在最上面。
## 2026-07-07 · v1.4.1 实例镜像发不出 + 版本耦合把面板锁死(连环)
- **症状**:v1.4.1 发布后,用户升级面板成功,但升级指示器消失、实例卡在 1.4.0(Chromium 150)、浏览器仍黑屏。
- **根因链(两个 bug 叠加)**:
1. **CI 实例镜像 job 失败**:门禁探针过了,但最终多架构构建在 **QEMU arm64** 下第二个 apt 层 OOM(`Cannot allocate memory`)——两次 `apt-get update` + snapshot 源在后续层被反复拉取,内存压力在模拟环境爆掉。结果 panel job 成功、wechat job 失败 → **只发布了面板、没发布实例镜像**
2. **版本耦合无兜底**:面板 1.4.1 因耦合去找 `wechat-on-cloud:1.4.1`(不存在)→ `latestInstanceImageId` 恒 null → 可升级检测全灭;升级也拉不到。一个「防错配」的功能,自己造成了更严重的错配瘫痪。
- **为什么会发生**:
- 产物门禁只覆盖 amd64(gate build 是 `load:true` 单架构),arm64 只在最终 push 步骤才构建 → 门禁没拦住 arm64 的失败;
- 版本耦合是「乐观锁」——假定同版本实例镜像一定存在,没考虑 CI 部分失败/发布时序;
- matrix 两个 job 相互独立,一个失败另一个照发 → 产生「面板新、实例旧」的错配(正是耦合想防的)。
- **修复**:
1. Dockerfile 合并两个 apt 层为一次 update+install、装完即移除 snapshot 源、`Acquire::Languages=none`+`Retries=3` → 砍掉 QEMU 下的内存/网络峰值。**本机原生 arm64 构建 + 全套探针验证通过**;
2. `resolveInstanceImage()` 兜底:偏好的版本 tag 在本地和 registry 都不可达 → 回退 `:latest`,保证功能永不因版本错配而瘫痪(已运行时验证)。
- **仍待办(未在本次)**:改用**原生 arm64 runner**(`ubuntu-24.04-arm`)替代 QEMU,从根上消除模拟内存问题并提速;matrix 失败应联动(实例发不出就别单独发面板)。
- **守则**:R1;门禁原则补充——**门禁必须覆盖所有发布架构**,不能只测 amd64。
## 2026-07-07 · v1.4.0 CI 镜像浏览器实例仍黑屏(Chromium 150 漂移)
- **症状**:v1.4.0 发布后,微信恢复正常,但浏览器(Chromium)实例升级后依然黑屏/只见壁纸无窗口,崩溃循环(每 2s 一次,裸 `Trace/breakpoint trap`,无任何错误输出,**没有** DPI 事故的 "not invertible" 特征)。

View File

@@ -16,13 +16,16 @@ ENV DEBIAN_FRONTEND=noninteractive
# 版本号),并跑通 doc/dev/发布门禁.md 的全部探针后才允许合并。
ARG CHROMIUM_SNAPSHOT=20260704T180435Z
ARG CHROMIUM_VERSION=149.0.7827.196-1~deb12u1
# 单层完成所有 apt 安装(此前拆成两层 → 两次 apt-get update在 CI 的 QEMU arm64 模拟下第二次
# update 处理全部源时 OOM "Cannot allocate memory",实例镜像发不出去)。合并为一次 update + 一次
# install并在装完立刻移除 snapshot 源,避免后续 apt 再去慢速快照站Languages=none 跳过翻译文件、
# Retries 抗网络抖动——共同把 QEMU 下的内存/网络压力降到最低。
RUN set -eux; \
echo "deb [check-valid-until=no] https://snapshot.debian.org/archive/debian-security/${CHROMIUM_SNAPSHOT}/ bookworm-security main" \
> /etc/apt/sources.list.d/woc-chromium-pin.list; \
printf 'Package: chromium*\nPin: version %s\nPin-Priority: 1001\n' "${CHROMIUM_VERSION}" \
> /etc/apt/preferences.d/woc-chromium-pin; \
# 中文字体(否则界面/消息显示方块)+ 语言环境 + 下载/解压工具
apt-get update; \
apt-get -o Acquire::Languages=none -o Acquire::Retries=3 update; \
apt-get install -y --no-install-recommends \
curl ca-certificates locales dpkg \
fonts-wqy-zenhei fonts-wqy-microhei fonts-noto-cjk fonts-noto-color-emoji \
@@ -30,42 +33,20 @@ RUN set -eux; \
xz-utils \
chromium="${CHROMIUM_VERSION}" \
xdotool xclip \
xwallpaper xsettingsd; \
xwallpaper xsettingsd \
# 微信原生版是 Qt 程序,依赖一组 xcb 平台库libxcb-cursor0 由 Qt 动态 dlopenldd 查不到,需主动装。
libatomic1 libxdamage1 libxkbcommon-x11-0 \
libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xkb1 libxcb-cursor0 \
# WeChatAppEx 是 Chromium 内核,需 GTK3 全家桶 + 一组 X 扩展 + cups
libgtk-3-0 libatk1.0-0 libatk-bridge2.0-0 libatspi2.0-0 libcups2 \
libxcomposite1 libxrandr2 libxfixes3 libxtst6 libxshmfence1 libdrm2; \
chromium --version; \
rm -f /etc/apt/sources.list.d/woc-chromium-pin.list /etc/apt/preferences.d/woc-chromium-pin; \
sed -i 's/# zh_CN.UTF-8 UTF-8/zh_CN.UTF-8 UTF-8/' /etc/locale.gen; \
locale-gen; \
apt-get clean; \
rm -rf /var/lib/apt/lists/*
# 微信运行时需要、但官方 deb 未声明的额外库(单独成层,避免动到上面缓存的安装层)。
# 微信原生版是 Qt 程序,依赖一组 xcb 平台库libxcb-cursor0 由 Qt 动态 dlopenldd 查不到,需主动装。
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
libatomic1 \
libxdamage1 \
libxkbcommon-x11-0 \
libxcb-icccm4 \
libxcb-image0 \
libxcb-keysyms1 \
libxcb-render-util0 \
libxcb-xkb1 \
libxcb-cursor0 \
# WeChatAppEx 是 Chromium 内核,需 GTK3 全家桶 + 一组 X 扩展 + cups
libgtk-3-0 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libatspi2.0-0 \
libcups2 \
libxcomposite1 \
libxrandr2 \
libxfixes3 \
libxtst6 \
libxshmfence1 \
libdrm2; \
apt-get clean; \
rm -rf /var/lib/apt/lists/*
# 字体/字体配置目录都在会话 HOME=/config 下(数据卷,运行时才挂载),故不在此预建;
# 由 autostart 在启动时按需 mkdir + 建 ~/.local/share/fonts → /config/.fonts 软链(见 docker/autostart

View File

@@ -20,7 +20,36 @@ function resolveWechatImage(): string {
const repo = m ? m[1] : noDigest;
return `${repo}:${ver}`;
}
const WECHAT_IMAGE = resolveWechatImage();
// 注意:可被 resolveInstanceImage() 在启动时改写为 :latest 兜底(见下),故用 let。
let WECHAT_IMAGE = resolveWechatImage();
// 版本耦合的安全兜底P0issue #112 后续)。版本耦合让面板偏好「与自身同版本」的实例镜像 tag
// 但若该 tag 在本地和 registry 都不存在——典型如某次 CI 只发布了面板、实例镜像构建失败——
// 面板就会指向一个不存在的 tag导致可升级检测恒空指示器消失、创建/升级实例拉取失败。
// 故启动时校验一次:偏好的具体版本 tag 不可达 → 回退 :latest保证功能永不因版本错配而瘫痪。
let imageResolved = false;
export async function resolveInstanceImage(): Promise<void> {
if (imageResolved) return;
imageResolved = true;
const preferred = WECHAT_IMAGE;
const tagM = preferred.split('@')[0].match(/:([^/:]+)$/);
if (!tagM || tagM[1] === 'latest') return; // 已是 latest 或无 tag → 无需兜底
try {
await docker.getImage(preferred).inspect();
return; // 本地已有该版本镜像 → 用它
} catch {
/* 本地没有,继续查 registry */
}
const ref = parseImageRef(preferred);
const remote = ref ? await fetchManifestDigest(ref) : null;
if (remote) return; // registry 上存在该版本 → 用它ensureImage 会拉)
const fallback = preferred.replace(/:[^/:]+$/, ':latest');
appendPanelLog(
'WARN',
`实例镜像 ${preferred} 在本地与镜像仓库均不存在,回退使用 ${fallback}(很可能该版本的实例镜像未成功发布;升级/检测将基于 :latest`,
);
WECHAT_IMAGE = fallback;
}
const PUID = process.env.PUID || '1000';
const PGID = process.env.PGID || '1000';
const TZ = process.env.TZ || 'Asia/Shanghai';
@@ -196,6 +225,7 @@ function envList(inst: Instance): string[] {
// 确保微信镜像在本地存在;缺失则从 GHCR 拉取(首次新建实例时镜像通常还没拉过)。
async function ensureImage(): Promise<void> {
await resolveInstanceImage(); // 版本兜底:若耦合的版本 tag 不可达则先回退 :latest
try {
await docker.getImage(WECHAT_IMAGE).inspect();
return;

View File

@@ -48,6 +48,7 @@ import {
pullImage,
pruneDanglingImages,
remoteInstanceImageNewer,
resolveInstanceImage,
removeInstance as removeInstanceContainer,
instanceRuntime,
instanceImageVersion,
@@ -1440,6 +1441,9 @@ app.server.on('upgrade', (req: IncomingMessage, socket: Socket, head: Buffer) =>
});
});
// 版本兜底:若面板偏好的「同版本实例镜像 tag」不可达则回退 :latest见 docker.ts
// 须在实例检测/升级/启动之前解析好,否则升级指示器会因指向不存在的 tag 而恒空。
await resolveInstanceImage().catch(() => {});
// 探测面板网络 + 重启后把已登记实例的容器拉起来
await ensureNetwork().catch(() => {});
for (const pub of listInstances()) {