2、容器运行时深度解析:Docker核心原理
大家好,我是你们的老朋友。今天咱们聊聊容器运行时,这是PaaS平台的基石。说实话,很多刚接触容器的朋友,上来就是docker run,但底层到底怎么工作的,往往一头雾水。我个人习惯,搞技术一定要把根扎深,不然线上出问题,你连日志都看不懂。
这一章,我会把Docker的核心原理掰开揉碎讲清楚。包括镜像分层、联合文件系统、网络模式,再对比一下containerd和CRI-O,最后聊聊安全最佳实践。嗯,内容不少,但都是干货。
2.1 镜像分层:为什么Docker镜像那么小?
你想想看,一个Ubuntu系统几个G,但Docker的Ubuntu镜像才一百多兆。为什么?
核心秘密就是分层。Docker镜像不是一个大文件,而是由多个只读层叠加而成。每一层代表一个Dockerfile指令。比如:
FROM ubuntu:20.04 # 基础层
RUN apt-get update && apt-get install -y nginx # 安装层
COPY index.html /var/www/html/ # 文件层
CMD ["nginx", "-g", "daemon off;"] # 元数据层
每一层都是增量的。你改了文件,Docker不会复制整个镜像,只新建一层。这就是写时复制(Copy-on-Write)机制。
关键点:镜像层是只读的。容器运行时,会在最上层加一个可写层。你对容器的所有修改,都写在这一层。容器删除,可写层也删了。
我在项目中遇到过一个问题:有同事在容器里装了软件,然后commit成新镜像。结果镜像越来越大。为什么?因为他把安装过程写在了可写层,commit时把整个可写层都打包了。正确的做法是写Dockerfile,让每一层都清晰可控。
2.2 联合文件系统:OverlayFS实战
分层存储靠什么实现?联合文件系统。Docker早期用AUFS,后来用OverlayFS。现在主流是Overlay2。
OverlayFS的原理很简单:把多个目录合并成一个。它有两个关键层:
- lowerdir:只读层,就是镜像层
- upperdir:可写层,容器运行时层
- merged:合并后的视图,容器看到的文件系统
举个例子:
# 查看容器的挂载信息
docker inspect <container_id> | grep GraphDriver
# 输出类似:
"GraphDriver": {
"Name": "overlay2",
"Data": {
"LowerDir": "/var/lib/docker/overlay2/.../diff",
"UpperDir": "/var/lib/docker/overlay2/.../diff",
"MergedDir": "/var/lib/docker/overlay2/.../merged"
}
}
你想想看,当你修改一个文件时,OverlayFS会怎么做?它会把文件从lowerdir复制到upperdir,然后在upperdir里修改。这就是写时复制。读文件时,先看upperdir,没有再看lowerdir。
避坑指南:我曾经遇到一个性能问题:容器里大量小文件读写,磁盘IO飙升。后来发现是OverlayFS的写时复制导致的。大量小文件修改,每次都要复制,性能自然差。解决方案是挂载宿主机目录,用volume绕过OverlayFS。
2.3 容器网络模式:五种模式详解
网络是容器最复杂的部分之一。Docker提供了五种网络模式,我一个个说。
| 模式 | 说明 | 适用场景 |
|---|---|---|
| bridge | 默认模式,通过docker0网桥通信 | 单机多容器通信 |
| host | 直接使用宿主机网络栈 | 性能敏感型应用 |
| none | 无网络,只有lo接口 | 安全隔离场景 |
| container | 共享另一个容器的网络栈 | sidecar模式 |
| overlay | 跨主机容器通信 | Swarm/Kubernetes集群 |
bridge模式是最常用的。Docker会创建一个docker0网桥,每个容器分配一个veth pair,一端在容器里,一端在docker0上。容器之间通过docker0转发。
host模式呢?说白了就是容器直接用宿主机的IP和端口。性能最好,但隔离性最差。我建议只在需要极致网络性能的场景用,比如日志采集、监控agent。
container模式很有意思。两个容器共享一个网络栈,可以通过localhost通信。我在项目中用这个模式做sidecar代理,主容器和代理容器共享网络,代理拦截所有流量。
注意:overlay模式需要配置键值存储(如consul、etcd),而且对网络性能有影响。跨主机通信时,数据包要封装和解封装,延迟会增加。生产环境建议用Calico、Flannel等CNI插件。
2.4 containerd与CRI-O对比
Docker虽然好用,但Kubernetes从1.24版本开始移除了dockershim。现在主流的容器运行时是containerd和CRI-O。
我直接说结论:containerd是当前最主流的选择。
| 特性 | containerd | CRI-O |
|---|---|---|
| 出身 | Docker公司,从Docker剥离 | Red Hat、SUSE等 |
| 成熟度 | 高,社区活跃 | 中等,但稳定 |
| 镜像管理 | 支持docker镜像格式 | 支持OCI镜像标准 |
| 性能 | 优秀,延迟低 | 优秀,资源占用少 |
| 生态 | 广泛,几乎所有云厂商支持 | Red Hat生态,OpenShift默认 |
我个人习惯用containerd。为什么?因为它的API设计更简洁,而且和Docker共享底层代码。你想想看,Docker本身也是调用containerd来管理容器的。所以用containerd,你几乎可以无缝迁移。
CRI-O呢?它更轻量,只实现CRI接口,不做多余的事。如果你用OpenShift,那CRI-O是默认选择。但社区生态不如containerd丰富。
我的建议:新项目直接用containerd。如果你还在用Docker,尽快迁移。Kubernetes官方已经不再支持dockershim了。
2.5 容器安全最佳实践
安全这个话题,怎么说都不为过。我见过太多因为容器配置不当导致的安全事故。下面是我总结的几个关键点。
2.5.1 不要以root运行容器
默认情况下,容器里的进程是root。但root在宿主机上也有权限。如果容器被攻破,攻击者可以直接控制宿主机。
# Dockerfile中指定用户
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
2.5.2 限制容器资源
不限制资源,一个容器就能把宿主机搞垮。用docker run的参数限制:
docker run --memory="512m" --cpus="1.5" --pids-limit=100 nginx
2.5.3 使用只读根文件系统
容器不需要写文件系统?那就设为只读。攻击者想写恶意文件都写不了。
docker run --read-only --tmpfs /tmp nginx
2.5.4 不要暴露Docker socket
我曾经见过有人把/var/run/docker.sock挂载到容器里。这等于把宿主机Docker的控制权交给了容器。攻击者拿到这个socket,就能在宿主机上执行任意命令。
绝对不要这样做:
docker run -v /var/run/docker.sock:/var/run/docker.sock ...
2.5.5 镜像安全扫描
用Trivy、Clair等工具扫描镜像漏洞。我习惯在CI/CD流程中加入镜像扫描,有高危漏洞直接阻断发布。
# 使用Trivy扫描镜像
trivy image nginx:latest
2.5.6 使用seccomp和AppArmor
seccomp可以限制容器能调用的系统调用。AppArmor可以限制程序的行为。Docker默认启用了一些seccomp规则,但你可以自定义更严格的策略。
避坑指南:我曾经遇到一个案例:容器里运行Java应用,频繁调用系统调用。默认seccomp规则太严格,导致应用性能下降。后来我根据Java的系统调用特征,自定义了一个seccomp策略,既保证了安全,又提升了性能。
小结
这一章我们聊了Docker的核心原理。镜像分层和联合文件系统是容器轻量化的基础。网络模式决定了容器如何通信。containerd和CRI-O是当前主流的容器运行时。安全方面,记住几个原则:最小权限、资源限制、只读文件系统、镜像扫描。
下一章,我会深入Kubernetes的调度器,讲讲Pod是怎么被分配到Node上的。到时候见。
公众号:蓝海资料掘金营,微信deep3321