容器运行时的内部结构和最新趋势(2023)
原文为 Akihiro Suda 在日本京都大学做的在线讲座,残缺的 PPT 可 点击此处下载
本文内容分为以下三个局部:
- 容器简介
- 容器运行时的内部结构
- 容器运行时的最新趋势
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 命名空间:隔离时钟。大多数容器尚未应用。
Cgroups
Cgroups(控制组)施加多种资源配额,例如 CPU 使用率、内存使用率、block I/O 以及容器中的过程数量。
Cgroup 还管制对设施节点的拜访。Docker 默认配置 容许无限度拜访 /dev/null
、/dev/zero
、/dev/urandom
等,不容许拜访 /dev/sda
(磁盘设施)、/dev/mem
(内存)等。
Capabilities
在 Linux 上,root 权限由 64-bit capability 标记。目前应用了 41 位。
Docker 的默认配置删除了零碎范畴的治理性能,例如 CAP_SYS_ADMIN
。
保留的能力包含:
CAP_CHOWN
:用于在容器内运行chown
。CAP_NET_BIND_SERVICE
:用于绑定容器内 1024 以下的 TCP 和 UDP 端口。CAP_NET_RAW
:用于运行须要制作原始以太网数据包的旧版ping
实现。这种性能十分危险,因为它容许在容器网络中进行 ARP 坑骗和 DNS 坑骗。Docker 的将来版本可能会默认禁用它。
(可选)Seccomp
Seccomp(平安计算)容许指定零碎调用的显式容许列表(或回绝列表)。Docker 的默认配置容许大概 350 个零碎调用。
Seccomp 用于 纵深进攻);对于容器来说这并不是硬性要求。为了向后兼容,Kubernetes 依然默认不应用 seccomp,并且在可预感的未来可能永远不会扭转默认配置。用户依然能够通过 KubeletConfiguration 抉择启用 seccomp。
(可选)AppArmor 或 SELinux
AppArmor 和 SELinux(平安增强型 Linux)是 LSM(Linux 平安模块),可提供更细粒度的配置旋钮。
这些是互相排挤的;由主机操作系统发行商(而不是容器镜像发行商)抉择:
- AppArmor:Debian、Ubuntu、SUSE 等抉择的。
- SELinux:由 Fedora、Red Hat Enterprise Linux 和相似的主机操作系统发行版抉择。
为了进行纵深进攻,Docker 的 默认 AppArmor 配置文件 简直与其性能、挂载掩码等默认配置重叠。用户能够增加自定义设置以进步安全性。
但 SELinux 的状况则不同。要在 selinux-enabled 模式下运行容器,您必须在绑定挂载上附加选项 :z
(小写字符)或 :Z
(大写字符),或者本人运行简单的 chcon
命令防止权限谬误。
:z
(小写字符)选项用于类型强制。类型强制通过为过程和文件调配“类型”来爱护主机文件免受容器的影响。以 container_t
类型运行的过程能够读取 container_share_t
类型的文件,并读 / 写 container_file_t
类型的文件,但无法访问其余类型的文件。
:Z
(大写字符)选项用于多类别安全性。多类别安全性通过为过程和文件调配类别号来爱护一个容器免受另一个容器的影响。例如,类别 42 的过程无法访问标记为类别 43 的文件。
实用于 Mac/Win 的 Docker
Docker Desktop 产品反对在 Mac 和 Windows 上运行 Linux 容器,但它们只是在底层运行 Linux 虚拟机来在其上运行容器。这些容器不间接在 macOS 和 Windows 上运行。
3. 容器运行时的最新趋势
Docker 的替代品(作为 Kubernetes 运行时)
Kubernetes 的第一个版本(2014 年)是专门为 Docker 制作的。Kubernetes v1.3 (2016) 增加了对名为 rkt
的代替容器运行时的长期反对,但 rkt
已于 2019 年服役。反对代替容器运行时的致力在 Kubernetes v1.5 (2016) 中产生了容器运行时接口 CRI
API。CRI 首次亮相后,业界已趋同于应用 containerd 和 CRI-O 这两种运行时其中之一:。
Kubernetes 依然内置了对 Docker 的反对,但最终在 Kubernetes v1.24(2022 年)中被删除。Docker 依然持续作为第三方运行时为 Kubernetes 工作(通过 cri-dockerd
shim),但 Docker 当初在 Kubernetes 中的使用率越来越低。
业界出名大厂曾经从 Docker 转向了 containerd 或者 CRI-O:
- containerd 的采纳者:Amazon Elastic Kubernetes Service (EKS)、Azure Kubernetes Service (AKS)、Google Kubernetes Engine (GKE)、k3s 等(很多)。
- CRI-O 的采纳者:Red Hat OpenShift、Oracle Container Engine for Kubernetes (OKE) 等。
Containerd 重视可扩展性,反对非 Kubernetes 工作负载以及 Kubernetes 工作负载。相比之下,CRI-O 重视简略性,并且仅反对 Kubernetes。
Docker 的代替计划(作为 CLI)
只管 Kubernetes 已成为多节点生产集群的规范,但用户依然心愿应用相似 Docker 的 CLI 在笔记本电脑上本地构建和测试容器。Docker 基本上满足了这个需要,然而社区中的运行时开发人员心愿构建本人的“实验室”CLI,以先于 Docker 和 Kubernetes 孵化新性能,因为通常很难向 Docker 和 Kubernetes 提出新性能,因为一些技术 / 技术因素起因。
Podman(以前称为 kpod)是由 Red Hat 等公司创立的兼容 Docker 的独立容器引擎。它与 Docker 的次要区别在于它默认没有守护过程。此外,Podman 的独特之处在于它为治理 Pod(共享雷同网络命名空间的容器组,通常共享同一主机上的数据卷以实现高效通信)以及容器提供一流的反对。然而,大多数用户仿佛只将 Podman 用于非 Pod 容器。
nerdctl(我于 2020 年创建)是一个实用于 containerd 的兼容 Docker 的 CLI。nerdctl 最后是为了试验新性能,例如提早拉取(稍后探讨),但它对于调试运行 containerd 的 Kubernetes 节点也很有用。
在 Mac 上运行容器
Docker Desktop 的 Mac 和 Windows 产品是专有的。Windows 用户能够在 WSL2 中运行 Docker 的 Linux 版本(Apache License 2.0,无图形界面),但迄今为止,Mac 用户还没有相应的解决方案。
Lima(也是我于 2021 年创建的)是一个命令行工具,用于在 macOS 上创立相似 WSL2 的环境来运行容器。Lima 默认应用 nerdctl,但它也反对 Docker 和 Podman。
Lima 还被 colima (2021)、Rancher Desktop (2021) 和 Finch (2022)等第三方我的项目采纳。
Podman 社区公布了 Podman Machine(命令行工具,2021 年)和 Podman Desktop(GUI,2022 年)作为 Docker Desktop 的替代品。Podman Desktop 也反对 Lima(可选)。
Docker 正在重构
containerd 次要提供两个子系统:运行时子系统和镜像子系统。然而,后者并未被 Docker 应用。这是一个问题,因为 Docker 本身的传统镜像子系统远远落后于 containerd 的古代镜像子系统(这也导致我启动了 nerdctl 我的项目):
- 不反对 lazy-pulling 惰性拉取(按需镜像拉取)
- 对多平台镜像的无限反对(例如 AMD64/ARM64 双平台镜像)
- OCI 标准的无限合规性
这个长期存在的问题终于失去解决。Docker v24 (2023) 在 /etc/docker/daemon.json
中增加了对应用 containerd 的镜像子系统和 undocumented option 的实验性反对:
{"features":{"containerd-snapshotter": true}}
Docker 的将来版本(2024?2025?)很可能默认应用 containerd 的镜像子系统。
Lazy-pulling 惰性拉取
容器镜像中的大多数文件从未被应用:
“拉取包占容器启动工夫的 76%,但其中只有 6.4% 的数据被读取”
摘自“Slacker:应用 Lazy Docker 容器进行疾速散发”(Harter 等人,FAST 2016)
“惰性拉取”是一种通过按需拉取局部镜像内容来缩小容器启动工夫的技术。对于 OCI 规范 tar.gz 镜像 来说这是不可能的,因为它们不反对 seek()
操作。人们提出了几种代替格局来反对惰性拉取:
- eStargz (2019):优化 seek() 能力的 gzip 粒度;向前兼容 OCI v1 tar.gz。
- SOCI (2022):捕捉 tar.gz 解码器状态的检查点;向前兼容 OCI v1 tar.gz。
- Nydus (2022):另一种图像格式;
与 OCI v1 tar.gz 不兼容。 - OverlayBD (2021):将块设施作为容器镜像;与 OCI v1 tar.gz 不兼容。
下图显示了 eStargz 的基准测试后果。惰性拉动(+ 额定优化)能够将容器启动工夫缩小到 1/9。
扩充 User namespace 的采纳
只管 Docker 自 v1.9(2015)以来始终反对用户命名空间,但在 Docker 和 Kubernetes 生态系统中依然很少应用。
起因之一是“chowning”容器 rootfs 作为伪根的复杂性和开销。Linux 内核 v5.12 (2021) 增加了“idmapped mounts”以打消 chown 的必要性。打算在 runc v1.2 中反对这一点。
runc v1.2 公布后,用户命名空间预计将在 Docker 和 Kubernetes 中更加风行,而 Docker 和 Kubernetes 刚刚在 v1.25(2022)中增加了对用户命名空间的 初步反对。出于兼容性思考,Kubernetes 不太可能默认启用用户命名空间。然而,Docker 未来 仍有可能默认启用用户命名空间。不过,所有还没有决定。
Rootless 容器
Rootless 容器 是一种将容器运行时以及容器搁置在由非 root 用户创立的用户命名空间中的技术,以加重运行时的潜在破绽。
即便容器运行时存在容许攻击者逃离容器的谬误,攻击者也无奈领有对其余用户的文件、内核、固件和设施的特权拜访权限。
以下是 rootless 容器的简史:
- 2014:LXC v1.0 引入了对 rootless 容器的反对。过后 rootless 容器被称为“非特权容器”。LXC 的非特权容器与古代 rootless 容器略有不同,因为它们须要 SETUID 二进制文件 来 启动网络。
- 2017:runc v1.0-rc4 取得对 rootless 容器的初步反对。
- 2018:一些工具曾经开始反对,containerd、BuildKit(
docker build
的后端)、Docker、Podman。slirp4netns 被我本人创立,以通过转换以太网来容许 SETUID-less 网络数据包发送至非特权套接字零碎调用。 - 2019:Docker v19.03 公布,对 rootless 容器提供实验性反对。Podman v1.1 也在往年公布,具备雷同的性能,略当先于 Docker v19.03。
- 2020:Docker v20.10 公布,rootless 容器全面可用。
从 2020 年到 2022 年,咱们还致力于 bypass4netns,通过在容器内挂钩套接字文件描述符并在容器外重建它们来打消 slirp4netns 的开销。所实现的吞吐量甚至比“rootful”容器更快。
Rootless 容器曾经胜利遍及,但也有人对 rootless 容器提出批评。特地是,是否应该容许非 root 用户创立运行无根容器所需的用户命名空间是有争议的。对于容器用户,我的答复是“是”,因为无根容器至多比以根身份运行所有内容要平安得多。然而,对于不应用容器的人,我宁愿答复“否”,因为用户命名空间也可能是攻击面。例如,CVE-2023–32233 破绽:“Privilege escalation in Linux Kernel due to a Netfilter nf_tables vulnerability.”。
社区曾经在寻求解决这一窘境的办法。Ubuntu(自 13.10 起)和 Debian 提供了一个 sysctl 设置 kernel.unprivileged_userns_clone=<bool>
来指定是否容许或禁止创立非特权用户命名空间。然而,他们的补丁并没有合并到上游 Linux 内核中。
相同,上游内核在 Linux v6.1 (2022) 中引入了新的 LSM(Linux 平安模块)钩子 userns_create
,以便 LSM 能够动静决定是否容许或禁止创立用户命名空间。该钩子可从 eBPF (bpf_program__atttach_lsm()
) 调用,因而预计将有一个不依赖于 AppArmor 或 SELinux 的细粒度且非特定于发行版的旋钮。然而,eBPF + LSM 的用户空间实用程序尚未成熟,无奈为此提供良好的用户体验。
更多 LSM
Landlock LSM 已合并到 Linux v5.13 (2021) 中。Landlock 与 AppArmor 相似,它通过门路(LANDLOCK_ACCESS_FS_EXECUTE
、LANDLOCK_ACCESS_FS_READ_FILE
等)限度文件拜访,但 Landlock 不须要 root 权限来设置新配置文件。Landlock 也与 OpenBSD 的 promise(2)
十分类似。
Landlock 依然 不受 OCI Runtime Spec 反对,但我猜它能够蕴含在 OCI Runtime Spec v1.2 中。
Kata Containers
正如我在第一局部中提到的,“容器”并不是一个定义明确的术语。任何货色只有能与现有的容器生态系统提供良好的兼容性,就能够称为“容器”。
Kata Containers (2017) 就是这样一种“容器”,实际上并不是广义上的容器。Kata 容器实际上是虚拟机,但反对 OCI 运行时标准。Kata 容器比 runc 容器平安得多,然而它们在性能方面存在缺点,并且在不反对嵌套虚拟化的典型非裸机 IaaS 实例上无奈失常工作。
Kata Containers 作为一个 containerd 运行时插件,并接管与 runc 容器雷同的镜像和运行时配置。它的用户体验与 runc 容器简直没有区别。
gVisor
gVisor (2018) 是另一个奇异的容器运行时。gVisor 捕捉零碎调用并在 Linux 兼容的用户模式内核中执行它们以加重攻打。gVisor 目前具备 三种 捕捉零碎调用的模式:
- KVM 模式:很少应用,然而裸机主机的最佳抉择
- ptrace 模式:最常见的选项,但速度较慢
- SIGSYS trap 模式(自 2023 年起):预计最终取代 ptrace 模式
gVisor 已用于 Google 的多个产品中,包含 Google Cloud Run。然而,Google Cloud Run 已于 2023 年从 gVisor 转向 microVM。这意味着 gVisor 的性能和兼容性问题对于他们的业务来说是不可漠视的。
WebAssembly
WebAssembly (WASM) 是一种独立于平台的字节代码格局,最后于 2015 年 为 Web 浏览器设计。WebAssembly 与 Java applet (1995) 有点类似,但它更重视可移植性和安全性。WebAssembly 的一个乏味的方面是它将代码地址空间与数据地址空间离开;没有像 JMP <immediate>
和 JMP *<reg>
这样的指令。它仅反对 跳转到在编译时解析的标签。这种设计缩小了任意代码执行谬误,只管它也就义了 JIT 将其余字节代码格局编译为 WebAssembly 的可行性。
WebAssembly 作为容器的潜在替代品也受到关注。为了在浏览器之外运行 WebAssembly,WASI(WebAssembly 零碎接口)于 2019 年提出,提供低级 API(例如 fd_read()
、fd_write()
、sock_recv()
、sock_send()
)可用于在其上实现相似 POSIX 的层。containerd 在 2022 年增加了 runWASI 插件,将 WASI 工作负载视为容器。
2023 年,WASIX 被提议扩大 WASI 以提供更不便(也有些争议)的性能:
- 线程:
thread_spawn()
,thread_join()
`, … - 过程:
proc_fork()
,proc_exec()
, … - 套接字:
sock_listen()
,sock_connect()
, …
最终,这些技术可能会取代很大一部分(但不是 100%)的容器。Docker 的创始人 Solomon Hykes 示意:“如果 WASM+WASI 在 2008 年就存在,咱们就不须要创立 Docker 了”。
总结
- 容器比虚拟机更高效,但安全性往往也更低。人们正在引入许多平安技术来强化容器。(用户命名空间、无根容器、Linux 平安模块……)
- Docker 的替代品不断涌现(containerd、CRI-O、Podman、nerdctl、Finch 等),但 Docker 并没有隐没。
- “Non-container”容器也是趋势。(Kata:基于 VM,gVisor:用户模式内核,runWASI:WebAssembly,…)
下图显示了驰名的运行时的详情。
更多内容另请参阅 PPT 的其余部分,理解本文中无奈涵盖的其余主题。
文本翻译自: https://medium.com/nttlabs/the-internals-and-the-latest-trend…