<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          源碼級(jí)解讀為何 kubernetes 棄用 Docker 容器運(yùn)行時(shí)

          共 19662字,需瀏覽 40分鐘

           ·

          2021-08-15 05:26

          本文轉(zhuǎn)載自【源碼解讀】從代碼實(shí)現(xiàn)層面思考 Kubernetes 為什么會(huì)棄用對(duì) Docker 的支持?[1]

          作者: ?Colstuwjx[2]

          2020年底,在 Kubernetes v1.20 正式發(fā)布的同時(shí),k8s 官方還搞了一個(gè)大動(dòng)作:他們宣布將會(huì)逐步棄用對(duì) Docker 容器運(yùn)行時(shí)的支持。為了不讓用戶(hù)驚慌失措,官方還貼心地寫(xiě)了一篇博客文章[3],對(duì)此事進(jìn)行了一番詳細(xì)說(shuō)明。

          **K8s 為什么會(huì)棄用對(duì) Docker 的支持呢?**除了官方的這篇文章以外,很多科技媒體也做了相應(yīng)的解讀,比如 infoq 的這篇文章[4]。但是,為什么一定要棄用 docker 呢?這方面的維護(hù)成本究竟有多高?為了得到一個(gè)明確的答案,筆者決定展開(kāi)一次 k8s 源碼的探索之旅,一探究竟。


          前世

          在官方發(fā)布的博客文章里鏈接了一份棄用 Dockershim 的常見(jiàn)問(wèn)題解答[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)行時(shí)標(biāo)準(zhǔn)接口的時(shí)候?yàn)榱思嫒?Docker,k8s 官方自行維護(hù)的一套臨時(shí)解決方案,他們現(xiàn)在不想再維護(hù)了。

          dockershim 的起點(diǎn)

          那么,dockershim 是什么時(shí)候加進(jìn)去的呢?當(dāng)時(shí)的背景又是怎樣的?

          筆者找到了當(dāng)初開(kāi)發(fā)人員提的第一個(gè) PR #29553 [6],PR title 里面有這么一句話(huà):

          yujuhong:?...?Add?a?new?docker?integration?with?kubelet?using?the?new?runtime?API?...

          根據(jù) PR 里給出的信息,順藤摸瓜,筆者又找到了相關(guān)的 umbrella issue [7],主要是用來(lái)跟蹤 CRI 對(duì)接的進(jìn)展。也就是說(shuō),為了讓 Docker 支持 CRI 標(biāo)準(zhǔn),核心開(kāi)發(fā) yujuhong 貢獻(xiàn)了集成 docker 操作并且支持新版 runtime API 的一個(gè)組件(也即是 dockershim ),具體可以查閱這個(gè) issue ,這是當(dāng)時(shí)用來(lái)跟蹤 dockershim 實(shí)現(xiàn) CRI 接口專(zhuān)門(mén)開(kāi)的一個(gè) issue。

          注:dockershim 的代碼位于 k8s 倉(cāng)庫(kù)的這里[8],我們可以很方便地通過(guò)追溯 commit history [9]來(lái)找到首次提交,最終便找到了這個(gè) PR 。

          前 CRI 時(shí)代

          在翻閱這塊代碼的時(shí)候,筆者內(nèi)心還有一個(gè)疑問(wèn): 在 CRI 標(biāo)準(zhǔn)提出之前,k8s 是怎么和容器運(yùn)行時(shí)交互的呢?

          帶著這個(gè)問(wèn)題,筆者通過(guò)搜索找到了宣布引入 CRI 標(biāo)準(zhǔn)的官方文章[10]。

          文章里介紹到,自 k8s 1.5 起,CRI 功能作為一個(gè) alpha 特性被引入到 k8s,而早在 1.3 版本開(kāi)始,k8s 就集成了 rkt 容器運(yùn)行時(shí)的支持,作為替代 docker 的可選方案。

          然而,這些代碼都是托管在 k8s kubelet 的核心代碼里,后續(xù)維護(hù)和增加更多容器運(yùn)行時(shí)支持都會(huì)變得越來(lái)越困難。

          那么,我們不妨來(lái)看看當(dāng)時(shí)版本的 k8s 具體是怎么和 docker 及 rkt 引擎做交互的吧。定位代碼的方式也很簡(jiǎn)單,直接選擇 1.5 之前的版本,比如 v1.4.0 的 tag 版本。然后,既然官方說(shuō)代碼嵌在了 kubelet 代碼里,我們可以直接切到 kubelet 代碼的目錄,不難找到下面這兩個(gè)子目錄:

          • rkt[11]
          • dockertools[12]

          注:這里還有一個(gè)小彩蛋,我們?cè)谠撃夸浵逻€找到了一個(gè) rktshim [13]的目錄,說(shuō)明當(dāng)初各個(gè)容器運(yùn)行時(shí)尚未普及對(duì) CRI 的支持時(shí),在 k8s 代碼里嵌 xxxshim 服務(wù)用作臨時(shí)支持是一個(gè)常規(guī)操作。

          那么,它們到底是咋交互的呢?

          我們不難在 dockertools 目錄下的 kube_docker_client.go[14] 里面找到一個(gè) 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
          }

          上面的注釋也寫(xiě)的挺詳細(xì),大致意思就是它是一個(gè)和 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 階段的一個(gè)迭代過(guò)程吧:

          1、在 CRI 標(biāo)準(zhǔn)落地之前,k8s 等于是為每一個(gè)容器運(yùn)行時(shí)都實(shí)現(xiàn)了一個(gè)具體的對(duì)接,rkt 和 dockertools 目錄下即對(duì)應(yīng)的代碼實(shí)現(xiàn);

          2、官方于 1.5 版本開(kāi)始正式引入 CRI 標(biāo)準(zhǔn),并實(shí)現(xiàn)了對(duì)應(yīng)的 shim 代碼,如 dockershim 和 rktshim,在各個(gè)容器運(yùn)行時(shí)尚未支持 CRI 標(biāo)準(zhǔn)的接口之前,充當(dāng)一個(gè)膠水服務(wù)。

          今生

          通過(guò)追溯之前的版本歷史,筆者終于了解了 k8s 在支持容器運(yùn)行時(shí)這塊的”坎坷經(jīng)歷”。

          然而,最開(kāi)始的問(wèn)題始終未能得到解答:為什么非得要棄用 dockershim ? 繼續(xù)維護(hù)下去的話(huà)究竟會(huì)有哪些具體的痛點(diǎn)呢?

          畢竟,如果棄用 dockershim 的話(huà),這意味著原本使用 docker 作為容器引擎的用戶(hù)需要為此計(jì)劃實(shí)施遷移到 containerd 或者其他支持 CRI 的容器運(yùn)行時(shí),這會(huì)是一個(gè)不小的時(shí)間和人力成本。

          想要解答這個(gè)問(wèn)題,恐怕還得先看看 dockershim 目前的使用場(chǎng)景以及 CRI 的發(fā)展現(xiàn)狀。

          啟動(dòng)前還要運(yùn)行 dockershim 服務(wù)?

          時(shí)至今日,kubelet 要去啟動(dòng)一個(gè) docker 容器的話(huà),究竟是怎么和 dockershim 配合工作的呢?不妨再來(lái)看看 kubelet 這層的代碼實(shí)現(xiàn)。

          這次筆者選的是剛發(fā)布不久的 v1.22[17] 版本的代碼。

          kubelet 的啟動(dòng)入口位于 cmd/kubelet/kubelet.go[18],熟悉 cobra 的朋友應(yīng)該知道,它最終是會(huì)調(diào)用具體 Command 的 Run 方法。對(duì)于 kubelet 來(lái)說(shuō),調(diào)用的即是它實(shí)現(xiàn)的 Run[19] 方法。

          在經(jīng)過(guò)一系列的處理后,kubelet 會(huì)走到核心的用來(lái)啟動(dòng)服務(wù)的 run 方法。直接看和 Dockershim 相關(guān)的部分!劃到靠近函數(shù)末尾的部分,可以看到在真正啟動(dòng)前,kubelet 執(zhí)行了一個(gè) kubelet.PreInitRuntimeService[20] 的操作。

          這個(gè) 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 時(shí),kubelet 需要執(zhí)行額外的 runDockershim 方法去啟動(dòng)一個(gè) dockershim 服務(wù)(可以看到,上面有一行警告 dockershim 已棄用的提醒),而如果是 kubetypes.RemoteContainerRuntime 類(lèi)型的話(huà),則什么事情也不用干。

          筆者還在 kubelet 目錄下找到了 kubelet_dockershim.go,該文件里即實(shí)現(xiàn)了這個(gè) runDockershim 方法,它會(huì)去調(diào)用 dockershim 的相關(guān)服務(wù)代碼并啟動(dòng)一個(gè) dockerServer

          很顯然, kubelet 是通過(guò)這個(gè) dockershim 服務(wù)包裝的一層 CRI 接口調(diào)用 docker 啟動(dòng) Pod 容器的。我們不妨看下 kubelet 實(shí)際是怎么去起 Pod 的,然后再來(lái)看看它是如何調(diào)用的容器運(yùn)行時(shí)。

          kubeGenericRuntimeManager 的用途

          回到 cmd/kubelet/app/server.go,在執(zhí)行了 PreInitRuntimeService 之后,不難發(fā)現(xiàn) kubelet 會(huì)去執(zhí)行 RunKubelet,并最終通過(guò) kubelet.NewMainKubelet 來(lái)初始化 kubelet 服務(wù)實(shí)例。

          注:關(guān)于 kubelet 完整的啟動(dòng)邏輯,有位網(wǎng)易的同學(xué)寫(xiě)了一個(gè)系列文章[21],有興趣的朋友可以看看。

          這里面有關(guān) runtime 部分最重要的就是這一段[22]了:

          runtime,?err?:=?kuberuntime.NewKubeGenericRuntimeManager(
          ????...
          )

          這里初始化了一個(gè) kubeGenericRuntimeManager 的對(duì)象,它可以做哪些事情呢?我們暫且按下不表,先從 kubelet 這一層找找入口。回過(guò)頭來(lái),我們?cè)賮?lái)看看 kubelet 啟動(dòng)入口 NewMainKubelet 這塊。可以看到,在初始化 kubeGenericRuntimeManager 之前,kubelet 初始化了一個(gè) 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)該能猜到。沒(méi)錯(cuò),這個(gè) podWorker 就是監(jiān)聽(tīng) 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)行時(shí)服務(wù)同步 Pod 的操作部分[24]:

          result?:=?kl.containerRuntime.SyncPod(pod,?podStatus,?pullSecrets,?kl.backOff)

          可以看到,這里 kubelet 實(shí)例調(diào)用的 containerRuntime[25] 毫無(wú)疑問(wèn)便是之前 kubelet 在 NewMainKubelet 初始化 kubeGenericRuntimeManager 時(shí)創(chuàng)建出來(lái)的 runtime 實(shí)例:

          runtime,?err?:=?kuberuntime.NewKubeGenericRuntimeManager(
          ????...
          )
          klet.containerRuntime?=?runtime

          那么,這個(gè) runtime manager 具體又是怎么調(diào)用容器運(yùn)行時(shí)服務(wù)來(lái) SyncPod 的呢?

          調(diào)用 runtime service 來(lái) SyncPod

          我們不妨先來(lái)看看 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)諧邏輯。

          按照它的說(shuō)法,它會(huì)計(jì)算 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)部分放到了幾個(gè)單獨(dú)的方法里,如:createSandbox、startContainer。

          這里,以 createSandbox 為例,看看 kubelet 在創(chuàng)建 Pod Sandbox 這塊,調(diào)用 dockershim 和其他支持 CRI 的容器運(yùn)行時(shí)有什么不同。略過(guò)生成 Pod 配置等步驟,直接看最核心的這一段:

          podSandBoxID,?err?:=?m.runtimeService.RunPodSandbox(podSandboxConfig,?runtimeHandler)

          隱約可以猜到,這個(gè) runtimeService 應(yīng)該就是一個(gè)統(tǒng)一實(shí)現(xiàn)調(diào)用 CRI 的入口,不妨回過(guò)頭來(lái)再看看 kuberuntime.NewKubeGenericRuntimeManager 這一步是怎么初始化這個(gè) runtimeService 的:

          runtime,?err?:=?kuberuntime.NewKubeGenericRuntimeManager(
          ?...
          ?kubeDeps.RemoteRuntimeService,
          ?...
          )

          咦?這個(gè) kubeDeps 又是何方神圣呢?順著源頭找,可以看到它是 NewMainKubelet 就傳入進(jìn)來(lái)的一個(gè)參數(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),這個(gè) kubeDeps 早在 kubelet NewKubeletCommand 時(shí)候就已經(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ì)一看,里面并沒(méi)有初始化 RemoteRuntimeService 啊,那什么時(shí)候做的呢?

          啊!前文提到過(guò),在執(zhí)行 RunKubelet 前,kubelet 事先執(zhí)行了 PreInitRuntimeService,它在里面是這樣初始化 kubeDeps 的相關(guān)運(yùn)行時(shí)依賴(lài)的:

          if?kubeDeps.RemoteRuntimeService,?err?=?remote.NewRemoteRuntimeService(remoteRuntimeEndpoint,?kubeCfg.RuntimeRequestTimeout.Duration);?err?!=?nil?{
          ?return?err
          }

          想必這個(gè) pkg/kubelet/cri/remote/remote_runtime.go[26] 便是統(tǒng)一實(shí)現(xiàn)了調(diào)用 CRI 的 client 接口!

          至此,kubelet 調(diào)用容器運(yùn)行時(shí)的流程基本浮出了水面:

          1、kubelet 在 NewKubeletCommand 命令入口便初始化了 kubeDeps 對(duì)象,用來(lái)存放一些 kubelet 需要的依賴(lài);

          2、在 Kubelet 執(zhí)行 RunKubelet 之前它會(huì)先執(zhí)行 PreInitRuntimeService 根據(jù) containerRuntime 參數(shù)初始化 runtimeService 句柄并存放到 kubeDeps 便于后面部分調(diào)用;

          3、在上一步驟中,如果是 docker 的話(huà),會(huì)額外執(zhí)行 runDockershim 啟動(dòng) dockershim 服務(wù);

          4、執(zhí)行 RunKubelet 方法時(shí),它會(huì)進(jìn)一步去執(zhí)行 NewMainKubelet 并最終啟動(dòng) kubelet 服務(wù);

          5、在 NewMainKubelet 這一步 kubelet 會(huì)初始化 Pod Worker 去執(zhí)行 Pod 調(diào)諧,具體執(zhí)行方法為 syncPodsyncTerminatingPod 等;

          6、此外,NewMainKubelet 這一步還在初始化 KubeGenericRuntimeManager 的時(shí)候傳入了 kubeDeps.RemoteRuntimeService,然后將 runtime manager 該實(shí)例賦給了 kubelet.containerRuntime

          7、當(dāng) kubelet 的 pod worker 進(jìn)入主要的 syncPod 調(diào)諧周期時(shí),它會(huì)調(diào)用 runtime manager 的 SyncPod 方法去做同步;

          8、runtime manager 的 SyncPod 方法會(huì)做一系列判斷,并執(zhí)行相應(yīng)的必要操作,比如 createSandbox,它會(huì)通過(guò)之前傳入的 runtimeService 的 RunPodSandbox 方法調(diào)用具體的容器運(yùn)行時(shí)服務(wù)做對(duì)應(yīng)的事情。

          dockershim 的 CRI 實(shí)現(xiàn)

          嗯 ,大致了解了 kubelet 調(diào)用容器運(yùn)行時(shí)做 syncPod 調(diào)諧的這個(gè)過(guò)程了。那 dockershim 又是怎樣具體實(shí)現(xiàn)這一套運(yùn)行時(shí)接口的呢?

          RunSandbox 這個(gè)接口為例,可以看到 dockershim 的實(shí)現(xiàn)里做了大量手動(dòng)操作的事情:

          //?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?會(huì)先保證?sandbox?鏡像的存在,按需執(zhí)行?docker?pull
          ?if?err?:=?ensureSandboxImageExists(ds.client,?image);?err?!=?nil?{
          ??return?nil,?err
          ?}
          ?...
          ?//?dockershim?還會(huì)根據(jù)配置手動(dòng)創(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?手動(dòng)創(chuàng)建?checkpoint
          ?if?err?=?ds.checkpointManager.CreateCheckpoint(createResp.ID,?constructPodSandboxCheckpoint(config));?err?!=?nil?{
          ??return?nil,?err
          ?}
          ?...
          ?//?dockershim?調(diào)用?docker?client?去啟動(dòng)容器
          ?//?注意,這個(gè)時(shí)候?infra?容器的網(wǎng)絡(luò)棧還沒(méi)設(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?還會(huì)去手動(dòng)重寫(xiě)該容器的?dns?配置
          ?//?這塊是真的沒(méi)想到,`rewriteResolvFile`?里就是一些調(diào)用操作系統(tǒng)接口去重寫(xiě)文件
          ?// docker client 難道沒(méi)有提供設(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?還專(zhuān)門(mén)實(shí)現(xiàn)了一個(gè)?network?部分,它會(huì)給?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)行時(shí),做了大量膠水性質(zhì)的粘合操作,比如設(shè)置 DNS Server 這種甚至是直接調(diào)用操作系統(tǒng)接口,以重寫(xiě) resolv.conf 文件形式實(shí)現(xiàn)的!

          注1:dns 配置這塊為什么是直接重寫(xiě)文件呢?為了解答這個(gè)問(wèn)題,筆者找到了最初實(shí)現(xiàn)版本[27],這里面是沒(méi)有做任何重寫(xiě)操作。繼續(xù)回溯歷史,可以找到這個(gè) PR #43368[28],似乎 dockertool 時(shí)代就已經(jīng)是這種方式設(shè)置 DNS 了,為了支持 k8s 的一些 DNS 設(shè)置方面的功能,社區(qū)沿用了之前 dockertool 的方案,在 dockershim 處理 Pod Sandbox 的時(shí)候也加入了重寫(xiě) resolv.conf 的邏輯。那么,為什么 dockertool 會(huì)重寫(xiě) resolv.conf 呢,繼續(xù)回溯版本后,筆者發(fā)現(xiàn)了關(guān)于 dns 設(shè)置這塊的一段注釋[29],它的出處是 PR 10266[30]。終于破案了,由于當(dāng)時(shí) docker 還不支持 ndots 選項(xiàng),k8s 選擇的是 hack 掉 infra 容器的 resolv.conf 來(lái)解決這個(gè)問(wèn)題。

          注2:接著上面一個(gè)注解,PR #10266 的確是通過(guò)魔改的方式給 k8s 加上了 ndots 選項(xiàng)的支持,但是,k8s 官方的核心開(kāi)發(fā)人員 thockin[31] 在同一年( 2015 年)的九月份就給 docker 提了 PR(見(jiàn) PR #16031[32] )加上了該功能。其實(shí)從這個(gè)事情也可以看出來(lái),兩個(gè)社區(qū)之間信息是不同步的,繼續(xù)維護(hù) dockershim 的話(huà)這樣的問(wèn)題還會(huì)不少。最好的解決辦法恐怕還是將這些運(yùn)行時(shí)方面的功能通過(guò) CRI 標(biāo)準(zhǔn)接口定義好,然后容器運(yùn)行時(shí)各自去實(shí)現(xiàn)。

          containerd beyond 1.0

          了解了 kubelet 調(diào)用 dockershim 這塊的情況以后,筆者又想到了它的表兄弟 containerd,按道理它應(yīng)該是 k8s 更為親和的方案。那么,它在這個(gè)過(guò)程中扮演什么樣的角色呢,現(xiàn)狀又如何呢?

          帶著這個(gè)疑問(wèn),筆者克隆了 containerd[33] 的倉(cāng)庫(kù)代碼。通過(guò) git log 很快便翻到了 commit 樹(shù)的起點(diǎn):

          commit?15a96783ca2ac8c0eb2c400701e8eb335059c63b?(HEAD)
          Author:?Michael?Crosby?<[email protected]>
          Date:???Thu?Nov?5?15:29:53?2015?-0800

          ????Initial?commit

          可以看到,containerd 作為一個(gè)單獨(dú)項(xiàng)目開(kāi)發(fā)已經(jīng)是 2015 年底了。有興趣的朋友還可以翻閱一下這個(gè)起點(diǎn) commit 的內(nèi)容,其實(shí)等于就是從頭開(kāi)始寫(xiě)了…

          那么,docker 什么時(shí)候開(kāi)始集成 containerd 作為它的容器運(yùn)行時(shí)呢?

          其實(shí)也很簡(jiǎn)單,查一下 docker 倉(cāng)庫(kù)的 PR 歷史就知道了。最終,筆者找到了 PR #20662[34]。在這個(gè) PR 變更內(nèi)容里,很容易就找到了集成的 containerd 的版本:

          ENV?CONTAINERD_COMMIT?7146b01a3d7aaa146414cdfb0a6c96cfba5d9091

          對(duì)比 commit 提交時(shí)間,大致是 v0.1.0 版本發(fā)布的時(shí)間。

          在 containerd 單獨(dú)立項(xiàng)開(kāi)發(fā)的兩年以后,2017 年 12 月份,containerd 1.0 GA 了,containerd 的核心開(kāi)發(fā)人員 Michel Crosby 也撰文講述了 containerd 抵達(dá) 1.0 的這個(gè)旅程,其中包括像從 Graphdriver 切換到 Snapshot 這樣的架構(gòu)層面的重新設(shè)計(jì)。

          而在此之前的 11 月份,k8s 1.8 加入了對(duì) containerd 運(yùn)行時(shí)的支持,見(jiàn) 1.8 changelog[35]。

          注1:有趣的是,containerd 自己也引入了一個(gè) containerd-shim,這個(gè) shim 是為了讓出自 containerd 的容器進(jìn)程能夠和 containerd 解耦,具體見(jiàn) containerd v0.5 的 PR #98 title[36]。

          注2:此外,值得一提的是,引入 containerd 后的 docker 自身也不是太穩(wěn)定(當(dāng)然,剝離 containerd 之前筆者在生產(chǎn)環(huán)境使用 docker daemon 也遇到過(guò)不少問(wèn)題),筆者自己就經(jīng)歷過(guò)一個(gè)詭異問(wèn)題,具體可以參考筆者 17 年時(shí)候?qū)懙?span style="color:#1e6bb8;font-weight:bold;">這篇博客[37],現(xiàn)在回過(guò)頭來(lái)看,可能和 containerd-shim 的這個(gè)玩法有關(guān)系。順便說(shuō)一句,那會(huì)兒的 containerd 盡管已經(jīng) 1.0 了,UX 交互卻還是相當(dāng)簡(jiǎn)陋,這也是很多用戶(hù)在 containerd 可以單獨(dú)作為容器運(yùn)行時(shí)選項(xiàng)時(shí)仍然堅(jiān)持選擇 docker 的重要原因之一。有興趣的朋友可以看下筆者在 18 年初試玩 containerd 的經(jīng)歷[38]。

          從 docker 到 containerd 的遷徙

          時(shí)至今日,CRI 已然在各個(gè)主流的容器運(yùn)行時(shí)得到支持和普及,containerd 的一些周邊支持也逐漸完善起來(lái),比如命令行工具這塊,crictl 沿用了之前 docker 留下來(lái)的操作習(xí)慣,相關(guān)命令均可以接近無(wú)縫地切換到 crictl

          業(yè)內(nèi)也出現(xiàn)一些從 docker 引擎遷移到 containerd 的案例,如 eBay 早在 2019 年就將運(yùn)行時(shí)從 docker 切換到了 containerd[39],各大公有云提供的 Kubernetes 服務(wù)也在 k8s 官方宣布棄用 dockershim 支持后不久便宣布使用 containerd 替換 docker[40]。

          結(jié)語(yǔ)

          呼,花了點(diǎn)時(shí)間,終于摸清了 dockershim 的身世背景。整體看下來(lái),似乎和 k8s 官方博客里說(shuō)的差不多。筆者也感受到,在迭代過(guò)程中社區(qū)的開(kāi)發(fā)人員為了彌補(bǔ) k8s 和 docker 之間的 gap 做出的一些妥協(xié):比如前面提到的實(shí)現(xiàn) dockershim 讓 docker 支持 CRI 標(biāo)準(zhǔn),以及重寫(xiě) resolv.conf 來(lái)支持 k8s 的一些 dns 功能等等。

          出于開(kāi)發(fā)和運(yùn)維方面的復(fù)雜性考慮,無(wú)論是 k8s 官方棄用 dockershim 還是社區(qū)用戶(hù)將運(yùn)行時(shí)切換到 containerd 其實(shí)都是非常理性的做法。

          只是,似乎 docker 的那個(gè)時(shí)代已經(jīng)落幕了。

          參考資料

          [1]

          [源碼解讀]從代碼實(shí)現(xiàn)層面思考 Kubernetes 為什么會(huì)棄用對(duì) 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/


          瀏覽 88
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本久久一级片 | 欧美综合播放网站在线观看 | 999一区二区三区 | 91探花国产综合在线精品 | 亚洲视频在线观看者 |