zhuhaiqiang
作者zhuhaiqiang2019-10-12 17:55
项目经理, 银行

从容器化到可编排化

字数 3854阅读 1304评论 0赞 1

容器给更先进的服务器架构和复杂部署技术带来新生。今天,容器已经被广泛应用在不同领域,其底层则依靠Linux提供的namespaces和cgroups的支持。但是因为容器已经被大量使用,因此单独把容器分离出来,抽象成一层势在必行。本文我将从最底层到最高层做个尝试,包括很多具体操作(代码,安装,配置,整合等……),当然,也会很有趣。这些内容随着时间以及我自己的理解也会不断变化。

容器运行时

从最底层非核心优先级——容器运行时环境层开始。“运行时环境(runtime)”在容器上下文中有些歧义。每个项目,公司或者社区对这个词,根据上下文不同,有不同的理解。大部分运行时环境的定义是包括从最小底层操作(创建namespaces,启动init进程)到复杂容器管理(包括但不限于)镜像操作等一系列操作。“。

这一部分属于底层运行时环境操作,一组操作者形成Open Container Initiative标准,简单说,“底层容易运行时环境”是一组软件,它们采用某个目录下关于rootfs和配置文件作为输入内容,启动一个独立进程,也就是容器。到2019年,广泛使用的runtime是runC,它本来是Docker的一部分(因此用Go编码),最终被拆解形成一个自满足的CLI工具。runC本质上是OCI标准的一种实现,但是却显得很重要。

另外一个OCI运行时环境实现叫crun,用C语言编码,可以用做可执行代码或者库。

容器管理

在命令行使用runC可以启动任意多的容器。但是如果想自动处理怎么办?比如启动大量容器并监控它们的状态,容器失效时重启,资源在终止时释放,镜像要从注册库中下载,容器间网络需要配置等等需求。这些都是比较高层的需求,由容器管理负责。老实说,我并不知道这些要求是否常见,但是我发现这样的架构更方便,因此我将containerd、cri-o、dockerd、podman归类为容器管理的功能。
containerd
通过runC,可以再次看到Docker的结构:containerd初始作为Docker项目的一部分,现今containerd已经是一个自满足运行时环境软件了。尽管标称为container运行时环境,但是很明显跟runC不同。不仅是containerd和runC责任不同,而且他们原生的目的也不同。runC只是一个命令行工具,containerd则是一个后台进程。runC实例必须依存于底层的containerd进程。一般创建进程后,执行containerd rootfs下特定的文件。另外,containerd支撑大量容器,有点儿像监听请求请启停汇报容器状态的服务。containerd和runC之间,containerd除了作为容器生命周期管理器,还负责镜像管理(从镜像库推拉镜像,本地存放镜像),容器间网络管理和其他功能。

cri-o
另一个容器管理是cri-o。跟containerd从Docker架构中发展而来不一样,cri-o来自于Kubernetes。回溯过往,Kubernetes(滥)用Docker管理容器。然而,随着rkt日渐兴起,一些用户给Kubernetes添加了容器运行时环境的互操作,允许Docker或者rkt可以完成容器管理的任务。这一改变使Kubernetes中有很多与上下文相关的代码造成混乱,为了改变这种情况,Container Runtime Interface(CRI)被引入Kubernetes以支持与CRI兼容的高级运行时环境(例如容器管理),从而在Kubernetes端不需要任何更改。cri-o是来自红帽公司的与CRI兼容的运行时环境。和containerd类似,cri-o也是暴露[gPRC]服务的后台进程,提供创建启停容器的功能。这样,cri-o可以使用任何OCI兼容的运行时环境支撑容器,当然默认的还是runC。cri-o主要还是作为Kubernetes下容器运行时环境,其版本跟Kubernetes一样,项目目标很清晰,代码量很小(截至2019年七月,大约有20CLOC,比containerd小5倍)。

幸运的是标准之间可以互操作。如果使用了CRI,那么就会出现一种运行在containerd之上完成CRI gPRC服务的插件。这个想法本来是临时性的,后来成为containerd内生的CRI支持模块,从而Kubernetes也可以使用两种运行时环境了。
dockerd
另外一个后台进程是dockerd,它有多种功能。首先,它开放了Docker命令行API,使得所有跟Docker工作流相关的工作可以运行(docker pull、docker push、docker run、docker stats等)。因为我们知道这些功能已经被转移到containerd中,因此如果说dockerd底层以来containerd应该也不会太惊讶。但这基本上意味着,dockerd只是一种前端适配器,负责将容器化API转换为以往我们所常用的Docker引擎API。

然而,dockerd还提供了Compose和Swarm解决容器编排问题,包括容器集群等问题。从Kubernetes可以看出,问题比较难解决,而且一个后台进程有两个职责并不太理想。

podman
完成这一功能的后台进程的例外是podman,这是红帽的一个项目,目的是提供一个名为libpod的库(不是后台进程)来管理镜像,容器生命周期以及Pod(容器组)。podman是建立在libpod之上的命令行管理工具,作为底层容器运行时环境,libpod使用了runC。从代码角度来看,podman和cri-o(都是红帽项目)有很多共同点。例如,内部都严重依赖存储和镜像库。cri-o项目中有直接使用libpod作为运行时环境的趋势。podman另一个有趣功能是引入了很多Docker工作流命令的替代,并且宣称某种程度提供Docker CLI API的兼容。
在有了dockerd、containerd和cri-o情况下,为什么还要开始新的项目?后台进程的最大问题就是必须要用root特权运行,尽管90%的功能不需要root权限,但是剩下的10%则需要用root权限启动。有了podman之后,终于可以在用户空间使用容器了。这是一个很大的转变,特别是在扩展CI或者多租户环境,因为即使非特权Docker容器和获得root权限之间也就隔着一个bug。

conman
这是我开发的项目[5]实现简单的容器管理器,主要是为了学习目的,但是最终目标是与CRI兼容并且用其作为容器运行时环境启动Kubernetes集群。

运行时环境的坑

如果自己尝试过,就会很快发现使用runC作为容器管理器编程有很多坑需要考虑:
容器管理器重启保证容器仍然正常工作
容器管理器因为升级或者crash需要重启,而容器则应该保持正常工作。意味着容器必须和调度启动它的容器管理器无关。幸运的是,runC提供了一个解耦开关(runc run -detach)。之后,我们还需要attach一个运行的容器。为了实现这个需求,runC可以运行一个由Linux哑终端(PTY)控制的容器。PTY的主控端需要与启动进程通讯,通过UNIX Socket(参见runc create --console-socket选项)交互PTY文件描述符。也就意味着,只要容器仍然运行,就需要保证启动进程运转正常以持有PTY文件描述符。如果在容器管理器中存放PTY文件描述符,容器管理器重启会导致描述符丢失,容器也会不可用。也就是说需要一个专门的(轻量级的)wrapper进程负责后天进程化和跟踪运行容器的状态。
容器管理器和runc实例之间同步
要实现添加一个wrapper进程将runC后台化,我们需要一个side-channel(仍然可能是unix socket)在容器和管理器之间通讯。
监控容器退出码
容器解耦需要容器状态更新,并将状态反馈到容器管理器。实现此目的,文件系统看起来是个好选择,我们可以让wrapper进程等待子runC进程退出并将退出码写到磁盘预定义地方。
一般用运行时环境片段(runtime shim)来实现。片段是一个控制运行容器的轻量后台进程,实例如conman和containerd runtime shim。我花了一些时间实现conman自己的shim,可以参见“实现容器runtime shim[6]”。

容器网络接口(CNI)

由于有很多运行时环境,因此需要为每个项目解压网络相关代码并重用,要么每个运行时环境自己配置网络相关参数。例如,cri-o和containerd会创建Linux network namespaces,设置Linux bridges和veth设备,为Kubernetes Pod创建沙箱。为了解决以上问题,CNI被引入。
CNI项目提供CNI接口规范定义CNI插件,此插件会被容器运行时环境(或者管理器)调用,设置(或者释放)网络资源。CNI项目与实现语言无关,并且提供一些列参考插件实现可以从库中下载。例如bridge、loopback、flannel等等。
一些第三方项目也有自己的CNI实现,例如项目Calico和Weave。

编排

容器编排是一个很大的话题,实际上,Kubernetes与其说是为了解决容器化不如说是为了解决可编排化的问题。因此编排值得更多的讨论。

如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!

1

添加新评论0 条评论

Ctrl+Enter 发表

作者其他文章

相关文章

相关问题

相关资料

X社区推广