90% 的安装失败都卡在 kubeadm init 的预检(preflight)阶段,报错会直接告诉你原因,逐条修复即可:
| 报错关键词 | 根因 | 解决方案 |
|---|---|---|
[ERROR Swap]: running with swap on | 没关 swap,kubelet 起不来 | swapoff -a + 注释 /etc/fstab swap 行 |
[ERROR DirAvailable-/var/lib/etcd]: is not empty | 上次 init 失败的数据残留 | kubeadm reset -f 后清空 /var/lib/etcd/* |
[ERROR Port-10250]: already in use | 端口被老 kubelet/别的进程占用 | ss -lntp | grep 10250 找到并停掉 |
failed to pull image ... no space left on device | 磁盘满,组件镜像拉不下来 | 按第 02 节清磁盘,再重跑 init |
container runtime is not running | containerd 没起 / sock 路径配错 | systemctl enable --now containerd,用 crictl ps 验证 |
connect: connection refused (6443) | apiserver 没起来 | journalctl -u kubelet -n 100 看真实原因 |
# 1. 关闭 swap(新手第一大坑,处理完重启也不会复活)
swapoff -a
sed -ri 's@^(/.*\sswap\s.*)@#\1@' /etc/fstab
free -h # Swap 一行必须全是 0
# 2. 容器运行时自检
systemctl enable --now containerd
crictl ps # 能正常执行(哪怕空列表)说明 runtime OK
# 3. 上次失败后的"彻底重来"(试错阶段最常用)
kubeadm reset -f
rm -rf /var/lib/etcd/* /etc/cni/net.d/* $HOME/.kube/config
iptables -F && ipvsadm -C 2>/dev/null
# 4. 重新 init(image-repository 建议直接给国内源,避免第 05 节的坑)
kubeadm init \
--image-repository=registry.aliyuncs.com/google_containers \
--pod-network-cidr=10.244.0.0/16 \
--service-cidr=10.96.0.0/12
# 5. init 成功后按提示三连(否则 kubectl 连不上)
mkdir -p $HOME/.kube && cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
kubectl get nodes # 能打印节点表即成功
kubeadm reset 只回滚集群组件,不会删除 PVC 数据、NFS 目录和手动装的 CNI 二进制;重装前请单独确认。节点磁盘可用率低于阈值(默认 nodefs.available<10%、imagefs.available<15%)时,kubelet 会把节点标记为 DiskPressure 并驱逐 Pod。
# 节点被打了磁盘压力标签
kubectl describe node qingfeng | grep -A2 DiskPressure
DiskPressure True KubeletHasDiskPressure kubelet has disk pressure
# 大批 Pod 被驱逐(Evicted)
kubectl get pods -A | grep -i evicted
harbor harbor-core-5d7c9f8b-x2k9p Evicted 0 23h
# 应用写文件直接报错(数据库最常见,见第 06 节)
IOError: [Errno 28] No space left on device
# 1. 总览:运行时与 kubelet 所在分区
df -h /var/lib/containerd /var/lib/kubelet
# 2. 三大户逐一定位(镜像层 / 容器日志 / journald)
du -h --max-depth=1 /var/lib/containerd | sort -rh | head
du -h --max-depth=1 /var/log/pods | sort -rh | head # 容器 stdout 日志
journalctl --disk-usage
# 1. 删"没有任何 Pod 在用"的镜像(最安全、见效最快)
crictl rmi --prune
# 2. 收缩 journald 系统日志,只保留 3 天
journalctl --vacuum-time=3d
# 3. 清理已被驱逐/失败的 Pod 残留
kubectl delete pods -A --field-selector=status.phase=Failed
# 4. 长效:限制单容器日志体积(/var/lib/kubelet/config.yaml)
containerLogMaxSize: 50Mi
containerLogMaxFiles: 3
# 改完 systemctl restart kubelet
# /var/lib/kubelet/config.yaml
evictionHard:
nodefs.available: "5%"
imagefs.available: "5%"
# systemctl restart kubelet 后 DiskPressure 会自动恢复为 False
rm -rf /var/lib/containerd/io.containerd.* 里的目录 —— 会把正在用的镜像/快照删坏;删镜像必须走 crictl rmi。Pod 卡在 ContainerCreating 十有八九是挂载问题,Events 里必有答案:
kubectl describe pod mysql-0 | sed -n '/Events:/,$p'
kubectl get events -A --sort-by=.lastTimestamp | grep -iE 'volume|mount|attach'
# PVC 是否 Bound?Pending 多半是 PV/StorageClass 没配好
kubectl get pvc -A
| Events 报错 | 根因 | 解决方案 |
|---|---|---|
mount.nfs: access denied by server | NFS exports 白名单没放行 / root_squash | 编辑 /etc/exports 加节点网段 + no_root_squash,exportfs -rv |
mount: wrong fs type, bad option | 节点没装 NFS 客户端 | yum install -y nfs-utils(Debian 系:nfs-common) |
hostPath path check failed ... not exist | 宿主机目录不存在 | 提前 mkdir -p;或 hostPath type 用 DirectoryOrCreate |
Multi-Attach error for volume ... exclusively attached | RWO 卷还被旧 Pod(或残留 attachment)占着 | 删旧 Pod;kubectl get volumeattachments 查残留并删 |
subPath ... is not found | subPath 指向的文件/目录不存在 | initContainer 先创建;kubectl explain pod.spec.containers.volumeMounts.subPath |
| ConfigMap 改了 Pod 里不生效 | subPath 挂载不会热更新(设计如此) | 重启 Pod;或改用目录挂载(自动更新,延迟 ~1 分钟) |
# NFS 服务端标准配置(/etc/exports)示例
/data/nfs/mysql *(rw,sync,no_subtree_check,no_root_squash)
# 服务端生效 + 客户端验证(节点上先手工 mount 一次,能通 K8S 里才能通)
exportfs -rv
mount -t nfs 192.168.10.10:/data/nfs/mysql /mnt && ls /mnt && umount /mnt
# 现象:Pod 反复 CrashLoopBackOff,logs 里是经典的 permission denied
kubectl logs mysql-0 -n prod
mkdir: cannot create directory '/var/lib/mysql': Permission denied
# 解决:Pod securityContext 加 fsGroup(让 kubelet 把卷 chown 给该组)
# fsGroup 的值 = 镜像内运行业务的系统用户组 id(mysql 官方镜像为 999)
spec:
securityContext:
fsGroup: 999
runAsUser: 999
runAsGroup: 999
# 快速判断:临时关闭后能写了,就是 SELinux 的锅
setenforce 0 # 临时 Permissive(重启失效)
# 正确做法(别长期关闭):给目录打容器标签
chcon -Rt svirt_sandbox_file_t /data/nfs/mysql
# 永久生效用 semanage fcontext -a -t svirt_sandbox_file_t "/data/nfs/mysql(/.*)?" && restorecon -Rv /data/nfs/mysql
| 报错 | 根因 | 解决方案 |
|---|---|---|
Forbidden: User "system:anonymous" cannot get ... | kubeconfig 没带凭证 / 权限未绑定 | 检查 KUBECONFIG;给 SA 绑 Role/ClusterRole |
x509: certificate has expired | 集群证书到期(常伴随节点时间不准) | ntpdate 校时;kubeadm certs renew all + 重启控制面 |
You must be logged in to the server (Unauthorized) | client 证书/ token 失效 | 重新分发 admin.conf 或重新签发用户证书 |
# 权限自检两连:我是谁 / 我能不能
kubectl config current-context
kubectl auth can-i get pods -n prod # 回答 yes/no,一目了然
kubectl auth can-i '*' '*' --as=system:serviceaccount:prod:deploy-sa
# 看证书还剩多久
kubeadm certs check-expiration
Permission denied → 先 fsGroup;节点上手工写也失败 → 查 SELinux/目录属主;kubectl 报 Forbidden/Unauthorized → 查 RBAC/证书。三类权限别混。kubectl get pods -n web
nginx-6b7f5d8c-x9k2p 0/1 ImagePullBackOff 0 3m
kubectl describe pod nginx-6b7f5d8c-x9k2p -n web | grep -A3 "Failed to pull"
Failed to pull image "nginx:1.27": failed to pull and unpack ... i/o timeout
| Events 里的报错 | 根因 | 跳转方案 |
|---|---|---|
manifest unknown / not found | 镜像名或 Tag 写错 | 方案 A |
i/o timeout / no route to host | 国内网络拉不到 Docker Hub | 方案 B(加速器) |
toomanyrequests / rate limited | Docker Hub 限流 | 方案 B / C |
unauthorized: authentication required | 私有仓库没带凭证 | 方案 D(imagePullSecrets) |
exec format error | 架构不匹配(x86 机器拉了 arm64 镜像) | 换 nginx:1.27-amd64 类带架构 Tag |
# 在任意节点上手工拉一次,报 not found 就是名字/Tag 写错
crictl pull nginx:1.27
# 1. /etc/containerd/config.toml 启用 certs.d 目录(v2 推荐方式)
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"
# 2. 给 docker.io 配加速源:/etc/containerd/certs.d/docker.io/hosts.toml
server = "https://docker.io"
[host."https://docker.m.daocloud.io"]
capabilities = ["pull", "resolve"]
# ★ 加速器对某些仓库(如 bitnamilegacy)可能挂起/不回源 → pull 卡住不动:
# 先 rm hosts.toml 恢复直连(机器 curl 通 docker.io 就直连可拉);
# 直连也不行再换源:docker.1ms.run / dockerproxy.net 等,改 [host."..."] 后重启 containerd
# 3. 重启生效(改了配置不重启 = 白改)
systemctl restart containerd
crictl pull nginx:1.27 # 现在应该秒拉
# 4. sandbox/pause 镜像也超时的话,init 第一步就卡死 —— 顺手改同文件里
# sandbox_image = "registry.aliyuncs.com/google_containers/pause:3.10"
# 有网机器上导出,拷进内网节点导入(注意必须是 k8s.io 命名空间)
ctr -n k8s.io images import nginx-1.27.tar
crictl images | grep nginx # 能看到即成功,Pod 会自动重试拉起
# 1. 建拉取凭证(用户名里 $ 记得转义,Robot 账号常见)
kubectl create secret docker-registry regcred -n web \
--docker-server=harbor.qingfeng.local \
--docker-username='robot$ci' \
--docker-password='Harbor12345'
# 2. Pod/Deployment spec 里引用
spec:
imagePullSecrets:
- name: regcred
kubectl delete pod xxx 重建最快。数据库是最怕磁盘满的工作负载:一旦写不进 binlog/数据文件,MySQL 会把自己挂起来,业务直接中断。处理顺序永远是先止血保服务,再扩容,最后防复发。
# 1. PVC 使用率告警
kubectl get pvc data-mysql -n prod
NAME STATUS VOLUME CAPACITY ...
data-mysql Bound pvc-8f3.. 20Gi
# 2. 进容器看实际占用(看挂载点,不是根分区)
kubectl exec -it mysql-0 -n prod -- df -h /var/lib/mysql
/dev/sdb 20G 18.5G 1.5G 93% /var/lib/mysql
# 3. 是数据涨了还是日志涨了?
kubectl exec -it mysql-0 -n prod -- sh -c 'du -sh /var/lib/mysql/* | sort -rh | head'
# binlog.0000xx 动辄几十 G —— 最常见的元凶
# ibdata1 / ib_logfile0 —— 真实数据/redo,不能删
# 1. binlog 立刻瘦身:只保留最近 2 天
kubectl exec -it mysql-0 -n prod -- \
mysql -uroot -p"$PW" -e "PURGE BINARY LOGS BEFORE NOW() - INTERVAL 2 DAY;"
# 2. 慢日志/错误日志轮转(>> 清空不换句柄,不断连接)
kubectl exec -it mysql-0 -n prod -- sh -c '> /var/log/mysql/slow.log'
# 3. 临时表/备份残留确认后再删(先看时间戳,别一把梭)
# 前提 1:StorageClass 必须允许扩容
kubectl get sc
NAME PROVISIONER ALLOWVOLUMEEXPANSION
nfs-sc nfs-provisioner true ← 允许,继续
local-sc kubernetes.io/no-provisioner false ← 本地盘不支持在线扩,只能迁移
# 直接 patch 目标容量(只能扩大,永远不能缩小!)
kubectl patch pvc data-mysql -n prod \
-p '{"spec":{"resources":{"requests":{"storage":"50Gi"}}}}'
# 盯进度:先等 controller 扩 PV,再等文件系统在线 expand(xfs/ext4 均支持)
kubectl get pvc data-mysql -n prod -w
# 容器内验证(无需重启 Pod)
kubectl exec -it mysql-0 -n prod -- df -h /var/lib/mysql
/dev/sdb 50G 18.5G 32G 37% /var/lib/mysql
# 1. binlog 自动过期(my.cnf [mysqld],3 天)
binlog_expire_logs_seconds = 259200
max_binlog_size = 512M
# 2. 容器日志限制(见第 02 节 containerLogMaxSize)
# 3. 监控告警阈值提前到 75%,别等 90% 才动手
/var/lib/mysql 下的 ibdata1、binlog.index 等文件(想清 binlog 用 SQL PURGE);② PVC 扩容只能扩大,缩容不支持;local PV / hostPath 无法在线扩,唯一路径是"新 PVC + 逻辑备份迁移 + 切换"。不管什么问题,先跑这三条,80% 的答案就在输出里:
kubectl describe pod <pod> -n <ns> # Events 区 = 病历本
kubectl logs <pod> -n <ns> --tail=100 --previous # 应用自己的报错(--previous 看上一次崩溃)
kubectl get events -A --sort-by=.lastTimestamp | tail -20 # 全集群最近发生了什么
| Pod 状态 | 大概率方向 | 跳转 |
|---|---|---|
ImagePullBackOff / ErrImagePull | 镜像拉取失败 | 第 05 节 |
ContainerCreating 卡住 | 挂载失败 / 拉镜像慢 | 第 03 节 / 05 |
CrashLoopBackOff | 权限 / 配置 / 应用启动报错 | 第 04 节 + logs |
Evicted | 磁盘 / 内存压力驱逐 | 第 02 节 |
Pending | 资源不足 / PVC 没绑定 / 节点选择器不匹配 | 第 03 节 |
CreateContainerConfigError | secret/configmap/subPath 不存在 | 第 03 节 |
# 1. 最多能跑几个:读节点 status.allocatable.pods(kubelet 上报的可分配上限)
kubectl get node qingfeng -o jsonpath='{.status.allocatable.pods}'; echo
110
# 2. 已经用了多少:describe 的 Allocated resources 段,盯 pods 那一行
kubectl describe node qingfeng | grep -A8 'Allocated resources'
Resource Requests Limits
cpu 850m (21%) 300m (7%)
memory 690Mi (9%) 512Mi (7%)
ephemeral-storage 0 (0%) 0 (0%)
pods 7 (6%) 0 (0%) ← 已分配 7 个 / 上限 110
# 附加:所有节点容量一览表
kubectl get nodes -o custom-columns='NODE:.metadata.name,MAX-PODS:.status.allocatable.pods'
NODE MAX-PODS
qingfeng 110
# 多节点集群:全部节点的 Pod 容量"总和"(jsonpath 逐行取出再 awk 累加)
kubectl get nodes -o jsonpath='{range .items[*]}{.status.allocatable.pods}{"\n"}{end}' \
| awk '{s+=$1} END{print "集群 Pod 总容量:", s}'
# 明细 + 合计一起看(表格末尾追加 TOTAL 行)
kubectl get nodes -o custom-columns='NODE:.metadata.name,MAX-PODS:.status.allocatable.pods' \
| awk 'NR>1{print; s+=$2} END{print "--------"; printf "TOTAL\t%d\n", s}'
# 对比"已经用了多少":集群当前 Pod 总数 / 只数 Running
kubectl get pods -A --no-headers | wc -l
kubectl get pods -A --field-selector=status.phase=Running --no-headers | wc -l
--max-pods(即 KubeletConfiguration 的 maxPods: 110),kubelet 启动时把它写进 status.capacity.pods 并上报 apiserver —— 这是社区经验值,兼顾每个 Pod 的进程/内存开销。100) —— 所以别硬编码 110,以 allocatable.pods 查询结果为准;"集群总容量" = 各节点 allocatable.pods 逐个相加(见上方 jsonpath+awk 命令)。min(maxPods, 剩余 CPU requests, 剩余内存 requests, CNI 剩余 IP)。所以即便 pods 显示 (6%) 很空闲,CPU 100% 照样 Pending(见第 07 节状态表)。# 1. 先确认当前生效值(节点上的 kubelet 配置)
grep maxPods /var/lib/kubelet/config.yaml
maxPods: 110
# 2. 改成目标值(如 200)
sed -i 's/^maxPods:.*/maxPods: 200/' /var/lib/kubelet/config.yaml
# 3. 重启 kubelet 生效(节点上执行,秒级,不影响已运行 Pod)
systemctl restart kubelet
# 4. 验证:上报的 allocatable.pods 已变成新值
kubectl get node qingfeng -o jsonpath='{.status.allocatable.pods}'; echo
200
kubectl get node qingfeng 查的是节点(一台真实的机器,本集群单节点就叫 qingfeng) —— maxPods 是节点属性,所以要 get node;而 namespace 是集群里的逻辑分区(default / kube-system / harbor ...),只负责把资源分组隔离,和节点没有包含关系。
| node 节点 | namespace 命名空间 | |
|---|---|---|
| 是什么 | 一台跑容器的机器(kubelet 所在) | 集群内的逻辑分组/虚拟小集群 |
| 典型名字 | qingfeng、worker-01 | default、kube-system、harbor、prod |
| 查看命令 | kubectl get nodes | kubectl get ns |
| Pod 数量上限 | maxPods(kubelet,默认 110) | ResourceQuota 的 pods: N(不配则不限) |
# 查看某个命名空间
kubectl get ns # 列出全部命名空间
kubectl get ns harbor -o wide # 看某个 ns 的状态
kubectl describe ns harbor # 详情:标签、资源配额(Quota)、状态
# "看一个 ns 里的东西"(最常见诉求,用 -n 指定)
kubectl get pods -n harbor # 该 ns 的 Pod
kubectl get all -n harbor # Pod/Svc/Deploy 一次全看
kubectl get pods -A # 想全看就去掉 -n 加 -A
# ns 级别的 Pod 数上限 = ResourceQuota(不建就没有限制)
kubectl get resourcequota -n harbor
# 给 harbor ns 限制最多 50 个 Pod:
kubectl create quota pod-limit -n harbor --hard=pods=50
get node,查后者 get ns,查"某个组里的资源"要加 -n。