当前位置: 首页 > 科技观察

实际运行容器的工具:深入了解Runc和OCI规范

时间:2023-03-16 00:56:32 科技观察

让我们谈谈位于Docker、Podman、CRI-O和Containerd核心的工具:runc。试图绘制从最终用户到实际容器进程的链条的原始容器运行时可能如下所示:runc是一个命令行客户端,用于运行根据开放容器倡议(OCI)格式打包的应用程序,并且是一个兼容的实现开放容器倡议规范。有一个开放容器计划(OCI)和关于如何运行容器和管理容器映像的规范。runc符合此规范,但还有其他符合OCI的运行时。它甚至可以运行OCI兼容的虚拟机。KataContainers和gVisor是符合OCI标准的虚拟机。以gVisor为代表的用户态Kernel方案是安全容器的未来,但还不完善。runc想要提供一个“OCI包”,它只是一个根文件系统和一个config.json文件。它没有像Podman或Docker那样的“镜像”的概念,所以你不能只执行runcrunnginx:latest来启动一个容器。Runc符合OCI(特别是运行时规范),这意味着它可以获取OCI包并从中运行容器。值得重申的是,这些包不是“容器镜像”,它们要简单得多。层、标签、容器注册表和存储库——所有这些都不是OCI包甚至运行时规范的一部分。有一个定义图像的OCI规范(image-spec)。文件系统包是您下载容器镜像并解压后得到的。所以它是这样的:OCIImage->OCIRuntimeBundle->OCIRuntime在我们的例子中,这意味着:Containerimage->Rootfilesystemandconfig.json->runc让我们构建一个应用程序包。我们可以从config.json文件开始,因为这部分非常简单:mkdirmy-bundlecdmy-bundleruncspecruncspec生成一个虚拟的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"],...它还定义了在哪里寻找rootfs..."root":{"path":"rootfs","re??adonly":true},...以及许多其他的,包括容器默认安装、功能、主机名等.如果您检查此文件,您会注意到许多部分与平台无关,并且特定于操作系统的部分嵌套在适当的内部部分中。例如,请注意有一个带有Linux特定选项的“linux”部分。如果我们尝试运行包,我们会得到一个错误:#runcruntestrootfs(/root/my-bundle/rootfs)doesnotexist如果我们只是创建文件夹,我们会得到另一个错误:#mkdirrootfs#runcruntestcontainer_linux.go:345:startingcontainerprocesscaused"exec:\"sh\":executablefilenotfoundin$PATH"这很有道理——空文件夹并不是真正有用的根文件系统,我们的容器没有机会做任何有用的事情。我们需要创建一个真正的Linux根文件系统。这里可以使用如下命令解压rootfs:$dockerexport$(dockercreatebusybox)|tar-C/mycontainer/rootfs-xvf-这里我们使用skopeo和umoci来获取OCI应用包。如何使用skopeo和umoci获取OCI应用程序包从头开始??创建根文件系统是一件很麻烦的事情,所以让我们使用busybox,它是可用的最小映像之一。要拉取镜像,我们首先需要安装skopeo。我们也可以使用Buildah,但它的功能太多无法满足我们的需求。Buildah专注于构建镜像,甚至具备运行容器的基本功能。因为我们今天要尽可能低级别,所以我们将使用skopeo:skopeo是一个命令行程序,可以对容器镜像和镜像存储库执行各种操作。skopeo可以在不同来源和目的地之间复制图像、检查图像甚至删除它们。skopeo无法构建镜像,它不知道如何处理Containerfile。它非常适合自动化容器映像升级的CI/CD管道。yuminstallskopeo-y然后复制busybox图像:skopeocopydocker://busybox:latestoci:busybox:latest没有“拉”——我们需要告诉skopeo图像的来源和目的地。skopeo支持几乎十几种不同类型的来源和目的地。NotethatthiscommandwillcreateanewbusyboxfolderwhereallOCIimagefileswillbefound,withdifferentimagelayers,manifests,etc.Don'tconfuseImagemanifestwithApplicationruntimebundlemanifest,theyarenotthesame.WhatwecopiedisanOCIImage,butwealreadyknowthatruncneedsOCIRuntimeBundle.Weneedatoolthatconvertsimagesintounpackedpackages.Thistoolwillbeumoci-anopenSUSEutilitywhosesolepurposeistomanipulateOCIimages.Toinstallit,getthelatestversionofPATHfromGithubReleases.在撰写本文时,最新版本是0.4.5.umociunpack获取OCI镜像并从中制作一个包:umociunpack--imagebusybox:latestbundle让我们看看bundle文件夹里面有什么:#lsbundleconfig.jsonrootfssha256_73c6c5e21d7d3467437633012becf19e632b2589234d7c6d0560083e1c70cd23.mtreeumoci.json让我们Copytherootfsdirectorytothemy-bundledirectorycreatedearlier.Ifyou'recurious,herearethecontentsoftherootfs,asfollows:bindevetchomeroottmpusrvarIfitlookslikeabasicLinuxrootfs,it'sright.AccordingtotheOCIRuntimespecification,applicationsundertheLinuxABIexpecttheLinuxenvironmenttoprovidethefollowingspecialfilesystems:/procfolder,mounttheprocfilesystem./sysfolder,mountthesysfsfilesystem./dev/ptsfolder,mountthedevptsfilesystem./dev/shmfolder,mountthetmpfsfilesystem.Thefunctionsofthesefoldersareomittedhere,interestedreaderscanrefertoman7.orgbythemselves.Additionalrequirementsintheruncdocumentation:/devfolder,mounttmpfsfilesystem./dev/mqueuefolder,mountthemqueuefilesystem.runc是OCI运行时规范的参考实现。该规范为容器创建提供了一个简洁的界面。你只需要为runc提供一个config.json[1]。使用runc运行OCI应用程序包我们已准备好将我们的应用程序包作为一个名为test的容器运行:runcruntest接下来发生的是我们最终进入了一个新创建的容器内的shell!#runcruntest/#lsbindevetchomeprocrootsystmpusrvar我们以默认前台模式运行之前的容器。在这种模式下,每个容器进程都成为一个长时间运行的runc进程的子进程:6801997\_sshd:root[priv]68056801\_sshd:root@pts/168066805\_-bash68256806\_zsh73426825\_runcruntest73607342|\_runcruntest如果我终止与该服务器的ssh会话,runc进程也会终止,最终终止容器进程。让我们通过替换config.json中的sleepinfinite命令并将终端选项设置为“false”来更仔细地检查这个容器。runc没有提供大量的命令行参数。它具有用于容器生命周期管理的启动、停止和运行等命令,但容器配置始终来自文件,而不是来自命令行:{"ociVersion":"1.0.1-dev","process":{"terminal":false,"user":{"uid":0,"gid":0},"args":["sleep","infinite"]...这次让我们以分离模式运行容器:runcruntest--detach我们可以看到正在运行的容器runc列表:IDPIDSTATUSBUNDLECREATEDOWNERtest4258running/root/my-bundle2020-04-23T20:29:39.371137097Zroot在Docker的情况下,有一个DockerDaemon,它知道容器的一切。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","re??adonlyfs":true,"rootPropagation":0,"mounts"....当我们以分离模式运行时,原来的runc运行命令(已经没有这个进程了)和这个容器进程没有关系。如果我们查看进程表,我们会看到容器的父进程是PID1:#psaxfopid,ppid,command42581sleepinfiniteDocker、containerd、CRI-O等使用分离模式。它的目的是简化runc和全功能容器管理工具之间的集成。值得一提的是,runc本身并不是某种库——它是一个CLI。当其他工具使用runc时,它们会调用我们刚刚看到的相同的runc命令。在runc文档中阅读有关前台模式和分离模式之间区别的更多信息。虽然容器进程的PID是4258,但是容器内部的PID显示为1:#runcexectestpsPIDUSERTIMECOMMAND1root0:00sleepinfinite13root0:00ps这要归功于Linux命名空间,这是真正容器背后的基础技术之一。我们可以通过lsns在主机系统上执行来列出所有当前的命名空间:#lsnsNSTYPENPROCSPIDUSERCOMMAND4026532219mnt14258rootsleepinfinite4026532220uts14258rootsleepinfinite4026532221ipc14258rootsleepinfinite4026532222pid14258rootsleepinfinite4026532224net14258rootsleepinfiniterunc负责我们容器进程的进程、网络、挂载和其他命名空间。Theshadowrulersofthecontainerworld,Podman,Docker,andallothertools,includingmostKubernetesclustersrunningoutthere,boildowntotheruncbinarythatstartsthecontainerprocess.Inpractice,youalmostneverdowhatIjustshowedyou-unlessyouaredevelopingordebuggingyourownorexistingcontainertools.Applicationbundlescannotbeassembledfromcontainerimages,anditwouldbebettertousePodmaninsteadofruncdirectly.runcistheimplementationoftheLow-Levelimplementation,andit'sveryhelpfulforustounderstandwhat'sgoingonbehindthescenesandwhat'sreallyinvolvedinrunningacontainer.Therearestillmanylayersbetweentheenduserandthefinalcontainerprocess,butifyouunderstandthelastlayer,thencontainersarenolongermagical,sometimesweird.Intheendyou'llfindthatthecontainerit'sjustruncspawnsaprocessinanamespace.Ofcourse,thelastlayeristheLinuxkernel,comparedtothecountlesslayersintheuniverse.ThemostimportantpartofruncisthatittrackstheOCIruntimespecification.Eventhoughnearlyeverycontainerthesedaysisspawnedwithrunc,itdoesn'thavetobespawnedwithrunc.Itcanbeswappedwithanyothercontainerruntimethatfollowstheruntimespecification,andcontainerenginessuchasCRI-Oshouldworkthesameway.High-Levelcontainerruntimecanbeindependentofruncitself.TheyrelyonsomecontainerruntimesthatfollowtheOCIspecification.Thisisthereallybeautifulpartoftoday'scontainerworld.参考[1]https://github.com/opencontainers/runtime-spec/blob/master/config.mdhttps://mkdev.me/en/posts/the-tool-that-really-runs-your-containers-深入了解runc和oci规范https://github.com/opencontainers/runc/blob/master/docs/terminals.mdhttps://katacontainers.io/https://polyverse.com/blog/skopeo-the-best-container-tool-you-need-to-know-about/https://umo.ci/quick-start/workflow/