Kubernetes 与 Docker 的区别,可以先用一句话概括:Docker 负责单个容器的创建、启动和暂停,Kubernetes 负责一组服务器上的容器调度、服务发现、故障恢复和配置管理。真正执行 Kubernetes 集群安装时,kubeadm 只是搭建控制平面的骨架,真正的容器生命周期仍然由容器运行时承担。这里的困难点在于,从 Kubernetes 1.24 开始,官方已经移除了 dockershim,kubelet 不再直接把 Docker 当作容器运行时来调用。因此“kubeadm 安装最新 Kubernetes 1.37.x,底层走 Docker 容器”这个需求,必须通过 cri-dockerd 作为 CRI 桥接组件来实现。
下面以 Kubernetes 1.37.x 为目标版本,完整走一遍 kubeadm 安装过程。这个过程与 1.24 之后的版本基本一致,但第一次操作时容易在 CRI socket、cgroup driver、CNI 网络三处翻车。本文会先把架构讲清楚,再给出完整命令、验证方式和排错清单。
1. 理解 kubeadm、kubelet、Docker 和 cri-dockerd 的分工
1.1 kubeadm 只负责把集群“骨架”立起来
kubeadm 是 Kubernetes 官方提供的集群初始化工具,它的职责包括生成证书、创建 etcd、启动 API Server、调度器和控制器管理器,以及生成 kubelet 拉取镜像时需要的 bootstrap token。它不负责直接运行容器,也不会自己创建 Pod。
kubeadm 初始化完成后,集群里会运行很多静态 Pod 和 DaemonSet。这些 Pod 真正由 kubelet 拉起,而 kubelet 不直接调用容器命令,它通过 CRI 接口和容器运行时通信。Kubernetes 只定义了一组 CRI 接口,容器运行时只要实现这组接口,就可以被 kubelet 使用。
1.2 Docker 与 Kubernetes 的 CRI 链路
Docker 本身不是 CRI 实现。在比较老的 Kubernetes 版本里,kubelet 内部有一块 dockershim 代码,把 CRI 请求翻译成 Docker API。这个方案维护成本高,官方便在 1.20 中宣布弃用,1.24 正式移除。
官方移除 dockershim 之后,如果你仍然要求“底层走 Docker 容器”,就需要把 cri-dockerd 装到节点上。cri-dockerd 是 Mirantis 维护的桥接组件,它本身实现 CRI 接口,可以接收 kubelet 发来的 CRI 请求,再通过 Docker Engine API 调用 Docker 来创建容器。这样从调用链上看,容器进程仍然是 Docker 拉起,但 Kubernetes 版本的演进不会直接影响这一层。
实际链路为:
kubectl -> kubelet -> CRI -> cri-dockerd -> Docker Engine -> containerd -> runc这里的“底层走 Docker 容器”不是说 kubelet 直接访问/var/run/docker.sock,而是指 kubelet 把请求交给 cri-dockerd,cri-dockerd 再把请求转交给 Docker Engine。少了 cri-dockerd 这一层,kubelet 就找不到可用的 CRI 后端。
1.3 组件分工速查
| 组件 | 作用 | 说明 |
|---|---|---|
| kubeadm | 初始化集群、生成证书、生成 join 命令 | 侧重控制平面搭建 |
| kubelet | 在节点上运行,管理 Pod 生命周期 | 通过 CRI 与运行时通信 |
| kubectl | 操作集群的客户端工具 | 可以装在你的工作站上 |
| Docker Engine | 实际创建和管理容器进程 | 不直接给 kubelet 使用 |
| cri-dockerd | 把 CRI 请求转成 Docker API | 节点上必须常驻运行 |
| containerd | Docker 内部负责容器运行时的组件 | 通常在 docker-ce 安装时一并安装 |
| CNI 插件 | 实现 Pod 网络 | 常见选择是 Calico、Flannel |
1.4 版本说明:1.37.x 按实际 stable 版本为准
Kubernetes 版本迭代非常快,文章中所有命令里的v1.37.x都表示“目标版本”,不一定代表当前官方 release 目录里已经存在。安装前先在官方 release 页面确认 1.37.x 是否已经进入 stable 仓库。如果还没有,就把v1.37替换成当前的稳定版本。下面的安装逻辑在 1.24 之后的版本上通用,差异主要集中在具体版本号和镜像仓库地址。
2. 环境准备:系统、内核参数与组件版本
2.1 主机规划
以一个控制平面节点加一个 worker 节点的最小集群为例。生产环境建议把控制平面节点和工作节点分离,测试环境可以直接复用单节点。
| 角色 | 主机名 | 建议配置 | 示例 IP |
|---|---|---|---|
| control-plane | k8s-master | 2C4G 起,磁盘 30G | 192.168.56.11 |
| worker | k8s-node1 | 2C4G 起,磁盘 30G | 192.168.56.12 |
操作系统推荐 Ubuntu 22.04 LTS,内核版本不低于 5.4。如果使用 CentOS 或 RHEL 系列,需要额外处理 SELinux 和 firewalld。下面的命令默认操作系统是 Ubuntu。
2.2 内核模块和 sysctl 配置
Kubernetes 节点要求开启 IPv4 转发,并要求 iptables 能看到 bridge 流量。否则 kubeadm 预检会直接失败,Pod 之间的网络也可能出现问题。
先加载 br_netfilter 模块:
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay sudo modprobe br_netfilter再写入内核参数:
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 EOF sudo sysctl --system检查是否生效:
sysctl net.bridge.bridge-nf-call-iptables sysctl net.ipv4.ip_forward这里要注意,不要只执行modprobe却不写/etc/modules-load.d,否则服务器重启后模块不会自动加载,节点可能重启后变为 NotReady。
关闭 swap,并注释掉/etc/fstab里的 swap 行:
sudo swapoff -a sudo sed -i '/ swap / s/^/#/' /etc/fstabKubernetes 不支持把 swap 作为可用的内存调度资源,kubelet 默认也在检测到 swap 时拒绝正常运行。
2.3 安装 Docker Engine 并设置 systemd cgroup
先安装依赖:
sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg添加 Docker 官方仓库:
sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null安装 Docker:
sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io安装完成后,需要修改 Docker 的 cgroup driver。Docker 默认在 systemd 系统上可能采用cgroupfs或systemd,但 kubeadm 生成的 kubelet 默认使用systemd。两者如果不一致,kubelet 启动时会报 cgroup driver 冲突。
修改/etc/docker/daemon.json:
{ "exec-opts": ["native.cgroupdriver=systemd"], "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" }, "storage-driver": "overlay2" }重启 Docker:
sudo systemctl daemon-reload sudo systemctl restart docker sudo systemctl enable docker检查 cgroup driver:
docker info | grep -i cgroup输出中应出现Cgroup Driver: systemd。这一步很关键,很多节点加入集群后显示 NotReady,就是因为 Docker 和 kubelet 的 cgroup driver 不一致。
2.4 安装 cri-dockerd
cri-dockerd 需要单独下载。示例使用 v0.3.x,实际安装时以 GitHub releases 页面里的版本为准。
CRI_DOCKERD_VERSION=0.3.18 wget https://github.com/Mirantis/cri-dockerd/releases/download/v${CRI_DOCKERD_VERSION}/cri-dockerd-${CRI_DOCKERD_VERSION}.amd64.tgz tar xvf cri-dockerd-${CRI_DOCKERD_VERSION}.amd64.tgz sudo install -m 0755 cri-dockerd /usr/local/bin/cri-dockerd cri-dockerd --version创建 systemd service 文件:
sudo tee /etc/systemd/system/cri-dockerd.service > /dev/null <<EOF [Unit] Description=CRI Interface for Docker Application Container Engine Documentation=https://docs.mirantis.com After=network-online.target Wants=network-online.target [Service] Type=notify ExecStart=/usr/local/bin/cri-dockerd --container-runtime-endpoint unix:///var/run/cri-dockerd.sock Restart=always RestartSec=10 [Install] WantedBy=multi-user.target EOF启动并设置开机自启:
sudo systemctl daemon-reload sudo systemctl start cri-dockerd sudo systemctl enable cri-dockerd确认 socket 出现:
ls -l /var/run/cri-dockerd.sockcri-dockerd 默认监听在unix:///var/run/cri-dockerd.sock。后面 kubeadm init 和 kubeadm join 都必须显式指定这个 socket,否则 kubeadm 可能检测到 containerd 的 socket,导致集群最终使用 containerd 而不是 Docker。
2.5 安装 kubeadm、kubelet、kubectl
这里以 v1.37 仓库为例。如果该版本还没进入 stable,先把v1.37改成实际存在的版本。
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.37/deb/Release.key | \ sudo gpg --dearmor -o /etc/apt/keyrings/k8s.gpg echo "deb [signed-by=/etc/apt/keyrings/k8s.gpg] https://pkgs.k8s.io/core:/stable:/v1.37/deb/ /" | \ sudo tee /etc/apt/sources.list.d/kubernetes.list安装:
sudo apt-get update sudo apt-get install -y kubelet=1.37.x-* kubeadm=1.37.x-* kubectl=1.37.x-* sudo apt-mark hold kubelet kubeadm kubectlapt-mark hold是为了防止 apt 自动升级这三个组件。Kubernetes 组件升级需要按顺序执行,不能任由包管理器随意升级。Docker 和 cri-dockerd 也需要在确定兼容版本后手动管理版本。
2.6 环境准备检查清单
| 检查项 | 命令或位置 | 预期结果 |
|---|---|---|
| 内核模块 | `lsmod | grep br_netfilter` |
| IPv4 转发 | sysctl net.ipv4.ip_forward | 1 |
| swap | swapon --show | 无输出 |
| Docker cgroup driver | `docker info | grep -i cgroup` |
| cri-dockerd socket | ls -l /var/run/cri-dockerd.sock | 文件存在 |
| kubeadm 版本 | kubeadm version | 1.37.x |
| 主机名 | hostnamectl set-hostname | 互相能解析 |
3. kubeadm init:让控制平面走 cri-dockerd
3.1 初始化参数说明
在控制平面节点上执行。如果准备使用 Calico 作为 CNI,这里可以把 Pod 网段设置为192.168.0.0/16。
sudo kubeadm init \ --kubernetes-version=v1.37.x \ --apiserver-advertise-address=192.168.56.11 \ --pod-network-cidr=192.168.0.0/16 \ --cri-socket=unix:///var/run/cri-dockerd.sock参数含义如下:
--kubernetes-version:显式指定版本,避免 kubeadm 使用默认版本与仓库版本不一致。--apiserver-advertise-address:控制平面节点的 API Server 监听地址,多个网卡时必须指定。--pod-network-cidr:Pod 网段,必须和 CNI 插件配置一致,这里匹配 Calico 默认网段。--cri-socket:告诉 kubeadm 使用 cri-dockerd。不指定时,kubeadm 会按顺序探测可用的 CRI socket,容易误选 containerd。
初始化输出末尾会给出两段重要信息:
- 配置 kubectl 的命令。
- 加入 worker 节点时使用的
kubeadm join命令。
如果输出没有 join token,可以稍后用kubeadm token create --print-join-command重新生成。
3.2 配置 kubectl
控制平面节点上,按 kubeadm 输出的提示操作:
mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config验证:
kubectl get nodes此时节点可能显示 NotReady,原因通常是 CNI 网络插件还没安装。
3.3 安装 Calico CNI
Calico 是 Kubernetes 常用的 CNI 插件。执行:
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.28/manifests/calico.yaml如果服务器无法直接访问 raw.githubusercontent.com,可以先下载到本地,或者放到公司内部文件服务器后同步到节点再执行。生产环境更应该先评审清单内容,再手动 apply。
等待 kube-system 命名空间里的 Pod 就绪:
kubectl wait -n kube-system \ --for=condition=Ready pod -l app=calico3.4 检查控制平面状态
网络插件安装完成后,检查节点状态:
kubectl get nodes kubectl get pods -n kube-system正常情况下,控制平面节点应该为 Ready,coredns、etcd、kube-apiserver、kube-controller-manager、kube-scheduler 等 Pod 均为 Running 或 Completed。
3.5 单节点测试时允许调度业务 Pod
默认控制平面节点带有node-role.kubernetes.io/control-plane:NoSchedule污点,业务 Pod 不会调度上来。如果你想在单节点学习环境中跑业务,可以去掉这个污点:
kubectl taint nodes --all node-role.kubernetes.io/control-plane-这只适合学习和临时测试。生产环境不要把控制平面节点作为普通业务节点,否则控制平面负载和业务负载互相影响,出现问题后排查范围也会扩大。
4. crictl、Docker 与 kubectl 三层验证
4.1 确认节点 Ready
kubectl get nodes -o wide输出中STATUS为 Ready,INTERNAL-IP为预期 IP。节点加入时间较长时,可以先看 kubelet 状态:
sudo systemctl status kubelet journalctl -u kubelet -f4.2 确认 kubelet 使用的 CRI endpoint
kubeadm 会把 kubelet 的启动参数写进/var/lib/kubelet/kubeadm-flags.env:
cat /var/lib/kubelet/kubeadm-flags.env这里应该能看到--container-runtime-endpoint=unix:///var/run/cri-dockerd.sock。如果这里变成 containerd socket,说明 init 时没有指定或指定错误。
4.3 用 crictl 看容器运行时视图
crictl 是 CRI 视角的容器查看工具。由于机器上可能同时存在 containerd 和 cri-dockerd 两个 socket,必须显式指定 endpoint。
先安装 crictl 并配置:
cat <<EOF | sudo tee /etc/crictl.yaml runtime-endpoint: unix:///var/run/cri-dockerd.sock image-endpoint: unix:///var/run/cri-dockerd.sock timeout: 10 debug: false EOF然后执行:
crictl ps crictl pods可以看到与 kubectl 对应的 sandbox 容器和业务容器。crictl 的输出不以 k8s 的“Pod”为单位,而是以容器和 Pod sandbox 为单位,这是正常的。
4.4 用 docker ps 看宿主机视图
同一时刻执行:
docker ps | grep k8s_会看到 cri-dockerd 通过 Docker Engine 创建出来的容器。容器名通常带有k8s_前缀,这是 cri-dockerd 的命名规范。
4.5 对比两种视图
| 工具 | 看到的内容 | 适用场景 |
|---|---|---|
| kubectl | Pod、Service、Deployment 等资源 | 日常业务操作 |
| crictl | Pod sandbox、容器运行时状态 | 排查 kubelet 与运行时一致性问题 |
| docker ps | 宿主机上的 Docker 容器进程 | 验证是否真的走了 Docker |
注意:在 Kubernetes 集群中,优先使用 kubectl 和 crictl 排查容器,不要直接用 docker stop、docker rm 去删容器。Kubernetes 会把期望状态和实际状态比较,直接操作 Docker 很可能造成一个容器被删了,另一个又被自动拉起来,最终更难排查。
5. worker 节点加入集群
5.1 worker 节点环境准备
worker 节点需要安装 Docker、cri-dockerd、kubeadm、kubelet,操作步骤和控制平面节点基本相同。worker 可以只安装 kubeadm 和 kubelet,不安装 kubectl,但安装 kubectl 也不影响使用。
需要重复的步骤包括:
- 配置 br_netfilter 和 sysctl。
- 关闭 swap。
- 安装 Docker 并设置
native.cgroupdriver=systemd。 - 安装 cri-dockerd 并启动。
- 安装 kubeadm、kubelet 并
apt-mark hold。
建议先在 worker 节点上执行环境准备检查清单,再执行 join,避免加入失败后反复查看日志。
5.2 生成新的 join 命令
控制平面节点上执行:
kubeadm token create --print-join-command这条命令适合 token 过期或丢失的情况。它输出的命令通常不包含--cri-socket,需要手动追加。
5.3 worker 节点 join 时指定 cri socket
在 worker 节点上执行:
sudo kubeadm join 192.168.56.11:6443 \ --token <token> \ --discovery-token-ca-cert-hash sha256:<hash> \ --cri-socket=unix:///var/run/cri-dockerd.sock这里最容易犯的错是直接复制 kubeadm init 输出中的 join 命令而不加--cri-socket。不加时 kubeadm 可能检测到 containerd,结果 worker 节点变成 containerd 运行时,而控制平面是 cri-dockerd 运行时,虽然都能工作,但节点环境不一致会让后续维护更困难。
5.4 验证工作负载调度
回到控制平面节点:
kubectl get nodes kubectl get pods -n kube-system -o wide如果看到 worker 节点 Ready,可以在集群里创建一个测试 Deployment:
kubectl create deployment nginx-test --image=nginx:1.27 kubectl expose deployment nginx-test --port=80 --type=NodePort查看 Pod 运行在哪个节点:
kubectl get pods -o wide然后访问 NodePort 服务验证网络链路是否正常。如果流量不通,优先检查 Calico 的 Pod 状态和节点 iptables 规则,而不是急着重启节点。
6. 常见问题排查:运行时、CNI 与 cgroup
6.1 报错:CRI v1 runtime API is not implemented
现象:kubelet 不断重启,日志中提示类似CRI v1 runtime API is not implemented或failed to get runtime endpoint。
成因:kubelet 和 cri-dockerd 的 CRI 版本不匹配,或者 cri-dockerd 版本过旧,不认识当前 kubelet 请求的 CRI 版本。
处理方式:
sudo systemctl status cri-dockerd cri-dockerd --version如果 cri-dockerd 版本太老,升级到最新 0.3.x。升级后分别重启 docker 和 cri-dockerd。
6.2 报错:cgroup driver 与 kubelet 不一致
现象:kubelet 日志出现failed to run Kubelet: failed to get cgroup driver for docker或者systemd与cgroupfs冲突。
成因:Docker、kubelet、cri-dockerd 三者的 cgroup driver 不一致。
处理方式:
docker info | grep -i cgroup如果显示不是systemd,修改/etc/docker/daemon.json中的exec-opts并重启 Docker:
sudo systemctl restart docker sudo systemctl restart cri-dockerd sudo systemctl restart kubelet6.3 报错:节点 NotReady 且 CNI 未就绪
现象:kubectl get nodes显示 NotReady,kubectl get pods -n kube-system中的 Calico Pod 一直 Pending 或 CrashLoopBackOff。
成因:Calico 无法下载镜像、Pod 网段不一致、节点 IP 没有正确检测,或/etc/cni/net.d目录下没有 CNI 配置。
处理方式:
先查看 Calico Pod 日志:
kubectl logs -n kube-system -l app=calico -f如果日志提示 IPAM 冲突或网段不匹配,检查kubeadm init时的--pod-network-cidr与 Calico 配置是否一致。如果想完全重置网络,可以清空/etc/cni/net.d后重新安装 Calico,但生产环境不要随意删除目录。
6.4 报错:crictl 连接不上
现象:执行crictl ps时报failed to connect或no such file or directory。
成因:crictl 默认尝试连接 containerd socket,但节点实际使用的是 cri-dockerd socket。
处理方式:
crictl --runtime-endpoint unix:///var/run/cri-dockerd.sock ps也可以直接把/etc/crictl.yaml里的 runtime-endpoint 改成 cri-dockerd,这样就不需要每次传参。
6.5 问题速查表
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| kubeadm preflight 报 bridge-nf 错误 | 没有配置 sysctl | sysctl net.bridge.bridge-nf-call-iptables | 加载 br_netfilter 并执行sysctl --system |
| kubelet 找不到 runtime | cri-dockerd 未启动 | systemctl status cri-dockerd | 启动并设置开机自启 |
| worker join 后节点 NotReady | join 时没指定 cri socket | 查看/var/lib/kubelet/kubeadm-flags.env | 重新执行带--cri-socket的 join |
| Calico Pod Pending | 镜像拉取失败或网段冲突 | kubectl logs -n kube-system -l app=calico | 核对 Pod CIDR 和镜像仓库地址 |
| Pod 启动后被不断重启 | 误删 Docker 容器或资源不足 | kubectl describe pod | 用 kubectl 和 crictl 排查,避免直接操作 docker |
| Docker 和 kubelet cgroup 不一致 | daemon.json 配置缺失 | `docker info | grep -i cgroup` |
7. 学习环境与生产环境的取舍
7.1 学习环境如何快速重来
单节点学习时,如果集群环境弄乱了,最快的方式是重置 kubeadm,而不是反复补配置。
sudo kubeadm reset --cri-socket=unix:///var/run/cri-dockerd.sock然后清理配置:
sudo rm -rf /etc/cni/net.d $HOME/.kube /var/lib/kubelet再次安装时需要重新执行kubeadm init。kubeadm reset 并不清理 Docker 镜像,如果需要彻底清理,再手动删除 Docker 中的数据。
7.2 生产环境为什么优先 containerd
如果你的团队没有强需求必须使用 Docker CLI 或 Docker API,生产环境更推荐直接使用 containerd。原因是 containerd 本身就是 CRI 原生实现,少一层 cri-dockerd 桥接,组件越少越好排查。kubeadm 官方文档默认也是推荐 containerd。
但如果你明确要求“底层走 Docker 容器”,例如现有监控、安全扫描、构建工具都绑定在 Docker API 上,那么 cri-dockerd 是必要选择。这种情况下要做好维护预案,确认 cri-dockerd 与 Kubernetes 版本都有同步升级计划。
7.3 必须保留 cri-dockerd 时的最佳实践
- 把 Docker、kubelet、kubeadm、kubectl、cri-dockerd 的版本固定下来,不要使用
latest。 - 在
/etc/docker/daemon.json中配置日志轮转,避免容器日志长期堆满磁盘。 - 使用 overlay2 存储驱动,避免 devicemapper 等旧驱动带来的性能问题。
- 不要在业务节点上开放 Docker API 给外部访问,Docker socket 权限等于宿主机 root 权限。
- 每次升级 Kubernetes 前,先确认 cri-dockerd 是否已经兼容目标 kubelet 版本。
- 监控
/var/run/cri-dockerd.sock的连通性,并用进程守护保证 cri-dockerd 异常后自动拉起。
7.4 扩展方向
这套流程跑通后,你可以继续往下走几条线:
- 高可用控制平面:为 API Server 增加负载均衡,并配置多个控制平面节点。
- Ingress 和证书管理:部署 Ingress Controller 并通过证书管理工具自动签发证书。
- 可观测性:部署 Prometheus 和 Loki,把节点、容器和 kubelet 日志统一起来。
- 应用部署流程:从 kubectl apply 逐步演进到 Helm Chart 和 GitOps。
刚开始练习时,最重要的是把“kubelet 通过 CRI 与运行时通信”这条链路记清楚。很多生产环境中的网络和容器问题,只要先确认 kubelet 的 runtime endpoint、cgroup driver 和 CNI 配置三者一致,就已经能排除掉一半的嫌疑点。