ARTICLE DETAIL

资讯详情

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

Docker网络原理与故障排查:从端口映射到跨主机组网

Docker网络原理与故障排查:从端口映射到跨主机组网 各位老哥今天想跟大伙聊聊 Docker 网络这块儿。我最早接触 Docker 的时候以为容器网络就是跑起来就能访问结果第一次部署应用就被上了一课容器起来了MySQL 本机连不上两个容器之间互相 ping 不通宿主机防火墙一开整个服务直接瘫痪。后来花了整整一个下午从docker inspect一路查到 iptables 规则才把整条链路摸清楚。这篇文章我就把这段时间折腾出来的经验做个梳理重点讲讲 Docker 的几种网络模式、端口映射背后的原理以及网络不通时怎么一步步定位问题。不管你是刚把 Docker 装好、正被docker网络不通折腾的新手还是准备从单机容器走向多机部署的开发者这篇内容应该都能给你一些参考。1. 容器网络为什么总让人摸不着头脑从一次常见故障说起很多教程只告诉你docker run -p 3306:3306 mysql这样一条命令用完就完事儿了。但真正出问题的时候你会发现端口映射看起来没问题为啥就是不通。这一章我们先不急着敲命令先把 Docker 网络最基本的工作原理讲清楚。1.1 bridge网桥与veth pair容器的虚拟网线Docker 默认使用 bridge 模式也就是通过一个虚拟网桥docker0把宿主机和容器连接起来。你可以把docker0想象成一台虚拟交换机宿主机是连在这台交换机上的一个端口每个容器则是交换机下的另一台设备。为了让容器能接入这台交换机Docker 会为每个容器创建一对 veth pair——这是 Linux 内核提供的一种虚拟网线一头连着容器内部的eth0另一头挂在宿主机的网桥上名字通常是vethxxxxxx这种随机后缀。创建一对 veth 接口就是把一根网线的两头分别插到两个namespace里数据进去一边另一边就能收到中间不需要真实物理链路参与。# 查看宿主机上的 veth 接口 ip link show type veth # 查看网桥上的接口列表 bridge link show docker0你执行完上面两条命令会看到一堆veth开头的接口。这就是每个容器在宿主机侧的网线插头。理解了这层关系你也就明白为什么两个容器默认能互通——它们本来就挂在同一个虚拟交换机上。容器之间的数据包从 A 的eth0发出经 A 的 veth 到达网桥网桥根据目的 MAC 地址转发到 B 的 veth最后进入 B 的eth0。这里有一个很容易被忽略的点docker0网桥的默认网段是172.17.0.0/16但如果你在服务器上部署了多个 Docker 项目并且每个项目都用了默认 bridge 网络它们之间是可以互相访问的。这在某些安全要求比较高的场景下是个隐患。后续我们会讲到自定义 bridge 网络那才是隔离的正确姿势。1.2 端口映射背后的隐藏机制docker-proxy与iptables DNAT当你执行docker run -p 8080:80时Docker 实际做了两件事第一启动一个docker-proxy用户态进程它监听宿主机的8080端口并把流量转发到容器的80端口。这个进程存在的历史原因是为了解决某些场景下内核转发不可用的问题在 Linux 上它更像一个兜底方案。第二在 iptables 的nat表中添加一条DNAT规则把所有发往宿主机8080端口的包目的地址改写为容器的 IP 和80端口。因为目标地址被改了这些包会直接通过网桥转发给容器根本不经过 docker-proxy 进程。这也是为什么你用ss -tlnp有时候能看到docker-proxy进程在监听但实际流量却没走它——真正的转发路径是 iptables 那条快速通道。反过来如果你在宿主机上开了防火墙但又没放行对应端口那条 DNAT 规则可能被防火墙策略拦截外面的流量根本进不来。这就引出了最经典的一个坑注意-p映射成功不代表外部一定可以访问。宿主机防火墙如果拦住了对应端口DNAT 规则再完美也没有用。我遇到过不只一次这种情况服务在本地 curl 完全正常换一台机器访问就是不通。排查到最后发现是云厂商安全组没放行端口跟 Docker 本身没有任何关系。所以遇到映射了但无法访问的问题第一件事先看宿主机防火墙和安全组不要急着怀疑 Docker。1.3 同一主机上容器间的三条互通路径同一台宿主机上的多个容器有几种互相访问的方式第一种直接通过 IP 访问。如果你在docker run时没有指定网络模式所有容器默认都在同一个docker0网桥的网段内通过docker inspect 容器名 | grep IPAddress查到 IP 后容器之间可以直接用这个 IP 通信。但这种方式有个问题容器重建后 IP 可能变化写死 IP 很容易翻车。第二种使用自定义网络和 Docker DNS 解析。创建一个自定义 bridge 网络后加入该网络的容器会自动启用内嵌 DNS 解析。只要网络内部有多个容器Docker 会自动根据容器名生成 DNS 记录容器之间可以直接用名字互访。比如你的应用容器里写jdbc:mysql://mysql:3306因为mysql这个容器名会被 DNS 解析成对应的容器 IP 地址而这个 IP 在容器重建后会自动更新。这也解释了为什么我强烈推荐用容器名而不是 IP 通信。第三种host 网络模式下的直通。当容器使用--network host时它共享宿主机的网络命名空间直接通过localhost就能访问同主机上的其他服务。这种方式性能损失最小但也失去了网络隔离端口冲突的风险也同步变高。2. 五种网络模式该如何选一张表讲清适用范围Docker 官方提供五种网络模式bridge、host、none、macvlan 和 overlay。很多人学完 Docker 只知道 bridge 和 host剩下三种听过名字但不知道怎么用。我把它们放在一起对比顺便补充每种模式适合什么场景。网络模式配置方式隔离性性能适合场景典型注意点bridge--network bridge容器间隔离共享网桥有少量NAT开销单机多容器、开发环境IP不固定需用服务名通信host--network host无隔离共享宿主机网络栈近乎物理机性能性能敏感、端口多且动态端口冲突无独立IPnone--network none完全隔离无网络能力安全性极高的离线任务无法联网需手动配置macvlan--network macvlan每个容器独立MAC/IP接近物理机性能希望容器像局域网设备一样工作依赖交换机不支持VLAN间通信overlay--network overlay跨主机虚拟二层网络有封装开销多机集群、Docker Swarm需初始化Swarm集群2.1 bridge模式单机默认方案的正确使用姿势bridge 模式是 Docker 的默认选项但这不意味着你什么都不用管。在实际项目中直接使用默认的docker0网桥并不是最优解。我推荐你在项目启动前先创建自定义 bridge 网络docker network create --driver bridge my-net为什么强调自定义原因有两点一是同一个自定义网络内的容器才能通过容器名互通默认网桥不支持自动 DNS 解析老版本 Docker 支持--link但那是历史遗留方案不建议新项目使用二是自定义网络可以独立配置网段、子网掩码和网关当你的公司内网网段恰好是172.17.0.0/16时默认网桥会产生路由冲突。比如你在服务器上同时跑着 OpenStack 相关服务内部管理网段是172.17.x.x这时候 Docker 默认网桥就跟它撞网段了路由表会变得非常混乱。遇到这种场景直接在创建网络时指定一个不冲突的网段docker network create --driver bridge --subnet192.168.100.0/24 --gateway192.168.100.1 my-bridge不过这种固定网段的方式也有代价Docker 不会自动管理 IP 分配如果你手动指定了静态 IP就要确保 IP 没有被别的容器占用否则网络会冲突。我见过不少人在 Docker Compose 里给每个容器固定 IP结果后面再加容器时忘了检查网段余量IP 冲突导致服务间歇性不可用排查起来非常痛苦。2.2 host模式性能优先的取舍与代价host 模式的原理是不为容器创建独立的网络命名空间容器直接使用宿主机的网络协议栈。这意味着容器的localhost和宿主机的localhost是同一个端口号直接落在宿主机上。这个模式的优点很明确网络性能几乎无损因为没有 NAT 和网桥转发同时也避免了端口映射这层代理容器内监听的端口在宿主机上直接可见。缺点同样明显一是端口冲突风险高你启动的容器如果监听 8080宿主机上任何进程都不能再用 8080二是容器没有独立 IP无法按容器维度做网络隔离。哪些场景适合 host我的经验是对网络性能要求极高的中间件容器比如一些日志采集 Agent需要高速上报数据、需要大量动态端口暴露的服务以及某些对时延极其敏感的实时通信程序。在这些场景下host 模式能让你少折腾不少映射配置。但有一点很多人没意识到host 模式下Docker 的-p参数会被直接忽略因为网络栈已经是宿主机的了再做端口映射就没有意义。如果你执行docker run --network host -p 8080:80 nginx运行时不会报错但 8080 映射不会生效你访问宿主机的 80 端口才能到 nginx。这个坑我踩过一次。2.3 none模式与macvlan模式特殊场景的定位none模式适合那些完全不需要网络功能的容器比如一些定期执行离线计算任务的批处理脚本容器、安全审计类容器或者你希望从外部桥接一个手工配置的网络给容器使用。启动后容器内只有lo回环接口没有 eth0连网关都没有任何出网操作都会失败。macvlan模式则非常有特色它允许你给容器分配一个宿主机所在局域网的真实 IP容器看起来就像网络上的一台独立物理设备。从交换机的视角看这个容器有自己独立的 MAC 地址和 IP 地址。这样做的好处是局域网内其他设备可以直接访问容器不需要经过端口映射容器之间的通信也不走网桥性能接近物理机。部署 macvlan 时宿主机网卡需要开启混杂模式因为实际上它会收到本来发给容器 MAC 地址的帧需要把这些帧交给容器的 veth 接口。这带来一个明显的限制大多数家用路由器/交换机不支持同一个物理端口下 MAC 地址在不同的 VLAN 间通信所以使用 macvlan 时容器默认不能和宿主机直接通信除非额外为宿主机创建一个 macvlan 网卡并配置同网段 IP。2.4 overlay模式跨主机网络的入口overlay 网络打通多台宿主机之间的容器通信它通过 VXLAN 隧道技术把分布在多台机器上的容器连接成一个虚拟的二层网络。Docker 的 overlay 模式通常配合 Docker Swarm 使用——你需要先初始化一个 Swarm 集群才能创建 overlay 网络。我用 overlay 的体会是它是 Kubernetes 和 Docker Swarm 这类容器编排平台的基础设施单机环境下用不上但一旦需要把服务跨机器部署它就成了刚需。VXLAN 的封装有一定性能损耗大约 10% 左右这在物理机网络带宽充裕的机房环境里通常可以接受。不过如果你跑的是高频交易或者物联网消息转发这类高吞吐低时延的服务还是优先考虑 host 模式或者直接部署在物理机上更稳妥。3. 从docker网络不通到定位根因一套可复用的排查链路我一直认为排查网络问题的能力比构建网络配置的能力更重要。你配置好网络之后几乎一劳永逸但一旦出问题如果没有清晰的排查思路很容易被困在看起来全对就是不通的死胡同里。下面这套流程是我自己反复用过的基本能覆盖大部分docker网络不通的排查场景。3.1 先摸清脑海里那张地图docker network系列命令排查第一件事是搞清楚当前容器挂在哪个网络上、IP 是什么、端口映射是什么。三个命令就够了# 查看所有网络 docker network ls # 查看指定网络的详细配置包括已连接的容器 docker network inspect my-net # 查看容器端口映射情况 docker port my-containerdocker network inspect输出里有两个信息特别有用一个是Containers段列出所有连到这个网络的容器以及它们的 IP另一个是IPAM段显示当前网络的子网范围和网关。子网范围决定 IP 分配池网关就是容器默认路由的下一跳。提示如果容器没出现在预期的网络里检查一下你的容器启动参数是不是多个容器加入了不同的网络。有时候你以为它们都在同一个网络栈里实际一个是 bridge另一个是 host自然互相访问不了。这一步做完你应该能回答三个问题容器在哪个网络IP 是什么端口怎么映射的如果这三个问题答不上来后面的排查都是盲人摸象。3.2 进入容器内部查看真实网络状态宿主机上的网络状态正常不代表容器内部网络正常。进入容器看看eth0是否分配了地址、路由表是否完整、DNS 配置是否正确# 进入容器并查看网络接口 docker exec -it my-container sh ip addr ip route cat /etc/resolv.conf一个常见故障是容器内的resolv.conf是空的或者指向了一个不可达的 DNS 服务器。Docker 默认把宿主机的 DNS 配置复制到容器里但如果你用了自定义 bridge 网络Docker 内嵌 DNS127.0.0.11会作为一个选项写入容器配置。如果这个内嵌 DNS 失效了容器之间通过名字互访就会失败表现为明明同一个网络但 ping 容器名不通。另外很多基础镜像是精简版里面连ping、ip这些工具都没有。遇到这种情况你可以用docker exec my-container cat /etc/hosts先看 hosts 文件或者临时装一个iproute2、iputils-ping再排查。我个人习惯在排查前先确认镜像内有哪些命令可用避免跑到一半发现没有工具干瞪眼。3.3 容器网络不通的常见根因清单这一步我按概率从高到低列一下可能导致网络不通的因素端口映射遗漏创建容器时忘了加-p外部访问自然不通。检查docker port是不是空的。宿主机防火墙cloud 平台安全组、firewalld、ufw或 iptables 规则拦截了对应端口。关闭防火墙测试一下就知道是不是这个原因。容器内进程监听地址错误容器里的应用只监听了127.0.0.1而没有监听0.0.0.0。这是最隐蔽的坑网络配置、端口映射全对但应用只听本机回环外部流量进来直接被内核拒绝。查一下容器内ss -tlnp看监听地址是0.0.0.0还是127.0.0.1。自定义网络和外部网络路由冲突比如容器内到某个目的网段的路由被 Docker 默认路由覆盖了导致包发不出去。这种情况ip route能看到异常。iptables 规则被第三方工具清空重写比如你装了一些安全软件或手动执行了iptables -F把 Docker 创建的链清掉了。Docker 的转发链通常叫DOCKER和DOCKER-USER如果这两个链不存在基本就是这个原因。3.4 一个完整的排查案例复盘这里我写一个真实的排查过程读者可以感受一下上面这套方法怎么串起来用有一次我在服务器上部署一个前端容器 A 和后端容器 B前端通过http://backend:8080访问后端 API。结果启动后浏览器一直报 502。我按上面三步走了一遍先用docker network inspect查看两个容器都在my-net网络上IP 也正常没有端口映射问题。然后进入容器 A 执行ping backend结果返回bad address backend——这说明 DNS 解析失败了。继续检查/etc/resolv.conf发现 nameserver 指向了127.0.0.11看起来正常。但我再仔细查看发现容器 A 实际没有加入自定义网络它用的是默认 network。原来容器 A 启动时是第一次写项目时创建的启动命令里漏掉了--network my-net参数。默认 bridge 网络不支持 Docker 内置 DNS 解析所以它自然无法解析backend这个容器名。解决方案很简单把容器 A 停掉用docker network connect my-net 容器A把它加入自定义网络再启动问题就解了。这个案例我想强调的点是很多时候不是你配置了错误而是某个容器漏了配置所以整个链路就断了。用docker inspect去逐个确认每个容器到底在网络里还是不在网络里比盯着一个可疑容器反复测试要高效得多。4. Docker Compose 与多容器项目的网络规划如果你用一个一个docker run来管理多个容器你会发现网络配置很快就失控了。Docker Compose 提供了更规范的多容器编排方式也把网络问题前置到了配置文件中让你可以在启动前就把整个网络拓扑设计好。4.1 compose 里网络到底该怎么配Docker Compose 默认会为每个项目创建一个独立的网络前缀通常是项目名比如myproject_default。所有服务默认加入这个网络服务之间可以通过服务名互访。如果你需要多个项目之间共享网络或者要建立一个更复杂的网络拓扑就得在docker-compose.yml里显式声明网络version: 3.8 services: nginx: image: nginx:latest ports: - 8080:80 networks: - frontend - backend app: build: . networks: - backend mysql: image: mysql:8.0 networks: - backend networks: frontend: driver: bridge backend: driver: bridge在这个例子里nginx同时挂在frontend和backend两个网络上app和mysql挂在backend网络上。这样设计的好处是nginx可以通过frontend网络暴露给外部同时通过backend网络访问app但mysql不会暴露给外部只有app能访问它。这在安全隔离上比所有服务混在同一个网络里要好得多。端口映射在 Compose 里默认是宿主机端口在前、容器端口在后。如果你漏写了ports容器就只能服务内部访问外部完全不可达。这个和docker run的行为一致但 Compose 里漏写的情况更容易被忽略。4.2 固定容器 IP 与依赖启动顺序的坑很多人喜欢在 Compose 里手动给每个服务指定 IPservices: mysql: networks: backend: ipv4_address: 192.168.100.10这样确实能让 IP 固定下来但随之而来的问题是你必须保证backend网络的子网覆盖这个 IP否则 Docker 会直接报错。其次如果你后续往同一个网络里加容器容器的自动分配 IP 可能会和你的静态 IP 重复到时候网络冲突排查起来真的要怀疑人生。我目前的做法是用服务名作为唯一的寻址方式不固定任何容器 IP。毕竟容器本身就是短暂的把 IP 写死等于给自己埋雷。另一个常见的坑是启动顺序的问题。比如你先启动 app 容器再启动 mysql 容器。app 容器在启动时会尝试连接mysql:3306但此时 mysql 还没起来连接失败整个应用直接崩溃退出。这不是网络配置的错而是应用缺少重试机制。正确的做法是在应用侧加入重试逻辑或者在 Compose 配置里添加depends_on再配合健康检查services: app: depends_on: mysql: condition: service_healthy mysql: healthcheck: test: [CMD, mysqladmin, ping, -h, localhost] interval: 5s timeout: 5s retries: 104.3 healthcheck 是分布式应用的网络缓冲上面这个healthcheck配置很关键。它让依赖方等待被依赖方处于健康状态再启动避免了网络已通但服务未就绪的尴尬。我见过太多人用depends_on却从头到尾不配置健康检查结果 MySQL 已经启动但还没完成初始化app 连过去直接报Access denied然后整个编排直接失败。健康检查本身也会消耗一定资源interval 不宜设置太短5 秒是相对合理的经验值。对于重负载的数据库建议把 interval 放宽到 10 秒左右避免频繁探测影响正常业务。5. 一套能扛住跨主机部署的网络设计思路单机 Docker 学到一定程度你必然会遇到多台服务器怎么组网的问题。这一章我把跨主机的常用方案和选型经验做个总结属于进阶一点的内容但也没有那么高深。5.1 为什么 Swarm 的 overlay 不一定是首选Docker Swarm 提供了开箱即用的 overlay 网络确实很方便。你只需要在 manager 节点上执行docker swarm init docker network create --driver overlay --attachable my-overlay之后任何加入 Swarm 集群的节点都能把容器挂到这个 overlay 网络上跨主机的容器就像在同一台机器上一样互访。--attachable参数很关键它允许非 Swarm 服务比如通过docker run启动的容器接入 overlay 网络否则路会堵死。但 Swarm 本身在持久化存储、跨主机卷复制方面比较薄弱很多团队最终会转向 Kubernetes 或者裸机上直接用更轻量的网络方案。如果你只是个人实验或者小团队几十个容器Swarm 足够用如果集群规模到几百个节点Kubernetes 生态的 CNI容器网络接口更成熟比如 Calico 和 Flannel它们底层用的也是 VXLAN/BGP 之类的技术但管理面丰富得多。5.2 从网络互通到流量治理更实际的中间层思路在跨主机网络里很多人只盯着二层互通忽略了更重要的流量治理需求。比如你要限制某个容器只能被特定的服务访问或者要对多个副本做负载均衡单靠 overlay 网络的自愈能力是不够的。一种折中方案是容器统一走 host 网络由宿主机上的 Nginx 或者 HAProxy 来做反向代理和负载均衡。在这种架构里容器没有独立 IP每个容器监听一个宿主机端口Nginx 通过 upstream 把请求分发到不同端口。这个方案牺牲了容器粒度的网络隔离但性能好、排障方便网络问题直接退化成普通的端口和负载均衡问题。很多生产环境的中间件集群就是这么跑的。5.3 网络策略与安全隔离很多 DBA 和运维会忽略的事最后提一个容易被忽略的点Docker 本身的网络隔离只做到二层和三层不提供四层以上访问控制。限制特定容器访问另一个容器的某个端口光靠 Docker network 是做不到的必须依赖 iptables 或者 CNI 网络策略。比如你不能因为 container A 和 container B 都在同一个 bridge 网络上就默认 A 可以访问 B——实际上确实可以访问因为 Docker bridge 网络默认没有 ACL 能力。如果你在 Kubernetes 环境里要善用 NetworkPolicy 资源来定义哪些 Pod 可以访问哪些服务。在纯 Docker 环境里你需要写 iptables 规则或者用weave、calico这类工具来补上网络策略能力。这块内容很深我目前实战经验也还在积累中但至少有一点是明确的网络规划和业务安全是强绑定的不要等事故出了再回头补网络策略。给新手的最后建议Docker 网络说难不难说简单也不简单。我自己是一路踩坑过来的从端口映射失效到容器间 DNS 解析失败再到跨主机路由冲突几乎每个问题都让我多长了一个心眼。如果你现在正准备搭建 Docker 环境我建议你把网络配置当成项目的一部分来设计而不是事后补救。多花十分钟想清楚容器之间怎么互访、哪些容器需要暴露给外部、要不要设固定 IP很可能帮你省下一整天的排查时间。最后分享一个实用的小技巧启动容器后立刻执行docker network inspect和docker exec验证一遍网络链路是否和预期一致花不了两分钟但能在出问题之前发现问题。网络这块永远值得多花点心思祝大家都能少踩几个网络不通的坑。
返回列表