Docker 网络进阶:容器间通信与 DNS 解析
IT策士 10余年一线大厂经验专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章助你少走弯路。第 8 篇我们打开了 Docker 网络的大门介绍了桥接网络、端口映射和 host 模式。你可能会好奇多个容器在自定义网络中能够通过容器名互相访问这背后到底发生了什么app.py里写的hostredis为什么能神奇地定位到 Redis 容器的 IP如果你创建了多个副本比如三个 Flask 实例它们之间如何发现彼此答案的关键就是Docker 内嵌的 DNS 服务器。这一篇我们将深入探讨容器间通信的完整链路把网络背后的“翻译官”彻底剖析清楚。一、容器间通信的核心DNS 服务发现在第 8 篇中我们看到了默认桥接网络和自定义桥接网络的本质区别后者内置了 DNS 解析功能。这个 DNS 服务器运行在127.0.0.11地址上并且被注入到每个连接到自定义网络的容器的/etc/resolv.conf中。你可以进入任何一个连接到自定义网络的容器验证这一点# 先启动两个容器在自定义网络 my-net 中dockernetwork create my-netdockerrun-d--nameweb--networkmy-net nginx:alpinedockerrun-d--nameapp--networkmy-net alpinesleep3600# 查看 app 容器的 DNS 配置dockerexecappcat/etc/resolv.conf输出会类似nameserver127.0.0.11 options ndots:0nameserver 127.0.0.11告诉容器所有 DNS 查询都发给本地的嵌入式 DNS 服务器。这个 DNS 服务器由 Docker 守护进程维护它会根据容器名、网络别名、服务名在 Docker Compose 或 Swarm 模式下动态返回对应的 IP 地址。二、DNS 解析的工作流程当一个容器尝试解析另一个容器的名字时数据包会经历以下步骤发起查询容器内应用调用getaddrinfo(redis)或执行ping redis。本地 DNS 请求容器的 DNS 客户端根据/etc/resolv.conf向127.0.0.11:53发送 UDP 查询。嵌入式 DNS 处理Docker 守护进程在宿主机的网络命名空间中捕获到这个 DNS 请求通过 iptables 规则将流量转发到 Docker 引擎。Docker 在内部维护了一张映射表记录了容器名 → IP的对应关系。返回 IP如果查询的容器名在当前网络中Docker 直接返回该容器的 IP如果不是Docker 会将查询转发到宿主机的 DNS或你自定义的外部 DNS。缓存解析结果会被缓存默认 TTL 通常较短提高后续查询速度。整个过程对于容器内的应用完全透明——它只需要知道目标服务的名称剩下的都由 Docker 网络栈接管。这种设计正是 Kubernetes Service 抽象的前身。在 K8s 中你访问my-serviceCoreDNS 返回的是 Service 的 ClusterIP而非单个 Pod IP但基于名称的稳定访问这一理念完全一致。三、实验深入理解 DNS 行为现在我们动手验证 DNS 解析的三种典型场景。3.1 容器名解析# 创建自定义网络dockernetwork create test-dns# 启动两个容器dockerrun-d--nameserver--networktest-dns nginx:alpinedockerrun-d--nameclient--networktest-dns alpinesleep3600# 从 client 解析 server 的 IPdockerexecclientnslookupserver输出示例Server:127.0.0.11 Address:127.0.0.11:53 Non-authoritative answer: Name: server Address:172.18.0.2nslookup明确显示 DNS 服务器就是127.0.0.11并且返回了server容器的 IP这里是172.18.0.2实际 IP 取决于网络子网分配。3.2 网络别名一个容器多个名字Docker 允许你为一个容器指定多个别名这在服务发现中极为实用。例如一个 Redis 容器除了自己的容器名外还可以有一个统一的cache别名# 启动 Redis 容器并指定一个网络别名 --network-aliasdockerrun-d--namemy-redis--networktest-dns --network-alias cache redis:alpine# 从 client 容器用别名访问dockerexecclientnslookupcache输出Server:127.0.0.11 Address:127.0.0.11:53 Non-authoritative answer: Name: cache Address:172.18.0.3这就意味着你可以通过cache这个名字来访问 Redis而不管实际的容器名是my-redis还是redis-prod-v2。Kubernetes 的 Service 正是这个模式的延伸Service 名称作为稳定的 DNS 记录后端 Pod 的 IP 可以随时变化。如果你再启动一个同样有cache别名的容器DNS 会以**轮询round-robin**方式返回多个 IP实现基本的客户端负载均衡dockerrun-d--namemy-redis2--networktest-dns --network-alias cache redis:alpine# 多次查询 cache观察返回的 IP 交替dockerexecclientnslookupcache# 第一次可能返回 172.18.0.3dockerexecclientnslookupcache# 第二次可能返回 172.18.0.4这就是 Docker 层面的“服务发现”和“负载均衡”雏形。当你进入 Kubernetes 时Service 的 ClusterIP 配合 kube-proxy 实现的正是同一套思想只不过规模更大、机制更完善。3.3 跨网络通信容器连接多个网络默认情况下不同网络的容器是相互隔离的。但你可以让一个容器加入多个网络充当桥梁。# 创建两个独立的网络dockernetwork create net1dockernetwork create net2# 在 net1 中启动一个容器dockerrun-d--nameapp-in-net1--networknet1 alpinesleep3600# 在 net2 中启动另一个容器dockerrun-d--nameapp-in-net2--networknet2 alpinesleep3600# 让 app-in-net1 也加入 net2dockernetwork connect net2 app-in-net1# 现在 app-in-net1 可以解析 app-in-net2dockerexecapp-in-net1ping-c2app-in-net2# 输出64 bytes from app-in-net2.net2 (172.19.0.2): ...这种方式常用于实现“前端-后端”的分层隔离我们在第 8 篇已演示过。一个更符合生产实践的用法是反向代理容器如 Nginx连接前端网络和后端网络成为两个网络之间的唯一入口——这和 K8s Ingress Controller 的架构角色完全一致。四、容器与宿主机通信数据包的完整旅程容器不仅需要与其他容器通信还需要访问宿主机上的服务或通过宿主机访问外网。理解这些场景的通信链路可以帮你快速定位网络故障。4.1 容器访问宿主机在 Docker for Mac/Windows 中可以使用特殊域名host.docker.internal来访问宿主机dockerrun--rmalpineping-c2host.docker.internal在 Linux 下这个域名默认不可用Docker Desktop for Linux 除外但你可以使用--add-host参数手动添加或直接使用 docker0 网桥的 IP通常是172.17.0.1dockerrun--rm--add-host host.docker.internal:host-gateway alpineping-c2host.docker.internalhost-gateway是 Docker 20.10 引入的特殊值会自动解析为宿主机的网关 IP。如果你的宿主机上运行着数据库或 API 服务容器通过这个地址就能访问到。4.2 容器访问外网容器访问外网依赖宿主机的 IP 伪装MASQUERADE。数据包路径为容器 → veth → docker0 网桥 → 宿主机 NAT修改源 IP→ 物理网卡 → 外网。你可以用iptables验证 NAT 规则的存在sudoiptables-tnat-LPOSTROUTING|grepMASQUERADE输出示例MASQUERADE all --172.17.0.0/160.0.0.0/0 MASQUERADE all --172.18.0.0/160.0.0.0/0这些规则确保容器的私有 IP 被替换为宿主机的真实 IP从而使外部网络能够正确回包。4.3 外部访问容器外部请求到达宿主机指定端口后由 iptables DNAT 规则将目标 IP 重写为容器 IP然后通过 docker0 网桥送达容器。这一整条路径我们在第 8 篇已经分析过但值得再次强调所有端口映射的可靠性都建立在 iptables 规则的正确性之上。当容器“莫名其妙连不通”时检查 iptables 规则是否被意外修改是一个有效排查方向。五、实战Flask Redis 计数器应用的网络通信验证现在我们回到贯穿本系列的 Flask Redis 计数器应用专门验证它的 DNS 解析和网络通信流程。在进入本篇实验前请确保你已经按照前几篇的步骤构建了flask-redis-counter:2.0镜像。# 创建网络dockernetwork create counter-net# 启动 Redis并给一个网络别名dockerrun-d--nameredis--networkcounter-net --network-alias cache redis:alpine# 启动 Flask 应用注意连接同一网络dockerrun-d--nameflask-app--networkcounter-net-p5000:5000 flask-redis-counter:2.0# 查看容器状态dockerps--formattable {{.Names}}\t{{.Status}}\t{{.Ports}}输出NAMES STATUS PORTS flask-app Up30seconds(healthy)0.0.0.0:5000-5000/tcp redis Up30seconds6379/tcp现在让我们深入flask-app容器内部验证它的 DNS 配置和 Redis 发现过程# 1. 查看 Flask 容器的 DNS 配置dockerexecflask-appcat/etc/resolv.conf# nameserver 127.0.0.11# options ndots:0# 2. 使用 nslookup 验证 Redis 的名称解析dockerexecflask-appnslookupredis# Server: 127.0.0.11# Address: 127.0.0.11:53# Non-authoritative answer:# Name: redis# Address: 172.18.0.2# 3. 使用别名 cache 解析与 redis 相同 IPdockerexecflask-appnslookupcache# Name: cache# Address: 172.18.0.2# 4. 测试应用功能curlhttp://localhost:5000# Hello World! I have been seen 1 times.Flask 应用里的redis.Redis(hostredis, port6379)之所以能工作正是因为容器的 DNS 解析将redis翻译成了 Redis 容器的 IP172.18.0.2。整个过程无需手动配置 IP无需硬编码完全是 Docker 网络栈在背后默默工作。增加副本验证 DNS 轮询如果我们启动另一个 Redis 实例模拟集群或读写分离并且给它相同的别名cache# 启动第二个 Redis 容器给予相同的网络别名dockerrun-d--nameredis-2--networkcounter-net --network-alias cache redis:alpine# 从 Flask 容器内多次解析 cachedockerexecflask-appnslookupcache# 可能返回两个 IP 地址轮询你可以观察到nslookup cache会交替返回redis和redis-2的 IP。这展示了 Docker 内置的 DNS 负载均衡能力。当你的应用只需要一个逻辑服务名如cache而不关心有多少个后端实例时网络的灵活性就体现出来了。六、故障排查DNS 解析失败怎么办在实际使用中你可能会遇到“容器名解析不了”的情况。以下是常见的排查步骤。6.1 检查容器是否在同一网络这是最常见的原因。两个容器必须连接到同一个自定义网络才能通过容器名互相解析。# 检查容器的网络信息dockerinspect flask-app--format{{json .NetworkSettings.Networks}}|python3-mjson.tool查看输出中是否包含counter-net。如果 Redis 容器不在这个网络里nslookup redis会失败。6.2 检查 DNS 服务器地址dockerexecflask-appcat/etc/resolv.conf如果nameserver不是127.0.0.11说明这个容器可能连接的是默认 bridge 网络默认 bridge 没有内嵌 DNS或者/etc/resolv.conf被错误修改。6.3 使用 ping 和 nslookup 定位问题ping IP 通但 ping 容器名不通DNS 解析问题。检查网络和 DNS 配置。ping IP 不通网络层问题。检查 iptables 规则、网桥状态、宿主机防火墙。# 先查 Redis 的 IPdockerinspect redis--format{{.NetworkSettings.Networks.counter-net.IPAddress}}# 假设输出 172.18.0.2# 从 Flask 容器 ping IPdockerexecflask-appping-c2172.18.0.2# 如果通说明网络层正常问题在 DNS# 如果不通检查网络安全组、iptables 规则或宿主机路由6.4 清空 DNS 缓存必要时Docker 内嵌 DNS 有短缓存但很少需要手动干预。如果是 Docker Compose 环境重启服务docker compose restart会让容器重新获得 DNS 记录。七、本篇总结这一篇我们深入到了 Docker 网络的神经中枢——DNS 服务发现。DNS 的核心作用将容器名、网络别名、服务名翻译为动态的 IP 地址实现服务发现。Docker 内嵌 DNS运行在127.0.0.11由 Docker 守护进程维护自动同步网络中容器的增删。网络别名--network-alias允许多个容器共享一个逻辑名称实现简单的 DNS 轮询负载均衡。多网络连接docker network connect让容器充当网络间的桥梁实现分层架构。全链路通信我们追踪了容器到容器、容器到宿主机、外部到容器的完整数据包路径。故障排查三板斧查网络归属 → 查 DNS 配置 → 查 IP 连通性。从下一篇文章开始我们将进入本系列第一个重要的里程碑——第 10 篇实战将一个多组件应用完整容器化。我们将不再零散地操作单个容器而是以项目化的方式把 Flask Redis 计数器应用连同它的依赖、配置、网络、数据卷一次性打包写成正式的容器化交付物并最终推送到 Docker Hub。想了解更多还可以去各个平台搜索「IT策士」一起升级 IT 思维