源碼級解讀為何 kubernetes 棄用 Docker 容器運(yùn)行時
本文轉(zhuǎn)載自【源碼解讀】從代碼實(shí)現(xiàn)層面思考 Kubernetes 為什么會棄用對 Docker 的支持?[1]
作者: ?Colstuwjx[2]
2020年底,在 Kubernetes v1.20 正式發(fā)布的同時,k8s 官方還搞了一個大動作:他們宣布將會逐步棄用對 Docker 容器運(yùn)行時的支持。為了不讓用戶驚慌失措,官方還貼心地寫了一篇博客文章[3],對此事進(jìn)行了一番詳細(xì)說明。
**K8s 為什么會棄用對 Docker 的支持呢?**除了官方的這篇文章以外,很多科技媒體也做了相應(yīng)的解讀,比如 infoq 的這篇文章[4]。但是,為什么一定要棄用 docker 呢?這方面的維護(hù)成本究竟有多高?為了得到一個明確的答案,筆者決定展開一次 k8s 源碼的探索之旅,一探究竟。
前世
在官方發(fā)布的博客文章里鏈接了一份棄用 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 是當(dāng)初 k8s 引入 CRI 容器運(yùn)行時標(biāo)準(zhǔn)接口的時候?yàn)榱思嫒?Docker,k8s 官方自行維護(hù)的一套臨時解決方案,他們現(xiàn)在不想再維護(hù)了。
dockershim 的起點(diǎn)
那么,dockershim 是什么時候加進(jìn)去的呢?當(dāng)時的背景又是怎樣的?
筆者找到了當(dāng)初開發(fā)人員提的第一個 PR #29553 [6],PR title 里面有這么一句話:
yujuhong:?...?Add?a?new?docker?integration?with?kubelet?using?the?new?runtime?API?...
根據(jù) PR 里給出的信息,順藤摸瓜,筆者又找到了相關(guān)的 umbrella issue [7],主要是用來跟蹤 CRI 對接的進(jìn)展。也就是說,為了讓 Docker 支持 CRI 標(biāo)準(zhǔn),核心開發(fā) yujuhong 貢獻(xiàn)了集成 docker 操作并且支持新版 runtime API 的一個組件(也即是 dockershim ),具體可以查閱這個 issue ,這是當(dāng)時用來跟蹤 dockershim 實(shí)現(xiàn) CRI 接口專門開的一個 issue。
注:dockershim 的代碼位于 k8s 倉庫的這里[8],我們可以很方便地通過追溯 commit history [9]來找到首次提交,最終便找到了這個 PR 。
前 CRI 時代
在翻閱這塊代碼的時候,筆者內(nèi)心還有一個疑問: 在 CRI 標(biāo)準(zhǔn)提出之前,k8s 是怎么和容器運(yùn)行時交互的呢?
帶著這個問題,筆者通過搜索找到了宣布引入 CRI 標(biāo)準(zhǔn)的官方文章[10]。
文章里介紹到,自 k8s 1.5 起,CRI 功能作為一個 alpha 特性被引入到 k8s,而早在 1.3 版本開始,k8s 就集成了 rkt 容器運(yùn)行時的支持,作為替代 docker 的可選方案。
然而,這些代碼都是托管在 k8s kubelet 的核心代碼里,后續(xù)維護(hù)和增加更多容器運(yùn)行時支持都會變得越來越困難。
那么,我們不妨來看看當(dāng)時版本的 k8s 具體是怎么和 docker 及 rkt 引擎做交互的吧。定位代碼的方式也很簡單,直接選擇 1.5 之前的版本,比如 v1.4.0 的 tag 版本。然后,既然官方說代碼嵌在了 kubelet 代碼里,我們可以直接切到 kubelet 代碼的目錄,不難找到下面這兩個子目錄:
- rkt[11]
- dockertools[12]
注:這里還有一個小彩蛋,我們在該目錄下還找到了一個 rktshim [13]的目錄,說明當(dāng)初各個容器運(yùn)行時尚未普及對 CRI 的支持時,在 k8s 代碼里嵌 xxxshim 服務(wù)用作臨時支持是一個常規(guī)操作。
那么,它們到底是咋交互的呢?
我們不難在 dockertools 目錄下的 kube_docker_client.go[14] 里面找到一個 kubeDockerClient [15]的實(shí)現(xiàn):
//?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
}
上面的注釋也寫的挺詳細(xì),大致意思就是它是一個和 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 的生命周期里需要用到的一些操作函數(shù)都已經(jīng)包含在內(nèi)。
到這里,大致概括一下 k8s 在引入 CRI 階段的一個迭代過程吧:
1、在 CRI 標(biāo)準(zhǔn)落地之前,k8s 等于是為每一個容器運(yùn)行時都實(shí)現(xiàn)了一個具體的對接,rkt 和 dockertools 目錄下即對應(yīng)的代碼實(shí)現(xiàn);
2、官方于 1.5 版本開始正式引入 CRI 標(biāo)準(zhǔn),并實(shí)現(xiàn)了對應(yīng)的 shim 代碼,如 dockershim 和 rktshim,在各個容器運(yùn)行時尚未支持 CRI 標(biāo)準(zhǔn)的接口之前,充當(dāng)一個膠水服務(wù)。
今生通過追溯之前的版本歷史,筆者終于了解了 k8s 在支持容器運(yùn)行時這塊的”坎坷經(jīng)歷”。
然而,最開始的問題始終未能得到解答:為什么非得要棄用 dockershim ? 繼續(xù)維護(hù)下去的話究竟會有哪些具體的痛點(diǎn)呢?
畢竟,如果棄用 dockershim 的話,這意味著原本使用 docker 作為容器引擎的用戶需要為此計劃實(shí)施遷移到 containerd 或者其他支持 CRI 的容器運(yùn)行時,這會是一個不小的時間和人力成本。
想要解答這個問題,恐怕還得先看看 dockershim 目前的使用場景以及 CRI 的發(fā)展現(xiàn)狀。
啟動前還要運(yùn)行 dockershim 服務(wù)?
時至今日,kubelet 要去啟動一個 docker 容器的話,究竟是怎么和 dockershim 配合工作的呢?不妨再來看看 kubelet 這層的代碼實(shí)現(xiàn)。
這次筆者選的是剛發(fā)布不久的 v1.22[17] 版本的代碼。
kubelet 的啟動入口位于 cmd/kubelet/kubelet.go[18],熟悉 cobra 的朋友應(yīng)該知道,它最終是會調(diào)用具體 Command 的 Run 方法。對于 kubelet 來說,調(diào)用的即是它實(shí)現(xiàn)的 Run[19] 方法。
在經(jīng)過一系列的處理后,kubelet 會走到核心的用來啟動服務(wù)的 run 方法。直接看和 Dockershim 相關(guān)的部分!劃到靠近函數(shù)末尾的部分,可以看到在真正啟動前,kubelet 執(zhí)行了一個 kubelet.PreInitRuntimeService[20] 的操作。
這個 PreInitRuntimeService 方法做了什么事情呢?
不妨繼續(xù)深入一下,看看它的具體內(nèi)容:
//?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)
?}
????...
}
可以看到,當(dāng) containerRuntime 參數(shù)是 kubetypes.DockerContainerRuntime 時,kubelet 需要執(zhí)行額外的 runDockershim 方法去啟動一個 dockershim 服務(wù)(可以看到,上面有一行警告 dockershim 已棄用的提醒),而如果是 kubetypes.RemoteContainerRuntime 類型的話,則什么事情也不用干。
筆者還在 kubelet 目錄下找到了 kubelet_dockershim.go,該文件里即實(shí)現(xiàn)了這個 runDockershim 方法,它會去調(diào)用 dockershim 的相關(guān)服務(wù)代碼并啟動一個 dockerServer。
很顯然, kubelet 是通過這個 dockershim 服務(wù)包裝的一層 CRI 接口調(diào)用 docker 啟動 Pod 容器的。我們不妨看下 kubelet 實(shí)際是怎么去起 Pod 的,然后再來看看它是如何調(diào)用的容器運(yùn)行時。
kubeGenericRuntimeManager 的用途
回到 cmd/kubelet/app/server.go,在執(zhí)行了 PreInitRuntimeService 之后,不難發(fā)現(xiàn) kubelet 會去執(zhí)行 RunKubelet,并最終通過 kubelet.NewMainKubelet 來初始化 kubelet 服務(wù)實(shí)例。
注:關(guān)于 kubelet 完整的啟動邏輯,有位網(wǎng)易的同學(xué)寫了一個系列文章[21],有興趣的朋友可以看看。
這里面有關(guān) 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 異步調(diào)諧這套控制器邏輯的朋友,應(yīng)該能猜到。沒錯,這個 podWorker 就是監(jiān)聽 kubelet 關(guān)注的 Pod 資源的變化,并執(zhí)行相應(yīng)的調(diào)諧邏輯。這里先看一下 syncPod 這塊的實(shí)現(xiàn)。
注:有興趣的朋友可以看看 syncPod 方法的注釋部分[23],里面描述了 syncPod 的整體流程。
syncPod 方法里的其他細(xì)節(jié)部分忽略,我們直接關(guān)注最終調(diào)用容器運(yùn)行時服務(wù)同步 Pod 的操作部分[24]:
result?:=?kl.containerRuntime.SyncPod(pod,?podStatus,?pullSecrets,?kl.backOff)
可以看到,這里 kubelet 實(shí)例調(diào)用的 containerRuntime[25] 毫無疑問便是之前 kubelet 在 NewMainKubelet 初始化 kubeGenericRuntimeManager 時創(chuàng)建出來的 runtime 實(shí)例:
runtime,?err?:=?kuberuntime.NewKubeGenericRuntimeManager(
????...
)
klet.containerRuntime?=?runtime
那么,這個 runtime manager 具體又是怎么調(diào)用容器運(yùn)行時服務(wù)來 SyncPod 的呢?
調(diào)用 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)?{
?...
}
可以看到,這就是一次經(jīng)典的調(diào)諧邏輯。
按照它的說法,它會計算 Pod 當(dāng)前的狀態(tài),然后按需清理環(huán)境,并嘗試保證 Pod Sandbox 及相關(guān)容器(依次是 ephemeral container、init container 以及應(yīng)用容器)處于運(yùn)行狀態(tài)。
快速瀏覽了一下 SyncPod 具體實(shí)現(xiàn)之后,不難發(fā)現(xiàn),它將一些具體的實(shí)現(xiàn)部分放到了幾個單獨(dú)的方法里,如:createSandbox、startContainer。
這里,以 createSandbox 為例,看看 kubelet 在創(chuàng)建 Pod Sandbox 這塊,調(diào)用 dockershim 和其他支持 CRI 的容器運(yùn)行時有什么不同。略過生成 Pod 配置等步驟,直接看最核心的這一段:
podSandBoxID,?err?:=?m.runtimeService.RunPodSandbox(podSandboxConfig,?runtimeHandler)
隱約可以猜到,這個 runtimeService 應(yīng)該就是一個統(tǒng)一實(shí)現(xiàn)調(diào)用 CRI 的入口,不妨回過頭來再看看 kuberuntime.NewKubeGenericRuntimeManager 這一步是怎么初始化這個 runtimeService 的:
runtime,?err?:=?kuberuntime.NewKubeGenericRuntimeManager(
?...
?kubeDeps.RemoteRuntimeService,
?...
)
咦?這個 kubeDeps 又是何方神圣呢?順著源頭找,可以看到它是 NewMainKubelet 就傳入進(jìn)來的一個參數(shù)項(xiàng)。再順著調(diào)用鏈的源頭,筆者找到了 cmd/kubelet/app/server.go 里的 RunKubelet:
if?err?:=?RunKubelet(s,?kubeDeps,?s.RunOnce);?err?!=?nil?{
?return?err
}
再往上走便可以發(fā)現(xiàn),這個 kubeDeps 早在 kubelet NewKubeletCommand 時候就已經(jīng)做了初始化:
//?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)
}
但是仔細(xì)一看,里面并沒有初始化 RemoteRuntimeService 啊,那什么時候做的呢?
?。∏拔奶岬竭^,在執(zhí)行 RunKubelet 前,kubelet 事先執(zhí)行了 PreInitRuntimeService,它在里面是這樣初始化 kubeDeps 的相關(guān)運(yùn)行時依賴的:
if?kubeDeps.RemoteRuntimeService,?err?=?remote.NewRemoteRuntimeService(remoteRuntimeEndpoint,?kubeCfg.RuntimeRequestTimeout.Duration);?err?!=?nil?{
?return?err
}
想必這個 pkg/kubelet/cri/remote/remote_runtime.go[26] 便是統(tǒng)一實(shí)現(xiàn)了調(diào)用 CRI 的 client 接口!
至此,kubelet 調(diào)用容器運(yùn)行時的流程基本浮出了水面:
1、kubelet 在 NewKubeletCommand 命令入口便初始化了 kubeDeps 對象,用來存放一些 kubelet 需要的依賴;
2、在 Kubelet 執(zhí)行 RunKubelet 之前它會先執(zhí)行 PreInitRuntimeService 根據(jù) containerRuntime 參數(shù)初始化 runtimeService 句柄并存放到 kubeDeps 便于后面部分調(diào)用;
3、在上一步驟中,如果是 docker 的話,會額外執(zhí)行 runDockershim 啟動 dockershim 服務(wù);
4、執(zhí)行 RunKubelet 方法時,它會進(jìn)一步去執(zhí)行 NewMainKubelet 并最終啟動 kubelet 服務(wù);
5、在 NewMainKubelet 這一步 kubelet 會初始化 Pod Worker 去執(zhí)行 Pod 調(diào)諧,具體執(zhí)行方法為 syncPod、syncTerminatingPod 等;
6、此外,NewMainKubelet 這一步還在初始化 KubeGenericRuntimeManager 的時候傳入了 kubeDeps.RemoteRuntimeService,然后將 runtime manager 該實(shí)例賦給了 kubelet.containerRuntime;
7、當(dāng) kubelet 的 pod worker 進(jìn)入主要的 syncPod 調(diào)諧周期時,它會調(diào)用 runtime manager 的 SyncPod 方法去做同步;
8、runtime manager 的 SyncPod 方法會做一系列判斷,并執(zhí)行相應(yīng)的必要操作,比如 createSandbox,它會通過之前傳入的 runtimeService 的 RunPodSandbox 方法調(diào)用具體的容器運(yùn)行時服務(wù)做對應(yīng)的事情。
dockershim 的 CRI 實(shí)現(xiàn)
嗯 ,大致了解了 kubelet 調(diào)用容器運(yùn)行時做 syncPod 調(diào)諧的這個過程了。那 dockershim 又是怎樣具體實(shí)現(xiàn)這一套運(yùn)行時接口的呢?
以 RunSandbox 這個接口為例,可以看到 dockershim 的實(shí)現(xiàn)里做了大量手動操作的事情:
//?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?鏡像的存在,按需執(zhí)行?docker?pull
?if?err?:=?ensureSandboxImageExists(ds.client,?image);?err?!=?nil?{
??return?nil,?err
?}
?...
?//?dockershim?還會根據(jù)配置手動創(chuàng)建?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?手動創(chuàng)建?checkpoint
?if?err?=?ds.checkpointManager.CreateCheckpoint(createResp.ID,?constructPodSandboxCheckpoint(config));?err?!=?nil?{
??return?nil,?err
?}
?...
?//?dockershim?調(diào)用?docker?client?去啟動容器
?//?注意,這個時候?infra?容器的網(wǎng)絡(luò)棧還沒設(shè)置
?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`?里就是一些調(diào)用操作系統(tǒng)接口去重寫文件
?// docker client 難道沒有提供設(shè)置 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)
??}
?...
?//?為了能夠調(diào)用?CNI?插件設(shè)置?infra?容器的網(wǎng)絡(luò)棧
?//?dockershim?還專門實(shí)現(xiàn)了一個?network?部分,它會給?CNI?插件傳入相應(yīng)的參數(shù),設(shè)置?infra?容器的網(wǎng)絡(luò)棧
?err?=?ds.network.SetUpPod(config.GetMetadata().Namespace,?config.GetMetadata().Name,?cID,?config.Annotations,?networkOptions)
?...
}
筆者在上述代碼里添加了一些自己的注釋??梢钥吹?,k8s 的 kubelet 為了兼容支持 docker 容器運(yùn)行時,做了大量膠水性質(zhì)的粘合操作,比如設(shè)置 DNS Server 這種甚至是直接調(diào)用操作系統(tǒng)接口,以重寫 resolv.conf 文件形式實(shí)現(xiàn)的!
注1:dns 配置這塊為什么是直接重寫文件呢?為了解答這個問題,筆者找到了最初實(shí)現(xiàn)版本[27],這里面是沒有做任何重寫操作。繼續(xù)回溯歷史,可以找到這個 PR #43368[28],似乎 dockertool 時代就已經(jīng)是這種方式設(shè)置 DNS 了,為了支持 k8s 的一些 DNS 設(shè)置方面的功能,社區(qū)沿用了之前 dockertool 的方案,在 dockershim 處理 Pod Sandbox 的時候也加入了重寫 resolv.conf 的邏輯。那么,為什么 dockertool 會重寫 resolv.conf 呢,繼續(xù)回溯版本后,筆者發(fā)現(xiàn)了關(guān)于 dns 設(shè)置這塊的一段注釋[29],它的出處是 PR 10266[30]。終于破案了,由于當(dāng)時 docker 還不支持 ndots 選項(xiàng),k8s 選擇的是 hack 掉 infra 容器的 resolv.conf 來解決這個問題。
注2:接著上面一個注解,PR #10266 的確是通過魔改的方式給 k8s 加上了 ndots 選項(xiàng)的支持,但是,k8s 官方的核心開發(fā)人員 thockin[31] 在同一年( 2015 年)的九月份就給 docker 提了 PR(見 PR #16031[32] )加上了該功能。其實(shí)從這個事情也可以看出來,兩個社區(qū)之間信息是不同步的,繼續(xù)維護(hù) dockershim 的話這樣的問題還會不少。最好的解決辦法恐怕還是將這些運(yùn)行時方面的功能通過 CRI 標(biāo)準(zhǔn)接口定義好,然后容器運(yùn)行時各自去實(shí)現(xiàn)。
containerd beyond 1.0
了解了 kubelet 調(diào)用 dockershim 這塊的情況以后,筆者又想到了它的表兄弟 containerd,按道理它應(yīng)該是 k8s 更為親和的方案。那么,它在這個過程中扮演什么樣的角色呢,現(xiàn)狀又如何呢?
帶著這個疑問,筆者克隆了 containerd[33] 的倉庫代碼。通過 git log 很快便翻到了 commit 樹的起點(diǎn):
commit?15a96783ca2ac8c0eb2c400701e8eb335059c63b?(HEAD)
Author:?Michael?Crosby?<[email protected]>
Date:???Thu?Nov?5?15:29:53?2015?-0800
????Initial?commit
可以看到,containerd 作為一個單獨(dú)項(xiàng)目開發(fā)已經(jīng)是 2015 年底了。有興趣的朋友還可以翻閱一下這個起點(diǎn) commit 的內(nèi)容,其實(shí)等于就是從頭開始寫了…
那么,docker 什么時候開始集成 containerd 作為它的容器運(yùn)行時呢?
其實(shí)也很簡單,查一下 docker 倉庫的 PR 歷史就知道了。最終,筆者找到了 PR #20662[34]。在這個 PR 變更內(nèi)容里,很容易就找到了集成的 containerd 的版本:
ENV?CONTAINERD_COMMIT?7146b01a3d7aaa146414cdfb0a6c96cfba5d9091
對比 commit 提交時間,大致是 v0.1.0 版本發(fā)布的時間。
在 containerd 單獨(dú)立項(xiàng)開發(fā)的兩年以后,2017 年 12 月份,containerd 1.0 GA 了,containerd 的核心開發(fā)人員 Michel Crosby 也撰文講述了 containerd 抵達(dá) 1.0 的這個旅程,其中包括像從 Graphdriver 切換到 Snapshot 這樣的架構(gòu)層面的重新設(shè)計。
而在此之前的 11 月份,k8s 1.8 加入了對 containerd 運(yùn)行時的支持,見 1.8 changelog[35]。
注1:有趣的是,containerd 自己也引入了一個 containerd-shim,這個 shim 是為了讓出自 containerd 的容器進(jìn)程能夠和 containerd 解耦,具體見 containerd v0.5 的 PR #98 title[36]。
注2:此外,值得一提的是,引入 containerd 后的 docker 自身也不是太穩(wěn)定(當(dāng)然,剝離 containerd 之前筆者在生產(chǎn)環(huán)境使用 docker daemon 也遇到過不少問題),筆者自己就經(jīng)歷過一個詭異問題,具體可以參考筆者 17 年時候?qū)懙?span style="color:#1e6bb8;font-weight:bold;">這篇博客[37],現(xiàn)在回過頭來看,可能和 containerd-shim 的這個玩法有關(guān)系。順便說一句,那會兒的 containerd 盡管已經(jīng) 1.0 了,UX 交互卻還是相當(dāng)簡陋,這也是很多用戶在 containerd 可以單獨(dú)作為容器運(yùn)行時選項(xiàng)時仍然堅持選擇 docker 的重要原因之一。有興趣的朋友可以看下筆者在 18 年初試玩 containerd 的經(jīng)歷[38]。
從 docker 到 containerd 的遷徙
時至今日,CRI 已然在各個主流的容器運(yùn)行時得到支持和普及,containerd 的一些周邊支持也逐漸完善起來,比如命令行工具這塊,crictl 沿用了之前 docker 留下來的操作習(xí)慣,相關(guān)命令均可以接近無縫地切換到 crictl 。
業(yè)內(nèi)也出現(xiàn)一些從 docker 引擎遷移到 containerd 的案例,如 eBay 早在 2019 年就將運(yùn)行時從 docker 切換到了 containerd[39],各大公有云提供的 Kubernetes 服務(wù)也在 k8s 官方宣布棄用 dockershim 支持后不久便宣布使用 containerd 替換 docker[40]。
結(jié)語呼,花了點(diǎn)時間,終于摸清了 dockershim 的身世背景。整體看下來,似乎和 k8s 官方博客里說的差不多。筆者也感受到,在迭代過程中社區(qū)的開發(fā)人員為了彌補(bǔ) k8s 和 docker 之間的 gap 做出的一些妥協(xié):比如前面提到的實(shí)現(xiàn) dockershim 讓 docker 支持 CRI 標(biāo)準(zhǔn),以及重寫 resolv.conf 來支持 k8s 的一些 dns 功能等等。
出于開發(fā)和運(yùn)維方面的復(fù)雜性考慮,無論是 k8s 官方棄用 dockershim 還是社區(qū)用戶將運(yùn)行時切換到 containerd 其實(shí)都是非常理性的做法。
只是,似乎 docker 的那個時代已經(jīng)落幕了。
參考資料
[1][源碼解讀]從代碼實(shí)現(xiàn)層面思考 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 排障經(jīng)歷: https://colstuwjx.github.io/2017/06/記一次失敗的docker排障經(jīng)歷/
[38]初試 containerd: https://colstuwjx.github.io/2018/02/原創(chuàng)-小嘗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/
