源码级解读为何 kubernetes 弃用 Docker 容器运行时
本文转载自【源码解读】从代码实现层面思考 Kubernetes 为什么会弃用对 Docker 的支持?[1]
作者: Colstuwjx[2]
2020年底,在 Kubernetes v1.20 正式发布的同时,k8s 官方还搞了一个大动作:他们宣布将会逐步弃用对 Docker 容器运行时的支持。为了不让用户惊慌失措,官方还贴心地写了一篇博客文章[3],对此事进行了一番详细说明。
**K8s 为什么会弃用对 Docker 的支持呢?**除了官方的这篇文章以外,很多科技媒体也做了相应的解读,比如 infoq 的这篇文章[4]。但是,为什么一定要弃用 docker 呢?这方面的维护成本究竟有多高?为了得到一个明确的答案,笔者决定展开一次 k8s 源码的探索之旅,一探究竟。
前世
在官方发布的博客文章里链接了一份弃用 Dockershim 的常见问题解答[5]。在这份 FAQ 里,官方也提到了弃用 Dockershim 的根本原因:
Docker itself doesn't currently implement CRI, thus the problem. Dockershim was always intended to be a temporary solution (hence the name: shim).
翻译一下就是: Dockershim 是当初 k8s 引入 CRI 容器运行时标准接口的时候为了兼容 Docker,k8s 官方自行维护的一套临时解决方案,他们现在不想再维护了。
dockershim 的起点
那么,dockershim 是什么时候加进去的呢?当时的背景又是怎样的?
笔者找到了当初开发人员提的第一个 PR #29553 [6],PR title 里面有这么一句话:
yujuhong: ... Add a new docker integration with kubelet using the new runtime API ...
根据 PR 里给出的信息,顺藤摸瓜,笔者又找到了相关的 umbrella issue [7],主要是用来跟踪 CRI 对接的进展。也就是说,为了让 Docker 支持 CRI 标准,核心开发 yujuhong 贡献了集成 docker 操作并且支持新版 runtime API 的一个组件(也即是 dockershim ),具体可以查阅这个 issue ,这是当时用来跟踪 dockershim 实现 CRI 接口专门开的一个 issue。
注:dockershim 的代码位于 k8s 仓库的这里[8],我们可以很方便地通过追溯 commit history [9]来找到首次提交,最终便找到了这个 PR 。
前 CRI 时代
在翻阅这块代码的时候,笔者内心还有一个疑问: 在 CRI 标准提出之前,k8s 是怎么和容器运行时交互的呢?
带着这个问题,笔者通过搜索找到了宣布引入 CRI 标准的官方文章[10]。
文章里介绍到,自 k8s 1.5 起,CRI 功能作为一个 alpha 特性被引入到 k8s,而早在 1.3 版本开始,k8s 就集成了 rkt 容器运行时的支持,作为替代 docker 的可选方案。
然而,这些代码都是托管在 k8s kubelet 的核心代码里,后续维护和增加更多容器运行时支持都会变得越来越困难。
那么,我们不妨来看看当时版本的 k8s 具体是怎么和 docker 及 rkt 引擎做交互的吧。定位代码的方式也很简单,直接选择 1.5 之前的版本,比如 v1.4.0 的 tag 版本。然后,既然官方说代码嵌在了 kubelet 代码里,我们可以直接切到 kubelet 代码的目录,不难找到下面这两个子目录:
- rkt[11]
- dockertools[12]
注:这里还有一个小彩蛋,我们在该目录下还找到了一个 rktshim [13]的目录,说明当初各个容器运行时尚未普及对 CRI 的支持时,在 k8s 代码里嵌 xxxshim 服务用作临时支持是一个常规操作。
那么,它们到底是咋交互的呢?
我们不难在 dockertools 目录下的 kube_docker_client.go[14] 里面找到一个 kubeDockerClient [15]的实现:
// kubeDockerClient is a wrapped layer of docker client for kubelet internal use. This layer is added to:
// 1) Redirect stream for exec and attach operations.
// 2) Wrap the context in this layer to make the DockerInterface cleaner.
// 3) Stabilize the DockerInterface. The engine-api is still under active development, the interface
// is not stabilized yet. However, the DockerInterface is used in many files in Kubernetes, we may
// not want to change the interface frequently. With this layer, we can port the engine api to the
// DockerInterface to avoid changing DockerInterface as much as possible.
// (See
// * https://github.com/docker/engine-api/issues/89
// * https://github.com/docker/engine-api/issues/137
// * https://github.com/docker/engine-api/pull/140)
// TODO(random-liu): Swith to new docker interface by refactoring the functions in the old DockerInterface
// one by one.
type kubeDockerClient struct {
// timeout is the timeout of short running docker operations.
timeout time.Duration
client *dockerapi.Client
}
上面的注释也写的挺详细,大致意思就是它是一个和 Docker 交互的 client,并封装了一些 k8s 操作 Docker 需要的一些接口方法,这套接口方法具体定义在同一目录的 docker.go[16] 里:
// DockerInterface is an abstract interface for testability. It abstracts the interface of docker client.
type DockerInterface interface {
ListContainers(options dockertypes.ContainerListOptions) ([]dockertypes.Container, error)
InspectContainer(id string) (*dockertypes.ContainerJSON, error)
CreateContainer(dockertypes.ContainerCreateConfig) (*dockertypes.ContainerCreateResponse, error)
StartContainer(id string) error
StopContainer(id string, timeout int) error
RemoveContainer(id string, opts dockertypes.ContainerRemoveOptions) error
InspectImage(image string) (*dockertypes.ImageInspect, error)
ListImages(opts dockertypes.ImageListOptions) ([]dockertypes.Image, error)
PullImage(image string, auth dockertypes.AuthConfig, opts dockertypes.ImagePullOptions) error
RemoveImage(image string, opts dockertypes.ImageRemoveOptions) ([]dockertypes.ImageDelete, error)
ImageHistory(id string) ([]dockertypes.ImageHistory, error)
Logs(string, dockertypes.ContainerLogsOptions, StreamOptions) error
Version() (*dockertypes.Version, error)
Info() (*dockertypes.Info, error)
CreateExec(string, dockertypes.ExecConfig) (*dockertypes.ContainerExecCreateResponse, error)
StartExec(string, dockertypes.ExecStartCheck, StreamOptions) error
InspectExec(id string) (*dockertypes.ContainerExecInspect, error)
AttachToContainer(string, dockertypes.ContainerAttachOptions, StreamOptions) error
ResizeContainerTTY(id string, height, width int) error
ResizeExecTTY(id string, height, width int) error
}
可以看到,k8s 在 Pod 的生命周期里需要用到的一些操作函数都已经包含在内。
到这里,大致概括一下 k8s 在引入 CRI 阶段的一个迭代过程吧:
1、在 CRI 标准落地之前,k8s 等于是为每一个容器运行时都实现了一个具体的对接,rkt 和 dockertools 目录下即对应的代码实现;
2、官方于 1.5 版本开始正式引入 CRI 标准,并实现了对应的 shim 代码,如 dockershim 和 rktshim,在各个容器运行时尚未支持 CRI 标准的接口之前,充当一个胶水服务。
今生通过追溯之前的版本历史,笔者终于了解了 k8s 在支持容器运行时这块的”坎坷经历”。
然而,最开始的问题始终未能得到解答:为什么非得要弃用 dockershim ? 继续维护下去的话究竟会有哪些具体的痛点呢?
毕竟,如果弃用 dockershim 的话,这意味着原本使用 docker 作为容器引擎的用户需要为此计划实施迁移到 containerd 或者其他支持 CRI 的容器运行时,这会是一个不小的时间和人力成本。
想要解答这个问题,恐怕还得先看看 dockershim 目前的使用场景以及 CRI 的发展现状。
启动前还要运行 dockershim 服务?
时至今日,kubelet 要去启动一个 docker 容器的话,究竟是怎么和 dockershim 配合工作的呢?不妨再来看看 kubelet 这层的代码实现。
这次笔者选的是刚发布不久的 v1.22[17] 版本的代码。
kubelet 的启动入口位于 cmd/kubelet/kubelet.go[18],熟悉 cobra 的朋友应该知道,它最终是会调用具体 Command 的 Run 方法。对于 kubelet 来说,调用的即是它实现的 Run[19] 方法。
在经过一系列的处理后,kubelet 会走到核心的用来启动服务的 run 方法。直接看和 Dockershim 相关的部分!划到靠近函数末尾的部分,可以看到在真正启动前,kubelet 执行了一个 kubelet.PreInitRuntimeService[20] 的操作。
这个 PreInitRuntimeService 方法做了什么事情呢?
不妨继续深入一下,看看它的具体内容:
// PreInitRuntimeService will init runtime service before RunKubelet.
func PreInitRuntimeService(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
kubeDeps *Dependencies,
crOptions *config.ContainerRuntimeOptions,
containerRuntime string,
runtimeCgroups string,
remoteRuntimeEndpoint string,
remoteImageEndpoint string,
nonMasqueradeCIDR string) error {
if remoteRuntimeEndpoint != "" {
// remoteImageEndpoint is same as remoteRuntimeEndpoint if not explicitly specified
if remoteImageEndpoint == "" {
remoteImageEndpoint = remoteRuntimeEndpoint
}
}
switch containerRuntime {
case kubetypes.DockerContainerRuntime:
klog.InfoS("Using dockershim is deprecated, please consider using a full-fledged CRI implementation")
if err := runDockershim(
kubeCfg,
kubeDeps,
crOptions,
runtimeCgroups,
remoteRuntimeEndpoint,
remoteImageEndpoint,
nonMasqueradeCIDR,
); err != nil {
return err
}
case kubetypes.RemoteContainerRuntime:
// No-op.
break
default:
return fmt.Errorf("unsupported CRI runtime: %q", containerRuntime)
}
...
}
可以看到,当 containerRuntime
参数是 kubetypes.DockerContainerRuntime
时,kubelet 需要执行额外的 runDockershim
方法去启动一个 dockershim
服务(可以看到,上面有一行警告 dockershim
已弃用的提醒),而如果是 kubetypes.RemoteContainerRuntime
类型的话,则什么事情也不用干。
笔者还在 kubelet 目录下找到了 kubelet_dockershim.go
,该文件里即实现了这个 runDockershim 方法,它会去调用 dockershim 的相关服务代码并启动一个 dockerServer
。
很显然, kubelet 是通过这个 dockershim
服务包装的一层 CRI 接口调用 docker 启动 Pod 容器的。我们不妨看下 kubelet 实际是怎么去起 Pod 的,然后再来看看它是如何调用的容器运行时。
kubeGenericRuntimeManager 的用途
回到 cmd/kubelet/app/server.go,在执行了 PreInitRuntimeService
之后,不难发现 kubelet 会去执行 RunKubelet,并最终通过 kubelet.NewMainKubelet 来初始化 kubelet 服务实例。
注:关于 kubelet 完整的启动逻辑,有位网易的同学写了一个系列文章[21],有兴趣的朋友可以看看。
这里面有关 runtime 部分最重要的就是这一段[22]了:
runtime, err := kuberuntime.NewKubeGenericRuntimeManager(
...
)
这里初始化了一个 kubeGenericRuntimeManager
的对象,它可以做哪些事情呢?我们暂且按下不表,先从 kubelet 这一层找找入口。回过头来,我们再来看看 kubelet 启动入口 NewMainKubelet
这块。可以看到,在初始化 kubeGenericRuntimeManager
之前,kubelet 初始化了一个 workQueue,并且初始化了一批 podWorker:
klet.podWorkers = newPodWorkers(
klet.syncPod,
klet.syncTerminatingPod,
klet.syncTerminatedPod,
kubeDeps.Recorder,
klet.workQueue,
klet.resyncInterval,
backOffPeriod,
klet.podCache,
)
熟悉 k8s 异步调谐这套控制器逻辑的朋友,应该能猜到。没错,这个 podWorker
就是监听 kubelet 关注的 Pod 资源的变化,并执行相应的调谐逻辑。这里先看一下 syncPod
这块的实现。
注:有兴趣的朋友可以看看 syncPod
方法的注释部分[23],里面描述了 syncPod 的整体流程。
syncPod 方法里的其他细节部分忽略,我们直接关注最终调用容器运行时服务同步 Pod 的操作部分[24]:
result := kl.containerRuntime.SyncPod(pod, podStatus, pullSecrets, kl.backOff)
可以看到,这里 kubelet 实例调用的 containerRuntime[25] 毫无疑问便是之前 kubelet 在 NewMainKubelet
初始化 kubeGenericRuntimeManager
时创建出来的 runtime
实例:
runtime, err := kuberuntime.NewKubeGenericRuntimeManager(
...
)
klet.containerRuntime = runtime
那么,这个 runtime manager 具体又是怎么调用容器运行时服务来 SyncPod 的呢?
调用 runtime service 来 SyncPod
我们不妨先来看看 SyncPod 方法的注释部分:
// SyncPod syncs the running pod into the desired pod by executing following steps:
//
// 1. Compute sandbox and container changes.
// 2. Kill pod sandbox if necessary.
// 3. Kill any containers that should not be running.
// 4. Create sandbox if necessary.
// 5. Create ephemeral containers.
// 6. Create init containers.
// 7. Create normal containers.
func (m *kubeGenericRuntimeManager) SyncPod(pod *v1.Pod, podStatus *kubecontainer.PodStatus, pullSecrets []v1.Secret, backOff *flowcontrol.Backoff) (result kubecontainer.PodSyncResult) {
...
}
可以看到,这就是一次经典的调谐逻辑。
按照它的说法,它会计算 Pod 当前的状态,然后按需清理环境,并尝试保证 Pod Sandbox 及相关容器(依次是 ephemeral container、init container 以及应用容器)处于运行状态。
快速浏览了一下 SyncPod
具体实现之后,不难发现,它将一些具体的实现部分放到了几个单独的方法里,如:createSandbox、startContainer。
这里,以 createSandbox
为例,看看 kubelet 在创建 Pod Sandbox 这块,调用 dockershim 和其他支持 CRI 的容器运行时有什么不同。略过生成 Pod 配置等步骤,直接看最核心的这一段:
podSandBoxID, err := m.runtimeService.RunPodSandbox(podSandboxConfig, runtimeHandler)
隐约可以猜到,这个 runtimeService 应该就是一个统一实现调用 CRI 的入口,不妨回过头来再看看 kuberuntime.NewKubeGenericRuntimeManager
这一步是怎么初始化这个 runtimeService
的:
runtime, err := kuberuntime.NewKubeGenericRuntimeManager(
...
kubeDeps.RemoteRuntimeService,
...
)
咦?这个 kubeDeps
又是何方神圣呢?顺着源头找,可以看到它是 NewMainKubelet
就传入进来的一个参数项。再顺着调用链的源头,笔者找到了 cmd/kubelet/app/server.go 里的 RunKubelet
:
if err := RunKubelet(s, kubeDeps, s.RunOnce); err != nil {
return err
}
再往上走便可以发现,这个 kubeDeps
早在 kubelet NewKubeletCommand
时候就已经做了初始化:
// use kubeletServer to construct the default KubeletDeps
kubeletDeps, err := UnsecuredDependencies(kubeletServer, utilfeature.DefaultFeatureGate)
if err != nil {
klog.ErrorS(err, "Failed to construct kubelet dependencies")
os.Exit(1)
}
但是仔细一看,里面并没有初始化 RemoteRuntimeService
啊,那什么时候做的呢?
啊!前文提到过,在执行 RunKubelet
前,kubelet 事先执行了 PreInitRuntimeService
,它在里面是这样初始化 kubeDeps
的相关运行时依赖的:
if kubeDeps.RemoteRuntimeService, err = remote.NewRemoteRuntimeService(remoteRuntimeEndpoint, kubeCfg.RuntimeRequestTimeout.Duration); err != nil {
return err
}
想必这个 pkg/kubelet/cri/remote/remote_runtime.go[26] 便是统一实现了调用 CRI 的 client 接口!
至此,kubelet 调用容器运行时的流程基本浮出了水面:
1、kubelet 在 NewKubeletCommand
命令入口便初始化了 kubeDeps
对象,用来存放一些 kubelet 需要的依赖;
2、在 Kubelet 执行 RunKubelet
之前它会先执行 PreInitRuntimeService
根据 containerRuntime
参数初始化 runtimeService
句柄并存放到 kubeDeps
便于后面部分调用;
3、在上一步骤中,如果是 docker 的话,会额外执行 runDockershim
启动 dockershim 服务;
4、执行 RunKubelet
方法时,它会进一步去执行 NewMainKubelet
并最终启动 kubelet 服务;
5、在 NewMainKubelet
这一步 kubelet 会初始化 Pod Worker 去执行 Pod 调谐,具体执行方法为 syncPod
、syncTerminatingPod
等;
6、此外,NewMainKubelet
这一步还在初始化 KubeGenericRuntimeManager
的时候传入了 kubeDeps.RemoteRuntimeService
,然后将 runtime manager 该实例赋给了 kubelet.containerRuntime
;
7、当 kubelet 的 pod worker 进入主要的 syncPod 调谐周期时,它会调用 runtime manager 的 SyncPod
方法去做同步;
8、runtime manager 的 SyncPod
方法会做一系列判断,并执行相应的必要操作,比如 createSandbox
,它会通过之前传入的 runtimeService 的 RunPodSandbox
方法调用具体的容器运行时服务做对应的事情。
dockershim 的 CRI 实现
嗯 ,大致了解了 kubelet 调用容器运行时做 syncPod 调谐的这个过程了。那 dockershim 又是怎样具体实现这一套运行时接口的呢?
以 RunSandbox
这个接口为例,可以看到 dockershim 的实现里做了大量手动操作的事情:
// RunPodSandbox creates and starts a pod-level sandbox. Runtimes should ensure
// the sandbox is in ready state.
// For docker, PodSandbox is implemented by a container holding the network
// namespace for the pod.
// Note: docker doesn't use LogDirectory (yet).
func (ds *dockerService) RunPodSandbox(ctx context.Context, r *runtimeapi.RunPodSandboxRequest) (*runtimeapi.RunPodSandboxResponse, error) {
...
// dockershim 会先保证 sandbox 镜像的存在,按需执行 docker pull
if err := ensureSandboxImageExists(ds.client, image); err != nil {
return nil, err
}
...
// dockershim 还会根据配置手动创建 infra 容器
createConfig, err := ds.makeSandboxDockerConfig(config, image)
if err != nil {
return nil, fmt.Errorf("failed to make sandbox docker config for pod %q: %v", config.Metadata.Name, err)
}
createResp, err := ds.client.CreateContainer(*createConfig)
if err != nil {
createResp, err = recoverFromCreationConflictIfNeeded(ds.client, *createConfig, err)
}
...
// dockershim 手动创建 checkpoint
if err = ds.checkpointManager.CreateCheckpoint(createResp.ID, constructPodSandboxCheckpoint(config)); err != nil {
return nil, err
}
...
// dockershim 调用 docker client 去启动容器
// 注意,这个时候 infra 容器的网络栈还没设置
err = ds.client.StartContainer(createResp.ID)
if err != nil {
return nil, fmt.Errorf("failed to start sandbox container for pod %q: %v", config.Metadata.Name, err)
}
...
// 如果 dns 配置需要定制,dockershim 还会去手动重写该容器的 dns 配置
// 这块是真的没想到,`rewriteResolvFile` 里就是一些调用操作系统接口去重写文件
// docker client 难道没有提供设置 dns 的方式吗?
...
if err := rewriteResolvFile(containerInfo.ResolvConfPath, dnsConfig.Servers, dnsConfig.Searches, dnsConfig.Options); err != nil {
return nil, fmt.Errorf("rewrite resolv.conf failed for pod %q: %v", config.Metadata.Name, err)
}
...
// 为了能够调用 CNI 插件设置 infra 容器的网络栈
// dockershim 还专门实现了一个 network 部分,它会给 CNI 插件传入相应的参数,设置 infra 容器的网络栈
err = ds.network.SetUpPod(config.GetMetadata().Namespace, config.GetMetadata().Name, cID, config.Annotations, networkOptions)
...
}
笔者在上述代码里添加了一些自己的注释。可以看到,k8s 的 kubelet 为了兼容支持 docker 容器运行时,做了大量胶水性质的粘合操作,比如设置 DNS Server 这种甚至是直接调用操作系统接口,以重写 resolv.conf 文件形式实现的!
注1:dns 配置这块为什么是直接重写文件呢?为了解答这个问题,笔者找到了最初实现版本[27],这里面是没有做任何重写操作。继续回溯历史,可以找到这个 PR #43368[28],似乎 dockertool 时代就已经是这种方式设置 DNS 了,为了支持 k8s 的一些 DNS 设置方面的功能,社区沿用了之前 dockertool 的方案,在 dockershim 处理 Pod Sandbox 的时候也加入了重写 resolv.conf 的逻辑。那么,为什么 dockertool 会重写 resolv.conf 呢,继续回溯版本后,笔者发现了关于 dns 设置这块的一段注释[29],它的出处是 PR 10266[30]。终于破案了,由于当时 docker 还不支持 ndots 选项,k8s 选择的是 hack 掉 infra 容器的 resolv.conf 来解决这个问题。
注2:接着上面一个注解,PR #10266 的确是通过魔改的方式给 k8s 加上了 ndots 选项的支持,但是,k8s 官方的核心开发人员 thockin[31] 在同一年( 2015 年)的九月份就给 docker 提了 PR(见 PR #16031[32] )加上了该功能。其实从这个事情也可以看出来,两个社区之间信息是不同步的,继续维护 dockershim 的话这样的问题还会不少。最好的解决办法恐怕还是将这些运行时方面的功能通过 CRI 标准接口定义好,然后容器运行时各自去实现。
containerd beyond 1.0
了解了 kubelet 调用 dockershim 这块的情况以后,笔者又想到了它的表兄弟 containerd,按道理它应该是 k8s 更为亲和的方案。那么,它在这个过程中扮演什么样的角色呢,现状又如何呢?
带着这个疑问,笔者克隆了 containerd[33] 的仓库代码。通过 git log 很快便翻到了 commit 树的起点:
commit 15a96783ca2ac8c0eb2c400701e8eb335059c63b (HEAD)
Author: Michael Crosby <crosbymichael@gmail.com>
Date: Thu Nov 5 15:29:53 2015 -0800
Initial commit
可以看到,containerd 作为一个单独项目开发已经是 2015 年底了。有兴趣的朋友还可以翻阅一下这个起点 commit 的内容,其实等于就是从头开始写了…
那么,docker 什么时候开始集成 containerd 作为它的容器运行时呢?
其实也很简单,查一下 docker 仓库的 PR 历史就知道了。最终,笔者找到了 PR #20662[34]。在这个 PR 变更内容里,很容易就找到了集成的 containerd 的版本:
ENV CONTAINERD_COMMIT 7146b01a3d7aaa146414cdfb0a6c96cfba5d9091
对比 commit 提交时间,大致是 v0.1.0 版本发布的时间。
在 containerd 单独立项开发的两年以后,2017 年 12 月份,containerd 1.0 GA 了,containerd 的核心开发人员 Michel Crosby 也撰文讲述了 containerd 抵达 1.0 的这个旅程,其中包括像从 Graphdriver 切换到 Snapshot 这样的架构层面的重新设计。
而在此之前的 11 月份,k8s 1.8 加入了对 containerd 运行时的支持,见 1.8 changelog[35]。
注1:有趣的是,containerd 自己也引入了一个 containerd-shim,这个 shim 是为了让出自 containerd 的容器进程能够和 containerd 解耦,具体见 containerd v0.5 的 PR #98 title[36]。
注2:此外,值得一提的是,引入 containerd 后的 docker 自身也不是太稳定(当然,剥离 containerd 之前笔者在生产环境使用 docker daemon 也遇到过不少问题),笔者自己就经历过一个诡异问题,具体可以参考笔者 17 年时候写的这篇博客[37],现在回过头来看,可能和 containerd-shim 的这个玩法有关系。顺便说一句,那会儿的 containerd 尽管已经 1.0 了,UX 交互却还是相当简陋,这也是很多用户在 containerd 可以单独作为容器运行时选项时仍然坚持选择 docker 的重要原因之一。有兴趣的朋友可以看下笔者在 18 年初试玩 containerd 的经历[38]。
从 docker 到 containerd 的迁徙
时至今日,CRI 已然在各个主流的容器运行时得到支持和普及,containerd 的一些周边支持也逐渐完善起来,比如命令行工具这块,crictl
沿用了之前 docker
留下来的操作习惯,相关命令均可以接近无缝地切换到 crictl
。
业内也出现一些从 docker 引擎迁移到 containerd 的案例,如 eBay 早在 2019 年就将运行时从 docker 切换到了 containerd[39],各大公有云提供的 Kubernetes 服务也在 k8s 官方宣布弃用 dockershim 支持后不久便宣布使用 containerd 替换 docker[40]。
结语呼,花了点时间,终于摸清了 dockershim 的身世背景。整体看下来,似乎和 k8s 官方博客里说的差不多。笔者也感受到,在迭代过程中社区的开发人员为了弥补 k8s 和 docker 之间的 gap 做出的一些妥协:比如前面提到的实现 dockershim 让 docker 支持 CRI 标准,以及重写 resolv.conf 来支持 k8s 的一些 dns 功能等等。
出于开发和运维方面的复杂性考虑,无论是 k8s 官方弃用 dockershim 还是社区用户将运行时切换到 containerd 其实都是非常理性的做法。
只是,似乎 docker 的那个时代已经落幕了。
参考资料
[1][源码解读]从代码实现层面思考 Kubernetes 为什么会弃用对 Docker 的支持?: https://colstuwjx.github.io/dive-into-sourcecode-why-k8s-deprecated-dockershim/
[2]Colstuwjx's site: https://colstuwjx.github.io/
[3]dont panic kubernetes and docker: https://kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-docker/
[4]Kubernetes 弃用 Docker 后怎么办?: https://www.infoq.cn/article/47hcixefry1cetbzugwd
[5]dockershim-faq: https://kubernetes.io/blog/2020/12/02/dockershim-faq/
[6]PR #29553: https://github.com/kubernetes/kubernetes/pull/29553
[7]umbrella issue: https://github.com/kubernetes/kubernetes/issues/28789
[8]dockershim: https://github.com/kubernetes/kubernetes/tree/v1.22.0/pkg/kubelet/dockershim
[9]commit history: https://github.com/kubernetes/kubernetes/commits/master?after=5a732dcfe1d4ec0e8ee2871b106605b7f8a69b98+104&branch=master&path[]=pkg&path[]=kubelet&path[]=dockershim&path[]=docker_service.go
[10]cri in kubernetes: https://kubernetes.io/blog/2016/12/container-runtime-interface-cri-in-kubernetes/
[11]rkt: https://github.com/kubernetes/kubernetes/tree/v1.4.0/pkg/kubelet/rkt
[12]dockertools: https://github.com/kubernetes/kubernetes/tree/v1.4.0/pkg/kubelet/dockertools
[13]kubelet rktshim: https://github.com/kubernetes/kubernetes/tree/v1.4.0/pkg/kubelet/rktshim
[14]kube docker client: https://github.com/kubernetes/kubernetes/blob/v1.4.0/pkg/kubelet/dockertools/kube_docker_client.go
[15]L38 kube docker client: https://github.com/kubernetes/kubernetes/blob/v1.4.0/pkg/kubelet/dockertools/kube_docker_client.go#L38
[16]docker.go#L64: https://github.com/kubernetes/kubernetes/blob/v1.4.0/pkg/kubelet/dockertools/docker.go#L64
[17]k8s v1.22.0: https://github.com/kubernetes/kubernetes/tree/v1.22.0
[18]kubelet #L36: https://github.com/kubernetes/kubernetes/blob/v1.22.0/cmd/kubelet/kubelet.go#L36
[19]kubelet server.go#L155: https://github.com/kubernetes/kubernetes/blob/v1.22.0/cmd/kubelet/app/server.go#L155
[20]kubelet server.go#L796: https://github.com/kubernetes/kubernetes/blob/v1.22.0/cmd/kubelet/app/server.go#L796
[21]系列文章: https://mp.weixin.qq.com/s/g3C0alyd21fNhbj4OqPprQ
[22]kubelet #L662: https://github.com/kubernetes/kubernetes/blob/v1.22.0/pkg/kubelet/kubelet.go#L662
[23]kubelet #L1498: https://github.com/kubernetes/kubernetes/blob/v1.22.0/pkg/kubelet/kubelet.go#L1498
[24]kubelet #L1729: https://github.com/kubernetes/kubernetes/blob/v1.22.0/pkg/kubelet/kubelet.go#L1729
[25]kubelet #L695: https://github.com/kubernetes/kubernetes/blob/v1.22.0/pkg/kubelet/kubelet.go#L695
[26]kubelet remote_runtime.go: https://github.com/kubernetes/kubernetes/blob/v1.22.0/pkg/kubelet/cri/remote/remote_runtime.go
[27]最初版本: https://github.com/kubernetes/kubernetes/commit/5960d87d2142055cd29ebbce0243652c4adc5742#diff-40b456472817aeb853ac82dfc7cdf7632243c09bd40a085b74c5748580f6e104R237
[28]PR #43368: https://github.com/kubernetes/kubernetes/pull/43368
[29]dockertools/manager.go #L1235: https://github.com/kubernetes/kubernetes/blob/v0.21.4/pkg/kubelet/dockertools/manager.go#L1235
[30]PR #10266: https://github.com/kubernetes/kubernetes/pull/10266
[31]thockin: https://github.com/thockin
[32]moby PR #16031: https://github.com/moby/moby/pull/16031
[33]containerd github: https://github.com/containerd/containerd
[34]moby PR #20662: https://github.com/moby/moby/pull/20662
[35]k8s 1.8 changelog: https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.8.md#container-runtime-interface-cri
[36]containerd PR#98: https://github.com/containerd/containerd/pull/98#issue-58078723
[37]docker 排障经历: https://colstuwjx.github.io/2017/06/记一次失败的docker排障经历/
[38]初试 containerd: https://colstuwjx.github.io/2018/02/原创-小尝containerd一/
[39]ebay 从 docker 切换到 containerd: https://www.infoq.cn/article/odslclsjvo8bnxmbrbk*
[40]azure-kubernetes-service-replaces-docker-with-containerd: https://thenewstack.io/azure-kubernetes-service-replaces-docker-with-containerd/