ARTICLE DETAIL

资讯详情

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

Docker网络实战:隔离网络、路由表与自定义驱动

Docker网络实战:隔离网络、路由表与自定义驱动 先说个我自己的事。去年给公司搭一套测试环境图省事把所有容器都丢在默认的 bridge 网络上MySQL、Redis、Nginx 一锅炖端口能开就开结果线上排查的时候差点把自己绕进去一个容器挂掉重启之后 IP 变了配置里写死的地址全部失效更尴尬的是安全审计发现数据库端口直接暴露在宿主机上任何容器都能访问。后来我把这套环境彻底拆成隔离网络补上了路由表排查和自定义网络驱动这几课才算真正理解了 Docker 网络的工作方式。这篇文章不会停在docker run -p 3306:3306 就能访问 MySQL这种入门层面而是直接讲生产中真正会用到的东西怎么用隔离网络把服务切成安全域、容器里的路由表到底在做什么、以及当内置网络驱动不够用时macvlan、ipvlan、overlay 甚至自定义网络驱动怎么选、怎么配。内容偏操作我会把自己踩过的坑一并写进去适合已经会跑 Docker 基础命令、但一遇到网络不通就懵的开发者。1. 为什么默认桥接网络在生产环境里不够用1.1 三种内置网络模式的真实分工Docker 安装完成后默认会创建三个网络bridge、host、none。很多人以为这只是三种连网方式其实它们是三种完全不同的网络模型隔离级别和通信路径都不一样。bridge 是默认模式。容器拥有独立的网络命名空间通过一对 veth 虚拟网线把容器内的 eth0 和宿主机的 docker0 网桥连在一起。访问外网时内核会把源地址伪装成宿主机 IP也就是 iptables 的 MASQUERADE 规则。优点是有隔离、能上网缺点是 IP 默认动态分配容器重启后可能就换了。host 模式直接把容器塞进宿主机的网络命名空间容器里看到的网卡就是宿主机的网卡没有独立 IP也没有端口隔离。性能最好但也最不安全而且和宿主机上的进程抢端口。一般只在需要极致网络性能、或者容器要绑定特定物理网卡时才会用。none 模式则是给了容器一个干净的网络命名空间里面只有 loopback 接口。它没法上网也没法和外界通信适合做离线计算或者完全自建网络栈的场景。如果你发现自己写了一套网络插件不想被 Docker 干扰就选用 none。模式独立网络栈容器IP外网访问典型场景bridge有网桥自动分配NAT 转换后可以默认单机容器通信host无无直接用宿主机IP直接使用宿主机网络高性能代理、抓包none有但只有回环口无不可访问离线任务、自定义驱动1.2 隔离网络解决什么业务问题生产环境里多套服务往往同时跑在一台宿主机上比如前端 Nginx、后端 API、数据库 MySQL。如果全部丢进默认 bridge它们之间虽然能互通但同时也意味着任何一个容器被攻破攻击者都能顺着桥接网络直达数据库容器连端口扫描都不用。这就是典型的网络平面未隔离。正确做法是给不同安全域建不同的网络frontend 网络放 Nginx 和部分 APIbackend 网络放 API 和 MySQLAPI 容器同时接入 frontend 和 backend充当跨域桥梁对外只暴露 Nginx 的 80/443 端口。这样一来MySQL 根本不需要暴露到宿主机也不需要发布 3306 端口。外层的 Nginx 容器没有 backend 网络的网卡路由表里根本没有去 MySQL 的路由网络层直接隔绝。即使攻击者拿到了 Nginx 容器的 shell他能探测到的也只有 frontend 网络里的那几个网段数据库对他来说是不可见的存在。隔离网络的好处不只是安全。它还让服务注册变得简单只要两个容器在同一个自定义网络里就能用容器名互相解析不用关心 IP 是否变化。这一点在生产环境里比省一次配置重要得多因为容器重建、扩容、迁移都会导致 IP 变化靠 IP 通信的架构迟早要炸。1.3 我自己踩过的端口全开坑之前我把 MySQL 容器启动命令写成docker run -d -p 3306:3306 mysql图的是本机好用 Navicat 连。后来安全扫描直接给了一个高危漏洞因为端口发布到宿主机之后只要宿主机防火墙没拦外部也能扫到。更麻烦的是同一台机器上如果还有别的项目容器它们都能轻松连上这个 MySQL数据边界完全不存在。后来我调整架构MySQL 只接入 backend 网络API 容器也接入 backend 网络API 内部通过mysql:3306连接完全不发布端口。需要临时调试时再用docker exec进容器或额外起一个临时工具容器接入同一个网络。这样既保留了可调试性又把数据库从默认的全网可见变成了只有特定网络可见。2. 手动创建隔离网络并完成容器接入2.1 创建自定义桥接网络的完整命令Docker 允许我们用docker network create来创建自定义网络。基础命令很简单但生产环境里建议显式指定子网和网关避免 Docker 自动选择的网段和公司内网冲突。docker network create \ --driver bridge \ --subnet 172.28.0.0/16 \ --gateway 172.28.0.1 \ frontend docker network create \ --driver bridge \ --subnet 172.29.0.0/16 \ --gateway 172.29.0.1 \ backend这里选 172.28.0.0/16 和 172.29.0.0/16 是为了避开默认的 172.17.0.0/16 网段也避开常见的公司内网段。如果两台宿主机上的 Docker 网段选重复了将来做多主机连通时会非常痛苦。创建之后可以用docker network ls看到多出来的 frontend 和 backend。出现自定义 bridge 网络时默认隔离行为会自动开启同一个网络里的容器可以通过 IP 通信不同网络之间默认互不可达。2.2 容器接入与固定IP分配创建网络之后启动容器时用--network指定加入哪个网络docker run -d --name mysql \ --network backend \ --ip 172.29.0.10 \ mysql:8.0显式指定 IP 在测试环境里很方便但要注意 IP 冲突风险生产环境更建议用 DNS 名字而不是固定 IP。如果容器已经启动又需要临时接入另一个网络可以用docker network connectdocker network connect frontend api docker network connect backend api这个方法非常实用。一个容器可以同时挂多个网络每个网络给它一张网卡。比如 API 容器连接 frontend 和 backend 之后它在 frontend 网络里是一个 IP在 backend 网络里是另一个 IP两个网络互不相通但 API 容器自己同时拥有一把两边的钥匙。接入之后可以用另一个容器来验证连通性docker run -it --rm --network backend curlimages/curl \ curl http://mysql:3306能收到 MySQL 的协议响应说明同网络下的容器名解析和通信都正常。2.3 Compose 场景下的网络声明使用 Docker Compose 时网络声明会更清晰。项目里一般会有一个docker-compose.ymlservices: nginx: image: nginx:1.25 networks: - frontend ports: - 80:80 api: image: my-api:latest networks: - frontend - backend mysql: image: mysql:8.0 networks: - backend environment: MYSQL_ROOT_PASSWORD: secret networks: frontend: driver: bridge ipam: config: - subnet: 172.28.0.0/24 backend: driver: bridge ipam: config: - subnet: 172.29.0.0/24Compose 会自动创建这两个网络并且让容器归属到对应网络。需要注意的是如果项目里的一个服务想加入一个已经存在的、由其他项目创建的网络需要把该网络声明为 externalnetworks: shared: external: true否则 Compose 会在启动时尝试创建一个和自己想要的外部网络同名的新网络导致冲突。这个细节我在跨项目联调时踩过花了一个多小时才反应过来。2.4 服务通信的常见误区连接不到容器内的MySQL访问 docker 容器内的 mysql、 docker网络不通 这些搜索热词说明一个问题大家最频繁遇到的故障就是服务连接失败。最常见的原因是以下几个。第一个误区应用容器和 MySQL 容器不在同一个网络。如果你用docker run --network frontend my-app启动应用而 MySQL 只在 backend 网络里应用容器里的路由表根本没有去 backend 网段的路由ping不通很正常。解决办法是让应用容器docker network connect backend或者启动时直接指定 backend 网络。第二个误区用localhost连接 MySQL。在容器内访问 MySQL不能用 localhost因为 MySQL 容器和当前容器各自拥有独立的网络命名空间。要使用 MySQL 容器的网络别名或 IP。第三个误区把数据库端口发布到宿主机然后应用容器通过宿主机 IP 去访问。这种架构能跑通是巧合不是设计。因为一旦宿主机防火墙策略变化、或者多宿主机环境里 IP 不通故障就会随机出现。正确做法是通过同一个 Docker 网络中的服务名访问jdbc:mysql://mysql:3306/db。提示容器启动时注意检查网络别名的生效范围。在 bridge 网络中Docker 默认会把容器名写到内置 DNS 里但如果你手动指定了 network-alias容器名解析也可能受影响。排查时优先看/etc/hosts和 Docker 内嵌 DNS 的解析结果。3. 路由表是排查Docker网络问题的第一现场3.1 容器内的路由表到底长什么样网络不通时我第一件事就是进容器看路由表docker exec -it 容器名 ip route对一个接入 bridge 网络的容器输出大体是这样default via 172.28.0.1 dev eth0 172.28.0.0/16 dev eth0 proto kernel scope link src 172.28.0.10第一行是默认路由网关指向 docker0 网桥或自定义网桥第二行是直连路由说明只要目标 IP 在 172.28.0.0/16 网段内就直接通过 eth0 发往二层网络。也就是说同一个隔离网络里的两个容器通信数据包不会出网桥直接走二层交换。再看/etc/resolv.confnameserver 127.0.0.11 options ndots:0127.0.0.11是 Docker 内置 DNS 的地址。它负责把容器名、服务别名解析成具体 IP。很多人遇到容器之间能 ping 通 IP 但 ping 不通名字问题多半出在内置 DNS 或网络别名配置上而不是路由。3.2 从路由表看懂容器通信路径通过路由表能准确判断一条通信路径是否可行。同一个网络内的容器通信A 容器向 B 容器发包因为 B 的 IP 在 A 的直连网段内数据包被打到 eth0然后由桥接设备在二层转发。全程不需要经过宿主机协议栈的转发所以速度很快。不同网络之间的容器通信A 在 frontendB 在 backendA 的路由表里没有 backend 网段默认路由会把包送到 frontend 网关也就是宿主机上的网桥。宿主机如果开启了 ip_forward 和对应的 iptables 规则可能还会尝试转发到 backend 网桥。但 Docker 默认的 bridge 隔离策略会直接 drop 这类跨网络转发所以不通是意料之中的。外部访问容器以发布端口为例宿主机收到外部请求后iptables 的 DNAT 规则会把目标地址改成容器的 IP然后由网桥转发到容器。这种场景下容器内看不到任何变化它只知道自己收到了一个源地址为网关的请求。3.3 宿主机侧的路由和iptables排查网络问题时光看容器内部是不够的还需要看宿主机的路由表ip route会发现多出几条类似172.28.0.0/16 dev br-xxxx scope link 172.29.0.0/16 dev br-yyyy scope linkbr-xxxx就是自定义网络对应的 Linux bridge 设备。如果容器网络一直不通检查这些 bridge 是否处于 UP 状态以及网卡是否绑在正确的 bridge 上。接着看 iptablesiptables -t nat -L -n | grep 172.28能看到 Docker 生成的 MASQUERADE 规则。外部流量访问容器时还要看 DOCKER 链里的 DNAT 规则。一个很容易被忽略的坑是有些云服务器镜像默认把 FORWARD 链策略设成 DROPDocker 创建网络时会尝试插入放行规则但如果 systemd 重启或 firewalld 干扰了规则顺序容器外网通信就会断。遇到这种情况可以检查sysctl net.ipv4.ip_forward输出为 1 才是正常为 0 时所有跨网桥转发都会失败。3.4 常见症状与排查动作对照表症状最可能原因排查动作容器能 ping 通网关但 ping 不通外网宿主机 ip_forward 关闭或防火墙 FORWARD 策略拦截sysctl net.ipv4.ip_forwardiptables -L FORWARD -n容器之间无法用名字通信不在同一网络或内置 DNS 未生效用docker network inspect检查容器归属进入容器查看/etc/resolv.conf从宿主机访问容器端口失败端口未发布或发布到了127.0.0.1docker port 容器名检查映射容器重启后 IP 变化导致连接失败没有使用服务名/IP固定改用内置 DNS 服务名连接或给容器设置固定 IP跨网络容器不通网络隔离设计如此将需要互通的容器docker network connect到同一网络这张表基本覆盖了我遇到的 90% 的网络故障。剩下的 10% 往往要靠抓包和看 daemon 日志但那已经是另一个深度了。4. 自定义网络驱动从macvlan到第三方插件4.1 Docker网络驱动架构内置驱动与扩展点前面讨论的都是 bridge 驱动。Docker 的网络驱动架构里除了 bridge还有 overlay、macvlan、ipvlan、none 等内置驱动以及可以独立安装的外部插件。标题里说的自定义网络驱动我理解有两层含义一是选用内置但非常规的驱动比如 macvlan、ipvlan二是通过 Docker 插件机制接入第三方或自研驱动。Docker 的网络能力基于 libnetwork它是一个插件化的网络管理库。网络驱动通过实现NetworkDriver接口来接入 Docker daemon具体包括创建网络、删除网络、创建端点、删除端点等操作。对绝大多数使用者来说不需要自己写驱动但了解接口边界能帮你判断当团队需要和现有物理网络打通时到底该选哪种驱动。4.2 macvlan让容器直接使用物理网络默认 bridge 网络最大的特点是 NAT容器的 IP 是私有的。但有些场景不希望用 NAT比如公司内部运维系统要求每台机器都有真实 LAN IP或者容器要直接参与物理网络的组播、广播。macvlan 驱动可以让容器直接使用物理网卡的 MAC 地址和交换机通信。每个容器拥有独立 MAC并在物理子网中获取一个真实 IP。创建命令docker network create -d macvlan \ --subnet192.168.1.0/24 \ --gateway192.168.1.1 \ -o parenteth0 \ macvlan_net-o parenteth0指定容器要绑定的物理网卡。启动容器时加入这个网络后容器会被分配 192.168.1.0/24 里的 IP直接能和局域网内其他设备通信。macvlan 最大的坑是宿主机和容器默认不通因为 macvlan 的虚拟接口不能和宿主机的物理接口直接通信这是 Linux 协议栈的硬限制。解决办法是额外创建一个 macvlan 子接口给宿主机使用或者干脆改用 ipvlan。4.3 ipvlanL2模式和L3模式的选择ipvlan 驱动和 macvlan 类似但所有虚拟接口共享宿主机的 MAC 地址靠 IP 区分二层流量。macvlan 面临交换机 MAC 地址表溢出的问题在这种情况下 ipvlan 会更稳定。它也支持两种模式L2 模式和 macvlan 一样直接在物理子网内工作L3 模式不依赖物理二层容器间跨子网由内核路由处理宿主机和容器可以直接通信。选用标准可以简单归纳为如果网络改造阻力大优先 macvlan如果希望容器和宿主机能互访或者担心交换机 MAC 学习问题优先 ipvlan L2如果希望完全摆脱子网限制只保留 IP 路由就用 ipvlan L3。4.4 overlay多主机环境的事实标准手头只有一两台机器bridge 网络够用但容器集群分散在多台宿主机时overlay 才是主流选择。overlay 驱动基于 VXLAN 技术通过宿主机之间的网络隧道把不同机器上的容器网络连成一个虚拟 L2 网络。跨主机容器只要加入同一个 overlay 网络就可以像在同一台机器上一样直接通过 IP 或服务名通信。创建 overlay 网络一般要先初始化 Swarm 集群或者使用支持 overlay 的编排工具docker swarm init docker network create -d overlay --attachable my-overlay如果你只是想在一台机器上用 overlay 做实验可以加上--attachable让普通容器也能加入。生产环境里微服务跨节点通信、服务发现、负载均衡底层往往都依赖 overlay 或同类隧道技术。4.5 自己编写网络驱动的思路如果是大型团队网络环境特殊内置驱动覆盖不了就需要考虑自定义网络驱动。做法一般是写一个 Docker 插件通过 Unix socket 和 Docker daemon 通信并实现NetworkDriver接口。接口的核心方法包括CreateNetwork接收网段、网关、驱动选项DeleteNetwork删除网络时清理资源CreateEndpoint为容器创建虚拟网卡并配置 IPDeleteEndpoint清理端点Join/Leave容器加入/离开网络时配置网络命名空间。如果你只是想快速实现可以看 Docker 官方插件 SDK 或一些开源驱动项目比如基于 Open vSwitch 的网络插件。实现完成后通过docker plugin install安装Docker 就会在docker network create -d 你的驱动名时调用。这一层其实是给高级网络研发准备的。普通业务团队不需要走到这步但理解了这个边界你就能明白 Calico、Weave 这些外部网络组件为什么能接入 Docker也就能在选型时更有底气。5. 热搜词背后的网络故障排查清单5.1 docker网络不通到底怎么查搜索热词里反复出现docker 网络不通和访问 docker 容器内的 mysql这类问题的排查路径其实非常固定我整理成了一套顺序先确认容器本身状态docker ps -a看是否在运行、是否反复重启确认容器归属网络docker inspect 容器名 | jq .[].NetworkSettings.Networks看有没有接入预期的网络进入容器验证网络栈docker exec -it 容器名 ip addr看有没有拿到 IP做分段连通性测试先 ping 网关再 ping 同网络容器最后 ping 外部域名如果只有 ping 不通但业务正常可能只是 ICMP 被防火墙禁了。按照这个顺序做大部分问题会在第 2 步或第 3 步暴露出来。我曾见过一个诡异案例容器内应用之间 HTTP 正常但ping超时查到最后是安全基线脚本把 ICMP drop 了。所以排查时不要只依赖 ping要用实际业务的协议去验证。5.2 permission denied经常被误当成网络问题热词里有一条这个错误permission denied while trying to connect to the docker api at unix:///var/run/docker.sock很多人以为这是网络权限问题跑来查 iptables其实这跟容器网络没有关系是当前用户没有访问 Docker daemon 的权限。解决办法就两行sudo usermod -aG docker $USER newgrp docker重新登录之后当前用户就能直接用docker命令了。判断标准很简单如果所有docker命令都报这个错那一定是 socket 权限问题而不是某个容器网络的问题。这种情况我在新入职的团队里见过很多次大家围着容器网络查了半天最后发现是用户组少了一个成员。5.3 镜像下载慢与网络配置的低级陷阱搜索热词里还有docker镜像下载慢和docker创建失败。镜像下载慢通常和容器运行时网络关系不大而是 registry 连接问题。常见处理办法是在/etc/docker/daemon.json里配置镜像加速源{ registry-mirrors: [https://your-mirror.example.com] }配置后重启 Docker daemonsudo systemctl restart docker这个配置属于看起来和网络无关却经常在容器网络排障时被翻出来的类型。你要明确一点镜像下载链路和容器运行网络是两个独立的网络路径前者是 daemon 访问 registry后者是容器访问业务系统别混在一起查。5.4 我沉淀下来的网络检查顺序这几年的实操经验告诉我调试 Docker 网络最忌讳东戳一下西戳一下。我给自己定了一个固定顺序每次都能快速定位问题docker network ls看网络是否存在、类型是否预期docker network inspect 网络名看容器端点列表、子网网关、驱动配置docker exec -it 容器名 ip route看容器内路由是否完整查看宿主机ip addr和iptables -t nat -L -n确认 bridge 和 NAT 状态抓包确认docker exec -it 容器名 tcpdump -i eth0看到底是请求没发出还是响应没回来。这套顺序的核心是从容器往上层查因为 Docker 网络的故障大多发生在宿主机的 bridge 或 iptables 层容器内部能报出来的错误往往只是表象。耐心执行一遍基本都能看到问题出在哪一环。做网络排障时间久了我个人最大的体会是Docker 的高级网络不是学出来的是救火救出来的。隔离网络让我的服务部署从碰运气能通变成了设计上必然能通路由表知识让我在别人还在瞎改防火墙的时候直接定位到容器缺了哪条路由而对自定义网络驱动的理解则让我在需要和物理网络融合时不至于被 macvlan 和 ipvlan 的细节卡住。希望这篇实战记录也能帮你少走一段弯路。
返回列表