容器一启动就以 exec format error 退出时,人们往往想到错误的方向:添加能力、挂载设备、修改隧道配置。这些做法都碰不到问题。内核根本无法执行该二进制,而在容器里这几乎总是意味着镜像面向的是与主机不同的 CPU 架构。你能在路由、凭据或权限上改动的一切,都发生在程序启动之后,而根本没有程序启动。

看起来像配置错误的启动信息

exec 格式失败来自加载器,发生在应用读取任何一项设置之前。因此它既不产生配置警告,也不产生隧道日志,因为没有活着的进程去写。失败还快得惊人,往往在容器日志出现任何一行之前就已发生。同一条简短信息会出现在二进制面向其他架构、其他操作系统或已损坏时。

把运行环境和架构与确切的镜像引用一并记录,然后检查该引用实际提供了什么。在 Docker Desktop 上,容器平台属于 Linux 虚拟机而不是笔记本,所以只看主机架构可能会误判。阅读该引用的索引条目列表,而不是其标签页上的营销文字。

运行命令之前先读镜像索引

当注册表发布多平台引用时,标签指向一个索引,每个条目都给出带自身 digest 的平台。检查正在审阅的那个引用,而不是相邻标签,并阅读索引,而不是假设其内容。

docker buildx imagetools inspect ghcr.io/example/vpn:1.4
docker image inspect --format '{{.Os}}/{{.Architecture}}' ghcr.io/example/vpn:1.4

第一条命令列出该引用发布的平台。第二条报告本地已有镜像的平台,那才是运行时会执行的。只有一条条目的引用意味着每台主机拿到同一个二进制,而该标签帮不了从未为它构建过的机器。

标签只是选择变体,并不创造变体

多平台索引让运行时挑选匹配的条目,这确实方便,却常被误当成可移植性。选择是查找,不是翻译:变体不存在,就没有可选的。因此同一标签在两台主机上可能解析为两个不同的二进制,在一台主机上则解析不出任何可用之物。

用 digest 固定消除了歧义,也让日后复核成为可能,因为 digest 精确定位了被测试的字节。把该 digest 与部署记录放在一起,而不是依赖当时碰巧是当前的那个标签。

模拟是一种安排,不是翻译层

通过受支持的运行时进行模拟,可以运行一些非原生用户空间工作负载,所以“一种架构永远无法运行另一种架构的镜像”这一绝对说法是错的。它也不是通用解决方案。模拟执行有不同的性能特征,而一个能启动的简单进程并不能说明隧道工作负载的表现是否可接受。

Docker 的多平台指南把原生构建、交叉编译和模拟当作不同策略,这才是阅读这些选项的正确方式。刻意选择其中一种,并在其下验证 VPN 工作负载,而不是假定等价。

权限解决的是另一个问题

授予网络管理能力或透传 TUN 设备,都不能让不兼容的可执行文件运行起来。这些是运行中进程使用的运行时权限;对已经拒绝该文件的加载器来说,它们是不可见的。

反过来也同样成立。原生二进制并不能证明权限、设备访问、解析器集成或隧道所需重启策略,所以能启动的容器还不等于能正常工作的容器。先确立平台约定,再带着有据可查的选项进入运行时约定。

把镜像 digest、受支持的目标和配置放在同一条记录里。会悄悄改变架构的标签更新,在写下旧 digest 时更容易被发现,而如果唯一症状是一个熟悉的错误和又一个下午的调试,就更难察觉。

相关文章