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定义的容器网络模型,它包含三个核心概念:

  1. Sandbox(沙箱): 对应一个容器的网络栈,包括网络命名空间、路由表、iptables规则等。一个Sandbox可以连接多个Endpoint。

  2. Endpoint(端点): 对应一个veth pair,连接Sandbox与Network。一个Endpoint属于一个Sandbox和一个Network。

  3. 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/16172.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在网络层面做了以下事情:

  1. 创建网络命名空间: Docker为容器创建一个独立的网络命名空间。

  2. 创建veth pair: Docker创建一对veth设备,一端放在容器的命名空间内(命名为eth0),另一端留在宿主机(命名为vethXXXXXX)。

  3. 连接到网桥: 宿主机端的veth设备被加入到docker0网桥。

  4. 分配IP地址: Docker的IPAM为容器的eth0分配一个IP地址(如172.17.0.2),并设置子网掩码。

  5. 配置路由: 容器内的路由表被设置,默认网关指向docker0的IP(172.17.0.1)。

  6. 配置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)

生产环境建议:

  1. 单机多容器: 使用自定义bridge网络(非默认docker0),获得DNS解析能力
  2. Swarm集群: 使用overlay网络,获得跨主机通信和服务发现能力
  3. 高性能需求: 评估host模式或macvlan模式
  4. 物理网络集成: 使用macvlan或ipvlan,但注意网络配置复杂性
  5. 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)发送数据时:

  1. 容器A通过ARP协议获取容器B的MAC地址
  2. 容器A将数据帧发送到eth0
  3. 数据帧通过veth pair到达宿主机的veth1
  4. veth1连接在docker0网桥上,docker0根据目标MAC地址查找FDB表
  5. docker0将数据帧从veth2转发出去
  6. 数据帧通过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的工作流程:

  1. Docker安装时,为默认bridge网络(docker0)分配子网172.17.0.0/16
  2. 每次创建新网络时,Docker从可用的子网池中选择一个未使用的子网
  3. 容器启动时,IPAM从网络对应的子网中分配一个未使用的IP地址
  4. 容器停止时,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按照以下顺序选择子网:

  1. 用户创建网络时指定的子网(--subnet)
  2. 从Docker配置的子网池中选择
  3. 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会在底层做两件事:

  1. 创建iptables DNAT规则: 将宿主机8080端口的流量转发到容器172.17.0.2:80
  2. 启动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规则在大多数情况下可以正常工作,但在以下场景中可能失效:

  1. 容器自己访问映射的端口(hairpin问题): 容器通过宿主机IP:端口访问自己的服务
  2. 宿主机内部访问: 从宿主机内部通过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

这个实战案例展示了:

  1. 使用自定义bridge网络(支持DNS解析)
  2. 通过容器名/网络别名进行服务间通信
  3. 端口映射让外部可以访问前端
  4. Nginx反向代理到后端API
  5. 后端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 connectdocker 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时:

  1. 容器内的DNS客户端向127.0.0.11(Docker内置DNS)发起查询
  2. Docker内置DNS查找注册表,发现web-server对应IP 172.43.0.2
  3. 返回IP地址给app-server
  4. app-server通过IP地址与web-server建立TCP连接
  5. 流量通过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

服务发现的特性:

  1. 自动注册: 容器启动时自动注册到DNS,停止时自动注销
  2. 实时更新: 容器IP变化时,DNS记录自动更新
  3. 网络作用域: DNS解析仅在容器所属的网络内有效
  4. 别名支持: 容器可以通过容器名或网络别名被解析
  5. 外部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

生产环境建议:

  1. 在Docker主机上,尽量使用iptables直接管理防火墙,而非firewalld
  2. 如果必须使用firewalld,确保Docker在firewalld之后启动
  3. 修改firewalld规则后,重启Docker以重建iptables规则
  4. 或者使用--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网络需要满足以下前提条件:

  1. 集群模式或key-value存储:

    • Docker Swarm模式(推荐)
    • 外部key-value存储(etcd/Consul/ZooKeeper)
  2. 主机间网络互通: 所有宿主机之间必须可以通过三层网络通信

  3. 防火墙规则开放: 需要开放以下端口:

    • TCP/UDP 2377: 集群管理通信
    • TCP/UDP 7946: 节点间通信
    • UDP 4789: VXLAN数据传输
  4. 内核支持: 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

容器名解析的特点:

  1. 自动注册: 容器启动时自动注册,无需手动配置
  2. 自动注销: 容器停止时DNS记录自动删除
  3. 网络作用域: 仅在同一自定义网络内有效
  4. 唯一性: 同一网络内容器名必须唯一

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问题时,可以使用dignslookup等工具进行逐步排查——先检查/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服务发现的核心价值:

  1. 服务通过名称而非IP通信,无需硬编码IP地址
  2. 新实例加入后自动被DNS发现,实现自动扩容
  3. 实例故障后DNS自动移除,实现自动故障转移
  4. 通过别名实现简单的负载均衡

第十章 网络安全与故障排查

网络安全和故障排查是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流量抓包、连通性测试、常见问题排查清单、性能优化以及生产环境架构方案。

关键要点回顾:

  1. 安全第一: 使用分层网络架构,敏感服务使用内部网络,端口映射绑定到特定IP
  2. 隔离为王: 不同安全级别的服务放在不同网络,通过容器连接多个网络实现受控通信
  3. 排查有法: 系统化的排查方法——从DNS到IP到端口到iptables,逐步缩小问题范围
  4. 性能可调: 通过禁用docker-proxy、调整MTU、优化内核参数提升网络性能
  5. 架构先行: 生产环境需要提前规划网络架构,单机用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规则,系统化地定位问题根源。这种排查问题的能力,比记住任何命令都更有价值。

更多推荐