docker daemon“假死”是线程级死锁所致,表现为docker ps卡住、api无响应但进程存活,根因多为cgroup v2 setup或namespace初始化阶段的goroutine阻塞;需通过sigusr1获取goroutine堆栈定位锁竞争点,并结合ebpf追踪锁持有者与阻塞路径,典型诱因包括cgroup_mutex争抢、nsproxy_mutex竞争及shim socket写锁拖累。
当 docker 客户端命令(如 docker ps、docker info)长时间无响应,但 ps aux | grep dockerd 显示进程仍在运行,基本可判定是守护进程(dockerd)发生了线程级死锁,而非崩溃退出。这类“假死”故障的核心在于 goroutine 阻塞在同步原语上,导致主事件循环无法处理新请求。
确认是否为假死而非彻底失效
先排除常见干扰项,避免误判:
- 检查进程存活且无响应:
kill -0 $(pgrep dockerd) && echo "alive" || echo "dead" - 验证 API 是否失联:
timeout 3s curl -f http://unix:///var/run/docker.sock/v1.43/info >/dev/null 2>&1 && echo "responsive" || echo "unresponsive" - 观察线程数是否异常膨胀:
ps -o pid,tid,nlwp,comm -T $(pgrep dockerd) | tail -n +2 | wc -l—— 若远超 300~500,说明大量 goroutine 卡住
抓取 goroutine 堆栈快照定位阻塞点
Docker 是 Go 程序,支持通过 SIGUSR1 触发运行时堆栈 dump,这是诊断死锁最直接有效的手段:
- 向 dockerd 主进程发送信号(注意:必须是真正主进程 PID,不是 systemd wrapper):
sudo kill -USR1 $(pgrep -f "dockerd.*--config-file" | head -n1) - 立即查看日志输出位置:
sudo journalctl -u docker.service --no-pager -n 200 | grep -A 50 "goroutine [0-9]* \[" - 重点关注处于
semacquire、sync.(*Mutex).Lock、runtime.gopark状态的 goroutine,尤其在runc.create、cgroup.v2.setup、nsproxy.init等调用链中出现的阻塞
结合 eBPF 追踪锁持有者与阻塞路径
仅看堆栈有时难以判断谁持有了锁、谁在等锁。需借助内核级观测工具补全上下文:
- 用
bpftrace监控cgroup_mutex或nsproxy_mutex的争抢:bpftrace -e 'kprobe:cgroup_mutex_lock { printf("lock held by %s\n", comm); }' - 追踪 shim socket 写操作延迟(常见于容器高频启停):
sudo ./trace -p $(pgrep containerd-shim) write(需提前部署trace工具) - 若发现某 goroutine 长时间停留在
writev或sendto,且对端是containerd-shim,说明 shim 侧响应慢或僵死,需进一步 inspect shim 进程状态
排查高频创建场景下的典型诱因
死锁多发于边缘设备或 CI/CD 流水线等高频容器创建环境,重点检查以下环节:
-
cgroup v2 初始化竞争:确认系统启用 cgroup v2 且 dockerd 启动参数未显式禁用;检查
/proc/$(pgrep dockerd)/cgroup是否显示0::/ -
namespace 创建阻塞:在堆栈中搜索
clone、unshare、setns调用,若大量 goroutine 卡在此处,可能与内核 namespace 配额或 SELinux 策略有关 -
shim 进程堆积或僵死:执行
ps aux | grep shim,若数量远超当前运行容器数,或存在Z(僵尸)状态,说明 shim 生命周期管理异常











