ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Docker容器名与服务名解析:内建DNS底层逻辑与排障实践

Docker容器名与服务名解析:内建DNS底层逻辑与排障实践 先说一个多数人踩过的坑把容器跑起来之后在另一个容器里直接用容器名去ping结果发现经常不通但用 IP 地址就能通。一旦换成docker compose编排服务名却又能通了。很多人第一反应是“网络模式不同”这没错但真正决定两种名字能否被解析的是 Docker 内建 DNS 这套机制。搞懂“服务名”和“容器名”在底层是如何进入 DNS 解析链路的网络排障就算入门了。这篇文章我把 Docker 网络通信中两种名字的底层逻辑拆开聊一遍Docker 内建 DNS 是怎么工作的、自定义网络和默认 bridge 有什么区别、Compose 里的服务名最终映射到什么、以及排查时最容易忽略的几个细节。1. 名字解析的本质DNS 解析链路是怎么搭起来的在容器的世界里所谓“用名字通信”本质上就是一次 DNS 解析。Docker 不会像传统交换机那样维护一张 MAC 地址表它维护的是一个 DNS 记录表然后把这个 DNS 服务告诉每个容器。Docker 创建容器时会往容器里写入一份/etc/resolv.conf里面的 nameserver 指向 127.0.0.11。这个地址就是 Docker 内建 DNS 的虚拟 IP监听在容器的 loopback 上。无论容器跑在哪种网络模式下只要不是 host 网络这个 127.0.0.11 就会存在。Docker 之所以选 127.0.0.11 而不是类似 172.17.0.1 这种网关地址是因为 DNS 查询必须走 loopback 才不会被本机的路由规则干扰。它通过 iptables 规则把发往 127.0.0.11:53 的 UDP/TCP 流量重定向到 Docker daemon 进程内部的 DNS 服务这个服务再去查它自己维护的记录表。这套机制有几个明显特点DNS 解析发生在容器内部不经过物理网络所以延迟极低Docker daemon 对容器的名字记录有全局视图不会出现 DNS 缓存失效问题每个容器拿到的 DNS 记录只包含它所在网络内的容器天然做了网络隔离。理解这一点之后再去看“容器名 ping 不通 / 服务名通”这类问题其实就是在问目标名字有没有被写进当前容器的 DNS 记录表里。1.1 docker0 默认网络与用户自定义网络的本质差异默认创建的容器如果没指定--network会挂在名为bridge的默认网络上对应宿主机上的docker0网桥。这个网络上容器可以用 IP 互访但用容器名互访默认是不通的。原因不在网桥本身——默认 bridge 上的容器也接入了内建 DNS名字解析本身是可以工作的。真正的问题在于 Docker 设计上的保守策略为了让默认 bridge 的行为足够简单避免新手把容器名当全局域名用Docker 默认关闭了默认网络上的名字解析功能。自定义网络就不是这个策略。只要你通过docker network create创建了一个网络然后把容器都挂上去内建 DNS 会自动开启容器名之间立刻可以互相解析。这里有一个容易被忽略的细节--link参数在默认 bridge 上也能实现名字解析但它不是通过 DNS而是往目标容器的/etc/hosts里写条目并且只对声明了 link 的容器生效。它跟自定义网络的 DNS 解析有本质区别——hosts 文件是静态的容器 IP 变了就得重建而 DNS 记录是动态跟随容器状态的。1.2 为什么 Docker 推荐用户自定义网络而不是默认 bridge从我实际使用的体验看Docker 官方文档里反复强调“使用自定义网络”不是为了让你多敲一条命令而是因为默认 bridge 有几个明显不适合生产的地方缺少内建 DNS 解析容器多了之后依赖 IP 互访基本不可维护没有自动隔离所有容器都在一个大的二层网段里没法做细粒度网络策略连通的容器无法动态解除默认网络上的容器只要在一个网段就能互访想临时断开某个容器只能靠防火墙或者停容器--link机制是单向且静态的A link 了 BA 能通过名字访问 BB 却不能通过名字访问 A这种不对称在实际维护时特别容易让人困惑。自定义网络则解决了这些问题名字自动解析、网络间隔离、容器可以在网络间自由加入或退出而不用重建。2. 一次“服务名”解析请求的完整链路拿一个典型的 Compose 场景拆解假设有两个服务web和db在同一个 Compose 项目里。现在web容器里执行ping db你看不到的是这样一条链路web容器发起getaddrinfo(db)调用glibc 读取/etc/resolv.conf发现 nameserver 是 127.0.0.11把 DNS 查询包发往 loopback 的 53 端口iptables 规则截获这个包转发给 Docker daemon 内嵌的 DNS 服务Docker DNS 在自己维护的记录表中查找db这个名字找到对应的容器 IP 后返回 A 记录web拿到 IP发起 ICMP 或 TCP 连接。这中间有一个很关键的环节/etc/resolv.conf除了 nameserver还包含 search 域。Docker 在写入这个文件时会根据容器所在的网络自动追加搜索域格式通常是项目名_网络名。2.1 DNS 搜索域与 ndots 的隐藏影响搜过 Docker 网络问题的朋友可能见过这样的输出search default.svc.cluster.local svc.cluster.local cluster.local这个一般是 Kubernetes 环境。原生 Docker 里搜索域长这样search 项目名_网络名比如 Compose 项目叫myapp网络叫myapp_default那 search 域就是myapp_default。这个搜索域不是摆设当你在容器里访问db时glibc 会按照ndots的规则决定先查db还是先查db.myapp_default。默认情况下ndots的值是 1也就是说域名里只要带一个点就先把整个域名当作绝对域名去查查不到再追加 search 域。db这个名字本身没有点所以 glibc 会先在/etc/hosts里找找不到就用原名字db查一次失败后再尝试db.myapp_default。这里容易混淆的点是名字里带点不代表“一定是个域名”。比如 Compose 服务名如果带了项目前缀像myapp_db_1它虽然不含点但它是 Docker 生成的容器名而不是服务名。服务名永远是不带下标的短名字。2.2 实测tcpdump 抓 DNS 查询验证搜索过程为了验证上面这条链路我在一个 Compose 环境里做过一次抓包实验。在web容器里执行tcpdump -i eth0 port 53 -n然后另开终端执行ping db。抓到的流量非常清晰先是db的 A 记录查询紧接着是db.myapp_default的查询。Docker 内建 DNS 对前一个查询返回了记录所以第二个查询其实多余但 glibc 并不知情它只会按顺序尝试。这个现象引出一个实用结论服务名解析慢的问题绝大多数不是 Docker DNS 慢而是 glibc 的搜索域探测机制多跑了几次无效查询。如果容器里装了nss-myhostname或者修改了ndots配置行为又会不一样。2.3 Docker DNS 的响应顺序服务名优先于容器名Docker 内建 DNS 维护的记录表里同一时刻可能有两个名字指向同一个容器服务名和容器名。比如 Compose 里服务叫db生成的容器名叫myapp-db-1这两个名字都能解析到这个容器。一旦发生名字冲突——比如某个容器名恰好和另一个服务名相同——Docker DNS 的响应顺序是服务名优先于容器名。这是我在一个实际事故里确认过的当时有个 Compose 项目服务名叫redis另外手动跑了一个容器名也是redis的容器。所有依赖redis这个名字的解析全部指向了 Compose 服务而手动起的那个只能靠容器全名访问。这种优先级设计是有意为之服务名在 Compose 项目内部是稳定的拓扑标识而容器名只是运行实例的标识。业务代码应该依赖服务名而不是容器名。3. Compose 里的服务名是如何映射到容器的Compose 是 Docker 网络通信里最常用到的编排工具绕不开。很多人在 Compose 里配了links或者depends_on但没搞懂这些配置到底影响了什么。逐个拆开。3.1 服务名、容器名和网络别名的三角关系Compose 创建服务时默认生成的容器名格式是项目名-服务名-序号比如myapp-web-1。这个容器名可以被 Docker DNS 解析。但 Compose 同时会为该容器创建一个网络别名network alias别名就是服务名本身。也就是说在 Compose 自动创建的网络里web这个名字并不是容器名而是别名。网络别名这个概念很关键。你可以手动给容器加别名docker network connect mynet mycontainer --alias my-alias加了之后同一个网络里的其他容器就可以用my-alias来访问mycontainer了。Compose 做的就是把服务名注册为容器的别名然后把容器加入到项目网络里。所以 Compose 环境下服务名解析路径是服务名 - 网络别名 - 容器 IP而手动docker run环境下的路径是容器名 - Docker DNS 记录 - 容器 IP两条路径最终都落到同一种解析机制上这也解释了为什么 Compose 服务名和容器名都能被解析但来源不同。3.2depends_on和links在网络解析里扮演什么角色depends_on的作用经常被误解。不少初学者以为它决定了网络连通性实际它只控制服务启动顺序不影响名字解析。即使不写depends_on只要两个服务在同一个网络里名字解析就能正常工作。links则有点历史包袱的味道。在 Compose 文件里写services: web: links: - db它的效果等效于在启动web容器时加了--link db本质是把db的容器名、服务名、IP 追加到web容器的/etc/hosts里。但现在的 Compose 环境里只要你让服务加入同一个自定义网络links完全没必要写。它还引入了一个副作用links会创建一种隐式的依赖关系导致 Compose 启动时额外等待目标容器完成启动但这个等待不检查目标服务的可用性所以根本解决不了“web 起来了但 db 还没就绪”这类问题。正确的做法是用depends_on加condition: service_healthy配合健康检查而不是靠links做网络接入。3.3 服务名解析与健康检查状态的关系这里有个值得注意的细节Docker DNS 不会因为容器不健康就把它的记录从表里摘掉。只要容器处于运行状态它的名字就能被解析。我遇到过的场景是Compose 里web依赖dbdb的健康检查还没通过但web已经开始尝试连接db。由于 DNS 能解析出db的 IP连接请求已经发出但db端口可能还没监听于是web疯狂报错。这不是 DNS 的问题但排障时容易误判成一连串解析失败。解决方式是在 Compose 里配置services: web: depends_on: db: condition: service_healthy db: healthcheck: test: [CMD, pg_isready, -U, postgres] interval: 5s timeout: 3s retries: 10让依赖关系真正做到“等待就绪”而不是只等进程启起来。4. 核心机制细节嵌入式 DNS 的解析行为与排障方法服务名和容器名的底层逻辑讲完之后来几个实际排障中非常有用但容易被忽略的机制细节。4.1 为什么 ping 不通但端口能通和名字解析无关但经常混在一起的现象容器 A 能解析出容器 B 的 IP但ping B不通而用nc探测 B 的端口却通了。原因通常不是 DNS而是容器 B 的镜像里没有装ping依赖的 ICMP 工具或者容器 B 的防火墙规则丢弃了 ICMP。DNS 解析只是把名字转成 IP能不能连通取决于目标容器是否响应这个协议。排障时要记住先区分“解析不到”和“到了但没响应”。前者看 DNS 返回后者看具体协议和端口这两件事的排查思路完全不一样。我用一句话总结服务名和容器名的通信链路里DNS 只是导流流量能不能到达应用层是网络策略和进程监听的问题。4.2 容器名到 IP 的映射在跨网络时为什么会失效每个自定义网络是一个独立的 DNS 视图。同一个容器如果同时连接了网络 A 和网络 B它在网络 A 里的名字解析记录和在网络 B 里的记录是互相独立的。如果容器 X 只连接了网络 A它去解析网络 B 里的容器名得到的不是“解析失败”而是NXDOMAIN即无法找到对应记录。这个“解析失败”本身是符合预期的因为 Docker 不会跨网络维护名字视图。遇到这种跨网络通信需求常规方案是让目标容器也接入当前网络docker network connect net-b container-b或者直接在 Compose 里声明两个网络services: app: networks: - net-a - net-b但要注意跨网络连接后容器会同时拿到多个网络的 IP这时目标容器需要监听在所有接口上比如0.0.0.0否则来自另一个网络的流量虽然能到达但应用只监听在特定 IP 上依然连不上。4.3 hosts 文件与嵌入式 DNS 的优先级容器里的/etc/hosts不是摆设。glibc 的解析顺序是先查/etc/hosts再查 DNS。Docker 创建容器时会自动往 hosts 文件里写入容器的自身记录和--link指定的条目。如果你手动用docker exec改了容器的/etc/hosts这个改动会在容器重启后丢失因为 Docker 每次启动容器会根据当前网络配置重新生成这个文件。正确做法是用extra_hosts配置docker run --add-host mydb:192.168.1.10 ...Compose 里对应extra_hosts: - mydb:192.168.1.10这种方式的优先级高于 DNS 记录适合临时指定外部地址或多个环境切换的场景。4.4 高频排查场景一条命令定位容器名解析问题排查名字解析问题时我的固定套路是四步第一步确认容器所在网络docker inspect -f {{range $k,$v : .NetworkSettings.Networks}}{{$k}} {{end}} container第二步确认目标容器在该网络的 IPdocker inspect -f {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}} target第三步进入容器测试 DNS 解析docker exec -it source getent hosts target-name如果返回了 IP说明 DNS 没问题如果没返回说明目标名字不在当前网络的解析视图里。第四步查看容器内的/etc/resolv.confdocker exec -it source cat /etc/resolv.conf确认 nameserver 是否为 127.0.0.11。如果容器是--network host模式这个文件会是宿主机的配置名字解析自然不生效。这套流程基本能覆盖 90% 的容器名通信问题不用装额外工具纯靠 Docker 原生命令就能完成。5. 实际问题排查与避坑经验整理几个我在实际项目里遇到过、并且真实浪费过时间的问题。5.1 服务名带了项目前缀导致解析到错误的容器Compose 项目里默认服务名解析没问题但如果你用container_name显式指定了容器名并且这个名字和其他服务重名就会出现解析歧义。看一个典型配置services: db: container_name: mydb此时网络别名仍然是db但额外多了一个容器名mydb。如果另一个服务里写的是连接mydb也能通因为容器名同样会被注册到 DNS 里。但这种写法的问题在于一旦你横向扩缩容比如docker compose up --scale db3container_name就不能用了因为名字冲突。更推荐的做法是不要设置container_name让 Compose 自动生成带序号的容器名业务代码里固定用服务名。5.2 容器重启后 IP 变化但名字还能解析这是自定义网络和内建 DNS 的典型优势容器重启后 IP 会重新分配但其他容器通过服务名访问不会受任何影响因为 DNS 记录是实时跟随容器状态的。但这个机制有一个前提使用自定义网络。如果在默认 bridge 上用了--link容器重启后 hosts 文件里的旧 IP 不会自动更新这就是为什么我一直强调生产环境一定要自定义网络。5.3 daemon 重启后跨主机网络的解析异常单机 Docker 环境里daemon 重启后 Docker 会重新创建网桥和网络容器的 IP 会变化但服务名解析还是能恢复。如果用的是 Swarm 或者跨主机 overlay 网络情况就不一样了。Overlay 网络里服务名解析依赖 Docker 内建 DNS 在集群节点间的同步。某个节点上的 daemon 重启后DNS 记录可能需要几秒钟才能在全集群重新同步这个窗口期内容器解析服务名有可能短暂失败。处理方式是在应用层做重试或者把依赖方的启动延时调高一点。5.4 网络策略和 DNS 解析的边界最后提醒一个容易让人误判的边界Docker 的嵌入式 DNS 只解决名字到 IP 的映射不负责网络连通性。即使名字解析成功容器 A 到容器 B 的流量还可能被以下因素拦截宿主机 iptables 规则Docker 网络间的隔离策略比如两个容器分别在不同自定义网络容器内应用监听的 IP 和端口。遇到名字能解析但连接失败时不要一味在 DNS 上下工夫先确认流量真正到了哪里。用tcpdump在目标容器上抓包是最直接的验证方式我在排障时几乎每次都靠这一招快速定位问题到底在网络层还是应用层。6. 经验复盘与实用建议关于服务名和容器名的底层逻辑我再强调几个实操中最容易踩坑的点这些是文档里不会细写的内容。6.1 可视化管理网络拓扑减少“想当然”的排障容器一多网络的拓扑关系就不靠脑子记了。我一般用docker network inspect配合jq输出每个网络下挂的容器和 IPdocker network inspect mynet | jq .[0].Containers | to_entries[] | {name: .value.Name, ipv4: .value.IPv4Address}这样能一眼看出来当前网络的完整成员列表排查“为什么解析不到”时就清楚了你要访问的目标容器到底在不在同一个网络里。6.2 服务名解析不是“全局可用”的不少人会习惯性地以为 Docker 和 Kubernetes 一样服务名是全集群可解析的。这个理解是错误的。Docker 的服务名解析是“网络内可见”的不在同一个自定义网络里的容器即使部署在同一台宿主机上也解析不到对方。这也是容器集群迁移到 Kubernetes 之后最容易踩的坑K8s 里 Service 是集群级别的 DNS 名称天然全集群可解析。如果带着 Docker 的经验去配置 K8s很容易忽略网络策略层面的隔离导致服务被不该访问的客户端扫到。6.3 给容器和服务起名时的规范性建议名字不是随便起的在实际维护中名字的规范性直接决定了排障效率。我的建议是容器名不要包含下划线DNS 和某些工具对下划线的支持不统一服务名使用短横线连接的小写单词比如user-service不要用驼峰服务名和容器名尽量不手动设置别名避免重复映射造成困惑如果业务代码需要访问数据库统一用服务名配置连接串不要写容器名或 IP。这套规范在 Docker 和 Compose 之间切换时基本可以无缝衔接也方便以后迁移到编排平台。回到最开始的问题容器名和服务名在 Docker 网络通信中为什么都能用答案其实很简单——它们都被注册进了 Docker 的内建 DNS只是来源不同。容器名是 Docker daemon 自动生成的记录服务名是 Compose 通过网络别名注册的记录。理解了这层映射关系再遇到名字解析失败你就能快速判断问题出在哪个环节而不是靠重启容器碰运气。
返回列表