Docker “超级大厨”

docker要解决的问题是:如何在成千上万个厨房里(服务器),以毫秒级的速度,做出一模一样的菜(应用环境)。

  • Docker 本质上不是虚拟化,而是**"特殊的进程"** 。它之所以能跑起来且不干扰宿主机,全靠 Linux 内核的两个核心特性:
    • Namespaces(命名空间) :负责隔离。让进程以为自己独享系统(看不见别人)。
    • Cgroups(控制组) :负责限制。给进程戴上镣铐(CPU、内存不能超支)。

第一部分:镜像分层 ------ "千层蛋糕"与"透明胶带"

1. 什么是镜像分层?

在传统虚拟机时代,你要部署一个 App,得先装个几 GB 的操作系统,慢得像蜗牛。

Docker 说:"太蠢了!咱们用千层蛋糕的思路。"

假设你要做一个"Python Web 应用"镜像:

  • 底层 (Base Image):铺一层 Ubuntu 系统(这是地基)。
  • 中间层 (Layer 2):撒一层 Python 依赖包(这是馅料)。
  • 顶层 (Layer 3) :放你的代码 app.py(这是樱桃)。

关键点来了:

Docker 有个超能力叫**"写时复制" (Copy-on-Write, CoW)** 。

当你运行这个镜像生成容器时,Docker 不会真的去复制那几百兆的底层蛋糕。它只是在最上面盖了一张**"透明保鲜膜"**(可写层)。

  • 读操作:如果没有修改,直接读下面的蛋糕。
  • 写操作:一旦你要改文件,Docker 就把改动写在最上面的"保鲜膜"上。
原理

上面说的还是比较高层一些,再进一步讲:分层存储和写时复制是通过联合文件系统(UnionFS) 实现的,在现代 Linux 上具体表现为 OverlayFS

深入底层:OverlayFS 与 分层原理

Docker 的镜像不是一个大文件,而是一堆目录的叠加。

  • 在 Linux 内核(3.18+)中,overlayfs 驱动负责把这些目录"骗"过应用程序,让它们看起来像是一个完整的文件系统。
1. 核心四层架构

当你启动一个容器时,内核会把四个目录挂载到一起:

层级名称 对应目录 权限 作用
LowerDir /var/lib/docker/overlay2/<id>/diff 只读 存放镜像层。比如 Ubuntu 基础镜像的文件都在这里。可以有多个 LowerDir 层层叠加。
UpperDir /var/lib/docker/overlay2/<container-id>/diff 读写 容器的真实存储。所有修改、新增文件都存这儿。删容器时这层就没了。
WorkDir /var/lib/docker/overlay2/<container-id>/work 私有 用于处理"重命名"等复杂操作的临时中转站,保证原子性。
MergedDir /var/lib/docker/overlay2/<id>/merged 视图 最终呈现给容器的文件系统。这是 Lower 和 Upper 合并后的"虚像"。
2. 代码级落地:手动实现一个"Docker"

别用 docker run,我们用原生的 Linux 命令来模拟 Docker 是怎么把层叠起来的。

假设你有两个文件夹:

  • lower:里面有个文件 hello.txt,内容是 "World"。

  • upper:空的。

    1. 创建目录结构

    mkdir -p lower upper work merged

    2. 准备数据

    echo "Hello World" > lower/hello.txt

    3. 【核心操作】使用 mount 命令进行 Overlay 挂载

    这就是 Docker 守护进程在后台偷偷干的事!

    sudo mount -t overlay overlay
    -o lowerdir=lower,upperdir=upper,workdir=work
    merged

    4. 验证"分层"效果

    ls merged

    输出: hello.txt (看起来像是 merged 里的,其实是映射的)

    cat merged/hello.txt

    输出: Hello World

原理揭秘 :此时,merged 目录并没有真正占用磁盘空间存 hello.txt,它只是把 lower 目录映射过来了。

深入底层:写时复制的具体流程

当问到了"如何自动触发",在内核层面,这不是代码逻辑,而是文件系统驱动的中断处理

场景 A:读取文件(零拷贝)

当你在容器里执行 cat hello.txt

  1. 内核收到请求,去 MergedDir 找。
  2. OverlayFS 驱动发现 UpperDir(可写层)没有这个文件。
  3. 它直接返回 LowerDir(只读层)里的文件句柄。
  4. 结果:直接读底层的磁盘块,速度极快,不消耗额外空间。
场景 B:修改文件(写时复制 COW)

当你在容器里执行 echo "Docker" > hello.txt

  1. 拦截 :内核发现这是一个写操作
  2. 检查 :OverlayFS 驱动检查 UpperDir,发现里面没有 hello.txt
  3. 复制 :驱动立刻把 LowerDir 里的 hello.txt 复制 一份到 UpperDir
  4. 写入 :所有的修改操作,实际上是在对 UpperDir 里的副本进行的。
  5. 遮蔽 :从此以后,MergedDir 再看到 hello.txt,展示的就是 UpperDir 里的版本。底层的原版被"挡住"了。
场景 C:删除文件(白盲遮挡)

这是最骚的操作。当你执行 rm hello.txt

  1. 内核不能在 LowerDir 删文件(那是只读的镜像)。
  2. 它会在 UpperDir 里创建一个特殊的字符设备文件 (Whiteout file),通常以 .wh. 开头。
  3. 效果 :OverlayFS 驱动看到这个特殊标记,就会告诉应用:"这文件没了",尽管它在 LowerDir 里还活得好好的。

2. 代码落地:为什么顺序很重要?

很多新手写 Dockerfile 就像乱搭积木,导致构建巨慢。
原理:Docker 是从上往下检查缓存的。如果上一层变了,下面所有的层都要重新构建。

错误写法(把变动频繁的放在前面):

复制代码
# 每次你改一行代码,这一层就失效了
COPY . . 
# 导致这层即使没变,也得重新跑一遍 pip install,浪费生命!
RUN pip install -r requirements.txt

正确写法(利用缓存,像剥洋葱一样从不变到万变):

复制代码
FROM python:3.9-slim

# 第一层:系统依赖(几乎不变)
RUN apt-get update && apt-get install -y gcc

# 第二层:只复制依赖文件(变动频率低)
COPY requirements.txt .

# 第三层:安装依赖(耗时最长,但利用了缓存)
# 只要 requirements.txt 不改,这步直接秒过!
RUN pip install -r requirements.txt

# 第四层:复制代码(天天变)
COPY . .

CMD ["python", "app.py"]

效果 :你改了 app.py,Docker 发现前三层没变,直接复用缓存,几秒钟就构建完了。

第二部分:容器网络 ------ "虚拟网线"与"隐形门神"

容器之间怎么说话?容器怎么上网?这背后是 Linux 内核的三个狠角色在配合:Namespace、Veth Pair、Bridge。(注意:容器网络不是虚拟出来的交换机,而是 Linux 内核的网络栈隔离)

1. 隔离:Namespace( namespace 是啥?)

每个容器启动时,Docker 都会给它发一个**"独立房间"** (Network Namespace)。

在这个房间里,容器以为自己拥有全世界:它有独立的 IP、网卡、路由表。它根本不知道宿主机和其他容器的存在。

  • 宿主机 :有 eth0, lo

  • 容器 :刚创建时,除了 lo(回环接口)什么都没有,是个瞎子。

    1. 创建一个新的网络命名空间,名字叫 my-container

    sudo ip netns add my-container

    2. 查看它里面的网卡(此时只有 lo)

    sudo ip netns exec my-container ip addr

    输出: 1: lo: <LOOPBACK> ...

    3. 此时,这个 namespace 里的进程完全看不到宿主机的 eth0

2. 连接:Veth Pair(虚拟网线)

既然容器在独立的房间里,它怎么跟外面联系?

为了让隔离的容器能上网,内核提供了一根管状设备:Veth Pair

它有两个端点,像一根管子:数据包从 A 进,必然从 B 出。

  • 一头 (eth0):插进容器的Namespace房间里。
  • 一头 :插在宿主机的Namespace里,连到交换机(docker0 网桥)上。

这样,数据包就能顺着网线流出来了。

复制代码
# 1. 创建一对虚拟网线:veth-host (宿主机端) 和 veth-container (容器端)
sudo ip link add veth-host type veth peer name veth-container

# 2. 把 veth-container 这一头扔进刚才创建的 my-container 命名空间
sudo ip link set veth-container netns my-container

# 3. 在容器内部配置 IP 和启动网卡
sudo ip netns exec my-container ip addr add 172.17.0.2/16 dev veth-container
sudo ip netns exec my-container ip link set veth-container up
sudo ip netns exec my-container ip link set lo up

# 4. 在宿主机启动另一头,并连到 docker0 网桥(模拟)
sudo ip link set veth-host up
sudo brctl addif docker0 veth-host

3. 转发:Bridge & Iptables(隐形门神)

数据出了容器,到了宿主机的 docker0 网桥(相当于一个大路由器)。

这时候,Iptables (Linux 的防火墙)登场了,它像个精明的门神,负责翻译和指路。

Docker 启动时会自动写入几条 NAT 规则:

  • POSTROUTING (SNAT) :当数据包从 docker0 网桥流向物理网卡 eth0 时,把源 IP(容器 IP)伪装成宿主机的 IP。
  • PREROUTING (DNAT) :当你访问 宿主机IP:8080 时,Iptables 根据端口映射规则,把目标 IP 修改为 容器IP:80,然后转发过去。

场景

  • 场景一:容器访问外网 (SNAT)

    • 容器说:"我要访问百度!"
    • 门神(Iptables)一把抓住数据包,把源地址从容器的内网 IP (172.17.0.2) 改成宿主机的公网 IP。
    • 百度回信给宿主机,门神再查小本本,把信转交给容器。
  • 场景二:外部访问容器 (DNAT / 端口映射)

    • 你在浏览器访问 宿主机IP:8080
    • 门神一看:"哟,8080 端口?这是映射给 Nginx 容器的!"
    • 门神瞬间修改目标地址,把数据包扔给 172.17.0.3:80(Nginx 容器)。

实战演练:手写网络配置

别光听理论,我们来看看怎么用代码玩转这些网络模式。

1. 默认的 Bridge 模式(自带 DNS 的自定义网络)

默认的 docker0 网络不支持通过容器名互访。生产环境一定要创建自定义网络

复制代码
# 1. 创建一个专属局域网
docker network create my-app-network

# 2. 启动 Redis,连入局域网
docker run -d --name redis-server --network my-app-network redis

# 3. 启动 Python 应用,连入同一个网络
# 注意:这里可以直接 ping 通 'redis-server',不需要记 IP!
docker run -it --rm --name python-app --network my-app-network alpine ping redis-server

底层发生了什么? Docker 内置的 DNS 服务器自动把 redis-server 解析成了它的内网 IP。

2. Host 模式(性能狂魔)

如果你嫌 NAT 转发(门神检查)太慢,想要极致的网络性能,可以让容器共享宿主机的网络栈

这就好比容器不住单间了,直接住进了宿主机的豪宅里,共用一个大门口。

复制代码
# 容器没有独立 IP,直接占用宿主机的 80 端口
docker run -d --net=host nginx

代价:端口容易冲突,隔离性差。一般用于高性能网关或监控探针。

3. Container 模式(Sidecar 模式)

这是 Kubernetes 里的经典玩法。让一个新容器共享另一个容器的网络(IP 和端口都一样)。

比如:业务容器 + 日志收集容器。

复制代码
# 1. 启动主容器
docker run -d --name main-app my-app

# 2. 启动日志容器,共享 main-app 的网络
# 它们通过 localhost 就能互相访问,就像在一个进程里一样
docker run -d --name log-agent --net=container:main-app fluentd
核心概念 通俗比喻 技术术语 作用
镜像分层 千层蛋糕 UnionFS / Overlay2 节省空间,秒级分发,复用缓存。
容器运行 蛋糕上加保鲜膜 Copy-on-Write 隔离修改,保护原镜像不被破坏。
网络隔离 独立房间 Network Namespace 让容器以为自己是独立的电脑。
网络连接 虚拟网线 Veth Pair 连通容器与宿主机的通道。
流量转发 智能门神 Iptables / NAT 负责地址转换,让内外网互通。

终极总结:Docker 的本质

Docker 并没有什么魔法,它只是一个高明的组装工

  1. 利用 UnionFS (Overlay2) :把一堆只读目录和一个读写目录,通过内核挂载,拼成一个看起来完整的目录给进程用。写时复制是内核驱动在截获写操作时自动完成的。
  2. 利用 Namespaces:给进程造个假象,让它以为自己有独立的 PID、网络和挂载点。
  3. 利用 Cgroups:给进程套上枷锁,防止它吃光内存 CPU。
  4. 利用 Veth Pair + Iptables:给隔离的进程拉根网线,并配置路由规则让它能上网。

所谓的"容器",就是带着这些镣铐和面具运行的普通 Linux 进程而已。

相关推荐
青山师1 小时前
CompletableFuture深度解析:异步编程范式与源码实现
java·单例模式·面试·性能优化·并发编程
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第42题】【JVM篇】第2题:JVM内存模型有哪些组成部分?
java·开发语言·jvm·面试
码云骑士1 小时前
jwt入门介绍
linux·运维·数据库
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第43题】【JVM篇】第3题:GC分为哪两种?Young GC 和 Full GC有什么区别?
java·开发语言·jvm·后端·面试
Carino_U1 小时前
并发编程之CPU缓存架构&Disruptor
java·缓存·架构
前端摸鱼匠1 小时前
【AI大模型春招面试题30】交叉熵损失(Cross-Entropy Loss)在大模型训练中的作用?为何适合语言生成任务?
人工智能·ai·面试·大模型·求职招聘
!沧海@一粟!2 小时前
NAT映射回流解决内网通过公网映射访问内部服务器
运维·网络
青山师2 小时前
Java内存模型深度解析:Happens-Before规则与内存屏障实现原理
java·spring·面试·职场和发展·java程序员·jmm
灵晔君2 小时前
【Linux】进程(一)
linux·运维·服务器