一、问题背景
在某次容器部署过程中,执行 docker run 命令后遇到如下错误:
typescript
docker: Error response from daemon: OCI runtime start failed: listen unixgram /var/run/docker/runtime-runc/moby/1e9d6aa4461f4e8352307b75f0580e7edacaa60797c6add24bbb6a9582e0e21f/notify/notify.sock: bind: invalid argument: unknown.
该错误导致容器无法启动,且无论创建多少个新容器,均以相同原因失败。本文将详细分析该问题的根本原因,并提供完整的解决方案。
二、环境信息
2.1 软件版本
| 组件 | 版本 |
|---|---|
| 操作系统 | Kylin V10 SP3 |
| 内核版本 | 4.19.90-89.26.v2401.ky10.x86_64 |
| Docker | 18.09.0(EulerVersion: 18.09.0.261) |
| containerd | 1.2.0.213.p05.ky10(升级后:1.2.0-213.p09.ky10) |
| runc | 1.1.12(commit: v1.1.12-0-g51d5e946) |
| Go 版本 | go1.15.7 |
| libseccomp | 2.5.4 |
| SELinux | Disabled |
2.2 系统环境检查命令
bash
# 查看操作系统版本
cat /etc/os-release
# 查看内核版本
uname -r
# 输出:4.19.90-89.26.v2401.ky10.x86_64
# 查看 Docker 版本
docker version
# 查看 containerd 版本
containerd --version
# 查看 runc 版本
runc --version
# 查看 SELinux 状态
getenforce
# 输出:Disabled
# 查看 Docker 服务配置
systemctl cat docker
# 查看 Docker 日志(最后 100 行)
sudo journalctl -u docker -n 100 --no-pager
2.3 容器信息
- 镜像地址 :
ouma-registry.cn-hangzhou.cr.aliyuncs.com/jiangxi/ygj:ygj-gateway-jx-prod-2.0 - 容器名称 :
ygj-gateway - 端口映射 :
-p 8080:8080 - 环境变量 :
PROFILE=prod
三、错误分析
3.1 错误信息解读
错误信息中的关键部分是 listen unixgram .../notify/notify.sock: bind: invalid argument。这表明 runc(OCI 容器运行时)在尝试创建一个 Unix 域套接字(Unix Domain Socket)时,bind() 系统调用返回了 EINVAL(无效参数)错误。
notify.sock 是 runc 用于与容器内 systemd 通信的套接字。Docker 在启动容器时,runc 会尝试在 /var/run/docker/runtime-runc/moby/<容器ID>/notify/ 目录下创建该套接字文件。
3.2 根本原因:Unix 域套接字路径长度限制
在 Unix/Linux 系统中,sockaddr_un 结构体中的 sun_path 字段最大长度为 108 字节 (包括结尾的空字符)。这是 POSIX 标准定义的限制,超过此限制会导致 bind() 或 connect() 系统调用失败。
计算一下上述错误中的路径长度:
javascript
/var/run/docker/runtime-runc/moby/ (31 字符)
+ 64 位容器 ID (64 字符)
+ /notify/notify.sock (17 字符)
= 112 字符
112 > 108 ,路径长度超限,导致 bind 失败。
3.3 验证:用 containerd 原生工具测试
为了确认问题是否出在 Docker 层,使用 containerd 的原生 CLI 工具 ctr 直接启动容器:
bash
ctr run -d --net-host --env PROFILE=prod \
ouma-registry.cn-hangzhou.cr.aliyuncs.com/jiangxi/ygj:ygj-gateway-jx-prod-2.0 \
ygj-gateway
结果:容器成功启动,日志中显示应用正常运行。
这说明底层 runc 和内核均正常工作,问题被锁定在 Docker 18.09 的 notify.sock 创建逻辑 上。ctr 命令不创建 notify.sock,因此绕过了该问题。
四、解决方案
4.1 方案对比
| 方案 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
| 升级 Docker 到 20.10+ | 根本解决,保留原生 Docker | EulerOS 可能无官方包,升级有风险 | ⭐⭐⭐⭐ |
| 安装 nerdctl 替代 Docker | 语法 100% 兼容,绕开 Docker bug,安装简单 | 需额外安装工具 | ⭐⭐⭐⭐⭐ |
| 使用 ctr 命令 | 无需装新工具 | 不支持 -p 端口映射,操作不便 |
⭐⭐ |
| 缩短 exec-root 路径 | 不引入新组件 | 仅缓解,不保证彻底解决 | ⭐⭐⭐ |
4.2 推荐方案:安装 nerdctl
nerdctl 是 containerd 官方出品的 Docker 兼容 CLI 工具,直接与 containerd 通信,绕过了 Docker daemon 的 notify.sock 创建逻辑。绝大多数 docker 命令都可以无缝替换为 nerdctl。
安装步骤
步骤一:下载 nerdctl-full
推荐使用 full 版本,它包含了 containerd、runc、CNI 网络插件等所有必需组件。
bash
cd /tmp
wget https://github.com/containerd/nerdctl/releases/download/v2.3.5/nerdctl-full-2.3.5-linux-amd64.tar.gz
注:nerdctl v2.3.5 兼容 containerd v1.7、v2.0、v2.1、v2.2 和 v2.3。
步骤二:解压安装
sql
sudo tar Cxzvvf /usr/local nerdctl-full-2.3.5-linux-amd64.tar.gz
解压到 /usr/local 后,所有二进制文件(nerdctl、containerd、runc、buildctl 等)会自动安装到 /usr/local/bin。
步骤三:验证安装
bash
nerdctl --version
# 输出示例:nerdctl version 2.3.5
步骤四:配置 CNI 网络(如需要)
full 版本已包含 CNI 插件,但需确认配置文件存在:
bash
sudo mkdir -p /etc/cni/net.d
sudo tee /etc/cni/net.d/nerdctl-bridge.conflist << 'EOF'
{
"cniVersion": "1.0.0",
"name": "bridge",
"plugins": [
{
"type": "bridge",
"bridge": "nerdctl0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"ranges": [[{"subnet": "10.89.0.0/24"}]],
"routes": [{"dst": "0.0.0.0/0"}]
}
},
{
"type": "portmap",
"capabilities": {"portMappings": true}
}
]
}
EOF
4.3 启动容器
安装完成后,即可用 nerdctl 替代 docker 启动容器,命令语法完全一致:
bash
# 清理之前的残留
ctr container rm -f ygj-gateway 2>/dev/null
docker rm -f ygj-gateway 2>/dev/null
# 用 nerdctl 启动
nerdctl run -d \
-e PROFILE=prod \
--name ygj-gateway \
-p 8080:8080 \
ouma-registry.cn-hangzhou.cr.aliyuncs.com/jiangxi/ygj:ygj-gateway-jx-prod-2.0
4.4 验证容器运行
bash
# 查看容器状态
nerdctl ps
# 查看日志,确认环境变量生效
nerdctl logs ygj-gateway
# 测试服务
curl http://localhost:8080/actuator/health
4.5 常用命令对照
| 功能 | Docker 命令 | nerdctl 命令 |
|---|---|---|
| 运行容器 | docker run -d --name xxx -p 8080:8080 image |
nerdctl run -d --name xxx -p 8080:8080 image |
| 查看容器 | docker ps |
nerdctl ps |
| 查看日志 | docker logs xxx |
nerdctl logs xxx |
| 停止容器 | docker stop xxx |
nerdctl stop xxx |
| 删除容器 | docker rm xxx |
nerdctl rm xxx |
| 查看镜像 | docker images |
nerdctl images |
| 拉取镜像 | docker pull xxx |
nerdctl pull xxx |
| 进入容器 | docker exec -it xxx bash |
nerdctl exec -it xxx bash |
五、技术原理深度解析
5.1 为什么 nerdctl 能解决问题?
Docker 18.09 在启动容器时,runc 会额外创建一个 notify.sock 套接字用于与容器内 systemd 通信。该套接字的路径由 Docker 硬编码生成,格式为:
bash
/var/run/docker/runtime-runc/moby/<64位容器ID>/notify/notify.sock
当容器 ID 为 64 位十六进制字符串时,完整路径长度恰好超过 108 字符限制。
而 nerdctl 作为 containerd 的客户端,直接与 containerd 通信,不经过 Docker daemon,因此绕过了 Docker 创建 notify.sock 的逻辑。containerd 本身不创建这个套接字,所以不会触发路径长度限制。
5.2 为什么 ctr 可以但 docker 不行?
ctr 是 containerd 的原生 CLI,与 nerdctl 一样直接与 containerd 通信。它不创建 notify.sock,因此可以成功启动容器。
但 ctr 的局限在于不支持 -p 端口映射 ,只能使用 --net-host 让容器共享宿主机网络。而 nerdctl 完整支持 -p 参数,与 Docker 体验一致。
5.3 路径长度计算验证
bash
# 获取容器 ID
CID="1e9d6aa4461f4e8352307b75f0580e7edacaa60797c6add24bbb6a9582e0e21f"
# 计算路径长度
echo -n "/var/run/docker/runtime-runc/moby/${CID}/notify/notify.sock" | wc -c
# 输出:112
六、总结
6.1 问题根因
Docker 18.09 在启动容器时创建的 notify.sock 套接字路径(112 字符)超过了 Linux 系统对 Unix 域套接字路径的 108 字符限制,导致 bind() 系统调用失败。
6.2 解决方案总结
安装 nerdctl-full 作为 Docker 的替代 CLI,直接与 containerd 通信,绕开 Docker 的缺陷。nerdctl 与 Docker 命令 100% 兼容,迁移成本极低。
6.3 经验教训
- Unix 域套接字路径限制:在容器化环境中,路径往往多层嵌套,容易超过 108 字符限制,需要特别注意。
- 老版本 Docker 的已知 Bug:Docker 18.09 及更早版本存在此问题,建议升级到 20.10+ 或使用 nerdctl 替代。
- containerd 生态日趋成熟:nerdctl 作为 containerd 的官方 CLI,已能很好地替代 Docker CLI,值得关注和采用。
- 问题定位方法论 :当 Docker 启动失败时,可用
ctr命令绕过 Docker 层测试,快速判断问题在 Docker 侧还是底层运行时侧。