主题
界面
主题
Docker 与 K8s 讲的是「平台能给什么」;本节讲「应用必须遵守什么契约」,否则再好的平台也救不了它。
★ 四组一手实测(JDK 自带 HttpServer,无任何依赖) ✓✓:
配置外置:默认值 → 环境变量覆盖(APP_PORT=9090 / prod-db / WARN)✓✓ 同一构件、不同环境
健康检查:/healthz=200(活着)而 /readyz=503(未就绪)→ **K8s 摘流量但绝不重启** ✓✓✓
优雅停机:停机信号 → 摘流量 → 新请求被拒 → **在途慢请求正常完成** → 进程退出 ✓✓✓
12-Factor:六条最常踩的自查清单 ✓★ 容器化后最常见的四类「应用侧事故」 ✗✗(都不是平台的锅):
| 事故 | 后果 |
|---|---|
| 配置写死在代码/镜像里 ✗ | 换环境要重新打包;生产密码进了仓库 ✗✗ |
| 日志写本地文件 ✗✗ | 容器一销毁日志全丢;kubectl logs 什么都看不到 ✗✗ |
| 被 kill 时直接退出 ✗✗ | 在途请求被砍断,用户看到 502 ✗✗ |
| 把「依赖未就绪」当 liveness 失败 ✗✗ | 容器被反复重启,雪上加霜 ✓✓ |
「构件不变、环境决定行为、进程可丢弃」 ✓✓✓
| 契约 | 含义 | 本节的实测 |
|---|---|---|
| 构件不变 | 同一个 jar/镜像,所有环境都用它 | 配置靠环境变量注入 ✓✓ |
| 环境决定行为 | dev/测试/生产只有 env 不同 | APP_PORT=9090 覆盖实测 ✓✓ |
| 进程可丢弃 | 随时可被杀、可扩容、可漂移 | 优雅停机 + 无状态 ✓✓✓ |
★ 默认值运行 ✓:
APP_PORT = 8080(默认值)
DB_URL = jdbc:postgresql://localhost:5432/dev(默认值)
LOG_LEVEL= INFO(默认值)★ 环境变量覆盖(同一份构件) ✓✓✓:
APP_PORT = 9090(来自环境变量 ✓)
DB_URL = jdbc:postgresql://prod-db.internal:5432/app(来自环境变量 ✓)
LOG_LEVEL= WARN(来自环境变量 ✓)★ 落地要点 ✓✓:代码里只出现「键名 + 默认值」,绝不出现环境专属值; 敏感信息用 Secret 注入(K8s Secret / 环境变量 / 配置中心),不进镜像、不进仓库 ✓✓✓。
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 入门》「配置探针」的进阶 ✓✓:探针配得对,还要应用「答得对」 ✓✓✓。
t= 2667ms 收到停机信号(模拟 SIGTERM / K8s 删除 Pod)
t= 2667ms /readyz → 503(**先摘流量**,不再接新请求)
t= 2682ms 停机期间的新请求被拒 → 503 ✓(正确行为)
t= 3571ms 在途慢请求完成:work-done ✓✓(没有被砍断)
t= 3573ms 在途请求清零 → 进程退出 ✓✓(对应 K8s 的 terminationGracePeriod)★★★ 顺序不能错 ✓✓✓:
① 收到 SIGTERM → ② /readyz 转 503(让负载均衡停止派新流量)→ ③ 等在途请求完成
→ ④ 关闭资源(连接池/消息消费者)→ ⑤ 退出★ 少任何一步的后果 ✗✗:请求被砍断(用户看到 502)、优雅停机形同虚设; Python/Go/Java 各语言都要自己实现这个「停机握手」 ✓✓。
| 条目 | 要求 | 常见违规 |
|---|---|---|
| III. 配置 | 环境变量注入 | 写死在代码/镜像 ✗✗ |
| VI. 进程 | 无状态、可随时被杀 | 会话/文件存本地 ✗ |
| VIII. 并发 | 靠进程水平扩展 | 一台机器无限加线程硬扛 ✗ |
| IX. 易处置 | 快启动 + 优雅停机 + 快速失败 | 启动 5 分钟、停机砍请求 ✗✗ |
| XI. 日志 | 写 stdout(结构化单行) | 自己写文件 + 轮转 ✗✗ |
| XII. 管理进程 | 一次性任务用独立进程 | 在 Web 进程里跑迁移 ✗ |
★ 为什么日志必须写 stdout ✓✓✓:容器的文件系统是「临时」的 —— stdout 由平台统一收集(EFK/Loki),轮转与保留是平台的事,应用只管输出 ✓✓。
| 层次 | 代表项目 | 你已经在用的 |
|---|---|---|
| 容器运行时 | containerd / Docker | 《Docker 基础与镜像容器》 ✓ |
| 编排 | Kubernetes | 《Kubernetes 入门》 ✓ |
| 服务网格 | Istio / Linkerd | 《服务网格》 ✓ |
| 可观测 | Prometheus / Grafana / Jaeger / Loki | T2-016 ✓ |
| 消息与流 | Kafka / RabbitMQ | 《消息队列基础与选型》 ✓ |
| CI/CD | Argo CD / GitHub Actions | 《CI/CD 流水线设计》 ✓ |
| 网关 | Envoy / Nginx Ingress | 《Nginx 安装与反向代理》 ✓ |
★ 使用建议 ✓✓:别为了「云原生」而堆组件 —— 每个新增组件都要有明确的、可度量的收益 ✓✓✓。
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");
});Runtime.getRuntime().addShutdownHook(new Thread(() -> {
shuttingDown = true; // ① /readyz 开始返回 503
while (inFlight.get() > 0) { sleep(10); } // ② 等在途请求
// ③ 关闭连接池 / 提交 offset —— 然后进程退出
}));「ping 不通数据库就不启动」✗✗ → 依赖故障时容器反复重启(CrashLoopBackOff)
正解:能启动就启动,**就绪状态交给 readiness 表达** ✓✓✓代码:labs/java-deep/src/main/java/cn/kb/learn/deep/architecture/CloudNativeLesson.java
实测输出(时间线,节选):
===== 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 在途请求清零 → 进程退出 ✓✓docker run -e APP_PORT=9090 ... 复现环境变量覆盖(呼应 《Docker 基础与镜像容器》)✓✓;terminationGracePeriodSeconds 调小到 1 秒:观察优雅停机被平台强制打断 ✗✓。| # | 错误 | 说明 |
|---|---|---|
| 1 | 日志写文件 + 自己轮转 ✗✗✗ | 容器销毁日志全丢 ✓✓ |
| 2 | liveness 里查依赖 ✗✗✗ | 依赖抖动 → 容器全体重启 ✓✓ |
| 3 | 没有优雅停机 ✗✗ | 滚动更新期间用户看到 502 ✓✓ |
| 4 | 配置/密钥打进镜像 ✗✗✗ | 换环境重打包 + 泄露风险 ✓✓ |
| 5 | 启动即强依赖(ping 不通就退出) ✗✗ | CrashLoopBackOff,雪上加霜 ✓✓ |
| 6 | 为了云原生而堆组件 ✗ | 复杂度是负债,收益要可度量 ✓✓ |
CloudNativeLesson,读完整时间线 ✓✓✓;-e 环境变量在 Docker 里跑同一个 jar(《Docker 基础与镜像容器》)✓✓;/healthz、/readyz 与优雅停机(Spring Boot 自带 management.endpoint + server.shutdown=graceful)✓✓✓;验收方式:给一个容器化的 Java 服务,能逐条核对 12-Factor 与探针/停机的实现。
扩展任务:为项目 A 写《云原生就绪度清单》:配置、日志、探针、停机、资源限制(requests/limits)、镜像构建 ✓✓
S6 到此全量收官(并发 → JVM → 分布式 → 架构)。 下一站:S7 求职与转岗(简历、项目表达、面试结构与高频题路径),以及 S8 持续学习(建立个人技术资产与知识库维护机制)。
暂无