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