从 Docker Compose 到 K3s:逐浪笔记首次完整部署与故障排查实战

文章目录

    • 一、实践背景与目标
      • [1.1 实践目标](#1.1 实践目标)
      • [1.2 安全边界](#1.2 安全边界)
    • 二、部署架构与核心原理
      • [2.1 最终部署架构](#2.1 最终部署架构)
    • [三、准备镜像:从 Docker 到 containerd](#三、准备镜像:从 Docker 到 containerd)
      • [3.1 为什么要处理镜像导入?](#3.1 为什么要处理镜像导入?)
      • [3.2 镜像导出与导入流程](#3.2 镜像导出与导入流程)
      • [3.3 为什么 Deployment 使用 `imagePullPolicy: Never`?](#3.3 为什么 Deployment 使用 imagePullPolicy: Never?)
    • [四、准备独立的 MySQL 测试数据库](#四、准备独立的 MySQL 测试数据库)
      • [4.1 为什么不直接让 K3s 连接线上数据库?](#4.1 为什么不直接让 K3s 连接线上数据库?)
      • [4.2 初始化测试数据库的思路](#4.2 初始化测试数据库的思路)
      • [4.3 使用 ConfigMap、Secret 和 PVC](#4.3 使用 ConfigMap、Secret 和 PVC)
      • [4.4 为什么需要 PVC?](#4.4 为什么需要 PVC?)
      • [4.5 初始化结果](#4.5 初始化结果)
    • [五、部署 Spring Boot 后端](#五、部署 Spring Boot 后端)
      • [5.1 后端配置](#5.1 后端配置)
      • [5.2 为什么需要覆盖 JDBC URL?](#5.2 为什么需要覆盖 JDBC URL?)
      • [5.3 为什么不能使用 localhost?](#5.3 为什么不能使用 localhost?)
      • [5.4 后端部署后的检查](#5.4 后端部署后的检查)
    • [六、核心故障:前端 Nginx 多次启动失败](#六、核心故障:前端 Nginx 多次启动失败)
      • [6.1 故障现象](#6.1 故障现象)
      • [6.2 关键诊断命令](#6.2 关键诊断命令)
      • [6.3 `host not found in upstream` 到底是什么意思?](#6.3 host not found in upstream 到底是什么意思?)
      • [6.4 为什么先排查 Service,而不是直接修改 Nginx?](#6.4 为什么先排查 Service,而不是直接修改 Nginx?)
      • [6.5 创建后端 Service 后,如何验证?](#6.5 创建后端 Service 后,如何验证?)
      • [6.6 这次故障的根因与解决思路](#6.6 这次故障的根因与解决思路)
    • [七、验证前端与后端 API 的完整链路](#七、验证前端与后端 API 的完整链路)
      • [7.1 检查 Pod 状态](#7.1 检查 Pod 状态)
      • [7.2 为什么使用端口转发?](#7.2 为什么使用端口转发?)
      • [7.3 验证前端页面](#7.3 验证前端页面)
      • [7.4 验证后端 API](#7.4 验证后端 API)
      • [7.5 为什么这次验证比 `kubectl get pods` 更重要?](#7.5 为什么这次验证比 kubectl get pods 更重要?)
    • 八、这次实践使用到的主要排障命令
      • [8.1 查看 Pod 与 Service](#8.1 查看 Pod 与 Service)
      • [8.2 查看容器日志](#8.2 查看容器日志)
      • [8.3 查看 Deployment 的详细状态](#8.3 查看 Deployment 的详细状态)
      • [8.4 查看后端数据库连接情况](#8.4 查看后端数据库连接情况)
      • [8.5 验证真实 API](#8.5 验证真实 API)
    • 九、从这次实践中总结出的技术原理
      • [9.1 Kubernetes 不保证应用按照依赖顺序启动](#9.1 Kubernetes 不保证应用按照依赖顺序启动)
      • [9.2 容器重启不等于问题已经解决](#9.2 容器重启不等于问题已经解决)
      • [9.3 Service 名称比 Pod IP 更适合作为服务依赖](#9.3 Service 名称比 Pod IP 更适合作为服务依赖)
      • [9.4 Service 存在不代表应用一定可用](#9.4 Service 存在不代表应用一定可用)
      • [9.5 `Running` 与 `Ready` 的含义不同](#9.5 Running 与 Ready 的含义不同)
    • 十、面试中如何讲清楚这个案例?
      • [10.1 一分钟左右的回答](#10.1 一分钟左右的回答)
      • [10.2 面试官继续追问:为什么 Nginx 会启动失败?](#10.2 面试官继续追问:为什么 Nginx 会启动失败?)
      • [10.3 面试官继续追问:你如何确认不是后端代码故障?](#10.3 面试官继续追问:你如何确认不是后端代码故障?)
      • [10.4 面试官继续追问:下一步会怎样改进?](#10.4 面试官继续追问:下一步会怎样改进?)
    • 十一、后续改进计划
    • 十二、最终复盘

实践日期: 2026 年 10 月 17 日
项目: 逐浪笔记(my-blog)
实践目标: 在不影响现有 Docker Compose 线上服务的前提下,将博客系统部署到单节点 K3s 集群,建立独立的测试数据库,验证前端、后端、数据库之间的访问链路,并记录真实的故障排查过程。

关键词:K3s、Kubernetes、Docker、containerd、Deployment、Service、ClusterIP、DNS、Nginx、Spring Boot、MySQL、PVC、ConfigMap、Secret、故障排查。


一、实践背景与目标

之前,逐浪笔记运行在 Docker Compose 环境中,由 Jenkins 负责代码拉取、构建、镜像制作和部署。

为了积累 Kubernetes 实战经验,同时准备 DevOps 运维岗位面试,我决定在现有服务器上增加一套 K3s 测试环境。

这次实践的重点不是立即把线上服务迁移到 Kubernetes,而是先完成一条可以独立验证的部署链路。

1.1 实践目标

  1. 安装并使用单节点 K3s 集群。
  2. 将现有博客前端、后端 Docker 镜像导入 K3s 使用的 containerd。
  3. 在 K3s 中部署独立 MySQL 测试数据库。
  4. 在测试数据库中初始化表结构和 Flyway 迁移历史。
  5. 部署 Spring Boot 后端,并验证数据库连接。
  6. 部署 Vue/Nginx 前端,配置后端 API 代理。
  7. 排查前端 Nginx 启动失败的问题。
  8. 通过 HTTP 请求验证前端与后端之间的实际调用链路。

1.2 安全边界

这是本次实践的重要前提。

现有 Docker Compose 服务继续运行,不进行线上流量切换,也不直接修改线上数据库。

项目 现有 Compose 环境 K3s 测试环境
前端 my-blog-frontend:a4ed36f 同版本镜像
后端 my-blog-backend:a4ed36f 同版本镜像
数据库 MySQL 8.0.46,数据库 blog MySQL 8.0.46,数据库 blog_k8s_test
网络 Docker Compose 网络 blog-net Kubernetes Pod 网络及 Service 网络
访问入口 现有线上入口 通过 kubectl port-forward 临时验证
目的 保持现有服务稳定 学习部署、排障与验证

安全原则:先并行部署,再独立验证,最后才讨论迁移。

测试环境使用单独的数据库和 PVC,不直接复用线上 MySQL 数据目录。本次不涉及线上服务停机、端口 80/443 抢占或生产数据库迁移。


二、部署架构与核心原理

2.1 最终部署架构

text 复制代码
浏览器 / curl
      |
      | kubectl port-forward
      | 本地 18080 -> Service 80
      v
K3s 前端 Service
      |
      v
Vue + Nginx Pod
      |
      | /api/ 代理到 my-blog-backend:8080
      v
Spring Boot 后端 Service
      |
      v
Spring Boot Backend Pod
      |
      | JDBC
      v
MySQL Service: blog-k8s-mysql:3306
      |
      v
MySQL Pod
      |
      v
独立测试数据库 blog_k8s_test

这张架构图中有几个需要特别理解的地方。

第一,Pod IP 不应该被当作固定地址。

本次检查时,Pod IP 包括:

  • MySQL Pod:10.42.0.216
  • Backend Pod:10.42.0.217
  • Frontend Pod:10.42.0.214

这些是当时的实际地址,并不保证重建 Pod 后仍然不变。应用之间应通过 Kubernetes Service 名称访问对方,而不是把 Pod IP 写死在配置中。

第二,Service 为 Pod 提供稳定的访问入口。

本次后端 Service 的信息为:

  • Service 名称:my-blog-backend
  • 类型:ClusterIP
  • ClusterIP:10.43.85.16
  • Service 端口:8080
  • 实际后端 Endpoint:10.42.0.217:8080

Service 将稳定的虚拟访问地址与后端 Pod 关联起来。Pod 替换后,只要新的 Pod 符合选择器且就绪,Service 就可以更新其后端 Endpoint。

第三,K3s 使用的镜像运行时与 Docker CLI 不是同一套镜像存储。

即使在服务器上执行 docker images 能看到某个镜像,也不能假设 K3s 一定能直接使用它。

本次需要将镜像导入 K3s 的 containerd 镜像存储,并在 Deployment 中设置适当的镜像拉取策略。


三、准备镜像:从 Docker 到 containerd

3.1 为什么要处理镜像导入?

博客原本运行在 Docker Compose 环境中,已有构建好的镜像:

text 复制代码
my-blog-backend:a4ed36f
my-blog-frontend:a4ed36f

但 K3s 使用 containerd 运行 Pod。

因此,镜像从 Docker 构建出来之后,还需要让 K3s 所使用的 containerd 能够识别它们。

3.2 镜像导出与导入流程

此前实践中,先从现有后端容器导出了 JAR 文件,用于检查实际打包配置:

bash 复制代码
docker cp my-blog-backend:/app/app.jar /tmp/my-blog-app.jar

随后使用 Docker 镜像归档和 K3s containerd 导入流程,将前后端镜像导入 K3s。

常见操作形式如下,具体导出文件名应以实际生成的文件为准:

bash 复制代码
# 查看 Docker 中已有的镜像
docker images | grep -E 'my-blog-(backend|frontend)'

# 示例:将指定镜像导出为归档文件
docker save my-blog-backend:a4ed36f -o /tmp/my-blog-backend.tar
docker save my-blog-frontend:a4ed36f -o /tmp/my-blog-frontend.tar

# 将归档文件导入 K3s 的 containerd
sudo k3s ctr images import /tmp/my-blog-backend.tar
sudo k3s ctr images import /tmp/my-blog-frontend.tar

# 检查 containerd 中的镜像
sudo k3s ctr images list | grep my-blog

以上命令展示的是通用操作方式,记录时应以实际执行的导出文件和镜像标签为准。

3.3 为什么 Deployment 使用 imagePullPolicy: Never?

本次部署使用本地已经导入的镜像,因此 Deployment 中采用:

yaml 复制代码
image: docker.io/library/my-blog-backend:a4ed36f
imagePullPolicy: Never

其含义是:只使用本地已有镜像,不尝试从远程仓库拉取。

优点是适合当前离线或本地镜像验证场景,不依赖外部仓库。

缺点是镜像不存在时,Pod 无法通过自动拉取来补救,通常会出现镜像不可用相关错误。

在后续更正式的 CI/CD 实践中,可以考虑使用镜像仓库、不可变版本标签以及适当的拉取策略。


四、准备独立的 MySQL 测试数据库

这是本次实践中非常重要的一步。

4.1 为什么不直接让 K3s 连接线上数据库?

如果只是为了验证 Kubernetes 部署,直接让新环境连接线上数据库,会带来不必要的风险:

  • 测试程序可能修改线上数据。
  • Flyway 可能尝试执行数据库迁移。
  • 错误配置可能导致数据读写异常。
  • 后续测试故障注入时,可能影响线上业务。

因此,本次为 K3s 单独创建了数据库 blog_k8s_test。

4.2 初始化测试数据库的思路

原项目使用 Flyway 管理数据库迁移。

此前检查发现,V1 迁移文件主要是 baseline 注释,而后续 V2--V10 才包含增量迁移。不能简单假设一个完全空白的数据库只靠执行这些增量迁移,就一定能建立出完整的应用表结构。

本次采取的办法是:

  1. 检查现有数据库表结构与 Flyway 历史。
  2. 导出测试需要的表结构。
  3. 将结构初始化脚本与相应 Flyway 历史记录准备好。
  4. 为测试环境使用独立数据库名。
  5. 在 K3s MySQL 中导入初始化内容。
  6. 检查表结构及迁移历史是否符合预期。

需要强调:本次复用的是数据库结构和迁移历史,不是将线上业务数据整体复制到测试库。

4.3 使用 ConfigMap、Secret 和 PVC

本次 K3s MySQL 使用了以下 Kubernetes 资源:

资源 名称 用途
Deployment blog-k8s-mysql 运行 MySQL 容器
Service blog-k8s-mysql 为后端提供稳定数据库访问地址
PVC blog-k8s-mysql-data 持久化数据库文件
ConfigMap blog-k8s-init 提供初始化 SQL
Secret blog-k8s-mysql 保存 MySQL root 密码

PVC 为 2Gi,使用 local-path StorageClass,状态为 Bound。

4.4 为什么需要 PVC?

容器文件系统通常不适合单独承担数据库的持久化职责。容器被重建后,容器内部未持久化的数据可能丢失。

PVC 为 Pod 提供持久化存储声明,使数据库数据能够存放在独立于容器生命周期的存储卷中。

不过,PVC 不等于完整备份方案。生产环境还需要考虑备份、恢复验证、存储可用性以及节点故障等问题。

4.5 初始化结果

在本次验证中,测试库包含以下 8 张表:

text 复制代码
article
article_comment
article_like
article_tag
category
flyway_schema_history
sys_user
tag

Flyway 历史中 V1--V10 均显示成功,数据库结构版本为 10。

当时 article 和 sys_user 表的记录数均为 0,符合独立测试库没有业务数据的预期。

安全结论:本次后端连接的数据库是 blog_k8s_test,不是线上数据库 blog。


五、部署 Spring Boot 后端

5.1 后端配置

后端使用已有镜像:

text 复制代码
docker.io/library/my-blog-backend:a4ed36f

容器监听端口为 8080。

部署时,设置了以下关键环境变量:

yaml 复制代码
env:
  - name: SPRING_PROFILES_ACTIVE
    value: prod

  - name: MYSQL_HOST
    value: blog-k8s-mysql

  - name: SPRING_DATASOURCE_URL
    value: "jdbc:mysql://blog-k8s-mysql:3306/blog_k8s_test?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai"

  - name: SPRING_DATASOURCE_USERNAME
    value: root

  - name: SPRING_DATASOURCE_PASSWORD
    valueFrom:
      secretKeyRef:
        name: blog-k8s-mysql
        key: MYSQL_ROOT_PASSWORD

  - name: TZ
    value: Asia/Shanghai

其中,数据库密码通过 Secret 引用,而不是直接写入 Deployment YAML。

正式生产部署还应考虑使用独立的应用数据库账号,并通过最小权限原则限制访问范围。

5.2 为什么需要覆盖 JDBC URL?

项目的生产配置使用了类似下面的 URL:

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://${MYSQL_HOST}:3306/blog?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai

注意这里默认连接的是数据库 blog。

但 K3s 测试环境的目标是 blog_k8s_test。因此部署时需要通过 SPRING_DATASOURCE_URL 显式覆盖数据库 URL。

这一步很关键:仅仅将 MYSQL_HOST 修改为新的 Service 名称,并不会自动改变 JDBC URL 中的数据库名称。

如果忘记覆盖,后端可能连接到错误的数据库,即使数据库主机名已经正确。

5.3 为什么不能使用 localhost?

在容器内部,localhost 指的是当前容器自身,而不是宿主机,也不是另一个 Pod。

因此,以下地址不适合在后端 Pod 中连接 MySQL:

text 复制代码
jdbc:mysql://localhost:3306/blog_k8s_test

正确做法是使用数据库 Service:

text 复制代码
jdbc:mysql://blog-k8s-mysql:3306/blog_k8s_test

在同一个 Namespace 中,后端可以通过 blog-k8s-mysql 访问对应 Service。

5.4 后端部署后的检查

执行:

bash 复制代码
kubectl get pods,svc -n my-blog-k8s

查看后端日志:

bash 复制代码
kubectl logs deployment/my-blog-backend \
  -n my-blog-k8s --tail=100

检查 Service 后端地址:

bash 复制代码
kubectl get endpoints my-blog-backend \
  -n my-blog-k8s

本次实际看到的 Endpoint 为:

text 复制代码
10.42.0.217:8080

后端日志中确认了以下关键结果:

  • Spring Boot 激活 prod 配置。
  • Hikari 数据库连接池启动成功。
  • JDBC 连接指向 blog-k8s-mysql:3306/blog_k8s_test。
  • Flyway 校验迁移记录,数据库版本为 10。
  • Tomcat 在 8080 端口启动。
  • BlogBackendApplication 启动成功。

这说明后端应用本身已经正常启动,且能够连接独立测试数据库。


六、核心故障:前端 Nginx 多次启动失败

这是本次实践最有价值的故障排查案例。

6.1 故障现象

前端 Pod 长时间处于:

text 复制代码
CrashLoopBackOff

当时前端 Deployment 使用的镜像为:

text 复制代码
docker.io/library/my-blog-frontend:a4ed36f

前端容器中的 Nginx 配置包含:

nginx 复制代码
location /api/ {
    proxy_pass http://my-blog-backend:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}

这段配置将 /api/ 请求转发给后端服务 my-blog-backend。

但当时 Kubernetes 集群中尚未有可用的同名后端 Service,导致 Nginx 在启动阶段解析上游地址失败。

6.2 关键诊断命令

先查看所有 Pod:

bash 复制代码
kubectl get pods -n my-blog-k8s -o wide

再查看前端日志:

bash 复制代码
kubectl logs deployment/my-blog-frontend \
  -n my-blog-k8s --tail=50

当时日志显示:

text 复制代码
2026/10/17 09:20:08 [emerg] 1#1: host not found in upstream "my-blog-backend" in /etc/nginx/conf.d/default.conf:21

nginx: [emerg] host not found in upstream "my-blog-backend" in /etc/nginx/conf.d/default.conf:21

这条日志是整个排障过程的关键证据。

它说明故障发生在 Nginx 启动阶段,上游主机名解析失败,而不是已经建立的 HTTP 请求返回了 404 或 500。

6.3 host not found in upstream 到底是什么意思?

Nginx 配置了:

nginx 复制代码
proxy_pass http://my-blog-backend:8080;

这里的 my-blog-backend 不是 Kubernetes 自动识别的特殊字符串,而是需要通过集群 DNS 解析的服务名称。

同一 Namespace 内存在名为 my-blog-backend 的 Service 时,通常可以直接使用这个名称。

但如果 Nginx 启动时,这个名称尚不可解析,Nginx 就可能直接报错退出。

由于容器退出,Kubernetes 会根据 Pod 的重启策略不断重新启动容器。反复失败后,Pod 状态显示为 CrashLoopBackOff。

需要注意,CrashLoopBackOff 表示容器反复退出后,Kubernetes 正在逐步延长重启间隔;它本身并不等于故障根因。

6.4 为什么先排查 Service,而不是直接修改 Nginx?

因为报错中明确指出了无法解析的名称:

text 复制代码
my-blog-backend

此时应先验证这个名称对应的 Kubernetes Service 是否存在,以及是否关联到了正确的后端 Pod。

排障顺序应当是:

  1. 检查后端 Deployment 是否存在。
  2. 检查后端 Pod 是否已经运行。
  3. 检查后端 Service 是否存在。
  4. 检查 Service 是否有有效 Endpoint。
  5. 再检查前端 Nginx 的 DNS 解析与代理配置。

如果后端 Service 根本不存在,那么优先补齐缺失的 Kubernetes 资源,比立即修改 Nginx 配置更合理。

6.5 创建后端 Service 后,如何验证?

后端 Deployment 和 Service 创建后,执行:

bash 复制代码
kubectl get endpoints my-blog-backend \
  -n my-blog-k8s

实际输出:

text 复制代码
NAME              ENDPOINTS
my-blog-backend   10.42.0.217:8080

这说明后端 Service 已经关联到一个后端 Pod 地址。

随后查看前端日志:

bash 复制代码
kubectl logs deployment/my-blog-frontend \
  -n my-blog-k8s --tail=50

这一次日志显示 Nginx 成功启动 Worker 进程:

text 复制代码
2026/10/17 09:25:09 [notice] 1#1: start worker processes
2026/10/17 09:25:09 [notice] 1#1: start worker process 30
2026/10/17 09:25:09 [notice] 1#1: start worker process 31

前端状态最终变成:

text 复制代码
1/1 Running

但这个过程中有一个细节值得记录:前端累计重启了 30 次,最近一次重启发生在观察时刻前约 6 分钟。

这与早期 Nginx 上游地址解析失败的日志相吻合。创建后端 Service 后,前端容器在后续重启时成功启动。

严格来说,这组证据支持"启动时上游地址不可解析,后来恢复"的判断;它并不能证明所有重启都只由这一条错误导致。若要进一步确认,还需要检查容器退出状态、Pod Events 以及更早的日志。

6.6 这次故障的根因与解决思路

已确认的直接故障:

Nginx 启动时无法解析 my-blog-backend,因而退出。

与现象相符的环境原因:

前端部署早于后端 Service 的可用时机,启动时依赖的上游名称不可解析。

恢复过程:

  • 创建后端 Deployment 与 Service。
  • 确认后端 Pod 正常运行。
  • 确认 Service Endpoint 指向后端 Pod。
  • 后续前端容器重启时,Nginx 成功启动。

这次没有必要直接重建所有资源,也没有必要修改现有线上 Compose 部署。


七、验证前端与后端 API 的完整链路

确认 Pod 正常运行之后,下一步不是只看 Running,而是发起真实请求。

7.1 检查 Pod 状态

执行:

bash 复制代码
kubectl get pods -n my-blog-k8s -o wide

当时的实际结果:

text 复制代码
NAME                                READY   STATUS
blog-k8s-mysql-6764dbb555-kfp55     1/1     Running
my-blog-backend-84dd8cf98-vf7d6     1/1     Running
my-blog-frontend-7d9d98cc5d-kng77   1/1     Running

三个组件都已进入 Running,并且 READY 均为 1/1。

7.2 为什么使用端口转发?

本次没有把 K3s 前端 Service 直接映射到宿主机 80 端口,而是采用:

bash 复制代码
kubectl port-forward -n my-blog-k8s \
  svc/my-blog-frontend 18080:80

保持这个命令运行,然后在另一个终端执行测试。

它建立的访问路径是:

text 复制代码
宿主机 127.0.0.1:18080
        |
        v
Kubernetes 前端 Service:80
        |
        v
前端 Nginx Pod:80

这种方法适合在不公开新入口、不切换线上流量的前提下进行验证。

7.3 验证前端页面

执行:

bash 复制代码
curl -I http://127.0.0.1:18080/

实际返回:

text 复制代码
HTTP/1.1 200 OK
Server: nginx/1.21.5
Content-Type: text/html

这证明 Nginx 能够正常提供前端页面。

但单独的首页 HTTP 200 还不能证明后端 API 代理可用,因此还需要继续测试接口。

7.4 验证后端 API

执行:

bash 复制代码
curl -i http://127.0.0.1:18080/api/categories

实际返回:

text 复制代码
HTTP/1.1 200
Server: nginx/1.21.5
Content-Type: application/json

这证明请求经由前端入口返回了 HTTP 200,并且响应类型为 JSON。

由于这次终端输出没有显示明确的 JSON 正文,所以仍需要用下面的命令进一步核实响应体:

bash 复制代码
curl -sS -w '\nHTTP_CODE=%{http_code}\n' \
  http://127.0.0.1:18080/api/categories

注意,之前还误执行过:

bash 复制代码
curl -sS http://127.0.0.1:18080/api/categorieses

这个路径与正确的 /api/categories 不同。它返回 [] 不能作为正确分类接口数据的证明,排查 API 时一定要核对路径。

7.5 为什么这次验证比 kubectl get pods 更重要?

Running 只说明容器当前处于运行状态,并不保证整个应用功能正常。

真实 HTTP 请求可以进一步验证:

  • 前端 Service 能够把请求转发给 Nginx。
  • Nginx 能够处理页面请求。
  • Nginx 的 /api/ 代理路径能够工作。
  • 后端能够对 API 请求作出响应。
  • 后端启动日志已确认数据库连接成功。

结合数据库连接日志和 HTTP 测试,才能更有把握地确认应用链路正常。


八、这次实践使用到的主要排障命令

建议把这些命令作为今后排查 Kubernetes 应用故障的基本工具集。

8.1 查看 Pod 与 Service

bash 复制代码
kubectl get pods -n my-blog-k8s -o wide

kubectl get svc -n my-blog-k8s

kubectl get endpoints my-blog-backend \
  -n my-blog-k8s

用途:

  • 查看 Pod 是否运行、是否就绪。
  • 查看 Service 是否存在。
  • 查看 Service 是否关联到实际后端地址。
  • 对照 Pod IP 与 Endpoint,判断 Service 是否选择了正确的 Pod。

较新版本的 Kubernetes 已逐步弃用旧版 Endpoints API。也可以使用 EndpointSlice 检查:

bash 复制代码
kubectl get endpointslices \
  -n my-blog-k8s \
  -l kubernetes.io/service-name=my-blog-backend

8.2 查看容器日志

bash 复制代码
kubectl logs deployment/my-blog-backend \
  -n my-blog-k8s --tail=100

kubectl logs deployment/my-blog-frontend \
  -n my-blog-k8s --tail=50

当容器反复重启时,查看上一轮容器日志:

bash 复制代码
kubectl logs deployment/my-blog-frontend \
  -n my-blog-k8s --previous --tail=50

--previous 对排查 CrashLoopBackOff 尤其有用,因为它可以查看上一轮已经退出的容器日志。

8.3 查看 Deployment 的详细状态

bash 复制代码
kubectl describe deployment my-blog-frontend \
  -n my-blog-k8s

查看 Pod 的详细状态和 Events:

bash 复制代码
kubectl describe pod \
  my-blog-frontend-7d9d98cc5d-kng77 \
  -n my-blog-k8s

kubectl get events -n my-blog-k8s \
  --sort-by=.metadata.creationTimestamp

注意:Pod 名称可能因重建而变化,执行时应先通过 kubectl get pods 获取当前名称。

8.4 查看后端数据库连接情况

bash 复制代码
kubectl logs deployment/my-blog-backend \
  -n my-blog-k8s --tail=100

重点关注:

  • 当前激活的 Spring Profile。
  • Hikari 连接池是否成功启动。
  • JDBC URL 指向哪个数据库。
  • Flyway 迁移验证是否成功。
  • Tomcat 是否成功监听 8080。
  • 是否存在数据库连接失败、表不存在或迁移异常。

8.5 验证真实 API

bash 复制代码
curl -i http://127.0.0.1:18080/api/categories

这里的 18080 来自前面运行的 kubectl port-forward。如果端口转发进程已退出,需要重新启动它。


九、从这次实践中总结出的技术原理

9.1 Kubernetes 不保证应用按照依赖顺序启动

创建多个 Deployment,并不意味着它们一定按照数据库、后端、前端的顺序启动。

Kubernetes 会分别管理这些工作负载,但不会自动理解业务组件之间的全部启动依赖。

因此,应用需要具备一定的容错能力。

9.2 容器重启不等于问题已经解决

Kubernetes 会根据重启策略重新启动退出的容器,并通过退避机制控制反复失败的频率。

如果故障条件一直存在,容器仍会不断失败。

本次前端在后端 Service 尚不可解析时启动失败,之后在环境条件改变时成功启动。这个过程说明,自动重启能够帮助恢复某些暂时性故障,但不能替代根因分析。

9.3 Service 名称比 Pod IP 更适合作为服务依赖

Pod IP 会随着 Pod 重建而变化。

Service 提供稳定的服务访问名称和虚拟 IP,DNS 则负责将服务名称解析到对应的 Service 地址。

在同一个 Namespace 内,可以使用:

text 复制代码
my-blog-backend:8080

跨 Namespace 时,通常可以使用更完整的服务 DNS 名称:

text 复制代码
my-blog-backend.my-blog-k8s.svc.cluster.local

前提是集群 DNS 正常工作,并且 Service 名称和 Namespace 正确。

9.4 Service 存在不代表应用一定可用

排障时要区分几个层次:

  1. Service 资源是否存在。
  2. Service 是否关联到 Endpoint。
  3. 后端 Pod 是否运行。
  4. 应用是否监听正确端口。
  5. 数据库连接是否正常。
  6. 实际 HTTP 请求是否成功。

本次使用 Endpoint 检查和 HTTP 请求验证,正是为了逐层缩小故障范围。

9.5 Running 与 Ready 的含义不同

Running 表示 Pod 已经进入运行阶段;Ready 则用于表示 Pod 是否准备好接收流量。

本次前端和后端配置了 TCP 探针,用于检查端口是否能够接受 TCP 连接。

TCP 探针能帮助识别端口尚未监听的情况,但它不能完全证明业务功能正常。例如,端口已监听而数据库不可用时,TCP 探针仍可能成功。

更完善的部署可以结合合理的启动探针、就绪探针和存活探针,并根据应用的实际启动过程设置阈值。


十、面试中如何讲清楚这个案例?

面试官可能会问:

"你在 Kubernetes 部署应用时遇到过什么问题?你是怎么排查的?"

可以按下面的逻辑回答。

10.1 一分钟左右的回答

我在把个人博客从 Docker Compose 扩展到 K3s 时,采用的是并行验证方案,没有直接切换线上流量。我先将现有前后端镜像导入 K3s 的 containerd,再为测试环境建立独立的 MySQL 数据库,避免影响线上数据。

部署过程中,前端 Pod 一度进入 CrashLoopBackOff。我先通过 kubectl get pods 查看状态,再用 kubectl logs --previous 查看上一轮容器日志,发现 Nginx 报错 host not found in upstream "my-blog-backend"。

根据日志,我判断问题出在 Nginx 启动时无法解析后端上游名称,而不是直接把它当成业务代码异常。我随后检查后端 Service 和 Endpoint,确认后端 Service 创建后已经指向实际运行的 Pod。之后前端容器成功启动,状态恢复为 1/1 Running。

最后,我通过 kubectl port-forward 建立临时测试入口,验证首页和 /api/categories 接口均返回 HTTP 200。后端日志也确认了它连接的是独立测试数据库。

这次实践让我更清楚地理解了 Kubernetes 的 Service、DNS、Pod 重启机制,以及为什么不能只凭 Running 就判断应用已经可用。

10.2 面试官继续追问:为什么 Nginx 会启动失败?

回答重点:

因为 Nginx 配置中使用了 proxy_pass http://my-blog-backend:8080。启动时 Nginx 无法解析这个上游名称,所以报错退出。Kubernetes 不保证前端和后端按照业务依赖顺序启动,容器重启也不会自动修复始终存在的配置错误或服务发现问题。

10.3 面试官继续追问:你如何确认不是后端代码故障?

回答重点:

我结合了多层证据:首先,Nginx 日志明确指出上游主机名解析失败;其次,后端日志显示 Spring Boot 已启动,Hikari 已连接数据库;然后通过 kubectl get endpoints 确认后端 Service 指向实际 Pod;最后从前端入口请求 API,得到 HTTP 200。这些证据共同支持服务发现和启动时机是当时的主要问题。

10.4 面试官继续追问:下一步会怎样改进?

回答重点:

我会考虑让 Nginx 对上游 DNS 变化具备更好的适应能力,避免依赖某个服务在启动瞬间已经可解析;同时检查 Deployment 的探针和启动参数,完善部署前后的健康检查,并在 CI/CD 中加入接口验证。正式生产迁移前,还需要准备可验证的回滚方案。

需要注意,以上改进属于后续计划,不应说成已经完成。


十一、后续改进计划

这次已经完成基础部署与初步端到端验证,但距离可靠的生产级交付还有一定距离。

第一阶段:完善当前 K3s 部署

  • 确认 /api/categories 的完整响应体。
  • 进一步检查前端历史重启记录,确认全部相关原因。
  • 研究 Nginx 的动态 DNS 解析与上游服务发现方式。
  • 检查启动探针、就绪探针和存活探针的配置。
  • 检查 Secret 的管理方式和数据库账号权限。
  • 验证 Pod 重建后 Service 仍能正确转发请求。

第二阶段:练习故障恢复与回滚

  • 在独立测试环境中模拟错误镜像标签。
  • 观察 ImagePullBackOff 等故障状态。
  • 练习 Deployment rollout 状态检查与历史版本查看。
  • 验证 kubectl rollout undo 的使用方式。
  • 记录故障注入、恢复过程和验证结果。

第三阶段:连接 CI/CD

  • 将镜像构建、版本标签和 Kubernetes 部署串联起来。
  • 避免只使用容易覆盖的 latest 标签。
  • 在部署后增加 HTTP 健康检查。
  • 让部署失败能够明确反馈给 Jenkins。
  • 设计版本回退和部署审计流程。

本阶段仍以测试环境为边界,不直接改变现有 Docker Compose 服务。


十二、最终复盘

本次实践的成果不只是创建了三个 Deployment,而是完整经历了一次真实的部署问题排查:

  1. 明确生产与测试环境的隔离边界。
  2. 解决 Docker 镜像与 K3s containerd 镜像存储之间的差异。
  3. 为 Kubernetes 建立独立 MySQL 测试数据库。
  4. 配置 Spring Boot 使用正确的 Service 地址和测试数据库名。
  5. 根据 Nginx 错误日志定位上游主机名解析失败。
  6. 通过 Service Endpoint 验证后端服务关联关系。
  7. 通过端口转发验证前端和 API 的 HTTP 响应。

其中最值得面试时展开的,是前端 CrashLoopBackOff 案例。

它体现了从日志获取证据、分析 DNS 与 Service 关系、检查 Endpoint,再到通过真实请求确认恢复的完整排障过程。

实践总结:先保护现有业务,再建立隔离环境;先看证据,再判断根因;先验证服务状态,再验证真实请求;最后才讨论自动化、可靠性和迁移。

这也是逐浪笔记从个人博客项目逐步转变为 DevOps 实战作品集的重要一步。

相关推荐
tz_zs1 小时前
【AI编程】使用笔记:AI编程项目文档体系与框架
笔记·ai编程
霖霖7141 小时前
【ESP32笔记】I2S的音频录制与播放
笔记·单片机·mcu·音视频
qq_284274051 小时前
电动机控制与PLC入门:自耦减压启动、反接制动与速度继电器KS,及PLC诞生、定义、特点与分类(含考点)
笔记·学习·自动化·制造
Source.Liu1 小时前
【A11】服务器底层权限表笔记
运维·服务器·笔记
吴声子夜歌2 小时前
Docker入门与实战——高级网络功能
网络·docker
噜~噜~噜~2 小时前
钢琴入门-基础乐理:第1讲到第3讲笔记
笔记·钢琴入门
坤坤子吖2 小时前
Linux 线程概念与控制——线程创建、退出与等待
linux·开发语言·c++·笔记·学习
qq_284274052 小时前
间歇运动机构与机械设计概论:棘轮与槽轮机构运动系数、零件失效与强度刚度准则、结构工艺性清根及标准件(含考点)
笔记·学习·平面·自动化·制造