【05-Docker底层原理】
Docker 的组件:
由五层组件组成流水线来构建一个完整的容器系统,每层组件只关注自己擅长的事情。
| 角色 | 组件 | 干什么 |
|---|---|---|
| 顾客 | Docker 客户端Docker CLI | 敲命令即点菜 |
| 前台收银 | Docker 守护进程(docker daemon ) | 接单、协调后厨、管理所有菜品 |
| 后厨主管 | 容器运行时 (containerd) | 菜品拆分成具体步骤 |
| 厨师 | OCI运行时 (runc) | 真正做菜的人 |
| 服务员 | 容器垫片Containerd-shim | 上菜之后持续关注你这桌,别让菜凉了 |
为什么使用C/S架构?
当用户输入docker run nginx ,发生了什么。
你敲 docker run nginx → CLI 发请求给 dockerd → dockerd 通过 gRPC 转交 containerd → containerd 拉起 shim → shim 调用 runc → runc 调用 Linux 内核隔离技术创建并运行 Nginx 容器。
这样做的好处是`解耦`。各个组件职责分离与良好的扩展性,其核心组件并非单一进程而是协同工作的生态系统。
流程关键点解析:
协议分层:Client与Daemon间使用REST API,Daemon与containerd间使用gRPC,这是为了兼顾外部接口的通用性和内部调用的高性能。
职责转移:
dockerd负责高层管理,但具体的容器创建任务通过gRPC"委托"给了containerd,实现了架构解耦。进程托管:
runc创建容器后即退出,由containerd-shim作为父进程托管容器,这确保了即使dockerd重启,容器也不会被意外终止。
容器的完整的生命周期
第1️⃣步:你敲了命令
docker run -d --name web -p 8080:80 nginx
Docker CLI 会把这条命令解析成一个HTTP REST请求发送给dockerd
第2️⃣步:dockerd 做"前台收银"该做的事
Dockerd 收到请求后,需要做一系列协调工作
镜像管理:检查本地是否存在 nginx 镜像,不存在则向 Registry 发起拉取,完成镜像分层落地存储。
存储准备:规划 overlay2 文件系统结构,调用 containerd snapshotter 创建 lowerdir、upperdir、workdir 目录,为后续联合挂载做准备。
网络准备:创建 veth pair 虚拟网卡,一端预置入容器网络命名空间、一端接入 docker0 网桥;分配容器内网 IP;配置 iptables NAT 端口转发规则,实现-p 8080:80端口映射。
配置整合:合并镜像内置元数据(ENV、CMD、EXPOSE)与命令行启动参数,整理容器运行配置模板。
下发请求:整理完毕全部容器配置,通过 gRPC 协议下发创建任务给 containerd。
关键点:dockerd 本身不直接操作 Linux 内核创建容器,它只是个"总调度"。
第3️⃣步:contarinerd 分发任务
containerd 分发任务(后厨主管统筹派活) containerd 通过 gRPC 接收到 dockerd 下发的容器创建请求,执行内部任务拆解分发:
校验镜像元数据,确认容器文件系统运行环境就绪;
为本次容器单独创建一个专属
containerd-shim垫片进程;把整理好的容器运行参数传递给 shim,由 shim 作为中间代理去调用 runc,规避 containerd 主进程与容器强耦合问题。
补充作用:后续即便 containerd 进程重启、崩溃,容器也不会被终止,由 shim 持续托管容器状态。
第4️⃣步:runc真正创建容器
Runc 是整个链路最底层的一环,调用Linux 内核系统调用来创建容器。
创建Namespace隔离环境:
配置Cgroup实现资源管控:
设置rootfs(通过Overlay2 挂载):
通过chroot切换容器根目录:
启动主进程
完成容器启动工作后自身退出,并将容器运行状态反馈给 containerd-shim
第5️⃣步:containerd-shim 接管
containerd-shim成为容器 PID1 进程的父进程,长期驻留后台托管整个容器生命周期。
containerd-shim (PID: 12345)
└── nginx master (PID: 1, 容器内的1号进程)
└── nginx worker
└── nginx worker
隔离守护进程,保容器不挂 shim 作为中间隔离层,容器归属 shim 管理。即使 containerd 重启或崩溃,容器依然正常运行,不被杀死。
回收子进程,防止僵尸进程 自动调用
wait()回收容器退出进程,读取退出码,上报给 containerd,避免产生僵尸进程。托管容器 IO 流与交互 统一接管容器标准输入输出,支撑
docker logs查看日志、docker exec进入容器交互,维持通信通路。转发控制信号 宿主机
docker stop/kill信号先给到 shim,由 shim 转发给容器主进程,实现容器正常启停。实时上报容器状态 持续将运行、暂停、异常、退出等状态通过 gRPC 上报给 containerd,最终同步到 dockerd,保证
docker ps状态准确。生命周期收尾 容器销毁终止后,shim 完成清理工作并自动退出,整个容器流程闭环结束。
厨师 (runc) 做完菜下班,服务员 (shim) 留在桌边全程照看:饭菜(容器)不会因为后厨主管 (containerd) 离岗就撤掉;客人呼叫添茶、撤盘(操作容器、查看日志)由服务员中转;菜品吃完收盘(容器停止)服务员收尾离岗。
镜像分层:理解容器的"OverlayFS联合文件系统"
Docker 的镜像不是一个巨大的文件,而是一堆只读的薄层堆叠在一起。分层的目的是为了让项目在不同环境下复用。
最佳实践建议:
优化Dockerfile:将变动频率低的层放在前面(如基础镜像
FROM),变动频率高的层放在后面(如添加应用ADD),以最大化利用层缓存,加速构建。合并层:在构建的最后阶段,可以考虑使用
多阶段构建或docker-squash工具减少层数,以精简镜像大小。
Overlay2:联合文件系统,是目前Liunx 主流的存储驱动。
核心概念是:
lowerdir (只读层) 多容器公用镜像底层,节约磁盘。
Upperdir (读写层):容器专属修改层,数据隔离,删除容器仅删除该层
Workdir (工作目录层): 写操作临时事务缓冲,防止文件损坏,用户不可见。
Merged (合并视图):上下层叠加后统一目录;读取自上而下查找,修改原有文件触发写时复制,改动最终落到 upperdir。
书本叠纸比喻:底层课本lowerdir是共用只读镜像,专属草稿纸upperdir存放容器所有修改,垫板workdir处理写时复制临时事务,肉眼所见完整书本就是两层合并后的merged统一视图。
配置示例(/etc/docker/daemon.json):
{ "storage-driver": "overlay2", "storage-opts": [ "overlay2.override_kernel_check=true" ] }网络:容器怎么和外界通信?
Docker 提供了多种网络模式:
当你运行一个容器而未指定网络时,Docker会将其连接到默认的bridge网络(对应网桥docker0)。其内部实现如下:
宿主机外部网络(eth0: 192.168.1.100) │ ┌────┴────┐ │ NAT │ ← iptables 规则 └────┬────┘ │ ┌────┴────┐ │ docker0 │ (172.17.0.1/16) └──┬───┬──┘ │ │ veth1 veth2 │ │ eth0 eth0 (容器A) (容器B) 172.17.0.2 172.17.0.3网络创建与通信流程:
Docker创建
docker0虚拟网桥(默认网段172.17.0.0/16)。为每个容器创建一对
veth设备,一端放入容器的Network Namespace(命名为eth0),另一端连接到docker0。docker0 只是桥接模式容器的网关。为容器的
eth0分配IP(如172.17.0.2)。通过
iptables设置NAT规则,实现容器访问外网和端口映射(-p 8080:80)。
使用默认bridge网络时,容器间只能通过IP通信。创建自定义网络可以启用容器名称发现。原理 :Docker为每个自定义网络内置了一个DNS服务器。当容器app2解析app1时,Docker DNS会返回app1在该网络中的IP地址。
数据持久化:为什么需要 Volume?
容器的可写层和容器的生命周期绑定了,当容器删除释放了,数据也会删除。
而且即使容器还在,可写层的性能也不如直接操作系统文件系统。
生产环境最佳实践:
使用命名Volume:
docker volume create app_data,然后通过-v app_data:/app/data挂载。避免使用匿名Volume:不利于管理和迁移。
谨慎使用Bind Mount:仅在需要直接编辑主机配置文件或进行开发调试时使用,docker run -v /host/test:/container/test nginx。-v 宿主机路径:容器路径。
Bind Mount 绑定挂载:用户指定宿主机已有路径,直接映射到容器,自由度高,适合挂载本地代码与外部配置;
Docker Volume 数据卷:Docker 自行管理存储空间,通过 docker volume create 创建,更适合业务数据持久化,不用手动维护宿主机目录。
内核基石:Namespace与Cgroups
容器隔离的本质是Linux内核中的N与C特性的组合使用。
Namespace 名称空间: 给进程创造一个"受限的视野",使用了6种Namespace
- PID Namespace: 它认为自己的PID是1
- Mount Namespace: 独立的文件系统挂载点 /、/proc、/sys
- Network Namespace: 独立的网络设备、IP、端口
- UTS Namespace: 独立的主机名
- IPC Namespace: 独立的信号量、消息队列、共享内存(进程间通信)
- User Namespace: 独立的用户和组ID映射(可选,增强安全
这不是虚拟机! 容器进程和宿主机进程运行在同一个内核上,只是通过 Namespace 限制了"它能看到什么"。
Cgroups:给进程"上枷锁", 解决的是进程组"能用多少资源";用于限制、统计和隔离进程组的资源(CPU、内存、IO等)。
一句话总结:Namespace 决定容器"能看到什么",Cgroups 决定容器"能用多少",OverlayFS 给容器一个独立的文件系统视图。三者组合,就是"容器"。
真实排查:用架构知识定位问题
- 问题1:容器启动失败,报错"OCI runtime create failed"
思路:OCI 运行创建失败,说明到达了底层runc ,但是调用内核失败了。docker info 查看namespace、overlay 是否正常;查看详细日志 docker --debug 查看containerd日志(journalctl -u containerd)。
常见原因:内核版本太低,缺少内核模块,磁盘满,cgroup版本不兼容。
- 问题2:容器内域名解析失败
思路:进入容器-exec 查看域名配置文件是否配置正常/etc/resolv.conf;
Ping 一下DNS服务器确认和服务器数据收发正常,检查防火墙是否拦截了;检查宿主机的DNS是否正常。
