】40-Docker遷移Containerd)
案例:從 Docker 遷移到 Containerd一句話定位:K8s 1.24 移除 Dockershim,500 節(jié)點從 Docker 遷移到 Containerd,這不是一次升級,而是一次涉及鏡像、構建、日志、監(jiān)控、CRI 兼容性的系統(tǒng)性工程——復盤遷移全過程與那些文檔里不會寫的坑。寫在前面2022 年 K8s 1.24 正式移除 Dockershim,這件事在社區(qū)吵了很久,但真正落到生產,很多團隊是 2023-2024 年才動手。原因是 Dockershim 移除不是換個運行時那么簡單,它牽扯到鏡像構建、日志采集、監(jiān)控指標、私有倉庫認證、特殊掛載等一整套鏈路。我們公司 2024 年 Q1 啟動遷移,500 節(jié)點、3 個集群、200 業(yè)務,前后花了 6 周完成全量遷移,中間踩了 7 個有意義的坑。這篇文章復盤整個遷移過程。遷移這類基礎設施替換工程,最大的難點不是技術,而是兼容性盲區(qū)——你以為都兼容,實際有 N 個邊角場景不兼容,而且往往在灰度到生產才暴露。我把我們遇到的所有盲區(qū)都列出來,希望大家遷移時能提前規(guī)避。遷移的核心方法論是:先評估兼容性,再灰度遷移,每一步都有回滾預案,絕不一把梭。案例概覽維度內容集群規(guī)模3 個生產集群,共 500 節(jié)點K8s 版本1.20(遷移前) → 1.24(遷移后)容器運行時Docker 20.10 → Containerd 1.7業(yè)務規(guī)模200 業(yè)務,4000 Pod時間跨度2024/02/15 啟動 → 2024/03/30 完成遷移方式節(jié)點排空 → 替換運行時 → 驗證 → 恢復調度關鍵挑戰(zhàn)私有倉庫認證、日志路徑、監(jiān)控指標、鏡像構建、特殊配置最終效果零業(yè)務中斷,資源占用降 15%,Pod 啟動快 20%遷移時間線:02-1802-2503-0303-1003-1703-2403-31影響評估兼容性測試灰度遷移10節(jié)點灰度遷移50節(jié)點全量遷移集群1全量遷移集群2全量遷移集群3遺留問題修復復盤評估灰度全量收尾Docker → Containerd 遷移時間線一、背景與挑戰(zhàn)1.1 遷移動機K8s 1.24 強制:K8s 1.24 移除 Dockershim,繼續(xù)用 Docker 需要裝 cri-dockerd 適配層,增加復雜度。資源節(jié)省:Docker 多了 dockerd containerd 兩層,containerd 單層,內存占用少 30%。性能更好:Pod 啟動更快,鏡像 pull 更快(containerd 并發(fā)拉取)。社區(qū)方向:K8s 社區(qū)主推 containerd,新特性優(yōu)先支持。1.2 影響評估遷移前先做影響評估,梳理所有受影響的鏈路:鏈路Docker 方式Containerd 方式影響程度鏡像構建docker buildbuildkit / kaniko高鏡像拉取docker pullcrictl pull / ctr中私有倉庫認證~/.docker/config.json/etc/containerd/certs.d高日志采集/var/lib/docker/containers/var/log/pods高監(jiān)控指標docker metricscontainerd metrics中運維命令docker ps/imagescrictl ps/images中特殊配置docker daemon.jsoncontainerd config.toml高CI/CDdocker pushdocker push(buildkit 兼容)低1.3 主要挑戰(zhàn)私有鏡像倉庫認證:Harbor 私有倉庫,Docker 用~/.docker/config.json,containerd 配置完全不同。日志采集路徑變化:Filebeat 原本采集/var/lib/docker/containers/*/*.log,遷移后路徑變了。監(jiān)控指標變化:cAdvisor 的 docker 指標消失,需切換到 containerd 指標。鏡像構建鏈路:開發(fā)本地用 docker build,遷移后需切 buildkit。特殊掛載:有業(yè)務用了docker.sock,containerd 沒有containerd.sock的等價權限場景。二、方案設計2.1 遷移策略:節(jié)點級灰度不做集群級切換,而是節(jié)點級灰度:每個節(jié)點單獨排空、替換運行時、驗證、恢復調度。這樣業(yè)務零中斷,出問題只影響單節(jié)點。遷移單節(jié)點流程: ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 1.排空 │──?│ 2.卸載 │──?│ 3.安裝 │──?│ 4.驗證 │──?│ 5.恢復 │ │ cordon │ │ Docker │ │Containerd│ │ Pod │ │ 調度 │ │ drain │ │ │ │ │ │ │ │ uncordon │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ ▼ ┌─────────────────┐ │ 出問題 → 回滾 │ │ 重裝 Docker │ └─────────────────┘2.2 灰度節(jié)奏第1批:10 節(jié)點(非核心業(yè)務節(jié)點),觀察 5 天第2批:50 節(jié)點(含部分核心業(yè)務),觀察 5 天第3批:全量剩余節(jié)點,分集群推進2.3 回滾預案每個節(jié)點遷移前打快照(虛擬機場景),出問題 5 分鐘回滾:# 回滾腳本核心邏輯kubectl uncordonnodesystemctl stop containerd yum remove-ycontainerd.io yuminstall-ydocker-ce systemctlenable--nowdocker# 重啟 kubelet(切回 docker)sed-is/containerd/docker//var/lib/kubelet/kubeadm-flags.env systemctl restart kubelet三、實施過程3.1 第一階段:兼容性測試(2/15 - 2/27)3.1.1 鏡像兼容性驗證containerd 和 Docker 都支持 OCI 鏡像格式,理論兼容。但實際有邊角問題:# 測試所有核心鏡像在 containerd 下能正常跑# 重點驗證:# 1. 多階段構建的鏡像# 2. 帶 --platform 的多架構鏡像# 3. 老鏡像(Docker 1.x 時代構建的,v2 schema 1)# 4. 帶特殊 LABEL/ANNOTATION 的鏡像# 用 containerd 拉取并運行測試forimgin$(catcore_images.txt);doecho測試鏡像:$imgctr-nk8s.io images pull$img||echoFAIL:$imgctr-nk8s.io run--rm$imgtest-$(date%s)echoOK||echoRUN FAIL:$imgdone踩坑1:發(fā)現(xiàn) 2 個老鏡像是 v2 schema 1,containerd 拒絕拉取。需要用docker pull后docker push轉成 v2 schema 2。# 轉換 schema 1 鏡像dockerpull old-registry.example.com/legacy-app:v1dockertag old-registry.example.com/legacy-app:v1 new-registry.example.com/legacy-app:v2dockerpush new-registry.example.com/legacy-app:v23.1.2 私有鏡像倉庫認證Docker 用~/.docker/config.json,containerd 用config.toml配置 certs.d:# /etc/containerd/config.toml version 2 [plugins.io.containerd.grpc.v1.cri.registry] config_path /etc/containerd/certs.d # 每個倉庫一個目錄 # /etc/containerd/certs.d/harbor.example.com/hosts.toml # /etc/containerd/certs.d/docker.io/hosts.toml# /etc/containerd/certs.d/harbor.example.com/hosts.toml server https://harbor.example.com [host.https://harbor.example.com] capabilities [pull, resolve] # 認證:base64(user:password) # 但更推薦用 ImagePullSecrets,不在節(jié)點配明文 # 這里只配 CA(如果是自簽證書) ca /etc/containerd/certs.d/harbor.example.com/ca.crt踩坑2:Harbor 用的是自簽證書,containerd 默認不信任。必須在hosts.toml配ca,或者skip_verify true(不推薦生產用)。# 把 Harbor CA 復制到所有節(jié)點scpharbor-ca.crt node-xxx:/etc/containerd/certs.d/harbor.example.com/ca.crt3.1.3 日志采集路徑變化Docker 日志在/var/lib/docker/containers/id/id-json.log,containerd 日志在/var/log/pods/ns_pod_uid/container/0.log。Filebeat 配置必須改:# Filebeat 新配置(containerd)filebeat.autodiscover:providers:-type:kubernetesnode:${NODE_NAME}hints.enabled:truetemplates:-condition:contains:kubernetes.labels.app:config:-type:containerpaths:-/var/log/containers/*${data.kubernetes.container.id}.log# containerd 軟鏈到 /var/log/pods/...processors:-add_kubernetes_metadata:host:${NODE_NAME}-add_fields:target:fields:runtime:containerd踩坑3:containerd 日志格式是 CRI 格式,不是 Docker JSON 格式,解析規(guī)則要改。Docker 格式: {log:...,stream:stdout,time:...} CRI 格式: 2024-02-20T10:00:00.123456789Z stdout F log contentFilebeat 的json解析器要換成cri解析器。3.2 第二階段:遷移腳本與配置準備(2/28 - 3/5)3.2.1 containerd 配置模板# /etc/containerd/config.toml - 生產配置模板 version 2 root /var/lib/containerd state /run/containerd oom_score -999 [grpc] address /run/containerd/containerd.sock uid 0 gid 0 max_recv_message_size 16777216 max_send_message_size 16777216 [debug] address uid 0 gid 0 level [metrics] address 127.0.0.1:1338 grpc_histogram false [plugins.io.containerd.grpc.v1.cri] # sandbox_image 是 pause 鏡像 sandbox_image registry.k8s.io/pause:3.9 max_container_log_line_size 16384 [plugins.io.containerd.grpc.v1.cri.containerd] default_runtime_name runc snapshotter overlayfs [plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc] runtime_type io.containerd.runc.v2 [plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options] SystemdCgroup true # ← 必須和 kubelet 一致 [plugins.io.containerd.grpc.v1.cri.registry] config_path /etc/containerd/certs.d # 鏡像垃圾回收 [plugins.io.containerd.gc.v1.scheduler] deletion_threshold 200 stale_threshold 4h3.2.2 kubelet 配置切換kubelet 的容器運行時參數(shù)要從 docker 切到 containerd:# /var/lib/kubelet/kubeadm-flags.env# 原來(Docker):# KUBELET_KUBEADM_ARGS--container-runtimeremote --container-runtime-endpointunix:///var/run/dockershim.sock# 改為(Containerd):KUBELET_KUBEADM_ARGS--container-runtimeremote --container-runtime-endpointunix:///run/containerd/containerd.sock3.2.3 遷移腳本#!/bin/bash# migrate_to_containerd.sh - 單節(jié)點遷移腳本set-euopipefailNODE$1LOG(){echo[$(date%F %T)] [$NODE]$*;}# 1. 排空節(jié)點LOGcordon 節(jié)點kubectl cordon$NODELOGdrain 節(jié)點kubectl drain$NODE--ignore-daemonsets --delete-emptydir-data--timeout5m# 2. 停止 DockerLOG停止 Dockerssh$NODEsystemctl stop kubelet docker# 3. 卸載 Docker(保留配置備份)LOG卸載 Dockerssh$NODEtar czf /root/docker-backup-$(date %s).tgz /etc/docker /var/lib/docker 2/dev/null || truessh$NODEyum remove -y docker-ce docker-ce-cli containerd.io || true# 4. 安裝 containerdLOG安裝 containerdssh$NODEyum install -y containerd.io-1.7.13# 5. 下發(fā)配置LOG下發(fā) containerd 配置scp/etc/containerd/config.toml$NODE:/etc/containerd/config.tomlscp-r/etc/containerd/certs.d$NODE:/etc/containerd/ssh$NODEsystemctl enable containerdssh$NODEsystemctl start containerd# 6. 修改 kubelet 配置LOG切換 kubelet 運行時ssh$NODEsed -i s|dockershim.sock|containerd/containerd.sock| /var/lib/kubelet/kubeadm-flags.env# 7. 啟動 kubeletLOG啟動 kubeletssh$NODEsystemctl start kubeletsleep10# 8. 驗證節(jié)點 ReadyLOG驗證節(jié)點狀態(tài)foriin{1..6};doSTATUS$(kubectl getnode$NODE-ojsonpath{.status.conditions[?(.typeReady)].status})RUNTIME$(kubectl getnode$NODE-ojsonpath{.status.nodeInfo.containerRuntimeVersion})if[$STATUSTrue]echo$RUNTIME|grep-qcontainerd;thenLOG節(jié)點正常,運行時:$RUNTIMEkubectl uncordon$NODELOG遷移完成exit0fisleep10done# 9. 失敗 → 回滾LOG遷移失敗,開始回滾ssh$NODEsystemctl stop kubelet containerdssh$NODEyum remove -y containerd.iossh$NODEyum install -y docker-ce docker-ce-clissh$NODEtar xzf /root/docker-backup-*.tgz -C /ssh$NODEsed -i s|containerd/containerd.sock|dockershim.sock| /var/lib/kubelet/kubeadm-flags.envssh$NODEsystemctl enable --now dockerssh$NODEsystemctl start kubeletsleep10kubectl uncordon$NODELOG回滾完成,人工介入排查exit13.3 第三階段:灰度遷移(3/6 - 3/15)3.3.1 第一批 10 節(jié)點選了 10 個非核心業(yè)務節(jié)點(測試環(huán)境、內部工具),腳本批量執(zhí)行:# 批量遷移(并發(fā) 2,避免同時影響太多)fornodein$(catbatch1_nodes.txt);do./migrate_to_containerd.sh$node[$(jobs-r-p|wc-l)-ge2]wait-ndonewait踩坑4:灰度第 3 天,有 Pod 報ImagePullBackOff,排查發(fā)現(xiàn)是 containerd 的鏡像緩存策略和 Docker 不同。Docker 會緩存所有 pull 過的鏡像,containerd 的 GC 會定期清理。修復:調整 containerd GC 配置,核心鏡像用imagePullPolicy: IfNotPresent DaemonSet 預熱。# 調整 GC,保留更久 [plugins.io.containerd.gc.v1.scheduler] deletion_threshold 500 stale_threshold 24h3.3.2 第二批 50 節(jié)點第二批包含 5 個核心業(yè)務節(jié)點。遷移時發(fā)現(xiàn) 2 個新問題:踩坑5:監(jiān)控指標缺失。原來用 cAdvisor 的container_cpu_usage_seconds_total等 Docker 相關指標,Prometheus 采集配置里寫了 Docker API 地址,遷移后采不到。修復:切換到 kubelet 內置 cAdvisor(端口 10250),指標名一致,但采集端點變了:# Prometheus 采集配置-job_name:kubernetes-nodes-cadvisorkubernetes_sd_configs:-role:nodescheme:httpstls_config:ca_file:/var/run/secrets/kubernetes.io/serviceaccount/ca.crtbearer_token_file:/var/run/secrets/kubernetes.io/serviceaccount/tokenrelabel_configs:-action:labelmapregex:__meta_kubernetes_node_label_(.)-target_label:__address__replacement:kubernetes.default.svc:443-source_labels:[__meta_kubernetes_node_name]regex:(.)target_label:__metrics_path__replacement:/api/v1/nodes/${1}/proxy/metrics/cadvisor踩坑6:有業(yè)務 Pod 掛載了/var/run/docker.sock,遷移后 Pod 啟動失敗。這個是最棘手的。有 3 個 Java 業(yè)務用 docker.sock 做服務發(fā)現(xiàn)(通過 docker API 查容器列表),遷移后 socket 不存在。解決方案分兩步:短期:在節(jié)點上創(chuàng)建一個軟鏈/var/run/docker.sock - /run/containerd/containerd.sock(不完美,API 不完全兼容)長期:推動業(yè)務改造,用 K8s API 做服務發(fā)現(xiàn)3.4 第四階段:全量遷移(3/16 - 3/30)3.4.1 全量遷移節(jié)奏每個集群分批,每批 20-30 節(jié)點,夜間低峰執(zhí)行:# 全量遷移編排腳本#!/bin/bashCLUSTER$1BATCH_SIZE20SLEEP_BETWEEN300# 批次間隔 5 分鐘whilereadnode;doecho遷移:$node./migrate_to_containerd.sh$nodemigrate.log21# 控制并發(fā)while[$(jobs-r-p|wc-l)-ge$BATCH_SIZE];dosleep5done# 每批之間 sleepif[$(($(wc-lmigrate.log)%$BATCH_SIZE))-eq0];thensleep$SLEEP_BETWEENfidone${CLUSTER}_nodes.txtwait# 最終驗證echo 遷移完成,驗證 kubectl get nodes-ojsonpath{range .items[*]}{.metadata.name}{ }{.status.nodeInfo.containerRuntimeVersion}{\n}{end}3.4.2 鏡像構建鏈路改造開發(fā)環(huán)境從 docker build 切到 buildkit:# 方式1:buildkit CLI(推薦)buildctl build\--frontenddockerfile.v0\--localcontext.\--localdockerfile.\--outputtypeimage,nameharbor.example.com/app:v1,pushtrue# 方式2:docker buildx(兼容 docker build 語法)dockerbuildx build--push-tharbor.example.com/app:v1.# 方式3:Kaniko(在 K8s 里構建,無 Docker daemon)kubectl apply-f-EOF apiVersion: v1 kind: Pod metadata: name: kaniko-builder spec: containers: - name: kaniko image: gcr.io/kaniko-project/executor:latest args: [--dockerfileDockerfile, --contextgit://github.com/example/repo.git, --destinationharbor.example.com/app:v1] volumeMounts: - name: docker-config mountPath: /kaniko/.docker volumes: - name: docker-config secret: secretName: harbor-cred EOF踩坑7:CI/CD 流水線里大量用了docker run做集成測試,遷移后失效。改造方案:用podman替代(命令兼容),或者改造為 K8s Job。# podman 替代 docker run(命令幾乎一致)podmanrun--rm-itharbor.example.com/test-runner:v1 ./run-tests.sh3.5 第五階段:遺留問題處理(3/25 - 3/30)3.5.1 日志采集雙寫過渡遷移期間部分節(jié)點 Docker、部分 containerd,Filebeat 采兩套路徑:filebeat.inputs:# Docker 節(jié)點-type:containerpaths:-/var/lib/docker/containers/*/*.logprocessors:-add_fields:target:fields:runtime:docker# Containerd 節(jié)點-type:containerpaths:-/var/log/containers/*.logprocessors:-add_fields:target:fields:runtime:containerd全量遷移后刪除 Docker 路徑配置。3.5.2 docker.sock 依賴業(yè)務改造推動 3 個業(yè)務從 docker.sock 切到 K8s API:// 原來:通過 docker.sock 查容器// DockerClient docker new DockerClient(unix:///var/run/docker.sock);// ListContainer containers docker.listContainers();// 改造后:用 K8s APIKubernetesClientk8snewKubernetesClient();PodListpodsk8s.pods().inNamespace(prod).withLabel(appxxx).list();四、踩坑與應急4.1 應急:灰度節(jié)點 Pod 大量 ImagePullBackOff現(xiàn)象:灰度第 1 天,遷移后的節(jié)點上 Pod 大量 ImagePullBackOff。定位:containerd 配置的 Harbor CA 路徑不對,導致 HTTPS 校驗失敗。修復:# 臨時修復sshnode-xxxmkdir -p /etc/containerd/certs.d/harbor.example.comscpharbor-ca.crt node-xxx:/etc/containerd/certs.d/harbor.example.com/ca.crtsshnode-xxxsystemctl restart containerd腳本修正 CA 下發(fā)邏輯后,問題不再出現(xiàn)。4.2 應急:節(jié)點 NotReady,kubelet 報 CRI 連接失敗現(xiàn)象:遷移后某節(jié)點 NotReady,kubelet 日志報 “Failed to connect to CRI”。定位:containerd 的 socket 權限不對,kubelet 無權限訪問。修復:# containerd 配置 socket 權限[grpc]address/run/containerd/containerd.sockuid0gid0# 關鍵:確保 kubelet 能訪問并檢查/run/containerd/containerd.sock權限是srw-rw---- root root。五、復盤與改進5.1 遷移效果指標DockerContainerd提升節(jié)點內存占用(運行時)380 MB210 MB-45%Pod 啟動時間(平均)12 秒9.5 秒-21%鏡像拉取時間(1GB)45 秒32 秒-29%日志采集延遲2 秒1 秒-50%5.2 經驗教訓兼容性測試要全面:鏡像格式、倉庫認證、日志路徑、監(jiān)控指標、特殊掛載,一個都不能漏。灰度要分批:10 → 50 → 全量,每批觀察足夠時間,別貪快。回滾預案要演練:回滾腳本不演練,真出事時一定卡殼。日志采集要雙寫:遷移期間兩種路徑共存,全量完成再切。docker.sock 依賴要早改:這是遷移最大障礙,提前半年推動業(yè)務改造。配置文件用配置管理:containerd config.toml 用 Ansible/Puppet 統(tǒng)一下發(fā),別手動改。監(jiān)控告警先切:遷移前先把監(jiān)控指標切到 kubelet cAdvisor,避免遷移后盲區(qū)。5.3 長期改進鏡像構建統(tǒng)一:全公司推行 Kaniko 在 K8s 內構建,淘汰本地 docker build鏡像倉庫治理:Harbor 升級,啟用鏡像簽名與漏洞掃描運行時監(jiān)控:建立 containerd 運行時專屬監(jiān)控大盤配置基線:containerd config.toml 納入 GitOps 管理六、可復用產出6.1 containerd 遷移清單(檢查表)階段檢查項狀態(tài)評估鏡像格式兼容性測試?評估私有倉庫認證方案?評估日志采集路徑方案?評估監(jiān)控指標切換方案?評估docker.sock 依賴排查?評估CI/CD 鏈路改造方案?準備containerd 配置模板?準備遷移腳本 回滾腳本?準備監(jiān)控告警切換?灰度第一批 10 節(jié)點 觀察 5 天?灰度第二批 50 節(jié)點 觀察 5 天?全量集群 1/2/3 分批遷移?收尾日志雙寫切單寫?收尾docker.sock 依賴業(yè)務改造?收尾復盤報告?6.2 docker vs containerd 命令對照表操作DockerContainerd(crictl)Containerd(ctr)查容器docker pscrictl psctr -n k8s.io c ls查鏡像docker imagescrictl imagesctr -n k8s.io i ls拉鏡像docker pullcrictl pullctr -n k8s.io i pull查日志docker logscrictl logsctr -n k8s.io c logs進入容器docker execcrictl execctr -n k8s.io c exec查容器詳情docker inspectcrictl inspectctr -n k8s.io c info查運行時信息docker infocrictl infoctr version清理容器docker rmcrictl rmctr -n k8s.io c rm清理鏡像docker rmicrictl rmictr -n k8s.io i rm查容器 statsdocker statscrictl statsctr -n k8s.io c metrics注意:crictl是 K8s 官方 CRI 調試工具,推薦用;ctr是 containerd 原生工具,功能更全但語法復雜。6.3 遷移腳本框架(見 3.2.3)6.4 回滾預案(見 2.3)思考題如果業(yè)務強依賴 docker.sock 且無法改造,你會如何在 containerd 集群中兼容?有哪些方案及其代價?containerd 的鏡像 GC 策略和 Docker 有何不同?如何避免核心鏡像被 GC?遷移過程中如何保證日志不丟?雙寫方案的代價是什么?延伸閱讀K8s 官方:Dockershim 移除 FAQcontainerd 官方文檔:https://containerd.io/docs/crictl 使用指南:https://github.com/kubernetes-sigs/cri-toolsBuildKit 官方文檔:https://github.com/moby/buildkitHarbor 與 containerd 集成配置