写在前面:这是我的 K8s 学习之旅的第一站。在完成了 Docker 全链路实践(Docker 化部署考试系统 + 镜像推送阿里云 ACR)之后,我决定在自己的 VMware 虚拟机上搭建一个轻量级 K8s 环境——k3s。这篇文章记录了从安装到部署考试系统过程中遇到的所有问题和解决过程,希望能帮到同样在学 K8s 的同学。

一、环境准备

项目

配置

操作系统

CentOS 7.9(内核 3.10)

虚拟化

VMware Workstation

虚拟机 IP

192.168.120.131

k3s 版本

v1.32.5+k3s1

部署目标

在线考试系统(MySQL + SpringBoot 后端 + Vue 前端)

⚠️ 重要提示:CentOS 7 的内核版本是 3.10,只支持 cgroup v1。k3s 高版本默认使用 cgroup v2,所以我在安装时需要特别注意版本兼容性。最终选择 v1.32.5+k3s1 是经过验证能在 CentOS 7 上稳定运行的版本。

安装命令很简单:

// bash
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="v1.32.5+k3s1" sh -

二、第一个坑:Docker Hub 被墙,镜像拉不下来

问题描述

k3s 安装成功后,我尝试部署一个 Nginx Demo 来练手:

// bash
kubectl run nginx --image=nginx --port=80

结果 Pod 一直卡在 ContainerCreating 或 ImagePullBackOff,kubectl describe pod 显示拉取镜像超时。

解决方案

配置 k3s 的镜像加速。创建 /etc/rancher/k3s/registries.yaml

// yaml
mirrors:
  docker.io:
    endpoint:
      - "https://docker.m.daocloud.io"

重启 k3s 使其生效:

// bash
systemctl restart k3s

之后镜像拉取恢复正常。

学到什么

K8s/k3s 的镜像拉取配置和 Docker 的 daemon.json 不一样,k3s 使用自己的 registry 配置文件。国内环境部署 K8s,镜像加速是第一步要做的

三、第二个坑:Service 的 selector 标签写错了

问题描述

部署了 Nginx Pod 后,我创建了 Service 来暴露它:

// yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx-svc
spec:
  type: NodePort
  selector:
    app: nginx-server    # ← 注意这里
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30080

kubectl get endpoints nginx-svc 显示 <none>,说明 Service 找不到后端的 Pod。

原因

Pod 的标签是 app: nginx,而我 Service 的 selector 写成了 app: nginx-server。K8s 的 Service 通过标签选择器(Label Selector)精确匹配 Pod,差一个字符都匹配不上。

解决

把 selector 改成和 Pod 标签完全一致:

// yaml
selector:
  app: nginx    # 和 Pod 的标签完全匹配

学到什么

K8s 的资源关联全靠标签匹配,没有任何隐式关联。 这是和 Docker Compose 最大的区别之一——在 Compose 里服务名就是 DNS 名,天然互通;在 K8s 里,Service 和 Pod 之间唯一的纽带就是 Label,写错了就是断联。

记住一个调试命令:

// bash
kubectl get endpoints <service-name>

如果 ENDPOINTS 列为空,第一时间检查 selector 是否匹配。

四、第三个坑:磁盘 DiskPressure,节点资源告急

问题描述

部署了几个 Pod 后,kubectl get node 显示节点状态变成了 Ready,SchedulingDisabled,新 Pod 无法调度。kubectl describe node 显示:

Conditions:
  Type           Status
  DiskPressure   True

排查

// bash
df -h

发现根分区使用率超过了 85%,触发了 kubelet 的磁盘压力阈值。

进一步排查发现,之前 Docker 时代残留的 /var/lib/docker 目录还占着 8.7GB 空间。虽然我已经切换到 k3s 了,但 Docker 的数据目录没有被清理。

解决

// bash
# 确认 Docker 已停止
systemctl stop docker

# 清理 Docker 残留数据(8.7GB)
rm -rf /var/lib/docker

# 磁盘使用率从 85%+ 降到 43%
df -h

学到什么

从 Docker 迁移到 K8s 时,旧环境的数据残留是被忽略的重灾区。Docker 的镜像缓存、容器层、volume 数据都在 /var/lib/docker 下,动辄几个 GB。切换运行时后一定要清理,否则会和 k3s 争抢磁盘空间。

kubelet 有三个磁盘相关阈值:

DiskPressure:磁盘使用率超过 85%,停止调度新 Pod

NodeAllocatable:可分配资源的计算

Eviction:超过阈值时会驱逐 Pod

五、第四个坑:CoreDNS 崩溃——Docker 网桥与 k3s Pod 网络冲突

问题描述

这是最棘手的一个问题。某天重启虚拟机后,CoreDNS Pod 一直 CrashLoopBackOff,所有 Pod 内的 DNS 解析全部失败。

排查过程

先看 CoreDNS 的日志:

// bash
kubectl logs -n kube-system coredns-xxx

发现 DNS 插件报错无法绑定端口。然后用 ip addr 查看网络接口:

docker0: 172.17.0.1/16    ← Docker 残留的网桥
cni0: 10.42.0.1/24        ← k3s flannel 的网桥

两个网桥同时存在,而 Docker 的 docker0 网段(172.17.0.0/16)和 k3s 的 Pod 网段存在路由冲突。

解决

// bash
# 删除 Docker 残留的 docker0 接口
ip link delete docker0

# 重启 k3s 让 CoreDNS 重新初始化
systemctl restart k3s

CoreDNS 恢复正常,DNS 解析通过。

学到什么

同一台机器上不能有两套容器网络共存。 Docker 和 k3s 都会创建自己的网桥和 iptables 规则,如果 Docker 没有完全清理干净,它的 docker0 网桥会干扰 k3s 的 flannel 网络。

这也解释了很多人在「Docker 和 k3s 共存」时遇到的各种诡异网络问题。最干净的做法是:决定用 k3s 后就彻底停掉 Docker daemon,删掉 docker0。

六、第五个坑:后端 Service 的 targetPort 写错了

问题描述

部署考试系统的后端时,前端通过 Nginx 反向代理访问后端 API,但所有 API 请求都返回 502。

Nginx 配置中:

// nginx
location /api/ {
    proxy_pass http://backend:9201/;
}

前端请求的是 9201 端口,但后端 Service 的 targetPort 写成了 8080:

// yaml
apiVersion: v1
kind: Service
metadata:
  name: backend
spec:
  selector:
    app: exam-backend
  ports:
    - port: 9201
      targetPort: 8080    # ← 错误!容器实际监听 9201

解决

把 targetPort 改成和容器实际监听的端口一致:

// yaml
ports:
  - port: 9201
    targetPort: 9201      # ← 改成 9201

学到什么

Service 的三个端口概念要分清:

端口

含义

谁在用

port

Service 自身的端口

集群内其他 Pod 访问

targetPort

容器实际监听的端口

Service 转发到这个端口

nodePort

节点上暴露的端口

外部访问用

targetPort 必须和容器内应用实际监听的端口一致,否则流量到了 Pod 但没人接收。

七、第六个坑:MySQL 数据目录残留导致密码不生效

问题描述

考试系统的 MySQL Pod 启动后,后端连接数据库报两个错误:

1. Host '10.42.0.74' is not allowed to connect —— 不允许远程连接

2. Access denied for user 'root'@'localhost' —— 密码错误

原因

MySQL Deployment 中配置了 hostPath: /data/mysql,这个目录在之前 Docker Compose 时代就有数据。MySQL 容器第一次启动时发现数据目录不为空,就跳过了初始化(不会执行 MYSQLROOTPASSWORD 和创建数据库),直接用了旧数据里的密码和权限配置。

解决

// bash
# 停掉 MySQL Pod
kubectl scale deployment mysql --replicas=0

# 清除旧数据
rm -rf /data/mysql/*

# 重新启动
kubectl scale deployment mysql --replicas=1

重新初始化后,MySQL 正确设置了 root 密码(123456),创建了 exam_system 数据库。然后再手动导入初始化 SQL:

// bash
kubectl exec -it mysql-xxx -- mysql -uroot -p123456 exam_system < /root/exam-system-docker/ExamSystem/online_exam.sql

并开启远程访问:

// sql
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456';
FLUSH PRIVILEGES;

学到什么

MySQL 官方镜像的初始化逻辑是:只有数据目录为空时才会执行初始化。如果挂载的 hostPath 里有旧数据,它会直接拿来用,忽略所有环境变量。这在从 Docker Compose 迁移到 K8s 时特别容易踩——你可能忘了清理旧数据。

八、终极 Boss:虚拟机重启后 k3s 网络全挂

这是整个部署过程中耗时最长、最痛苦的问题。

问题现象

VM 重启后:

kubectl get pods 三个考试 Pod 全部 Running ✅

curl localhost:32557 返回 000(连接失败)❌

curl 10.42.0.85:80(Pod IP)也返回 000 ❌

ip link show cni0 → Device does not exist ❌

flannel.1 状态为 DOWN ❌

排查过程

第一步:确认网络接口状态

// bash
ip link show cni0      # 不存在
ip link show flannel.1 # 存在但 state DOWN

第二步:手动拉起接口(无效)

// bash
ip link set flannel.1 up
ip link set cni0 up    # 报错:不存在

第三步:停止 k3s → 删除所有网络接口 → 重启 k3s

cni0 仍然没有被重建。

第四步:查看 CNI 配置文件

// bash
cat /var/lib/rancher/k3s/agent/etc/cni/net.d/10-flannel.conflist

配置文件存在且内容正确(type=flannel, VXLAN 后端),但接口没被创建。

第五步:查看 k3s 启动日志中的 flannel 初始化

// bash
journalctl -u k3s --since "2026-07-20 14:00:00" | grep -iE "flannel|vxlan|subnet"

发现 flannel 初始化日志正常走完:

Starting flannel with backend vxlan
Flannel found PodCIDR assigned for node localhost.localdomain
Interface flannel.1 mac address set to: 1a:0e:db:d4:f2:4b
Starting flannel in iptables mode...
Running flannel backend.

flannel 进程正常工作了,但 cni0 没有被创建。

第六步:检查 flannel 二进制文件

// bash
find /var/lib/rancher/k3s/data/ -name "flannel"
# 找到:/var/lib/rancher/k3s/data/xxx/bin/flannel ✅

第七步:检查 VXLAN 内核模块

// bash
lsmod | grep vxlan
# vxlan 53857 0 ✅ 已加载

第八步:关键发现——dmesg 的线索

// bash
dmesg | grep -i cni0

发现 cni0 曾经有 9 个 veth 端口,后来全部进入 disabled state。这说明 cni0 是存在过的,但在 k3s 停止过程中被拆除了。

第九步:找到根因

k3s 重启后,flannel 恢复了 flannel.1 接口,但 cni0 桥需要等新 Pod 调度时才由 CNI 插件创建。然而:

三个考试 Pod 在网络中断前就已创建,它们的网络命名空间还引用着旧的(已删除的)cni0

k3s 认为这些 Pod 是 Running 的,不会重新调度它们

CNI 插件只在 Pod 创建/沙箱初始化时被调用

所以 cni0 永远不会被重建

同时,kube-system 中的系统 Pod(coredns、metrics-server、traefik)全部 CrashLoopBackOff,也在循环中反复失败。

最终解决

分两步操作:

Step 1:清理残留 Docker 网桥 + 强制系统 Pod 重建

// bash
# 停止 k3s
systemctl stop k3s

# 清理 Docker 残留的网桥(之前只删了 docker0,漏了其他的)
ip link delete br-7c5ddd7c663b
ip link delete br-c9af4eaaac3c

# 清理 flannel 状态缓存
rm -f /run/flannel/subnet.env
rm -rf /var/lib/cni/flannel/*

# 删除残留的 flannel.1
ip link delete flannel.1

# 重启 k3s
systemctl start k3s

重启后 flannel.1 恢复了(UP 状态),但 cni0 仍未创建。

Step 2:删掉 CrashLoopBackOff 的 Pod,触发 CNI 创建桥

// bash
# 删掉循环崩溃的系统 Pod,让 kubelet 重新调度
kubectl delete pod -n kube-system coredns-697968c856-zw2mt
kubectl delete pod -n kube-system metrics-server-6f4c6675d5-l5z4m
kubectl delete pod -n kube-system traefik-c98fdf6fb-xfsmb

新 Pod 被调度 → CNI 插件被调用 → cni0 自动创建成功

// bash
ip link show cni0
# 1872: cni0: <BROADCAST,MULTICAST,UP> mtu 1450
#     inet 10.42.0.1/24 brd 10.42.0.255 scope global cni0

Step 3:删掉旧的业务 Pod,重建网络

// bash
kubectl delete pod exam-backend-xxx exam-frontend-xxx mysql-xxx

新 Pod 获得新的 IP(10.42.0.5/0.6/0.7),接入新的 cni0 桥。

最终验证:

// bash
curl -s -o /dev/null -w "%{http_code}" http://localhost:32557
# 200 ✅

根因总结

CentOS 7 上 k3s 的网络接口(cni0、flannel.1)不是系统持久化接口,它们由 flannel CNI 插件在运行时创建。当 k3s 停止时这些接口会被拆除,但重启后只有当新的 Pod 被调度时 CNI 插件才会重新创建 cni0 桥。如果旧 Pod 仍然被 k3s 认为是 Running 状态,它们不会触发 CNI 重建流程,导致 Pod 虽然 Running 但实际网络不通。

经验教训

1. k3s 网络是"懒加载"的:cni0 不是启动 k3s 就创建,而是第一个新 Pod 调度时才创建

2. 旧 Pod ≠ 有网络:Pod 的 Running 状态只表示容器在运行,不代表网络正常

3. 从 Docker 迁移到 K8s,必须彻底清理 Docker 残留:docker0、残留网桥、/var/lib/docker 一个都不能留

4. 排查网络问题的三板斧ip link show → journalctl -u k3s | grep flannel → kubectl get endpoints

九、最终成果

所有服务正常运行:

// bash
kubectl get pods -A

Namespace

Pod

Status

IP

default

exam-backend

Running

10.42.0.5

default

exam-frontend

Running

10.42.0.6

default

mysql

Running

10.42.0.7

kube-system

coredns

Running

10.42.0.x

kube-system

metrics-server

Running

10.42.0.x

kube-system

traefik

Running

10.42.0.x

浏览器访问 http://192.168.120.131:32557,考试系统登录页面正常显示,账号密码登录成功,全流程验证通过 ✅

十、踩坑清单速查

#

问题

根因

解决方案

1

镜像拉取失败

Docker Hub 被墙

配置 DaoCloud 镜像加速

2

Service 找不到 Pod

selector 标签不匹配

确保 Label 和 Selector 完全一致

3

Pod 无法调度

DiskPressure,磁盘满了

清理 /var/lib/docker 释放 8.7GB

4

CoreDNS 崩溃

docker0 网桥与 flannel 网络冲突

删除 docker0 接口

5

API 返回 502

Service targetPort 和容器端口不一致

targetPort 改成 9201

6

MySQL 密码错误/拒绝远程连接

hostPath 有旧数据,跳过初始化

清空 /data/mysql 重新初始化

7

VM 重启后网络全挂

cni0 桥未被重建,旧 Pod 不触发 CNI

清理残留网桥 + 删除旧 Pod 重建

写在最后

从 Docker Compose 到 K8s,最大的感受是:抽象层次高了,但对底层原理的要求也高了。

在 Docker Compose 里,docker-compose up 一行命令就能把所有东西跑起来,网络、DNS、端口映射都是自动的。但在 K8s 里,每一个环节(标签选择、端口映射、CNI 网络、DNS 解析)都可能成为故障点,你需要理解它们的工作原理才能排查问题。

这次部署让我对 K8s 的网络模型有了很深的理解:Pod 怎么通过 veth pair 接入 cni0 桥、flannel 怎么用 VXLAN 封装跨节点流量、Service 怎么用 iptables 规则做负载均衡、CoreDNS 怎么通过 ClusterIP 提供集群内 DNS。这些在课本上很抽象的概念,在一次次排错中变得具体了。

下一步计划:学习 K8s 的 ConfigMap 和 Secret 管理、StatefulSet 部署有状态服务、以及 Helm 包管理器。继续加油 

本文为原创内容,记录个人 K8s 学习过程。如有错误欢迎指正,转载请注明出处。

更多推荐