05-Docker网络深度解析
Docker网络深度解析
本文是Docker系列专栏的第五篇,将全面深入地解析Docker网络的核心原理、各种网络模式、跨主机通信、服务发现、安全与故障排查等全方位知识。文章面向有一定Docker基础的开发者和运维工程师,力求做到"知其然,更知其所以然"。
引言
在Docker的世界里,网络一直是最复杂、最容易让人困惑的话题之一。很多开发者能够熟练地使用docker run启动容器,也能够用-p参数做端口映射,但当面对容器间通信失败、跨主机网络不通、DNS解析异常、iptables规则冲突等问题时,往往束手无策。
究其原因,在于Docker网络并非一个简单的"配置项",而是建立在Linux内核网络栈之上的一套复杂体系。它涉及网络命名空间(Network Namespace)、虚拟以太网设备(veth pair)、网桥(bridge)、路由表(routing table)、iptables/netfilter、NAT(网络地址转换)、DNS解析、VXLAN隧道等众多底层技术。只有理解了这些底层机制,才能真正掌握Docker网络,在生产环境中做到游刃有余。
本文将从Linux网络基础讲起,逐步深入Docker的各种网络模式,剖析bridge网络的底层原理,讲解自定义网络管理、容器间通信、端口映射、overlay跨主机网络、macvlan/ipvlan物理网络直连、DNS与服务发现,最后覆盖网络安全与故障排查。每个知识点都配有大量代码示例和实战案例,力求让读者既能理解原理,又能动手实践。
无论你是正在学习Docker的开发者,还是需要在生产环境中部署容器化应用的运维工程师,相信本文都能为你提供系统而深入的知识参考。
第一章 Docker网络基础
1.1 容器网络的核心问题
要理解Docker网络,首先要搞清楚容器网络到底要解决什么问题。与虚拟机不同,容器并不是一个完整的操作系统,它共享宿主机的内核,只是通过Linux命名空间(Namespace)技术实现了进程、网络、文件系统等资源的隔离。因此,容器的网络本质上是"宿主机网络的一个隔离视图"。
容器网络需要解决三个核心问题:
问题一:IP地址分配
每个容器都需要一个IP地址才能进行网络通信。但是宿主机本身的IP地址是有限的,容器不可能直接使用宿主机的IP。因此,Docker需要为每个容器分配一个容器网络内部的IP地址,并确保同一网络内的容器IP不冲突。
# 查看容器的IP地址
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' my-container
# 输出示例: 172.17.0.2
Docker默认使用172.17.0.0/16这个子网为bridge网络中的容器分配IP。每个自定义网络也有自己的子网。Docker内部维护了一个IP地址分配器(IPAM),负责管理这些子网的地址池。
问题二:端口映射
容器有自己的IP地址(如172.17.0.2),但这个地址在外部网络中是不可路由的。外部主机无法直接访问172.17.0.2。因此,当需要从外部访问容器中运行的服务时,必须通过端口映射,将宿主机的某个端口映射到容器的端口上。
# 将宿主机的8080端口映射到容器的80端口
docker run -d -p 8080:80 nginx
# 此时访问宿主机的8080端口,流量会被转发到容器的80端口
curl http://localhost:8080
端口映射的底层实现依赖于iptables的DNAT(目标地址转换)规则,以及一个名为docker-proxy的用户态代理进程。
问题三:跨主机通信
当容器分布在多台宿主机上时,不同宿主机上的容器默认无法直接通信,因为它们的IP地址在各自的子网内,互相不可路由。要实现跨主机容器通信,需要使用overlay网络或其他跨主机网络方案,通过VXLAN隧道等技术将不同宿主机的容器网络打通。
宿主机A (192.168.1.10) 宿主机B (192.168.1.11)
├── 容器1 (172.18.0.2) ├── 容器3 (172.18.0.4)
└── 容器2 (172.18.0.3) └── 容器4 (172.18.0.5)
问题: 容器1如何与容器3通信?
答案: 通过overlay网络的VXLAN隧道
1.2 Linux网络基础回顾
Docker网络完全建立在Linux内核的网络功能之上。在深入Docker网络之前,有必要回顾几个关键的Linux网络技术。
1.2.1 网络命名空间(Network Namespace)
网络命名空间是Linux内核提供的一种网络隔离机制。每个网络命名空间拥有独立的网络栈,包括网卡、路由表、iptables规则、socket等。Docker容器正是通过网络命名空间实现了网络隔离。
# 查看当前网络命名空间
ip netns list
# 创建一个新的网络命名空间
ip netns add test-ns
# 在指定命名空间中执行命令
ip netns exec test-ns ip addr
# 删除网络命名空间
ip netns del test-ns
当Docker启动一个容器时,它会为该容器创建一个独立的网络命名空间。容器内的所有网络操作(绑定端口、查看路由表、配置iptables等)都发生在这个命名空间内,与宿主机相互隔离。
1.2.2 veth pair(虚拟以太网设备对)
veth(Virtual Ethernet)pair是一对虚拟网络设备,它们像一条"网线"一样连接在一起。从一端写入的数据会从另一端读出。Docker使用veth pair将容器的网络命名空间与宿主机的网桥连接起来。
┌──────────────┐ veth pair ┌──────────────┐
│ 容器命名空间 │◄──────────────────►│ 宿主机命名空间 │
│ eth0 │ vethxxxxxx │ docker0 │
└──────────────┘ └──────────────┘
# 创建veth pair
ip link add veth-host type veth peer name veth-container
# 将一端移入容器的网络命名空间
ip link set veth-container netns <容器PID的命名空间>
# 另一端连接到网桥
ip link set veth-host master docker0
1.2.3 bridge(网桥)
Linux网桥工作在数据链路层(二层),类似于物理交换机。它将多个网络接口连接在一起,使它们能够像在同一局域网中一样通信。Docker的docker0就是一个网桥,所有默认bridge网络的容器都连接到这个网桥上。
# 查看网桥
brctl show
# 输出示例:
# bridge name bridge id STP enabled interfaces
# docker0 8000.0242a8b5f2d3 no veth1234
# veth5678
# 创建网桥
ip link add name my-bridge type bridge
ip link set my-bridge up
# 将接口加入网桥
ip link set veth-host master my-bridge
# 为网桥配置IP地址(作为网关)
ip addr add 172.20.0.1/16 dev my-bridge
网桥维护着一个MAC地址转发表(FDB),记录了哪个MAC地址在哪个接口上。当网桥收到数据帧时,会根据目标MAC地址决定从哪个接口转发,这与真实交换机的工作原理完全一致。
1.2.4 iptables与netfilter
iptables是Linux内核netfilter框架的用户态工具,用于配置防火墙规则和网络地址转换(NAT)。Docker大量使用iptables规则来实现端口映射、流量转发和网络隔离。
iptables有多个表(table),每个表包含多个链(chain),每个链包含多条规则(rule):
| 表(Table) | 功能说明 | Docker的用途 |
|---|---|---|
| filter | 包过滤(接受/丢弃) | 网络隔离、访问控制 |
| nat | 网络地址转换 | 端口映射(DNAT)、出站伪装(MASQUERADE) |
| mangle | 包修改(TOS/TTL等) | 较少使用 |
| raw | 连接追踪豁免 | 较少使用 |
# 查看nat表的所有规则
iptables -t nat -L -n -v
# 查看filter表的所有规则
iptables -t filter -L -n -v
# Docker创建的典型NAT规则示例:
# DNAT规则: 将宿主机8080端口的流量转发到容器172.17.0.2:80
iptables -t nat -A DOCKER -p tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
# MASQUERADE规则: 容器出站流量伪装成宿主机IP
iptables -t nat -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
1.2.5 route(路由表)
路由表决定了数据包的转发路径。每个网络命名空间都有自己独立的路由表。容器内的路由表通常将默认网关指向docker0网桥的IP地址。
# 查看宿主机路由表
route -n
# 输出示例:
# Kernel IP routing table
# Destination Gateway Genmask Flags Metric Ref Use Iface
# 0.0.0.0 192.168.1.1 0.0.0.0 UG 100 0 0 eth0
# 172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
容器内部的路由表:
# 进入容器查看路由表
docker exec my-container route -n
# 输出示例:
# Kernel IP routing table
# Destination Gateway Genmask Flags Metric Ref Use Iface
# 0.0.0.0 172.17.0.1 0.0.0.0 UG 0 0 0 eth0
# 172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
可以看到,容器的默认网关是172.17.0.1,这正是docker0网桥的IP地址。容器发往外网的所有流量都会先发给docker0,再由宿主机进行转发。
路由决策的完整流程: 当容器内的应用程序发送一个网络数据包时,数据包会经过以下路由决策过程。首先,容器内核会检查数据包的目标IP地址,并在容器的路由表中查找匹配的路由项。如果目标IP在同一子网(如172.17.0.0/16)内,则直接通过ARP协议获取目标MAC地址,并通过二层网络发送数据帧。如果目标IP不在同一子网,则将数据包发送给默认网关(即docker0网桥的IP地址172.17.0.1),由网关负责后续的三层转发。
# 查看容器的ARP缓存
docker exec my-container arp -a
# 输出示例:
# ? (172.17.0.1) at 02:42:a8:b5:f2:d3 [ether] on eth0
# ? (172.17.0.3) at 02:42:ac:11:00:03 [ether] on eth0
# 查看容器的邻居表(与ARP等价)
docker exec my-container ip neigh show
# 172.17.0.1 dev eth0 lladdr 02:42:a8:b5:f2:d3 REACHABLE
# 172.17.0.3 dev eth0 lladdr 02:42:ac:11:00:03 REACHABLE
ARP缓存记录了IP地址与MAC地址的映射关系。当容器首次与某个IP通信时,会发送ARP广播请求,询问"谁拥有这个IP地址?",拥有该IP的设备会回复自己的MAC地址。在Docker的bridge网络中,这个ARP请求和回复都通过docker0网桥转发,整个过程与真实局域网中的ARP行为完全一致。
Linux网络组件协同工作: 理解Docker网络的关键在于理解上述Linux网络组件如何协同工作。网络命名空间提供了隔离的环境,veth pair提供了跨命名空间的数据通道,网桥提供了同一网络内的二层交换功能,路由表决定了数据包的三层转发路径,iptables/netfilter则在数据包流经的关键节点上进行过滤和地址转换。这五个组件共同构成了Docker容器网络的基石。Docker所做的,本质上就是通过自动化脚本编排这些Linux内核网络功能,为每个容器创建一个隔离的、可配置的、可通信的网络环境。
1.3 Docker网络架构总览
Docker的网络架构可以分为三层:
┌─────────────────────────────────────────────────────────┐
│ Docker CLI / API │
│ (docker network create/ls/rm/...) │
├─────────────────────────────────────────────────────────┤
│ Docker Daemon (network) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ CNM │ │ libnetwork │ │ IPAM │ │ Driver │ │
│ │ 模型 │ │ 核心库 │ │ 地址管理 │ │ 接口层 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────────────┤
│ Network Drivers │
│ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │
│ │bridge │ │ host │ │ none │ │overlay│ │macvlan│ │
│ └───────┘ └───────┘ └───────┘ └───────┘ └───────┘ │
├─────────────────────────────────────────────────────────┤
│ Linux Kernel Network Stack │
│ (netfilter/iptables, veth, bridge, vxlan, ...) │
└─────────────────────────────────────────────────────────┘
CNM(Container Network Model) 是Docker定义的容器网络模型,它包含三个核心概念:
-
Sandbox(沙箱): 对应一个容器的网络栈,包括网络命名空间、路由表、iptables规则等。一个Sandbox可以连接多个Endpoint。
-
Endpoint(端点): 对应一个veth pair,连接Sandbox与Network。一个Endpoint属于一个Sandbox和一个Network。
-
Network(网络): 一组可以互相通信的Endpoint的集合,对应一个网桥或overlay网络。
Sandbox (容器A的网络栈)
┌───┐
│ E1│──── Network1 (bridge: my-net)
│ E2│──── Network2 (bridge: my-net2)
└───┘
Sandbox (容器B的网络栈)
┌───┐
│ E3│──── Network1 (bridge: my-net)
└───┘
容器A和容器B都连接到Network1,因此可以互相通信
容器A还连接到Network2,可以与Network2中的容器通信
libnetwork 是CNM的参考实现,它是Docker Daemon中的一个库,负责网络的创建、管理和销毁。libnetwork不仅实现了CNM规范定义的接口,还提供了网络生命周期管理、服务发现(内置DNS)、容器连接与断开等核心功能。当用户执行docker network create命令时,Docker CLI通过REST API将请求发送给Docker Daemon,Daemon调用libnetwork的相应方法,libnetwork再根据指定的网络驱动(driver)将具体操作委托给对应的驱动实现。这种分层架构使得Docker可以灵活地支持多种网络方案,而不需要修改上层代码。
IPAM(IP Address Management) 是IP地址管理子系统,负责为网络和容器分配IP地址、子网、网关等。Docker内置了一个默认的IPAM驱动,也支持使用外部IPAM驱动(如Infoblox)。默认IPAM驱动的工作原理是:当创建网络时,如果不指定子网,IPAM会自动从Docker预留的私有地址段中选择一个未使用的子网分配给该网络。Docker默认使用的私有地址段包括172.17.0.0/16(默认bridge)、172.18.0.0/16到172.31.0.0/16(自定义bridge)以及192.168.0.0/16等。当容器启动并连接到网络时,IPAM从该网络的地址池中分配一个未使用的IP地址给容器。当容器停止或断开网络时,IPAM回收该IP地址,使其可以再次被分配。这种自动化的地址管理机制大大简化了容器网络的运维工作,但在大规模部署时也可能遇到地址耗尽的问题,因此合理规划子网范围非常重要。
# 查看Docker使用的IPAM驱动信息
docker network inspect bridge | python3 -c "
import json, sys
data = json.load(sys.stdin)
ipam = data[0]['IPAM']
print('IPAM Driver:', ipam['Driver'])
print('Subnet:', ipam['Config'][0]['Subnet'])
print('Gateway:', ipam['Config'][0]['Gateway'])
"
# 输出:
# IPAM Driver: default
# Subnet: 172.17.0.0/16
# Gateway: 172.17.0.1
# 查看所有网络的子网分配情况
docker network ls --format '{{.Name}}' | while read net; do
subnet=$(docker network inspect "$net" -f '{{range .IPAM.Config}}{{.Subnet}}{{end}}')
echo "$net: $subnet"
done
1.4 Docker网络驱动(Network Driver)体系
Docker通过网络驱动(Network Driver)来支持不同的网络模式。每种驱动实现了不同的网络拓扑和通信方式:
| 驱动名称 | 网络模式 | 是否跨主机 | 适用场景 |
|---|---|---|---|
| bridge | bridge | 否(单机) | 默认模式,开发测试,单机多容器 |
| host | host | 否(单机) | 需要最高网络性能,无隔离需求 |
| null | none | 否(单机) | 无网络需求,或自定义网络栈 |
| overlay | overlay | 是 | Docker Swarm集群,跨主机容器通信 |
| macvlan | macvlan | 是(需物理网络支持) | 容器需要直接出现在物理网络中 |
| ipvlan | ipvlan | 是(需物理网络支持) | MAC地址受限环境,L3路由模式 |
| remote | - | - | 第三方网络插件(Calico/Flannel等) |
# 查看Docker支持的网络驱动
docker info | grep -i network
# 查看当前所有网络
docker network ls
# 输出示例:
# NETWORK ID NAME DRIVER SCOPE
# a1b2c3d4e5f6 bridge bridge local
# b2c3d4e5f6a1 host host local
# c3d4e5f6a1b2 none null local
其中SCOPE字段表示网络的作用范围:
local: 单机网络,仅在本机生效swarm: Swarm集群范围网络global: 全局网络
1.5 容器网络与宿主机网络的关系
理解容器网络与宿主机网络的关系,是掌握Docker网络的关键。让我们通过一个实际的例子来梳理:
当执行docker run -d --name web nginx时,Docker在网络层面做了以下事情:
-
创建网络命名空间: Docker为容器创建一个独立的网络命名空间。
-
创建veth pair: Docker创建一对veth设备,一端放在容器的命名空间内(命名为
eth0),另一端留在宿主机(命名为vethXXXXXX)。 -
连接到网桥: 宿主机端的veth设备被加入到
docker0网桥。 -
分配IP地址: Docker的IPAM为容器的
eth0分配一个IP地址(如172.17.0.2),并设置子网掩码。 -
配置路由: 容器内的路由表被设置,默认网关指向docker0的IP(
172.17.0.1)。 -
配置iptables: 宿主机上添加MASQUERADE规则,使容器可以通过宿主机的IP访问外部网络。
# 查看容器的网络命名空间
# Docker默认不会在/var/run/netns下创建命名空间文件
# 可以通过以下方式查看容器的网络命名空间
docker inspect -f '{{.State.Pid}}' web
# 输出: 12345
# 通过PID查看网络命名空间
ls -l /proc/12345/ns/net
# 输出: net:[4026532193]
# 进入容器的网络命名空间执行命令
nsenter -t 12345 -n ip addr
# 输出:
# 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 ...
# 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
# link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
# inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
在宿主机上查看docker0网桥和veth设备:
# 查看docker0网桥
ip addr show docker0
# 输出:
# 3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
# link/ether 02:42:a8:b5:f2:d3 brd ff:ff:ff:ff:ff:ff
# inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
# valid_lft forever preferred_lft forever
# 查看网桥上的接口
brctl show docker0
# 输出:
# bridge name bridge id STP enabled interfaces
# docker0 8000.0242a8b5f2d3 no veth1234abcd
# 查看veth设备
ip link show
# 可以看到一个veth设备,它与容器内的eth0是一对
# 5: veth1234abcd@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
总结一下容器网络与宿主机网络的对应关系:
| 组件 | 宿主机视角 | 容器视角 |
|---|---|---|
| 网络命名空间 | 宿主机默认命名空间 | 容器独立命名空间 |
| 网络接口 | docker0网桥 + veth设备 | eth0 |
| IP地址 | docker0: 172.17.0.1 | eth0: 172.17.0.2 |
| 路由表 | 含docker0网段路由 | 默认网关指向172.17.0.1 |
| iptables | NAT/MASQUERADE规则 | 通常为空 |
| DNS | /etc/resolv.conf | 127.0.0.11(内置DNS) |
这种"宿主机网桥 + veth pair + NAT"的架构,是Docker bridge网络的本质。理解了这一点,后面所有的网络模式都是在这个基础上的变体。
第二章 Docker网络模式详解
Docker提供了多种网络模式,每种模式都有不同的网络拓扑、隔离程度和性能特征。本章将逐一详细讲解每种网络模式的原理、配置方法和适用场景。
2.1 bridge模式(默认模式)
bridge模式是Docker的默认网络模式。当执行docker run时如果不指定--network参数,容器就会使用bridge模式。
在bridge模式下,Docker为容器创建独立的网络命名空间,并通过veth pair将容器连接到docker0网桥上。容器获得一个docker0子网内的IP地址,通过NAT方式访问外部网络。
# 默认使用bridge模式 (不指定--network参数)
docker run -d --name nginx-bridge nginx
# 显式指定bridge模式
docker run -d --name nginx-bridge2 --network bridge nginx
# 查看容器使用的网络
docker inspect -f '{{json .NetworkSettings.Networks}}' nginx-bridge | python3 -m json.tool
# 输出示例:
# {
# "bridge": {
# "IPAMConfig": null,
# "Links": null,
# "Aliases": null,
# "NetworkID": "a1b2c3d4...",
# "EndpointID": "e5f6a7b8...",
# "Gateway": "172.17.0.1",
# "IPAddress": "172.17.0.2",
# "IPPrefixLen": 16,
# "IPv6Gateway": "",
# "GlobalIPv6Address": "",
# "GlobalIPv6PrefixLen": 0,
# "MacAddress": "02:42:ac:11:00:02",
# "DriverOpts": null
# }
# }
bridge模式的特点:
- 每个容器有独立的网络命名空间和IP地址
- 容器之间通过docker0网桥进行二层通信
- 容器通过NAT(MASQUERADE)访问外部网络
- 需要通过
-p参数进行端口映射才能从外部访问容器 - 默认bridge网络不支持通过容器名进行DNS解析(这是与自定义bridge网络的重要区别)
默认bridge网络的局限: 默认的docker0 bridge网络不支持自动DNS解析。也就是说,在默认bridge网络中,容器之间只能通过IP地址通信,不能通过容器名通信。
# 在默认bridge网络中创建两个容器
docker run -d --name container-a alpine sleep 3600
docker run -d --name container-b alpine sleep 3600
# 尝试通过容器名ping (会失败!)
docker exec container-a ping -c 2 container-b
# ping: bad address 'container-b'
# 只能通过IP地址ping (成功)
docker exec container-a ping -c 2 172.17.0.3
# PING 172.17.0.3 (172.17.0.3): 56 data bytes
# 64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.123 ms
这个局限是促使我们使用自定义bridge网络的主要原因之一(详见第三章和第四章)。
2.2 host模式
host模式下,容器不创建独立的网络命名空间,而是直接使用宿主机的网络命名空间。容器没有自己的IP地址,而是共享宿主机的IP地址和网络栈。
# 使用host模式启动容器
docker run -d --name nginx-host --network host nginx
# 此时nginx直接监听在宿主机的80端口
# 不需要-p参数做端口映射
curl http://localhost:80
# 在容器内查看网络信息,与宿主机完全一致
docker exec nginx-host ip addr
# 输出与宿主机的ip addr输出完全相同
host模式的特点:
- 最高网络性能: 没有veth pair、没有NAT、没有网桥,网络延迟最低,吞吐量最高
- 无网络隔离: 容器与宿主机共享网络栈,端口可能冲突
- 无需端口映射: 容器直接绑定宿主机端口
- 不支持端口映射参数: 使用
-p参数会被忽略
# host模式下使用-p参数会被忽略并给出警告
docker run -d --name nginx-host-warn --network host -p 8080:80 nginx
# 输出警告: WARNING: Published ports are discarded when using host network mode.
host模式的适用场景:
- 网络性能要求极高的应用(如高频交易、实时流媒体)
- 需要直接操作宿主机网络的应用(如网络监控工具)
- 不需要网络隔离的场景
注意事项: host模式在Docker Desktop(Mac/Windows)上的行为与Linux不同。在Mac/Windows上,容器运行在一个轻量级虚拟机中,host模式共享的是虚拟机的网络,而非宿主机的网络。
2.3 none模式
none模式下,容器拥有独立的网络命名空间,但不配置任何网络接口(只有loopback接口)。容器完全处于网络隔离状态。
# 使用none模式启动容器
docker run -d --name isolated --network none alpine sleep 3600
# 查看容器的网络接口
docker exec isolated ip addr
# 输出:
# 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
# link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# inet 127.0.0.1/8 scope host lo
# valid_lft forever preferred_lft forever
# 只有lo接口,没有eth0,无法与外界通信
docker exec isolated ping -c 1 8.8.8.8
# ping: connect: Network is unreachable
none模式的适用场景:
- 安全要求极高的应用,完全不需要网络
- 需要自定义网络栈的应用(在容器内手动配置网络)
- 批处理任务,只读取本地文件不需要网络
# 实战: 在none模式容器中手动配置网络
# 1. 获取容器PID
PID=$(docker inspect -f '{{.State.Pid}}' isolated)
# 2. 创建veth pair
ip link add veth-test type veth peer name veth-peer
# 3. 将一端移入容器命名空间
ip link set veth-peer netns $PID
# 4. 在容器内配置IP
nsenter -t $PID -n ip link set veth-peer name eth0
nsenter -t $PID -n ip link set eth0 up
nsenter -t $PID -n ip addr add 192.168.100.10/24 dev eth0
nsenter -t $PID -n ip route add default via 192.168.100.1
# 5. 现在容器可以通信了
docker exec isolated ip addr
# 可以看到eth0接口已经配置好
none模式的深度价值: none模式虽然在日常使用中较少直接使用,但它在特定场景下具有不可替代的价值。首先,none模式提供了最高级别的网络隔离——容器完全无法与外界通信,这对于处理高度敏感数据的批处理任务非常重要,例如加密货币钱包的密钥生成、敏感文件的离线处理等。其次,none模式为网络研究和测试提供了干净的起点——网络工程师可以在none模式容器中从零开始构建网络栈,测试各种网络配置和协议,而不受Docker默认网络配置的干扰。最后,none模式也是一些特殊容器化应用的基础,例如网络工具容器需要在容器内安装和配置特定的网络工具(如OpenVPN、WireGuard等),然后通过手动配置网络接口来建立VPN隧道。
# 实战: 使用none模式容器构建VPN客户端
docker run -d --name vpn-client --network none --privileged \
--device=/dev/net/tun \
-v /etc/openvpn:/etc/openvpn:ro \
openvpn-client
# 手动配置VPN网络
PID=$(docker inspect -f '{{.State.Pid}}' vpn-client)
# 创建TUN设备
nsenter -t $PID -n ip tuntap add dev tun0 mode tun
# 配置VPN接口
nsenter -t $PID -n ip link set tun0 up
nsenter -t $PID -n ip addr add 10.8.0.2/24 dev tun0
# 添加路由(通过VPN网关)
nsenter -t $PID -n ip route add default via 10.8.0.1
# 验证VPN网络
docker exec vpn-client ip addr show tun0
2.4 container模式
container模式让一个容器共享另一个容器的网络命名空间。两个容器共享相同的网络栈,包括IP地址、端口、路由表等。
# 先创建一个bridge模式的容器
docker run -d --name web-server -p 8080:80 nginx
# 再创建一个容器,共享web-server的网络命名空间
docker run -d --name sidecar --network container:web-server alpine sleep 3600
# sidecar容器的IP地址与web-server相同
docker exec sidecar ip addr
# 可以看到与web-server相同的网络接口和IP地址
# sidecar容器内可以直接访问localhost:80
docker exec sidecar wget -qO- http://localhost:80
# 返回nginx的欢迎页面内容
container模式的工作原理:
┌─────────────────────────────────────────┐
│ 共享的网络命名空间 │
│ ┌─────────────────────────────────┐ │
│ │ eth0: 172.17.0.2 │ │
│ │ 路由表、iptables、端口等 │ │
│ └─────────────────────────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ ┌────────┴───────┐ ┌────────┴───────┐ │
│ │ web-server │ │ sidecar │ │
│ │ (nginx进程) │ │ (sleep进程) │ │
│ │ 独立的文件系统 │ │ 独立的文件系统 │ │
│ │ 独立的进程空间 │ │ 独立的进程空间 │ │
│ └────────────────┘ └────────────────┘ │
└─────────────────────────────────────────┘
container模式的适用场景:
- Sidecar模式: 一个主容器运行应用,一个辅助容器运行日志收集、监控代理等
- 调试: 在不改变目标容器网络配置的情况下,从另一个容器调试网络问题
- 网络代理: 多个容器共享同一网络栈,避免端口冲突
# 实战: Sidecar模式 - 日志收集
# 主容器: nginx Web服务器
docker run -d --name web-app -p 8080:80 \
-v /var/log/nginx:/var/log/nginx \
nginx
# Sidecar容器: 共享网络,收集日志
docker run -d --name log-collector \
--network container:web-app \
-v /var/log/nginx:/var/log/nginx \
busybox sh -c "tail -f /var/log/nginx/access.log"
# 注意: 共享网络但文件系统是独立的
# 需要通过volume共享日志文件
重要注意事项:
- 共享网络的容器不能绑定相同的端口,否则会冲突
- 如果被共享的容器停止,共享其网络的容器也会失去网络
--network container:xxx中的xxx必须是正在运行的容器名或ID
2.5 overlay模式
overlay模式用于跨主机容器通信。它通过VXLAN(Virtual Extensible LAN)隧道技术,在不同宿主机的容器之间建立虚拟的二层网络。
overlay网络的详细原理和使用方法将在第七章专门讲解,这里先做一个概览:
# 前提: 需要Docker Swarm集群
# 初始化Swarm
docker swarm init
# 创建overlay网络
docker network create -d overlay my-overlay-net
# 在不同节点上启动容器,使用同一个overlay网络
docker run -d --name service-a --network my-overlay-net nginx
docker run -d --name service-b --network my-overlay-net nginx
# 即使在不同宿主机上,容器之间也可以通过容器名通信
docker exec service-a ping -c 2 service-b
2.6 macvlan模式
macvlan模式允许为容器分配物理网络中的MAC地址,使容器在物理网络上看起来就像一台独立的主机。容器直接获得物理网络的IP地址,流量不经过NAT。
# 查看宿主机的物理网卡
ip link show
# 假设物理网卡为eth0
# 创建macvlan网络
docker network create -d macvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
-o parent=eth0 \
my-macvlan-net
# 启动使用macvlan网络的容器
docker run -d --name macvlan-container \
--network my-macvlan-net \
--ip 192.168.1.100 \
alpine sleep 3600
# 容器直接获得物理网络IP,其他主机可以直接访问
ping 192.168.1.100
macvlan的详细原理和配置将在第八章讲解。
2.7 ipvlan模式
ipvlan与macvlan类似,但容器不分配独立的MAC地址,而是共享父接口的MAC地址。ipvlan有两种模式:L2模式和L3模式。
# 创建ipvlan L2网络
docker network create -d ipvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
-o parent=eth0 \
-o ipvlan_mode=l2 \
my-ipvlan-l2
# 创建ipvlan L3网络
docker network create -d ipvlan \
--subnet=10.0.0.0/24 \
-o parent=eth0 \
-o ipvlan_mode=l3 \
my-ipvlan-l3
ipvlan的详细原理和与macvlan的对比将在第八章讲解。
2.8 各网络模式对比表
| 特性 | bridge | host | none | container | overlay | macvlan | ipvlan |
|---|---|---|---|---|---|---|---|
| 网络隔离 | 是 | 否 | 完全隔离 | 与目标容器共享 | 是 | 是 | 是 |
| 独立IP | 是 | 共享宿主机 | 无 | 共享目标容器 | 是 | 是(物理网络) | 是(物理网络) |
| 端口映射 | 需要 | 不需要 | 无 | 不需要 | 不需要 | 不需要 | 不需要 |
| 跨主机通信 | 否 | 否 | 否 | 否 | 是 | 是(需路由) | 是(需路由) |
| DNS解析 | 自定义网络支持 | - | - | - | 支持 | 否 | 否 |
| 性能 | 中等 | 最高 | - | 中等 | 较低 | 高 | 高 |
| 复杂度 | 低 | 最低 | 最低 | 低 | 高 | 中 | 中 |
| MAC地址 | 容器独有 | 宿主机 | 无 | 宿主机 | 容器独有 | 容器独有 | 共享父接口 |
| 适用场景 | 默认/开发 | 高性能 | 安全隔离 | Sidecar | 集群 | 物理网络 | MAC受限 |
2.9 网络模式选择指南
根据不同的使用场景,推荐的网络模式选择如下:
是否需要跨主机通信?
├── 是
│ ├── 需要容器直接出现在物理网络中?
│ │ ├── 是 → macvlan 或 ipvlan
│ │ └── 否 → overlay (推荐,特别是Swarm集群)
│ └── 使用Kubernetes? → 使用CNI插件(Calico/Flannel等)
└── 否
├── 需要最高网络性能?
│ ├── 是 → host (但失去网络隔离)
│ └── 否 → 继续判断
├── 需要完全无网络?
│ └── 是 → none
├── 需要共享其他容器的网络?
│ └── 是 → container:<容器名>
└── 默认选择 → bridge (推荐自定义bridge而非默认docker0)
生产环境建议:
- 单机多容器: 使用自定义bridge网络(非默认docker0),获得DNS解析能力
- Swarm集群: 使用overlay网络,获得跨主机通信和服务发现能力
- 高性能需求: 评估host模式或macvlan模式
- 物理网络集成: 使用macvlan或ipvlan,但注意网络配置复杂性
- Kubernetes: 不使用Docker原生网络,改用CNI插件```
网络模式选择的深入考量:
在实际的生产环境中,选择网络模式不能仅仅看功能是否满足,还需要综合考虑性能、安全、可维护性和团队技术栈等多个维度。
bridge模式的选择考量: bridge模式是Docker的默认网络模式,也是绝大多数开发测试场景的首选。自定义bridge网络提供了容器间DNS解析、网络隔离、固定IP分配等关键功能,足以满足大多数单机容器化应用的需求。选择bridge模式时,需要注意合理规划子网范围,避免不同网络之间的子网冲突。同时,对于需要暴露给外部访问的服务,要仔细规划端口映射策略,避免端口冲突和不必要的安全暴露。
host模式的选择考量: host模式虽然提供了最高的网络性能,但由于完全失去了网络隔离,在生产环境中的使用需要格外谨慎。host模式适合那些对网络延迟极度敏感的应用,例如高频交易系统、实时流媒体处理、网络性能测试工具等。使用host模式时,要特别注意端口冲突问题——如果宿主机上已经运行了占用相同端口的服务,容器将无法启动。此外,host模式下的容器可以直接访问宿主机上的所有网络服务,这在安全上是一个潜在的隐患。
overlay模式的选择考量: overlay模式是Docker Swarm集群中跨主机容器通信的标准方案。它通过VXLAN隧道封装实现跨主机二层网络,虽然会带来一定的性能开销(约10%-15%的吞吐量降低),但提供了透明的跨主机通信和自动服务发现。如果团队使用Docker Swarm进行容器编排,overlay模式是自然的选择。如果使用Kubernetes,则应该使用CNI插件(如Calico、Flannel、Cilium)而不是Docker原生的overlay网络。
macvlan/ipvlan模式的选择考量: 这两种模式适用于容器需要直接出现在物理网络中的特殊场景,例如遗留系统的迁移、需要被物理网络设备直接访问的容器、网络监控和测试等。macvlan由于为每个容器分配独立的MAC地址,可能会遇到交换机MAC地址表限制或网络安全策略限制的问题。ipvlan通过共享父接口的MAC地址避免了这个问题,但配置和理解上更为复杂。在选择这两种模式时,需要与网络团队密切协作,确保物理网络配置(如交换机端口模式、VLAN配置、MAC地址安全策略等)与容器网络方案兼容。
第三章 bridge网络深度剖析
bridge网络是Docker最核心、最常用的网络模式。本章将深入剖析bridge网络的每一个细节,从docker0网桥的工作原理到iptables NAT规则的逐条分析,让你彻底理解bridge网络的底层机制。
3.1 docker0网桥的工作原理
docker0是Docker安装时自动创建的Linux网桥,它是默认bridge网络的核心。所有使用默认bridge网络的容器都通过veth pair连接到docker0上。
# 查看docker0网桥的详细信息
ip addr show docker0
# 输出示例:
# 3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
# link/ether 02:42:8f:3a:b2:1c brd ff:ff:ff:ff:ff:ff
# inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
# valid_lft forever preferred_lft forever
# inet6 fe80::42:8fff:fe3a:b21c/64 scope link
# valid_lft forever preferred_lft forever
docker0的关键属性:
- IP地址:
172.17.0.1/16,这是默认bridge网络的网关地址 - MAC地址: 自动生成,格式为
02:42:xx:xx:xx:xx(Docker使用的MAC地址前缀) - MTU: 默认1500,可通过
--mtu参数修改 - STP(Spanning Tree Protocol): 默认关闭
docker0的工作流程:
┌──────────────────────────────────┐
│ docker0 网桥 │
│ (172.17.0.1/16) │
│ MAC转发表(FDB): │
│ 02:42:ac:11:00:02 → veth1 │
│ 02:42:ac:11:00:03 → veth2 │
└──────┬───────────┬───────────────┘
│ │
┌──────┴──┐ ┌───┴───────┐
│ veth1 │ │ veth2 │
└──────┬──┘ └───┬───────┘
│ │
┌──────┴──┐ ┌───┴───────┐
│ 容器A │ │ 容器B │
│eth0 │ │ eth0 │
│172.17 │ │ 172.17 │
│.0.2 │ │ .0.3 │
└─────────┘ └───────────┘
当容器A(172.17.0.2)向容器B(172.17.0.3)发送数据时:
- 容器A通过ARP协议获取容器B的MAC地址
- 容器A将数据帧发送到eth0
- 数据帧通过veth pair到达宿主机的veth1
- veth1连接在docker0网桥上,docker0根据目标MAC地址查找FDB表
- docker0将数据帧从veth2转发出去
- 数据帧通过veth pair到达容器B的eth0
# 查看docker0的MAC转发表(FDB)
bridge fdb show dev docker0
# 输出示例:
# 02:42:ac:11:00:02 dev docker0 master docker0
# 02:42:ac:11:00:03 dev docker0 master docker0
# 33:33:00:00:00:01 dev docker0 master docker0 (组播地址)
# 查看docker0的ARP表
ip neigh show dev docker0
# 输出示例:
# 172.17.0.2 lladdr 02:42:ac:11:00:02 REACHABLE
# 172.17.0.3 lladdr 02:42:ac:11:00:03 REACHABLE
网桥转发的底层细节: docker0网桥的转发过程虽然看似简单,但涉及多个Linux内核网络子系统的协同工作。当数据帧从veth1到达docker0时,首先由网桥的接收函数(br_handle_frame)处理。网桥会检查数据帧的目标MAC地址,并在FDB(Forwarding Database,转发数据库)中查找该MAC地址对应的端口。如果找到了匹配项,数据帧只从对应的端口(veth2)转发出去,这称为单播转发。如果没有找到匹配项,或者目标MAC是广播地址(FF:FF:FF:FF:FF:FF)或多播地址,数据帧会从所有端口转发出去,这称为泛洪(flooding)。
在Docker环境中,ARP请求是一个典型的广播帧。当容器A首次需要与容器B通信时,它不知道容器B的MAC地址,因此会发送一个ARP广播请求。这个广播帧通过veth pair到达docker0网桥,docker0将其泛洪到所有连接的端口。所有容器都会收到这个ARP请求,但只有IP地址匹配的容器B会回复ARP响应。ARP响应是单播帧,直接发送回容器A。此后,容器A的ARP缓存中就有了容器B的MAC地址,后续的通信可以直接通过单播转发,不再需要ARP请求。
# 查看网桥的STP(生成树协议)状态
# STP用于防止网络环路,但Docker默认关闭STP
bridge link show
# 可以看到STP状态为disabled
# 查看网桥的VLAN过滤配置
bridge vlan show
# Docker默认不使用VLAN过滤
# 手动操作FDB表
# 添加静态FDB条目
bridge fdb add 02:42:ac:11:00:05 dev veth3 master docker0
# 删除FDB条目
bridge fdb del 02:42:ac:11:00:05 dev veth3 master docker0
# 查看网桥的统计信息
ip -s link show docker0
# 可以看到接收和发送的数据包数量、字节数、错误数等
# 监控网桥的转发行为
# 使用bridge monitor命令实时监控FDB变化
bridge monitor fdb
# 当容器启动或停止时,可以看到FDB条目的添加和删除
3.2 容器如何获取IP地址
Docker容器获取IP地址的方式与传统的DHCP不同。Docker使用了内置的IPAM(IP Address Management)驱动来分配IP地址,这是一个用户态的地址分配器,不依赖DHCP协议。
IPAM的工作流程:
- Docker安装时,为默认bridge网络(docker0)分配子网
172.17.0.0/16 - 每次创建新网络时,Docker从可用的子网池中选择一个未使用的子网
- 容器启动时,IPAM从网络对应的子网中分配一个未使用的IP地址
- 容器停止时,IP地址被回收到地址池
# 查看默认bridge网络的IPAM配置
docker network inspect bridge | python3 -m json.tool
# 输出示例:
# {
# "Name": "bridge",
# "Driver": "bridge",
# "IPAM": {
# "Driver": "default",
# "Options": null,
# "Config": [
# {
# "Subnet": "172.17.0.0/16",
# "Gateway": "172.17.0.1"
# }
# ]
# }
# }
子网分配策略: Docker按照以下顺序选择子网:
- 用户创建网络时指定的子网(
--subnet) - 从Docker配置的子网池中选择
- 从
172.17.0.0/16之后依次选择:172.18.0.0/16,172.19.0.0/16, …
# 查看Docker的子网池配置
cat /etc/docker/daemon.json
# 可能的配置示例:
# {
# "bip": "172.17.0.1/16", # docker0的IP
# "default-address-pools": [
# {"base": "172.20.0.0/16", "size": 24}
# ]
# }
# bip (Bridge IP): 设置docker0网桥的IP地址和子网
# default-address-pools: 设置自定义网络使用的子网池
IP分配顺序: IPAM从子网的第二个地址开始分配(第一个是网关)。例如对于172.17.0.0/16:
172.17.0.1: 网关(docker0)172.17.0.2: 第一个容器172.17.0.3: 第二个容器- …
# 验证IP分配顺序
docker run -d --name c1 alpine sleep 3600
docker run -d --name c2 alpine sleep 3600
docker run -d --name c3 alpine sleep 3600
docker inspect -f '{{.Name}}: {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' c1 c2 c3
# /c1: 172.17.0.2
# /c2: 172.17.0.3
# /c3: 172.17.0.4
# 停止并删除c2,再启动新容器,会复用172.17.0.3
docker rm -f c2
docker run -d --name c4 alpine sleep 3600
docker inspect -f '{{.IPAddress}}' c4
# 172.17.0.3
3.3 容器与宿主机通信路径
容器与宿主机之间的通信路径相对简单,因为docker0网桥就在宿主机的网络命名空间中。
容器访问宿主机:
容器(172.17.0.2) → eth0 → veth pair → docker0(172.17.0.1) → 宿主机
容器可以直接访问docker0的IP地址(172.17.0.1),这就是访问宿主机:
# 在容器内访问宿主机
docker exec c1 ping -c 2 172.17.0.1
# 输出:
# PING 172.17.0.1 (172.17.0.1): 56 data bytes
# 64 bytes from 172.17.0.1: seq=0 ttl=64 time=0.085 ms
# 64 bytes from 172.17.0.1: seq=1 ttl=64 time=0.063 ms
# 在宿主机上启动一个服务,容器可以通过docker0的IP访问
# 例如在宿主机上启动一个HTTP服务
python3 -m http.server 8888 &
# 容器内访问
docker exec c1 wget -qO- http://172.17.0.1:8888
宿主机访问容器:
宿主机可以直接通过容器的IP地址访问容器,不需要端口映射:
# 宿主机直接访问容器IP
# 先在容器内启动一个服务
docker run -d --name web-test nginx
# 获取容器IP
CIP=$(docker inspect -f '{{.IPAddress}}' web-test)
echo $CIP # 172.17.0.5
# 宿主机直接访问容器
curl http://$CIP
# 返回nginx欢迎页面
宿主机之所以能直接访问容器IP,是因为docker0网桥在宿主机网络命名空间中,宿主机内核知道如何路由到172.17.0.0/16子网:
# 查看宿主机路由表
route -n | grep docker
# 172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
3.4 容器与容器通信路径(同一bridge)
同一bridge网络中的容器之间的通信通过docker0网桥进行二层转发。这个过程不涉及NAT,是纯二层通信。
通信流程详解:
假设容器A(172.17.0.2)要ping容器B(172.17.0.3):
步骤1: 容器A查看路由表,发现172.17.0.3在直连子网172.17.0.0/16中
步骤2: 容器A发送ARP请求: "谁是172.17.0.3?"
步骤3: docker0网桥将ARP请求广播到所有端口
步骤4: 容器B收到ARP请求,回复自己的MAC地址
步骤5: 容器A构造ICMP数据帧,目标MAC为容器B的MAC
步骤6: 数据帧从容器A的eth0 → veth pair → vethA → docker0
步骤7: docker0查FDB表,发现目标MAC在vethB端口上
步骤8: docker0将数据帧从vethB转发 → veth pair → 容器B的eth0
步骤9: 容器B收到ICMP请求,回复ICMP响应(反向路径相同)
# 验证容器间通信
# 创建两个容器
docker run -d --name c1 alpine sleep 3600
docker run -d --name c2 alpine sleep 3600
# 获取IP
C1_IP=$(docker inspect -f '{{.IPAddress}}' c1)
C2_IP=$(docker inspect -f '{{.IPAddress}}' c2)
# 从c1 ping c2
docker exec c1 ping -c 3 $C2_IP
# 输出:
# PING 172.17.0.3 (172.17.0.3): 56 data bytes
# 64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.098 ms
# 64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.055 ms
# 64 bytes from 172.17.0.3: seq=2 ttl=64 time=0.052 ms
# 使用tcpdump在docker0上抓包,观察通信过程
# 在另一个终端运行:
tcpdump -i docker0 -n icmp
# 然后在容器间ping,可以看到抓包输出:
# 10:00:01.123456 IP 172.17.0.2 > 172.17.0.3: ICMP echo request
# 10:00:01.123489 IP 172.17.0.3 > 172.17.0.2: ICMP echo reply
同一bridge中容器间通信不经过NAT: 这一点非常重要。iptables的FORWARD链中有规则允许docker0网桥上的容器间通信:
# 查看iptables filter表FORWARD链中Docker相关的规则
iptables -t filter -S FORWARD | grep docker
# 输出示例:
# -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# -A FORWARD -o docker0 -j DOCKER
# -A FORWARD -i docker0 ! -o docker0 -j ACCEPT
# -A FORWARD -i docker0 -o docker0 -j ACCEPT
# 关键规则: -A FORWARD -i docker0 -o docker0 -j ACCEPT
# 这条规则允许从docker0进来、从docker0出去的流量(即容器间通信)
3.5 容器与外部网络通信路径(iptables MASQUERADE)
容器访问外部网络(如互联网)时,需要经过NAT(网络地址转换)。因为容器的IP地址(如172.17.0.2)是私有地址,在外部网络中不可路由。Docker使用iptables的MASQUERADE规则将容器的源IP替换为宿主机的IP。
通信流程详解:
假设容器A(172.17.0.2)要访问外部服务器(8.8.8.8):
容器A(172.17.0.2) → eth0 → veth pair → docker0(172.17.0.1)
→ 宿主机路由表 → eth0(宿主机物理网卡)
→ iptables MASQUERADE (SNAT: 172.17.0.2 → 192.168.1.10)
→ 外部网络 → 8.8.8.8
# 查看MASQUERADE规则
iptables -t nat -S POSTROUTING | grep docker
# 输出示例:
# -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
# 这条规则的含义:
# -s 172.17.0.0/16: 源地址来自docker0子网
# ! -o docker0: 且不是从docker0接口出去的(即目标是外部网络)
# -j MASQUERADE: 执行SNAT,将源IP替换为出口网卡的IP
验证容器访问外部网络:
# 容器内访问外部网络
docker exec c1 ping -c 2 8.8.8.8
# 输出:
# PING 8.8.8.8 (8.8.8.8): 56 data bytes
# 64 bytes from 8.8.8.8: seq=0 ttl=117 time=15.123 ms
# 64 bytes from 8.8.8.8: seq=1 ttl=117 time=14.987 ms
# 在宿主机eth0上抓包,可以看到源IP已被替换为宿主机IP
tcpdump -i eth0 -n icmp host 8.8.8.8
# 输出:
# 10:00:01 IP 192.168.1.10 > 8.8.8.8: ICMP echo request
# 10:00:01 IP 8.8.8.8 > 192.168.1.10: ICMP echo reply
# 注意: 源IP是192.168.1.10(宿主机IP),不是172.17.0.2(容器IP)
MASQUERADE vs SNAT: MASQUERADE是SNAT的一种特殊形式。SNAT需要指定固定的目标IP,而MASQUERADE会自动使用出口网卡的当前IP。这对于动态获取IP的环境(如DHCP)更灵活。
3.6 端口映射底层原理(iptables DNAT + docker-proxy)
端口映射是bridge网络中最重要的功能之一。当执行docker run -p 8080:80 nginx时,Docker会在底层做两件事:
- 创建iptables DNAT规则: 将宿主机8080端口的流量转发到容器172.17.0.2:80
- 启动docker-proxy进程: 作为用户态代理,处理特殊场景的流量
iptables DNAT规则分析:
# 启动一个带端口映射的容器
docker run -d --name web -p 8080:80 nginx
# 查看DNAT规则
iptables -t nat -S | grep 8080
# 输出示例:
# -A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
# 规则解析:
# ! -i docker0: 不是从docker0接口进来的流量(即外部流量)
# -p tcp --dport 8080: TCP协议,目标端口8080
# -j DNAT --to-destination 172.17.0.2:80: 执行DNAT,目标地址改为容器IP:端口
完整的端口映射流量路径:
外部客户端 → 宿主机:8080
→ iptables PREROUTING链 → DOCKER链
→ DNAT: 目标地址 0.0.0.0:8080 → 172.17.0.2:80
→ 宿主机路由表 → docker0
→ veth pair → 容器eth0:80
→ nginx处理请求并返回响应
# 查看完整的NAT规则链
iptables -t nat -L DOCKER -n -v
# 输出示例:
# Chain DOCKER (2 references)
# pkts bytes target prot opt in out source destination
# 10 600 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80
# 查看PREROUTING链中的DOCKER跳转
iptables -t nat -L PREROUTING -n -v
# 输出示例:
# Chain PREROUTING (policy ACCEPT)
# pkts bytes target prot opt in out source destination
# 100 6000 DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
3.7 docker-proxy与hairpin NAT
除了iptables DNAT规则外,Docker还会为每个端口映射启动一个docker-proxy进程。这个进程是Docker守护进程的子进程,运行在用户态。
# 查看docker-proxy进程
ps aux | grep docker-proxy
# 输出示例:
# root 1234 0.0 0.0 12345 2345 ? Ssl 10:00 0:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.2 -container-port 80
# docker-proxy的作用:
# 监听宿主机的0.0.0.0:8080,将流量转发到172.17.0.2:80
为什么需要docker-proxy?
iptables DNAT规则在大多数情况下可以正常工作,但在以下场景中可能失效:
- 容器自己访问映射的端口(hairpin问题): 容器通过宿主机IP:端口访问自己的服务
- 宿主机内部访问: 从宿主机内部通过localhost或宿主机IP访问映射端口
# hairpin问题演示
# 容器web映射了宿主机8080端口
docker run -d --name web -p 8080:80 nginx
# 获取宿主机IP
HOST_IP=$(hostname -I | awk '{print $1}')
echo $HOST_IP # 192.168.1.10
# 从容器内通过宿主机IP:端口访问自己
docker exec web curl -s http://$HOST_IP:8080
# 如果没有docker-proxy或hairpin NAT,这个请求可能失败
# 因为DNAT规则可能将流量路由回容器,但源IP是docker0的IP,
# 导致响应路径异常
Hairpin NAT的iptables规则:
Docker通过以下规则解决hairpin问题:
# 查看POSTROUTING链中的MASQUERADE规则
iptables -t nat -S POSTROUTING | grep docker
# 输出示例:
# -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
# -A POSTROUTING -s 172.17.0.2/32 -d 172.17.0.2/32 -p tcp -m tcp --dport 80 -j MASQUERADE
# 第二条规则就是hairpin NAT规则:
# 当源地址是容器IP(172.17.0.2),目标地址也是容器IP(172.17.0.2),目标端口是80时
# 执行MASQUERADE,将源IP替换为docker0的IP(172.17.0.1)
# 这样容器的响应会发送给docker0,再由docker0转发回容器
禁用docker-proxy: 在某些场景下(如iptables工作正常且不需要docker-proxy),可以禁用它以节省资源:
# 修改Docker配置
# /etc/docker/daemon.json
{
"userland-proxy": false
}
# 重启Docker
systemctl restart docker
# 禁用后,Docker只会使用iptables规则做端口映射
# 优点: 减少用户态进程开销,性能更好
# 缺点: 某些边缘场景可能不工作
3.8 自定义bridge网络 vs 默认bridge网络
Docker的默认bridge网络(docker0)和用户自定义bridge网络在功能上有重要区别:
| 特性 | 默认bridge (docker0) | 自定义bridge |
|---|---|---|
| DNS解析(容器名) | 不支持 | 支持 |
| 网络隔离 | 所有容器在同一网络 | 不同自定义网络互相隔离 |
| 配置灵活性 | 有限 | 支持自定义子网、网关、IP范围 |
| 容器连接 | 只能在一个网络 | 可连接多个网络 |
| 兼容性 | 向后兼容 | 推荐使用 |
| –link参数 | 支持(已弃用) | 不需要(有DNS) |
# 默认bridge网络的DNS问题
docker run -d --name app1 alpine sleep 3600
docker run -d --name app2 alpine sleep 3600
# 默认bridge中无法通过容器名解析
docker exec app1 ping -c 1 app2
# ping: bad address 'app2'
# 创建自定义bridge网络
docker network create my-custom-net
# 在自定义网络中创建容器
docker run -d --name app3 --network my-custom-net alpine sleep 3600
docker run -d --name app4 --network my-custom-net alpine sleep 3600
# 自定义bridge中可以通过容器名解析!
docker exec app3 ping -c 1 app4
# PING app4 (172.18.0.3): 56 data bytes
# 64 bytes from 172.18.0.3: seq=0 ttl=64 time=0.055 ms
为什么自定义bridge支持DNS而默认bridge不支持?
默认bridge网络(docker0)设计于Docker早期,为了向后兼容,没有加入DNS功能。自定义bridge网络是Docker后来引入的,集成了Docker内置DNS服务器(127.0.0.11),自动为同一网络中的容器提供名称解析。
默认bridge网络的历史包袱: Docker最初的版本只有默认bridge网络(docker0),当时容器间通信主要依赖--link参数。--link参数会在容器的/etc/hosts文件中添加目标容器的名称和IP映射,从而实现简单的名称解析。但这种方式有很多局限:首先,--link是静态的,如果目标容器的IP发生变化(如重启后IP改变),映射不会自动更新;其次,--link只能在创建容器时指定,不能动态添加;最后,--link创建的是单向链接,被链接的容器并不知道谁链接了它。自定义bridge网络通过内置DNS服务器彻底解决了这些问题——DNS解析是动态的,容器启动和停止时DNS记录会自动更新,而且同一网络中的所有容器都可以互相解析,不需要额外配置。
# 对比 --link 和自定义网络DNS
# 方式1: 使用--link (已弃用,不推荐)
docker run -d --name legacy-db mysql:8.0
docker run -d --name legacy-app --link legacy-db:db nginx
# legacy-app的/etc/hosts中会添加: 172.17.0.2 db legacy-db
# 问题: 如果legacy-db重启后IP变为172.17.0.3,hosts文件不会更新!
# 方式2: 使用自定义网络 (推荐)
docker network create modern-net
docker run -d --name modern-db --network modern-net mysql:8.0
docker run -d --name modern-app --network modern-net nginx
# modern-app可以通过 "modern-db" 直接访问数据库
# 如果modern-db重启,DNS自动更新,不需要任何手动操作
bridge网络中的ICMP行为: 在bridge网络中,容器之间的ICMP(ping)通信是通过网桥的二层转发实现的。需要注意的是,Docker默认开启容器间通信(ICC, Inter-Container Communication),即同一bridge网络中的容器可以自由通信。如果出于安全考虑需要禁用容器间通信,可以在Docker配置中设置icc=false。但请注意,禁用ICC后,同一bridge网络中的所有容器都无法互相通信,这通常不是期望的行为。更好的做法是使用多个自定义网络来实现网络隔离,而不是禁用ICC。
# 查看当前的ICC设置
docker network inspect bridge | grep com.docker.network.bridge.enable_icc
# "com.docker.network.bridge.enable_icc": "true"
# 在自定义网络中禁用容器间通信
docker network create --opt com.docker.network.bridge.enable_icc=false restricted-net
# 注意: 禁用ICC后,同一网络中的容器之间也无法通过容器名解析
# 因为DNS查询也需要网络通信
# 更好的隔离方式: 使用多个网络
docker network create frontend-net
docker network create backend-net
# 前端容器只连frontend-net,后端容器只连backend-net
# 它们自然无法通信,实现了网络隔离
# 查看自定义网络中容器的DNS配置
docker exec app3 cat /etc/resolv.conf
# 输出:
# nameserver 127.0.0.11
# options ndots:0
# 127.0.0.11就是Docker内置DNS服务器
# 而默认bridge网络中容器的DNS配置:
docker exec app1 cat /etc/resolv.conf
# 输出:
# nameserver 8.8.8.8 (宿主机的DNS配置)
# nameserver 8.8.4.4
# 没有使用内置DNS服务器
3.9 bridge网络实战: 多容器Web应用
让我们通过一个完整的多容器Web应用来实践bridge网络的各种功能。
场景: 部署一个包含Nginx(前端)、Node.js(API后端)、MySQL(数据库)的Web应用。
# 第一步: 创建自定义bridge网络
docker network create web-app-net
# 查看网络
docker network inspect web-app-net
# 子网自动分配,如172.20.0.0/16
# 第二步: 启动MySQL数据库容器
docker run -d \
--name mysql-db \
--network web-app-net \
--network-alias db \
-e MYSQL_ROOT_PASSWORD=secret123 \
-e MYSQL_DATABASE=myapp \
-e MYSQL_USER=appuser \
-e MYSQL_PASSWORD=apppass \
mysql:8.0
# 第三步: 启动Node.js后端API容器
# 先创建一个简单的Node.js应用
mkdir -p /tmp/myapp-api
cat > /tmp/myapp-api/package.json << 'EOF'
{
"name": "myapp-api",
"version": "1.0.0",
"main": "server.js",
"dependencies": {
"express": "^4.18.0",
"mysql2": "^3.6.0"
}
}
EOF
cat > /tmp/myapp-api/server.js << 'JSEOF'
const express = require('express');
const mysql = require('mysql2/promise');
const app = express();
const port = 3000;
app.get('/', async (req, res) => {
try {
const conn = await mysql.createConnection({
host: 'db', // 使用容器名(网络别名)作为主机名
user: 'appuser',
password: 'apppass',
database: 'myapp'
});
const [rows] = await conn.execute('SELECT NOW() as time');
await conn.end();
res.json({
message: 'Hello from API',
dbTime: rows[0].time,
hostname: require('os').hostname()
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.listen(port, '0.0.0.0', () => {
console.log(`API server running on port ${port}`);
});
JSEOF
cat > /tmp/myapp-api/Dockerfile << 'EOF'
FROM node:18-alpine
WORKDIR /app
COPY package.json .
RUN npm install --production
COPY server.js .
CMD ["node", "server.js"]
EOF
# 构建API镜像
docker build -t myapp-api /tmp/myapp-api/
# 启动API容器
docker run -d \
--name api-server \
--network web-app-net \
--network-alias api \
myapp-api
# 第四步: 启动Nginx前端容器
mkdir -p /tmp/myapp-nginx
cat > /tmp/myapp-nginx/default.conf << 'NGINXEOF'
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
}
location /api/ {
proxy_pass http://api:3000/; # 使用容器名作为主机名
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
NGINXEOF
cat > /tmp/myapp-nginx/index.html << 'HTMLEOF'
<!DOCTYPE html>
<html>
<head><title>My Docker App</title></head>
<body>
<h1>Welcome to My Docker Web App</h1>
<div id="api-result">Loading...</div>
<script>
fetch('/api/').then(r => r.json()).then(data => {
document.getElementById('api-result').innerHTML =
'<pre>' + JSON.stringify(data, null, 2) + '</pre>';
}).catch(err => {
document.getElementById('api-result').innerHTML = 'Error: ' + err;
});
</script>
</body>
</html>
HTMLEOF
cat > /tmp/myapp-nginx/Dockerfile << 'EOF'
FROM nginx:alpine
COPY default.conf /etc/nginx/conf.d/default.conf
COPY index.html /usr/share/nginx/html/index.html
EOF
# 构建Nginx镜像
docker build -t myapp-nginx /tmp/myapp-nginx/
# 启动Nginx容器,映射端口到宿主机
docker run -d \
--name web-frontend \
--network web-app-net \
-p 8080:80 \
myapp-nginx
# 第五步: 验证应用
# 从宿主机访问
curl http://localhost:8080/
curl http://localhost:8080/api/
# 从API容器内访问数据库(通过容器名)
docker exec api-server wget -qO- http://db:3306 # MySQL不响应HTTP,但说明DNS解析正常
# 查看各容器的IP
docker network inspect web-app-net --format '{{range .Containers}}{{.Name}}: {{.IPv4Address}}{{"\n"}}{{end}}'
# api-server: 172.20.0.3/16
# mysql-db: 172.20.0.2/16
# web-frontend: 172.20.0.4/16
# 清理
docker rm -f web-frontend api-server mysql-db
docker network rm web-app-net
这个实战案例展示了:
- 使用自定义bridge网络(支持DNS解析)
- 通过容器名/网络别名进行服务间通信
- 端口映射让外部可以访问前端
- Nginx反向代理到后端API
- 后端API连接MySQL数据库
第四章 自定义网络管理
自定义网络是Docker网络管理中最重要的能力之一。通过自定义网络,我们可以精确控制子网、网关、IP范围,实现容器间DNS解析,管理容器与多个网络的连接,以及实现服务发现。本章将全面讲解Docker自定义网络的创建和管理。
4.1 docker network create命令详解
docker network create是创建自定义网络的核心命令,其完整语法如下:
docker network create [OPTIONS] NETWORK_NAME
常用选项:
| 选项 | 说明 | 示例 |
|---|---|---|
--driver / -d |
网络驱动(默认bridge) | -d bridge |
--subnet |
子网CIDR | --subnet 172.20.0.0/16 |
--gateway |
网关地址 | --gateway 172.20.0.1 |
--ip-range |
IP分配范围 | --ip-range 172.20.0.0/24 |
--ipam-driver |
IPAM驱动(默认default) | --ipam-driver default |
--internal |
限制为内部网络(禁止外部访问) | --internal |
--attachable |
允许手动连接容器(overlay) | --attachable |
--scope |
网络范围(local/swarm/global) | --scope local |
--config-from |
从其他网络复制配置 | --config-from other-net |
--label |
添加标签 | --label env=prod |
--opt / -o |
驱动选项 | -o com.docker.network.bridge.enable_icc=true |
# 最简单的创建方式
docker network create my-net
# 查看创建的网络
docker network inspect my-net
# 指定子网和网关创建
docker network create \
--subnet 172.22.0.0/16 \
--gateway 172.22.0.1 \
my-net2
# 指定IP范围(从子网中限定一个范围分配IP)
docker network create \
--subnet 172.23.0.0/16 \
--ip-range 172.23.0.0/24 \ # 只在172.23.0.0-172.23.0.255范围内分配
--gateway 172.23.0.1 \
my-net3
# 创建内部网络(不允许访问外部网络)
docker network create \
--internal \
my-internal-net
# 创建带标签的网络
docker network create \
--label environment=production \
--label team=backend \
my-prod-net
4.2 创建自定义bridge网络(指定子网、网关、IP范围)
让我们深入实践自定义bridge网络的创建:
# 场景: 为微服务应用创建专用网络
# 前端网络: 172.30.0.0/24
# 后端网络: 172.30.1.0/24
# 数据库网络: 172.30.2.0/24 (内部网络)
# 创建前端网络
docker network create \
--driver bridge \
--subnet 172.30.0.0/24 \
--gateway 172.30.0.1 \
--label tier=frontend \
frontend-net
# 创建后端网络
docker network create \
--driver bridge \
--subnet 172.30.1.0/24 \
--gateway 172.30.1.1 \
--label tier=backend \
backend-net
# 创建数据库网络(内部网络,不可访问外网)
docker network create \
--driver bridge \
--subnet 172.30.2.0/24 \
--gateway 172.30.2.1 \
--internal \
--label tier=database \
db-net
# 查看所有自定义网络
docker network ls
# NETWORK ID NAME DRIVER SCOPE
# ... frontend-net bridge local
# ... backend-net bridge local
# ... db-net bridge local
# 查看网络详情
docker network inspect frontend-net
# 验证内部网络的隔离性
docker run -d --name test-internal --network db-net alpine sleep 3600
docker exec test-internal ping -c 1 8.8.8.8
# ping: connect: Network is unreachable (内部网络无法访问外网)
# 但可以访问同网络的其他容器
docker run -d --name test-internal2 --network db-net alpine sleep 3600
docker exec test-internal ping -c 1 test-internal2
# 可以ping通
# 清理
docker rm -f test-internal test-internal2
4.3 网络驱动选项配置
创建网络时,可以通过-o(或--opt)参数传递驱动特定的选项:
# bridge驱动支持的选项
docker network create \
--driver bridge \
--subnet 172.31.0.0/16 \
-o com.docker.network.bridge.name=br-custom \ # 自定义网桥名称
-o com.docker.network.bridge.enable_icc=true \ # 启用容器间通信(默认true)
-o com.docker.network.bridge.enable_ip_masquerade=true \ # 启用MASQUERADE(默认true)
-o com.docker.network.bridge.host_binding_ipv4=0.0.0.0 \ # 端口绑定地址
-o com.docker.network.mtu=1500 \ # 设置MTU
-o com.docker.network.bridge.default_bridge=false \ # 非默认桥
custom-bridge-net
# 验证自定义网桥名称
ip link show br-custom
# 可以看到br-custom网桥已创建
# 查看网桥详细信息
docker network inspect custom-bridge-net | python3 -m json.tool
# 可以看到Options中包含了所有自定义选项
常用驱动选项详解:
| 选项 | 说明 | 默认值 |
|---|---|---|
com.docker.network.bridge.name |
网桥在Linux中的名称 | br-XXXXXXXXXXXX |
com.docker.network.bridge.enable_icc |
是否允许容器间通信 | true |
com.docker.network.bridge.enable_ip_masquerade |
是否启用IP伪装 | true |
com.docker.network.bridge.host_binding_ipv4 |
端口映射绑定的IP | 0.0.0.0 |
com.docker.network.mtu |
MTU值 | 1500 |
com.docker.network.bridge.default_bridge |
是否为默认桥 | false |
# 实战: 禁用容器间通信(ICC)
# 创建一个ICC=false的网络
docker network create \
--subnet 172.32.0.0/16 \
-o com.docker.network.bridge.enable_icc=false \
no-icc-net
# 启动两个容器
docker run -d --name c1 --network no-icc-net alpine sleep 3600
docker run -d --name c2 --network no-icc-net alpine sleep 3600
# 尝试容器间通信(应该失败)
docker exec c1 ping -c 1 c2
# PING c2 (172.32.0.3): 56 data bytes
# (无响应,因为ICC被禁用)
# 但DNS解析仍然工作
docker exec c1 nslookup c2
# Name: c2
# Address 1: 172.32.0.3 c2.no-icc-net
# 清理
docker rm -f c1 c2
docker network rm no-icc-net
MTU配置的重要性: MTU(Maximum Transmission Unit,最大传输单元)决定了网络数据包的最大尺寸。默认情况下,Docker bridge网络的MTU为1500字节,这是以太网的标准MTU。但在某些场景下,默认MTU可能导致网络问题。例如,当宿主机本身运行在云环境中(如AWS VPC、Azure VNet),底层网络可能使用了VXLAN或GRE隧道,这些隧道会在原始数据包上添加封装头(通常50-70字节),导致实际可用MTU小于1500。如果容器发出的数据包超过了底层网络的MTU,数据包会被分片或丢弃,导致网络性能下降或通信失败。
# 场景1: 云环境中MTU不匹配的问题
# 症状: 容器可以ping通外部IP,但大文件传输或HTTPS握手失败
# 原因: 小包(如ICMP)可以通过,但大包(如TLS握手)被丢弃
# 排查: 测试不同大小的数据包
docker exec my-container ping -c 3 -s 1472 8.8.8.8 # 1472+28(ICMP头)=1500
# 如果失败,逐步减小包大小
docker exec my-container ping -c 3 -s 1400 8.8.8.8
docker exec my-container ping -c 3 -s 1200 8.8.8.8
# 解决: 创建网络时指定较小的MTU
docker network create \
--subnet 172.33.0.0/16 \
-o com.docker.network.mtu=1400 \
cloud-compatible-net
# 或者在Docker全局配置中设置MTU
# /etc/docker/daemon.json
# {
# "mtu": 1400
# }
# 重启Docker后,所有新创建的网络都会使用1400的MTU
内部网络的安全价值: --internal选项创建的内部网络不允许容器访问外部网络,也不允许外部网络访问容器。这通过不创建MASQUERADE规则和 FORWARD链DROP规则来实现。内部网络非常适合部署数据库、缓存等不需要互联网访问的敏感服务。即使容器被攻击者控制,由于没有出站网络通道,攻击者也无法将数据外泄或建立反向连接。这是纵深防御策略在网络层面的体现。
# 验证内部网络的iptables规则差异
# 普通网络: 有MASQUERADE规则
docker network create normal-net
iptables -t nat -L | grep normal-net
# 可以看到MASQUERADE规则
# 内部网络: 没有MASQUERADE规则
docker network create --internal internal-net
iptables -t nat -L | grep internal-net
# 没有任何MASQUERADE规则,容器无法访问外网
# 清理
docker network rm normal-net internal-net
4.4 docker network connect/disconnect
docker network connect和docker network disconnect命令可以让一个运行中的容器连接到或断开某个网络。这是实现容器连接多个网络的关键命令。
# docker network connect 语法
docker network connect [OPTIONS] NETWORK CONTAINER
# 选项:
# --ip 指定IP地址
# --ip6 指定IPv6地址
# --alias 添加网络别名
# --link 添加到另一个容器的链接
# --link-local-ip 添加链路本地地址
# docker network disconnect 语法
docker network disconnect [OPTIONS] NETWORK CONTAINER
# 选项:
# -f / --force 强制断开
实战: 容器连接多个网络
# 创建两个网络
docker network create --subnet 172.33.0.0/16 net-a
docker network create --subnet 172.34.0.0/16 net-b
# 创建一个连接到net-a的容器
docker run -d --name multi-net-container \
--network net-a \
--ip 172.33.0.10 \
alpine sleep 3600
# 将容器也连接到net-b,指定IP和别名
docker network connect \
--ip 172.34.0.10 \
--alias container-b \
net-b multi-net-container
# 查看容器的网络信息
docker inspect multi-net-container --format '{{json .NetworkSettings.Networks}}' | python3 -m json.tool
# 输出:
# {
# "net-a": {
# "IPAddress": "172.33.0.10",
# "Aliases": ["multi-net-container"],
# "Gateway": "172.33.0.1"
# },
# "net-b": {
# "IPAddress": "172.34.0.10",
# "Aliases": ["multi-net-container", "container-b"],
# "Gateway": "172.34.0.1"
# }
# }
# 容器内可以看到两个网络接口
docker exec multi-net-container ip addr
# eth0: 172.33.0.10 (net-a)
# eth1: 172.34.0.10 (net-b)
# 在net-a中创建另一个容器,通过容器名访问
docker run -d --name container-in-a --network net-a alpine sleep 3600
docker exec container-in-a ping -c 1 multi-net-container
# 可以ping通 (通过net-a)
# 在net-b中创建另一个容器,通过别名访问
docker run -d --name container-in-b --network net-b alpine sleep 3600
docker exec container-in-b ping -c 1 container-b
# 可以ping通 (通过net-b,使用别名container-b)
# 断开容器与net-b的连接
docker network disconnect net-b multi-net-container
# 验证
docker exec multi-net-container ip addr
# 现在只有eth0 (net-a),eth1已移除
# 清理
docker rm -f multi-net-container container-in-a container-in-b
docker network rm net-a net-b
多网络的应用场景:
# 场景: 一个API网关容器需要连接前端网络和后端网络
docker network create frontend-net
docker network create backend-net
# 启动前端服务
docker run -d --name frontend --network frontend-net nginx:alpine
# 启动后端服务
docker run -d --name backend --network backend-net nginx:alpine
# 启动API网关,先连接前端网络
docker run -d --name gateway \
--network frontend-net \
--network-alias gw-frontend \
nginx:alpine
# 再连接后端网络
docker network connect \
--alias gw-backend \
backend-net gateway
# 现在gateway可以同时与frontend和backend通信
# frontend可以通过gw-frontend访问gateway
# backend可以通过gw-backend访问gateway
# 清理
docker rm -f frontend backend gateway
docker network rm frontend-net backend-net
4.5 docker network ls/rm/inspect/prune
docker network ls: 列出所有网络
# 列出所有网络
docker network ls
# 按驱动过滤
docker network ls --filter driver=bridge
# 按ID过滤
docker network ls --filter id=a1b2c3
# 按名称过滤
docker network ls --filter name=net
# 按标签过滤
docker network ls --filter label=environment=production
# 不截断ID
docker network ls --no-trunc
# 格式化输出
docker network ls --format "table {{.ID}}\t{{.Name}}\t{{.Driver}}\t{{.Scope}}"
docker network rm: 删除网络
# 删除单个网络
docker network rm my-net
# 删除多个网络
docker network rm net1 net2 net3
# 注意: 不能删除正在使用中的网络
docker network rm bridge # 错误: bridge是预定义网络,不能删除
# Error response from daemon: bridge is a pre-defined network and cannot be removed
docker network inspect: 查看网络详情
# 查看网络详情
docker network inspect my-net
# 查看多个网络
docker network inspect net1 net2
# 格式化输出特定字段
docker network inspect my-net --format '{{.Driver}}'
# bridge
# 查看网络中的容器
docker network inspect my-net --format '{{json .Containers}}' | python3 -m json.tool
# 查看IPAM配置
docker network inspect my-net --format '{{json .IPAM}}' | python3 -m json.tool
# 查看网络选项
docker network inspect my-net --format '{{json .Options}}' | python3 -m json.tool
docker network prune: 清理未使用的网络
# 清理所有未使用的网络(会提示确认)
docker network prune
# 不提示确认,直接清理
docker network prune -f
# 按标签过滤清理
docker network prune --filter label=environment=test
# 清理超过24小时未使用的网络
docker network prune --filter "until=24h"
4.6 固定IP分配(–ip参数)
在某些场景下(如配置文件中需要指定固定IP),我们需要为容器分配固定的IP地址。
# 创建带子网的网络
docker network create --subnet 172.35.0.0/16 my-fixed-ip-net
# 指定固定IP启动容器
docker run -d \
--name fixed-ip-container \
--network my-fixed-ip-net \
--ip 172.35.0.100 \
nginx
# 验证IP
docker inspect fixed-ip-container --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
# 172.35.0.100
# 对已运行容器连接网络时指定IP
docker run -d --name another-container --network bridge alpine sleep 3600
docker network connect --ip 172.35.0.200 my-fixed-ip-net another-container
# 验证
docker inspect another-container --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}}: {{$v.IPAddress}}{{"\n"}}{{end}}'
# bridge: 172.17.0.2
# my-fixed-ip-net: 172.35.0.200
# IPv6固定IP
docker network create --subnet 2001:db8:1::/64 --ipv6 my-ipv6-net
docker run -d \
--name ipv6-container \
--network my-ipv6-net \
--ip6 2001:db8:1::100 \
alpine sleep 3600
# 清理
docker rm -f fixed-ip-container another-container ipv6-container
docker network rm my-fixed-ip-net my-ipv6-net
固定IP的注意事项:
- 必须在创建网络时指定子网,才能使用
--ip参数 - 指定的IP必须在子网范围内
- 如果指定的IP已被使用,Docker会报错
- 容器停止重启后,如果网络配置不变,IP会保持一致
4.7 网络别名(–network-alias)
网络别名允许为容器在一个网络中设置多个名称,其他容器可以通过任何一个别名来访问该容器。
# 创建网络
docker network create alias-net
# 启动容器时指定网络别名
docker run -d \
--name redis-server \
--network alias-net \
--network-alias redis \
--network-alias cache \
--network-alias session-store \
redis:alpine
# 同一网络中的其他容器可以通过任何别名访问
docker run -it --rm --network alias-net alpine sh -c \
"ping -c 1 redis && ping -c 1 cache && ping -c 1 session-store"
# 所有别名都解析到同一个IP
docker run -it --rm --network alias-net alpine sh -c \
"nslookup redis && nslookup cache && nslookup session-store"
# 输出示例:
# Name: redis
# Address 1: 172.36.0.2 redis.alias-net
# Name: cache
# Address 1: 172.36.0.2 redis.alias-net
# Name: session-store
# Address 1: 172.36.0.2 redis.alias-net
# 对已运行容器添加别名
docker network connect --alias db alias-net some-container
# 清理
docker rm -f redis-server
docker network rm alias-net
网络别名的应用场景:
# 场景: 数据库迁移,从MySQL迁移到PostgreSQL
# 应用配置中使用别名"database"连接数据库
# 迁移前: MySQL容器使用别名"database"
docker run -d \
--name mysql-old \
--network app-net \
--network-alias database \
-e MYSQL_ROOT_PASSWORD=secret \
mysql:8.0
# 应用通过database:3306连接MySQL
# 迁移后: PostgreSQL容器使用别名"database"
docker rm -f mysql-old
docker run -d \
--name postgres-new \
--network app-net \
--network-alias database \
-e POSTGRES_PASSWORD=secret \
postgres:15
# 应用通过database:5432连接PostgreSQL
# 只需要修改端口号,主机名不需要改变
4.8 自定义网络中的服务发现(自动DNS解析)
自定义bridge网络和overlay网络内置了DNS服务发现功能。当容器连接到这些网络时,Docker会自动为容器提供DNS解析服务。
# Docker内置DNS服务器地址: 127.0.0.11
# 容器中的/etc/resolv.conf会自动配置为使用该DNS
# 创建自定义网络
docker network create discovery-net
# 启动多个容器
docker run -d --name web --network discovery-net nginx:alpine
docker run -d --name api --network discovery-net --network-alias backend nginx:alpine
docker run -d --name db --network discovery-net --network-alias database nginx:alpine
# 在一个容器中测试DNS解析
docker run -it --rm --network discovery-net alpine sh
# 在容器内执行:
# nslookup web
# Server: 127.0.0.11
# Address: 127.0.0.11:53
#
# Name: web
# Address 1: 172.37.0.2 web.discovery-net
# nslookup backend
# Name: backend
# Address 1: 172.37.0.3 api.discovery-net
# nslookup database
# Name: database
# Address 1: 172.37.0.4 db.discovery-net
# 使用容器名访问
# wget -qO- http://web
# wget -qO- http://backend
# wget -qO- http://database
# 查看DNS解析过程
# dig web
# 清理
docker rm -f web api db
docker network rm discovery-net
服务发现的工作原理:
1. 容器启动并连接到自定义网络时
→ Docker将容器名和别名注册到内置DNS(127.0.0.11)
2. 同一网络中的容器发起DNS查询时
→ 查询发送到127.0.0.11
→ Docker内置DNS拦截查询
→ 查找注册的容器名/别名
→ 返回对应容器的IP地址
3. 如果查询的名称不在Docker注册列表中
→ Docker将查询转发到外部DNS服务器
→ 返回外部DNS的解析结果
服务发现的优势与价值: Docker内置的服务发现机制极大地简化了容器化应用的部署和运维。在传统架构中,服务之间的连接通常通过配置文件中的IP地址或主机名来实现,当服务实例的IP地址发生变化时(例如服务器迁移、扩缩容等),需要手动修改配置并重启服务。而Docker的服务发现机制完全消除了这个问题——容器只需要知道目标服务的名称,Docker内置DNS会自动将服务名解析为当前运行的容器IP地址。当容器启动、停止或重启时,DNS记录会自动更新,不需要任何人工干预。
这种机制使得容器化应用的弹性伸缩变得异常简单。以一个Web应用为例,当流量增加需要增加后端API实例时,只需要启动新的容器并连接到同一网络,前端容器就能自动通过DNS发现新的API实例并进行负载均衡(通过DNS轮询)。同样,当某个API实例出现故障被停止时,它的DNS记录会自动被移除,前端容器不会再将请求发送到故障实例。
# 演示服务发现的动态更新
docker network create sd-demo
# 启动第一个API实例
docker run -d --name api-1 --network sd-demo --network-alias api nginx:alpine
# 启动客户端容器,解析api
docker run -it --rm --network sd-demo alpine nslookup api
# Name: api Address: 172.38.0.2
# 启动第二个API实例(相同别名)
docker run -d --name api-2 --network sd-demo --network-alias api nginx:alpine
# 再次解析,DNS自动包含新实例
docker run -it --rm --network sd-demo alpine nslookup api
# Name: api Address 1: 172.38.0.2
# Name: api Address 2: 172.38.0.3
# 停止第一个实例
docker stop api-1
# DNS自动移除停止的实例
docker run -it --rm --network sd-demo alpine nslookup api
# Name: api Address: 172.38.0.3
# 清理
docker rm -f api-1 api-2
docker network rm sd-demo
4.9 网络管理实战案例
案例: 为多环境应用创建网络架构
# 场景: 在一台服务器上部署开发、测试、生产三套环境
# 每套环境有前端、API、数据库三个容器
# 环境之间互相隔离
# 创建三个环境的网络
docker network create --subnet 172.40.0.0/16 dev-net --label env=dev
docker network create --subnet 172.41.0.0/16 test-net --label env=test
docker network create --subnet 172.42.0.0/16 prod-net --label env=prod
# 开发环境
docker run -d --name dev-db --network dev-net --network-alias db \
-e MYSQL_ROOT_PASSWORD=devpass mysql:8.0
docker run -d --name dev-api --network dev-net --network-alias api \
-e DB_HOST=db -e DB_PASS=devpass myapp-api
docker run -d --name dev-web --network dev-net -p 8081:80 myapp-nginx
# 测试环境
docker run -d --name test-db --network test-net --network-alias db \
-e MYSQL_ROOT_PASSWORD=testpass mysql:8.0
docker run -d --name test-api --network test-net --network-alias api \
-e DB_HOST=db -e DB_PASS=testpass myapp-api
docker run -d --name test-web --network test-net -p 8082:80 myapp-nginx
# 生产环境
docker run -d --name prod-db --network prod-net --network-alias db \
-e MYSQL_ROOT_PASSWORD=prodpass mysql:8.0
docker run -d --name prod-api --network prod-net --network-alias api \
-e DB_HOST=db -e DB_PASS=prodpass myapp-api
docker run -d --name prod-web --network prod-net -p 8083:80 myapp-nginx
# 验证隔离性
# dev-api可以访问dev-db (同一网络)
docker exec dev-api ping -c 1 db
# 成功
# dev-api无法访问test-db (不同网络)
docker exec dev-api ping -c 1 172.41.0.2
# 失败 (不同子网之间隔离)
# 查看各环境网络
docker network ls --filter label=env
# 查看网络中的容器
echo "=== 开发环境 ==="
docker network inspect dev-net --format '{{range .Containers}}{{.Name}}: {{.IPv4Address}}{{"\n"}}{{end}}'
echo "=== 测试环境 ==="
docker network inspect test-net --format '{{range .Containers}}{{.Name}}: {{.IPv4Address}}{{"\n"}}{{end}}'
echo "=== 生产环境 ==="
docker network inspect prod-net --format '{{range .Containers}}{{.Name}}: {{.IPv4Address}}{{"\n"}}{{end}}'
# 访问各环境
# 开发: http://localhost:8081
# 测试: http://localhost:8082
# 生产: http://localhost:8083
# 清理
docker rm -f dev-db dev-api dev-web test-db test-api test-web prod-db prod-api prod-web
docker network rm dev-net test-net prod-net
Docker Compose中的网络管理: 在实际开发中,我们通常使用Docker Compose来编排多容器应用。Docker Compose自动为每个Compose项目创建一个默认网络,项目中的所有服务都连接到这个网络,并可以通过服务名互相访问。除了默认网络外,还可以在Compose文件中定义自定义网络,实现更精细的网络隔离。
# docker-compose.yml 示例: 多网络架构
version: '3.8'
services:
# 前端服务 - 连接frontend和backend网络
web:
image: nginx:alpine
ports:
- "80:80"
networks:
- frontend
- backend
depends_on:
- api
# API服务 - 连接backend和data网络
api:
image: my-api:latest
networks:
- backend
- data
environment:
- DB_HOST=database
- REDIS_HOST=cache
depends_on:
- database
- cache
# 数据库 - 只在data网络中
database:
image: postgres:15
networks:
- data
volumes:
- db-data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=secret
# 缓存 - 只在data网络中
cache:
image: redis:alpine
networks:
- data
networks:
# 前端网络(对外暴露)
frontend:
driver: bridge
labels:
tier: frontend
# 后端网络(应用层通信)
backend:
driver: bridge
labels:
tier: backend
# 数据网络(内部网络,不可访问外网)
data:
driver: bridge
internal: true
labels:
tier: data
volumes:
db-data:
# 使用Compose启动
docker-compose up -d
# Compose自动创建的网络名称格式: <项目名>_<网络名>
docker network ls | grep myapp
# myapp_frontend
# myapp_backend
# myapp_data
# 验证网络隔离
# web可以访问api (通过backend网络)
docker exec myapp-web-1 ping -c 1 api
# 成功
# web无法直接访问database (不在同一网络)
docker exec myapp-web-1 ping -c 1 database
# ping: bad address 'database' (无法解析)
# database无法访问外网 (internal网络)
docker exec myapp-database-1 ping -c 1 8.8.8.8
# Network is unreachable
# 使用已有网络(不创建新网络)
# 在docker-compose.yml中:
# networks:
# existing-net:
# external: true
# name: my-existing-network
# 使用固定IP
# services:
# api:
# networks:
# backend:
# ipv4_address: 172.20.0.10
这种通过Docker Compose定义的网络架构,使得多容器应用的网络配置变得声明式和可版本化,大大提升了部署的可重复性和可维护性。
第五章 容器间通信
容器间通信是Docker网络最核心的使用场景。无论是微服务架构中的服务调用,还是Web应用的前后端交互,都依赖于容器之间的网络通信。本章将全面讲解容器间通信的各种方式、原理和最佳实践。
5.1 同一网络内容器通信(通过容器名)
在自定义bridge网络和overlay网络中,容器可以通过容器名或网络别名进行通信,这得益于Docker内置的DNS服务发现机制。
# 创建自定义网络
docker network create app-network
# 启动Web服务器容器
docker run -d \
--name web-server \
--network app-network \
nginx:alpine
# 启动应用容器
docker run -d \
--name app-server \
--network app-network \
alpine sleep 3600
# 通过容器名通信
docker exec app-server wget -qO- http://web-server
# 返回nginx欢迎页面HTML
# 通过容器名ping
docker exec app-server ping -c 2 web-server
# PING web-server (172.43.0.2): 56 data bytes
# 64 bytes from 172.43.0.2: seq=0 ttl=64 time=0.089 ms
# 使用nslookup查看DNS解析
docker exec app-server nslookup web-server
# Server: 127.0.0.11
# Address: 127.0.0.11:53
#
# Name: web-server
# Address 1: 172.43.0.2 web-server.app-network
# 清理
docker rm -f web-server app-server
docker network rm app-network
通信原理: 当app-server容器尝试访问web-server时:
- 容器内的DNS客户端向
127.0.0.11(Docker内置DNS)发起查询 - Docker内置DNS查找注册表,发现
web-server对应IP172.43.0.2 - 返回IP地址给
app-server app-server通过IP地址与web-server建立TCP连接- 流量通过docker0网桥(或自定义网桥)进行二层转发
5.2 不同网络间容器通信
默认情况下,不同网络中的容器无法直接通信。要实现跨网络通信,有以下几种方法:
方法一: 将容器连接到目标网络
# 创建两个隔离的网络
docker network create --subnet 172.44.0.0/16 network-x
docker network create --subnet 172.45.0.0/16 network-y
# 在network-x中启动容器A
docker run -d --name container-a --network network-x alpine sleep 3600
# 在network-y中启动容器B
docker run -d --name container-b --network network-y alpine sleep 3600
# 此时A无法通过容器名访问B
docker exec container-a ping -c 1 container-b
# ping: bad address 'container-b' (DNS无法解析,不在同一网络)
# 将容器A也连接到network-y
docker network connect network-y container-a
# 现在A可以通过容器名访问B
docker exec container-a ping -c 2 container-b
# PING container-b (172.45.0.3): 56 data bytes
# 64 bytes from 172.45.0.3: seq=0 ttl=64 time=0.078 ms
# 查看A的网络接口
docker exec container-a ip addr
# eth0: 172.44.0.2 (network-x)
# eth1: 172.45.0.2 (network-y)
# 清理
docker rm -f container-a container-b
docker network rm network-x network-y
方法二: 通过端口映射和宿主机IP
# 容器A在network-x中,映射端口到宿主机
docker run -d --name service-a --network network-x -p 8080:80 nginx:alpine
# 容器B在network-y中,通过宿主机IP+端口访问A
HOST_IP=$(hostname -I | awk '{print $1}')
docker run -d --name service-b --network network-y alpine sleep 3600
docker exec service-b wget -qO- http://$HOST_IP:8080
# 可以访问,但流量经过了NAT,性能较差
方法三: 使用容器IP直接通信(不推荐)
# 获取容器A的IP
A_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' container-a)
# 在容器B中通过IP访问(如果两个网络的路由允许)
docker exec container-b ping -c 1 $A_IP
# 可能失败,取决于iptables规则和路由配置
5.3 容器连接多个网络
一个容器可以同时连接多个网络,每个网络分配一个独立的网络接口和IP地址。
# 创建三个网络
docker network create --subnet 172.46.0.0/16 web-tier
docker network create --subnet 172.47.0.0/16 app-tier
docker network create --subnet 172.48.0.0/16 db-tier
# 创建一个API容器,连接到web-tier和app-tier
docker run -d --name api \
--network web-tier \
--ip 172.46.0.10 \
--network-alias api-web \
alpine sleep 3600
docker network connect \
--ip 172.47.0.10 \
--alias api-app \
app-tier api
# 创建一个数据库容器,连接到app-tier和db-tier
docker run -d --name database \
--network db-tier \
--ip 172.48.0.10 \
alpine sleep 3600
docker network connect \
--ip 172.47.0.20 \
--alias db-app \
app-tier database
# 查看api容器的网络接口
docker exec api ip addr
# eth0: 172.46.0.10 (web-tier)
# eth1: 172.47.0.10 (app-tier)
# api可以通过app-tier网络访问database
docker exec api ping -c 2 db-app
# PING db-app (172.47.0.20): 56 data bytes
# 64 bytes from 172.47.0.20: seq=0 ttl=64 time=0.078 ms
# database的网络接口
docker exec database ip addr
# eth0: 172.48.0.10 (db-tier)
# eth1: 172.47.0.20 (app-tier)
# 网络拓扑:
# web-tier: api(172.46.0.10)
# app-tier: api(172.47.0.10) ↔ database(172.47.0.20)
# db-tier: database(172.48.0.10)
#
# api和database通过app-tier通信
# web-tier中的其他容器只能访问api
# db-tier中的其他容器只能访问database
# 清理
docker rm -f api database
docker network rm web-tier app-tier db-tier
5.4 容器间通信的安全考量
在设计容器间通信时,安全性是一个重要考虑因素:
1. 网络隔离原则
# 最佳实践: 按安全级别划分网络
# 公开网络(面向外部) → 应用网络 → 数据库网络(内部)
# 数据库网络设为内部网络
docker network create --internal --subnet 172.49.0.0/16 secure-db-net
# 数据库容器只在内部网络中
docker run -d --name mysql --network secure-db-net \
-e MYSQL_ROOT_PASSWORD=secret mysql:8.0
# 数据库容器无法访问外网(减少攻击面)
docker exec mysql ping -c 1 8.8.8.8
# ping: connect: Network is unreachable
# 应用容器连接到前端网络和数据库网络
docker network create --subnet 172.50.0.0/16 frontend-net
docker run -d --name app \
--network frontend-net \
-p 8080:8080 \
myapp
# 应用容器连接到数据库网络
docker network connect secure-db-net app
# 现在app可以访问mysql,但mysql无法被外部直接访问
docker exec app ping -c 1 mysql
# 可以访问
# 外部无法直接访问mysql (没有端口映射,且在内部网络中)
2. 限制容器间通信(ICC)
# 创建ICC=false的网络,禁止同网络容器间直接通信
docker network create \
--subnet 172.51.0.0/16 \
-o com.docker.network.bridge.enable_icc=false \
restricted-net
# 即使在同一网络中,容器之间也无法直接通信
docker run -d --name svc1 --network restricted-net nginx:alpine
docker run -d --name svc2 --network restricted-net alpine sleep 3600
docker exec svc2 ping -c 1 svc1
# 无法ping通(ICC被禁用)
# 这种模式适用于需要严格隔离的场景
# 容器只能通过端口映射与外部通信
3. 使用iptables进行细粒度控制
# 可以通过自定义iptables规则限制容器间的访问
# 例如: 只允许app容器访问mysql容器的3306端口
# 获取容器IP
APP_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' app)
MYSQL_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mysql)
# 默认拒绝app到mysql的所有流量
iptables -I DOCKER-USER -s $APP_IP -d $MYSQL_IP -j DROP
# 只允许3306端口
iptables -I DOCKER-USER -s $APP_IP -d $MYSQL_IP -p tcp --dport 3306 -j ACCEPT
# 验证: app可以访问mysql的3306端口
docker exec app mysql -h $MYSQL_IP -u root -psecret -e "SELECT 1"
# 验证: app无法ping mysql (ICMP被拒绝)
docker exec app ping -c 1 $MYSQL_IP
# 不通
5.5 服务发现机制(Embedded DNS)
Docker的服务发现机制是其网络功能的核心特性之一。以下是服务发现的详细工作流程:
┌────────────────────────────────────────────────────────┐
│ Docker Daemon │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 内置DNS服务器 (127.0.0.11) │ │
│ │ │ │
│ │ DNS注册表: │ │
│ │ ┌─────────────┬──────────────┬────────────────┐ │ │
│ │ │ 容器名/别名 │ IP地址 │ 所属网络 │ │ │
│ │ ├─────────────┼──────────────┼────────────────┤ │ │
│ │ │ web-server │ 172.52.0.2 │ my-net │ │ │
│ │ │ api-server │ 172.52.0.3 │ my-net │ │
│ │ │ db │ 172.52.0.4 │ my-net │ │ │
│ │ │ cache │ 172.52.0.4 │ my-net (别名) │ │ │
│ │ └─────────────┴──────────────┴────────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
# 深入理解DNS服务发现
# 创建网络并启动容器
docker network create sd-demo
docker run -d --name svc1 --network sd-demo --network-alias service nginx:alpine
docker run -d --name svc2 --network sd-demo --network-alias service nginx:alpine
# 当多个容器使用相同的别名时,DNS会返回多个IP
docker run -it --rm --network sd-demo alpine sh -c "nslookup service"
# 输出示例:
# Server: 127.0.0.11
# Address: 127.0.0.11:53
#
# Name: service
# Address 1: 172.52.0.2 svc1.sd-demo
# Address 2: 172.52.0.3 svc2.sd-demo
# 这实现了简单的DNS轮询负载均衡
# 每次解析可能返回不同的IP顺序
docker run -it --rm --network sd-demo alpine sh -c "for i in 1 2 3 4; do nslookup service | grep Address | tail -1; done"
# 清理
docker rm -f svc1 svc2
docker network rm sd-demo
服务发现的特性:
- 自动注册: 容器启动时自动注册到DNS,停止时自动注销
- 实时更新: 容器IP变化时,DNS记录自动更新
- 网络作用域: DNS解析仅在容器所属的网络内有效
- 别名支持: 容器可以通过容器名或网络别名被解析
- 外部DNS转发: 非容器名的查询会被转发到外部DNS服务器
5.6 容器间通信实战: 前端+后端+数据库
完整的微服务通信实战:
# 创建专用网络
docker network create microservice-net
# 1. 启动数据库(PostgreSQL)
docker run -d \
--name postgres-db \
--network microservice-net \
--network-alias postgres \
--network-alias db \
-e POSTGRES_DB=myapp \
-e POSTGRES_USER=appuser \
-e POSTGRES_PASSWORD=apppass \
postgres:15-alpine
# 2. 创建后端API镜像
mkdir -p /tmp/backend-svc
cat > /tmp/backend-svc/package.json << 'EOF'
{
"name": "backend",
"version": "1.0.0",
"main": "app.js",
"dependencies": {
"express": "^4.18.0",
"pg": "^8.11.0"
}
}
EOF
cat > /tmp/backend-svc/app.js << 'JSEOF'
const express = require('express');
const { Pool } = require('pg');
const app = express();
const port = 3000;
const pool = new Pool({
host: 'db', // 通过DNS服务发现连接数据库
user: 'appuser',
password: 'apppass',
database: 'myapp'
});
app.get('/health', (req, res) => {
res.json({ status: 'ok', service: 'backend' });
});
app.get('/api/users', async (req, res) => {
try {
// 创建表(如果不存在)
await pool.query(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
)
`);
// 插入示例数据
await pool.query(`
INSERT INTO users (name, email) VALUES
('Alice', 'alice@example.com'),
('Bob', 'bob@example.com')
ON CONFLICT DO NOTHING
`);
const result = await pool.query('SELECT * FROM users');
res.json({
users: result.rows,
hostname: require('os').hostname()
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.listen(port, '0.0.0.0', () => {
console.log(`Backend API running on port ${port}`);
});
JSEOF
cat > /tmp/backend-svc/Dockerfile << 'EOF'
FROM node:18-alpine
WORKDIR /app
COPY package.json .
RUN npm install --production
COPY app.js .
CMD ["node", "app.js"]
EOF
docker build -t backend-svc /tmp/backend-svc/
# 3. 启动后端API
docker run -d \
--name backend \
--network microservice-net \
--network-alias api \
backend-svc
# 4. 创建前端Nginx配置
mkdir -p /tmp/frontend-svc
cat > /tmp/frontend-svc/nginx.conf << 'NGINXEOF'
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://api:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
NGINXEOF
cat > /tmp/frontend-svc/index.html << 'HTMLEOF'
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Microservice Demo</title>
<style>
body { font-family: Arial; max-width: 800px; margin: 50px auto; }
.card { background: #f5f5f5; padding: 20px; margin: 10px 0; border-radius: 8px; }
pre { background: #333; color: #0f0; padding: 15px; border-radius: 5px; }
</style>
</head>
<body>
<h1>Microservice Demo</h1>
<div class="card">
<h2>Backend Health</h2>
<pre id="health">Loading...</pre>
</div>
<div class="card">
<h2>Users from Database</h2>
<pre id="users">Loading...</pre>
</div>
<script>
fetch('/api/health')
.then(r => r.json())
.then(d => document.getElementById('health').textContent = JSON.stringify(d, null, 2))
.catch(e => document.getElementById('health').textContent = 'Error: ' + e);
fetch('/api/users')
.then(r => r.json())
.then(d => document.getElementById('users').textContent = JSON.stringify(d, null, 2))
.catch(e => document.getElementById('users').textContent = 'Error: ' + e);
</script>
</body>
</html>
HTMLEOF
cat > /tmp/frontend-svc/Dockerfile << 'EOF'
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY index.html /usr/share/nginx/html/index.html
EOF
docker build -t frontend-svc /tmp/frontend-svc/
# 5. 启动前端
docker run -d \
--name frontend \
--network microservice-net \
-p 8080:80 \
frontend-svc
# 6. 验证整个应用链路
# 测试后端健康检查
docker exec frontend wget -qO- http://api:3000/health
# {"status":"ok","service":"backend"}
# 测试后端到数据库的连接
docker exec frontend wget -qO- http://api:3000/api/users
# {"users":[{"id":1,"name":"Alice","email":"alice@example.com"},...],"hostname":"..."}
# 从宿主机访问完整应用
curl http://localhost:8080/api/health
curl http://localhost:8080/api/users
# 7. 查看网络拓扑
docker network inspect microservice-net --format '
Network: {{.Name}}
Driver: {{.Driver}}
Subnet: {{range .IPAM.Config}}{{.Subnet}}{{end}}
Containers:
{{range .Containers}} - {{.Name}}: {{.IPv4Address}}
{{end}}'
# 输出:
# Network: microservice-net
# Driver: bridge
# Subnet: 172.53.0.0/16
# Containers:
# - frontend: 172.53.0.5/16
# - backend: 172.53.0.4/16
# - postgres-db: 172.53.0.2/16
# 清理
docker rm -f frontend backend postgres-db
docker network rm microservice-net
5.7 负载均衡与容器
在Docker中实现负载均衡主要有以下几种方式:
方式一: DNS轮询
# 多个容器使用相同的网络别名
docker network create lb-net
# 启动3个后端实例,都使用别名"backend"
for i in 1 2 3; do
docker run -d --name backend-$i \
--network lb-net \
--network-alias backend \
nginx:alpine
done
# DNS查询会返回多个IP
docker run -it --rm --network lb-net alpine nslookup backend
# Address 1: 172.54.0.2 backend-1.lb-net
# Address 2: 172.54.0.3 backend-2.lb-net
# Address 3: 172.54.0.4 backend-3.lb-net
# 客户端每次解析可能获得不同的IP(DNS轮询)
# 但这种方式不能保证均匀分配,且无法处理健康检查
# 清理
docker rm -f backend-1 backend-2 backend-3
docker network rm lb-net
方式二: Nginx反向代理负载均衡
docker network create nginx-lb-net
# 启动3个后端
for i in 1 2 3; do
docker run -d --name web-$i \
--network nginx-lb-net \
--network-alias web-$i \
-e SERVER_ID=$i \
nginx:alpine
done
# 创建Nginx负载均衡配置
cat > /tmp/nginx-lb.conf << 'EOF'
upstream backend {
server web-1:80;
server web-2:80;
server web-3:80;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
EOF
# 启动Nginx负载均衡器
docker run -d --name load-balancer \
--network nginx-lb-net \
-p 8080:80 \
-v /tmp/nginx-lb.conf:/etc/nginx/conf.d/default.conf \
nginx:alpine
# 多次请求会分发到不同的后端
for i in $(seq 1 9); do
curl -s http://localhost:8080/ | head -1
done
# 清理
docker rm -f load-balancer web-1 web-2 web-3
docker network rm nginx-lb-net
方式三: Docker Swarm的内置负载均衡
# Swarm模式下,服务自动提供VIP负载均衡
docker swarm init
docker network create -d overlay swarm-lb-net
# 创建有3个副本的服务
docker service create --name web --replicas 3 --network swarm-lb-net -p 8080:80 nginx
# Swarm自动提供VIP负载均衡
# 访问服务名会自动分发到不同副本
# 这比DNS轮询更可靠,支持健康检查
# 清理
docker service rm web
docker network rm swarm-lb-net
docker swarm leave -f
5.8 容器间通信故障排查
当容器间通信出现问题时,可以按以下步骤排查:
# === 故障排查步骤 ===
# 步骤1: 确认容器是否在同一网络
docker inspect -f '{{json .NetworkSettings.Networks}}' container-a | python3 -m json.tool
docker inspect -f '{{json .NetworkSettings.Networks}}' container-b | python3 -m json.tool
# 如果网络名称不同,容器无法通过容器名通信
# 步骤2: 检查DNS解析
docker exec container-a nslookup container-b
# 如果返回 "server can't find",说明DNS有问题
# 检查/etc/resolv.conf是否指向127.0.0.11
docker exec container-a cat /etc/resolv.conf
# 步骤3: 检查网络连通性
docker exec container-a ping -c 2 <container-b的IP>
# 如果ping不通,检查iptables规则
# 步骤4: 检查端口是否监听
docker exec container-b netstat -tlnp
# 或
docker exec container-b ss -tlnp
# 确认服务是否在正确地址监听(0.0.0.0而非127.0.0.1)
# 步骤5: 检查iptables规则
iptables -t filter -L DOCKER-USER -n -v
iptables -t filter -L FORWARD -n -v
# 步骤6: 使用tcpdump抓包
# 在宿主机的网桥上抓包
docker network inspect my-net -f '{{range .Options}}{{.}}{{end}}'
# 获取网桥名称,如br-xxxx
tcpdump -i br-xxxx -n host <container-a-ip> and host <container-b-ip>
# 步骤7: 检查容器是否正常运行
docker ps | grep container-b
docker logs container-b
# === 常见问题及解决方案 ===
# 问题1: "ping: bad address 'container-name'"
# 原因: 使用了默认bridge网络(不支持DNS)或容器不在同一网络
# 解决: 使用自定义bridge网络
# 问题2: "Connection refused"
# 原因: 目标端口没有服务监听,或服务绑定在127.0.0.1
# 解决: 确保服务绑定在0.0.0.0
# 问题3: 间歇性连接失败
# 原因: 可能是MTU不匹配
# 解决: 检查并调整MTU
docker exec container-a ip link show eth0
# 如果MTU不一致,创建网络时指定MTU
docker network create -o com.docker.network.mtu=1400 my-net
# 问题4: 连接超时
# 原因: 可能是iptables规则阻止,或路由问题
# 解决: 检查iptables和路由表
iptables -t filter -L FORWARD -n -v
docker exec container-a route -n
容器间通信的安全考量深入分析:
容器间通信虽然方便,但也带来了安全风险。在同一bridge网络中,默认情况下所有容器都可以无限制地互相访问对方的任何端口。这意味着如果其中一个容器被攻破,攻击者可以将其作为跳板,访问同一网络中的数据库、缓存等敏感服务。
网络分段策略: 安全的最佳实践是按照最小权限原则进行网络分段。具体做法是为不同安全级别的服务创建不同的网络,只有需要通信的容器才连接到同一网络。例如,前端应用和数据库不应该在同一网络中——前端只需要与后端API通信,后端API才需要访问数据库。通过将前端、后端、数据库分别放在不同的网络中,即使前端被攻破,攻击者也无法直接访问数据库。
# 安全的网络分段架构
# 创建三个网络,模拟DMZ、应用层和数据层
docker network create --subnet 172.60.0.0/24 dmz-net
docker network create --subnet 172.60.1.0/24 app-net
docker network create --subnet 172.60.2.0/24 --internal data-net
# 数据库只在data-net中(内部网络,无法访问外网)
docker run -d --name mysql-db \
--network data-net \
--ip 172.60.2.10 \
-e MYSQL_ROOT_PASSWORD=secret \
mysql:8.0
# 后端API连接app-net和data-net
docker run -d --name api-server \
--network app-net \
--ip 172.60.1.10 \
my-api-image
docker network connect --ip 172.60.2.20 data-net api-server
# 前端连接dmz-net和app-net
docker run -d --name web-frontend \
--network dmz-net \
--ip 172.60.0.10 \
-p 80:80 \
my-web-image
docker network connect --ip 172.60.1.20 app-net web-frontend
# 安全验证: 前端无法直接访问数据库
docker exec web-frontend ping -c 1 mysql-db
# ping: bad address 'mysql-db' (无法解析,不在同一网络)
docker exec web-frontend ping -c 1 172.60.2.10
# ping: sendto: Network is unreachable (路由不可达)
# 安全验证: 数据库无法访问外网
docker exec mysql-db ping -c 1 8.8.8.8
# ping: connect: Network is unreachable (内部网络限制)
容器间通信的流量加密: 在某些安全要求极高的场景中,即使容器间通信被限制在同一网络内,也需要对通信流量进行加密。Docker原生的bridge网络不提供流量加密功能,但overlay网络支持通过--opt encrypted选项启用IPSec加密。对于bridge网络中需要加密通信的场景,可以在应用层使用TLS/mTLS来保护通信安全,或者使用第三方网络插件(如Calico)提供的网络加密功能。
# overlay网络加密
docker network create -d overlay --opt encrypted secure-overlay-net
# 启用加密后,跨主机的VXLAN流量会被IPSec加密
# 注意: 加密会带来约10%-20%的性能开销
# 在应用层使用mTLS保护bridge网络中的通信
# 示例: 后端API配置mTLS
docker run -d --name secure-api \
--network app-net \
-v /etc/ssl/certs/api-cert.pem:/certs/cert.pem:ro \
-v /etc/ssl/private/api-key.pem:/certs/key.pem:ro \
-v /etc/ssl/certs/ca-cert.pem:/certs/ca.pem:ro \
-e TLS_CERT=/certs/cert.pem \
-e TLS_KEY=/certs/key.pem \
-e TLS_CA=/certs/ca.pem \
-e REQUIRE_CLIENT_CERT=true \
my-secure-api
第六章 端口映射详解
端口映射是让外部网络能够访问容器内服务的关键机制。在bridge网络模式下,容器的IP地址在外部网络中不可路由,必须通过端口映射将宿主机的端口与容器的端口绑定。本章将全面讲解Docker端口映射的各种用法和底层原理。
6.1 -p参数完整语法
-p(或--publish)参数的完整语法为:
-p [宿主机IP:]宿主机端口[:容器端口[/协议]]
最常用的形式:
# 基本端口映射: 宿主机端口:容器端口
docker run -d -p 8080:80 nginx
# 访问宿主机的8080端口 → 容器的80端口
# 验证
curl http://localhost:8080
# 宿主机端口和容器端口相同
docker run -d -p 80:80 nginx
# 访问宿主机的80端口 → 容器的80端口
# 验证
curl http://localhost:80
6.2 指定IP地址的端口映射
默认情况下,端口映射绑定到宿主机的所有IP地址(0.0.0.0)。可以指定绑定的IP地址:
# 指定绑定的IP地址
# 格式: IP:宿主机端口:容器端口
docker run -d -p 127.0.0.1:8080:80 nginx
# 只有通过127.0.0.1才能访问,外部无法访问
# 验证
curl http://127.0.0.1:8080 # 成功
curl http://0.0.0.0:8080 # 也成功(0.0.0.0包含127.0.0.1)
curl http://192.168.1.10:8080 # 失败(没有绑定到外部IP)
# 绑定到特定网卡IP
docker run -d -p 192.168.1.10:8081:80 nginx
# 只有通过192.168.1.10才能访问
# 验证
curl http://192.168.1.10:8081 # 成功
curl http://127.0.0.1:8081 # 失败
# 查看端口绑定情况
docker port <容器名>
# 80/tcp -> 192.168.1.10:8081
指定IP的应用场景:
- 多网卡服务器,只想在特定网卡上暴露服务
- 安全要求,某些服务只允许本地访问(绑定127.0.0.1)
- 内网/外网分离,不同服务绑定不同网卡
# 实战: 多网卡服务器的端口分配
# 假设服务器有两个网卡:
# eth0: 192.168.1.10 (内网)
# eth1: 10.0.0.10 (管理网)
# Web服务只在内网暴露
docker run -d -p 192.168.1.10:80:80 --name web-internal nginx
# 管理服务只在管理网暴露
docker run -d -p 10.0.0.10:8080:8080 --name admin-service myadmin
# 数据库服务只允许本地访问
docker run -d -p 127.0.0.1:3306:3306 --name mysql mysql:8.0
6.3 随机端口映射(-P大写)
使用大写-P参数,Docker会自动将容器的暴露端口映射到宿主机的随机高端口(49000-49900):
# 随机端口映射
docker run -d -P --name nginx-random nginx
# 查看映射的端口
docker port nginx-random
# 80/tcp -> 0.0.0.0:49153
# 访问
curl http://localhost:49153
# 查看随机分配的端口
docker inspect -f '{{range $p, $conf := .NetworkSettings.Ports}}{{$p}} -> {{$conf}}{{"\n"}}{{end}}' nginx-random
# 80/tcp -> [{0.0.0.0 49153}]
# -P映射容器EXPOSE的所有端口
# Dockerfile中EXPOSE的端口会被映射
docker run -d -P --name multi-port myapp
# 如果Dockerfile中有: EXPOSE 80 443 8080
# 则三个端口都会被随机映射
# 查看所有映射
docker port multi-port
# 80/tcp -> 0.0.0.0:49154
# 443/tcp -> 0.0.0.0:49155
# 8080/tcp -> 0.0.0.0:49156
随机端口映射的应用场景:
- 开发测试环境,避免端口冲突
- 动态扩容,自动分配端口
- 与服务发现系统配合,动态注册端口
6.4 UDP端口映射
默认情况下,端口映射使用TCP协议。可以通过在端口后加/udp来映射UDP端口:
# 映射UDP端口
docker run -d -p 53:53/udp --name dns-server dnsmasq
# 同时映射TCP和UDP
docker run -d \
-p 53:53/tcp \
-p 53:53/udp \
--name dns-server \
dnsmasq
# 随机映射UDP端口
docker run -d -P --name dns-server dnsmasq
# docker port会显示TCP和UDP的映射
docker port dns-server
# 53/tcp -> 0.0.0.0:49157
# 53/udp -> 0.0.0.0:49158
UDP端口映射的特殊性: UDP是无连接的协议,与TCP的端口映射在底层实现上有一些差异。TCP端口映射依赖iptables的DNAT规则和conntrack连接追踪来维护连接状态,而UDP端口映射虽然也使用DNAT规则,但由于UDP没有连接建立过程,conntrack对UDP连接的追踪基于源地址、目标地址和端口的五元组,超时时间较短(默认30秒)。这意味着UDP流量如果长时间没有数据传输,conntrack中的连接记录会过期,后续的数据包可能会被当作新连接处理,导致映射失败。对于需要长时间保持UDP连接的应用(如VPN、视频流),可能需要调整conntrack的UDP超时时间。
# 实战: DNS服务器
docker run -d \
--name my-dns \
-p 53:53/udp \
-p 53:53/tcp \
-e DNS_SERVER=8.8.8.8 \
andyshinn/dnsmasq
# 测试DNS解析
dig @localhost example.com
# 查看iptables中的UDP映射规则
iptables -t nat -L DOCKER -n -v | grep udp
# 查看UDP conntrack超时时间
cat /proc/sys/net/netfilter/nf_conntrack_udp_timeout
# 默认30秒
# 对于需要长时间保持UDP连接的应用,增大超时时间
sysctl -w net.netfilter.nf_conntrack_udp_timeout=180
# 将UDP超时时间延长到180秒
# 实战: WebRTC信令服务器(需要UDP端口映射)
docker run -d \
--name webrtc-server \
-p 8080:8080/tcp \
-p 10000-10010:10000-10010/udp \
-e TURN_PORT=10000 \
my-webrtc-image
# 验证UDP端口映射是否正常工作
# 从另一台机器发送UDP包
echo "test" | nc -u <宿主机IP> 53
# 在宿主机上抓包验证
tcpdump -i docker0 -n udp port 53
6.5 端口范围映射
Docker不支持直接映射端口范围(如-p 8000-8100:8000-8100),但可以通过多次使用-p参数来实现:
# 映射多个连续端口
docker run -d \
-p 3000:3000 \
-p 3001:3001 \
-p 3002:3002 \
-p 3003:3003 \
-p 3004:3004 \
--name multi-port-app \
myapp
# 使用循环创建多个端口映射(在docker-compose中更方便)
# docker-compose.yml示例:
cat > /tmp/docker-compose-ports.yml << 'EOF'
version: '3.8'
services:
app:
image: myapp
ports:
- "3000-3004:3000-3004"
# Docker Compose支持端口范围映射
EOF
Docker Compose中的端口范围映射:
# docker-compose.yml
version: '3.8'
services:
web:
image: nginx
ports:
- "8080:80" # 单端口
- "8443:443" # 单端口
- "9000-9010:9000-9010" # 端口范围
- "127.0.0.1:3306:3306" # 指定IP
- "53:53/udp" # UDP端口
6.6 多端口映射
一个容器可以映射多个端口:
# 映射多个端口
docker run -d \
--name web-app \
-p 80:80 \
-p 443:443 \
-p 8080:8080 \
-p 127.0.0.1:3306:3306 \
nginx
# 查看所有端口映射
docker port web-app
# 80/tcp -> 0.0.0.0:80
# 443/tcp -> 0.0.0.0:443
# 8080/tcp -> 0.0.0.0:8080
# 3306/tcp -> 127.0.0.1:3306
# 查看端口映射详情
docker inspect web-app --format '{{json .NetworkSettings.Ports}}' | python3 -m json.tool
# 实战: Web应用 + SSH + 数据库
docker run -d \
--name full-stack \
-p 80:80 \ # HTTP
-p 443:443 \ # HTTPS
-p 2222:22 \ # SSH (映射到2222避免冲突)
-p 127.0.0.1:3306:3306 \ # MySQL (只允许本地)
-p 127.0.0.1:6379:6379 \ # Redis (只允许本地)
my-full-stack-app
6.7 端口映射查看(docker port)
docker port命令用于查看容器的端口映射情况:
# 查看容器的所有端口映射
docker port web-app
# 查看特定端口的映射
docker port web-app 80
# 0.0.0.0:80
# 查看特定协议的端口映射
docker port web-app 53/udp
# 0.0.0.0:53
# 使用docker inspect查看更详细的信息
docker inspect web-app --format '{{range $p, $conf := .NetworkSettings.Ports}}{{$p}} -> {{range $conf}}{{.HostIp}}:{{.HostPort}} {{end}}{{"\n"}}{{end}}'
# 输出示例:
# 80/tcp -> 0.0.0.0:80
# 443/tcp -> 0.0.0.0:443
# 3306/tcp -> 127.0.0.1:3306
6.8 端口冲突解决
当多个容器尝试映射到宿主机的同一个端口时,会产生端口冲突:
# 端口冲突演示
docker run -d -p 8080:80 --name web1 nginx
docker run -d -p 8080:80 --name web2 nginx
# 错误: Bind for 0.0.0.0:8080 failed: port is already allocated
# 解决方案1: 使用不同的宿主机端口
docker run -d -p 8081:80 --name web2 nginx # 改用8081
# 解决方案2: 绑定到不同的IP
docker run -d -p 127.0.0.1:8080:80 --name web2 nginx # 绑定到localhost
# 但注意: 0.0.0.0:8080已经占用了所有IP,所以这也会失败
# 解决方案3: 使用随机端口
docker run -d -P --name web2 nginx # Docker自动选择空闲端口
# 解决方案4: 使用host模式(不推荐,失去隔离)
# 但要注意容器内服务端口也不能与宿主机冲突
检查端口占用:
# 查看宿主机端口占用
ss -tlnp | grep 8080
# 或
netstat -tlnp | grep 8080
# 查看Docker占用的端口
docker ps --format "table {{.Names}}\t{{.Ports}}"
# 查看所有容器的端口映射
docker ps -a --format "{{.Names}}: {{.Ports}}"
# 找到占用特定端口的容器
docker ps --format "{{.ID}} {{.Names}} {{.Ports}}" | grep 8080
6.9 iptables规则分析(Docker端口映射相关规则)
端口映射的底层实现完全依赖iptables。让我们详细分析Docker创建的iptables规则:
# 启动一个带端口映射的容器
docker run -d -p 8080:80 --name web nginx
# 查看nat表的所有规则
iptables -t nat -L -n -v --line-numbers
# Docker创建的NAT规则分为几个部分:
# 1. PREROUTING链 (处理外部进入的流量)
iptables -t nat -L PREROUTING -n -v
# 规则: -A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
# 含义: 目标地址类型为LOCAL的流量跳转到DOCKER链
# 2. DOCKER链 (实际执行DNAT)
iptables -t nat -L DOCKER -n -v
# 规则: -A DOCKER ! -i docker0 -p tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
# 含义: 不是从docker0进入的TCP流量,目标端口8080,DNAT到容器172.17.0.2:80
# 3. POSTROUTING链 (处理出站流量,MASQUERADE)
iptables -t nat -L POSTROUTING -n -v
# 规则1: -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
# 含义: 来自docker0子网的流量,不从docker0出去的,MASQUERADE
# 规则2: -A POSTROUTING -s 172.17.0.2/32 -d 172.17.0.2/32 -p tcp --dport 80 -j MASQUERADE
# 含义: hairpin NAT规则
# 4. OUTPUT链 (处理本机产生的流量)
iptables -t nat -L OUTPUT -n -v
# 规则: -A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
# 含义: 本机产生的,目标为LOCAL地址的流量跳转到DOCKER链
# 5. filter表的FORWARD链 (控制转发)
iptables -t filter -L FORWARD -n -v
# 规则1: -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# 含义: 允许已建立连接的回包
# 规则2: -A FORWARD -o docker0 -j DOCKER
# 含义: 转发到docker0的流量跳转到DOCKER链
# 规则3: -A FORWARD -i docker0 ! -o docker0 -j ACCEPT
# 含义: 从docker0出来但不回到docker0的流量允许通过
# 规则4: -A FORWARD -i docker0 -o docker0 -j ACCEPT
# 含义: docker0内部的流量允许通过(容器间通信)
# 6. filter表的DOCKER链 (容器访问控制)
iptables -t filter -L DOCKER -n -v
# 规则: -A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp --dport 80 -j ACCEPT
# 含义: 允许到容器172.17.0.2:80的TCP流量
完整的端口映射数据流:
外部客户端 (203.0.113.5:54321)
│
▼ HTTP请求 → 宿主机:8080
│
├─→ PREROUTING链 (nat表)
│ └─→ DOCKER链 (nat表)
│ └─→ DNAT: 目标 0.0.0.0:8080 → 172.17.0.2:80
│
├─→ FORWARD链 (filter表)
│ ├─→ DOCKER链 (filter表)
│ │ └─→ ACCEPT (允许到172.17.0.2:80)
│ └─→ 转发到docker0网桥
│
├─→ docker0 → veth pair → 容器eth0:80
│
├─→ nginx处理请求,返回响应
│
└─→ 响应路径: 容器172.17.0.2:80 → docker0 → 宿主机
└─→ POSTROUTING链: 连接追踪自动反向NAT
源 172.17.0.2:80 → 0.0.0.0:8080
└─→ 返回给客户端 203.0.113.5:54321
6.10 firewalld与Docker的兼容性问题
在CentOS/RHEL等使用firewalld的系统上,Docker与firewalld可能存在兼容性问题:
问题原因: Docker直接操作iptables,而firewalld也管理iptables。当firewalld重新加载规则时,可能会覆盖Docker创建的iptables规则,导致端口映射失效。
# 检查firewalld状态
systemctl status firewalld
# 检查Docker状态
systemctl status docker
# 当firewalld重启时,Docker的iptables规则可能丢失
systemctl restart firewalld
# 此时容器端口映射可能失效!
# 验证
curl http://localhost:8080
# 可能连接失败
解决方案1: 重启Docker服务(最简单)
# 重启firewalld后,重启Docker以重建iptables规则
systemctl restart docker
# 验证端口映射恢复
curl http://localhost:8080
# 恢复正常
解决方案2: 使用firewalld的docker zone
# 创建docker zone
firewall-cmd --permanent --new-zone=docker
firewall-cmd --permanent --zone=docker --add-interface=docker0
firewall-cmd --permanent --zone=docker --add-masquerade
firewall-cmd --permanent --zone=docker --add-port=8080/tcp
firewall-cmd --reload
解决方案3: 配置Docker使用firewalld
# 修改Docker配置
# /etc/docker/daemon.json
{
"iptables": false # 禁止Docker管理iptables
}
# 但这需要手动配置所有iptables规则,不推荐
# 更好的方案: 让Docker和firewalld共存
# 在/usr/lib/systemd/system/docker.service中确保Docker在firewalld之后启动
# After=firewalld.service
解决方案4: 使用nftables后端(推荐)
# 新版firewalld默认使用nftables而非iptables
# 检查firewalld后端
firewall-cmd --info-zone=public | grep -i iptables
# 如果使用iptables后端,可以切换到nftables
# /etc/firewalld/firewalld.conf
FirewallBackend=nftables
# 重启firewalld
systemctl restart firewalld
# 重启Docker
systemctl restart docker
生产环境建议:
- 在Docker主机上,尽量使用iptables直接管理防火墙,而非firewalld
- 如果必须使用firewalld,确保Docker在firewalld之后启动
- 修改firewalld规则后,重启Docker以重建iptables规则
- 或者使用
--reload命令而非完全重启firewalld
# 安全的firewalld操作流程
# 1. 添加规则
firewall-cmd --permanent --add-port=8080/tcp
# 2. 重载(不完全重启)
firewall-cmd --reload
# 3. 如果端口映射仍然失效,重启Docker
systemctl restart docker
# 4. 验证
docker port web
curl http://localhost:8080
iptables规则链的完整分析: Docker在iptables中创建了多条自定义链来管理网络规则。理解这些链的层次关系对于排查端口映射问题至关重要。Docker创建的主要链包括DOCKER、DOCKER-USER、DOCKER-ISOLATION-STAGE-1和DOCKER-ISOLATION-STAGE-2。其中DOCKER-USER链是专门为用户自定义规则保留的,Docker不会修改这个链中的规则,用户可以在这里添加自己的安全规则来控制容器流量。
# 查看Docker创建的所有自定义链
iptables -t nat -L | grep "Chain DOCKER"
iptables -t filter -L | grep "Chain DOCKER"
# 输出示例:
# Chain DOCKER (2 references)
# Chain DOCKER-ISOLATION-STAGE-1 (1 references)
# Chain DOCKER-ISOLATION-STAGE-2 (9 references)
# Chain DOCKER-USER (1 references)
# DOCKER-USER链的内容(默认为空,允许所有流量)
iptables -t filter -L DOCKER-USER -n -v
# Chain DOCKER-USER (1 references)
# pkts bytes target prot opt in out source destination
# 100 6000 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
# 在DOCKER-USER链中添加自定义安全规则
# 例如: 禁止外部IP 203.0.113.50访问容器
iptables -I DOCKER-USER -i eth0 -s 203.0.113.50 -j DROP
# 例如: 只允许特定IP段访问容器
iptables -I DOCKER-USER -i eth0 ! -s 192.168.1.0/24 -j DROP
# 注意: 此规则会阻止所有非192.168.1.0/24的外部流量访问容器
# DOCKER-ISOLATION-STAGE-1和STAGE-2链用于网络隔离
# 它们确保不同bridge网络之间的流量被丢弃
iptables -t filter -L DOCKER-ISOLATION-STAGE-1 -n -v
# 规则: 从br-xxx出去但不到br-xxx的流量,跳转到STAGE-2
# STAGE-2: 如果目标是其他bridge网络,则DROP
# 查看网络隔离规则的实际效果
docker network create net-a
docker network create net-b
docker run -d --name test-a --network net-a alpine sleep 3600
docker run -d --name test-b --network net-b alpine sleep 3600
# 尝试从net-a访问net-b的容器IP(应该失败)
A_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' test-a)
B_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' test-b)
docker exec test-a ping -c 1 $B_IP
# PING 172.x.x.x ... (无响应,被ISOLATION规则丢弃)
# 清理
docker rm -f test-a test-b
docker network rm net-a net-b
端口映射与conntrack表: Docker的端口映射依赖iptables的conntrack(连接追踪)模块来管理网络连接状态。每个经过NAT的网络连接都会在conntrack表中占据一个条目。当容器数量庞大或并发连接数很高时,conntrack表可能会被填满,导致新的连接无法建立。这是一个在生产环境中常见但容易被忽视的问题。
# 查看conntrack表使用情况
cat /proc/sys/net/netfilter/nf_conntrack_count
# 输出: 15234 (当前连接数)
cat /proc/sys/net/netfilter/nf_conntrack_max
# 输出: 65536 (最大连接数)
# 计算使用率
echo "scale=2; $(cat /proc/sys/net/netfilter/nf_conntrack_count) / $(cat /proc/sys/net/netfilter/nf_conntrack_max) * 100" | bc
# 输出: 23.24%
# 如果使用率超过80%,需要增大conntrack表
echo 262144 > /proc/sys/net/netfilter/nf_conntrack_max
# 或在sysctl.conf中永久设置
# net.netfilter.nf_conntrack_max = 262144
# 查看conntrack表中的Docker相关条目
conntrack -L | grep "172.17" | head -20
# 可以看到容器相关的连接追踪记录
# 查看连接追踪超时时间
cat /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established
# 默认432000秒(5天),可以根据需要调整
第七章 overlay跨主机网络
当容器部署在多台宿主机上时,跨主机容器通信成为必须解决的问题。Docker的overlay网络模式通过VXLAN隧道技术,在不同宿主机的容器之间建立虚拟的二层网络,实现了跨主机容器通信和服务发现。本章将深入讲解overlay网络的原理和实践。
7.1 overlay网络工作原理(VXLAN隧道)
overlay网络在现有网络(underlay)之上构建一个虚拟网络(overlay)。Docker使用VXLAN(Virtual Extensible LAN)技术来封装和传输容器间的数据。
VXLAN基本概念:
- VNI (VXLAN Network Identifier): 24位的网络标识符,支持1600万个虚拟网络
- VTEP (VXLAN Tunnel Endpoint): VXLAN隧道的端点,负责封装和解封装
- 封装: 将原始的二层以太网帧封装在UDP包中,通过三层网络传输
┌──────────────────────────────────────────────────────────────┐
│ Overlay Network (VXLAN) │
│ │
│ 容器A 容器B 容器C 容器D │
│ 10.0.0.2 10.0.0.3 10.0.0.4 10.0.0.5 │
│ ─────────────────────────────────────────────────────────── │
│ VNI: 256 (同一个VXLAN网络) │
│ ─────────────────────────────────────────────────────────── │
│ VTEP │ VTEP │
│ ┌─────────┐ │ ┌─────────┐ │
│ │ Host A │◄──── VXLAN隧道(UDP 4789) ────►│ Host B │ │
│ │.1.10 │ │ │.1.11 │ │
│ └─────────┘ │ └─────────┘ │
│ │
├──────────────────────────────────────────────────────────────┤
│ Underlay Network (物理网络) │
│ 192.168.1.0/24 │
└──────────────────────────────────────────────────────────────┘
数据封装过程:
原始以太网帧 (容器A → 容器C):
┌──────────────────────────────────────┐
│ 目标MAC: 容器C的MAC │
│ 源MAC: 容器A的MAC │
│ 目标IP: 10.0.0.4 │
│ 源IP: 10.0.0.2 │
│ 数据... │
└──────────────────────────────────────┘
│
▼ VXLAN封装
┌──────────────────────────────────────────────────────────────┐
│ 外层以太网头: 目标MAC=HostB网卡MAC, 源MAC=HostA网卡MAC │
│ 外层IP头: 目标IP=192.168.1.11, 源IP=192.168.1.10 │
│ UDP头: 目标端口=4789, 源端口=随机 │
│ VXLAN头: VNI=256 │
│ ┌──────────────────────────────────────────┐ │
│ │ 原始以太网帧 (容器A → 容器C) │ │
│ └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
7.2 overlay网络前提条件
创建overlay网络需要满足以下前提条件:
-
集群模式或key-value存储:
- Docker Swarm模式(推荐)
- 外部key-value存储(etcd/Consul/ZooKeeper)
-
主机间网络互通: 所有宿主机之间必须可以通过三层网络通信
-
防火墙规则开放: 需要开放以下端口:
- TCP/UDP 2377: 集群管理通信
- TCP/UDP 7946: 节点间通信
- UDP 4789: VXLAN数据传输
-
内核支持: Linux内核需要支持VXLAN模块(3.12+)
# 检查内核VXLAN支持
modinfo vxlan
# 或
ls /lib/modules/$(uname -r)/kernel/drivers/net/vxlan.ko
# 检查VXLAN模块是否加载
lsmod | grep vxlan
# 如果没有加载,手动加载
modprobe vxlan
# 开放防火墙端口(如果使用firewalld)
firewall-cmd --permanent --add-port=2377/tcp
firewall-cmd --permanent --add-port=2377/udp
firewall-cmd --permanent --add-port=7946/tcp
firewall-cmd --permanent --add-port=7946/udp
firewall-cmd --permanent --add-port=4789/udp
firewall-cmd --reload
# 或使用iptables
iptables -I INPUT -p tcp --dport 2377 -j ACCEPT
iptables -I INPUT -p udp --dport 4789 -j ACCEPT
iptables -I INPUT -p tcp --dport 7946 -j ACCEPT
iptables -I INPUT -p udp --dport 7946 -j ACCEPT
7.3 使用Docker Swarm创建overlay网络
Docker Swarm模式是创建overlay网络最简单的方式:
# === 在管理节点上操作 ===
# 初始化Swarm集群
docker swarm init --advertise-addr 192.168.1.10
# 输出worker加入命令:
# docker swarm join --token SWMTKN-1-xxx 192.168.1.10:2377
# === 在工作节点上操作 ===
docker swarm join --token SWMTKN-1-xxx 192.168.1.10:2377
# === 回到管理节点 ===
# 查看集群节点
docker node ls
# ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
# xxx manager Ready Active Leader
# yyy worker1 Ready Active
# 创建overlay网络
docker network create -d overlay my-overlay-net
# 查看网络
docker network inspect my-overlay-net
# 输出关键信息:
# {
# "Name": "my-overlay-net",
# "Driver": "overlay",
# "Scope": "swarm",
# "IPAM": {
# "Config": [
# {
# "Subnet": "10.0.0.0/24", # 自动分配的子网
# "Gateway": "10.0.0.1"
# }
# ]
# }
# }
# 创建带自定义子网的overlay网络
docker network create -d overlay \
--subnet 10.10.0.0/16 \
--gateway 10.10.0.1 \
my-overlay-net2
# 创建加密的overlay网络(数据传输加密)
docker network create -d overlay --opt encrypted my-secure-net
# 创建attachable的overlay网络(允许手动docker run连接)
docker network create -d overlay --attachable my-attachable-net
创建overlay服务:
# 创建服务(自动在多节点上调度)
docker service create \
--name web-service \
--replicas 3 \
--network my-overlay-net \
-p 8080:80 \
nginx
# 查看服务
docker service ls
docker service ps web-service
# 在不同节点上查看容器
docker ps
# 测试跨主机容器通信
# 在一个容器中ping另一个节点上的容器
docker exec <容器ID> ping <其他容器名>
# 更新服务,添加网络别名
docker service update --network-add my-overlay-net --network-alias-rm web --network-alias-add frontend web-service
7.4 使用etcd作为后端创建overlay网络(非Swarm模式)
在非Swarm模式下,可以通过外部key-value存储创建overlay网络。以下使用etcd作为示例:
# === 环境准备 ===
# 三台主机:
# etcd-server: 192.168.1.100 (运行etcd)
# docker-host1: 192.168.1.10
# docker-host2: 192.168.1.11
# === 在etcd-server上安装并启动etcd ===
# 下载etcd
wget https://github.com/etcd-io/etcd/releases/download/v3.5.0/etcd-v3.5.0-linux-amd64.tar.gz
tar xzf etcd-v3.5.0-linux-amd64.tar.gz
cd etcd-v3.5.0-linux-amd64
# 启动etcd
./etcd \
--name etcd-0 \
--data-dir /var/lib/etcd \
--listen-client-urls http://0.0.0.0:2379 \
--advertise-client-urls http://192.168.1.100:2379 \
--listen-peer-urls http://0.0.0.0:2380 \
--initial-advertise-peer-urls http://192.168.1.100:2380 \
--initial-cluster etcd-0=http://192.168.1.100:2380 \
--initial-cluster-token etcd-cluster-1 \
--initial-cluster-state new
# 验证etcd
etcdctl --endpoints=http://192.168.1.100:2379 endpoint health
# === 在docker-host1和docker-host2上配置Docker ===
# 停止Docker
systemctl stop docker
# 修改Docker配置
# /etc/docker/daemon.json (docker-host1)
cat > /etc/docker/daemon.json << 'EOF'
{
"cluster-store": "etcd://192.168.1.100:2379",
"cluster-advertise": "192.168.1.10:2376"
}
EOF
# /etc/docker/daemon.json (docker-host2)
cat > /etc/docker/daemon.json << 'EOF'
{
"cluster-store": "etcd://192.168.1.100:2379",
"cluster-advertise": "192.168.1.11:2376"
}
EOF
# 启动Docker
systemctl start docker
# === 创建overlay网络 ===
# 在docker-host1上创建overlay网络
docker network create -d overlay \
--subnet 10.20.0.0/16 \
multi-host-net
# 在docker-host2上可以看到相同的网络(通过etcd同步)
docker network ls
# NETWORK ID NAME DRIVER SCOPE
# xxx multi-host-net overlay global
# === 启动容器 ===
# 在docker-host1上启动容器
docker run -d --name container-1 --network multi-host-net nginx
# 在docker-host2上启动容器
docker run -d --name container-2 --network multi-host-net nginx
# === 测试跨主机通信 ===
# 在docker-host1上的容器访问docker-host2上的容器
docker exec container-1 ping -c 3 container-2
# PING container-2 (10.20.0.3): 56 data bytes
# 64 bytes from 10.20.0.3: seq=0 ttl=64 time=1.234 ms
# 通过容器名通信(DNS服务发现)
docker exec container-1 wget -qO- http://container-2
7.5 overlay网络中的容器通信
overlay网络中的容器通信涉及VXLAN封装。让我们深入分析通信过程:
# 查看overlay网络中的容器
docker network inspect my-overlay-net --format '{{range .Containers}}{{.Name}}: {{.IPv4Address}}{{"\n"}}{{end}}'
# 查看VXLAN接口
ip -d link show type vxlan
# 输出示例:
# 6: vxlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 ...
# vxlan id 256 dev eth0 srcport 0 0 dstport 4789 ...
# ...
# VXLAN的MTU比物理网卡小50字节(因为VXLAN封装头部开销)
# 物理网卡MTU=1500, VXLAN接口MTU=1450
# 查看VTEP的FDB(转发数据库)
bridge fdb show dev vxlan0
# 输出示例:
# 00:00:00:00:00:00 dev vxlan0 dst 192.168.1.11 self permanent
# (全零MAC表示: 未知单播/广播帧发送到192.168.1.11)
# 02:42:0a:00:00:03 dev vxlan0 dst 192.168.1.11 self permanent
# (MAC 02:42:0a:00:00:03 在远端192.168.1.11上)
overlay通信的完整流程:
容器A (Host1, 10.0.0.2) → 容器B (Host2, 10.0.0.3)
步骤1: 容器A发送IP包 (目标10.0.0.3)
步骤2: 容器A的eth0 → veth pair → overlay网桥(br-xxx)
步骤3: overlay网桥查找FDB,发现目标MAC在远端192.168.1.11
步骤4: VTEP封装VXLAN包:
- 外层以太网: 目标MAC=Host2网卡MAC
- 外层IP: 192.168.1.10 → 192.168.1.11
- 外层UDP: 目标端口4789
- VXLAN头: VNI=256
- 内层: 原始以太网帧
步骤5: VXLAN包通过物理网络到达Host2
步骤6: Host2的VTEP解封装,提取原始以太网帧
步骤7: 原始帧 → overlay网桥 → veth pair → 容器B的eth0
步骤8: 容器B收到IP包
7.6 VXLAN封装详解
让我们通过抓包来实际观察VXLAN封装:
# 在Host1上启动容器
docker run -d --name vxlan-test1 --network my-overlay-net alpine sleep 3600
# 在Host2上启动容器
docker run -d --name vxlan-test2 --network my-overlay-net alpine sleep 3600
# 在Host1的物理网卡上抓包(过滤VXLAN端口4789)
tcpdump -i eth0 -n udp port 4789 -XX
# 在另一个终端,从test1 ping test2
docker exec vxlan-test1 ping -c 1 vxlan-test2
# 抓包输出(详细分析):
# 10:00:01.123 IP 192.168.1.10.12345 > 192.168.1.11.4789: VXLAN, flags [I], vni 256
#
# 以太网层:
# 外层目标MAC: 192.168.1.11的MAC
# 外层源MAC: 192.168.1.10的MAC
# IP层:
# 外层源IP: 192.168.1.10
# 外层目标IP: 192.168.1.11
# UDP层:
# 源端口: 12345 (随机)
# 目标端口: 4789 (VXLAN标准端口)
# VXLAN头:
# Flags: I (I标志位表示VNI有效)
# VNI: 256
# 内层以太网帧:
# 内层源MAC: 容器1的MAC (02:42:0a:00:00:02)
# 内层目标MAC: 容器2的MAC (02:42:0a:00:00:03)
# 内层IP:
# 源IP: 10.0.0.2 (容器1)
# 目标IP: 10.0.0.3 (容器2)
# ICMP数据
7.7 overlay网络性能分析
overlay网络由于VXLAN封装,会引入额外的性能开销:
性能影响因素:
| 因素 | 影响 | 说明 |
|---|---|---|
| 封装开销 | MTU减少50字节 | VXLAN头50字节(外层以太网18+IP20+UDP8+VXLAN8-2) |
| CPU开销 | 封装/解封装 | 每个包需要VXLAN封装和解封装 |
| 延迟增加 | 额外处理 | 封装/解封装引入微秒级延迟 |
| 吞吐量 | 约降低5-15% | 取决于硬件和负载 |
# 性能测试: bridge vs overlay
# === bridge网络性能测试 ===
docker network create test-bridge
docker run -d --name perf-server --network test-bridge iperf3:latest
docker run -it --rm --network test-bridge iperf3:latest -c perf-server -t 10
# 输出示例:
# [ 4] 0.00-10.00 sec 9.50 GBytes 8.16 Gbits/sec throughput
# === overlay网络性能测试 ===
# (需要在Swarm模式下)
docker network create -d overlay test-overlay
docker service create --name perf-server --network test-overlay iperf3:latest
docker run -it --rm --network test-overlay iperf3:latest -c perf-server -t 10
# 输出示例:
# [ 4] 0.00-10.00 sec 7.80 GBytes 6.70 Gbits/sec throughput
# 性能差异: overlay比bridge约低15-20%
性能优化建议:
# 1. 使用支持VXLAN硬件卸载的网卡
ethtool -k eth0 | grep -i vxlan
# tx-vlan-offload: on
# rx-vlan-offload: on
# 2. 调整MTU避免分片
# 如果物理网络MTU=1500,overlay MTU应设为1450
# 如果物理网络支持Jumbo Frame(MTU=9000),overlay MTU可设为8950
docker network create -d overlay -o com.docker.network.mtu=1450 my-overlay-net
# 3. 启用加密会降低性能,仅在需要时使用
# 加密的overlay网络性能比非加密低20-30%
docker network create -d overlay --opt encrypted my-secure-net
# 4. 使用TCP BBR拥塞控制算法提升跨WAN性能
sysctl -w net.ipv4.tcp_congestion_control=bbr
overlay网络的MTU问题深入分析: VXLAN封装会在原始数据包上添加50字节的外层头部(18字节以太网头+20字节IP头+8字节UDP头+8字节VXLAN头,减去原始以太网帧的2字节帧校验),因此overlay网络的MTU应该比物理网络小50字节。如果物理网络MTU为1500,overlay网络的MTU应该设置为1450。Docker默认会自动处理这个差异——创建overlay网络时,Docker会自动将容器的MTU设置为物理网络MTU减去50字节。但如果物理网络使用了Jumbo Frame(巨帧,MTU=9000),则需要手动确认overlay网络的MTU设置正确。
# 查看overlay网络中容器的MTU
docker exec <overlay-container> ip link show eth0
# 如果物理MTU=1500,容器eth0的MTU应为1450
# 查看VXLAN接口的MTU
ip link show | grep vxlan
# vxlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 ...
# MTU不匹配的典型症状:
# 1. 小包(如ping)可以通,但大包(如curl HTTPS)超时
# 2. SSH连接建立后卡住
# 3. 文件传输在大文件时中断
# 排查MTU问题的方法
# 从容器A ping容器B,逐步增大包大小
docker exec container-a ping -c 3 -M do -s 1422 container-b
# -M do: 禁止分片
# -s 1422: 1422+28(ICMP头)=1450(overlay MTU)
# 如果1450能通但1451不通,说明MTU设置正确
# 如果需要修改overlay网络的MTU
docker network rm my-overlay-net
docker network create -d overlay \
-o com.docker.network.mtu=1400 \
my-overlay-net
overlay网络与服务发现: overlay网络不仅提供跨主机容器通信,还集成了增强的服务发现功能。在overlay网络中,容器可以通过名称互相解析,而且Docker Swarm的VIP(Virtual IP)负载均衡也是基于overlay网络的DNS服务发现实现的。当创建一个Swarm服务时,Docker会为该服务分配一个VIP,所有发往该VIP的流量会被负载均衡到各个容器实例。这种机制比DNS轮询更可靠,因为它使用了IPVS(IP Virtual Server)内核模块实现四层负载均衡,支持健康检查和会话保持。
# 创建Swarm服务并观察VIP
docker service create --name web-svc --replicas 3 --network my-overlay-net nginx
# 查看服务的VIP
docker service inspect web-svc --format '{{range .Endpoint.VirtualIPs}}{{.Addr}}{{end}}'
# 输出: 10.0.0.5/24 (这是VIP,不是任何容器的真实IP)
# 在overlay网络中解析服务名
docker run -it --rm --network my-overlay-net alpine nslookup web-svc
# Name: web-svc
# Address 1: 10.0.0.5 (返回的是VIP,不是单个容器的IP)
# 直接解析容器名(返回真实IP)
docker run -it --rm --network my-overlay-net alpine nslookup web-svc.1.xxx
# Address 1: 10.0.0.3 (某个容器的真实IP)
# VIP负载均衡验证
docker run -it --rm --network my-overlay-net alpine sh -c "
for i in 1 2 3 4 5 6; do
wget -qO- http://web-svc | grep 'Welcome'
echo '---'
done
"
# 每次请求可能被分发到不同容器(通过IPVS负载均衡)
VIP负载均衡与DNS轮询的对比: 在Docker Swarm中,服务的负载均衡通过VIP(Virtual IP)机制实现,这与前面章节介绍的DNS轮询有本质区别。DNS轮询是在DNS层面返回多个IP地址,由客户端选择使用哪个IP,这种方式存在客户端缓存导致负载不均匀、无法感知容器健康状态等问题。而VIP机制则是在内核层面通过IPVS(IP Virtual Server)实现四层负载均衡——所有发往VIP的流量都会被IPVS截获,然后根据负载均衡算法(默认为轮询)将流量转发到后端的某个容器实例。由于IPVS工作在内核态,不需要修改数据包的应用层内容,性能非常高。更重要的是,当某个容器实例不可用时,Swarm会自动将其从IPVS的后端列表中移除,实现自动健康检查和故障转移。
# 查看IPVS规则
ipvsadm -L -n
# 输出示例:
# TCP 10.0.0.5:80 rr
# -> 10.0.0.2:80 Masq 1 0 0
# -> 10.0.0.3:80 Masq 1 0 0
# -> 10.0.0.4:80 Masq 1 0 0
# VIP 10.0.0.5:80 通过轮询(rr)算法分发到3个后端容器
# 查看IPVS统计信息(连接数、数据包数、字节数)
ipvsadm -L -n --stats
# 可以看到每个后端容器接收的流量统计
# 查看IPVS连接状态
ipvsadm -L -n --rate
# 实时显示每个后端的连接速率
# 测试VIP负载均衡的效果
# 启动一个测试容器,多次请求服务
docker run -it --rm --network my-overlay-net alpine sh -c "
for i in 1 2 3 4 5 6 7 8 9 10; do
wget -qO- http://web-svc 2>/dev/null | head -1
sleep 0.5
done
"
# 可以看到请求被均匀分发到不同容器实例
# 模拟容器故障,验证自动健康检查
# 停止其中一个副本
docker service scale web-svc=2
# 等待几秒后,IPVS规则自动更新
ipvsadm -L -n
# 只剩下2个后端容器
# 恢复副本
docker service scale web-svc=3
# IPVS自动添加新的后端
7.8 跨主机网络实战: 多节点Web集群
完整的多节点Web集群部署实战:
# === 环境准备 ===
# 三台主机:
# manager: 192.168.1.10 (Swarm管理节点)
# worker1: 192.168.1.11 (工作节点)
# worker2: 192.168.1.12 (工作节点)
# === 在manager上初始化Swarm ===
docker swarm init --advertise-addr 192.168.1.10
# 获取worker加入命令
docker swarm join-token worker
# 输出: docker swarm join --token SWMTKN-xxx 192.168.1.10:2377
# === 在worker1和worker2上加入Swarm ===
docker swarm join --token SWMTKN-xxx 192.168.1.10:2377
# === 在manager上查看集群 ===
docker node ls
# ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
# xxx manager Ready Active Leader
# yyy worker1 Ready Active
# zzz worker2 Ready Active
# === 创建overlay网络 ===
docker network create -d overlay \
--subnet 10.0.0.0/16 \
--attachable \
web-cluster-net
# === 部署数据库服务 ===
docker service create \
--name db \
--network web-cluster-net \
--network-alias database \
--replicas 1 \
--constraint 'node.role==manager' \
-e MYSQL_ROOT_PASSWORD=clusterpass \
-e MYSQL_DATABASE=webapp \
mysql:8.0
# === 部署后端API服务(3个副本) ===
docker service create \
--name api \
--network web-cluster-net \
--network-alias backend \
--replicas 3 \
-e DB_HOST=database \
-e DB_PASSWORD=clusterpass \
myapp-api
# 查看服务分布
docker service ps api
# ID NAME IMAGE NODE DESIRED STATE CURRENT STATE
# 1 api.1 myapp-api worker1 Running Running 5 seconds ago
# 2 api.2 myapp-api worker2 Running Running 5 seconds ago
# 3 api.3 myapp-api manager Running Running 5 seconds ago
# === 部署前端Nginx服务(2个副本) ===
docker service create \
--name web \
--network web-cluster-net \
--replicas 2 \
-p 80:80 \
--config-source nginx-conf=/etc/nginx/conf.d/default.conf \
myapp-nginx
# === 部署Redis缓存服务 ===
docker service create \
--name cache \
--network web-cluster-net \
--network-alias redis \
--replicas 1 \
redis:alpine
# === 查看所有服务 ===
docker service ls
# ID NAME MODE REPLICAS IMAGE PORTS
# xxx db replicated 1/1 mysql:8.0
# yyy api replicated 3/3 myapp-api
# zzz web replicated 2/2 myapp-nginx *:80->80/tcp
# www cache replicated 1/1 redis:alpine
# === 测试跨主机通信 ===
# 在manager上启动一个临时容器测试
docker run -it --rm --network web-cluster-net alpine sh
# 在容器内:
# ping database # 测试数据库连通性
# ping backend # 测试API连通性(会通过VIP负载均衡到某个副本)
# ping redis # 测试缓存连通性
# wget -qO- http://web # 测试前端连通性
# === 测试服务高可用 ===
# 模拟节点故障: 停止worker1
# (在worker1上)
systemctl stop docker
# 在manager上查看服务状态
docker service ps api
# api.1的副本会被自动调度到其他节点
# === 扩缩容 ===
# 扩展API到5个副本
docker service scale api=5
# 查看分布
docker service ps api
# 缩减到2个副本
docker service scale api=2
# === 滚动更新 ===
docker service update --image myapp-api:v2 api
# === 清理 ===
docker service rm web api db cache
docker network rm web-cluster-net
docker swarm leave -f # 在所有节点上执行
第八章 macvlan与ipvlan网络
在某些场景下,我们需要让容器直接出现在物理网络中,拥有物理网络的IP地址,而不是使用NAT或端口映射。macvlan和ipvlan网络驱动就是为了满足这种需求而设计的。本章将深入讲解这两种网络模式的原理和实践。
8.1 macvlan工作原理
macvlan是Linux内核的网络驱动,它允许在单个物理网卡上创建多个虚拟网络接口,每个虚拟接口拥有独立的MAC地址,就像多块物理网卡一样。
┌──────────────────────────────────────────┐
│ 物理网卡 eth0 │
│ (MAC: aa:bb:cc:dd:ee:ff) │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │macvlan1 │ │macvlan2 │ │macvlan3 │ │
│ │MAC:001 │ │MAC:002 │ │MAC:003 │ │
│ │容器A │ │容器B │ │容器C │ │
│ │.100 │ │.101 │ │.102 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 所有macvlan接口共享物理网卡的带宽 │
│ 但每个接口有独立的MAC地址和IP地址 │
└──────────────────────────────────────────┘
物理交换机看到的是3个不同的MAC地址
将容器视为物理网络上的独立主机
macvlan的四种模式:
| 模式 | 说明 | 特点 |
|---|---|---|
| bridge | 默认模式,同一物理网卡上的macvlan接口可以直接通信 | 类似网桥,容器间可直接通信 |
| private | macvlan接口之间完全隔离 | 最高隔离性,容器间不可通信 |
| vepa(Virtual Ethernet Port Aggregator) | 所有流量通过物理交换机转发 | 需要交换机支持反射中继 |
| passthru | 只允许一个macvlan接口绑定到物理网卡 | 每个物理网卡只能一个容器 |
Docker默认使用bridge模式。
8.2 创建macvlan网络(bridge模式/802.1Q trunk)
基本macvlan网络创建:
# 查看物理网卡
ip link show
# 假设物理网卡为eth0,IP段为192.168.1.0/24
# 创建macvlan网络(bridge模式)
docker network create -d macvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
-o parent=eth0 \
my-macvlan
# 启动容器使用macvlan网络
docker run -d \
--name macvlan-container1 \
--network my-macvlan \
--ip 192.168.1.100 \
nginx
# 验证容器获得了物理网络IP
docker exec macvlan-container1 ip addr show eth0
# eth0: inet 192.168.1.100/24
# 从同一物理网络的其他机器直接访问容器
# 在另一台机器上(192.168.1.50):
ping 192.168.1.100
curl http://192.168.1.100
802.1Q VLAN trunk模式:
# 如果物理网络使用VLAN,可以创建带VLAN tag的macvlan
# 创建VLAN 100的子接口
docker network create -d macvlan \
--subnet=192.168.100.0/24 \
--gateway=192.168.100.1 \
-o parent=eth0.100 \ # eth0的VLAN 100子接口
my-vlan100-net
# 创建VLAN 200的子接口
docker network create -d macvlan \
--subnet=192.168.200.0/24 \
--gateway=192.168.200.1 \
-o parent=eth0.200 \ # eth0的VLAN 200子接口
my-vlan200-net
# 启动不同VLAN的容器
docker run -d --name vlan100-container --network my-vlan100-net --ip 192.168.100.10 nginx
docker run -d --name vlan200-container --network my-vlan200-net --ip 192.168.200.10 nginx
# 不同VLAN的容器在物理网络上隔离
指定macvlan模式:
# 创建private模式的macvlan网络
docker network create -d macvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
-o parent=eth0 \
-o macvlan_mode=private \
my-private-macvlan
# 创建vepa模式的macvlan网络
docker network create -d macvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
-o parent=eth0 \
-o macvlan_mode=vepa \
my-vepa-macvlan
8.3 macvlan容器获取物理网络IP
macvlan容器直接获得物理网络的IP地址,其他主机可以像访问普通主机一样访问容器:
# 查看物理网络信息
ip addr show eth0
# eth0: inet 192.168.1.10/24
# 创建macvlan网络
docker network create -d macvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
-o parent=eth0 \
phys-net
# 启动多个容器,每个获得物理网络IP
docker run -d --name web1 --network phys-net --ip 192.168.1.101 nginx
docker run -d --name web2 --network phys-net --ip 192.168.1.102 nginx
docker run -d --name web3 --network phys-net --ip 192.168.1.103 nginx
# 验证容器IP
for c in web1 web2 web3; do
echo "$c: $(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $c)"
done
# web1: 192.168.1.101
# web2: 192.168.1.102
# web3: 192.168.1.103
# 从物理网络的其他机器直接访问
# 在192.168.1.50上:
curl http://192.168.1.101
curl http://192.168.1.102
curl http://192.168.1.103
# 查看物理交换机的MAC地址表(如果可以访问)
# 可以看到3个不同的MAC地址对应的端口
8.4 macvlan的通信限制(容器与宿主机无法通信)
macvlan有一个重要的限制: macvlan容器无法与宿主机直接通信。这是因为Linux内核出于安全考虑,禁止同一物理网卡上的macvlan接口与父接口(物理网卡)之间直接通信。
# 宿主机IP: 192.168.1.10
# 容器IP: 192.168.1.100
# 从容器ping宿主机 (失败!)
docker exec macvlan-container1 ping -c 2 192.168.1.10
# PING 192.168.1.10 (192.168.1.10): 56 data bytes
# (无响应,100%丢包)
# 从宿主机ping容器 (也失败!)
ping -c 2 192.168.1.100
# (无响应)
# 但容器可以访问物理网络中的其他主机
docker exec macvlan-container1 ping -c 2 192.168.1.50
# 可以ping通(其他物理主机)
# 容器之间也可以通信(bridge模式下)
docker exec macvlan-container1 ping -c 2 192.168.1.102
# 可以ping通(同macvlan网络的其他容器)
限制原因图解:
┌──────────────────────────────────────────┐
│ 物理网卡 eth0 │
│ 192.168.1.10 │
│ ┌─────────┐ ┌─────────┐ │
│ │macvlan1 │ │macvlan2 │ │
│ │.100 │ ✗ ←-→ │.102 │ ←✗ │
│ │容器A │ 容器间 │容器B │ 宿主机 │
│ └─────────┘ 通信OK └─────────┘ 通信 │
│ 失败! │
│ ←✗ 宿主机与macvlan容器之间无法通信 │
└──────────────────────────────────────────┘
8.5 解决macvlan通信限制的方案
虽然macvlan容器无法直接与宿主机通信,但可以通过创建一个macvlan接口在宿主机上来绕过这个限制:
# 方案: 在宿主机上创建一个macvlan接口,作为与容器通信的桥梁
# 1. 创建宿主机的macvlan接口
ip link add macvlan-host link eth0 type macvlan mode bridge
# 2. 为该接口分配IP(与容器在同一子网,但不能与现有IP冲突)
ip addr add 192.168.1.200/24 dev macvlan-host
# 3. 启用该接口
ip link set macvlan-host up
# 4. 修改路由,让发往容器网段的流量通过macvlan-host接口
# 先删除默认路由中通过eth0到容器网段的路由(如果有)
# ip route del 192.168.1.0/24 dev eth0
# 添加通过macvlan-host的路由
ip route add 192.168.1.100/32 dev macvlan-host
ip route add 192.168.1.101/32 dev macvlan-host
ip route add 192.168.1.102/32 dev macvlan-host
# 5. 现在宿主机可以与容器通信了
ping -c 2 192.168.1.100
# PING 192.168.1.100 (192.168.1.100): 56 data bytes
# 64 bytes from 192.168.1.100: seq=0 ttl=64 time=0.123 ms
# 6. 容器也可以ping宿主机的macvlan接口IP
docker exec macvlan-container1 ping -c 2 192.168.1.200
# 可以ping通!
创建开机自动配置的脚本:
# 创建systemd服务来自动配置macvlan宿主机接口
cat > /etc/systemd/system/macvlan-host.service << 'EOF'
[Unit]
Description=Configure macvlan host interface
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/bash -c 'ip link add macvlan-host link eth0 type macvlan mode bridge && ip addr add 192.168.1.200/24 dev macvlan-host && ip link set macvlan-host up'
ExecStop=/bin/bash -c 'ip link del macvlan-host'
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable macvlan-host.service
systemctl start macvlan-host.service
8.6 ipvlan L2模式
ipvlan与macvlan类似,但容器不分配独立的MAC地址,而是共享父接口(物理网卡)的MAC地址。ipvlan有两种模式:L2模式和L3模式。
ipvlan L2模式:
在L2模式下,容器和宿主机共享同一个MAC地址,但每个容器有独立的IP地址。交换机看到的是同一个MAC地址,但来自不同的IP。
# 创建ipvlan L2网络
docker network create -d ipvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
-o parent=eth0 \
-o ipvlan_mode=l2 \
my-ipvlan-l2
# 启动容器
docker run -d --name ipvlan-c1 --network my-ipvlan-l2 --ip 192.168.1.110 nginx
docker run -d --name ipvlan-c2 --network my-ipvlan-l2 --ip 192.168.1.111 nginx
# 验证: 所有容器和宿主机共享同一个MAC地址
docker exec ipvlan-c1 ip link show eth0
# 可以看到MAC地址与eth0相同
# 在物理交换机上查看MAC地址表
# 只能看到一个MAC地址(物理网卡的),但有多个IP
# ipvlan L2模式下,容器可以与宿主机通信(与macvlan不同!)
docker exec ipvlan-c1 ping -c 2 192.168.1.10
# 可以ping通!
# 容器之间也可以通信
docker exec ipvlan-c1 ping -c 2 192.168.1.111
# 可以ping通
ipvlan L2 vs macvlan bridge对比:
| 特性 | macvlan bridge | ipvlan L2 |
|---|---|---|
| MAC地址 | 每个容器独有 | 共享父接口MAC |
| 容器与宿主机通信 | 不可以 | 可以 |
| 容器间通信 | 可以 | 可以 |
| 交换机MAC表 | 每个容器一个条目 | 只有一个条目 |
| MAC地址消耗 | 多 | 少 |
| 适用场景 | 需要独立MAC | MAC地址受限 |
8.7 ipvlan L3模式
ipvlan L3模式工作在三层(IP层),容器之间通过路由进行通信,不依赖ARP广播。
# 创建ipvlan L3网络
# 注意: L3模式不需要指定gateway
docker network create -d ipvlan \
--subnet=10.0.1.0/24 \
-o parent=eth0 \
-o ipvlan_mode=l3 \
my-ipvlan-l3
# 启动容器
docker run -d --name ipvlan-l3-c1 --network my-ipvlan-l3 --ip 10.0.1.10 alpine sleep 3600
docker run -d --name ipvlan-l3-c2 --network my-ipvlan-l3 --ip 10.0.1.11 alpine sleep 3600
# L3模式下,容器之间的通信通过路由而非ARP
# 查看容器路由表
docker exec ipvlan-l3-c1 route -n
# Destination Gateway Genmask Flags Metric Ref Use Iface
# 0.0.0.0 0.0.0.0 0.0.0.0 UG 0 0 0 eth0
# 注意: L3模式下没有网关,所有流量直接通过父接口路由
# 容器间通信
docker exec ipvlan-l3-c1 ping -c 2 10.0.1.11
# 可以ping通
# 创建另一个子网的ipvlan L3网络
docker network create -d ipvlan \
--subnet=10.0.2.0/24 \
-o parent=eth0 \
-o ipvlan_mode=l3 \
my-ipvlan-l3-2
# 不同子网的容器也可以通过L3路由通信
docker run -d --name ipvlan-l3-c3 --network my-ipvlan-l3-2 --ip 10.0.2.10 alpine sleep 3600
# 需要在宿主机上添加路由
ip route add 10.0.1.0/24 dev eth0
ip route add 10.0.2.0/24 dev eth0
8.8 macvlan vs ipvlan对比
| 对比维度 | macvlan | ipvlan L2 | ipvlan L3 |
|---|---|---|---|
| 工作层级 | 二层 | 二层 | 三层 |
| MAC地址 | 独立MAC | 共享父接口MAC | 共享父接口MAC |
| 容器与宿主机通信 | 不可以 | 可以 | 不可以(需路由) |
| 容器间通信 | bridge模式可以 | 可以 | 可以(通过路由) |
| ARP广播 | 有 | 有 | 无 |
| 交换机要求 | 无特殊要求 | 无特殊要求 | 支持路由 |
| MAC地址消耗 | 高 | 低 | 低 |
| 性能 | 最高 | 高 | 高 |
| 适用网络 | 标准以太网 | MAC受限网络 | 路由网络 |
| 调试难度 | 中 | 低 | 高 |
选择建议:
- 如果物理网络没有MAC地址限制,优先使用macvlan bridge模式
- 如果物理交换机有MAC地址数量限制,使用ipvlan L2
- 如果需要跨子网通信且不想使用ARP,使用ipvlan L3
- 如果需要容器与宿主机直接通信,使用ipvlan L2(macvlan不支持)
macvlan与ipvlan在生产环境中的注意事项:
使用macvlan和ipvlan模式时,需要特别注意它们与物理网络设备的交互。macvlan模式为每个容器生成独立的MAC地址,这意味着物理交换机的MAC地址表中会出现大量新的MAC地址条目。某些企业级交换机配置了端口安全策略,限制了单个物理端口可以学习的MAC地址数量。当容器数量超过这个限制时,交换机会丢弃来自新MAC地址的流量,导致容器网络不通。此外,某些交换机的MAC地址表老化时间较短,如果容器长时间不通信,其MAC地址条目可能被清除,下次通信时需要重新学习,这可能导致短暂的延迟。
# 检查交换机端口安全限制(如果可以访问交换机管理界面)
# Cisco交换机示例:
# show port-security interface fastethernet 0/1
# 显示: Maximum MAC Addresses: 5, Current MAC Addresses: 3
# 如果MAC地址数量受限,改用ipvlan L2模式
# ipvlan L2模式下,所有容器共享物理网卡的MAC地址
# 交换机只看到一个MAC地址,但对应多个IP
# 查看容器的MAC地址
docker exec macvlan-container1 ip link show eth0
# MAC: 02:42:c0:a8:01:64 (独立的MAC地址)
docker exec ipvlan-l2-c1 ip link show eth0
# MAC: 与eth0(物理网卡)相同的MAC地址
# 在宿主机上验证
ip link show eth0
# 对比容器的MAC地址
DHCP与IP地址管理: macvlan和ipvlan模式让容器直接出现在物理网络中,因此容器的IP地址需要与物理网络的IP地址规划协调。在生产环境中,通常需要为容器预留一段IP地址范围,避免与物理网络中的其他设备冲突。如果物理网络使用DHCP分配IP地址,可以为容器创建一个使用DHCP的macvlan网络,但这种方式不太可靠,因为DHCP分配的IP地址可能变化,且DHCP服务器可能对大量MAC地址的请求处理缓慢。更推荐的做法是为容器网络预留静态IP地址段,并在创建容器时使用--ip参数指定固定IP。
# 为容器预留IP地址段
# 假设物理网络: 192.168.50.0/24
# DHCP范围: 192.168.50.100-192.168.50.200
# 为容器预留: 192.168.50.210-192.168.50.250
# 创建macvlan网络,指定IP范围
docker network create -d macvlan \
--subnet=192.168.50.0/24 \
--gateway=192.168.50.1 \
--ip-range=192.168.50.210/28 \
-o parent=eth0 \
container-net
# 容器会从192.168.50.210-192.168.50.223范围中获取IP
docker run -d --name svc1 --network container-net nginx
docker run -d --name svc2 --network container-net nginx
# 或者指定固定IP
docker run -d --name svc3 --network container-net --ip 192.168.50.220 nginx
# 查看容器获得的IP
docker network inspect container-net --format '{{range .Containers}}{{.Name}}: {{.IPv4Address}}{{"\n"}}{{end}}'
- 如果需要跨子网路由且不想用ARP,使用ipvlan L3
- 如果需要容器与宿主机直接通信,使用ipvlan L2
8.9 物理网络直连实战案例
案例: 在企业网络中部署容器化监控服务
# 场景: 企业内网 192.168.50.0/24
# 需要部署Prometheus + Grafana + Node Exporter
# 要求容器直接使用企业内网IP,便于网络管理
# 1. 创建macvlan网络
docker network create -d macvlan \
--subnet=192.168.50.0/24 \
--gateway=192.168.50.1 \
-o parent=eth0 \
-o macvlan_mode=bridge \
monitoring-net
# 2. 创建宿主机macvlan接口(用于管理容器)
ip link add mv-host link eth0 type macvlan mode bridge
ip addr add 192.168.50.200/24 dev mv-host
ip link set mv-host up
# 3. 部署Prometheus (固定IP: 192.168.50.101)
docker run -d \
--name prometheus \
--network monitoring-net \
--ip 192.168.50.101 \
-v /etc/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml \
prom/prometheus
# 4. 部署Grafana (固定IP: 192.168.50.102)
docker run -d \
--name grafana \
--network monitoring-net \
--ip 192.168.50.102 \
-e GF_SECURITY_ADMIN_PASSWORD=admin \
grafana/grafana
# 5. 部署Node Exporter (固定IP: 192.168.50.103)
docker run -d \
--name node-exporter \
--network monitoring-net \
--ip 192.168.50.103 \
prom/node-exporter
# 6. 配置Prometheus抓取Node Exporter
cat > /etc/prometheus/prometheus.yml << 'EOF'
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'node-exporter'
static_configs:
- targets: ['192.168.50.103:9100']
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
EOF
# 重启Prometheus加载配置
docker restart prometheus
# 7. 从企业内网任意机器访问
# 在192.168.50.50上:
curl http://192.168.50.101:9090 # Prometheus
curl http://192.168.50.102:3000 # Grafana
curl http://192.168.50.103:9100 # Node Exporter
# 8. 验证容器MAC地址(每个容器独立MAC)
for c in prometheus grafana node-exporter; do
echo "$c: $(docker exec $c ip link show eth0 | grep link/ether)"
done
# prometheus: link/ether 02:42:c0:a8:32:65
# grafana: link/ether 02:42:c0:a8:32:66
# node-exporter: link/ether 02:42:c0:a8:32:67
# 9. 在交换机上查看MAC地址表
# 可以看到3个新的MAC地址出现在eth0对应的端口上
# 清理
docker rm -f prometheus grafana node-exporter
docker network rm monitoring-net
ip link del mv-host
第九章 Docker DNS与服务发现
DNS解析和服务发现是Docker网络中至关重要的功能。通过内置的DNS服务器,Docker让容器可以通过名称而非IP地址进行通信,大大简化了容器间通信的配置和管理。本章将全面解析Docker的DNS机制。
9.1 Docker内置DNS服务器(127.0.0.11)
Docker在自定义bridge网络和overlay网络中内置了一个DNS服务器,监听地址为127.0.0.11:53。这个DNS服务器由Docker Daemon管理,运行在每个容器的网络命名空间中。
# 查看自定义网络中容器的DNS配置
docker network create dns-demo
docker run -d --name dns-test --network dns-demo alpine sleep 3600
# 查看容器的/etc/resolv.conf
docker exec dns-test cat /etc/resolv.conf
# 输出:
# nameserver 127.0.0.11
# options ndots:0
# 解析:
# nameserver 127.0.0.11 → 使用Docker内置DNS
# options ndots:0 → 不带点的名称直接查询,不做搜索域拼接
内置DNS的工作原理:
容器内的DNS查询流程:
应用程序
│
▼ DNS查询: "web-server"
/etc/resolv.conf
│ nameserver 127.0.0.11
▼
Docker内置DNS (127.0.0.11:53)
│
├─ 查询是容器名/别名?
│ ├─ 是 → 查找内置注册表,返回IP
│ │ 172.20.0.2
│ │
│ └─ 否 → 转发到外部DNS
│ ↓
│ 8.8.8.8 (宿主机DNS)
│ ↓
│ 返回外部域名IP
│ 93.184.216.34
│
▼ 返回解析结果
应用程序使用IP建立连接
# 验证DNS服务器在监听
docker exec dns-test netstat -tlnp
# 或
docker exec dns-test ss -tlnp
# 可以看到127.0.0.11:53在监听
# 使用dig查询DNS
docker exec dns-test sh -c "apk add bind-tools && dig web-server"
# 对比: 默认bridge网络中的DNS配置
docker run -d --name default-bridge-test alpine sleep 3600
docker exec default-bridge-test cat /etc/resolv.conf
# 输出:
# nameserver 8.8.8.8 ← 使用外部DNS,不是内置DNS
# nameserver 8.8.4.4
# options ndots:0
# 默认bridge网络不使用内置DNS,因此不支持容器名解析
# 清理
docker rm -f dns-test default-bridge-test
docker network rm dns-demo
9.2 容器名称解析
在自定义网络中,容器的名称会自动注册到内置DNS,其他容器可以通过名称解析到该容器的IP:
# 创建网络
docker network create name-resolve-net
# 启动多个容器
docker run -d --name redis-svc --network name-resolve-net redis:alpine
docker run -d --name postgres-svc --network name-resolve-net postgres:15-alpine
docker run -d --name nginx-svc --network name-resolve-net nginx:alpine
docker run -d --name app-svc --network name-resolve-net alpine sleep 3600
# 从app-svc容器中解析其他容器名
docker exec app-svc nslookup redis-svc
# Server: 127.0.0.11
# Address: 127.0.0.11:53
# Name: redis-svc
# Address 1: 172.55.0.2 redis-svc.name-resolve-net
docker exec app-svc nslookup postgres-svc
# Name: postgres-svc
# Address 1: 172.55.0.3 postgres-svc.name-resolve-net
docker exec app-svc nslookup nginx-svc
# Name: nginx-svc
# Address 1: 172.55.0.4 nginx-svc.name-resolve-net
# 通过名称访问服务
docker exec app-svc ping -c 2 redis-svc
# PING redis-svc (172.55.0.2): 56 data bytes
# 64 bytes from 172.55.0.2: seq=0 ttl=64 time=0.089 ms
docker exec app-svc wget -qO- http://nginx-svc
# 返回nginx欢迎页面
# 容器停止后,DNS记录自动删除
docker stop nginx-svc
docker exec app-svc nslookup nginx-svc
# server can't find nginx-svc: NXDOMAIN
# 容器重新启动后,DNS记录自动恢复
docker start nginx-svc
docker exec app-svc nslookup nginx-svc
# Name: nginx-svc
# Address 1: 172.55.0.4 nginx-svc.name-resolve-net
# 清理
docker rm -f redis-svc postgres-svc nginx-svc app-svc
docker network rm name-resolve-net
容器名解析的特点:
- 自动注册: 容器启动时自动注册,无需手动配置
- 自动注销: 容器停止时DNS记录自动删除
- 网络作用域: 仅在同一自定义网络内有效
- 唯一性: 同一网络内容器名必须唯一
9.3 网络别名解析
除了容器名,还可以通过--network-alias为容器设置多个别名,所有别名都可以被DNS解析:
# 创建网络
docker network create alias-resolve-net
# 启动容器并设置多个别名
docker run -d \
--name mysql-primary \
--network alias-resolve-net \
--network-alias mysql \
--network-alias db \
--network-alias database \
--network-alias primary-db \
-e MYSQL_ROOT_PASSWORD=secret \
mysql:8.0
# 从另一个容器解析所有别名
docker run -it --rm --network alias-resolve-net alpine sh -c "
echo '=== mysql ==='
nslookup mysql
echo '=== db ==='
nslookup db
echo '=== database ==='
nslookup database
echo '=== primary-db ==='
nslookup primary-db
echo '=== mysql-primary (容器名) ==='
nslookup mysql-primary
"
# 所有名称都解析到同一个IP
# 清理
docker rm -f mysql-primary
docker network rm alias-resolve-net
别名的网络作用域: 别名是网络级别的,同一个容器在不同网络中可以有不同的别名:
# 创建两个网络
docker network create net-alpha
docker network create net-beta
# 在net-alpha中,容器别名为"service-a"
docker run -d \
--name multi-alias \
--network net-alpha \
--network-alias service-a \
alpine sleep 3600
# 连接到net-beta,设置不同别名
docker network connect --alias service-b net-beta multi-alias
# 在net-alpha中,只能解析service-a
docker run -it --rm --network net-alpha alpine nslookup service-a
# 成功
docker run -it --rm --network net-alpha alpine nslookup service-b
# 失败 (service-b不在net-alpha中)
# 在net-beta中,只能解析service-b
docker run -it --rm --network net-beta alpine nslookup service-b
# 成功
docker run -it --rm --network net-beta alpine nslookup service-a
# 失败 (service-a不在net-beta中)
# 清理
docker rm -f multi-alias
docker network rm net-alpha net-beta
9.4 自定义DNS配置(–dns, --dns-search, --dns-opt)
Docker允许为容器自定义DNS配置:
–dns: 指定DNS服务器
# 指定自定义DNS服务器
docker run -d \
--name custom-dns \
--network bridge \
--dns 8.8.8.8 \
--dns 1.1.1.1 \
alpine sleep 3600
# 查看DNS配置
docker exec custom-dns cat /etc/resolv.conf
# nameserver 8.8.8.8
# nameserver 1.1.1.1
# 注意: 在自定义网络中,--dns会添加到127.0.0.11之后
docker run -d \
--name custom-dns2 \
--network my-net \
--dns 8.8.8.8 \
alpine sleep 3600
docker exec custom-dns2 cat /etc/resolv.conf
# nameserver 127.0.0.11
# nameserver 8.8.8.8
# 127.0.0.11仍然在最前面,保证容器名解析优先
–dns-search: 指定DNS搜索域
# 指定搜索域
docker run -d \
--name search-domain-test \
--dns-search example.com \
--dns-search internal.example.com \
alpine sleep 3600
docker exec search-domain-test cat /etc/resolv.conf
# nameserver ...
# search example.com internal.example.com
# 当查询不带点的名称时,会自动追加搜索域
# 例如: ping web → 先查询 web.example.com, 再查询 web.internal.example.com
–dns-opt: 指定DNS选项
# 指定DNS选项
docker run -d \
--name dns-opt-test \
--dns-opt "timeout:2" \
--dns-opt "attempts:3" \
--dns-opt "ndots:1" \
alpine sleep 3600
docker exec dns-opt-test cat /etc/resolv.conf
# options timeout:2 attempts:3 ndots:1
# 常用DNS选项:
# timeout:N DNS查询超时时间(秒)
# attempts:N DNS查询重试次数
# ndots:N 名称中点数少于N时,追加搜索域
# rotate 轮询使用多个DNS服务器
# single-request 只发送单个DNS请求
全局DNS配置:
# 通过daemon.json为所有容器设置默认DNS
# /etc/docker/daemon.json
{
"dns": ["8.8.8.8", "1.1.1.1"],
"dns-search": ["example.com"],
"dns-opts": ["timeout:2", "attempts:3"]
}
# 重启Docker
systemctl restart docker
# 之后所有新创建的容器都会使用这些DNS配置
9.5 /etc/hosts与/etc/resolv.conf在容器中的行为
Docker会自动管理容器内的/etc/hosts和/etc/resolv.conf文件:
/etc/hosts文件:
# 查看容器的/etc/hosts
docker run -it --rm --name hosts-test --network-alias my-alias alpine cat /etc/hosts
# 输出示例:
# 127.0.0.1 localhost
# ::1 localhost ip6-localhost ip6-loopback
# 172.17.0.2 hosts-test # 容器名 → 容器IP
# 172.17.0.2 my-alias # 别名 → 容器IP
# 172.17.0.2 6b3a2c1d4e5f # 容器ID → 容器IP
# 添加自定义hosts条目
docker run -d \
--name custom-hosts \
--add-host "api.example.com:192.168.1.100" \
--add-host "db.example.com:192.168.1.101" \
--add-host "cache.example.com:192.168.1.102" \
alpine sleep 3600
docker exec custom-hosts cat /etc/hosts
# 172.17.0.2 custom-hosts
# 192.168.1.100 api.example.com # 自定义条目
# 192.168.1.101 db.example.com # 自定义条目
# 192.168.1.102 cache.example.com # 自定义条目
# /etc/hosts的优先级高于DNS
# 先查/etc/hosts,找不到再查DNS
/etc/resolv.conf文件:
# 默认bridge网络
docker run -it --rm alpine cat /etc/resolv.conf
# nameserver 8.8.8.8 (来自宿主机的DNS)
# nameserver 8.8.4.4
# options ndots:0
# 自定义bridge网络
docker network create my-test-net
docker run -it --rm --network my-test-net alpine cat /etc/resolv.conf
# nameserver 127.0.0.11 (Docker内置DNS)
# options ndots:0
# host模式
docker run -it --rm --network host alpine cat /etc/resolv.conf
# 直接使用宿主机的/etc/resolv.conf内容
# none模式
docker run -it --rm --network none alpine cat /etc/resolv.conf
# nameserver 127.0.0.11 (只有内置DNS,但没有网络接口)
# 清理
docker network rm my-test-net
–add-host与DNS的区别:
--add-host写入/etc/hosts,是静态的,优先级最高- DNS解析是动态的,容器IP变化时自动更新
- 建议优先使用DNS(自定义网络),只在需要静态解析时使用
--add-host
9.6 DNS轮询负载均衡
当多个容器使用相同的网络别名时,Docker内置DNS会返回所有容器的IP地址,实现简单的DNS轮询负载均衡:
# 创建网络
docker network create dns-lb-net
# 启动3个Web容器,都使用别名"web"
for i in 1 2 3; do
docker run -d \
--name web-$i \
--network dns-lb-net \
--network-alias web \
-e SERVER_ID=$i \
nginx:alpine
done
# DNS查询返回多个IP
docker run -it --rm --network dns-lb-net alpine nslookup web
# 输出:
# Server: 127.0.0.11
# Address: 127.0.0.11:53
#
# Name: web
# Address 1: 172.56.0.2 web-1.dns-lb-net
# Address 2: 172.56.0.3 web-2.dns-lb-net
# Address 3: 172.56.0.4 web-3.dns-lb-net
# 客户端解析时,DNS返回IP的顺序可能不同(轮询)
docker run -it --rm --network dns-lb-net alpine sh -c "
for i in 1 2 3 4 5 6; do
nslookup web 2>/dev/null | grep 'Address' | tail -1
done
"
# 输出(顺序可能变化):
# Address 1: 172.56.0.2 web-1.dns-lb-net
# Address 1: 172.56.0.3 web-2.dns-lb-net
# Address 1: 172.56.0.2 web-1.dns-lb-net
# Address 1: 172.56.0.4 web-3.dns-lb-net
# Address 1: 172.56.0.3 web-2.dns-lb-net
# Address 1: 172.56.0.4 web-3.dns-lb-net
# DNS轮询的局限:
# 1. 不能保证均匀分配(取决于客户端DNS缓存策略)
# 2. 没有健康检查(故障容器的IP仍然会被返回)
# 3. 没有会话保持(同一客户端可能分配到不同容器)
# 4. 适合简单的负载分配场景
# 对于生产环境,建议使用Nginx/Haproxy或Swarm内置负载均衡
# 清理
docker rm -f web-1 web-2 web-3
docker network rm dns-lb-net
DNS轮询与客户端缓存的关系: DNS轮询负载均衡的效果受客户端DNS缓存行为的影响很大。大多数应用程序和HTTP客户端库都会缓存DNS解析结果,缓存的持续时间由DNS响应中的TTL(Time To Live)字段决定。Docker内置DNS服务器返回的TTL默认为600秒(10分钟),这意味着客户端在10分钟内会使用同一个IP地址,只有缓存过期后才会重新查询DNS,此时才可能获得不同的IP。这种缓存机制使得DNS轮询的负载分配往往不够均匀——先启动的容器会获得更多的连接,因为它们的IP在更多客户端的缓存中。
# 查看DNS响应的TTL
docker run -it --rm --network dns-lb-net alpine sh -c "
apk add bind-tools
dig web | grep -A2 'ANSWER SECTION'
"
# 输出示例:
# ;; ANSWER SECTION:
# web. 600 IN A 172.56.0.2
# web. 600 IN A 172.56.0.3
# web. 600 IN A 172.56.0.4
# TTL=600秒(10分钟)
# 对比不同客户端的DNS缓存行为
# curl: 默认缓存1分钟
# wget: 不缓存DNS
# Python requests: 依赖操作系统的DNS缓存
# Java: 默认缓存30秒(可通过networkaddress.cache.ttl配置)
# 测试: 不缓存的客户端(每次都重新解析)
docker run -it --rm --network dns-lb-net alpine sh -c "
for i in 1 2 3 4 5 6 7 8 9 10; do
# wget不缓存DNS,每次都重新查询
wget -qO- http://web/ 2>/dev/null | grep -o 'Server ID: [0-9]' || echo 'failed'
done
"
# 输出可能更均匀,因为wget不缓存DNS
Docker DNS的实现细节: Docker的内置DNS服务器并非一个独立的进程,而是嵌入在Docker Daemon中的功能模块。当容器启动并连接到自定义网络时,Docker会在容器的网络命名空间中配置iptables规则,将所有发往53端口的DNS查询重定向到127.0.0.11。这个地址实际上是一个特殊的本地地址,Docker Daemon监听这个地址上的DNS查询请求。当收到查询时,Docker首先检查查询的名称是否是同一网络中某个容器或别名的名称——如果是,返回对应容器的IP地址;如果不是,将查询转发到宿主机配置的外部DNS服务器。
# 查看容器中DNS相关的iptables规则
docker run -it --rm --network my-net alpine sh -c "
iptables -t nat -L -n -v 2>/dev/null | head -30
"
# 可以看到将UDP 53端口流量重定向到127.0.0.11的规则
# 查看Docker DNS查询日志
# 需要开启Docker的debug日志
# /etc/docker/daemon.json:
# { "debug": true }
# 然后查看Docker日志
journalctl -u docker | grep -i dns
# 可以看到DNS查询和响应的日志
# 测试DNS转发行为
# 在容器内解析外部域名
docker run -it --rm --network my-net alpine sh -c "
apk add bind-tools
# 解析容器名(由Docker内置DNS处理)
dig my-container-name
# 解析外部域名(由Docker转发到外部DNS)
dig www.google.com
"
9.7 外部DNS与容器DNS的交互
Docker内置DNS不仅负责容器名解析,还负责将非容器名的查询转发到外部DNS服务器:
# 在容器内解析外部域名
docker run -it --rm --network my-net alpine sh -c "
echo '=== 解析容器名 ==='
nslookup some-container
echo '=== 解析外部域名 ==='
nslookup www.google.com
nslookup github.com
"
# DNS查询流程:
# 1. 查询 some-container → 内置DNS查找注册表 → 返回容器IP
# 2. 查询 www.google.com → 内置DNS不在注册表中 → 转发到外部DNS → 返回IP
# 查看DNS转发配置
# Docker使用宿主机的/etc/resolv.conf作为转发目标
# 可以在创建容器时覆盖
docker run -d \
--name ext-dns-test \
--dns 114.114.114.114 \
alpine sleep 3600
# 此容器的DNS查询流程:
# 1. 容器名查询 → 127.0.0.11 (内置DNS)
# 2. 外部域名查询 → 127.0.0.11 → 转发到 114.114.114.114
# 验证
docker exec ext-dns-test nslookup www.baidu.com
# 使用114.114.114.114解析
# 清理
docker rm -f ext-dns-test
DNS解析链路的完整分析: 当容器内的应用程序发起一个域名解析请求时,这个请求会经过一个完整的解析链路。首先,应用程序会检查本地的/etc/hosts文件,如果找到匹配的条目,直接返回对应的IP地址,不再进行DNS查询。如果/etc/hosts中没有匹配项,应用程序会向/etc/resolv.conf中配置的DNS服务器(即127.0.0.11)发起查询。Docker内置DNS收到查询后,首先在自己的注册表中查找——如果查询的名称是同一网络中某个容器名或网络别名,返回对应容器的IP地址。如果注册表中没有匹配项,Docker内置DNS会将查询转发到外部DNS服务器(通常是宿主机/etc/resolv.conf中配置的DNS服务器,或通过--dns参数指定的DNS服务器)。外部DNS服务器解析完成后,将结果返回给Docker内置DNS,再由内置DNS返回给应用程序。
DNS解析的常见问题与调试: 在容器环境中,DNS解析问题是最常见的网络故障之一。典型的问题包括:容器名无法解析(通常是因为使用了默认bridge网络而非自定义网络)、外部域名解析超时(通常是因为DNS服务器不可达或防火墙阻止了53端口)、DNS解析结果不正确(通常是因为/etc/hosts中的静态条目覆盖了DNS结果)。调试DNS问题时,可以使用dig、nslookup等工具进行逐步排查——先检查/etc/resolv.conf是否正确配置了127.0.0.11,然后使用dig命令测试容器名和外部域名的解析,最后检查iptables规则是否正确地将53端口的流量重定向到了127.0.0.11。
# 完整的DNS调试流程
docker run -it --rm --network my-net alpine sh -c "
apk add bind-tools
echo '=== 1. 检查resolv.conf ==='
cat /etc/resolv.conf
# 应该看到 nameserver 127.0.0.11
echo '=== 2. 检查hosts文件 ==='
cat /etc/hosts
# 查看是否有覆盖DNS的静态条目
echo '=== 3. 测试容器名解析 ==='
dig +short my-container-name
# 应该返回容器IP
echo '=== 4. 测试外部域名解析 ==='
dig +short www.google.com
# 应该返回外部IP
echo '=== 5. 测试DNS响应时间 ==='
dig www.google.com | grep 'Query time'
# 正常应小于50ms
echo '=== 6. 检查DNS重定向规则 ==='
iptables -t nat -L -n 2>/dev/null | grep '127.0.0.11'
# 应该看到53端口重定向到127.0.0.11的规则
"
9.8 服务发现实战: 微服务注册与发现
让我们通过一个完整的微服务示例来展示Docker DNS服务发现的应用:
# 场景: 微服务架构
# - API Gateway (入口)
# - User Service (用户服务)
# - Order Service (订单服务)
# - Product Service (商品服务)
# - Redis (缓存)
# - PostgreSQL (数据库)
# 1. 创建微服务网络
docker network create microservice-net
# 2. 部署PostgreSQL
docker run -d \
--name user-db \
--network microservice-net \
--network-alias postgres \
--network-alias user-database \
-e POSTGRES_DB=users \
-e POSTGRES_USER=svc \
-e POSTGRES_PASSWORD=secret \
postgres:15-alpine
# 3. 部署Redis
docker run -d \
--name cache \
--network microservice-net \
--network-alias redis \
--network-alias cache-server \
redis:alpine
# 4. 创建并部署User Service
mkdir -p /tmp/user-service
cat > /tmp/user-service/app.py << 'PYEOF'
from flask import Flask, jsonify
import redis
import psycopg2
import os, socket
app = Flask(__name__)
# 通过DNS服务发现连接Redis和PostgreSQL
# 不需要硬编码IP地址,只需使用服务名
redis_client = redis.Redis(
host='redis', # DNS解析到Redis容器
port=6379,
decode_responses=True
)
def get_db_conn():
return psycopg2.connect(
host='postgres', # DNS解析到PostgreSQL容器
user='svc',
password='secret',
database='users'
)
@app.route('/health')
def health():
return jsonify({
'status': 'ok',
'service': 'user-service',
'hostname': socket.gethostname()
})
@app.route('/users/<int:user_id>')
def get_user(user_id):
# 先查缓存
cache_key = f'user:{user_id}'
cached = redis_client.get(cache_key)
if cached:
return jsonify({'source': 'cache', 'data': cached})
# 查数据库
conn = get_db_conn()
cur = conn.cursor()
cur.execute('CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT, email TEXT)')
cur.execute('INSERT INTO users (name, email) VALUES (%s, %s) ON CONFLICT DO NOTHING',
('User_' + str(user_id), f'user{user_id}@example.com'))
conn.commit()
cur.execute('SELECT id, name, email FROM users WHERE id = %s', (user_id,))
row = cur.fetchone()
conn.close()
if row:
user_data = {'id': row[0], 'name': row[1], 'email': row[2]}
redis_client.setex(cache_key, 300, str(user_data))
return jsonify({'source': 'database', 'data': user_data})
return jsonify({'error': 'not found'}), 404
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
PYEOF
cat > /tmp/user-service/requirements.txt << 'EOF'
flask==3.0.0
redis==5.0.0
psycopg2-binary==2.9.9
EOF
cat > /tmp/user-service/Dockerfile << 'EOF'
FROM python:3.11-alpine
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]
EOF
docker build -t user-service /tmp/user-service/
# 启动User Service (2个实例,使用相同别名)
docker run -d --name user-svc-1 --network microservice-net --network-alias user-service user-service
docker run -d --name user-svc-2 --network microservice-net --network-alias user-service user-service
# 5. 创建并部署API Gateway (Nginx)
cat > /tmp/api-gateway/nginx.conf << 'EOF'
upstream user_service {
# 通过DNS服务发现,自动负载均衡到多个实例
server user-service:5000;
}
server {
listen 80;
location /user-service/ {
proxy_pass http://user_service/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /health {
return 200 '{"status":"ok","service":"api-gateway"}';
add_header Content-Type application/json;
}
}
EOF
cat > /tmp/api-gateway/Dockerfile << 'EOF'
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
EOF
docker build -t api-gateway /tmp/api-gateway/
docker run -d --name gateway --network microservice-net -p 8080:80 api-gateway
# 6. 测试微服务
# 测试健康检查
curl http://localhost:8080/health
# 测试用户服务(多次请求会分发到不同实例)
for i in 1 2 3 4 5; do
curl -s http://localhost:8080/user-service/health | python3 -m json.tool
echo "---"
done
# 测试获取用户信息(第一次从数据库,第二次从缓存)
curl http://localhost:8080/user-service/users/1 # 数据库
curl http://localhost:8080/user-service/users/1 # 缓存
# 7. 验证DNS服务发现
# 从gateway容器内查看DNS解析
docker exec gateway nslookup user-service
# 返回两个IP (user-svc-1和user-svc-2)
# 从user-svc容器内查看其他服务
docker exec user-svc-1 nslookup redis
docker exec user-svc-1 nslookup postgres
# 8. 动态扩容: 再启动一个user-service实例
docker run -d --name user-svc-3 --network microservice-net --network-alias user-service user-service
# 验证DNS自动更新
docker exec gateway nslookup user-service
# 现在返回三个IP
# 9. 模拟服务故障
docker stop user-svc-1
# DNS记录自动更新
docker exec gateway nslookup user-service
# user-svc-1的IP被移除
# 清理
docker rm -f gateway user-svc-1 user-svc-2 user-svc-3 cache user-db
docker network rm microservice-net
这个实战案例完整展示了Docker DNS服务发现的核心价值:
- 服务通过名称而非IP通信,无需硬编码IP地址
- 新实例加入后自动被DNS发现,实现自动扩容
- 实例故障后DNS自动移除,实现自动故障转移
- 通过别名实现简单的负载均衡
第十章 网络安全与故障排查
网络安全和故障排查是Docker网络管理中不可或缺的环节。本章将全面讲解容器网络的安全风险、隔离策略、流量审计、故障排查方法和性能优化,帮助你构建安全可靠的Docker网络环境。
10.1 容器网络安全风险分析
Docker容器网络存在多种安全风险,了解这些风险是做好安全防护的前提:
风险一: 容器逃逸
如果攻击者获得了容器的控制权,可能通过内核漏洞或Docker API逃逸到宿主机。网络层面,攻击者可能通过容器访问宿主机的其他服务。
# 风险示例: 容器可以访问宿主机上的所有服务
# 如果宿主机上运行了Redis(绑定127.0.0.1:6379)
# 容器可以通过docker0网桥的IP(172.17.0.1)访问
docker exec container redis-cli -h 172.17.0.1 -p 6379
# 防护措施: 使用内部网络隔离敏感服务
# 或使用iptables限制容器访问宿主机服务
iptables -A DOCKER-USER -i docker0 -d 172.17.0.1 -j DROP
# 禁止容器访问docker0网桥IP上的所有服务
风险二: 端口暴露
不必要的端口暴露会增加攻击面:
# 检查所有暴露的端口
docker ps --format "{{.Names}}: {{.Ports}}"
# 风险: 数据库端口暴露到公网
docker run -d -p 3306:3306 mysql:8.0
# 任何人都可以通过宿主机IP:3306访问数据库
# 正确做法: 只绑定到localhost或内网IP
docker run -d -p 127.0.0.1:3306:3306 mysql:8.0
# 或
docker run -d -p 192.168.1.10:3306:3306 mysql:8.0
风险三: 同一bridge网络中的容器互相攻击
在默认bridge网络中,所有容器可以互相访问,一个被攻破的容器可能攻击其他容器:
# 防护措施1: 使用多个自定义网络隔离不同应用
docker network create frontend-net
docker network create backend-net
docker network create --internal db-net
# 防护措施2: 禁用ICC(容器间通信)
docker network create -o com.docker.network.bridge.enable_icc=false isolated-net
# 防护措施3: 使用iptables细粒度控制
# 只允许app容器访问db容器的3306端口
APP_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' app)
DB_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' db)
iptables -I DOCKER-USER -s $APP_IP -d $DB_IP -j DROP
iptables -I DOCKER-USER -s $APP_IP -d $DB_IP -p tcp --dport 3306 -j ACCEPT
风险四: ARP欺骗与MAC欺骗
在bridge网络中,恶意容器可能进行ARP欺骗,劫持其他容器的流量:
# 防护措施: Docker的网桥默认不启用ARP检查
# 可以通过以下方式增强安全:
# 1. 使用自定义网络并限制容器间通信
# 2. 使用network namespace的ARP限制
# 3. 使用支持ARP检查的网络插件
10.2 网络隔离最佳实践
分层网络架构:
┌─────────────────────────────────────────────────────────────┐
│ 外部网络 (Internet) │
│ │ │
│ ┌────┴────┐ │
│ │ 防火墙 │ │
│ └────┬────┘ │
│ │ │
│ ┌──────────────────────┴────────────────────────────────┐ │
│ │ 宿主机 │ │
│ │ │ │
│ │ ┌─────────────┐ ┌──────────────┐ ┌───────────┐ │ │
│ │ │ DMZ网络 │ │ 应用网络 │ │ 数据网络 │ │ │
│ │ │ (前端Web) │───→│ (后端API) │───→│ (数据库) │ │ │
│ │ │ 端口映射 │ │ 内部通信 │ │ 内部网络 │ │ │
│ │ │ -p 80:80 │ │ 无端口暴露 │ │ --internal│ │ │
│ │ └─────────────┘ └──────────────┘ └───────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
# 实现分层网络架构
# 1. 创建DMZ网络(前端,有端口映射)
docker network create --subnet 172.60.0.0/16 dmz-net
# 2. 创建应用网络(后端,无端口映射)
docker network create --subnet 172.61.0.0/16 app-net
# 3. 创建数据网络(数据库,内部网络)
docker network create --subnet 172.62.0.0/16 --internal data-net
# 4. 部署数据库(只在data-net中)
docker run -d \
--name mysql \
--network data-net \
--network-alias database \
-e MYSQL_ROOT_PASSWORD=secret \
mysql:8.0
# 5. 部署后端API(连接app-net和data-net)
docker run -d \
--name api \
--network app-net \
--network-alias backend \
myapp-api
docker network connect --alias db-access data-net api
# 6. 部署前端(连接dmz-net和app-net)
docker run -d \
--name web \
--network dmz-net \
-p 80:80 \
--network-alias frontend \
myapp-nginx
docker network connect app-net web
# 安全验证:
# 1. 外部只能访问web的80端口
# 2. web可以访问api(通过app-net)
# 3. api可以访问mysql(通过data-net)
# 4. web无法直接访问mysql(不在同一网络)
# 5. mysql无法访问外网(internal网络)
# 验证web无法直接访问mysql
docker exec web ping -c 1 database
# ping: bad address 'database' (无法解析,不在同一网络)
# 验证mysql无法访问外网
docker exec mysql ping -c 1 8.8.8.8
# Network is unreachable
# 清理
docker rm -f web api mysql
docker network rm dmz-net app-net data-net
零信任网络模型在容器环境中的应用: 传统的容器网络安全模型依赖于网络分段——即信任同一网络内的容器,只在不同网络之间设置访问控制。但这种模型在面对容器被攻破的攻击场景时显得不够充分。零信任网络模型的核心原则是"永不信任,始终验证",即使在同一网络内,容器之间的通信也应该经过身份认证和授权。在Docker环境中,可以通过以下方式实现零信任网络:首先,为每个容器配置唯一的TLS证书,容器间通信使用mTLS(双向TLS)进行身份认证;其次,使用网络策略(Network Policy)精确控制容器间的通信规则——虽然Docker原生网络不直接支持网络策略,但可以使用Calico等CNI插件来实现;最后,对所有容器间通信进行日志记录和审计,便于事后追溯。
# 零信任网络实践: 使用自签名CA为每个容器签发证书
# 1. 创建CA
openssl genrsa -out ca-key.pem 4096
openssl req -new -x509 -days 3650 -key ca-key.pem -out ca-cert.pem \
-subj "/CN=Docker-Internal-CA"
# 2. 为每个容器签发证书
for svc in api web worker; do
openssl genrsa -out ${svc}-key.pem 2048
openssl req -new -key ${svc}-key.pem -out ${svc}.csr \
-subj "/CN=${svc}"
openssl x509 -req -in ${svc}.csr -CA ca-cert.pem -CAkey ca-key.pem \
-CAcreateserial -out ${svc}-cert.pem -days 365
done
# 3. 容器启动时挂载证书
docker run -d --name secure-api \
--network app-net \
-v $(pwd)/api-cert.pem:/certs/cert.pem:ro \
-v $(pwd)/api-key.pem:/certs/key.pem:ro \
-v $(pwd)/ca-cert.pem:/certs/ca.pem:ro \
-e TLS_ENABLED=true \
-e TLS_CERT=/certs/cert.pem \
-e TLS_KEY=/certs/key.pem \
-e TLS_CA=/certs/ca.pem \
my-secure-api
# 4. 验证: 未持有证书的容器无法访问
docker run -it --rm --network app-net alpine \
wget --no-check-certificate -qO- https://api:8443/health
# 失败: 证书验证不通过
# 5. 持有证书的容器可以访问
docker run -it --rm --network app-net \
-v $(pwd)/web-cert.pem:/certs/cert.pem:ro \
-v $(pwd)/web-key.pem:/certs/key.pem:ro \
-v $(pwd)/ca-cert.pem:/certs/ca.pem:ro \
alpine sh -c "
wget --certificate=/certs/cert.pem \
--private-key=/certs/key.pem \
--ca-certificate=/certs/ca.pem \
-qO- https://api:8443/health
"
# 成功: 返回健康状态
容器网络的流量监控与可视化: 在生产环境中,仅依靠iptables规则和tcpdump进行网络安全管理是不够的。需要部署持续的网络流量监控系统,实时检测异常流量模式。例如,如果某个通常只与数据库通信的API容器突然开始与外部IP建立大量连接,这可能是容器被入侵后试图外泄数据的信号。可以使用Prometheus + Grafana监控容器网络指标,使用Falco或Sysdig等运行时安全工具检测异常网络行为。
# 使用ctop实时监控容器网络流量
# 安装ctop
wget https://github.com/bcicen/ctop/releases/download/v0.7.7/ctop-0.7.7-linux-amd64 -O /usr/local/bin/ctop
chmod +x /usr/local/bin/ctop
# 运行ctop,可以实时查看每个容器的网络IO
ctop
# 使用Docker stats查看网络统计
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}"
# 导出容器网络统计(cumulative)
docker inspect --format '{{.Name}}: RX={{.NetworkSettings.RxBytes}} TX={{.NetworkSettings.TxBytes}}' $(docker ps -q)
# 使用nethogs按进程监控网络流量
nethogs docker0
# 安装Falco进行运行时安全监控
# Falco可以检测异常的网络行为,如:
# - 容器内出现了不期望的网络连接
# - 容器向外部发送了大量数据
# - 容器监听了非预期的端口
docker run -d --name falco \
--privileged \
-v /var/run/docker.sock:/host/var/run/docker.sock \
-v /dev:/host/dev \
-v /proc:/host/proc:ro \
-v /boot:/host/boot:ro \
-v /lib/modules:/host/lib/modules:ro \
-v /usr:/host/usr:ro \
falcosecurity/falco:latest
10.3 iptables规则审计
定期审计Docker创建的iptables规则,确保没有安全漏洞:
# === iptables审计脚本 ===
# 1. 查看所有Docker相关的iptables规则
echo "=== NAT表 PREROUTING ==="
iptables -t nat -L PREROUTING -n -v --line-numbers
echo "=== NAT表 DOCKER链 ==="
iptables -t nat -L DOCKER -n -v --line-numbers
echo "=== NAT表 POSTROUTING ==="
iptables -t nat -L POSTROUTING -n -v --line-numbers
echo "=== Filter表 DOCKER-USER链 ==="
iptables -t filter -L DOCKER-USER -n -v --line-numbers
echo "=== Filter表 DOCKER链 ==="
iptables -t filter -L DOCKER -n -v --line-numbers
echo "=== Filter表 FORWARD链 ==="
iptables -t filter -L FORWARD -n -v --line-numbers
# 2. 检查是否有过于宽松的规则
# 重点关注:
# - 是否有ACCEPT所有流量的规则
# - 是否有不必要的MASQUERADE规则
# - 端口映射规则是否合理
# 3. 添加自定义安全规则
# 限制容器到宿主机的访问
iptables -I DOCKER-USER -i docker0 -d 172.17.0.1 -j DROP
# 限制特定容器之间的通信
# 假设只允许web容器访问api容器的8080端口
WEB_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web)
API_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' api)
# 默认拒绝web到api的所有流量
iptables -I DOCKER-USER -s $WEB_IP -d $API_IP -j DROP
# 只允许8080端口
iptables -I DOCKER-USER -s $WEB_IP -d $API_IP -p tcp --dport 8080 -j ACCEPT
# 4. 持久化iptables规则
# CentOS/RHEL
iptables-save > /etc/sysconfig/iptables
# Ubuntu/Debian
iptables-save > /etc/iptables/rules.v4
# 5. 审计端口映射
# 列出所有容器的端口映射
echo "=== 容器端口映射审计 ==="
docker ps --format "table {{.Names}}\t{{.Ports}}\t{{.Networks}}" | \
awk '{print $1, $2, $3}'
# 检查是否有绑定到0.0.0.0的敏感端口
echo "=== 检查敏感端口暴露 ==="
docker ps --format "{{.Ports}}" | grep -E "0\.0\.0\.0:(3306|5432|6379|27017|9200)"
# 如果有输出,说明敏感数据库端口暴露到了所有接口
10.4 容器网络流量抓包(tcpdump/wireshark)
tcpdump是排查网络问题的利器。以下是容器网络抓包的常用技巧:
# === 基础抓包操作 ===
# 1. 在docker0网桥上抓包
tcpdump -i docker0 -n
# 2. 抓取特定容器的流量
# 先获取容器的veth接口名
CONTAINER_PID=$(docker inspect -f '{{.State.Pid}}' my-container)
VETH=$(ip link | grep -B1 "master docker0" | grep -o "veth[0-9a-f]*" | head -1)
# 或更精确地:
VETH=$(nsenter -t $CONTAINER_PID -n ip link show eth0 | grep -oP '^\d+: \K[^@]+')
# 在veth接口上抓包
tcpdump -i $VETH -n
# 3. 在容器内部抓包(需要安装tcpdump)
docker exec my-container tcpdump -i eth0 -n -w /tmp/capture.pcap
docker cp my-container:/tmp/capture.pcap /tmp/capture.pcap
# 4. 抓取特定协议的流量
# HTTP流量
tcpdump -i docker0 -n port 80
# HTTPS流量
tcpdump -i docker0 -n port 443
# DNS流量
tcpdump -i docker0 -n port 53
# ICMP流量(ping)
tcpdump -i docker0 -n icmp
# 5. 抓取特定容器间的流量
C1_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' container1)
C2_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' container2)
tcpdump -i docker0 -n host $C1_IP and host $C2_IP
# === 高级抓包技巧 ===
# 6. 抓包并保存为pcap文件(供Wireshark分析)
tcpdump -i docker0 -w /tmp/docker-traffic.pcap
# 在Wireshark中打开
# wireshark /tmp/docker-traffic.pcap
# 7. 抓取HTTP请求内容
tcpdump -i docker0 -n -A port 80
# 8. 抓取DNS查询
tcpdump -i docker0 -n port 53 -vv
# 9. 限制抓包数量
tcpdump -i docker0 -n -c 100 # 只抓100个包
# 10. 抓取TCP三次握手
tcpdump -i docker0 -n 'tcp[tcpflags] & tcp-syn != 0'
# === 实战: 排查容器间通信问题 ===
# 场景: container1无法访问container2的HTTP服务
# 步骤1: 抓取container1发出的流量
tcpdump -i docker0 -n src $C1_IP and dst $C2_IP
# 步骤2: 查看是否有SYN包但无SYN-ACK响应
# 如果只有SYN,说明container2没有响应,可能是端口未监听或iptables阻止
# 步骤3: 在container2上抓包
docker exec container2 tcpdump -i eth0 -n port 80
# 如果container2收到了SYN但没有响应,检查服务是否运行
docker exec container2 ss -tlnp | grep 80
# 步骤4: 检查iptables是否阻止
iptables -t filter -L DOCKER-USER -n -v
iptables -t filter -L FORWARD -n -v
10.5 容器网络连通性测试
系统化的容器网络连通性测试方法:
# === 连通性测试工具箱 ===
# 创建测试网络和容器
docker network create test-connectivity
docker run -d --name test-src --network test-connectivity alpine sleep 3600
docker run -d --name test-dst --network test-connectivity nginx:alpine
# 1. DNS解析测试
echo "=== DNS解析测试 ==="
docker exec test-src nslookup test-dst
# 如果失败,检查:
# - 是否在自定义网络中(默认bridge不支持DNS)
# - /etc/resolv.conf是否指向127.0.0.11
# 2. ICMP连通性测试
echo "=== ICMP测试 ==="
docker exec test-src ping -c 3 test-dst
# 如果失败,检查:
# - iptables FORWARD链是否允许
# - 容器是否在同一网络
# 3. TCP端口测试
echo "=== TCP端口测试 ==="
docker exec test-src wget -qO- --timeout=3 http://test-dst:80
# 或使用nc
docker exec test-src sh -c "nc -zv test-dst 80 2>&1"
# 4. 路由测试
echo "=== 路由测试 ==="
docker exec test-src traceroute test-dst
# 或
docker exec test-src sh -c "apk add mtr && mtr -n --c 3 test-dst"
# 5. 外部网络连通性测试
echo "=== 外部网络测试 ==="
docker exec test-src ping -c 2 8.8.8.8
# 如果失败,检查MASQUERADE规则
iptables -t nat -L POSTROUTING -n -v | grep MASQUERADE
# 6. 端口映射测试
echo "=== 端口映射测试 ==="
# 从宿主机访问
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080
# 从外部访问
# 在另一台机器上: curl http://<宿主机IP>:8080
# 7. MTU测试
echo "=== MTU测试 ==="
# 测试大包是否能通过
docker exec test-src ping -c 2 -s 1400 test-dst
# 如果大包失败但小包成功,可能是MTU问题
# 清理
docker rm -f test-src test-dst
docker network rm test-connectivity
10.6 常见网络问题排查清单
# === Docker网络问题排查清单 ===
# 1. 容器无法获取IP
echo "--- 问题1: 容器无法获取IP ---"
# 检查docker0网桥
ip addr show docker0
# 检查子网是否耗尽
docker network inspect bridge | grep -A5 IPAM
# 检查是否有足够的IP地址
# 解决: 创建新的自定义网络或调整子网范围
# 2. 容器无法访问外网
echo "--- 问题2: 容器无法访问外网 ---"
# 检查MASQUERADE规则
iptables -t nat -L POSTROUTING -n -v | grep MASQUERADE
# 检查IP转发是否开启
sysctl net.ipv4.ip_forward
# 如果为0,开启IP转发
sysctl -w net.ipv4.ip_forward=1
# 检查FORWARD链
iptables -t filter -L FORWARD -n -v
# 3. 端口映射不生效
echo "--- 问题3: 端口映射不生效 ---"
# 检查iptables DNAT规则
iptables -t nat -L DOCKER -n -v
# 检查docker-proxy进程
ps aux | grep docker-proxy
# 检查端口是否被占用
ss -tlnp | grep <端口号>
# 检查firewalld是否干扰
systemctl status firewalld
# 4. 容器名无法解析(DNS问题)
echo "--- 问题4: DNS解析失败 ---"
# 检查是否使用自定义网络(默认bridge不支持DNS)
docker inspect <容器名> --format '{{json .NetworkSettings.Networks}}' | python3 -m json.tool
# 检查/etc/resolv.conf
docker exec <容器名> cat /etc/resolv.conf
# 应该包含 nameserver 127.0.0.11
# 5. overlay网络容器无法跨主机通信
echo "--- 问题5: overlay跨主机不通 ---"
# 检查VXLAN端口是否开放
# 在所有节点上检查:
nc -uvz <对端IP> 4789
# 检查VXLAN模块
lsmod | grep vxlan
# 检查Swarm节点状态
docker node ls
# 检查overlay网络详情
docker network inspect <overlay网络名>
# 6. 网络性能差
echo "--- 问题6: 网络性能差 ---"
# 检查MTU
docker exec <容器名> ip link show eth0
# 检查是否有大量重传
docker exec <容器名> cat /proc/net/snmp | grep -i tcp
# 检查iptables规则数量(过多会影响性能)
iptables -L -n | wc -l
# 检查conntrack表是否满
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max
# 7. 容器频繁断连
echo "--- 问题7: 容器频繁断连 ---"
# 检查conntrack超时
sysctl net.netfilter.nf_conntrack_tcp_timeout_established
# 检查ARP缓存
ip neigh show
# 检查是否有ARP表溢出
cat /proc/sys/net/ipv4/neigh/default/gc_thresh3
10.7 Docker网络性能优化
# === 网络性能优化 ===
# 1. 禁用docker-proxy(减少用户态开销)
# /etc/docker/daemon.json
cat > /etc/docker/daemon.json << 'EOF'
{
"userland-proxy": false,
"max-concurrent-downloads": 10,
"max-concurrent-uploads": 5
}
EOF
systemctl restart docker
# 2. 调整MTU
# 如果网络中有VPN或隧道,可能需要减小MTU
docker network create -o com.docker.network.mtu=1400 my-net
# 3. 调整conntrack参数
# 增大conntrack表
sysctl -w net.netfilter.nf_conntrack_max=262144
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=86400
# 4. 调整网络缓冲区
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
sysctl -w net.ipv4.tcp_rmem="4096 87380 16777216"
sysctl -w net.ipv4.tcp_wmem="4096 65536 16777216"
# 5. 开启TCP快速打开
sysctl -w net.ipv4.tcp_fastopen=3
# 6. 使用BBR拥塞控制
sysctl -w net.core.default_qdisc=fq
sysctl -w net.ipv4.tcp_congestion_control=bbr
# 7. 性能对比测试
# bridge vs host vs macvlan
echo "=== bridge性能 ==="
docker run -d --name perf-bridge --network bridge iperf3:latest -s
docker run --rm --network bridge iperf3:latest -c perf-bridge -t 5
echo "=== host性能 ==="
docker run -d --name perf-host --network host iperf3:latest -s
docker run --rm --network host iperf3:latest -c 127.0.0.1 -t 5
# 预期结果: host > macvlan > bridge > overlay
# 8. 减少iptables规则数量
# 定期清理无用的端口映射
docker container prune -f
# 避免创建过多网络
docker network prune -f
# 9. 持久化优化配置
cat >> /etc/sysctl.conf << 'EOF'
# Docker网络优化
net.ipv4.ip_forward=1
net.netfilter.nf_conntrack_max=262144
net.core.rmem_max=16777216
net.core.wmem_max=16777216
net.ipv4.tcp_congestion_control=bbr
net.core.default_qdisc=fq
EOF
sysctl -p
网络性能优化的深度分析:
Docker容器网络性能优化的核心在于理解不同网络模式的性能特征,以及找出当前网络架构中的性能瓶颈。在bridge网络模式下,数据包需要经过veth pair、网桥转发和NAT处理,每个环节都会引入一定的开销。veth pair本身是一个虚拟设备对,数据在一端写入后需要通过内核网络栈传递到另一端,这个过程涉及上下文切换和内存拷贝。网桥转发需要查找MAC地址转发表,虽然查找速度很快,但在容器数量极多时(数百个容器连接到同一网桥),转发表的大小和查找时间可能成为瓶颈。NAT处理(iptables MASQUERADE和DNAT)需要修改数据包的IP头和端口信息,并维护conntrack连接追踪表,这是bridge网络中性能开销最大的环节。
# 使用perf工具分析网络性能瓶颈
# 安装perf
yum install -y perf # CentOS/RHEL
# 或
apt install -y linux-tools-common # Ubuntu/Debian
# 分析Docker网络相关的内核函数调用
perf record -a -g -- sleep 10
perf report | grep -E "(veth|bridge|iptables|nf_conntrack)"
# 使用iperf3进行详细的网络性能测试
# 测试TCP吞吐量
docker run -d --name iperf-server --network bridge iperf3:latest -s
docker run --rm --network bridge iperf3:latest -c iperf-server -t 30 -P 4
# -P 4: 使用4个并行流测试聚合吞吐量
# 测试UDP吞吐量
docker run --rm --network bridge iperf3:latest -c iperf-server -u -b 10G -t 10
# 测试延迟
docker run --rm --network bridge iperf3:latest -c iperf-server ---latency -t 10
# 测试不同包大小对性能的影响
for size in 64 256 1024 4096 16384; do
echo -n "包大小 ${size}B: "
docker run --rm --network bridge iperf3:latest -c iperf-server -t 3 -l $size -J | \
python3 -c "import json,sys; d=json.load(sys.stdin); print(f'{d[\"end\"][\"sum_received\"][\"bits_per_second\"]/1e6:.1f} Mbps')"
done
docker-proxy禁用的利弊分析: docker-proxy是Docker为每个端口映射启动的用户态代理进程。它的存在是为了处理某些iptables无法覆盖的边缘场景,例如容器内部通过宿主机的外部IP访问映射端口的场景(hairpin NAT场景)。但docker-proxy运行在用户态,每个连接都需要在内核态和用户态之间切换,性能较差。在大多数生产环境中,如果iptables规则配置正确且不需要hairpin NAT场景,可以安全地禁用docker-proxy。禁用后,端口映射完全由iptables处理,减少了用户态开销,性能提升约5%-10%。但需要注意,禁用docker-proxy后,如果容器内部需要通过宿主机的外部IP访问映射端口,可能会失败。
内核网络参数调优详解: Linux内核提供了大量可调网络参数,合理配置这些参数可以显著提升Docker容器的网络性能。其中最重要的参数包括:TCP缓冲区大小(rmem_max和wmem_max)决定了TCP连接的发送和接收缓冲区上限,增大这些值可以提升高延迟高带宽链路的吞吐量;conntrack表大小(nf_conntrack_max)在高并发场景下尤为关键,如果conntrack表满,新的连接将被拒绝;TCP拥塞控制算法的选择也会影响网络性能,传统的CUBIC算法适合大多数场景,而BBR算法在跨WAN的高延迟链路上表现更好。
# 查看当前内核网络参数
sysctl -a | grep -E "(net.core|net.ipv4.tcp)" | head -30
# 推荐的生产环境网络参数配置
cat > /etc/sysctl.d/99-docker-network.conf << 'EOF'
# IP转发
net.ipv4.ip_forward = 1
# conntrack表
net.netfilter.nf_conntrack_max = 262144
net.netfilter.nf_conntrack_buckets = 65536
net.netfilter.nf_conntrack_tcp_timeout_established = 86400
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
# TCP缓冲区
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# TCP性能
net.ipv4.tcp_congestion_control = bbr
net.core.default_qdisc = fq
net.ipv4.tcp_fastopen = 3
net.ipv4.tcp_slow_start_after_idle = 0
net.ipv4.tcp_mtu_probing = 1
# 连接复用
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
# ARP缓存
net.ipv4.neigh.default.gc_thresh1 = 1024
net.ipv4.neigh.default.gc_thresh2 = 2048
net.ipv4.neigh.default.gc_thresh3 = 4096
EOF
sysctl -p /etc/sysctl.d/99-docker-network.conf
10.8 生产环境网络架构方案
方案一: 单机多容器架构(中小规模)
Internet
│
┌────┴────┐
│ Nginx │ (端口映射80/443)
│ 反向代理 │
└────┬────┘
│ (自定义bridge: web-net)
┌────────┼────────┐
│ │ │
┌───┴──┐ ┌──┴───┐ ┌──┴───┐
│Web 1 │ │Web 2 │ │Web 3 │
└──────┘ └──────┘ └──────┘
│
(自定义bridge: app-net)
│
┌───┴──┐
│ API │
└──┬───┘
│
(内部网络: db-net, --internal)
│
┌───────┴───────┐
│ │
┌───┴───┐ ┌────┴───┐
│MySQL │ │ Redis │
└───────┘ └────────┘
方案二: 多机Swarm集群架构(中大规模)
Internet
│
┌────┴────┐
│ HAProxy │ (外部负载均衡)
└────┬────┘
│
┌───────────┼───────────┐
│ │ │
┌────┴───┐ ┌───┴────┐ ┌───┴────┐
│Node 1 │ │Node 2 │ │Node 3 │
│Manager │ │Worker │ │Worker │
└────┬───┘ └───┬────┘ └───┬────┘
│ │ │
└──── overlay网络 ─────┘
(VXLAN)
┌────────┼────────┐
│ │ │
Web服务 API服务 数据库
(3副本) (3副本) (1副本)
Swarm集群网络架构的设计考量: 在设计多机Swarm集群的网络架构时,需要考虑多个层面的网络需求。首先是外部流量入口层——通常使用HAProxy或Nginx作为外部负载均衡器,将流量分发到各个节点上的Web服务。外部负载均衡器可以配置为四层(TCP)或七层(HTTP)模式,四层模式下流量直接转发到容器的端口,七层模式下可以根据URL路径进行智能路由。其次是跨主机容器通信层——通过overlay网络实现不同节点上的容器之间的透明通信。overlay网络的选择需要考虑性能因素:非加密的overlay网络性能较好但流量明文传输,加密的overlay网络安全性更高但性能下降约20%。最后是数据层——数据库和缓存通常部署在特定节点上,通过overlay网络与API服务通信。对于数据一致性要求高的场景,数据库应使用单副本并配合数据卷持久化,而不是多副本部署。
# Swarm集群网络架构的部署示例
# 1. 创建分层overlay网络
docker network create -d overlay --attachable web-overlay
docker network create -d overlay --attachable api-overlay
docker network create -d overlay --internal db-overlay
# 2. 部署数据库服务(只在db-overlay中)
docker service create --name postgres \
--network db-overlay \
--constraint 'node.role==manager' \
--mount type=volume,source=pg-data,target=/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
postgres:15
# 3. 部署API服务(连接api-overlay和db-overlay)
docker service create --name api \
--network api-overlay \
--network db-overlay \
--replicas 3 \
-e DB_HOST=postgres \
my-api:latest
# 4. 部署Web服务(连接web-overlay和api-overlay)
docker service create --name web \
--network web-overlay \
--network api-overlay \
--replicas 3 \
-p 80:80 \
nginx:alpine
# 5. 验证网络隔离
# web服务无法直接访问数据库(不在同一网络)
docker exec $(docker ps -qf name=web) ping -c 1 postgres
# ping: bad address 'postgres' (无法解析)
方案三: Kubernetes CNI架构(大规模)
# 生产环境推荐使用Kubernetes + CNI插件
# 常见CNI选择:
# - Calico: BGP路由,高性能,网络策略
# - Flannel: 简单易用,VXLAN或host-gw
# - Cilium: eBPF,高性能,可观测性
# - Weave: 加密,简单易用
# 以下是Docker Swarm的Stack文件示例(替代方案):
cat > /tmp/production-stack.yml << 'EOF'
version: '3.8'
services:
# 前端负载均衡
lb:
image: haproxy:2.8
ports:
- "80:80"
- "443:443"
networks:
- frontend
deploy:
replicas: 2
placement:
constraints: [node.role == manager]
# 前端Web
web:
image: myapp-web:latest
networks:
- frontend
- backend
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: any
# 后端API
api:
image: myapp-api:latest
networks:
- backend
- data
environment:
- DB_HOST=database
- REDIS_HOST=cache
deploy:
replicas: 3
# 数据库
database:
image: postgres:15
networks:
- data
volumes:
- db-data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
secrets:
- db_password
deploy:
replicas: 1
placement:
constraints: [node.role == manager]
# 缓存
cache:
image: redis:alpine
networks:
- data
deploy:
replicas: 1
networks:
frontend:
driver: overlay
backend:
driver: overlay
data:
driver: overlay
internal: true # 内部网络,隔离数据库
volumes:
db-data:
secrets:
db_password:
file: /run/secrets/db_password.txt
EOF
# 部署
docker stack deploy -c /tmp/production-stack.yml myapp
# 查看服务
docker service ls
docker stack services myapp
10.9 本章总结与下一期预告
本章总结:
本章我们从安全风险分析开始,涵盖了容器网络的分层隔离架构、iptables规则审计、tcpdump流量抓包、连通性测试、常见问题排查清单、性能优化以及生产环境架构方案。
关键要点回顾:
- 安全第一: 使用分层网络架构,敏感服务使用内部网络,端口映射绑定到特定IP
- 隔离为王: 不同安全级别的服务放在不同网络,通过容器连接多个网络实现受控通信
- 排查有法: 系统化的排查方法——从DNS到IP到端口到iptables,逐步缩小问题范围
- 性能可调: 通过禁用docker-proxy、调整MTU、优化内核参数提升网络性能
- 架构先行: 生产环境需要提前规划网络架构,单机用bridge,多机用overlay或Kubernetes CNI
全文总结:
本文从Docker网络的基础概念出发,深入剖析了以下核心内容:
- 第一章: 容器网络的核心问题(IP分配、端口映射、跨主机通信)和Linux网络基础(veth、bridge、iptables、route)
- 第二章: 七种网络模式(bridge、host、none、container、overlay、macvlan、ipvlan)的原理和对比
- 第三章: bridge网络的深度剖析,包括docker0网桥、IP分配、通信路径、iptables NAT规则、docker-proxy和hairpin NAT
- 第四章: 自定义网络管理,包括创建、配置、connect/disconnect、固定IP、网络别名和服务发现
- 第五章: 容器间通信的各种方式,包括同网络通信、跨网络通信、多网络连接、安全考量和负载均衡
- 第六章: 端口映射的完整语法、底层iptables DNAT规则分析和firewalld兼容性
- 第七章: overlay跨主机网络的VXLAN隧道原理、Swarm模式部署和性能分析
- 第八章: macvlan和ipvlan的物理网络直连方案,包括四种macvlan模式和ipvlan L2/L3模式
- 第九章: Docker内置DNS服务器(127.0.0.11)的工作原理、容器名解析、别名解析和DNS轮询负载均衡
- 第十章: 网络安全风险、隔离策略、iptables审计、tcpdump抓包、故障排查和性能优化
Docker网络是一个庞大而复杂的主题,涉及Linux内核网络栈的众多组件。真正掌握Docker网络,需要理解底层原理,并在实践中不断积累经验。希望这篇超过3万字的深度解析文章,能够帮助你建立起完整的Docker网络知识体系,在实际工作中做到"知其然,更知其所以然"。
学习路径建议: 对于想要深入学习Docker网络的读者,建议按照以下路径循序渐进。首先,扎实掌握Linux网络基础——网络命名空间、veth pair、网桥、路由和iptables这五个核心概念是理解一切容器网络技术的基础。可以通过手动创建网络命名空间和veth pair来加深理解。其次,深入实践bridge网络的各项功能——创建自定义网络、配置子网和网关、使用网络别名和DNS解析、进行端口映射和容器间通信。然后,学习跨主机网络方案——搭建Docker Swarm集群,创建overlay网络,理解VXLAN封装原理,实践跨主机容器通信和服务发现。最后,研究高级网络方案和安全实践——尝试macvlan和ipvlan模式,学习iptables规则审计和网络抓包,实践网络分段和零信任安全架构。
实践是最好的老师: 理论知识需要通过大量的动手实践才能真正内化。建议读者在虚拟机或云服务器上搭建实验环境,按照本文的示例逐步操作。遇到问题时,不要急于寻找答案,而是运用本文介绍的故障排查方法——从DNS解析到IP连通性到端口监听到iptables规则,系统化地定位问题根源。这种排查问题的能力,比记住任何命令都更有价值。
更多推荐
所有评论(0)