知序
返回
保存草稿
发布
H1
H2
H3
1
2
3
Excalidraw 图
AI 写作
AI 优化
目录
输入内容后显示目录
Markdown
# 04 - 完整实战:一套 Spring Boot + MySQL 服务上线 > 本篇目标:把前几篇的东西串一遍,用**一份 YAML** 把真实应用部署到外网可访问,并验证滚动更新真的是零停机。 > 前置:01~03 篇。示例应用就用一个典型的 Spring Boot + MySQL 服务。 --- ## 1. 目标形态  *图:实战目标形态全架构:从外网到 Ingress、Service、Deployment,再到集群外的 MySQL* ```text 外网 │ https://blog.example.com v ┌─────────────┐ ┌──────────────────┐ ┌──────────────────────┐ │ Ingress │────>│ Service(ClusterIP)│────>│ Deployment(3 副本) │ │ 七层路由 │ │ blog-system:80 │ │ 容器 8080 │ └─────────────┘ └──────────────────┘ └───────────┬──────────┘ │ ┌───────────v──────────┐ │ ConfigMap / Secret │ │ (环境变量注入) │ └──────────────────────┘ │ ┌───────────v──────────┐ │ 集群外 MySQL │ │ mysql.infra:3306 │ └──────────────────────┘ ``` **为什么数据库放在集群外**:03 篇讲过,MySQL 属于有状态服务,一上来就用 StatefulSet / Operator 会引入太多新概念。先让应用无状态化,是把 K8s 学明白的最短路径。 --- ## 2. 前置:把镜像准备好 ```bash # 在你的项目目录构建镜像(示例用 Spring Boot 的 Buildpacks,不需要写 Dockerfile) ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=registry.example.com/blog-system:1.0.0 # 推送(镜像仓库的地址按你自己的改) docker push registry.example.com/blog-system:1.0.0 ``` **私有仓库要在 K8s 侧配凭证**,否则会一直 `ImagePullBackOff`: ```bash kubectl create secret docker-registry regcred \ --docker-server=registry.example.com \ --docker-username=<用户名> \ --docker-password=<密码> ``` 然后在 Deployment 里引用: ```yaml spec: imagePullSecrets: - name: regcred # ← 漏了这行就是拉不到镜像 ``` 如果你用的是 kind 本地集群,可以让 kind 直接读本地镜像,省掉推仓库: ```bash kind load docker-image registry.example.com/blog-system:1.0.0 --name k8s-lab ``` --- ## 3. 一份完整的 `app.yaml` ```yaml apiVersion: v1 kind: Namespace metadata: name: blog --- apiVersion: v1 kind: ConfigMap metadata: name: blog-config namespace: blog data: SPRING_PROFILES_ACTIVE: "prod" SPRING_DATASOURCE_URL: "jdbc:mysql://mysql.infra:3306/blog?useSSL=false&characterEncoding=UTF-8" --- apiVersion: v1 kind: Secret metadata: name: blog-secret namespace: blog type: Opaque stringData: SPRING_DATASOURCE_USERNAME: blog SPRING_DATASOURCE_PASSWORD: "CHANGE_ME" --- apiVersion: apps/v1 kind: Deployment metadata: name: blog-system namespace: blog spec: replicas: 3 selector: matchLabels: app: blog-system strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 0 # 零停机的关键 template: metadata: labels: app: blog-system spec: imagePullSecrets: - name: regcred containers: - name: app image: registry.example.com/blog-system:1.0.0 ports: - containerPort: 8080 envFrom: - configMapRef: name: blog-config - secretRef: name: blog-secret resources: requests: cpu: "250m" memory: "512Mi" limits: cpu: "1" memory: "1Gi" readinessProbe: # 没它就绪就不接流量 httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 20 periodSeconds: 10 livenessProbe: # 没响应就重启 httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 60 periodSeconds: 20 --- apiVersion: v1 kind: Service metadata: name: blog-system namespace: blog spec: type: ClusterIP selector: app: blog-system ports: - name: http port: 80 targetPort: 8080 --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: blog-system namespace: blog spec: ingressClassName: nginx rules: - host: blog.example.com http: paths: - path: / pathType: Prefix backend: service: name: blog-system port: number: 80 ``` --- ## 4. 按顺序跑一遍 ```bash # 1. 提交所有资源 kubectl apply -f app.yaml # 2. 确认三层都起来了 kubectl -n blog get deploy,rs,pods -o wide # 3. 确认 Service 真的挂上了 Pod(Endpoints 不能为空!) kubectl -n blog get endpoints blog-system # 4. 先在集群内验证服务本身是通的 kubectl -n blog port-forward svc/blog-system 8080:80 # → 本机访问 http://localhost:8080,能打开说明应用 + Service 没问题 # 5. 再看对外入口 kubectl -n blog get ingress kubectl -n blog describe ingress blog-system # 6. 发布新版本(会滚动更新,期间服务不中断) kubectl -n blog set image deploy/blog-system app=registry.example.com/blog-system:1.0.1 kubectl -n blog rollout status deploy/blog-system # 7. 出问题就回滚 kubectl -n blog rollout undo deploy/blog-system ``` **本机验证 Ingress 用 hosts 而不是改 DNS**: ```bash # 拿到 Ingress Controller 的入口地址 kubectl -n ingress-nginx get svc ingress-nginx-controller # 本地加一条 hosts(Linux/macOS: /etc/hosts,Windows: C:\Windows\System32\drivers\etc\hosts) # <入口IP> blog.example.com curl -H "Host: blog.example.com" http://<入口IP>/actuator/health ``` --- ## 5. 验证"零停机"是否真的成立 ```bash # 一个终端持续打请求(观察有没有失败) while true; do curl -s -o /dev/null -w "%{http_code} " https://blog.example.com/actuator/health sleep 0.2 done # 另一个终端触发滚动更新 kubectl -n blog set image deploy/blog-system app=registry.example.com/blog-system:1.0.2 ``` 全程应该只看到 `200`。如果中间出现 `502`/`503`,八成是这两个原因: | 现象 | 原因 | 修法 | |---|---|---| | 发布瞬间有 502 | 没配 `readinessProbe`,新 Pod 还没起来就被塞流量 | 加就绪探针 | | 发布瞬间有 503 | `maxUnavailable` 不是 0,可用 Pod 数掉到 0 | 设 `maxUnavailable: 0` | --- ## 6. 再往前一步:把探针调对 Spring Boot 的 `/actuator/health` **默认会连数据库探活**。这在 K8s 里是个陷阱:  *图:探针设错的雪崩链路:2 秒的数据库抖动被放大成全副本反复重启* ```text 数据库抖动 2 秒 └─→ 所有 Pod 的 livenessProbe 同时失败 └─→ 所有 Pod 被同时重启 └─→ 重启期间服务完全不可用(比数据库抖动本身严重得多) └─→ 雪崩 ``` **正确做法**:让存活探针只检查"进程还活着",别让它依赖外部组件。 ```yaml # 1) 应用侧:分组暴露健康检查 management: endpoint: health: group: liveness: include: ping # 只检查自己 readiness: include: db,redis # 就绪才依赖外部 endpoints: web: exposure: include: health ``` ```yaml # 2) K8s 侧:两个探针指向不同的组 readinessProbe: httpGet: path: /actuator/health/readiness # 依赖 db/redis port: 8080 livenessProbe: httpGet: path: /actuator/health/liveness # 只看自己 port: 8080 ``` 这样数据库抖动时,Pod 会被**摘掉流量**(readiness 失败),但**不会被重启**(liveness 仍通过)。等数据库恢复,Pod 自动重新接流量 —— 这才是正确的行为。 --- ## 7. 上线前自检清单 ```text □ 镜像有明确版本 tag(不要用 latest) □ 配了 imagePullSecrets(私有仓库) □ 设了 resources.requests 和 limits □ readinessProbe 指向只依赖"必须就绪"的接口,且路径正确 □ livenessProbe 不依赖外部组件 □ maxUnavailable: 0(要零停机的话) □ 配置走 ConfigMap / Secret,没硬编码在镜像里 □ 副本数 ≥ 2(单副本等于还是有单点) □ 数据库/中间件不在同一个集群里"裸奔" □ 知道怎么回滚(rollout undo 试过一次) ``` --- ## 8. 本篇小结 ```text 镜像 → Deployment(自愈/滚动更新) ↓ Service(稳定入口 + 就绪探针摘流量) ↓ Ingress(七层路由对外) ↓ ConfigMap / Secret(配置外置) 出问题的定位顺序: get pods → describe pod → get endpoints → port-forward → describe ingress ``` 下一篇把排障系统化:状态含义、命令速查、异常对照。
预览
发布文章
添加摘要让读者更快了解文章内容
取消
确认发布
AI 写作助手
输入一个主题,AI 将为你生成一篇 Markdown 博客文章
AI 正在写作中...
取消
开始生成
AI 优化助手
描述你的修改需求,AI 将在原文基础上进行补充或微调
AI 正在优化中...
取消
开始优化
画布
从已有图里选
✕
图名
保存后生成 /images/excalidraw/<图名>.svg(只能用字母、数字、短横线、下划线、点)
取消
保存并插入