Skip to content

云原生 ​

S6-020选修高级约 60 分钟JDK 21Java 高级与架构 · 架构设计

1. 本节定位 ​

Docker 与 K8s 讲的是「平台能给什么」;本节讲「应用必须遵守什么契约」,否则再好的平台也救不了它。

★ 四组一手实测(JDK 自带 HttpServer,无任何依赖) ✓✓:

text
配置外置:默认值 → 环境变量覆盖(APP_PORT=9090 / prod-db / WARN)✓✓ 同一构件、不同环境
健康检查:/healthz=200(活着)而 /readyz=503(未就绪)→ **K8s 摘流量但绝不重启** ✓✓✓
优雅停机:停机信号 → 摘流量 → 新请求被拒 → **在途慢请求正常完成** → 进程退出 ✓✓✓
12-Factor:六条最常踩的自查清单 ✓

2. 学习目标 ​

  • 能背出云原生的应用契约(构件不变 / 环境决定行为 / 进程可丢弃)★★★;
  • 会实现配置外置与结构化 stdout 日志 ★★★;
  • 能区分 liveness 与 readiness 的语义并正确实现 ★★★;
  • 会写优雅停机(先摘流量 → 等在途 → 退出)★★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

★ 容器化后最常见的四类「应用侧事故」 ✗✗(都不是平台的锅):

事故后果
配置写死在代码/镜像里 ✗换环境要重新打包;生产密码进了仓库 ✗✗
日志写本地文件 ✗✗容器一销毁日志全丢;kubectl logs 什么都看不到 ✗✗
被 kill 时直接退出 ✗✗在途请求被砍断,用户看到 502 ✗✗
把「依赖未就绪」当 liveness 失败 ✗✗容器被反复重启,雪上加霜 ✓✓

5. 核心概念 ​

5.1 云原生的应用契约(一句话)★★★ ​

「构件不变、环境决定行为、进程可丢弃」 ✓✓✓

契约含义本节的实测
构件不变同一个 jar/镜像,所有环境都用它配置靠环境变量注入 ✓✓
环境决定行为dev/测试/生产只有 env 不同APP_PORT=9090 覆盖实测 ✓✓
进程可丢弃随时可被杀、可扩容、可漂移优雅停机 + 无状态 ✓✓✓

5.2 配置外置(实测)★★★ ​

★ 默认值运行 ✓:

text
APP_PORT = 8080(默认值)
DB_URL   = jdbc:postgresql://localhost:5432/dev(默认值)
LOG_LEVEL= INFO(默认值)

★ 环境变量覆盖(同一份构件) ✓✓✓:

text
APP_PORT = 9090(来自环境变量 ✓)
DB_URL   = jdbc:postgresql://prod-db.internal:5432/app(来自环境变量 ✓)
LOG_LEVEL= WARN(来自环境变量 ✓)

★ 落地要点 ✓✓:代码里只出现「键名 + 默认值」,绝不出现环境专属值; 敏感信息用 Secret 注入(K8s Secret / 环境变量 / 配置中心),不进镜像、不进仓库 ✓✓✓。

5.3 健康检查:两个探针,两种语义(实测)★★★ ​

text
t=  420ms GET /healthz → 200(进程活着 = liveness 通过 ✓)
t=  554ms GET /readyz  → 503(依赖未就绪 = readiness 不通过)✓✓
   ★ 此时 K8s 的行为:**不派流量,但绝不重启** —— 这就是两个探针的区别 ✓✓✓
   ★ 如果把「依赖未就绪」写进 liveness:容器会被反复重启(经典事故)✗✗
t= 2561ms 依赖就绪 → GET /readyz → 200(开始接流量 ✓)
探针回答的问题失败后果实现要点
liveness进程还健康吗?重启容器 ✗只做「自我检查」(死锁检测),不查依赖 ✓✓
readiness能接流量了吗?摘掉流量,不重启 ✓查依赖就绪 + 停机时主动返回 503 ✓✓✓
startup(《Kubernetes 入门》)慢启动应用重启大应用/JVM 预热场景 ✓

★★ 这是 《Kubernetes 入门》「配置探针」的进阶 ✓✓:探针配得对,还要应用「答得对」 ✓✓✓。

5.4 优雅停机:先摘流量、再等存量、最后退出(实测)★★★ ​

text
t= 2667ms 收到停机信号(模拟 SIGTERM / K8s 删除 Pod)
t= 2667ms /readyz → 503(**先摘流量**,不再接新请求)
t= 2682ms 停机期间的新请求被拒 → 503 ✓(正确行为)
t= 3571ms 在途慢请求完成:work-done ✓✓(没有被砍断)
t= 3573ms 在途请求清零 → 进程退出 ✓✓(对应 K8s 的 terminationGracePeriod)

★★★ 顺序不能错 ✓✓✓:

text
① 收到 SIGTERM → ② /readyz 转 503(让负载均衡停止派新流量)→ ③ 等在途请求完成
→ ④ 关闭资源(连接池/消息消费者)→ ⑤ 退出

★ 少任何一步的后果 ✗✗:请求被砍断(用户看到 502)、优雅停机形同虚设; Python/Go/Java 各语言都要自己实现这个「停机握手」 ✓✓。

5.5 12-Factor 自查(六条最常踩的)★★ ​

条目要求常见违规
III. 配置环境变量注入写死在代码/镜像 ✗✗
VI. 进程无状态、可随时被杀会话/文件存本地 ✗
VIII. 并发靠进程水平扩展一台机器无限加线程硬扛 ✗
IX. 易处置快启动 + 优雅停机 + 快速失败启动 5 分钟、停机砍请求 ✗✗
XI. 日志写 stdout(结构化单行)自己写文件 + 轮转 ✗✗
XII. 管理进程一次性任务用独立进程在 Web 进程里跑迁移 ✗

★ 为什么日志必须写 stdout ✓✓✓:容器的文件系统是「临时」的 —— stdout 由平台统一收集(EFK/Loki),轮转与保留是平台的事,应用只管输出 ✓✓。

5.6 CNCF 全景:知道「有什么」,按需再深入 ★★ ​

层次代表项目你已经在用的
容器运行时containerd / Docker《Docker 基础与镜像容器》 ✓
编排Kubernetes《Kubernetes 入门》 ✓
服务网格Istio / Linkerd《服务网格》 ✓
可观测Prometheus / Grafana / Jaeger / LokiT2-016 ✓
消息与流Kafka / RabbitMQ《消息队列基础与选型》 ✓
CI/CDArgo CD / GitHub Actions《CI/CD 流水线设计》 ✓
网关Envoy / Nginx Ingress《Nginx 安装与反向代理》 ✓

★ 使用建议 ✓✓:别为了「云原生」而堆组件 —— 每个新增组件都要有明确的、可度量的收益 ✓✓✓。

6. 递进示例 ​

6.1 健康检查端点(最小实现) ​

java
server.createContext("/healthz", exchange -> respond(exchange, 200, "alive"));
server.createContext("/readyz", exchange -> {
    boolean ready = dependencyReady && !shuttingDown;
    respond(exchange, ready ? 200 : 503, ready ? "ready" : "not-ready");
});

6.2 优雅停机的骨架 ​

java
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    shuttingDown = true;                 // ① /readyz 开始返回 503
    while (inFlight.get() > 0) { sleep(10); }   // ② 等在途请求
    // ③ 关闭连接池 / 提交 offset —— 然后进程退出
}));

6.3 反例:启动时把依赖地址 ping 死了才启动 ​

text
「ping 不通数据库就不启动」✗✗ → 依赖故障时容器反复重启(CrashLoopBackOff)
正解:能启动就启动,**就绪状态交给 readiness 表达** ✓✓✓

7. 完整可运行代码与实测 ​

代码:labs/java-deep/src/main/java/cn/kb/learn/deep/architecture/CloudNativeLesson.java

实测输出(时间线,节选):

text
===== 1. 健康检查:/healthz 与 /readyz 的语义差异 =====
  t=  420ms GET /healthz → 200(进程活着 = liveness 通过 ✓)
  t=  554ms GET /readyz  → 503(依赖未就绪 = readiness 不通过)✓✓
  t= 2561ms 依赖就绪 → GET /readyz → 200(开始接流量 ✓)

===== 2. 优雅停机:在途请求必须做完 =====
  t= 2667ms 收到停机信号(模拟 SIGTERM / K8s 删除 Pod)
  t= 2682ms 停机期间的新请求被拒 → 503 ✓(正确行为)
  t= 3571ms 在途慢请求完成:work-done ✓✓(没有被砍断)
  t= 3573ms 在途请求清零 → 进程退出 ✓✓

8. Debug 观察点 ​

  1. 把 liveness 改成「依赖就绪才 200」:体验「依赖挂了 → 容器反复重启」✗✗✓;
  2. 去掉优雅停机的等待逻辑:观察在途请求被砍断 ✗✗✓;
  3. 用 docker run -e APP_PORT=9090 ... 复现环境变量覆盖(呼应 《Docker 基础与镜像容器》)✓✓;
  4. 在 K8s 里把 terminationGracePeriodSeconds 调小到 1 秒:观察优雅停机被平台强制打断 ✗✓。

9. 常见错误 ​

#错误说明
1日志写文件 + 自己轮转 ✗✗✗容器销毁日志全丢 ✓✓
2liveness 里查依赖 ✗✗✗依赖抖动 → 容器全体重启 ✓✓
3没有优雅停机 ✗✗滚动更新期间用户看到 502 ✓✓
4配置/密钥打进镜像 ✗✗✗换环境重打包 + 泄露风险 ✓✓
5启动即强依赖(ping 不通就退出) ✗✗CrashLoopBackOff,雪上加霜 ✓✓
6为了云原生而堆组件 ✗复杂度是负债,收益要可度量 ✓✓

10. 动手实验 ​

  1. 跑 CloudNativeLesson,读完整时间线 ✓✓✓;
  2. 用 -e 环境变量在 Docker 里跑同一个 jar(《Docker 基础与镜像容器》)✓✓;
  3. 给项目 A 加上 /healthz、/readyz 与优雅停机(Spring Boot 自带 management.endpoint + server.shutdown=graceful)✓✓✓;
  4. 在 K8s 里验证:滚动更新时「零 502」(用 T2-015 的压测脚本持续打流量)✓✓✓。

验收方式:给一个容器化的 Java 服务,能逐条核对 12-Factor 与探针/停机的实现。

扩展任务:为项目 A 写《云原生就绪度清单》:配置、日志、探针、停机、资源限制(requests/limits)、镜像构建 ✓✓

11. 练习题 ​

  1. 云原生应用契约的三句话是什么?
  2. liveness 与 readiness 的失败后果分别是什么?为什么 liveness 不能查依赖?
  3. 优雅停机的正确顺序?少一步会怎样?
  4. 为什么日志必须写 stdout?谁来负责收集与轮转?
  5. 「构件不变」为什么重要?反例是什么?
参考答案(要点)
  1. 构件不变、环境决定行为、进程可丢弃 ✓✓★
  2. liveness 失败 → 重启;readiness 失败 → 摘流量;依赖抖动会引发全体重启 ✓✓★
  3. 摘流量 → 等在途 → 关资源 → 退出;顺序错了请求被砍断(502)✓✓★
  4. 容器文件系统是临时的;平台统一收集(EFK/Loki)并轮转 ✓✓★
  5. 同一个构件穿过所有环境才有可复现性;反例:为生产重新打包(构建产物与测试过的不是同一个)✗✗★

12. 掌握标准 ​

  • [ ] 能背应用契约三句话 ★★★;
  • [ ] 会实现配置外置与 stdout 日志 ★★★;
  • [ ] 能正确实现 liveness/readiness ★★★;
  • [ ] 会写优雅停机 ★★★;
  • [ ] 能逐条核对 12-Factor ★★。

13. 扩展资源 ​

14. 下一步学习 ​

S6 到此全量收官(并发 → JVM → 分布式 → 架构)。 下一站:S7 求职与转岗(简历、项目表达、面试结构与高频题路径),以及 S8 持续学习(建立个人技术资产与知识库维护机制)。

后续知识

暂无

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引