文章目录
-
- 一、实践背景与目标
-
- [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 面试官继续追问:下一步会怎样改进?)
- 十一、后续改进计划
-
- [第一阶段:完善当前 K3s 部署](#第一阶段:完善当前 K3s 部署)
- 第二阶段:练习故障恢复与回滚
- [第三阶段:连接 CI/CD](#第三阶段:连接 CI/CD)
- 十二、最终复盘
实践日期: 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 实践目标
- 安装并使用单节点 K3s 集群。
- 将现有博客前端、后端 Docker 镜像导入 K3s 使用的 containerd。
- 在 K3s 中部署独立 MySQL 测试数据库。
- 在测试数据库中初始化表结构和 Flyway 迁移历史。
- 部署 Spring Boot 后端,并验证数据库连接。
- 部署 Vue/Nginx 前端,配置后端 API 代理。
- 排查前端 Nginx 启动失败的问题。
- 通过 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 才包含增量迁移。不能简单假设一个完全空白的数据库只靠执行这些增量迁移,就一定能建立出完整的应用表结构。
本次采取的办法是:
- 检查现有数据库表结构与 Flyway 历史。
- 导出测试需要的表结构。
- 将结构初始化脚本与相应 Flyway 历史记录准备好。
- 为测试环境使用独立数据库名。
- 在 K3s MySQL 中导入初始化内容。
- 检查表结构及迁移历史是否符合预期。
需要强调:本次复用的是数据库结构和迁移历史,不是将线上业务数据整体复制到测试库。
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。
排障顺序应当是:
- 检查后端 Deployment 是否存在。
- 检查后端 Pod 是否已经运行。
- 检查后端 Service 是否存在。
- 检查 Service 是否有有效 Endpoint。
- 再检查前端 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 存在不代表应用一定可用
排障时要区分几个层次:
- Service 资源是否存在。
- Service 是否关联到 Endpoint。
- 后端 Pod 是否运行。
- 应用是否监听正确端口。
- 数据库连接是否正常。
- 实际 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,而是完整经历了一次真实的部署问题排查:
- 明确生产与测试环境的隔离边界。
- 解决 Docker 镜像与 K3s containerd 镜像存储之间的差异。
- 为 Kubernetes 建立独立 MySQL 测试数据库。
- 配置 Spring Boot 使用正确的 Service 地址和测试数据库名。
- 根据 Nginx 错误日志定位上游主机名解析失败。
- 通过 Service Endpoint 验证后端服务关联关系。
- 通过端口转发验证前端和 API 的 HTTP 响应。
其中最值得面试时展开的,是前端 CrashLoopBackOff 案例。
它体现了从日志获取证据、分析 DNS 与 Service 关系、检查 Endpoint,再到通过真实请求确认恢复的完整排障过程。
实践总结:先保护现有业务,再建立隔离环境;先看证据,再判断根因;先验证服务状态,再验证真实请求;最后才讨论自动化、可靠性和迁移。
这也是逐浪笔记从个人博客项目逐步转变为 DevOps 实战作品集的重要一步。