☰
Docker容器“失联”?下划线服务名的DNS解析坑
2026/9/30 17:38:48 网站建设 项目流程

“谁能想到,一个下划线 _ 竟让我的 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/tcp
myapp_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:3306

MySQL 的 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/hosts:

docker 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_HOST=user-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 直连也通”迷惑,觉得网络一定没问题。这里我重新梳理一下链路,你会发现失败点非常靠前:

  1. 容器创建、路由转发、TCP/IP 连接:完全不关心名字里有没有下划线,所以docker ps是绿的,端口映射是通的,IP 直连也是通的。
  2. 业务进程发起连接时:先对目标主机名做字符串校验,这里下划线就可能直接出局。
  3. DNS 解析:即使查询发出去了,某些客户端也会因为校验失败而放弃。
  4. 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 -d,Compose 可能因为检测不到服务定义变化而不重建配置,或者新老容器并存,导致你以为改坏了。最稳妥的办法是 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后面跟着的是你创建的网络名(默认就是default),aliases是这个网络下该容器的别名列表。配好之后,在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_ADDR=172.18.0.3 MYSQL_PORT_3306_TCP_PORT=3306

这个命名规则会把-转换成_,字母全部大写。如果你在 Compose 里给服务命名用了连字符,然后想当然地在代码里引用“和配置一模一样的自动环境变量名”,很容易对不上。现代 Compose 部署我建议显式通过environment传入自定义变量,不要依赖--link遗留的自动环境变量体系,既清晰又可控。

5.3 Compose 项目名和目录名下划线:它不是第一个雷

除了服务名,Compose 的项目名默认取自目录名。如果你的项目目录叫my_shop,容器名会带上这个前缀,比如my_shop_user_api_1。虽然不影响容器间访问的解析逻辑,但会让docker ps的输出乱七八糟,脚本批量操作时容易眼花。解决方式很简单,在.env文件里加一行:

COMPOSE_PROJECT_NAME=myshop

或者启动时显式指定:

docker compose -p myshop up -d

让项目名干干净净,不光看着舒服,排查问题时也更省力。

6. 排查类似“失联”问题的操作清单

最后把我这次的经验浓缩成一张排查清单,下次你再遇到容器间互访异常,按这个顺序走,比逐个工具乱试高效得多。

  1. 看业务日志,区分是解析失败(no such host、invalid domain name)、连接拒绝(connection refused)还是超时(timeout)。解析失败直接跳去检查名字和 DNS,别先动防火墙。
  2. docker network inspect确认两个容器在同一个自定义网络。
  3. 用 IP 直连目标端口,确认网络层和应用层正常。
  4. 进入容器看/etc/resolv.conf,确认 nameserver 是127.0.0.11,明确走的是 Docker 内置 DNS。
  5. 用带网络工具镜像的临时容器跑nslookup,多次对比带下划线和连字符的解析结果。
  6. 把可疑服务名临时改成规范名称,重启容器验证。这一步是终极实锤。

我个人的习惯是,服务名、容器名、网络别名全部使用[a-z0-9-]这个字符集,也就是小写字母、数字、连字符,坚决不用下划线。这套规则我已经用了很久,容器间访问再没有出现过因为名字字符导致的“失联”。

还记得那次故障解决后,我笑着跟同事说,如果有人再跟我说“下划线也能出现在主机名里”,我会让他先跑一次 Go 项目连接user_db试试。名字这东西,看起来是个小事,但越是小地方,越容易在关键时刻给人上一课。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询