ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Docker容器化Kubernetes一键部署:路线、避坑与可复跑实践

Docker容器化Kubernetes一键部署:路线、避坑与可复跑实践

简介:面向希望快速搭建容器化 Kubernetes 集群的运维与开发人员,这份一键部署 Shell 脚本资源聚焦 Docker 与 K8s 环境初始化、组件安装及集群配置,可减少逐项手动部署和参数调整的重复工作,适合具备基础 Linux 操作能力、需要复现实验环境或搭建测试集群的读者。压缩包共 7 个文件,约 10.67MB,以 Shell 脚本、YAML/YML 配置、TXT 说明和 RPM 安装包为主,分别对应自动化安装、集群参数定义、网络插件配置与清理等用途,结构紧凑,便于按节点规划和软件版本进行调整。已有 953 人学习下载,说明其在同类部署脚本中具有一定参考价值。读者可从中获得一套可修改的集群部署脚本框架、容器运行时与 Kubernetes 版本组合参考,以及配置样例和卸载清理思路,便于理解容器化集群搭建中的组件关系、参数组织方式和常见配置结构。

1. 拿到脚本先别 sudo bash:这份 Docker + Kubernetes 一键部署包的真实定位

同事把一个压缩包丢到群里,说里面是一套 Docker 容器化 Kubernetes 集群的一键部署 shell 脚本,解压之后sudo bash deploy.sh就能出一套集群。这类东西第一次跑通常很顺,第二次换台机器就翻车——不是因为脚本写得烂,而是它把大量隐含假设埋在了脚本里:内核 cgroup 版本、Docker 的 cgroup driver、镜像从哪来、节点之间能不能互通、重复执行会不会把上次的残留踩坏。

这份资源解决的就是这段重复劳动:把装 Docker、配内核参数、锁 Kubernetes 组件版本、预拉镜像、kubeadm 初始化和 worker 加入这一长串动作收敛成几个脚本。它适合三类人:需要在测试环境反复重建集群的运维、要在 CI 里跑一套一次性集群的开发、以及想弄明白 kubeadm 到底做了什么的初学者。往下读之前先记住一句血泪经验:脚本跑通不等于集群可用,中间还有一层验证要做。

2. 脚本拆开看:Docker 运行时与容器节点的两条路线怎么选

同一句“Docker 容器化 Kubernetes”,在工程落地里其实对应两条完全不同的路径,脚本的执行分支、资源开销、能覆盖的场景都不一样。混淆这两条路径是很多人读完说明仍然不知道脚本怎么改的根源。先把路线分清楚,再看脚本目录和变量,最后说明为什么版本必须锁在脚本里,这三件事决定了后面所有排错的方向。

2.1 两条路线:Docker 当运行时,还是 Docker 当节点

第一条路线是让 Docker 作为 Kubernetes 的容器运行时。节点仍然是真机或者虚拟机,Docker 负责跑容器,kubelet 通过 CRI 接口和它对话。早年间 Kubernetes 内置 dockershim,配置简单,后来这条内置通道被移除,脚本里通常要额外装一个 CRI 适配组件(常见的是 cri-dockerd),并显式告诉 kubelet 去连哪个 socket。这条路线适合你手上已经有一批机器、团队镜像仓库和流水线都围绕 Docker 构建、迁移成本敏感的场景。它的代价是多一个守护进程要维护,kubelet 的--container-runtime-endpoint参数一个字符写错就起不来。

第二条路线是把 Docker 当成节点本身。每个 Kubernetes 节点其实是一个 Docker 容器,容器里跑 kubelet 或者轻量发行版,控制面和工作节点都是docker run出来的。这条路线的代表思路就是 kind / k3d 那一类工具,本质是把节点当作可丢弃的进程。好处极明显:一台笔记本就能起三节点集群,删掉容器就等于删掉节点,CI 里每次跑完不用清理。代价是网络嵌套了一层,LoadBalancer 类型服务、跨主机路由、内核参数调优这些都会受限,不适合拿来验证生产网络方案。

脚本包一般用MODE变量切换这两条路线,我在实际项目里通常会这样分工:开发自测和 CI 用容器节点模式,预发和压测用 Docker 运行时模式。判断依据可以看下面这张对照表。

判断项Docker 当运行时Docker 当节点载体
最低资源每节点 2C4G 起单机 8G 内存可跑三节点
节点数量受限于真实机器单机可虚拟出多个节点
网络能力完整,可验证 CNI 与路由受限,LoadBalancer 需额外组件
重建成本分钟级,需重装系统包秒级,删容器即可
适用场景预发、压测、类生产验证开发自测、CI、教学演示
常见故障点cgroup driver、CRI socket端口映射、容器内 systemd

选错路线的典型症状是:拿容器节点模式去跑生产网络插件的验收测试,结果一切正常,上线后跨节点 Pod 通信大面积超时。这不是插件的问题,是嵌套网络根本没经过真实的三层转发路径。

2.2 脚本目录与变量:一份能改的配置骨架

脚本包的结构通常不复杂,但每个文件的职责边界要清楚,否则改错地方会白折腾。下面是一种常见的组织方式,名称可能不同,职责基本对应。

文件职责改动频率
cluster.conf全局变量,唯一需要改的文件每次部署
preflight.sh环境预检,只读不写不改
install-runtime.sh安装 Docker 与 CRI 组件不改
init-controlplane.sh控制面初始化不改
join-worker.sh工作节点加入不改
verify.sh集群体检不改

真正需要动的是cluster.conf。我一般会把它改成下面这个样子,把版本、网段、模式全抽出来,脚本主体一行版本号都不写死。

#!/usr/bin/env bash # cluster.conf —— 部署前唯一必须修改的文件 # 部署模式:node=真实节点+Docker 运行时;container=容器充当节点 MODE="node" # 三条命令版本必须完全一致,否则 kubeadm 会直接拒绝初始化 K8S_VERSION="v1.xx.y" # 容器运行时 socket,Docker 当运行时时必须显式指向适配组件 CRI_SOCKET="unix:///var/run/cri-dockerd.sock" # 网段规划:Pod 网段与 CNI 插件里填的必须一致 POD_CIDR="10.244.0.0/16" SERVICE_CIDR="10.96.0.0/12" # 控制面对外暴露地址,多网卡机器上写错会导致 worker 连不上 API_ADVERTISE="192.168.10.11" # 镜像仓库前缀,离线环境改成内网仓库地址 IMAGE_REPO="registry.internal.example/k8s" # 工作节点清单,一行一个 IP WORKER_LIST="./workers.txt"

变量的作用不只是省事,更是排错时的定位锚点。K8S_VERSION决定 kubeadm 去拉哪个 tag 的镜像,CRI_SOCKET决定 kubelet 连不连得上运行时,POD_CIDR决定 CNI 插件分配的地址池,API_ADVERTISE决定 worker 的 kubelet 能不能回连控制面。这四个变量出错,现象各不相同但都表现为“集群起不来”,所以脚本预检阶段必须逐个校验,而不是等到kubeadm init报错再去翻日志。IMAGE_REPO在纯离线环境里尤其关键,它把公网地址整体换成内网前缀,配合后面的镜像导出导入流程使用。

我习惯在改完配置后先跑一次bash -n cluster.conf做语法检查,再source一次确认变量都能正常展开。这一步花十秒,能挡掉后面半小时的莫名错误。

2.3 为什么把版本锁在脚本里而不是交给系统包管理器

很多人图省事,apt install kubelet kubeadm kubectl不加版本号,结果装出来的三个二进制版本不一致。kubeadm 对此非常敏感,初始化时会直接检查 kubelet 版本偏差,超出允许范围就中止。更隐蔽的问题是 Docker 和 kubelet 的 cgroup driver 不一致:Docker 默认可能用 cgroupfs,kubelet 新版默认用 systemd,两者混用会让节点看起来 Ready,但 Pod 起不来,日志里飘着failed to create cgroup之类的报错。这类问题在网上被反复讨论,根因就一句话——两边的 driver 必须一致。

脚本锁定版本的正确做法是:把版本号写进cluster.conf,安装时用包管理器指定版本,并且把 Docker 的daemon.json也一起配好,让两边从第一次启动就对齐。下面这段是安装段的核心,顺序不能颠倒。

#!/usr/bin/env bash # install-runtime.sh —— 先运行时后 kubelet,顺序不可换 source ./cluster.conf # 1. 写 Docker 守护进程配置,关键是 cgroupdriver 与 kubelet 对齐 install -d /etc/docker cat > /etc/docker/daemon.json <<'EOF' { "exec-opts": ["native.cgroupdriver=systemd"], "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" }, "data-root": "/var/lib/docker" } EOF # 2. 装指定版本的 Docker 组件,避免 latest 漂移 apt-get install -y --allow-downgrades \ docker-ce docker-ce-cli containerd.io systemctl enable --now docker docker info | grep -i "cgroup driver" # 必须输出 systemd # 3. 装 CRI 适配组件,让 kubelet 能通过 CRI 调用 Docker apt-get install -y cri-dockerd systemctl enable --now cri-docker # 4. 装 Kubernetes 三件套,版本与 K8S_VERSION 严格一致 apt-get install -y --allow-downgrades \ "kubelet=${K8S_VERSION#v}-*" \ "kubeadm=${K8S_VERSION#v}-*" \ "kubectl=${K8S_VERSION#v}-*" # 5. 锁住版本,防止后续 apt upgrade 把集群升挂 apt-mark hold kubelet kubeadm kubectl docker-ce cri-dockerd

这段脚本的关键点有三个。第一,daemon.json必须在 Docker 第一次启动前写好,如果 Docker 已经在跑,改完要systemctl restart docker,但重启会杀掉所有容器,生产环境要挑时间窗口。第二,cri-dockerd必须在 kubelet 之前就绪,因为 kubelet 启动时会立刻去连 CRI socket,连不上就一直重启,日志里刷connection refused。第三,apt-mark hold这一步经常被漏掉,漏掉的后果是某次系统批量升级后,某个节点的 kubelet 小版本跳到别处,节点被标记为版本偏差过大,调度器不再往上排 Pod。

${K8S_VERSION#v}这个写法是去掉版本号前面的v,因为包管理器的版本字符串不带v。这种细节看着琐碎,但脚本一旦跑在十几台机器上,逐个手工修正的成本就上来了。我一般会把这段单独抽成install-runtime.sh,跑完立刻用kubeadm version、kubelet --version、kubectl version --client三条命令交叉比对,三个输出必须一致,这是后面所有步骤的前提。

3. 从裸机到 Ready:一键脚本的执行链路与四段代码

脚本主体是四段线性流程:预检、安装、初始化、加入。每段都有明确的输入输出,段与段之间靠配置文件传递状态。理解这条链路之后,出错时你能立刻判断问题出在哪一段,而不是从头再看一遍脚本。这一章按执行顺序拆开,给出每段的核心代码、参数含义和失败时的观察点。

3.1 预检段:内核、cgroup、swap 与端口

预检段是整个脚本里最值钱的部分,也是多数一键脚本最糊弄的部分。理想状态下它应该在动手改任何东西之前,把所有会失败的条件列出来。我通常按五个维度检查:内核模块、cgroup 版本、swap 状态、端口占用、时间同步。

#!/usr/bin/env bash # preflight.sh —— 只读检查,失败即退出,不做任何修改 set -euo pipefail source ./cluster.conf fail() { echo "[FAIL] $*" >&2; exit 1; } warn() { echo "[WARN] $*" >&2; } # 1. 必备内核模块:overlay 用于存储驱动,br_netfilter 用于网桥转发 for m in overlay br_netfilter nf_conntrack; do modprobe "$m" 2>/dev/null || fail "内核模块 $m 加载失败" done # 2. cgroup 版本:v2 与 v1 的配置路径不同,脚本要能识别 CGROUP_TYPE=$(stat -fc %T /sys/fs/cgroup) echo "[INFO] cgroup 类型: ${CGROUP_TYPE}" # 3. swap 必须关闭,kubelet 在新版本下不再容忍 swap 开启 if swapon --show | grep -q .; then warn "检测到 swap 已开启,脚本将执行 swapoff -a 并注释 fstab" fi # 4. 控制面关键端口不能已被占用 PORTS=(6443 2379 2380 10250 10257 10259) for p in "${PORTS[@]}"; do if ss -lntH "sport = :$p" | grep -q .; then fail "端口 $p 已被占用,请先确认是否残留旧集群" fi done # 5. 时间偏差超过 5 秒会导致证书校验与 token 失效 command -v chronyc >/dev/null && chronyc tracking | grep -q "Leap status.*Normal" \ || warn "时间同步未确认,证书签发可能出现偏差"

这段脚本的每个检查项都对应一类真实故障。内核模块缺失最常见于裁剪过的云镜像,overlay没加载会导致 Docker 存储驱动回退到 vfs,磁盘瞬间被打满。cgroup 版本的判断是为了后面配置对齐,v2 环境下/sys/fs/cgroup挂载类型是cgroup2fs,v1 是tmpfs,这直接影响 kubelet 的启动参数。swap 检查不必多解释,但要注意光swapoff -a不够,重启后会被/etc/fstab重新挂上,所以脚本里必须同时处理 fstab。

端口检查是我加得最晚但救过最多命的一项。上一套集群没清干净,6443 还占着,新集群kubeadm init会跑到一半失败,留下的半成品状态比完全没装更难清理。至于时间同步,它的隐蔽性在于不会立刻报错,而是过几个小时之后 kubelet 证书轮换失败,节点自己变成 NotReady,查起来毫无头绪。

3.2 安装段:Docker 与 kubelet 的安装顺序

安装段在上一章已经给过核心代码,这里补三个容易被忽略的参数级细节。第一个是native.cgroupdriver=systemd这一项,只在 Docker 使用 systemd 作为 init 系统的发行版上生效,如果你的机器用的是别的 init 系统,这一行反而会让 Docker 起不来,脚本里最好加一个判断而不是无脑写入。第二个是># 运行时自检 docker run --rm hello-world # 只验证 Docker 本体 systemctl is-active cri-docker # 适配组件必须是 active crictl --runtime-endpoint "${CRI_SOCKET}" info # 走 CRI 通道探测 # kubelet 预检,只做检查不启动集群 kubeadm init phase preflight --config ./kubeadm-config.yaml

docker run hello-world只能证明 Docker 自己能跑容器,不能证明 kubelet 能用它。真正的验证点是crictl info,这条命令走的是 CRI 协议,输出的 JSON 里能看到runtimeName和runtimeVersion,只要它返回正常,说明适配组件和 socket 路径都对。kubeadm init phase preflight是很多人不知道的用法,它把初始化前能查的都查一遍但不实际执行,特别适合在正式跑之前做一次彩排。

3.3 初始化段:kubeadm init 的参数从哪来

控制面初始化不要用一长串命令行参数,写成配置文件更可控,也便于版本管理和复用。下面是脚本里常见的一份配置骨架,具体 apiVersion 要跟着你的 K8s 版本线走,新版本线上字段名有过调整,照抄旧模板会直接被拒绝解析。

# kubeadm-config.yaml apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.10.11 # 多网卡机器必须显式指定 bindPort: 6443 nodeRegistration: criSocket: unix:///var/run/cri-dockerd.sock kubeletExtraArgs: cgroup-driver: systemd --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.xx.y controlPlaneEndpoint: 192.168.10.11:6443 networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12 --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemd

三段配置各管一摊。InitConfiguration管本机怎么起控制面,advertiseAddress填错是多网卡环境下的头号杀手,填了一个 worker 路由不到的地址,后面 join 的时候表现是连接超时但 ping 得通。ClusterConfiguration管网段和版本,podSubnet必须和后面 CNI 插件里填的完全一致,不一致的症状是 Pod 有 IP 但跨节点不通。第三段KubeletConfiguration是很多人漏掉的,它把 cgroup driver 显式写进 kubelet 配置,比在命令行参数里传更稳。

初始化执行的命令本身很短:

# 拉取镜像(离线环境可跳过,改用 load 方式) kubeadm config images pull --config ./kubeadm-config.yaml # 正式初始化,日志同时落盘便于失败后回溯 kubeadm init --config ./kubeadm-config.yaml --v=5 2>&1 | tee init.log # 配置本机 kubectl 凭据 mkdir -p $HOME/.kube cp -f /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config # 确认控制面组件是否都起来了 kubectl -n kube-system get pods -o wide

--v=5是调试级别,正常部署用默认级别就够,但第一次在新环境跑我通常会开,它会把每一步的等待和超时都打出来。init.log一定要留,kubeadm 失败时终端输出会被后续日志淹没,回头翻文件最省事。控制面组件全部 Running 之前不要急着 join worker,静态 Pod 起不来通常是镜像或者证书问题,早 join 只会让问题叠加。

3.4 加入段:worker 的 join 令牌怎么安全落地

worker 加入靠一条带 token 的命令,这条命令有时效性。默认 token 有效期不长,大批量节点加入时经常出现前几台成功、后面的全部报 token 过期。稳妥做法是先生成命令、立刻分发执行,或者显式创建一个有效期更长的 token。

# 生成 join 命令,包含当前有效 token JOIN_CMD=$(kubeadm token create --print-join-command) # 需要加入控制面节点时,额外上传证书密钥 CERT_KEY=$(kubeadm init phase upload-certs --upload-certs | tail -n1) # 批量分发执行,逐台记录结果 while read -r host; do echo "==> joining ${host}" ssh "${host}" "${JOIN_CMD} --cri-socket ${CRI_SOCKET}" \ && echo "[OK] ${host}" || echo "[FAIL] ${host}" done < "${WORKER_LIST}"

这里有两个参数必须补上。--cri-socket在 Docker 当运行时的时候不能省,join 阶段 kubeadm 同样要连 CRI,不指定它会去默认路径找 containerd 的 socket,找不到就失败。--control-plane和--certificate-key只在加入控制面节点时需要,普通 worker 不需要。

批量循环里的失败处理也要写清楚,我习惯让脚本在单台失败时继续执行后面的节点并汇总结果,而不是第一台失败就整体退出。原因是 join 失败往往是单机环境问题(防火墙、时间、镜像),一台坏不影响其他台,先让好的进来,坏的最后单独处理效率更高。执行完之后用kubectl get nodes -o wide确认状态,再把kubectl get pods -A -o wide | grep -v Running的结果看一眼,这比只看节点 Ready 更能反映真实情况。

4. 镜像与网络:离线分发、CNI 落地和跨容器通信

集群能 init 成功,离“能用”还有两座山:镜像能不能拉到,Pod 之间能不能通。这两件事在内网环境和容器节点模式下各有各的坑。这一章讲镜像预拉与分发、CNI 插件的选型差异,以及怎么验证网络真的通了而不是看起来通了。

4.1 镜像预拉与 save/load 分发

Kubernetes 组件镜像的体积不小,十几台机器同时从公网拉,慢是一方面,更常见的是某几台因为网络抖动拉了一半失败,kubelet 反复重试,节点卡在 NotReady。稳妥做法是在一台机器上集中拉取、导出成 tar、再分发到各节点导入。

#!/usr/bin/env bash # sync-images.sh —— 集中拉取 + 分发 + 导入 set -euo pipefail source ./cluster.conf # 1. 列出初始化所需的全部镜像,输出带完整 registry 前缀 kubeadm config images list --config ./kubeadm-config.yaml > images.txt # 2. 逐条拉取,加上 CNI 插件自己的镜像 while read -r img; do docker pull "${img}" done < images.txt docker pull "${IMAGE_REPO}/flannel/flannel:vx.y.z" # 3. 一次性导出,避免多条 tar 文件管理混乱 docker save -o k8s-images.tar $(cat images.txt) "${IMAGE_REPO}/flannel/flannel:vx.y.z" # 4. 分发到每个 worker 并导入 while read -r host; do scp -q k8s-images.tar "${host}:/tmp/" || { echo "[FAIL] scp ${host}"; continue; } ssh "${host}" "docker load -i /tmp/k8s-images.tar && rm -f /tmp/k8s-images.tar" \ && echo "[OK] ${host}" done < "${WORKER_LIST}"

kubeadm config images list这一步很关键,它输出的镜像列表和你的版本、仓库配置完全对应,比手工拼镜像名靠谱得多。docker save后跟多个镜像名会把它们打进同一个 tar,导入时docker load一次搞定,比每个镜像一个文件省事。

但这里有个容易踩的点:镜像 tag 里的 registry 地址必须和 kubelet 实际去拉的地址一致。如果你把IMAGE_REPO配成了内网仓库,但镜像导出时还带着公网前缀,导入后 tag 对不上,kubelet 依然会去公网拉。正确做法是在导出前用docker tag统一改前缀,或者干脆在cluster.conf里从一开始就用内网地址拉取。前者适合已有的镜像资产,后者适合全新部署。

4.2 CNI 选型:Flannel 与 Calico 在脚本里的差别

CNI 插件决定 Pod 网段怎么分配、跨节点流量怎么走。脚本层面两者的差别不在安装命令,而在需要提前准备的参数和失败模式。

维度FlannelCalico
部署复杂度一个清单即可需按网卡环境调参数
网段配置必须与 podSubnet 一致支持 IP 池细分配
多网卡机器一般无需额外配置需指定网卡发现方式
网络策略基础支持完整支持
常见失败网段不一致导致跨节点不通选错网卡导致 BGP 不通

Flannel 的问题九成出在网段。清单里的Network字段和你kubeadm-config.yaml里的podSubnet必须一字不差,差一个网段,本节点内 Pod 通信正常,跨节点立刻断,而且kubectl get pods全绿,非常有迷惑性。

Calico 的问题九成出在多网卡。机器上有管理网、存储网、业务网三块网卡时,Calico 默认的自动探测可能挑中存储网,BGP 邻居建不起来,节点之间路由学不到。脚本里要显式指定发现方式,把管理网卡的网段写进去,而不是靠默认值。

# 落地 CNI:先确认网段一致,再 apply grep -n "Network\|podSubnet" ./cni-manifest.yaml # 人工比对一次 kubectl apply -f ./cni-manifest.yaml # 等待 CNI 就绪,而不是立刻 join worker kubectl -n kube-system rollout status daemonset --timeout=180s kubectl -n kube-system get pods -l k8s-app=flannel -o wide

rollout status这行建议写进脚本,很多一键部署包 apply 完 CNI 直接去 join worker,worker 上来时 CNI 还没就绪,kubelet 报network plugin not ready,节点卡在 NotReady 几分钟。虽然最终会自己恢复,但对自动化流程来说,这种不确定性会让后续步骤依赖超时判断,很脆弱。

4.3 验证网络与 DNS 是否真的通了

节点 Ready、Pod Running 都不代表网络可用。我见过太多次集群表面健康,实际 DNS 解析超时、跨节点 Pod 通信丢包。验证要分层做:先看节点间基础连通,再看 Pod 网段路由,最后看 DNS。

# 1. 节点状态与 Pod 分布,重点看是否有节点没跑到 CNI Pod kubectl get nodes -o wide kubectl -n kube-system get pods -o wide | grep -v Running # 2. 起一个临时 Pod 做 DNS 与跨节点解析测试 kubectl run nettest --image=registry.internal.example/tools/dnsutils:latest \ --restart=Never --command -- sleep 3600 kubectl exec nettest -- nslookup kubernetes.default kubectl exec nettest -- nslookup kube-dns.kube-system.svc.cluster.local # 3. 清理 kubectl delete pod nettest --ignore-not-found

nslookup kubernetes.default能解析出 Service 网段的地址,说明 DNS 链路通。如果超时,先看kube-dns的 Pod 是否 Running,再看它的 Endpoints 有没有地址,最后回头看 CNI 是否在所有节点就绪。这个顺序能把问题从最外层一路收窄到具体组件。

在容器节点模式下,验证还要多一步:确认容器内部的路由没有和宿主机冲突。常见做法是从一个节点容器docker exec进去,直接 ping 另一个节点容器里 Pod 的 IP,通不过就说明嵌套网络没打通,这时候调 CNI 参数意义不大,得回头看容器的网络模式配置。

5. 避坑与排查:五条现象、原因和解决

真正耗时间的不是部署,是部署完之后那些看起来毫无关联的症状。下面五条是我在不同环境里反复遇到的,按“现象 → 原因 → 解决”整理,遇到类似表现可以先对照排查,能省掉大量翻日志的时间。

5.1 现象:kubelet 反复重启,日志报 cgroup 相关错误

现象:systemctl status kubelet显示不断重启,journalctl -u kubelet里能看到failed to create cgroup或misconfiguration: kubelet cgroup driver之类的字样,节点始终 NotReady。

原因:Docker 和 kubelet 使用的 cgroup driver 不一致。Docker 默认可能是 cgroupfs,kubelet 新版本默认 systemd,两边对同一个容器的资源管理路径理解不同,创建失败。

解决:两条路一起改。在/etc/docker/daemon.json里写"exec-opts": ["native.cgroupdriver=systemd"],重启 Docker;同时在KubeletConfiguration里写cgroupDriver: systemd,重启 kubelet。改完用docker info | grep -i cgroup和kubectl describe node交叉确认。注意改 Docker 配置前先docker ps看一眼有没有在跑的容器,重启会全部杀掉。

5.2 现象:Pod 卡在 ContainerCreating,事件里是镜像拉取失败

现象:kubectl describe pod的事件里反复出现Failed to pull image或ImagePullBackOff,kubectl get events里能看到超时。

原因:三种可能。一是离线环境里镜像 tag 前缀和 kubelet 请求的不一致;二是私有仓库没配认证,kubelet 拉取时没凭据;三是镜像本身在仓库里不存在,tag 写错。

解决:先crictl images看本地有没有这个镜像,有的话说明是 tag 不匹配,用docker tag改成请求的完整地址;没有的话检查仓库连通性与认证,认证问题通过创建imagePullSecrets挂到 ServiceAccount 上解决。别急着重装集群,镜像问题占这类故障的绝大多数。

5.3 现象:worker 节点 NotReady,CNI 相关 Pod 没起来

现象:kubectl get nodes里 worker 一直是 NotReady,kubectl get pods -n kube-system -o wide能看到 CNI 的 DaemonSet 在这个节点上没有实例。

原因:CNI 的 DaemonSet 依赖节点上的 kubelet 先就绪才能被调度,而 kubelet 又依赖 CNI 才能把节点标记为 Ready,这是经典的先后依赖问题。常见触发点是节点上有污点、CNI 镜像拉不下来、或者节点内核缺少必要的转发模块。

解决:先确认 CNI 镜像在该节点本地存在,再检查节点有没有被误打上NoSchedule污点,最后确认br_netfilter和ip_forward是否开启。这三项排除后重新kubectl rollout restart daemonset -n kube-system,一般就能恢复。

5.4 现象:重复执行脚本时 kubeadm init 报已初始化

现象:第二次跑部署脚本,kubeadm init直接失败,提示集群已存在或端口被占用,脚本停在中途,留下半新半旧的状态。

原因:脚本没有幂等判断,上一次的/etc/kubernetes目录、etcd 数据、CNI 配置都还在。

解决:不要手工删目录了事。正确顺序是kubeadm reset -f,然后清理$HOME/.kube、/etc/cni/net.d、/var/lib/cni、/var/lib/etcd,再执行iptables -F && iptables -t nat -F清理残留规则。整套动作封成一个reset.sh,部署脚本开头检测到已初始化就先调它,或者至少提示用户手动执行。

5.5 现象:集群起来了但集群内 DNS 解析超时

现象:Pod 之间用 IP 能通,用 Service 名解析超时,nslookup kubernetes.default卡住。

原因:多数是kube-proxy的 iptables 规则没生效,或者节点上nf_conntrack模块缺失导致连接跟踪异常;容器节点模式下还可能是宿主机的转发规则覆盖了容器的规则。

解决:kubectl -n kube-system logs看 kube-proxy 有没有报错,节点上lsmod | grep conntrack确认模块加载,iptables -t nat -L KUBE-SERVICES看规则是否存在。容器节点模式下要额外检查宿主机的net.ipv4.ip_forward是否为 1,以及 Docker 的默认网桥是否和 Pod 网段冲突。

6. 把一键脚本改造成可复跑:幂等、干跑与集群体检

一键部署脚本最大的价值不是“一键”,是“可重复”。第一次跑通靠运气,第二次跑通靠设计。我后来所有部署脚本都会加三样东西:幂等判断、干跑模式、体检脚本。

幂等改造集中在三处。第一处是初始化前检查/etc/kubernetes/admin.conf是否存在,存在就跳过 init 直接进验证;第二处是安装包前先查版本,版本一致就不重复装,避免apt操作把已经 hold 住的包解锁;第三处是 join 前检查节点是否已在集群里,用kubectl get node <hostname>判断,已在就跳过。这三处加起来不到二十行,但能把“重复执行导致状态错乱”这个高频问题彻底堵死。

#!/usr/bin/env bash # 幂等判断的三个片段 source ./cluster.conf # 1. 控制面是否已初始化 if [[ -f /etc/kubernetes/admin.conf ]]; then echo "[SKIP] 控制面已初始化,跳过 init" else kubeadm init --config ./kubeadm-config.yaml fi # 2. 包版本是否已匹配 CURRENT=$(kubeadm version -o short 2>/dev/null || echo "none") [[ "${CURRENT}" == "${K8S_VERSION}" ]] && echo "[SKIP] 版本已匹配" || install_runtime # 3. 节点是否已在集群内 if kubectl get node "$(hostname)" >/dev/null 2>&1; then echo "[SKIP] 节点已在集群中" else eval "${JOIN_CMD}" fi

干跑模式的做法是给脚本加DRY_RUN=1,所有写操作前面套一层run()函数,干跑时只打印不执行。这一步在批量环境里特别有用,先在十台机器上跑一遍干跑,把输出里的变量展开结果逐行看一眼,能提前发现 IP 填错、网段冲突这类问题。

体检脚本我一般固定收集五类信息:节点状态与版本偏差、系统组件 Pod 状态、CNI Pod 在各节点的分布、DNS 解析结果、以及kubectl get events里近十分钟的异常事件。这五项拼起来基本能覆盖八成以上的集群问题。体检输出我会存成带时间戳的文件,出问题时对比两次输出,比翻聊天记录靠谱得多。

还有一个我吃过亏的细节:脚本里的set -euo pipefail是双刃剑。它在管道中间某条命令失败时也会退出,看起来很安全,但某些命令失败是预期内的(比如grep没匹配到),不加|| true就会让脚本莫名其妙中断。我现在会在每个可能“正常失败”的地方显式处理,而不是靠全局开关兜底。

从那以后我每次拿到这类一键部署包,都会先通读一遍变量文件和预检段,再在干净机器上跑一次干跑,最后才正式执行——这三步加起来不到十分钟,比事后清理半成品集群省心太多。脚本包我放在原来的位置,按上面的变量表改完再跑,出问题先对照第 5 章的五条现象排一遍,希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进