草庐IT

Docker、Podman、Containerd 谁才是真正王者?

意海还念か 2023-04-09 原文

Docker、Podman、Containerd 谁才是真正王者?

Docker VS Podman

背景

目前,随着容器技术的快速发展,围绕着容出现了越来越多的技术。原本集所有功能为一体的Docker也进行了模块化,但是Docker将大多数的功能都集中到了Docker daemon,这里不利用系统发展的,系统耦合度高,在一些场景下,Docker不适合应用,所以Podman就诞生了。

Podman原来是CRI-O项目的一部分,后来被分离成了一个单独的项目libpod。容器化始于Docker,Docker的守护进程管理着容器的一切。但是随着OCI标准出台后,容器管理被分成了一个一个的部分,被模块化成一个个小工具,每个工具各司其职,而不是像Docker一样,守护进程管理着一切。图1为它们架构的对比。

图1 Docker vs podman

什么是Docker

与传统的虚拟机不同,Docker共享主机的内核,利用linux的namespace进行资源隔离,cgroups进行资源控制,使得应用程序像在一个个的集装箱中运行,而承载集装箱的就是docker守护进程。从图2中,我们可以看出容器和传统的虚拟机的对比,容器比虚拟机更轻量。

图2Docker的架构

Docker Client是Docker的客户端程序,用于将用户请求发送给Dockerd。Dockerd实际调用的是containerd的API接口,containerd是Dockerd和runc之间的一个中间交流组件,主要负责容器运行、镜像管理等。containerd向上为Dockerd提供了gRPC接口,使得Dockerd屏蔽下面的结构变化,确保原有接口向下兼容;向下,通过containerd-shim与runc结合创建及运行容器。值得一提的是,守护进程使用REST API,利用HTTP的客户端就能访问Docker的守护进程。

但是Docker也存在着一些问题:

  1. Docker在单个进程上运行,可能会导致单点故障。

  2. 所有子进程均归此守护进程所有。

  3. 在任何时候,如果Docker守护程序失败,所有子进程都会失去其跟踪。

  4. 并进入孤立状态。

  5. 安全漏洞。

  6. 对于Docker操作,所有步骤都需要由root执行。

什么是Podman

Podman是一个由 RedHat 公司推出的无守护程序的容器引擎,用于在Linux系统上开发,管理和运行OCI容器。容器可以以root用户或无root权限模式运行。无root权限运行容器,能够使得容器更加安全。

如图1所示,和Docker不一样的是,Podman分散了容器管理所需的所有组件,并将它们个性化为较小的组件,仅在必要时使用。

Podman使用buildah管理镜像,通过skopeo来和镜像仓库进行交互,使用容器运行时Runc来直接运行容器。所以Podman启动容器的速度相对快一些,不需要一个庞大臃肿的守护进程就能运行容器。并且使用起来和Docker没有太大的区别,甚至可以alias docker=podman,Podman几乎复制了Docker的所有命令,所以你可以很轻松的从Docker转移到Podman。

Podman、 Buildah 和 Skopeo都是OCI容器生态计划中的一部分,相关的生态软件都能在https://github.com/containers中找到。并且这些工具都是符合CRI标准的,基于 unix 传统的 fork-exec 模型,解决了由于 Docker 守护程序导致的启动和安全问题,提高了容器的性能和安全。

从RHEL8开始,已经移除了docker,并且使用Podman代替,虽然你依然可以从别处安装Docker,但是不是RHEL官方所推荐的。当然,Podman还可以管理pod,从其名就能看出。

Podman可以运行Rootless容器

由于Podman的模块化体系结构,因此不必以root用户身份运行容器。这是一个很大的优势,因为你可以与具有不同特权的其他用户一起运行容器,而不必冒有人可以访问容器服务并以root用户身份运行容器的风险,并且不会对服务器造成破坏。

Podman以非root用户身份运行时所执行的操作是在用户的主目录中创建一个目录,并将该用户拥有的图像和容器的所有信息存储在那里。因此,例如,如果你与非root用户一起制作Podman映像,它将仅显示该用户创建或下载的映像。

Podman的另一个优点是它能够使用使用命名空间的UID分隔,这在运行容器时提供了额外的隔离层。在安全性方面,Docker服务泄漏比获得root(sudo)特权更加危险。

当你获得管理员许可权(sudo)并在系统上执行某项操作时,它总是在系统审核日志中注册,因此总会有一条跟踪记录。但是,如果你访问Docker服务并从具有特权的容器中进行操作并摆脱该容器,那么几乎不可能知道你做了什么。它不会保存在任何日志中,也不会保存你的操作记录。这表明Podman是一种更安全的工具。

podman对比docker总结

使用Podman,Skopeo和Buildah的新一代容器架构后,可以解决由于docker守护程序导致的启动和安全问题。使用新架构后除了"没有守护进程"和"不需要sudo访问"之外,没有发现很多不同之处。构建的Rootless容器都位于用户目录下(~/.local/containers中)而不是全局的(在/var/lib/docker中),即面向用户而不是面向守护进程。

什么是 OCI?

OCI (Open Container Initiative),是一个轻量级,开放的治理结构(项目)。在 Linux 基金会的支持下成立,致力于围绕容器格式和运行时创建开放的行业标准。

OCI 项目由 Docker、CoreOS 和容器行业中的其它领导者在 2015 年 6 月的时候启动,OCI 的技术委员会成员包括 Red Hat、Microsoft、Docker、Cruise、IBM、Google、Red Hat 和 SUSE 等。

什么是 CRI?

CRI(Container Runtime Interface)是 Kubernetes v1.5 引入的容器运行时接口,它将 Kubelet 与容器运行时解耦,将原来完全面向 Pod 级别的内部接口拆分成面向 Sandbox 和 Container 的 gRPC 接口,并将镜像管理和容器管理分离到不同的服务。

位于 Docker、Podman、CRI-O 和 Containerd 核心的工具:runc

原始容器运行时

如果试图将链从最终用户绘制到实际的容器进程,它可能如下所示:

runc 是一个命令行客户端,用于运行根据 Open Container Initiative (OCI) 格式打包的应用程序,并且是 Open Container Initiative 规范的兼容实现。

有一个关于如何运行容器和管理容器映像的开放容器计划(OCI) 和规范。runc 符合此规范,但还有其他符合 OCI 的运行时。甚至可以运行符合 OCI 标准的虚拟机,Kata Containers 与gVisor就是符合符合 OCI 标准的虚拟机。gVisor 为代表的用户态 Kernel 方案是安全容器的未来,只是现在还不够完善。

runc 希望提供一个“ OCI 包”,它只是一个根文件系统和一个config.json 文件。而不是Podman 或 Docker 那样有“镜像”概念,所以不能只执行runc run nginx:latest这样来启动一个容器。

Runc 符合 OCI 规范(具体来说,是runtime-spec),这意味着它可以使用 OCI 包并从中运行一个容器。值得重申的是,这些bundle并不是“容器镜像”,它们要简单得多。层、标签、容器注册表和存储库等功能 - 所有这些都不是 OCI 包甚至运行时规范的一部分。有一个单独的 OCI-spec (image-spec )定义镜像。

文件系统包是你下载容器镜像并解压后得到的。所以它是这样的:

# OCI Image -> OCI Runtime Bundle -> OCI Runtime

在我们的例子中,这意味着:

# Container image -> Root filesystem and config.json -> runc

让我们构建一个应用程序包。我们可以从 config.json 文件开始,因为这部分非常简单:

# mkdir my-bundle
# cd my-bundle
# runc spec

runc spec生成一个虚拟的 config.json。它已经有一个“进程”部分,用于指定在容器内运行哪个进程 - 即使有几个环境变量。

{
        "ociVersion": "1.0.1-dev",
        "process": {
             "terminal": true,
                "user": {
                        "uid": 0,
                        "gid": 0
                },
                "args": [
                        "sh"
                ],
                "env": [
                     "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                        "TERM=xterm"
                ],
...

它还定义了在哪里查找根文件系统…

...
        "root": {
                "path": "rootfs",
                "readonly": true
        },
...

…以及其他许多内容,包括容器内的默认挂载、功能、主机名等。如果检查此文件,会注意到,许多部分与平台无关,并且特定于具体操作系统的部分嵌套在适当的内部部分。例如,会注意到有一个带有 Linux 特定选项的“linux”部分。

如果我们尝试运行这个包,我们会得到一个错误:

# runc run test
rootfs (/root/my-bundle/rootfs) does not exist

如果我们简单地创建文件夹,我们会得到另一个错误:

# mkdir rootfs
# runc run test
container_linux.go:345: starting container process caused "exec: \"sh\": executable file not found in $PATH"

这完全有道理 - 空文件夹并不是真正有用的根文件系统,我们的容器没有机会做任何有用的事情。我们需要创建一个真正的 Linux 根文件系统。这里可以使用如下命令解压rootfs:

$ docker export $(docker create busybox) | tar -C /mycontainer/rootfs -xvf -

这里我们使用skopeo 和 umoci 获取 OCI 应用程序包。

如何使用 skopeo 和 umoci 获取 OCI 应用程序包

从头开始创建 rootfilesystem 是一种相当麻烦的事情,因此让我们使用现有的最小映像之一 busybox。

要拉取镜像,我们首先需要安装skopeo。我们也可以使用 Buildah,但它的功能太多,无法满足我们的需求。Buildah 专注于构建镜像,甚至具有运行容器的基本功能。由于我们今天尽可能地低级别,我们将使用 skopeo:

  • skopeo 是一个命令行程序,可对容器镜像和镜像存储库执行各种操作。
  • skopeo 可以在不同来源和目的地之间复制镜像、检查镜像甚至删除它们。
  • skopeo 无法构建映像,它不知道如何处理 Containerfile。它非常适合自动化容器镜像升级的 CI/CD 管道。
yum install skopeo -y

然后复制busybox镜像:

skopeo copy docker://busybox:latest oci:busybox:latest

没有“拉取”——我们需要告诉 skopeo 镜像的来源和目的地。skopeo 支持几乎十几种不同类型的来源和目的地。请注意,此命令将创建一个新busybox文件夹,将在其中找到所有 OCI 镜像文件,具有不同的镜像层、清单等。

不要混淆 Image manifest 和 Application runtime bundle manifest,它们是不一样的。

我们复制的是一个 OCI Image,但是我们已经知道,runc 需要 OCI Runtime Bundle。我们需要一个将镜像转换为解压包的工具。这个工具将是umoci - 一个 openSUSE 实用程序,其唯一目的是操作 OCI 镜像。要安装它,请从 Github Releases获取最新版本的PATH。在撰写本文时,最新版本是0.4.5. umoci unpack获取 OCI 镜像并从中制作一个包:

umoci unpack --image busybox:latest bundle

让我们看看bundle文件夹里面有什么:

# ls bundle
config.json
rootfs
sha256_73c6c5e21d7d3467437633012becf19e632b2589234d7c6d0560083e1c70cd23.mtree
umoci.json

让我们将rootfs目录复制到之前创建的my-bundle目录。如果你好奇,这是rootfs的内容,如下:

bin  dev  etc  home  root  tmp  usr  var

如果它看起来像一个基本的 Linux 根文件系统,那么就是对的。

根据 OCI Runtime 规范,Linux ABI 下的应用程序会期望 Linux 环境提供以下特殊的文件系统:

  • /proc 文件夹,挂载 proc 文件系统。
  • /sys 文件夹,挂载 sysfs 文件系统。
  • /dev/pts 文件夹,挂载 devpts 文件系统。
  • /dev/shm 文件夹,挂载 tmpfs 文件系统。

这几个文件夹的作用这里略去,有兴趣的读者可以自行查阅 man7.org。runc 文档中还额外要求提供:

  • /dev 文件夹,挂载 tmpfs 文件系统。
  • /dev/mqueue 文件夹,挂载 mqueue 文件系统。

runc 是 OCI Runtime 规范的参考实现,规范为容器的创建提供了整洁的接口,只需要为 runc 提供一份 config.json [1]。

使用 runc 运行 OCI 应用程序包

我们准备好将我们的应用程序包作为名为 的容器运行test:

runc run test

接下来发生的事情是我们最终进入了一个新创建的容器内的 shell!

# runc run test
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var

我们以默认foreground模式运行前一个容器。在这种模式下,每个容器进程都成为一个长时间运行的runc进程的子进程:

6801   997  \_ sshd: root [priv]
6805  6801      \_ sshd: root@pts/1
6806  6805          \_ -bash
6825  6806              \_ zsh
7342  6825                  \_ runc run test
7360  7342                  |   \_ runc run test

如果我终止与该服务器的 ssh 会话,runc 进程也会终止,最终杀死容器进程。让我们通过sleep infinite在 config.json 中替换 command并将终端选项设置为“false”来更仔细地检查这个容器。

runc不提供大量的命令行参数。它有类似start,stop和 run的命令来做容器的生命周期管理,但是容器的配置总是来自文件,而不是来自命令行:

{
        "ociVersion": "1.0.1-dev",
        "process": {
                "terminal": false,
                "user": {
                        "uid": 0,
                        "gid": 0
                },
                "args": [
                        "sleep",
                        "infinite"
                ]
...

这次让我们以分离模式运行容器:

runc run test --detach

我们可以看到正在运行的容器runc list:

ID          PID         STATUS      BUNDLE            CREATED                          OWNER
test        4258        running     /root/my-bundle   2020-04-23T20:29:39.371137097Z   root

在 Docker 的情况下,有一个Docker Daemon守护进程知道关于容器的一切。runc 如何找到我们的容器?事实证明,它只是在文件系统上保持状态,默认情况下在里面/run/runc/CONTAINER_NAME/state.json:

# cat /run/runc/test/state.json
{"id":"test","init_process_pid":4258,"init_process_start":9561183,"created":"2020-04-23T20:29:39.371137097Z","config":{"no_pivot_root":false,"parent_death_signal":0,"rootfs":"/root/my-bundle/rootfs","readonlyfs":true,"rootPropagation":0,"mounts"....

当我们在分离模式下运行时,原始runc run命令(不再有这样的进程)和这个容器进程之间没有关系。如果我们查看进程表,我们会看到容器的父进程是PID 1:

# ps axfo pid,ppid,command
4258     1 sleep infinite

Docker、containerd、CRI-O 等使用分离模式。它的目的是简化 runc 和全功能容器管理工具之间的集成。值得一提的是 runc 本身并不是某种类型的库——它是一个 CLI。当其他工具使用 runc 时,它们会调用我们刚刚在操作中看到的相同 runc 命令。

在runc 文档中阅读有关前台模式和分离模式之间差异的更多信息。虽然容器进程的PID是4258,但在容器内部PID显示为1:

# runc exec test ps                     
PID   USER     TIME  COMMAND
    1 root      0:00 sleep infinite
   13 root      0:00 ps

这要归功于Linux 命名空间,它是真正的容器背后的基本技术之一。我们可以通过lsns在主机系统上执行来列出所有当前的命名空间 :

# lsns
NS TYPE   NPROCS   PID USER COMMAND
4026532219 mnt         1  4258 root sleep infinite
4026532220 uts         1  4258 root sleep infinite
4026532221 ipc         1  4258 root sleep infinite
4026532222 pid         1  4258 root sleep infinite
4026532224 net         1  4258 root sleep infinite

runc 负责我们容器进程的进程、网络、挂载和其他命名空间。

容器世界的影子统治者

Podman、Docker 和所有其他工具,包括在那里运行的大多数 Kubernetes 集群,都归结为runc启动容器进程的二进制文件。

在实际工作中,几乎永远不会做我刚刚给你展示的事情 - 除非正在开发或者调试自己的或现有的容器工具。不能从容器映像中组装应用程序包,并且使用 Podman 而不是直接使用 runc 会更好。

runc就是Low-Level实现的实现,我们了解幕后发生的事情以及运行容器真正涉及的内容是非常有帮助的。最终用户和最终容器过程之间仍然有很多层,但是如果了解最后一层,那么容器将不再是神奇的东西,有时也很奇怪。最后你会发现容器它只是 runc 在命名空间中生成一个进程。当然最后一层是Linux内核,相比宇宙中有无数层。

runc 最重要的部分是它跟踪 OCI运行时规范。尽管几乎每一个容器,这些天与runc催生,它不具有与runc催生。可以将其与遵循运行时规范的任何其他容器运行时交换,并且容器引擎(如 CRI-O)应该以相同的方式工作。

High-Level容器运行时可以不依赖于 runc 本身。它们依赖于一些遵循 OCI 规范的容器运行时。这是当今容器世界真正美丽的部分。

有关Docker、Podman、Containerd 谁才是真正王者?的更多相关文章

  1. Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting - 2

    1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里

  2. ruby-on-rails - 我需要一个真正的 UNIX RoR 开发环境 - 2

    从一开始,我就是一个Windows高手。我从MS-DOS开始。我安装了Windows2.1以及此后的所有Windows。现在,我家里有10台不同的Windows机器在运行,从Windows7Ultimate到各种版本的WindowsServer。我还没有完成Windows8,也不想去那里。我在服务器和各种软件方面都有UNIX经验,但它并不是我的首选环境。但是,我想我正在转换。我试图假装使用Cygwin和MSYS在Windows下运行UNIX。我的目的是搭建一个开发环境。两者都让我失望了。我花了比开发更多的时间来解决一系列技术问题。这是NotAcceptable。到目前为止,我的Ruby

  3. ruby -\b 在 Ruby 正则表达式中的真正含义是什么? - 2

    我有一个文件,其中包含诸如“CanyonSt/27thWay”之类的短语,我正试图使用​​Ruby正则表达式将其转换为“CanyonStand27thWay”。我使用file=file.gsub(/(\b)\/(\b)/,"#{$1}and#{$2}")进行匹配,但我我对\b的真正含义以及为什么$1包含斜线之前的单词边界之前的所有字符以及为什么$2包含从下一个单词开始的单词边界之后的所有字符感到有点困惑。通常,我希望正则表达式括号中的任何内容都在$1和$2中,但我不确定单词边界周围的括号真正意味着什么,因为从单词字符到字符的转换之间确实没有任何内容一个空白字符。

  4. ruby-on-rails - my_object.save(false) 并没有真正跳过我的 Active Record 验证 - 2

    所以我一直在努力解决我一直遇到的这个错误,我终于找到了导致它的原因。我一直觉得,当我调用@my_model.save(false)我会跳过我的ActiveRecord验证。事实证明这是部分正确的。我的对象正在保存到数据库中DESPITE我的ActiveRecord验证。我的问题存在是因为我的一个验证在验证过程中修改了一个子模型(这是一个24小时位置的调度应用程序,因此当午餐被保存时,我对照他们保存的那天和第二天检查它们以及确保用户不是指“凌晨2点”表示要上夜类。我的问题是:有没有办法真正跳过我的验证并直接移动到数据库?这是正常的ActiveRecord行为还是我应该更深入地研究我的验证

  5. ruby-on-rails - 私有(private) gem 没有安装在 docker 中 - 2

    我正在尝试使用docker运行一个Rails应用程序。通过github的sshurl安装的gem很少,如下所示:Gemfilegem'swagger-docs',:git=>'git@github.com:xyz/swagger-docs.git',:branch=>'my_branch'我在docker中添加了keys,它能够克隆所需的repo并从git安装gem。DockerfileRUNmkdir-p/root/.sshCOPY./id_rsa/root/.ssh/id_rsaRUNchmod700/root/.ssh/id_rsaRUNssh-keygen-f/root/.ss

  6. ruby-on-rails - 将 Heroku 环境变量传输到 Docker 实例 - 2

    我在Heroku上构建了一个必须在Docker容器内运行的RoR应用程序。为此,我使用officialDockerfile.因为它在Heroku中很常见,所以我需要一些附加组件才能使这个应用程序完全运行。在生产中,变量DATABASE_URL在我的应用程序中可用。但是,如果我尝试其他一些使用环境变量(在我的例子中是Mailtrap)的加载项,变量不会在运行时复制到实例中。所以我的问题很简单:如何让docker实例在Heroku上执行时知道环境变量?您可能会问,我已经知道我们可以在docker-compose.yml中指定一个environment指令。我想避免这种情况,以便能够通过项目

  7. ruby - 更新 gem 时 Docker 包安装缓存问题 - 2

    我在开发和生产中都使用docker,真正困扰我的一件事是docker缓存的简单性。我的ruby​​应用程序需要bundleinstall来安装依赖项,因此我从以下Dockerfile开始:添加GemfileGemfile添加Gemfile.lockGemfile.lock运行bundleinstall--path/root/bundle所有依赖项都被缓存,并且在我添加新gem之前效果很好。即使我添加的gem只有0.5MB,从头开始安装所有应用程序gem仍然需要10-15分钟。由于依赖项文件夹的大小(大约300MB),然后再花10分钟来部署它。我在node_modules和npm上遇到了

  8. 【详解】Docker安装Elasticsearch7.16.1集群 - 2

    开门见山|拉取镜像dockerpullelasticsearch:7.16.1|配置存放的目录#存放配置文件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/config#存放数据的文件夹mkdir-p/opt/docker/elasticsearch/node-1/data#存放运行日志的文件夹mkdir-p/opt/docker/elasticsearch/node-1/log#存放IK分词插件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/plugins若你使用了moba,直接右键新建即可如上图所示依次类推创建

  9. 你真正了解什么是接口测试么?接口实战一“篇”入魂 - 2

    最近在工作中,看到一些新手测试同学,对接口测试存在很多疑问,甚至包括一些从事软件测试3,5年的同学,在聊到接口时,也是一知半解;今天借着这个机会,对接口测试做个实战教学,顺便总结一下经验,分享给大家。计划拆分成4个模块跟大家做一个分享,(接口测试、接口基础知识、接口自动化、接口进阶)感兴趣的小伙伴记得关注,希望对你的日常工作和求职面试,带来一些帮助。注:文章较长有5000多字,希望小伙伴们认真看完,当然有些内容对小白同学不是太友好,如果你需要详细了解其中的一些概念或者名词,请在文章之后留言,后续我将针对大家的疑问,整理输出一些大家感兴趣的文章。随着开发模式的迭代更新,前后端分离已不是新的概念,

  10. 转转测试环境docker化实践 - 2

        测试环境对于任何一个软件公司来讲,都是核心基础组件之一。转转的测试环境伴随着转转的发展也从单一的几套环境发展成现在的任意的docker动态环境+docker稳定环境环境体系。期间环境系统不断的演进,去适应转转集群扩张、新业务的扩展,走了一些弯路,但最终我们将系统升级到了我们认为的终极方案。下面我们介绍一下转转环境的演进和最终的解决方案。1测试环境演进1.1单体环境    转转在2017年成立之初,5台64G内存的机器,搭建5个完整的测试环境。就满足了转转的日常所需。一台分给开发,几台分给测试。通过沟通协调就能解决多分支并行开发下冲突问题。1.2动态环境+稳定环境    随着微服务化的进

随机推荐