简述 Docker 与 CNI 的调用链路
解读
国内面试官问“Docker 与 CNI 的调用链路”,并不是想听长篇源码,而是考察候选人是否真正在生产环境排过网络故障。
一句话:当 Docker 收到 docker run 或 docker network create 指令时,它如何把“插网线”这件事交给 CNI 插件完成,又如何把结果返回给容器?
答不出“谁调谁、参数怎么传、事件在哪看”,就会被判定为“只用过 docker run,没玩过 K8s 或 Swarm 生产”。
知识点
- Docker 网络后端模式:bridge/host/overlay/macvlan/remote;只有 remote 驱动 才会走 CNI。
- CNM vs CNI:Docker 原生是 CNM(Network/Endpoint/Sandbox),而 CNI 是 K8s 社区标准;两者数据模型不同,需要 docker-containerd-shim 层做翻译。
- 调用入口:dockerd → containerd → containerd-shim → libcni(CNITool 代码包)。
- libcni 三件套:ADD / DEL / CHECK,参数通过 stdin 传 JSON(CNI_CONFIG、CNI_IFNAME、CNI_ARGS)。
- 结果回写:插件把 veth 名称、IP、路由、DNS 返给 libcni,shim 再把结果写回 Sandbox,最终 dockerd 把网卡挂到容器 namespace。
- 排障锚点:
- /var/log/messages 里 “cni0: ADD failed” 代表插件返回非零;
- /opt/cni/bin 下可手动 CNITool add calico < /tmp/net.conf 复现;
- iptables-save | grep CNI 能看到链是否被注入。
答案
Docker 默认用 bridge 驱动,不走 CNI;只有当集群选型为 Swarm + overlay 并启用 userland-proxy=false,或 K8s 调用 Docker 作为 runtime 时,链路才切换到 CNI。
流程如下:
- dockerd 收到创建容器请求,把网络模式解析为 remote;
- dockerd 通过 gRPC 把 NetworkCreate 请求 发给 containerd;
- containerd 调用 containerd-shim-runc-v2,shim 里内置 libcni;
- libcni 按顺序执行 /opt/cni/bin 下的插件:
- bridge 插件 先创建 cni0 网桥并分配 veth pair;
- host-local 插件 把空闲 IP 写回 /var/lib/cni/networks;
- portmap 插件 加 iptables DNAT 规则;
- 插件把结果 JSON(IP、路由、DNS)返给 libcni,shim 再把网卡移入容器 netns;
- dockerd 收到 “network ready” 事件,启动容器主进程;
- 容器停止时,shim 调用 libcni DEL,插件回收 IP、清掉 iptables,完成资源释放。
整条链路中,dockerd 只负责把 CNI_CONFIG 和 netns 路径传下去,真正的网络设备生命周期由 CNI 插件全权接管。
拓展思考
- 国内金融客户常把 Calico + eBPF 模式 当合规标配,此时 CNI 不再用 veth,而直接复用宿主机路由表;若仍用 Docker 做 runtime,需要 关闭 docker0 网桥,防止双路由冲突。
- 排查“容器有 IP 但 ping 不通 Service”时,先 tcpdump -i cni0 看有没有 ARP 回复,再 iptables -t nat -L CNI-HOSTPORT-DNAT 确认端口映射链是否被 K8s 网络策略清空。
- 如果未来节点升级到 containerd + CRI 插件,Docker 被裁掉,这条链路就简化为 kubelet → cri-containerd → libcni,但 CNI 插件本身不变,因此现在吃透 Docker-CNI 交互,后续切 runtime 零成本。