容器运行时的内部结构和最新趋势(2023)
原文为 Akihiro Suda 在日本京都大学做的在线讲座,完整的 PPT 可 点击此处下载
本文内容分为以下三个部分:
- 1. 容器简介
- 2. 容器运行时的内部结构
- 3. 容器运行时的最新趋势
1. 容器简介
什么是容器?
容器是一组用于隔离文件系统、CPU 资源、内存资源、系统权限等的各种轻量级方法。容器在很多意义上类似于虚拟机,但它们比虚拟机更高效,而安全性则往往低于虚拟机。
有趣的是,“容器”目前还没有严格的定义。当虚拟机提供类似容器的接口时,例如,当它们实现 OCI(开放容器)规范 时,甚至虚拟机也可以被称为“容器”。这种“非容器”的容器将在后面的第三部分中讨论。
Docker
Docker 是最流行的容器引擎。Docker 本身支持 Linux 容器和 Windows 容器,但 Windows 容器不在本次讨论的范围之内。
启动 Docker 容器的典型命令行如下:
docker run -p 8080:80 -v .:/usr/share/nginx/html nginx:1.25
执行该命令后,可以在 http://<the host’s IP>:8080/
中看到当前目录下 index.html
的内容。
命令中的 -p 8080:80
部分指定将主机的 TCP 8080 端口转发到容器的 80 端口。
命令中的 -v .:/usr/share/nginx/html
部分指定将主机上的当前目录挂载到容器中的 /usr/share/nginx/html
。
命令中的 nginx:1.25
指定使用 Docker Hub 上的 官方 nginx 镜像。Docker 镜像与虚拟机镜像有些相似,但是它们通常不包含额外的诸如 systemd 和 sshd 等守护进程。
您也可以在 Docker Hub 上找到其他应用程序的官方镜像。您还可以使用称为 Dockerfile
的语言自行构建自己的镜像:
FROM debian:12 RUN apt-get update && apt-get install -y openjdk-17-jre COPY myapp.jar /myapp.jar CMD ["java", "-jar", "/myapp.jar"]
可以使用 docker build 命令构建镜像,并使用 docker push 命令将其推送到 Docker Hub 或其它镜像仓库。
Kubernetes
Kubernetes 将多个容器主机(例如(但不限于)Docker 主机)集群化,以提供负载平衡和容错功能。
值得注意的是,Kubernetes 也是一个抽象框架,用于与 Pods(始终在同一主机上共同调度的容器组)、Services(网络连接实体)和 其它类型的对象 进行交互,但是本次演讲不会深入介绍 kubernetes。
Docker 与 Docker 之前的容器
虽然容器直到 2013 年 Docker 发布才受到太多关注,但 Docker 并不是第一个容器平台:
- • 1999:FreeBSD Jail
- • 2000:Linux 虚拟环境系统(Virtuozzo 和 OpenVZ 的前身)
- • 2001:Linux Vserver
- • 2002:Virtuozzo
- • 2004:BSD Jail for Linux
- • 2004:Solaris Containers(显然,“容器”这个词就是这次创造的)
- • 2005:OpenVZ
- • 2008:LXC
- • 2013:Docker
人们普遍认为 FreeBSD Jail(大约 1999 年)是类 Unix 操作系统的第一个实用容器实现,尽管“容器”这个术语并不是在那时创造的。
从那时起,Linux 上也出现了几种实现。然而,Docker 之前的容器与 Docker 容器有本质上的不同。前者专注于模仿整个机器,其中包含 System V init、sshd、syslogd 等。当时经常将 Web 服务器、应用服务器、数据库服务器和所有内容放入一个容器中。
Docker 改变了整个范式。就 Docker 而言,一个容器通常只包含一个服务,因此容器可以是无状态且不可变的。这种设计显着降低了维护成本,因为容器现在是一次性的;当需要更新某些内容时,您只需删除容器并从最新镜像重新创建它即可。您也不再需要在容器内安装 sshd 和其他实用程序,因为您永远不需要对其进行 shell 访问。这也简化了多主机集群的负载平衡和容错。
2. 容器运行时的内部结构
本节假设使用 Docker v24 及其默认配置,但大多数部分也适用于非 Docker 容器。
Docker 底层
Docker 由客户端程序(docker
CLI)和守护进程(dockerd
)组成。docker
CLI 通过 Unix 套接字 (/var/run/docker.sock
) 连接到 dockerd
守护进程来创建容器。
然而,dockerd
守护进程本身并不创建容器,它将控制权委托给 containerd
守护进程来创建容器。但 containerd
也不创建容器,而是进一步将控制权委托给 runc
运行时,它包含了多个 Linux 内核功能,例如 Namespaces、Cgroups 和 Capabilities,以实现“容器”的概念。Linux 内核中并没有“容器”对象。
Namespace 命名空间
Namespace 命名空间 将资源与主机和其他容器隔离。
最知名的命名空间是 mount namespace。Mount 命名空间隔离文件系统视图,以便容器可以使用 pivot_root(2)
系统调用将 rootfs 更改为 /var/lib/docker/.../<container's rootfs>
。该系统调用类似于传统的 chroot(2)
但 更安全。
容器的 rootfs 与主机的结构非常相似,但它对 /proc
、/sys
和 /dev
有一些限制。例如,
- •
/proc/sys
目录被重新挂载为只读绑定以禁止 sysctl。 - • 通过挂载
/dev/null
来屏蔽/proc/kcore
文件(RAM)。 - • 通过挂载空的只读 tmpfs 来屏蔽
/sys/firmware
目录(固件数据)。 - • 对
/dev
目录的访问受到 Cgroup 的限制(稍后讨论)。
Network namespace 允许为容器分配专用 IP 地址,以便它们可以通过 IP 相互通信。
PID namespace 隔离进程树,以便容器无法控制其外部的进程。
User namespace(不要与用户空间 混淆)通过将主机上的非 root 用户映射到容器中的伪 root 来隔离 root 权限。伪 root 可以像容器中的root 一样运行 apt-get
、dnf
等,但它没有对容器外部资源的特权访问。
用户命名空间显着减轻了潜在的容器突破攻击,但 Docker 中默认不使用它。
其他命名空间:
- • IPC命名空间:隔离 System V 进程间通信对象等。
- • UTS 命名空间:隔离主机名。"UTS"(Unix Time Sharing system)似乎对这个命名空间来说是个用词不当的称呼。
- • (可选)Cgroup 命名空间:隔离
/sys/fs/cgroup
层次结构。 - • (可选)Time 命名空间:隔离时钟。大多数容器尚未使用。