资讯详情

资讯详情

Docker容器“失联”?下划线服务名的DNS解析坑

“谁能想到一个下划线 _ 竟让我的 Docker 容器‘失联’了”这句话是我上周排查一个故障之后发自内心的感叹。当时我用 Docker Compose 把一个后端服务和 MySQL 拆成两个容器负责管理的服务叫user_api数据库叫user_db——名字里都带下划线。结果 API 服务一路启动一路在日志里刷连接数据库失败提示lookup user_db: no such host两个容器明明都是 Up 状态却像是互相“失联”了一样。如果你也遇到过 Docker 容器之间互相访问时不时超时、换 IP 直连就好、用服务名就不行这类怪事这篇排查过程很值得看一遍。我会把定位问题的每一个步骤、背后的命名的规范、以及最终的解决方案都写清楚适合所有用 Docker Compose 组织多容器应用的朋友尤其是刚把服务拆开部署、开始踩网络坑的新手。1. 事故现场两个容器都活着却互相“失联”先描述一下当时的部署情况。项目结构非常简单docker-compose.yml里定义了两个服务services: user_api: image: my-backend:latest environment: DB_HOST: user_db DB_PORT: 3306 ports: - 8080:8080 networks: - default user_db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 networks: - default networks: default: driver: bridge两个服务都在同一个自定义网络myapp_default里跑着。docker ps 一看状态如下容器镜像状态容器 IP映射端口myapp_user_api_1my-backend:latestUp172.18.0.20.0.0.0:8080-8080/tcpmyapp_user_db_1mysql:8.0Up172.18.0.30.0.0.0:3306-3306/tcp从宿主机访问这两个端口都正常MySQL 也能通过mysql -h 127.0.0.1 -P 3306连上。但user_api容器里的进程就是连不上user_db日志永远停在这样一行2025/03/02 10:12:44 dial tcp: lookup user_db on 127.0.0.11:53: no such host这句日志非常关键。它不是connection refused不是timeout而是lookup user_db ... no such host。到了这一步基本可以断定问题出在名字解析而不是端口、防火墙、账号权限。可问题也来了两个容器明明确认在同一网络里MySQL 的 IP 也查得到为什么名字解析会失败我后来复盘最大的干扰项就是“下划线”。因为 Docker 允许容器名、服务名里带_Compose 自动生成的一长串容器名本身就带着下划线比如myapp_user_api_1。平时你docker exec -it myapp_user_api_1 sh进容器、用docker logs myapp_user_api_1看日志全都正常工作所以你根本不会把“下划线”和“网络失联”联系在一起。它就像一个潜伏的刺客平时不声不响一到关键访问就给你一刀。2. 排查链路从 ping 不通一路查到下划线这类故障最忌讳上来就猜最好按层一层层剥。我当时的排查顺序是网络拓扑 → IP 直连 → DNS 配置 → 名字解析对比 → 最小化复现。每一步都有明确结论最后才锁定下划线。2.1 先确认网络拓扑排除“不在同一网络”的低级错误多容器互访失败最常见的原因其实是它们压根不在同一个自定义网络里。比如一个容器挂在bridge默认网络另一个挂在myapp_default双方各走各的虚拟网桥自然互相不可见。这个问题使用习惯越好越不容易踩但排查时仍然要第一个排除。我执行了docker network inspect myapp_default输出里能看到子网是172.18.0.0/16下面挂着两个容器的条目而且两个容器的 IPv4Address 一个是172.18.0.2/16一个是172.18.0.3/16。确认它们就在同一个 L2 网络里。这一步排除掉“网段隔离”和“错挂网络”后问题范围缩到了 DNS/主机名解析。这里我多说一句很多人习惯用docker inspect 容器名看 IP但更推荐直接看整个网络因为能把所有同网段容器放在一张视图里谁连上了、谁没连上、IP 有没有重复一眼就能扫出来。2.2 用 IP 直连确认容器本身和端口都没问题既然网络拓扑没问题我再验证应用级连通性。进入myapp_user_api_1尝试用 IP 直连数据库端口docker exec -it myapp_user_api_1 sh # 容器里没有 nc 时用 wget 或者 bash 的 /dev/tcp 也可以 # 我用的是连接测试工具 / # wget -qO- http://172.18.0.3:3306MySQL 的 3306 端口即使不认 HTTP 协议只要监听正常连接动作本身会成功wget 会报 HTTP 相关错误但不会报连接失败。这一步的结论是TCP 层面完全能接通MySQL 进程在监听容器之间的三层路由和二层转发都没有问题。这就把范围进一步缩到了“为什么名字 user_db 不被解析”。因为如果 IP 能通但名字不通问题只可能是 DNS 解析链路上的某个环节。2.3 查看容器 DNS 配置确认走的是 Docker 内置 DNS再看容器里的 DNS 配置和 hosts 文件docker exec -it myapp_user_api_1 cat /etc/resolv.conf输出nameserver 127.0.0.11 options ndots:0这个127.0.0.11就是 Docker 内置 DNS 服务。在自定义网络里每个容器的/etc/resolv.conf都会被指向它容器名、网络别名、Compose 服务名的解析都靠它完成。解析不了的域名它会转发给宿主机配置的上游 DNS。同时我也看了/etc/hostsdocker exec -it myapp_user_api_1 cat /etc/hosts里面只写了容器自身的主机名和两个 IP 相关条目并没有user_db。这是一个非常常见的认知误区很多人以为 Docker 会把同一网络里所有容器名都写进/etc/hosts实际上默认并不会。容器之间互相访问依赖的是内置 DNS不是静态 hosts 文件。这个机制差异对后面理解故障至关重要。2.4 nslookup 对比测试发现“工具说能解析应用却不行”因为瘦身镜像里通常没有 nslookup我直接起了一个临时网络排查容器nicolaka/netshoot加入同一个网络来查询docker run --rm --network myapp_default nicolaka/netshoot nslookup user_db结果很微妙在 netshoot 容器里user_db能够被解析成172.18.0.3。我当时愣了一下因为 API 服务的 Go 进程明明报no such host。同一个名字、同一个网络、同一个 DNS 服务器一个能解析一个不行。再试user-db把下划线换成连字符也能解析到同一个 IP。这已经强烈暗示问题不在 DNS 服务端而在“客户端对主机名字符的接受程度”。有些工具会比较宽容有些工具会严格执行主机名规范下划线就是那个被严格拒绝的字符。真正排查到这里时我已经有八分把握是下划线惹的祸但还差最后一个实锤。2.5 最小化复现只把下划线改成连字符故障消失我回到docker-compose.yml把服务名user_db全部改成user-db包括环境变量里的DB_HOSTuser-db然后执行docker compose down docker compose up -d两个容器重建后API 服务日志立刻恢复正常数据读写得非常顺畅。整个故障前后只改了一个字符现象就完全消失。到了这里不需要再看别的结论就是容器间服务名/主机名里的下划线直接破坏了部分客户端库对主机名的合法性校验导致“失联”。3. 根因拆解下划线是如何“合法地”变成定时炸弹的找到凶手只是第一步把它背后的机制讲清楚才能确保以后不再犯。3.1 Docker 内置 DNS 的工作原理没你想的那么“严格”在用户自定义网络模式下Docker 会给每个容器注入一个监听在127.0.0.11:53的嵌入式 DNS 代理。这个代理负责三件事解析同一个网络里的容器名比如myapp_user_db_1解析 Compose 服务名和网络别名比如user_db、user-db解析外部域名转发到宿主机配置的上游 DNS它对“名字长什么样”的容忍度其实相当宽松。Docker 在创建容器、创建网络别名时只做了很基础的字符检查_是允许出现在容器名里的否则 Compose 自动生成的myapp_user_api_1这种名字本身就活不下去。这带来了一个盲区凡是 Docker 管理层面允许的名字用户就会默认它是安全的、可解析的但应用层未必这么想。所以你会看到一种分裂现象docker exec能进、docker ps正常、nslookup 也能出结果偏偏业务进程连不上。问题不在 Docker 本身而在业务进程使用的语言运行时或网络库它们对主机名字符的校验比 Docker 严得多。3.2 主机名规范 RFC 952 / RFC 1123下划线从来不是合法主机名这里涉及一个几十年前就定下的规则。传统 DNS 主机名规范由 RFC 952 定义主机名只能包含字母、数字、连字符并且应当以字母或数字开头。后来的 RFC 1123 放宽了数字开头的限制但始终没有把下划线列为合法字符。下划线并不是完全不可以出现在 DNS 世界里它被允许用于 SRV 记录和 TXT 记录这类“服务记录”例如_http._tcp.example.com。但那是用于表示服务类型和协议不是用来给某台主机当名字的。一台真正的主机它的 A/AAAA 记录对应的主机名里出现下划线在严格校验的客户端里就是不合法。Go 语言的标准库net包就是一个典型。它内部对主机名有isDomainName这类合法性检查遇到带_的名字会直接判定为非法域名拒绝发起后续解析流程。所以 Go 进程访问user_db时即使 Docker 内置 DNS 能查到记录客户端那层已经先把名字“毙”了。表现到日志上有的版本是no such host有的版本是invalid domain name本质都是同一个原因。3.3 从“能启动、能 ping IP”到“服务连不上”缺的是哪一环很多人会被“容器能启动、IP 直连也通”迷惑觉得网络一定没问题。这里我重新梳理一下链路你会发现失败点非常靠前容器创建、路由转发、TCP/IP 连接完全不关心名字里有没有下划线所以docker ps是绿的端口映射是通的IP 直连也是通的。业务进程发起连接时先对目标主机名做字符串校验这里下划线就可能直接出局。DNS 解析即使查询发出去了某些客户端也会因为校验失败而放弃。TCP 连接建立只要走到这里基本就成功了但前两步已经拦住。所以“容器失联”这个说法要打引号网络其实没有断断的是“名字信任链”。容器管理层面很宽容应用层很严格下划线正好卡在两者之间。这类问题最坑的地方在于它不是每次都失败工具、语言、版本一变表现就不一样很容易让人觉得是玄学。4. 解决方案改名、别名还是 extra_hosts按场景取舍定位到问题之后方案其实很清晰让服务间的访问名称符合主机名规范。但具体怎么改要看你的存量情况。4.1 最推荐把服务名、容器名统一改成小写字母加连字符干净、彻底、一劳永逸。修改后的 Compose 文件大致如下services: user-api: image: my-backend:latest environment: DB_HOST: user-db DB_PORT: 3306 ports: - 8080:8080 networks: - default user-db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 networks: - default networks: default: driver: bridge改完后一定要docker compose down再docker compose up -d。注意如果只执行docker compose up -dCompose 可能因为检测不到服务定义变化而不重建配置或者新老容器并存导致你以为改坏了。最稳妥的办法是 down 干净再把旧容器清掉docker compose down -v这里-v会删除容器关联的卷如果数据库有初始化数据执行前务必确认数据已经备份或者可以接受重建。不放心的话可以只docker compose down不加-v。为什么连字符是最优解因为 RFC 952/1123 允许、Docker 允许、主流语言校验全部通过而且你在 Compose 文件里写DB_HOST: user-db也好、写成环境变量注入也好都不会再撞上字符校验问题。连字符在 Docker 自动生成环境变量时会有一次“翻译”连字符变成下划线、字母变大写但只要你显式传入环境变量就不用依赖那些自动生成的变量名不会踩坑。4.2 不想改容器名用网络别名给服务一个“干净身份”如果某些遗留系统已经把user_db这个名字写死在配置文件里批量改名成本高你可以不改容器名而是给容器增加一个合规的网络别名比如user-db。Docker 内置 DNS 会把别名解析到同一个容器 IP其他容器用user-db访问即可services: user_db: image: mysql:8.0 container_name: user_db networks: default: aliases: - user-db关键是 YAML 的缩进networks后面跟着的是你创建的网络名默认就是defaultaliases是这个网络下该容器的别名列表。配好之后在user_api里连user-db:3306就和连user_db:3306效果一样。这个方案我实际用过多次最适合存量项目微调。它只改 Compose 文件的一小段业务代码里把连接地址换一下就行不用动容器名、不用改镜像、不用重建目录结构。需要注意一点别名只在同一个网络内有效多个容器如果都声明了同一个 alias解析结果会有随机性千万别重复。4.3 兜底方案extra_hosts 和应用层环境变量第三个思路是绕过 DNS直接往容器的/etc/hosts里写映射services: user-api: extra_hosts: - user-db:172.18.0.3这个方案适合临时排查不适合长期使用。因为容器 IP 会随重建变化写死 IP 等于把“易变信息”变成了“静态配置”容器一旦重建就失效。真要长期用的话还得靠脚本动态更新 IP反而更麻烦。更实用的兜底是把业务代码里的数据库地址完全改成环境变量驱动environment: DB_HOST: user-db这样即便哪天主机名再出新问题比如证书匹配不上、需要切换域名你只需要改 Compose 里的一行配置不用重新构建镜像。很多框架默认就从环境变量读取配置比如 Spring Boot 的SPRING_DATASOURCE_URL、Go 项目里常见的DB_HOST使用起来非常顺手。5. 同类隐形命名坑这些字符也不让人省心下划线既然能坑人其他“看起来合法、用起来暗雷”的命名字符同样值得列一列。我整理了一张表都是我实际见过或者亲手踩过的字符/命名风格Docker 容器名是否允许典型故障现象建议下划线_允许部分客户端主机名校验失败服务间解析失败用连字符替代大写字母允许主机名大小写错乱DNS/证书匹配不一致全部小写点号.允许DNS 把名字当成 FQDN 的一部分搜索域拼接后解析到错误节点除非模拟真实域名否则避免驼峰命名允许自动生成环境变量时被改写配置对不上全小写加连字符空格/中文/emoji大多数场景不允许创建失败或解析乱码直接不用连字符-允许且最安全几乎没有默认选项5.1 点号的坑你以为在访问服务名实际可能被当成了完整域名点号在 DNS 语义里是层级分隔符。如果你给 Compose 服务起了user.db这种名字某些解析工具会把它当作一个完整的 FQDN 处理而不是当前网络里的服务别名。它会在搜索域里拼来拼去最终很可能解析到宿主机 DNS 返回的外部记录或者干脆解析失败。而且名字带点号后和 SSL 证书、TLS SNI 的域名匹配逻辑也容易纠缠在一起平白给排查增加难度。5.2 --link 时代的自动环境变量连字符也会被“翻译”在比较老的 Docker 使用方式里--link会给容器注入很多自动生成的环境变量例如MYSQL_PORT_3306_TCP_ADDR172.18.0.3 MYSQL_PORT_3306_TCP_PORT3306这个命名规则会把-转换成_字母全部大写。如果你在 Compose 里给服务命名用了连字符然后想当然地在代码里引用“和配置一模一样的自动环境变量名”很容易对不上。现代 Compose 部署我建议显式通过environment传入自定义变量不要依赖--link遗留的自动环境变量体系既清晰又可控。5.3 Compose 项目名和目录名下划线它不是第一个雷除了服务名Compose 的项目名默认取自目录名。如果你的项目目录叫my_shop容器名会带上这个前缀比如my_shop_user_api_1。虽然不影响容器间访问的解析逻辑但会让docker ps的输出乱七八糟脚本批量操作时容易眼花。解决方式很简单在.env文件里加一行COMPOSE_PROJECT_NAMEmyshop或者启动时显式指定docker compose -p myshop up -d让项目名干干净净不光看着舒服排查问题时也更省力。6. 排查类似“失联”问题的操作清单最后把我这次的经验浓缩成一张排查清单下次你再遇到容器间互访异常按这个顺序走比逐个工具乱试高效得多。看业务日志区分是解析失败no such host、invalid domain name、连接拒绝connection refused还是超时timeout。解析失败直接跳去检查名字和 DNS别先动防火墙。docker network inspect确认两个容器在同一个自定义网络。用 IP 直连目标端口确认网络层和应用层正常。进入容器看/etc/resolv.conf确认 nameserver 是127.0.0.11明确走的是 Docker 内置 DNS。用带网络工具镜像的临时容器跑nslookup多次对比带下划线和连字符的解析结果。把可疑服务名临时改成规范名称重启容器验证。这一步是终极实锤。我个人的习惯是服务名、容器名、网络别名全部使用[a-z0-9-]这个字符集也就是小写字母、数字、连字符坚决不用下划线。这套规则我已经用了很久容器间访问再没有出现过因为名字字符导致的“失联”。还记得那次故障解决后我笑着跟同事说如果有人再跟我说“下划线也能出现在主机名里”我会让他先跑一次 Go 项目连接user_db试试。名字这东西看起来是个小事但越是小地方越容易在关键时刻给人上一课。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →