news 2026/10/24 4:42:42

Docker容器名解析与自定义网络排障指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Docker容器名解析与自定义网络排障指南

把localhost:6379改成redis:6379,重启容器,日志里立刻冒出Connection refused或Name or service not known——这个场景我在不同的团队里见过太多回。很多人第一反应是“Docker 的 DNS 坏了”,其实是根本没进到带名字解析的那张网络里。Docker 容器之间通过名字相互访问这件事,答案并不在容器本身,而在它接入的网络类型上:默认的bridge网络不提供自动名字解析,只有自定义网络才会给容器塞进一个内置 DNS,让容器名、网络别名、Compose 服务名变成可解析的主机名。这篇内容讲的就是这条链路怎么搭、怎么验、坏了怎么查。刚上手容器编排、被unknown host卡住半天的人可以照着做;已经在跑多服务栈、想搞清楚“为什么 scale 之后流量全压在一个副本上”的人,也能在这里找到答案。

1. 默认 bridge 网络里容器名为什么“无法通过名字访问”

1.1 一次典型的连接失败:地址从 localhost 改成容器名的落差

先说清楚失败从哪里来。单机开发的时候,很多人习惯把数据库和缓存跑到宿主机上,应用配置里写127.0.0.1:6379,一切正常。后来图省事,把 Redis 也塞进容器,配置改成redis:6379,代码没动一行,连接直接挂掉。这里有两个独立的问题叠在一起,新手往往只看到第一个。

第一个问题是网络不在同一张网里。不加--network启动的容器,默认落在bridge网络上,也就是宿主机上那块docker0虚拟网卡。这张网络确实能让容器互通 IP,但它不提供基于名字的自动发现。你在app容器里ping redis,得到的是bad address,因为/etc/resolv.conf里根本没有指向 Docker 的解析器。

第二个问题是端口语义搞混了。-p 6379:6379这种端口映射是给宿主机和外部访问用的,容器之间通信走的是容器自己监听的端口,跟映射没关系。有人把映射端口改成-p 16379:6379,然后容器里还去连redis:16379,自然连不上。这两个坑我在代码评审里都指出来过,改起来都是一行的事,但定位起来能磨掉半天。

1.2 127.0.0.11:自定义网络悄悄加入的解析器

判断一个容器有没有名字解析能力,最快的办法是看它的/etc/resolv.conf。默认 bridge 网络下,这个文件基本是宿主机那份配置的拷贝,nameserver指向的是公司内网 DNS 或者公共 DNS,解析redis这种单段名字会直接失败。

换成自定义网络,内容立刻变了:

docker run --rm --network demo_net alpine cat /etc/resolv.conf
nameserver 127.0.0.11 options ndots:0

127.0.0.11这个地址不是随便挑的,它是守护进程内置的 DNS 解析器,每个容器通过 loopback 访问它。外部域名(比如registry.example.com)的查询会被它转发给宿主机的上游 DNS,而容器名、网络别名这类“内部名字”则由它直接答复。这就解释了为什么有些环境里容器能访问外网域名、却解析不了隔壁容器——转发链路是通的,内部记录根本没有。

顺带提一句ndots:0。这个选项意味着任何带点的名字都会被优先当成完整域名去查,不再尝试拼接搜索域。它省掉了无谓的查询,但也意味着你写db.local这种名字时要格外小心,Docker 不会帮你补全后缀。排查“解析慢”问题时,这个细节值得看一眼。

1.3 名字解析的三个来源:容器名、网络别名、Compose 服务名

能解析的名字不止一种,混着用是后面很多混乱的根源。把它们分清,比记住命令重要得多。

  • 容器名:docker run --name cache里的cache。容器接入自定义网络后,这个名字会被注册进内置 DNS,同网络的其他容器可以直接用它访问。
  • 网络别名:通过--network-alias或 Compose 的aliases显式挂上去的名字,一个容器可以挂多个。它才是“稳定入口”,因为它不跟着容器实例走。
  • Compose 服务名:docker compose启动时,会把服务名作为别名注册到该服务所有副本上。这就是为什么redis这个服务名在 Compose 里天然可解析,哪怕实际容器叫demo-redis-1。

三者的生命周期不一样。容器名跟着具体实例走,容器删了重建,如果名字变了(Compose 自动加序号就会变),依赖它的配置全废。服务名和别名跟着服务走,实例怎么滚动都不影响。我在项目里定的规矩很简单:对外只认服务名或别名,容器名当成调试用的临时标签。这条规矩看着保守,但它能省掉后面无数次配置漂移。

还要提醒一个容易误导排查的现象:在某些旧版本或者使用了--link的场景下,容器名会被写进/etc/hosts。如果你在hosts里看到一条记录,就以为名字解析没问题,结果换个网络就崩了。名字解析的正规来源是内置 DNS,hosts里的残留只能当线索,不能当依据。

2. 手工建一张自定义网络,把名字互通流程跑通并验证

2.1 建网络时的三个参数不要省略

自定义网络一条命令就能建,但我建议一开始就把网段和网关写清楚,不要全靠自动分配:

docker network create \ --driver bridge \ --subnet 172.28.0.0/16 \ --gateway 172.28.0.1 \ demo_net

为什么值得多敲这几个字符?因为默认自动分配的网段是172.17.0.0/16之后的空闲段,多台机器、多个项目并行开发时,很容易和公司内网、办公网段撞上。一旦撞了,表现是“容器能解析但连不通”,排查起来极难往网段冲突上想。我吃过一次这个亏,本地能跑、换台机器就间歇性超时,最后发现是172.18.0.0/16和办公网某段重叠了。

/16这个大小也值得说一下。65534 个可用地址对本地开发绰绰有余,但如果你的编排平台会给每个项目建一张网,段划得太小会在容器数量上来之后出现地址耗尽,日志里报no available IPv4 addresses。/24起步、按项目递增是比较稳妥的做法,比如172.28.0.0/24、172.29.0.0/24,一眼看得出归属。

2.2 起容器:--name 与 --network-alias 的分工

网络有了,接下来两个容器接入同一张网:

docker run -d --name cache \ --network demo_net \ --network-alias cache \ redis:7-alpine docker run -d --name api \ --network demo_net \ demo/api:latest

--name cache和--network-alias cache在这里看起来重复,实际含义不同。前者是容器实例的名字,后者是网络层面的解析记录。之所以两个都写,是因为我希望即使某天容器被换成别的名字(比如临时起一个cache-debug做对比测试),cache这个别名依然指向正确的服务。别名是稳定的契约,容器名是易变的实现。

一个容器挂多个别名也是常规操作,用来兼容历史配置:

docker run -d --name cache \ --network demo_net \ --network-alias cache \ --network-alias redis \ --network-alias kv \ redis:7-alpine

老代码里写的是kv,新代码写的是redis,过渡期两边都能跑,不用一次性全改。这种做法在迁移期特别省事,但记得给自己设一个截止日期,别名堆太多,半年后没人说得清哪个是正式的。

反过来,多个容器挂同一个别名会怎样?内置 DNS 会为这个名字返回多个地址,顺序不固定,客户端连到哪个看运气。这算是一种简陋的负载分摊,只能在“多个副本完全无状态”的前提下用,别拿它替代正规的服务发现。

2.3 验证链路:从 getent 到 nc,从解析到连接

服务起完不要直接跑应用,先把链路分段验一遍,出问题时才知道断在哪一环。

第一步,确认两个容器在同一张网:

docker network inspect demo_net --format '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{"\n"}}{{end}}'

第二步,确认名字能解析。getent hosts比ping好用的地方在于它不依赖 ICMP,很多精简镜像里没有 ping,但getent基本都在:

docker exec -it api sh -c "getent hosts cache"

第三步,确认端口能通。nc在部分镜像里也没有,那就用更原始的办法,或者临时起一个带工具的容器接入同一张网:

docker run --rm -it --network demo_net nicolaka/netshoot \ sh -c "nc -zv cache 6379"

我通常把这三步写成一个小脚本放在仓库的scripts/下,换机器、换环境先跑一遍。它的价值不在于省时间,而在于把“网络层、解析层、传输层”三个独立的故障域拆开。见过太多次,有人直接拿应用日志排查,其实连 DNS 都没通过,绕了两小时才回到第一步。

2.4 一个容器接两张网:跨网络访问的正确做法

真实场景里经常需要一个容器同时出现在两套网络中,比如让网关既连着前端网,又连着后端网。改网络用docker network connect:

docker network connect backend api

跑完之后api就在frontend和backend两张网里各有一张网卡。这里有个必须记住的点:容器内直接按名字访问,只对“同网络”的容器成立。api能解析frontend网里的对端,也能解析backend网里的对端;但frontend网里的另一个容器,解析不到backend网里的名字,因为它不在那张网里。这正是网络隔离的实现方式,不是 bug。

还有个细节:用--network启动时,docker run支持重复指定,但只有第一次指定可以用--ip固定地址,后续connect时再指定--ip才生效。想固定 IP 的话,建议一开始就把地址规划好写进文档,否则容器重启后 IP 漂移,任何硬编码了 IP 的配置都会随机失效。

3. Compose 里的服务名解析,和你想的不太一样

3.1 服务名、容器名、别名:到底该依赖哪一个

Compose 把上面手工做的事全自动化了,代价是很多东西被藏起来,出问题时不好定位。先看一个典型片段:

services: web: image: demo/web:latest networks: - frontend api: image: demo/api:latest networks: - frontend - backend db: image: postgres:16-alpine networks: backend: aliases: - postgres - pg networks: frontend: backend:

默认情况下,Compose 会建一张名为<项目名>_default的网络,把所有服务放进去。上面这段覆盖了默认行为,明确划了前后端两张网。此时web里可以解析api,但解析不到db,因为web不在backend网里。api两边都能解析,它是那道门。

服务名解析是 Compose 自动注册的别名,实打实能用。但要注意一个反直觉的点:服务名解析到的是该服务所有副本的集合,不是某个具体实例。你docker compose up --scale api=3,api这个名字会返回三个地址。这听起来是好事,实际埋着一个坑,下一节细说。

至于container_name,我的建议是能不用就不用。设了它之后,同一个服务的多副本会直接报名字冲突;而且容器重建后的名字由你控制,一旦改名,所有依赖该名字的配置全断。它唯一的价值是让docker exec好敲一点,这个收益不值得承担运维风险。

3.2 scale 之后服务名解析到多个地址,长连接会把负载分配得不均匀

这是我在生产环境遇到过的最隐蔽的问题之一。服务api扩到 5 个副本,前面挂一个网关,按服务名访问http://api:8080。上线后监控显示 5 个副本里只有一个 CPU 跑满,其余四个接近空闲,但请求量统计又对不上——流量确实是进来了,只是全压在一个实例上。

原因很简单:DNS 层面确实返回了 5 个地址,但应用层用了连接池和 HTTP keep-alive。连接一旦建立就不断复用,后续几百上千个请求都走同一条 TCP 连接,自然全落在同一个副本上。DNS 轮转只在“新建连接”时起作用,而连接根本不会被重建。

这个问题的解法不在 DNS,在客户端:

  • 把连接池的空闲超时压短,比如 30 秒到 60 秒,让连接定期重建,重新走一遍解析。
  • 对于 gRPC 这类长连接协议,使用支持服务发现的客户端负载均衡模式,而不是默认的直连模式。
  • 如果前面有反向代理,把代理层的上游解析周期配置得比 DNS TTL 短,或者在代理配置里显式列出所有副本。
  • 实在绕不开,就在应用里做一个轻量的轮询选择器,每次选一个地址建立短连接。

顺便说个容易忽略的连带问题:容器重建后 IP 会变。如果你的客户端缓存了 DNS 结果又持有长连接,重建瞬间会打到一个已经不存在的地址上,报错是连接超时而不是解析失败,看起来像网络抖动。日志里如果出现“偶发超时,重启后自动恢复”,优先怀疑这条链路。

3.3 用网络隔离把“不该互通”变成默认状态

名字能解析是一把双刃剑。默认把所有服务放进一张网,意味着任何容器都能解析任何容器,一个被攻破的边缘服务可以直接摸到数据库端口。网络隔离解决的就是这个问题,而且成本极低——多写几行 YAML 而已。

做法是按数据流分层:只有确实需要通信的两个服务才共享一张网。上面那段配置里,web和db之间没有共享网络,即使web被完全控制,它也无法通过服务名摸到db。这种“默认拒绝、按需开通”的思路和防火墙是一个逻辑,只是粒度更细,管的是容器级别。

实践中有两个细节值得注意。第一,隔离之后调试会变麻烦,你docker exec进web想连db看看,会发现连不上,这不是环境坏了。临时办法是起一个调试容器,同时接入两张网,用完即删。第二,隔离要写在最初的架构里,等系统跑起来再补,改动面会很大,因为很多隐式依赖是在开发阶段顺手写下的。

3.4 跨 Compose 栈共享网络:external 网络的用法与代价

一个项目一个 compose 文件是最理想的情况,现实里经常是多个栈:基础设施栈、业务栈、监控栈。它们之间要互通,靠容器名硬编码是行不通的,因为彼此的默认网络互相不可见。

正确做法是先建一张共享网络,然后让各个栈以外部网络的方式接入:

docker network create shared_backend
networks: shared_backend: external: true name: shared_backend

然后在需要互通的 service 上挂这张网。这样两个栈的容器就在同一张网里,服务名可以互相解析。

代价有三个,提前说清楚比较好。第一,external网络不会被docker compose down删掉,协作时容易出现“我本地明明重建了,为什么还能解析到旧容器”——因为旧网络还在,旧容器可能也没删干净。第二,共享网络里的服务名冲突风险上升,两个栈都叫redis,解析结果就不可控了。约定命名前缀是必要的,比如infra-redis、biz-redis。第三,网络是隐式依赖,新人 clone 代码直接up会失败,因为网络不存在。解决办法是在 README 或者一个初始化脚本里写清楚前置命令,别指望别人猜。

4. 名字解析失效时的排查链路

4.1 一张按顺序走的检查表

排查网络问题最怕东一榔头西一棒子。我习惯按“越靠下越先查”的顺序走,因为底层没通,上层全是噪音。

现象优先怀疑建议命令
unknown host、bad address两个容器是否在同一张自定义网络docker network inspect <net>
解析出 IP 但连接被拒服务监听地址、端口号是否写错nc -zv <name> <port>、ss -lntp
解析出 IP 但超时服务是否只监听了 127.0.0.1容器内ss -lntp看监听地址
间歇性失败,重启后恢复应用层 DNS 缓存 / 连接池持有旧 IP查客户端 TTL 与池化配置
换了机器就解析失败网络名、项目名不一致docker network ls对比

这张表的用法是从上往下走,走到哪一行失败就停在哪,别跳。我见过有人在“解析出 IP 但连接被拒”的情况下回头怀疑 DNS,白折腾一轮,其实问题就是端口写成了宿主机映射端口。

关于“服务只监听 127.0.0.1”这一条,值得单独强调。很多服务默认只绑 loopback,比如某些队列、某些中间件的默认配置。容器内curl 127.0.0.1:6379能通,别的容器连它就超时。这个坑几乎人人都踩过一次,因为本地测试时一切正常,只有跨容器访问才暴露。检查办法是在容器内跑ss -lntp,看监听地址是0.0.0.0:6379还是127.0.0.1:6379。前者才对外可达。

4.2 五个反复出现的坑:depends_on、host 模式、缓存、AAAA、端口

第一个坑:把depends_on当成“等它准备好”。这个指令只管启动顺序,容器进程起来了它就放行,不管进程内部是否完成初始化。数据库容器启动三秒后未必能接受连接,应用这时候连上去就是被拒。正确做法是配合healthcheck加condition: service_healthy,同时在应用侧做带退避的重试。两件事都要做,少一件都会偶发失败。

第二个坑:network_mode: host让名字解析失效。用了 host 模式,容器共享宿主机网络命名空间,不再接入任何自定义网络,内置 DNS 也就不存在了。此时你要访问同栈的其他服务,只能用宿主机端口或者127.0.0.1。同理,network_mode: "service:xxx"的容器共享对方的网络命名空间,访问对端时应该用127.0.0.1,而不是服务名。这两种模式在性能敏感场景下有人用,但一定要在配置里写注释,后来人不知道会陷进去。

第三个坑:应用层 DNS 缓存。容器重建,IP 变了,但客户端还拿着旧记录。不同语言的默认行为差别很大,有的会缓存几十秒,有的跟随系统解析器的 TTL,有的干脆永久缓存直到进程重启。排查时的判断依据是:报错是连接超时(旧 IP 没人应答),而不是解析失败。解决办法是把缓存 TTL 配小,或者接受“重建后滚动重启客户端”这个前提。

第四个坑:AAAA 查询带来的额外等待。网络是 IPv4 的情况下,对内嵌 DNS 发起 AAAA 查询通常拿不到有效地址,某些客户端会把 A 和 AAAA 查询并发发出,然后等待两个都有结果才继续,表现是解析比预期慢几十毫秒甚至更久。如果你的服务在启动阶段有大量连接建立,这个延迟会累积成可感知的启动变慢。排查时用getent hosts和客户端自己的解析日志对比时间差,能快速定位。

第五个坑:端口映射与容器间端口的混淆。再强调一遍:容器之间通信走容器监听端口,跟-p的宿主机端口无关。-p 8080:80只影响从宿主机和外部怎么进来。这个认知没建立起来的人,会反复在“为什么容器里连 8080 不通”上转圈。

4.3 容器里没有 ping 和 nslookup 时怎么查

精简镜像里这些工具基本都不装,硬装又违背“镜像最小化”的原则。我的做法是两条腿走路。

第一条,优先用镜像里几乎一定有的工具。getent hosts依赖 libc,很可能在;cat /etc/resolv.conf永远可用;wget -qO- http://<name>:<port>/在有 wget 的镜像里也能验证连通。这些组合起来足够判断是解析问题还是连接问题。

第二条,起一个专用调试容器接入同一张网。nicolaka/netshoot这类镜像把dig、nc、tcpdump、ss全打包了,用完--rm删掉,不污染业务镜像:

docker run --rm -it --network demo_net nicolaka/netshoot \ sh -c "dig cache +short && nc -zv cache 6379"

需要抓包确认 DNS 查询有没有发出去、有没有答复时,这个容器里直接tcpdump -i any port 53就能看到。我在排查过一次“解析偶发失败”之后,就把这条命令存进了笔记:内置 DNS 的答复在抓包里是明文可见的,比猜快得多。

5. 生产环境里我坚持的几条规矩

5.1 容器间只认服务名,IP 一律不写死

这条是底线。容器 IP 是动态分配的,重建、扩缩容、换宿主都会变。任何写进配置文件、环境变量、数据库连接串的容器 IP,都是一颗定时炸弹。判断标准很简单:把整个栈down再up,如果哪里连不上,说明那里硬编码了 IP 或者容器名。

同样的道理适用于容器名。Compose 自动生成的容器名带项目前缀和序号,换个目录名、换个-p项目参数,名字就变了。只有服务名和显式网络别名是稳定的,配置里就该只出现这两个。

5.2 启动顺序靠健康检查加客户端重试,不靠运气

depends_on只能保证启动先后,保证不了“能用”。我现在的标准配置是:关键依赖(数据库、缓存、消息队列)一律加healthcheck,依赖方用condition: service_healthy;同时客户端必须有重试,指数退避,上限设一个合理值比如 30 秒。

为什么两套都要?因为 Compose 的健康检查只在启动阶段起作用,运行期间依赖挂掉再恢复,Compose 不会帮你重连。真正兜底的是客户端的重试逻辑。只做前者,运行期一定出问题;只做后者,启动期会刷一堆失败日志,还会拖慢启动。

5.3 连接池和 DNS 缓存的配合要提前设计

前面提过 scale 之后流量倾斜的问题,根治办法是在设计阶段就定好:这个服务是短连接还是长连接,连接池多大,空闲多久回收,DNS 缓存多久刷新。这些参数单看都不起眼,组合起来决定了扩容是不是真的有效。

我的经验值是长连接池的空闲回收设在 30 到 60 秒,比大多数场景下的容器重建耗时要短,能让客户端在合理时间内感知到拓扑变化。这个值需要根据实际压测调整,不能照抄。上线前做一次“扩副本 + 观察流量分布”的验证,比事后看监控猜原因划算得多。

最后分享一个我在实际项目里的体会:容器网络这类问题,难的从来不是配置本身,而是出问题时不知道从哪一层开始查。我后来强制自己做了一件事——每个新项目在写业务代码之前,先把getent hosts、nc -zv、ss -lntp这三条命令跑通一遍,把结果截图放进仓库文档。这个过程只花十分钟,但它把“名字解析”和“端口可达”这两个最容易混淆的故障域提前分开了。之后无论谁接手,遇到连不上,先照着文档跑一遍,绝大多数问题在第二分钟就能定位到具体是网络没通、解析没通,还是服务没监听。这比任何排查技巧都实在。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 4:42:24

高通跃龙IQ-9100端侧平台实现电池极片缺陷检测最小闭环

在写这篇实战记录之前&#xff0c;先说明一下背景。我手上有一段长期跟进的模拟项目X&#xff0c;做的是某型号电池极片的外观缺陷检测。过去用传统方案搭出来的样机占地大、启动慢&#xff0c;每次开机要等工控机进系统、等采集卡加载驱动、等GPU风扇开始转&#xff0c;产线操…

作者头像 李华
网站建设 2026/10/24 4:42:05

PSO/MPSO/TACPSO/SOA/GA栅格路径规划Matlab对比

把栅格地图路径规划这件事做到公平对比&#xff0c;其实比写算法本身更考验功底。市面上讲PSO、GA路径规划的代码很多&#xff0c;但能把PSO、MPSO、TACPSO、SOA、GA这五种算法放在同一张二维栅格地图上&#xff0c;用同一套适应度函数和参数口径去横向对比的示例并不多。这个项…

作者头像 李华
网站建设 2026/10/24 4:41:51

Matlab二维栅格地图下PSO、MPSO、TACPSO、SOA与GA路径规划对比实现

平时写路径规划对比实验&#xff0c;最怕的不是算法本身难写&#xff0c;而是五六个算法各跑各的&#xff0c;地图不一样、迭代次数不一样、适应度函数也不一样&#xff0c;最后给一个“对比图”根本没有说服力。这次我重新整理了一整套基于Matlab的二维栅格地图路径规划对比代…

作者头像 李华
网站建设 2026/10/24 4:41:36

Abaqus子弹穿钢板仿真全解析:从Johnson-Cook本构到参数调试

0. 引言做CAE仿真的人&#xff0c;尤其是搞冲击动力学方向的&#xff0c;基本都绕不开一个经典算例——子弹穿钢板。这个模型在Abaqus里属于典型的高非线性显式动力学问题&#xff1a;接触、大变形、材料失效、单元删除、高速冲击&#xff0c;全凑齐了。很多朋友拿到一个完整的…

作者头像 李华
网站建设 2026/10/24 4:41:22

曼哈顿距离与欧几里得距离:距离度量怎么选才不踩坑

1. 距离度量的直觉&#xff1a;两个坐标点之间的“路”先从一个我实际踩过的坑说起。前阵子我给一个推荐系统做用户相似度计算&#xff0c;默认用了欧几里得距离&#xff0c;结果离线评测效果平平。后来换成曼哈顿距离&#xff0c;在同样的特征下&#xff0c;指标直接涨了好几个…

作者头像 李华
网站建设 2026/10/24 4:40:42

WOA-TCN回归+SHAP可解释性分析:MATLAB时序预测完整方案

做时序预测做得久了&#xff0c;你会发现一个特别尴尬的循环&#xff1a;模型效果不错&#xff0c;但业务方永远会追问一句“这个数到底是怎么算出来的”&#xff1b;等你想换更复杂的结构提升精度&#xff0c;调参又变成另一场噩梦。今天要聊的这套WOA-TCN回归SHAP分析新数据预…

作者头像 李华