Docker网络与数据 —— 让容器互联互通、数据永不丢失
一、容器网络:让"集装箱"们互相通信
问题场景
小明用Docker分别运行了一个Nginx容器和一个PHP-FPM容器,但Nginx怎么也连不上PHP——它们明明是跑在同一台服务器上的,为什么不能通信?
根因:Docker默认给每个容器分配了独立的网络空间,容器之间就像住在不同房间、没有门牌号的人——互相找不到。
类比:想象一栋宿舍楼(宿主机),每间宿舍(容器)有自己的门牌号和WiFi。如果同学们想要互相串门,需要知道对方的房间号;如果想上网,还需要通过宿舍楼的大门(端口映射)。
二、Docker四大网络模式
2.1 模式总览
# 查看Docker支持的网络模式
docker network ls
| 网络模式 | 参数 | 类比 | 特点 |
|---|---|---|---|
| bridge(默认) | --network bridge | 每间宿舍有独立的WiFi | 容器有独立IP,通过端口映射访问外部 |
| host | --network host | 住在客厅,共享一切 | 容器直接使用宿主机的网络 |
| none | --network none | 被关在密室里 | 没有网络,完全隔离 |
| container | --network container:NAME | 和室友合住 | 与指定容器共享网络命名空间 |
2.2 Bridge模式(默认,最常用)
# 默认启动就是bridge模式
docker run -d --name web nginx
# 显式指定
docker run -d --name web --network bridge nginx
工作原理:
┌─────────────────────────────────────────────┐
│ 宿主机 (Host) │
│ │
│ ┌─────────────────────────────────┐ │
│ │ docker0 网桥 │ │
│ │ IP: 172.17.0.1/16 │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ │ │
│ │ │ 容器A │ │ 容器B │ │ │
│ │ │172.17.0.2│ │172.17.0.3│ │ │
│ │ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────┘ │
│ ↕ NAT(端口映射) │
│ 物理网卡 (eth0: 192.168.1.100) │
└─────────────────────────────────────────────┘
docker0网桥详解:
Docker安装后会自动创建一个虚拟网桥 docker0,相当于宿舍楼里的"内部交换机":
# 查看docker0网桥信息
ip addr show docker0
# 输出示例:
# 3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
# inet 172.17.0.1/16 scope global docker0
# 查看网桥上的容器连接
brctl show docker0 # 需要安装bridge-utils
# 或使用
bridge link show
# 每个容器启动时,Docker会:
# 1. 创建一对虚拟网卡(veth pair),像一根网线的两端
# 2. 一端插入docker0网桥
# 3. 另一端放入容器内,命名为eth0
# 4. 从172.17.0.0/16中分配一个IP给容器
互动提问:为什么docker0的IP是
172.17.0.1?它是一个B类私有地址段(172.16.0.0/12),不会和大多数家庭/学校网络(192.168.x.x或10.x.x.x)冲突。
2.3 Host模式
docker run -d --name web --network host nginx
# 容器直接使用宿主机的IP和端口
# 访问 http://宿主机IP:80 即可
特点:
- 性能好(没有NAT转换开销)
- 但端口不能冲突(容器占了80端口,宿主机上其他程序就不能用80)
- Linux有效,Windows/macOS的Docker Desktop不完全支持
类比:Host模式就像"不住宿舍了,直接住在学校大门口的门面房"——地址就是学校的地址,但没有隔离。
2.4 None模式
docker run -d --name isolated --network none alpine sleep 3600
# 容器内没有任何网络接口(只有lo回环)
docker exec isolated ip addr
# 只有 127.0.0.1(lo),无法访问任何网络
用途:安全敏感的计算任务、纯粹的数据处理容器。
2.5 Container模式
# 先启动一个"主容器"
docker run -d --name main-web nginx
# 新容器共享main-web的网络空间
docker run -d --name sidecar --network container:main-web alpine sleep 3600
# sidecar容器可以访问main-web的所有端口(包括未映射的)
# 但两个容器共享同一个IP
用途:Sidecar模式——主容器跑应用,辅助容器跑日志采集或监控代理。类比:你和室友合住,共用一个门牌号,但各干各的事。
三、自定义网络:给容器建一个"专属小区"
3.1 为什么需要自定义网络?
问题:默认的bridge网络不支持通过容器名直接访问其他容器。如果你有10个微服务容器,每次通信都要记IP地址,太不方便。
解决:创建自定义网络,自动启用内置DNS解析——容器名就是主机名!
3.2 创建自定义网络
# 创建桥接网络(最常用)
docker network create my-app-net
# 指定子网和网关
docker network create \
--driver bridge \
--subnet 10.10.0.0/24 \
--gateway 10.10.0.1 \
my-custom-net
# 指定IP地址范围
docker network create \
--driver bridge \
--subnet 10.20.0.0/16 \
--ip-range 10.20.1.0/24 \
my-range-net
# 查看网络详情
docker network inspect my-app-net
# 查看所有网络
docker network ls
3.3 在自定义网络中使用容器
# 启动MySQL容器
docker run -d \
--name mysql-server \
--network my-app-net \
-e MYSQL_ROOT_PASSWORD=123456 \
mysql:8.0
# 启动Web容器,通过容器名直接连接MySQL
docker run -d \
--name my-web \
--network my-app-net \
my-webapp:latest
# 在my-web容器内,可以直接用 "mysql-server" 作为主机名连接数据库
docker exec my-web ping mysql-server
# PING mysql-server (10.10.0.2): 56 data bytes
# 64 bytes from 10.10.0.2: seq=0 ttl=64 time=0.085 ms
核心优势:自定义网络中的容器可以通过容器名互相发现,无需记IP!
3.4 网络管理命令
# 将运行中的容器加入网络
docker network connect my-app-net existing-container
# 将容器从网络断开
docker network disconnect my-app-net existing-container
# 删除网络
docker network rm my-app-net
# 清理所有未使用的网络
docker network prune
互动提问:如果两个容器在不同的自定义网络中,它们能互相通信吗?(答案:默认不能,除非同时连接到同一个网络,或者通过宿主机端口映射间接通信。)
四、端口映射:让外面的世界找到容器
4.1 端口映射详解
# -p 宿主机端口:容器端口(指定协议)
docker run -d -p 8080:80 nginx # TCP(默认)
docker run -d -p 8080:80/udp nginx # UDP
docker run -d -p 8080:80/tcp -p 8080:80/udp nginx # 同时TCP和UDP
# 绑定到特定IP(只允许内网访问)
docker run -d -p 192.168.1.100:8080:80 nginx
# -P 大写P:随机映射所有EXPOSE的端口
docker run -d -P nginx
docker ps # 查看随机分配的端口号
# 输出类似:0.0.0.0:49153->80/tcp
# 映射多个端口
docker run -d -p 8080:80 -p 8443:443 nginx
# 映射端口范围
docker run -d -p 8000-8010:8000-8010 myapp
4.2 端口映射原理
外部请求 → 宿主机:8080 → iptables DNAT规则 → 容器:80
┌──────────┐ ┌──────────┐
│ 浏览器 │ ── 192.168.1.100 │ 宿主机 │
│ │ :8080 │ │
└──────────┘ │ iptables │──→ ┌──────────┐
│ DNAT │ │ 容器 │
└──────────┘ │ 172.17. │
│ 0.2:80 │
└──────────┘
# 查看端口映射的iptables规则
sudo iptables -t nat -L -n | grep docker
五、容器间通信:从 --link 到自定义DNS
5.1 --link(旧方式,不推荐)
# 问题:早期Docker没有自定义网络,容器间通信靠--link
docker run -d --name mysql-server mysql:8.0
docker run -d --name web --link mysql-server:db nginx
# web容器内的/etc/hosts会添加一行:172.17.0.2 db
# 缺点:
# 1. 必须在容器启动时指定,不能动态添加
# 2. 容器重启后IP可能变化,hosts文件不一定更新
# 3. 只支持默认的bridge网络
5.2 自定义网络DNS(推荐方式)
# 创建自定义网络
docker network create app-net
# 启动容器
docker run -d --name mysql-server --network app-net mysql:8.0
docker run -d --name web --network app-net nginx
# web容器内可以直接用 mysql-server 作为主机名
docker exec web ping mysql-server
# Docker内置DNS服务器自动解析容器名到IP
对比:
| 特性 | --link | 自定义网络DNS |
|---|---|---|
| 动态发现 | 否(静态hosts文件) | 是(内置DNS) |
| 支持默认bridge | 是 | 否(仅自定义网络) |
| 容器重启后自动更新 | 否 | 是 |
| 网络隔离 | 无 | 支持 |
| 推荐程度 | 已废弃 | 官方推荐 |
六、跨主机网络:让不同服务器的容器通信
问题场景
学校有两台服务器(Server-A和Server-B),分别运行着Docker容器。如何让Server-A上的容器访问Server-B上的数据库?
6.1 方案对比
| 方案 | 原理 | 适用场景 | 复杂度 |
|---|---|---|---|
| Overlay | Docker Swarm原生支持 | 小型集群 | 低 |
| Flannel | CoreOS开发,基于etcd | Kubernetes默认方案 | 中 |
| Calico | BGP路由,纯三层方案 | 大型Kubernetes集群 | 高 |
6.2 Overlay网络简介
# Docker Swarm模式下创建overlay网络
docker swarm init
docker network create -d overlay my-overlay-net
# 不同主机上的容器加入同一overlay网络后,可以直接通过容器名通信
# 底层使用VXLAN封装,跨主机的容器像在同一个二层网络中
原理类比:Overlay就像在两个宿舍楼之间建了一条"空中走廊",虽然物理上隔着一条街,但走起来就像在同一栋楼。
6.3 Flannel简介
Server-A Server-B
┌────────────┐ ┌────────────┐
│ 容器 10.1. │ Flannel隧道 │ 容器 10.1. │
│ 1.2 │ ════════════════ │ 2.3 │
└────────────┘ └────────────┘
↕ ↕
flanneld ←─── etcd ───→ flanneld
(每个节点分配一个子网,通过封装实现跨节点通信)
6.4 Calico简介
Calico不使用隧道封装,而是利用BGP协议直接在三层路由:
Server-A (BGP Peer) ←──BGP路由──→ Server-B (BGP Peer)
容器网段 10.1.1.0/24 容器网段 10.1.2.0/24
优点:无封装开销,性能好;支持细粒度网络策略
缺点:需要BGP支持,配置较复杂
互动提问:如果你的学校只有3台服务器,跑几十个容器,你会选择哪种跨主机方案?(提示:考虑复杂度和实际需求,Overlay可能最合适。)
七、数据卷:让容器数据"活"下来
问题场景
小明用Docker运行了一个MySQL数据库容器,辛辛苦苦录入了几千条学生成绩数据。一天容器意外被删除了——数据全没了!
根因:容器的文件系统是临时的("易失性"),容器删除后,里面写入的所有数据都会消失。
类比:容器就像一个"移动板房"——板房拆了,里面的家具(数据)也跟着没了。我们需要一个"独立的仓库"来存放重要物品。
7.1 三种数据挂载方式对比
| 方式 | 命令参数 | 类比 | 特点 |
|---|---|---|---|
| Volume(数据卷) | -v mydata:/path | 专业仓库 | Docker管理,最推荐 |
| Bind Mount(绑定挂载) | -v /host/path:/path | 自家车库 | 直接映射宿主机目录 |
| tmpfs | --tmpfs /path | 临时背包 | 存在内存中,容器停止即消失 |
7.2 Volume(Docker数据卷)——最推荐
# 创建数据卷
docker volume create mysql-data
# 查看所有数据卷
docker volume ls
# 查看数据卷详情(在宿主机的实际存储位置)
docker volume inspect mysql-data
# 输出中 Mountpoint 显示实际路径,通常是:
# /var/lib/docker/volumes/mysql-data/_data
# 使用数据卷运行容器
docker run -d \
--name my-mysql \
-v mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
mysql:8.0
# 即使容器被删除,数据卷中的数据依然保留
docker rm -f my-mysql
docker run -d \
--name my-mysql-new \
-v mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
mysql:8.0
# 新容器挂载同一个数据卷,数据完好无损!
# 删除数据卷(谨慎!数据会真正丢失)
docker volume rm mysql-data
# 清理所有未使用的数据卷
docker volume prune
7.3 Bind Mount(绑定挂载)
# 将宿主机的目录直接映射到容器
docker run -d \
--name my-web \
-p 8080:80 \
-v /home/user/my-website:/usr/share/nginx/html \
nginx
# 修改宿主机文件,容器内立刻生效(开发调试利器)
echo "<h1>Hello</h1>" > /home/user/my-website/index.html
# 浏览器访问 http://localhost:8080 立刻看到变化
# 只读挂载(容器不能修改挂载的文件)
docker run -d \
-v /home/user/config:/etc/nginx/conf.d:ro \
nginx
# 注意:Bind Mount的宿主机路径必须是绝对路径
7.4 tmpfs(内存临时存储)
# 创建临时存储(存在内存中,不写入磁盘)
docker run -d \
--name temp-app \
--tmpfs /tmp:rw,size=100m \
myapp:latest
# 适用场景:临时文件、密钥、缓存等不需要持久化的数据
# 容器停止后,tmpfs中的数据立即消失
7.5 三种方式详细对比
┌──────────────────────────────────────────────────────────┐
│ 宿主机 (Host) │
│ │
│ ┌─ Volume ─────────┐ ┌─ Bind Mount ──┐ │
│ │ /var/lib/docker/ │ │ /home/user/ │ │
│ │ volumes/mydata/ │ │ my-website/ │ │
│ │ (Docker管理) │ │ (用户管理) │ │
│ └────────┬──────────┘ └───────┬───────┘ │
│ │ │ │
│ ┌────────┴─────────────────────┴────────────────┐ │
│ │ 容器 (Container) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ /data │ │ /html │ │ /tmp │ │ │
│ │ │(Volume) │ │(Bind) │ │(tmpfs) │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
| 对比维度 | Volume | Bind Mount | tmpfs |
|---|---|---|---|
| 存储位置 | Docker管理的目录 | 宿主机任意目录 | 内存 |
| 持久性 | 容器删除后保留 | 容器删除后保留 | 容器停止即消失 |
| 性能 | 好 | 好 | 最好(内存速度) |
| 可移植性 | 高(Docker API管理) | 低(依赖宿主机路径) | 无 |
| 多容器共享 | 支持 | 支持 | 不支持 |
| 推荐场景 | 数据库、应用数据 | 开发调试、配置文件 | 临时文件、密钥 |
八、数据持久化实战:MySQL容器
8.1 完整的MySQL容器部署方案
# ===== 第一步:创建数据卷和配置目录 =====
docker volume create mysql-data
mkdir -p /opt/mysql/conf /opt/mysql/init
# ===== 第二步:准备自定义配置文件 =====
cat > /opt/mysql/conf/my.cnf << 'EOF'
[mysqld]
# 字符集设置为UTF-8(支持中文)
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
# 最大连接数
max_connections=200
# InnoDB缓冲池大小
innodb_buffer_pool_size=256M
EOF
# ===== 第三步:准备初始化SQL =====
cat > /opt/mysql/init/init.sql << 'EOF'
CREATE DATABASE IF NOT EXISTS school DEFAULT CHARSET utf8mb4;
USE school;
CREATE TABLE IF NOT EXISTS students (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
grade VARCHAR(10),
score DECIMAL(5,2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO students (name, grade, score) VALUES
('张三', '高三(1)班', 95.5),
('李四', '高三(2)班', 88.0),
('王五', '高三(1)班', 92.3);
EOF
# ===== 第四步:启动MySQL容器 =====
docker run -d \
--name mysql-server \
-p 3306:3306 \
-v mysql-data:/var/lib/mysql \
-v /opt/mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro \
-v /opt/mysql/init:/docker-entrypoint-initdb.d:ro \
-e MYSQL_ROOT_PASSWORD=School2024! \
-e MYSQL_DATABASE=school \
mysql:8.0
# ===== 第五步:等待初始化完成并验证 =====
# 查看日志,等待出现 "ready for connections"
docker logs -f mysql-server
# 连接数据库验证
docker exec -it mysql-server mysql -uroot -p'School2024!' -e "
USE school;
SELECT * FROM students;
"
# ===== 第六步:模拟灾难恢复 =====
# 记录当前数据
docker exec mysql-server mysql -uroot -p'School2024!' -e "
USE school;
INSERT INTO students (name, grade, score) VALUES ('赵六', '高三(2)班', 91.0);
SELECT COUNT(*) AS total FROM students;
"
# "意外"删除容器
docker rm -f mysql-server
# 用同一数据卷重建容器
docker run -d \
--name mysql-server \
-p 3306:3306 \
-v mysql-data:/var/lib/mysql \
-v /opt/mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro \
-e MYSQL_ROOT_PASSWORD=School2024! \
mysql:8.0
# 验证数据完好
docker exec mysql-server mysql -uroot -p'School2024!' -e "
USE school;
SELECT * FROM students;
"
# 数据完整保留,包括后来插入的"赵六"!
8.2 数据备份方案
# 方式1:使用mysqldump逻辑备份
docker exec mysql-server mysqldump -uroot -p'School2024!' --all-databases > backup_$(date +%Y%m%d).sql
# 方式2:直接备份数据卷
docker run --rm \
-v mysql-data:/source:ro \
-v /backup:/backup \
alpine tar czf /backup/mysql-data_$(date +%Y%m%d).tar.gz -C /source .
# 恢复数据卷
docker run --rm \
-v mysql-data:/target \
-v /backup:/backup \
alpine tar xzf /backup/mysql-data_20240101.tar.gz -C /target
互动提问:
docker-entrypoint-initdb.d目录中的SQL脚本什么时候执行?答案是只在数据目录为空时(即首次初始化时),所以重建容器不会重复执行初始化脚本。
九、数据卷容器:共享数据的"中转站"
9.1 概念与使用
# 创建一个专门管理数据卷的"数据容器"
docker create \
--name db-data-container \
-v /dbdata \
busybox
# 其他容器通过 --volumes-from 共享这个数据卷
docker run -d \
--name app1 \
--volumes-from db-data-container \
myapp:latest
docker run -d \
--name app2 \
--volumes-from db-data-container \
myapp:latest
# app1和app2共享同一个/dbdata目录
# 注意:--volumes-from是早期方案,现在推荐使用命名卷或自定义网络
十、资源限制:防止容器"霸占"系统资源
10.1 问题场景
一个"写bug"的应用程序内存泄漏,容器吃光了宿主机的所有内存,导致其他容器甚至宿主机本身都崩溃了。
10.2 内存限制
# 限制容器最多使用512MB内存
docker run -d --name limited-app --memory=512m myapp:latest
# 限制内存+swap总共1GB
docker run -d --name limited-app \
--memory=512m \
--memory-swap=1g \
myapp:latest
# 查看容器内存使用情况
docker stats --no-stream limited-app
10.3 CPU限制
# 限制容器使用1个CPU核心
docker run -d --name limited-app --cpus=1 myapp:latest
# 限制容器使用0.5个CPU核心
docker run -d --name limited-app --cpus=0.5 myapp:latest
# 指定容器绑定到特定CPU核心(核心0和核心1)
docker run -d --name limited-app --cpuset-cpus="0,1" myapp:latest
# CPU权重(默认1024,值越大优先级越高)
docker run -d --name high-priority --cpu-shares=2048 myapp:latest
docker run -d --name low-priority --cpu-shares=512 myapp:latest
# 综合示例:限制2核CPU + 1GB内存
docker run -d \
--name production-app \
--cpus=2 \
--memory=1g \
--memory-swap=2g \
myapp:latest
10.4 其他资源限制
# 限制进程数(防止fork炸弹)
docker run -d --pids-limit=100 myapp:latest
# 限制磁盘IO(读取速度限制为10MB/s)
docker run -d --device-read-bps /dev/sda:10mb myapp:latest
# 重启策略
docker run -d --restart=always myapp:latest # 总是重启
docker run -d --restart=on-failure:3 myapp:latest # 失败时最多重启3次
docker run -d --restart=unless-stopped myapp:latest # 除非手动停止,否则总是重启
互动提问:如果给容器设置了
--memory=256m,但应用实际需要512MB会怎样?容器会被OOM Killer终止(Out of Memory),就像你的水杯只有256ml容量,却想倒512ml水——必然溢出。
十一、底层原理:cgroup和namespace
11.1 Namespace —— 隔离("给每个人一个独立房间")
Namespace是Linux内核的一种资源隔离机制,让容器内的进程"以为"自己独占整个系统。
| Namespace类型 | 隔离什么 | 类比 |
|---|---|---|
| PID | 进程编号 | 每个房间有自己的门牌号系统 |
| Network | 网络栈(IP/端口/路由表) | 每个房间有自己的网线和WiFi |
| Mount | 文件系统挂载点 | 每个房间有自己的文件柜 |
| UTS | 主机名和域名 | 每个房间可以挂不同的门牌 |
| IPC | 进程间通信 | 每个房间内部的对讲机系统 |
| User | 用户和组ID | 每个房间内的"管理员"在外部只是普通住户 |
# 查看当前系统的namespace
ls -la /proc/self/ns/
# 查看容器的namespace
docker inspect --format '{{.State.Pid}}' my-container
ls -la /proc/<PID>/ns/
# 对比宿主机和容器的PID namespace
# 宿主机上可以看到所有进程
ps aux | wc -l
# 容器内只能看到容器自己的进程
docker exec my-container ps aux | wc -l
通俗讲解:想象一座大酒店(宿主机),有100个房间(容器)。每个房间:
- 有自己的门牌号(PID namespace)—— 房间内的1号不是酒店的1号
- 有自己的电话线路(Network namespace)—— 房间内的电话号码与酒店总机不同
- 有自己的储物柜(Mount namespace)—— 看不到其他房间的柜子
- 可以取自己的房间名(UTS namespace)—— "豪华套房"而非"3楼第5间"
11.2 Cgroup —— 限制("给每个人规定能用的资源量")
Cgroup(Control Groups)是Linux内核的资源管控机制,限制一组进程能使用的系统资源。
| 子系统 | 控制什么 | Docker参数 |
|---|---|---|
| memory | 内存使用量 | --memory |
| cpu | CPU使用比例 | --cpu-shares |
| cpuset | CPU核心绑定 | --cpuset-cpus |
| blkio | 磁盘IO | --device-read-bps |
| pids | 进程数量 | --pids-limit |
# 查看容器的cgroup配置
# Docker通过cgroup文件控制容器资源
cat /sys/fs/cgroup/memory/docker/<容器ID>/memory.limit_in_bytes
cat /sys/fs/cgroup/cpu/docker/<容器ID>/cpu.cfs_quota_us
# cgroup v2 路径有所不同
cat /sys/fs/cgroup/system.slice/docker-<容器ID>.scope/memory.max
通俗讲解:还是酒店类比——
- Cgroup就是酒店的"管理规定":
- 每个房间最多用2度电(
--memory=2g) - 每个房间空调最多开2台(
--cpus=2) - 每个房间最多住3人(
--pids-limit=100)
- 每个房间最多用2度电(
- 即使酒店有100度电、50台空调,每个房间也只能用规定额度
- 超额的房间会被"限电"(OOM Kill或CPU Throttle)
11.3 Namespace + Cgroup = 容器的"两翼"
容器 = Namespace(隔离视图) + Cgroup(限制资源)
Namespace说:"你看到的是一整个世界"(但其实只是隔离的一小块)
Cgroup说:"你最多只能用这么多资源"(防止你影响别人)
两者配合,才实现了"安全隔离 + 资源可控"的容器化。
十二、Overlay文件系统:容器的"透明覆层"
12.1 Copy-on-Write(写时复制)原理
Docker使用OverlayFS作为默认的存储驱动,实现了镜像层和容器层的分离。
┌──────────────────────────────────┐
│ 容器可写层 (Upper Dir) │ ← 容器运行时的修改都写在这里
├──────────────────────────────────┤
│ 镜像只读层 (Lower Dir) Layer 3 │ ← 镜像的最上层
├──────────────────────────────────┤
│ 镜像只读层 (Lower Dir) Layer 2 │ ← 镜像中间层
├──────────────────────────────────┤
│ 镜像只读层 (Lower Dir) Layer 1 │ ← 镜像基础层
└──────────────────────────────────┘
写时复制(CoW)原理:
- 读取文件:从上往下逐层查找,找到即返回
- 修改文件:第一次修改时,先把文件从只读层"拷贝"到可写层,再在可写层修改
- 删除文件:在可写层创建一个"whiteout"标记,遮盖下层的文件
- 新建文件:直接写在可写层
类比:想象你在图书馆借了一本教科书(只读层),你需要做笔记但不想在原书上写——于是你用了一张透明覆膜(可写层)盖在书页上,笔记写在覆膜上,原书完好无损。
12.2 实践验证
# 查看Docker使用的存储驱动
docker info | grep "Storage Driver"
# 查看容器的overlay挂载信息
docker inspect <容器ID> --format '{{.GraphDriver.Data}}'
# 查看容器可写层的大小
docker ps -s
# SIZE列显示可写层大小,virtual SIZE是总大小
# 验证CoW:修改容器内文件
docker run -d --name test-cow ubuntu:22.04 sleep 3600
docker exec test-cow bash -c "echo '修改了' >> /etc/hostname"
# 此时/etc/hostname被拷贝到可写层并修改
# 原始镜像层中的/etc/hostname未受影响
互动提问:为什么10个基于同一个Ubuntu镜像的容器不会占用10倍空间?因为只读层是共享的,每个容器只有自己的可写层是独立的——这就是"层"的复用机制。
十三、网络排查工具箱
当容器网络出问题时,按照以下流程排查:
# ===== 1. 基础连通性 =====
# 容器能否ping通外网?
docker exec my-container ping -c 3 8.8.8.8
# 容器能否解析域名?
docker exec my-container ping -c 3 baidu.com
# ===== 2. 网络配置检查 =====
# 查看容器的IP和网关
docker exec my-container ip addr
docker exec my-container ip route
# 查看容器的DNS配置
docker exec my-container cat /etc/resolv.conf
# ===== 3. 端口与连通性 =====
# 从宿主机测试容器端口
curl -v http://localhost:8080
telnet localhost 8080
nc -zv localhost 8080
# 容器间连通性测试
docker exec container-a ping container-b
docker exec container-a curl http://container-b:80
# ===== 4. 深入排查 =====
# 查看Docker网桥状态
ip addr show docker0
bridge link show
# 查看iptables规则
sudo iptables -t nat -L -n
sudo iptables -L FORWARD -n
# 查看容器的veth网卡对
docker inspect <容器ID> --format '{{.NetworkSettings.Networks}}'
ip link show | grep veth
# 抓包分析
docker exec my-container sh -c "apt-get update && apt-get install -y tcpdump && tcpdump -i eth0 -n port 80"
# ===== 5. DNS排查 =====
# 测试Docker内置DNS
docker exec my-container nslookup other-container 127.0.0.11
# 127.0.0.11 是Docker自定义网络的内置DNS服务器地址
运维常用命令精选
# === 网络管理 ===
docker network ls # 列出所有网络
docker network create --subnet 10.10.0.0/24 mynet # 创建自定义网络
docker network inspect mynet # 查看网络详情
docker network connect mynet my-container # 容器加入网络
docker network disconnect mynet my-container # 容器离开网络
docker network prune -f # 清理未使用网络
# === 数据卷管理 ===
docker volume ls # 列出所有数据卷
docker volume create mydata # 创建数据卷
docker volume inspect mydata # 查看数据卷详情
docker volume prune -f # 清理未使用数据卷
docker system df -v # 查看详细的磁盘使用
# === 资源监控 ===
docker stats # 实时资源使用(Ctrl+C退出)
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}" # 格式化输出
docker top my-container # 容器内进程列表
# === 排查利器 ===
docker exec -it my-container sh -c "cat /etc/resolv.conf" # DNS配置
docker exec -it my-container ip addr # 网络接口
docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' my-container # 获取容器IP
docker logs --since 5m my-container # 最近5分钟日志
趣味命令
# 1. 看看Docker给你创建了多少虚拟网卡
ip link show | grep -c veth
# 每个运行中的容器都有一对veth网卡
# 2. 在容器内看看"你是谁"
docker run --rm alpine sh -c "whoami && hostname && cat /etc/hostname"
# 3. 测试容器的网络隔离
docker run -d --name iso-test --network none alpine sleep 300
docker exec iso-test ping -c 2 baidu.com
# 100% packet loss —— 完全隔离!
# 4. 看看容器的cgroup限制
docker run --rm --memory=128m alpine cat /sys/fs/cgroup/memory.max 2>/dev/null || \
docker run --rm --memory=128m alpine cat /sys/fs/cgroup/memory/memory.limit_in_bytes 2>/dev/null
# 5. 查看Docker网桥上连着多少容器
bridge link show 2>/dev/null || ip link show type veth
# 6. 容器里的"我来自哪里"
docker run --rm alpine sh -c "cat /etc/os-release | head -3"
# 7. 一键查看所有容器的IP地址
docker inspect -f '{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker ps -aq)
# 8. 画一个ASCII网络拓扑图
docker run --rm alpine sh -c "
echo '=== Docker Network Topology ==='
echo ' [Internet]'
echo ' |'
echo ' [Host eth0]'
echo ' |'
echo ' [iptables NAT]'
echo ' |'
echo ' [docker0 bridge]'
echo ' |'
echo ' ┌────┼────┐'
echo '[Container1] [Container2] [Container3]'
"
小结:Docker网络让容器能够互联互通,自定义网络配合DNS解析是最推荐的方案。数据卷解决了容器数据持久化的问题,MySQL实战展示了完整的部署与恢复流程。资源限制(cgroup)和进程隔离(namespace)是容器的两大底层支柱,而Overlay文件系统实现了高效的层式存储。