4_Docker网络实战指南

Docker 网络是容器通信的基础设施层。选错网络模式可能带来安全暴露、性能损耗或排障噩梦。本文从 Linux 内核的网络原语出发,逐层拆解 Docker 网络的实现机制,覆盖单机网络、跨主机网络和生产实战。

1. 全景架构

1.1 容器网络模型(CNM)

如果没有统一的网络模型,每创建一个容器就要手动配置 IP、手动建虚拟网卡、手动写路由规则。10 个容器重复 10 次,容器一重启 IP 就变,之前的配置全部作废。

CNM(Container Network Model)是 Docker 的网络管理规范,由 libnetwork 库实现,把复杂的网络配置拆成三个组件:

概念类比作用对应 Linux 原语
Sandbox独立的房间容器的独立网络栈(网卡、路由表、DNS),互不干扰Network Namespace
Endpoint房间的网口连接 Sandbox 到 Network 的虚拟接口veth pair 的一端
Network交换机一组可互通的 Endpoint,接到同一个 Network 的容器才能通信Linux Bridge / VXLAN

一个 Sandbox 可以拥有多个 Endpoint,接入多个 Network——这是容器多网卡能力的基础。

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#3B82F6', 'primaryTextColor': '#1E3A5F', 'primaryBorderColor': '#2563EB', 'lineColor': '#60A5FA', 'secondaryColor': '#10B981', 'tertiaryColor': '#F59E0B'}}}%%
flowchart LR
    subgraph S1 [Sandbox: 容器 A]
        E1["Endpoint 1"]
        E2["Endpoint 2"]
    end
    subgraph S2 [Sandbox: 容器 B]
        E3["Endpoint 3"]
    end
    subgraph N1 [Network: frontend]
        direction LR
    end
    subgraph N2 [Network: backend]
        direction LR
    end

    E1 -->|"接入"| N1
    E3 -->|"接入"| N1
    E2 -->|"接入"| N2

    classDef sandbox fill:#3B82F6,stroke:#2563EB,color:#fff
    classDef endpoint fill:#10B981,stroke:#059669,color:#fff
    classDef network fill:#F59E0B,stroke:#D97706,color:#fff

    class S1,S2 sandbox
    class E1,E2,E3 endpoint
    class N1,N2 network

1.2 五种网络驱动

Docker 提供 5 种网络驱动,覆盖不同使用场景:

驱动一句话理解适用场景性能隔离性
bridge容器通过虚拟交换机通信,出网走 NAT单机多容器(最常用)
host容器直接用宿主机网络栈,无隔离监控 Agent、高吞吐代理最高
overlay宿主机之间建隧道,跨机器通信Swarm 集群跨主机中低
macvlan容器直接出现在物理网络上,有独立 IP需要固定 IP 对接的旧系统
none完全断网,只有 localhost安全沙箱、离线计算N/A完全隔离

不知道选哪个?按这个决策树走:

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#3B82F6', 'lineColor': '#60A5FA'}}}%%
flowchart TD
    Q1{"容器需要访问网络吗?"}
    Q2{"容器在多台机器上?"}
    Q3{"对网络延迟极端敏感?"}
    Q4{"需要容器拥有物理网络独立 IP?"}

    Q1 -->|"不需要"| NONE["none: 完全隔离"]
    Q1 -->|"需要"| Q2
    Q2 -->|"是,跨主机"| OVERLAY["overlay: 自动建隧道"]
    Q2 -->|"否,单机"| Q3
    Q3 -->|"是"| HOST["host: 零 NAT 开销"]
    Q3 -->|"否"| Q4
    Q4 -->|"是"| MACVLAN["macvlan: 直连物理网络"]
    Q4 -->|"否"| BRIDGE["bridge(自定义): 最通用"]

    classDef decision fill:#3B82F6,stroke:#2563EB,color:#fff
    classDef result fill:#10B981,stroke:#059669,color:#fff

    class Q1,Q2,Q3,Q4 decision
    class NONE,OVERLAY,HOST,MACVLAN,BRIDGE result

五种驱动中 Bridge 覆盖了绝大多数场景,下面重点拆解它的实现机制。

2. Bridge 网络

Bridge 是 Docker 默认且最常用的网络模式。理解它就是理解 Docker 单机网络的全部。

2.1 数据包流转路径

一个容器发出 HTTP 请求到外网,数据包经历以下路径:

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#3B82F6', 'lineColor': '#60A5FA'}}}%%
sequenceDiagram
    participant App as "容器内应用"
    participant eth0_c as "容器 eth0 (veth 内端)"
    participant veth_h as "宿主机 vethXXX (veth 外端)"
    participant br0 as "docker0 网桥"
    participant ipt as "iptables"
    participant eth0_h as "宿主机 eth0"
    participant Internet as "外部网络"

    App->>eth0_c: ① 应用发包 src: 172.17.0.2
    eth0_c->>veth_h: ② veth pair 直通到宿主机侧
    veth_h->>br0: ③ 网桥收包,查 MAC 转发表
    br0->>ipt: ④ 进入 iptables FORWARD 链
    ipt->>ipt: ⑤ POSTROUTING MASQUERADE (SNAT)
    ipt->>eth0_h: ⑥ 路由到出口网卡
    eth0_h->>Internet: ⑦ 发往外网

返回路径是逆过程:外网响应到达宿主机 eth0 → conntrack 表匹配原始连接 → DNAT 还原目标为容器 IP → 经 docker0 网桥 → 通过 veth pair 回到容器。

2.2 默认 Bridge Vs 自定义 Bridge

维度默认 bridge (docker0)自定义 bridge
DNS 解析不支持容器名解析,只能用 IP 或 --link(已废弃)内置 DNS(127.0.0.11),自动按容器名解析
隔离性所有未指定网络的容器共享同一网桥按网络分组,不同网络的容器默认互不可达
子网配置不可自定义支持自定义子网、IP 范围、网关
热插拔不支持运行中连接/断开支持 docker network connect/disconnect

默认 bridge 的 DNS 缺失是生产事故高发区。容器重启后 IP 变化,硬编码 IP 通信会导致服务间连接断裂。自定义 bridge 通过内置 DNS 解决这个问题。

1
2
3
4
5
6
7
8
9
10
# 创建自定义网络
docker network create --driver bridge \
--subnet 172.20.0.0/16 \
--gateway 172.20.0.1 \
my-app-net

# 容器直接用名字通信
docker run -d --name api --network my-app-net nginx
docker run -d --name db --network my-app-net postgres
# api 容器内可直接 ping db,DNS 自动解析

2.3 手动模拟 Bridge 网络

docker network create 一条命令就搞定了,但底层到底发生了什么?手动走一遍,遇到网络问题就知道该查哪里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# === 第一步:创建网桥(虚拟交换机) ===
ip link add br-demo type bridge # 创建虚拟交换机
ip addr add 172.30.0.1/24 dev br-demo # 分配网关 IP
ip link set br-demo up # 启动

# === 第二步:创建两个 Network Namespace ===
ip netns add container1
ip netns add container2

# === 第三步:用 veth pair 连接 Namespace 到网桥 ===
# veth pair = 一根两头的虚拟网线
# container1 的连接
ip link add veth-c1 type veth peer name veth-c1-br
ip link set veth-c1 netns container1 # 一头放进 Namespace
ip link set veth-c1-br master br-demo # 另一头接到网桥
ip link set veth-c1-br up

# container2 的连接(同样操作)
ip link add veth-c2 type veth peer name veth-c2-br
ip link set veth-c2 netns container2
ip link set veth-c2-br master br-demo
ip link set veth-c2-br up

# === 第四步:配置 Namespace 内的网络 ===
ip netns exec container1 ip addr add 172.30.0.2/24 dev veth-c1
ip netns exec container1 ip link set veth-c1 up
ip netns exec container1 ip link set lo up
ip netns exec container1 ip route add default via 172.30.0.1

ip netns exec container2 ip addr add 172.30.0.3/24 dev veth-c2
ip netns exec container2 ip link set veth-c2 up
ip netns exec container2 ip link set lo up
ip netns exec container2 ip route add default via 172.30.0.1

# === 第五步:验证通信 ===
ip netns exec container1 ping -c 2 172.30.0.3 # container1 → container2 ✅
ip netns exec container2 ping -c 2 172.30.0.1 # container2 → 网关 ✅

# === 第六步:配置 NAT 使容器能上外网 ===
echo 1 > /proc/sys/net/ipv4/ip_forward
iptables -t nat -A POSTROUTING -s 172.30.0.0/24 ! -o br-demo -j MASQUERADE
iptables -A FORWARD -i br-demo -j ACCEPT
iptables -A FORWARD -o br-demo -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

# === 清理 ===
ip netns del container1
ip netns del container2
ip link del br-demo

这 6 步与 Docker 自动行为的对照:

手动操作Docker 自动完成对应命令
创建网桥启动时创建 docker0docker network create
创建 Namespace每个容器自带docker run
创建 veth pair 并连接容器启动时创建docker run --network
配置 IP 和路由IPAM 自动分配自动
配置 NAT自动写 iptables 规则-p 端口映射

理解了 Bridge 的底层实现,接下来看 Docker 在 iptables 中写了哪些规则。

2.4 Iptables 规则链

Docker 的端口映射(-p 8080:80)本质是在 iptables 中写转发规则。

核心概念:

  • DNAT(目标地址转换):外部请求目标从 “ 宿主机:8080” 改写为 “ 容器 IP:80”
  • SNAT/MASQUERADE(源地址转换):容器出网时,源地址从容器 IP 改为宿主机 IP

docker run -d -p 8080:80 --name web nginx 为例,Docker 写入以下规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 查看 NAT 表规则
iptables -t nat -L -n -v

# ============ PREROUTING 链 ============
# 所有进入的包,交给 DOCKER 链处理
Chain PREROUTING (policy ACCEPT)
target prot opt in out source destination
DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL

# ============ DOCKER 链 ============
# 访问宿主机 8080 端口的请求 → 改写目标为 172.17.0.2:80
Chain DOCKER (2 references)
target prot opt in out source destination
DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80

# ============ POSTROUTING 链 ============
# 容器出网时,源地址改为宿主机 IP
Chain POSTROUTING (policy ACCEPT)
target prot opt in out source destination
MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0

完整数据流:

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#3B82F6', 'lineColor': '#60A5FA'}}}%%
flowchart LR
    subgraph IN [外部请求进入]
        direction LR
        A1["PREROUTING"] --> A2["DOCKER 链: DNAT 改写目标地址"] --> A3["FORWARD: 允许转发"] --> A4["到达容器"]
    end

    subgraph OUT [容器响应返回]
        direction LR
        B1["容器发包"] --> B2["POSTROUTING: MASQUERADE 改写源地址"] --> B3["发往外部"]
    end

    classDef chain fill:#3B82F6,stroke:#2563EB,color:#fff
    classDef action fill:#10B981,stroke:#059669,color:#fff

    class A1,A3,B2 chain
    class A2,A4,B1,B3 action

排障要点:端口映射不通时,按顺序检查——(1) iptables -t nat -L -n 确认 DNAT 规则存在;(2) iptables -L FORWARD -n 确认 FORWARD 链允许转发;(3) 检查 firewalld / ufw 是否覆盖了 Docker 规则。

2.5 端口映射:-p Vs -P Vs EXPOSE

命令/指令作用示例
EXPOSE 80 (Dockerfile)纯文档声明,不打开任何端口告诉使用者 “ 我监听 80”
-p 8080:80精确映射:宿主机 8080 → 容器 80docker run -p 8080:80 nginx
-p 127.0.0.1:8080:80绑定到特定 IP,仅本机可访问生产环境推荐
-P自动映射所有 EXPOSE 端口到随机高端口快速测试

-p 80:80 默认绑定 0.0.0.0,所有网络接口(含公网 IP)均可访问。生产环境务必绑定具体 IP。

Bridge 网络解决了单机容器通信问题,但 Host、None、Macvlan 在特定场景下不可替代。

3. Host、None 与 Macvlan

3.1 Host 网络

容器直接共享宿主机的 Network Namespace,没有独立网络栈,没有 veth pair,没有 NAT。

场景:Prometheus Node Exporter 需要采集宿主机的 CPU、内存、网络指标。Bridge 模式下,容器看到的网卡是 veth 虚拟网卡,采集到的是容器自身数据而非宿主机数据。

1
2
3
4
5
6
7
8
docker run -d --network host --name node-exporter \
-v /proc:/host/proc:ro \
-v /sys:/host/sys:ro \
prom/node-exporter \
--path.procfs=/host/proc \
--path.sysfs=/host/sys
# 直接监听宿主机 9100 端口,无需 -p
# 采集到宿主机真实网络数据

代价:完全丧失网络隔离。容器端口直接占用宿主机端口,两个容器不能监听同一端口。

性能参考:Bridge 的 NAT 转发引入约 2-5% 的额外延迟。对每秒百万级请求的场景有意义;大多数 Web 应用用 Bridge 足够。

3.2 None 网络

容器拥有独立的 Network Namespace,但仅有 loopback 接口——彻底断网。

场景:银行风控模型处理客户征信数据,监管要求数据不能有任何网络外传的可能。即使容器被攻破,攻击者也无法将数据发送出去。

1
2
3
4
5
6
7
8
9
# 数据通过卷挂载进去,结果通过卷取出来,全程无网络
docker run --network none \
-v /data/input:/input:ro \
-v /data/output:/output \
risk-model:latest python run_model.py

# 验证没有网络
docker exec -it <container_id> ip addr
# 只有 lo 接口,没有 eth0

适用场景:纯 CPU 计算、离线数据处理、安全沙箱。也可手动用 ip link 为 None 容器绑定自定义网络接口,实现完全自定义的网络拓扑。

3.3 Macvlan 与 IPvlan

两者都让容器直接出现在物理网络上,拥有独立 IP,绕过 NAT。

场景:某制造企业有一套 10 年历史的 ERP 系统,IP 地址(192.168.1.100)硬编码在几十台设备配置中。容器化后需要保持原 IP 不变。Bridge 模式做不到——容器在 172.17.x.x 子网里,必须走 NAT。

1
2
3
4
5
6
7
8
9
10
# 创建 Macvlan 网络
docker network create -d macvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
-o parent=eth0 \
erp-net

# 容器使用原 IP
docker run -d --network erp-net --ip 192.168.1.100 --name erp legacy-erp:latest
# 所有设备不改配置,直接访问 192.168.1.100 ✅

Macvlan 与 IPvlan 的工作原理差异:

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#3B82F6', 'lineColor': '#60A5FA'}}}%%
flowchart TD
    subgraph MAC [Macvlan 模式]
        direction TB
        PHY1["物理网卡 eth0"] --> M1["子接口 MAC-A: 容器 1"]
        PHY1 --> M2["子接口 MAC-B: 容器 2"]
        M1 -.- NOTE1["每个容器独立 MAC"]
    end

    subgraph IP [IPvlan L3 模式]
        direction TB
        PHY2["物理网卡 eth0"] --> I1["子接口: 容器 1"]
        PHY2 --> I2["子接口: 容器 2"]
        I1 -.- NOTE2["共享父接口 MAC, 三层路由"]
    end

    classDef phy fill:#3B82F6,stroke:#2563EB,color:#fff
    classDef container fill:#10B981,stroke:#059669,color:#fff
    classDef note fill:#F59E0B,stroke:#D97706,color:#fff

    class PHY1,PHY2 phy
    class M1,M2,I1,I2 container
    class NOTE1,NOTE2 note
维度MacvlanIPvlan
MAC 地址每个容器独立 MAC共享父接口 MAC
交换机兼容需要混杂模式,部分交换机限制 MAC 数量无需混杂模式
云平台几乎不可用(AWS/GCP 禁用混杂模式)兼容性更好
DHCP每个容器可独立获取 DHCP 地址共享 MAC,DHCP 受限
宿主机通信宿主机无法直接访问容器(内核限制)L3 模式下宿主机可达

Macvlan 的 “ 宿主机无法访问容器 “ 是 Linux 内核限制:同一物理接口的不同 MAC 地址之间不能直接通信。解决方法是在宿主机上额外创建 Macvlan 子接口,但会增加配置复杂度。

选型建议:需要物理网络独立 IP → 优先 IPvlan(兼容性好)。仅在需要独立 DHCP 地址且交换机支持时用 Macvlan。

单机网络模式掌握后,多容器编排是下一个实际问题。

4. Docker Compose 多容器网络

Docker Compose 自动为每个项目创建独立的 bridge 网络,项目内容器通过服务名互通,项目间天然隔离。

4.1 微服务网络拓扑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# docker-compose.yml
version: "3.8"

services:
api-gateway:
image: nginx
ports:
- "127.0.0.1:80:80" # 仅绑定 localhost
networks:
- frontend

user-service:
image: my-user-svc
networks:
- frontend
- backend

order-service:
image: my-order-svc
networks:
- frontend
- backend

postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
networks:
- backend

redis:
image: redis:7
networks:
- backend

networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # 禁止出外网,数据库层完全隔离

网络拓扑可视化:

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#3B82F6', 'lineColor': '#60A5FA'}}}%%
flowchart TD
    EXT["外部请求 :80"] --> GW["api-gateway"]

    subgraph FN [frontend 网络]
        GW
        US["user-service"]
        OS["order-service"]
    end

    subgraph BN [backend 网络 - internal]
        US2["user-service"]
        OS2["order-service"]
        PG["postgres"]
        RD["redis"]
    end

    US -.->|"同一容器, 双网卡"| US2
    OS -.->|"同一容器, 双网卡"| OS2

    classDef external fill:#EF4444,stroke:#DC2626,color:#fff
    classDef frontend fill:#3B82F6,stroke:#2563EB,color:#fff
    classDef backend fill:#10B981,stroke:#059669,color:#fff
    classDef bridge fill:#F59E0B,stroke:#D97706,color:#fff

    class EXT external
    class GW,US,OS frontend
    class US2,OS2,PG,RD backend

设计要点:

  • api-gateway 只接入 frontend,不能直连数据库
  • postgres 和 redis 在 internal: true 的 backend 中,无法访问外网
  • user-service 和 order-service 同时接入两个网络,充当桥梁
  • 端口映射绑定 127.0.0.1,前面再放反向代理

4.2 验证网络隔离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 启动
docker compose up -d
docker network ls | grep myproject

# 验证 api-gateway 无法直连 postgres
docker compose exec api-gateway ping -c 1 postgres
# 预期:ping: bad address 'postgres'(DNS 解析失败)

# 验证 user-service 可以连通两端
docker compose exec user-service ping -c 1 api-gateway # 通
docker compose exec user-service ping -c 1 postgres # 通

# 验证 backend 不能出外网
docker compose exec postgres ping -c 1 8.8.8.8
# 预期:超时(internal: true 禁止出网)

5. 网络排障

网络问题排查的核心思路:从最近的地方开始,一层一层往外推。

5.1 分层排障流程

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#3B82F6', 'lineColor': '#60A5FA'}}}%%
flowchart TD
    START["容器网络不通"] --> Q1{"容器内能 ping 自己吗?"}
    Q1 -->|"不能"| FIX1["容器网络栈异常,重启容器"]
    Q1 -->|"能"| Q2{"能 ping 对方容器名吗?"}
    Q2 -->|"DNS 解析失败"| Q3{"在同一自定义网络吗?"}
    Q3 -->|"不在"| FIX2["加入同一网络"]
    Q3 -->|"在"| FIX3["检查是否用了默认 bridge,改用自定义 bridge"]
    Q2 -->|"能 ping 通"| Q4{"应用能连通吗?"}
    Q4 -->|"不能"| FIX4["检查服务是否监听正确端口"]
    Q4 -->|"能"| Q5{"外部能访问吗?"}
    Q5 -->|"不能"| FIX5["检查 iptables 和防火墙"]
    Q5 -->|"能"| FIX6["网络正常,问题在应用层"]

    classDef question fill:#3B82F6,stroke:#2563EB,color:#fff
    classDef fix fill:#10B981,stroke:#059669,color:#fff

    class Q1,Q2,Q3,Q4,Q5 question
    class FIX1,FIX2,FIX3,FIX4,FIX5,FIX6 fix

5.2 排障工具速查

排查目标使用时机工具命令
容器网卡和 IP确认容器自身网络正常ip, ssdocker exec <c> ip addr
容器间通信两容器互相 ping 不通ping, curldocker exec <c1> ping <c2>
DNS 解析容器名报 “bad address”nslookupdocker exec <c> nslookup db
网桥状态怀疑网桥有问题bridgebridge link show
iptables 规则端口映射不通iptablesiptables -t nat -L -n -v
抓包分析需要看具体包内容tcpdumptcpdump -i docker0 -n port 80
Docker 网络详情查容器在哪个网络dockerdocker network inspect <net>

5.3 容器访问宿主机

从容器内访问宿主机服务(如本地数据库),优先使用 host.docker.internal

1
2
# 容器内访问宿主机的 MySQL
docker run --rm -it mysql mysql -h host.docker.internal -u root -p
  • Docker Desktop (Mac/Windows) 默认支持
  • Linux 上 Docker 20.10+ 需要加 --add-host=host.docker.internal:host-gateway

不要用 172.17.0.1——该地址仅在默认 bridge 网络中有效,自定义网络的网关地址不同。

6. 理解检验

概念辨析

  1. 默认 bridge 和自定义 bridge 的关键区别是什么?技术根因是什么?

    自定义 bridge 内置 DNS 服务(监听 127.0.0.11:53),通过 iptables DNAT 规则拦截容器的 DNS 请求并代为解析。默认 bridge 没有这层 iptables 规则,因此只能通过 IP 或已废弃的 --link 通信。

  2. VXLAN 封装为什么用 UDP 而不用 TCP?

    VXLAN 传输的是内层的完整以太网帧,内层协议可能已经是 TCP。如果外层再用 TCP,等于做两层可靠传输(TCP over TCP),重传风暴会导致严重的性能塌方。UDP 只负责 “ 送到 “,可靠性交给内层协议处理。

场景设计

三个微服务(API 网关 + 用户服务 + 订单服务)部署在同一台机器,API 网关需对外暴露 80 端口,其他仅内部通信。如何设计网络?

  • 创建两个自定义 bridge:frontend(API 网关 + 用户服务 + 订单服务)和 backend(用户服务 + 订单服务 + DB)
  • API 网关映射 -p 127.0.0.1:80:80,只接入 frontend
  • DB 放在 internal: true 的 backend 中,API 网关无法直连 DB,实现最小权限原则

实践验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 1. 创建自定义网络
docker network create net-a
docker network create net-b
docker run -d --name c1 --network net-a alpine sleep 3600
docker run -d --name c2 --network net-a alpine sleep 3600
docker run -d --name c3 --network net-b alpine sleep 3600

# 2. 验证同网络 DNS 解析
docker exec c1 ping -c 2 c2 # 通 ✅

# 3. 验证跨网络隔离
docker exec c1 ping -c 2 c3 # 不通(DNS 解析失败)✅

# 4. 动态接入第二个网络
docker network connect net-b c1
docker exec c1 ping -c 2 c3 # 通 ✅

# 5. 清理
docker rm -f c1 c2 c3
docker network rm net-a net-b

完成标准:第 2 步 DNS 解析成功,第 3 步隔离生效,第 4 步热插拔后互通。