docker 重启非 host 网络容器造成 dns 异常的梳理和pr修复
由来
内部产品是 toB 和 toG,针对 toG 的完全内网,测试会在搭建的 K8S 集群上所有节点配置个假的 DNS,这样黑盒下测功能,避免业务访问公网而造成功能问题。但是有些后续新业务是依赖公网的,测试测完没公网的部分后就会配置真实 dns 后测这部分功能,之前就遇到过好几次配置节点 DNS 后,个别 Pod 内 DNS 内容不是 k8s 的,而是下面这样类似变成宿主机:
1 | # Generated by Docker Engine. |
我们是使用的 cri-dockerd + k8s 组合。
过程
之前反馈了几次,但是一直没稳定复现手段,就先去看了下大概这块 docker 源码,然后给测试说,下次开发环境遇到了别删除 Pod 和对应容器,直接喊我,2025/11/11 下午反馈找到稳定复现步骤了,我们有个 dashboard,测试环境上开发在上面重启了他的 Pod 下的容器,该 Pod 的 spec.containers 只有一个,勾选的是类似 docker ps -a 的那样,/pause 容器和他的容器都勾选点重启的。上面点了下确实发生了。
日志
找到容器所在节点上去看:
1 | $ kubectl get pod -o wide -A | grep ai-aixxxxapi |
容器 id e4c64897be98 找下日志看看:
1 | journalctl -xe --no-pager -u docker | grep -P '96ea991f9bb3|e4c64897be98' |
也看下容器时间相关:
1 | docker inspect e4c |
也看下 cri-dockerd 的日志:
1 | journalctl -xe --no-pager -u cri-dockerd | grep e4c64897be9891d88b999e81bfd55bb0cc1c21d626708749691d43158062f2bb |
根据上面日志总结时间线:
15.44.47创建/pause容器15:45:03cri-dockerd 拉完业务镜像后创建 sandbox 容器,re-write 了容器的resolv.conf,这块源码逻辑可以搜Will attempt to re-write15.52.45重启了容器- 容器的
resolv.conf根据 mtime 看发生改变
最小复现
后端重启容器逻辑是同事写的,我记得大概逻辑是 python docker client 调用 docker 重启的,直接二分,如果 docker restart 复现了就不是 dashboard 后端逻辑造成的。然后环境上复现了,然后自己搭建个干净 K8S 也复现了。
1 | cat testpod.yml |
单节点,如果固定节点的话设置下 nodeName 即可,复现:
1 | docker ps -a | grep testpod |
Pod 的创建流程是容器运行时先创建一个 /pause 容器,然后 pod.spec.containers 的容器会 join 到 /pause 上,而 docker 下容器的 hosts、resolv.conf 和 hostname 这些是单独一层 init 层处理的,会创建文件,Pod 的所有容器都使用同一份:
1 | docker ps -a | grep testpod |
然后发现最小化复现是只重启 /pause 容器发生:
1 | docker ps -a | grep testpod |
相关源码
既然确定是 docker 逻辑造成,那就需要看下这块源码逻辑了,可以看到有生成注释的,代码里搜关键字 Generated by Docker Engine. 搜到:
1 | // https://github.com/moby/moby/blob/v26.1.4/libnetwork/sandbox_dns_unix.go#L248-L278 |
逆向思维,搜了下 loadResolvConf( 发现有在
func (sb *Sandbox) setupDNS() error {func (sb *Sandbox) updateDNS(ipv6Enabled bool) error {func (sb *Sandbox) rebuildDNS() error {
断点调试
golang 二进制调试构建的时候需要设置 -gcflags=all=-N -l 才能 dlv 调试,编译 dockerd 开启调试:
1 | DOCKER_DEBUG=1 ./hack/make.sh binary-daemon |
替换启动,找到 pid:
1 | systemctl stop docker |
然后附加 pid 上调试,dlv 打了几个断点后发现在 setupDNS() 里:
1 | go install github.com/go-delve/delve/cmd/dlv@master |
然后另一个终端上重启下 /pause 容器,这边终端的 dlv 就走到断点了:
1 | (dlv) c |
分析
大致看了下 docker 这块逻辑:
1 | // https://github.com/moby/moby/blob/v26.1.4/libnetwork/sandbox_dns_unix.go#L280C1-L296C2 |
sb.restoreResolvConfPath() 填充变量,也就是实际的 ResolvConfPath 和他的 .hash 文件:
1 | func (sb *Sandbox) restoreResolvConfPath() { |
sb.loadResolvConf 方法就是把 Linux 的 DNS 内容解析成结构体,传入的 sb.config.getOriginResolvConfPath() 是获取宿主机的 dns 文件路径:
1 | (dlv) n |
机器如果使用了 systemd-resolv 下 originResolvConfPath 则会是 /run/systemd/resolve/resolv.conf, 然后继续调试:
1 | (dlv) list |
有个 hash 文件,那看起来就最下面的 rc.WriteFile 改写的内容了,看下它的逻辑:
1 | // https://github.com/moby/moby/blob/v26.1.4/libnetwork/internal/resolvconf/resolvconf.go#L373-L402 |
content 就是生成最后写入的内容写入,以及计算当前 content 的 hash 写入到 .hash 文件。这个逻辑是 docker daemon 检查到容器的 resolv.conf 文件的计算 hash 对不上 .hash 保存的的,就说明客户修改过容器的 DNS,docker daemon 不再修改它。
那还是得看 setupDNS() 的更上层调用,搜到 setupResolutionFiles(),而调用它的有两个:
func (c *Controller) NewSandbox(func (sb *Sandbox) Refresh(
俩都打断点看看:
1 | dlv attach 19035 |
走的 NewSandbox ,打印下 backtrace:
1 | (dlv) bt |
以及另一块大概的相关逻辑:
1 | 0 0x000000000247da42 in github.com/docker/docker/libnetwork.(*Endpoint).sbJoin |
sandbox change
1 | $ docker ps -a | grep testpod |
可以看到 sandbox 变了导致重建了 DNS。
containerd 没复现
顺便找人 containerd + nerdctl 试了下没问题:
1 | nerdctl -n k8s.io ps -a | grep zgz |
containerd 没复现
重启偶尔重建Pod
偶尔发现重启 /pause 容器被 k8s 重建了
1 | docker restart c213ae917535 |
K8S 源码里搜 Pod sandbox changed 大致看了下这块 kubelet 代码,就是刚好检测 Pod 的 sandbox 状态和重启行为重合就会启动新的容器避免了这个问题,但是这种情况发生的概率不是百分之百重合。
修复
从以上结论来看,kubelet 没问题,cri-dockerd 也没问题(它 re-write 行为和人为修改一样),只可能在 docker 方面修复了,根据上面调用链,restart 容器实际走的:
1 | (dlv) bt |
看了下就是根据 restart 的 id 获取 containerConfig,containerRestart 调用逻辑是先 stop 在 start,由于是 sandbox,所以重建了一个 sandbox,重建的关键地方在 container_operations.go 内,使用 stop 之前的 containerConfig, 创建 SandboxOption 的 slice 。
1 | sbOptions, err := buildSandboxOptions(cfg, ctr) |
想法是增加一个 SandboxOption 设置 sandbox 的结构体增加一个 flag ,来判断 SandBox 是否由重启行为创建,然后写相关逻辑处理就行,pr 改动如下:
1 | diff --git a/daemon/container_operations.go b/daemon/container_operations.go |
2025/11/12 提交 pr https://github.com/moby/moby/pull/51507 后,单元测试啥的都在 github action 里跑过了,然后 docker member 发现一个类似问题:
1 | docker run -d --name hello nginx:alpine |
按照我修复后的代码测试了下,也可以解决这个问题,docker libnetwork 负责人 akerouanton 说我这种重启 /pause 是非标行为,但是上面这种默认桥接网络也会发生,他给我 code review了下。
reivew 评论说 createdByRestart 标志会无效,当 docker 宕机以及类似掉电情况下就无效了,让我去掉这个选项。
仔细想了下确实,意外情况下不会走正常的 restart 流程去应用上这个选项,后面就是讨论和 2025/11/26 合入了,预计 docker v29.0.5 版本带出。