Hi在上一期《虚拟化及云计算硬核技术内幕 (33) —— 你说的这个朋友是不是你自己》中,我们理解了怎么样使用docker实现应用的快速部署。
由于docker的启动无需重新启动一个操作系统,还可以复用宿主机操作系统内置的依赖库等运行时所需要的组件,把应用打包成为docker镜像后,部署和启动应用的过程可以大大缩短。
那么,如果需要为应用在不同的宿主机上,启动总共100个实例,我们应该怎么做呢?
思路A:手工在若干台宿主机上敲docker run命令:
docker run -itd --net bridge --ip 172.17.0.10
(记得别忘了指定不同的IP地址,并为其指定bridge方式的docker容器网络,不然应用是无法正常启动或提供服务的)
显然,这种脑回路会被认为是很傻很天真 (洋文曰:Too simple, sometimes naive),然后被大家批判一番。
思路B:利用Ansible一类的自动化工具,定制可以驱动docker的剧本(playbook),自动化在不同宿主机上执行docker命令。
这种思路有几个缺陷:
因此,这只是权宜之计。
思路C:开发一套程序,进行docker容器的自动编排,自动化批量运行和调度容器,为容器赋予网络和存储等功能。这被叫做容器编排平台。
Kubernetes就是最常见的容器编排平台。
Kubernetes的介绍,大家可以在其官方网站获取:
https://kubernetes.io/
熟悉kubernetes的同学可以很容易地回忆起来,kubernetes有deployment, deamonset, cronjob等各种工作负载模型,可以自动化地批量拉起容器,并将容器按策略调度到工作节点上。实际上,在kubernetes早期的版本中,真正的工作还是由docker完成的。
那么,kubernetes是如何调用docker的呢?是通过命令或脚本的方式吗?
我们知道,在Kubernetes的每个work node上,会部署一个kubelet。Kubernetes的master上的scheduler,会驱动kubelet,调用docker一类的容器运行时,拉取容器镜像,实施容器的启动/销毁等行为。
如图,kubelet位于每个节点上,让container runtime运行C1,C2,C3等容器,并通过kube-proxy向网络提供服务。
让我们注意到一个细节:
docker运行一个容器的步骤,其实分为三个部分:
1. 获取容器镜像;
2. 对容器镜像进行解析,并解包到unionfs;
3. 从unionfs中读取容器中应用的可执行文件并执行;
而容器运行时的OCI(Open Container Initiative)组织,制定的容器运行时标准,只规范了第3部分的行为。如果其他容器运行时组件没有实现1和2,实际上并不能实现镜像的管理,也就是无法提供完整的取代docker的功能!
根据著名的墨菲定律,如果我们不期望一些事情发生,那么这件事情从长远看是一定会发生的。由于一些特殊的利益分配原因,kubernetes宣布从1.22版本起不再支持docker,取而代之的是containerd,rkt或cri-o一类第三方容器运行时引擎。
也正如墨菲定律描述的那样,虽然containerd和cri-o完整地实现了docker的三个功能,但其他容器运行时引擎(如lxc,runc等),实际上只能实现功能3,并不能实现功能1/2,也就是无法获取和管理容器的镜像。这一类容器运行时引擎被称为"Low level container runtime"。与此对立的则是"High level container runtime"。后者可以实现镜像管理,以及gRPC方式的API接口。
这种gRPC方式的API接口,就是所谓的CRI(Container Runtime Interface)接口,Kubernetes通过CRI接口能够实现让High level container runtime获取和管理容器镜像。
CRI的介绍可以看这里: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-node/container-runtime-interface.md
CRI中,定义了两个gRPC服务:RuntimeService 和 ImageManagerService,分别用于容器运行时和镜像的管理。
https://github.com/kubernetes/cri-api/blob/master/pkg/apis/services.go
这个地方有其定义:
// RuntimeService interface should be implemented by a container runtime.
// The methods should be thread-safe.
type RuntimeService interface {
RuntimeVersioner
ContainerManager
PodSandboxManager
ContainerStatsManager
// UpdateRuntimeConfig updates runtime configuration if specified
UpdateRuntimeConfig(runtimeConfig *runtimeapi.RuntimeConfig) error
// Status returns the status of the runtime.
Status(verbose bool) (*runtimeapi.StatusResponse, error)
}
// ImageManagerService interface should be implemented by a container image
// manager.
// The methods should be thread-safe.
type ImageManagerService interface {
// ListImages lists the existing images.
ListImages(filter *runtimeapi.ImageFilter) ([]*runtimeapi.Image, error)
// ImageStatus returns the status of the image.
ImageStatus(image *runtimeapi.ImageSpec, verbose bool) (*runtimeapi.ImageStatusResponse, error)
// PullImage pulls an image with the authentication config.
PullImage(image *runtimeapi.ImageSpec, auth *runtimeapi.AuthConfig, podSandboxConfig *runtimeapi.PodSandboxConfig) (string, error)
// RemoveImage removes the image.
RemoveImage(image *runtimeapi.ImageSpec) error
// ImageFsInfo returns information of the filesystem that is used to store images.
ImageFsInfo() ([]*runtimeapi.FilesystemUsage, error)
}
以对标docker的containerd为例,它的工作原理如下图:
图中,Kubernetes通过Kubelet,经grpc(CRI)调用containerd的daemon,containerd本身可以实现镜像的拉取,解析和管理,而具体pod的运行则由另一个low level container runtime实现,如runc。containerd和runc之间的接口叫做shim。
有了low level container runtime和high level container runtime的配合,可以构成完整的容器运行时,从而支持Kubernetes为代表的容器编排引擎自动化发放容器业务,快速实现应用的部署和自动伸缩。
提到应用的自动伸缩,就不得不提到古希腊神话中的一个人物——
大家猜猜看是谁?