1. 为什么“优雅”不是形容词,而是容器生命周期的硬性指标
在某次线上服务发布后,监控面板突然跳出一连串红色告警:订单支付成功率从99.98%断崖式跌到92%,持续了整整73秒。回溯日志发现,问题出在一次滚动更新——新Pod刚启动,旧Pod就被Kubernetes直接发出了SIGKILL信号,而它正在处理一笔跨微服务的库存扣减请求。下游服务收到半截响应,上游重试机制又触发了重复扣减,最终导致库存负数。这不是理论风险,是我在三个不同业务线都亲手复现过的典型故障。
很多人把“优雅启动和终止”当成一个可有可无的运维优化项,甚至觉得“反正有健康检查兜底”。但现实是:Kubernetes的默认行为恰恰是“不优雅”的。它只管调度、启停、探针反馈,却不管你的应用内部状态是否就绪、是否已清理完资源、是否已拒绝新请求。所谓“优雅”,本质是让容器进程与Kubernetes调度器之间建立一套可协商、可预期、可验证的状态契约。这个契约的两端,一端是你写的代码(或配置),另一端是kubelet的信号处理逻辑。
关键词里没写,但必须前置强调:livenessProbe、readinessProbe、startupProbe、preStop Hook、terminationGracePeriodSeconds——这五个配置项,就是你握在手里的全部谈判筹码。它们不是孤立的开关,而是一套协同工作的状态机。比如,readinessProbe为false时,Service流量不会打进来,但这不等于你的进程已经准备好接收请求;startupProbe超时后,kubelet会直接杀死容器,哪怕你的应用其实只差最后100ms就能初始化完成。这些细节的偏差,就是生产环境里那些“偶发性503”“重启后数据不一致”的根源。
我见过太多团队在压测时一切正常,一上生产就出问题。后来排查发现,他们把readinessProbe的initialDelaySeconds设成了30秒,而实际应用冷启动需要42秒——这12秒的窗口期,流量被错误地导向了尚未就绪的实例。更隐蔽的是,有些Java应用在JVM Full GC期间,readinessProbe的HTTP接口会卡住超时,导致kubelet误判为“不可用”,反复重启Pod。这些问题,没有一个能靠“加个探针”解决,必须理解每个参数背后的信号传递链条。
所以这篇文章不讲“怎么配探针”,而是带你拆解:当kubectl apply -f deployment.yaml执行的那一刻,从API Server接收到请求,到你的容器进程真正开始处理第一个HTTP请求,再到它被安全终止的全过程里,每一个关键节点上,Kubernetes在做什么、你的应用该做什么、以及如果做错了会付出什么代价。这是一份给SRE、后端开发、云原生架构师的“容器状态契约手册”。
2. 启动阶段的三重门:Startup、Readiness、Liveness如何分工协作
容器启动不是“一键开机”,而是一场分阶段的资格认证。Kubernetes用三个探针构建了三道门禁,每一道门的开启条件、审核标准、拒入后果都截然不同。把它们混为一谈,是绝大多数启动失败问题的起点。
2.1 Startup Probe:唯一允许你“慢启动”的特权通道
Startup Probe是Kubernetes 1.16引入的救命稻草。它的存在,就是为了对抗那些天生启动缓慢的应用——Spring Boot的全量类扫描、Python Django的数据库迁移、Node.js的Webpack热编译。在它出现之前,工程师只能把readinessProbe的initialDelaySeconds调得极大,结果是:应用早已就绪,却要等满30秒才开始接收流量,资源白白闲置。
Startup Probe的核心逻辑是:在它成功之前,其他所有探针(包括readiness和liveness)全部暂停执行。这意味着,你可以放心地设置一个宽松的超时策略,而不影响健康检查的灵敏度。
startupProbe: httpGet: path: /healthz port: 8080 failureThreshold: 30 # 允许最多30次失败 periodSeconds: 10 # 每10秒探测一次 timeoutSeconds: 5 # 单次探测超时5秒 # 总最长等待时间 = 30 * 10 = 300秒(5分钟)提示:failureThreshold × periodSeconds 是你应用启动的“绝对宽限期”。不要把它设成无限大——如果应用真的卡死,你需要快速失败并触发重启,而不是让Pod永远处于Pending状态。
实操中我发现一个关键细节:很多Go应用用http.ListenAndServe()启动HTTP服务,但这个函数本身是阻塞的,导致main goroutine无法继续执行后续的初始化逻辑(比如连接Redis、预热缓存)。结果是,/healthz接口虽然能响应,但应用核心功能并未就绪。解决方案是:用http.Server显式启动,并在ListenAndServe前完成所有初始化,再用goroutine异步启动服务。这样,startupProbe探测到HTTP服务可用时,应用确实已准备就绪。
2.2 Readiness Probe:流量入口的实时闸门
当startupProbe通过后,readinessProbe立刻接管。它的使命只有一个:告诉kube-proxy,“此刻,这个Pod是否应该出现在Endpoint列表里”。只要它返回200,Service的iptables或IPVS规则就会把流量导过来;一旦失败,kube-proxy会在几秒内(取决于kube-proxy的同步周期)将该Pod从Endpoint中剔除。
这里有个致命误区:认为readinessProbe成功 = 应用100%可用。错。它只验证你定义的那个端点。比如你配置:
readinessProbe: exec: command: ["cat", "/tmp/ready"]这只能证明/tmp/ready文件存在,但无法保证数据库连接池已建好、Redis哨兵已选举完成、或者本地缓存已加载完毕。更危险的是,有些团队把readinessProbe指向一个轻量级的/ping接口,而这个接口根本不校验任何下游依赖。结果是:Pod明明连不上MySQL,readinessProbe却一直返回200,所有流量涌向一个“假活”的实例,引发雪崩。
我推荐的做法是:readinessProbe必须做“最小可行依赖检查”。对Web服务,至少应包含:
- HTTP服务监听端口是否可连通(基础层)
- 主数据库连接是否可用(核心依赖)
- 缓存服务(如Redis)是否可写(高频依赖)
用Spring Boot Actuator的/actuator/health?show-details=always是个好选择,但它默认不检查所有组件。你需要在配置中显式启用:
management: endpoint: health: show-details: always endpoints: web: exposure: include: health health: db: show-details: always redis: show-details: always这样,/health端点会返回JSON,其中status字段为UP时,才代表所有关键依赖就绪。kubelet的HTTP探针会自动解析HTTP状态码,你无需额外写脚本。
2.3 Liveness Probe:系统级的“心跳监护仪”
如果说readinessProbe管“能不能干活”,livenessProbe就管“还活着吗”。它的失败意味着:这个Pod已经失去响应能力,必须被强制杀死并重建。这是Kubernetes防止“僵尸进程”的最后一道防线。
但滥用livenessProbe比不用更危险。最常见的错误是:把livenessProbe和readinessProbe配置成完全一样的检查项。比如两者都指向/healthz。这会导致一个诡异现象:当应用因GC停顿或CPU过载而短暂无响应时,livenessProbe连续失败,kubelet立即发送SIGTERM,然后SIGKILL。结果是,一个本可以自我恢复的瞬时抖动,被升级成了强制重启——而重启过程本身又会加剧系统压力。
正确的策略是:livenessProbe的阈值必须比readinessProbe更宽松,且检查项应聚焦于“进程是否挂起”,而非“依赖是否可用”。例如:
- 对Java应用,检查JVM进程是否存在(
jps -l | grep MyApp) - 对Node.js,检查
process.uptime()是否大于0(证明主循环未卡死) - 对Python,检查
/proc/self/status中的State字段是否为R(运行中)或S(睡眠中),而非Z(僵尸)
注意:永远不要在livenessProbe中检查外部依赖(如数据库)。如果DB挂了,所有Pod的livenessProbe都会失败,触发全量重启,这叫“雪崩式自毁”。
这三个探针不是并列关系,而是有严格时序和优先级的协作链。StartupProbe是“入学考试”,Readiness是“上岗考核”,Liveness是“在职体检”。漏掉任何一环,或者把它们的职责搞混,你的服务就永远在“半活”状态中挣扎。
3. 终止阶段的生死时速:PreStop Hook与Termination Grace Period的博弈
容器终止,远比启动更凶险。因为启动失败,顶多是服务不可用;而终止失败,直接导致数据损坏、状态不一致、甚至服务永久性中断。我亲眼见过一个金融系统的结算服务,在Pod终止时未正确关闭数据库连接池,导致连接泄漏,3小时后整个集群的MySQL连接数被打满,所有业务全部瘫痪。
Kubernetes的终止流程是一条单向时间线,由两个核心参数驱动:terminationGracePeriodSeconds(默认30秒)和preStop Hook。它们共同决定了你的应用有多少时间“体面谢幕”。
3.1 Termination Grace Period:留给应用的“黄金30秒”
当你执行kubectl delete pod xxx或滚动更新触发旧Pod驱逐时,kubelet会做三件事:
- 向Pod内所有容器发送SIGTERM信号(软终止)
- 启动一个倒计时器,时长为
terminationGracePeriodSeconds - 倒计时结束,若容器仍未退出,则发送SIGKILL(硬终止,进程立即消失)
这30秒,就是你的应用进行“优雅关闭”的全部时间。但很多人不知道:这个倒计时,是从SIGTERM发出那一刻就开始的,而不是从preStop Hook执行完才开始。这意味着,如果你的preStop Hook本身耗时25秒,那么你的应用进程只剩下最后5秒来响应SIGTERM并自行退出。
实测数据:在某高负载环境下,一个典型的Spring Boot应用,从收到SIGTERM到完成所有Shutdown Hook(关闭线程池、提交未完成事务、刷新缓存),平均耗时18秒。如果terminationGracePeriodSeconds仍用默认30秒,那它只有12秒的缓冲余量。一旦遇到Full GC或磁盘IO瓶颈,这12秒很容易被吃掉,最终被SIGKILL粗暴终结。
因此,我的经验是:必须根据压测数据,为terminationGracePeriodSeconds设置一个“安全边际”。公式如下:
terminationGracePeriodSeconds = (平均优雅关闭耗时) + (P95延迟毛刺) + (10秒冗余)比如,压测显示平均关闭耗时18秒,P95毛刺是7秒,那么建议设为18 + 7 + 10 = 35秒。宁可多给5秒,也不要少给1秒。
3.2 PreStop Hook:终止前的“临终托付”
PreStop Hook是Kubernetes在发送SIGTERM之前,为你预留的一个“最后操作机会”。它支持两种类型:
exec:执行一条shell命令httpGet:发起一个HTTP请求
它的最大价值在于:让你能在应用进程收到信号前,先完成一些必须由外部协调的操作。比如:
- 通知注册中心下线(如Nacos、Eureka)
- 将当前Pod从负载均衡器的健康检查池中移除(某些云厂商提供API)
- 触发一个分布式锁释放(避免其他实例误以为它还在工作)
一个经典案例:某消息队列消费者服务,使用Kafka。如果Pod直接被SIGTERM杀死,正在处理的那条消息会丢失(因为offset提交是异步的)。解决方案是在preStop中调用一个内部API,强制提交当前消费位点:
lifecycle: preStop: exec: command: ["/bin/sh", "-c", "curl -X POST http://localhost:8080/kafka/commit-offsets && sleep 2"]注意:preStop Hook的执行时间,会计入
terminationGracePeriodSeconds的总时长!所以上面的sleep 2不是随意加的,而是为了确保HTTP请求有足够时间完成。如果网络抖动,这个请求可能超时,导致preStop失败,进而跳过它直接发SIGTERM。
更稳健的做法是:把preStop Hook设计成“尽力而为”,而非“必须成功”。比如,用curl -f -s -o /dev/null --max-time 3,设置3秒超时,失败也不阻塞后续流程。毕竟,真正的优雅终止,核心还是应用自身对SIGTERM的响应能力。
3.3 SIGTERM到SIGKILL:应用进程的“生死判决书”
无论你有没有preStop Hook,SIGTERM都是必经之路。而应用能否抓住这个信号,决定了它是“优雅退场”还是“暴毙离线”。
Java应用:必须注册Shutdown Hook。Spring Boot 2.3+已内置,但需确认spring.lifecycle.timeout-per-shutdown-phase配置合理(默认30秒,建议调至45秒以匹配terminationGracePeriodSeconds)。
// Spring Boot中,可通过以下方式自定义关闭逻辑 @Bean public DisposableBean disposableBean() { return () -> { // 关闭数据库连接池 dataSource.close(); // 提交未完成的异步任务 taskExecutor.shutdown(); // 等待任务完成,最多30秒 taskExecutor.awaitTermination(30, TimeUnit.SECONDS); }; }Node.js应用:必须监听SIGTERM事件,并在回调中执行清理:
process.on('SIGTERM', () => { console.log('SIGTERM received, shutting down gracefully'); server.close(() => { console.log('HTTP server closed'); // 关闭数据库连接 db.close(); process.exit(0); }); // 设置超时,防止无限等待 setTimeout(() => { console.error('Forcing exit after 30s'); process.exit(1); }, 30000); });Python应用(Flask):使用signal模块捕获信号:
import signal import sys def graceful_shutdown(signum, frame): print(f"Received signal {signum}, shutting down...") # 关闭数据库连接 db.close() # 停止Flask服务器(需配合WSGI服务器如Gunicorn) sys.exit(0) signal.signal(signal.SIGTERM, graceful_shutdown) signal.signal(signal.SIGINT, graceful_shutdown)关键教训:所有清理逻辑,必须在同一个进程中完成。不要试图在preStop Hook里用
kill -15去杀自己的进程——这会造成竞态:preStop还没执行完,kubelet的倒计时已到,直接发SIGKILL,你的清理脚本根本没机会跑。
4. 实战排障:从日志碎片中还原优雅启停的完整证据链
再完美的设计,也架不住生产环境的千奇百怪。当“优雅”失效时,你不能靠猜,而要像侦探一样,从分散的日志、事件、指标中,拼凑出完整的启停时间线。以下是我在多个故障复盘中总结的标准化排查路径。
4.1 第一步:定位问题Pod的精确启停时间戳
Kubernetes事件(Event)是第一手线索。执行:
kubectl get events --sort-by='.lastTimestamp' | grep "pod-name"你会看到类似这样的记录:
10m Normal Scheduled pod/myapp-7d8b9c4f5d-abcde Successfully assigned default/myapp-7d8b9c4f5d-abcde to node-03 9m55s Normal Pulling pod/myapp-7d8b9c4f5d-abcde Pulling image "my-registry/myapp:v2.1" 9m40s Normal Pulled pod/myapp-7d8b9c4f5d-abcde Successfully pulled image "my-registry/myapp:v2.1" 9m38s Normal Created pod/myapp-7d8b9c4f5d-abcde Created container myapp 9m37s Normal Started pod/myapp-7d8b9c4f5d-abcde Started container myapp 5m20s Normal Killing pod/myapp-7d8b9c4f5d-abcde Stopping container myapp注意Started和Killing的时间差,这就是该Pod的实际存活时长(这里是4分17秒)。如果这个时间异常短(比如只有几秒),说明启动阶段就失败了;如果接近terminationGracePeriodSeconds(如30秒),则大概率是优雅终止失败,被SIGKILL强杀。
4.2 第二步:交叉验证容器内日志与Kubernetes事件
进入Pod查看日志:
kubectl logs myapp-7d8b9c4f5d-abcde --since=10m重点搜索关键词:
Starting/Started:应用启动完成的标志Shutting down/Gracefully stopping:收到SIGTERM的标志Exiting/Exit code:进程退出的标志
理想日志流应该是:
[INFO] Starting application... [INFO] Database connection pool initialized. [INFO] Redis client connected. [INFO] Application started in 28.5 seconds. [INFO] Shutting down gracefully... [INFO] Closing database connections... [INFO] Flushing cache... [INFO] Exiting with code 0.如果日志在Shutting down之后戛然而止,或者根本没有Shutting down字样,那就基本确定:应用没收到SIGTERM,或者收到了但没处理。此时,立刻检查kubectl describe pod输出中的Containers部分,看State是否为Terminated,Reason是否为OOMKilled(内存溢出被杀)或Error(启动失败)。
4.3 第三步:用Metrics锁定“假活”与“真死”的时间窗
Prometheus是终极裁判。你需要关注三个核心指标:
kube_pod_status_phase{phase="Running"}:Pod是否处于Running状态kube_pod_container_status_restarts_total:容器重启次数(突增说明livenessProbe频繁失败)container_cpu_usage_seconds_total和container_memory_usage_bytes:结合kube_pod_container_status_phase,判断Pod是否“假活”(Running但CPU/Mem为0)
一个典型故障模式:kube_pod_status_phase{phase="Running"}显示Pod一直Running,但container_cpu_usage_seconds_total在某个时间点后归零。这说明:Pod进程已被SIGKILL杀死,但kubelet还没来得及更新Pod状态(状态同步有延迟)。此时,kubectl get pods仍显示Running,但kubectl logs已报错Unable to retrieve container logs。
解决方案:在Prometheus中创建一个告警规则,当kube_pod_status_phase{phase="Running"}为1,且rate(container_cpu_usage_seconds_total[5m]) == 0持续超过1分钟时,触发告警。这能比kubectl get pods更早发现“僵尸Pod”。
4.4 第四步:深度剖析——为什么Readiness Probe“假阳性”
最棘手的问题,是readinessProbe一直返回200,但服务实际不可用。这时,必须深入容器内部,用kubectl exec直连诊断:
# 进入容器 kubectl exec -it myapp-7d8b9c4f5d-abcde -- /bin/sh # 检查应用进程是否在运行 ps aux | grep java # 检查端口监听状态 netstat -tuln | grep :8080 # 手动调用readinessProbe的端点,看真实响应 curl -v http://localhost:8080/actuator/health # 检查下游依赖(如MySQL) telnet mysql-host 3306我曾在一个故障中发现,/actuator/health返回{"status":"UP"},但手动执行SELECT 1却超时。追查发现,Actuator的HealthIndicator默认使用HikariCP的getConnection(),而这个方法在连接池耗尽时会阻塞,直到超时(默认30秒)。结果是,readinessProbe的timeoutSeconds: 1根本不够用,它总是超时失败,但因为failureThreshold设得很高,kubelet误以为“只是暂时慢”,继续发流量。
修复方案:为数据库HealthIndicator单独配置超时:
management: health: db: show-details: always # 关键:设置数据库健康检查超时为1秒 timeout: 1000这种“假阳性”,往往源于对探针底层实现的无知。每一次curl,背后都是一次真实的网络调用或系统调用。你必须像测试API一样,去测试你的探针。
5. 高阶实践:用eBPF观测容器启停的“上帝视角”
当传统日志和Metrics无法满足深度诊断需求时,eBPF是打开黑盒的终极钥匙。它能让你在内核层面,无侵入地观测SIGTERM的发送、进程的响应、网络连接的关闭,精度达到毫秒级。这不是未来科技,而是我们已在生产环境稳定运行两年的方案。
5.1 eBPF观测的核心价值:打破“黑盒”假设
Kubernetes的启停流程,涉及至少四个独立组件的协作:
- kubelet(发送信号)
- 容器运行时(如containerd,转发信号)
- Linux内核(投递SIGTERM到进程)
- 应用进程(捕获并处理信号)
传统工具(如strace)只能观测单个环节。而eBPF程序可以同时挂载在sys_enter_kill(kubelet调用kill系统调用)、tracepoint:sched:sched_process_exit(进程退出)、kprobe:tcp_close(TCP连接关闭)等多个内核事件点上,从而串联起完整因果链。
5.2 一个真实案例:定位“SIGTERM丢失”的元凶
某次发布后,大量Pod在terminationGracePeriodSeconds结束时被SIGKILL杀死,但应用日志里完全没有Shutting down字样。我们部署了一个简单的eBPF程序(基于BCC工具包):
from bcc import BPF from time import sleep # 加载eBPF程序,跟踪kill系统调用 bpf_text = """ #include <uapi/linux/ptrace.h> #include <linux/sched.h> struct data_t { u32 pid; u32 tgid; u64 timestamp; int sig; }; BPF_PERF_OUTPUT(events); int kprobe__sys_kill(struct pt_regs *ctx, pid_t pid, int sig) { if (sig == 15) { // SIGTERM struct data_t data = {}; data.pid = bpf_get_current_pid_tgid() >> 32; data.tgid = bpf_get_current_pid_tgid(); data.timestamp = bpf_ktime_get_ns(); data.sig = sig; events.perf_submit(ctx, &data, sizeof(data)); } return 0; } """ b = BPF(text=bpf_text) def print_event(cpu, data, size): event = b["events"].event(data) print(f"[{event.timestamp}] PID {event.pid} sent SIGTERM to {event.tgid}") b["events"].open_perf_buffer(print_event) print("Tracing SIGTERM... Hit Ctrl-C to end.") while True: try: b.perf_buffer_poll() except KeyboardInterrupt: exit()运行后,我们惊讶地发现:kubelet的PID(如12345)确实在Killing事件发生时,向目标Pod的init进程(PID 1)发送了SIGTERM。但eBPF在tracepoint:sched:sched_process_exit上,却捕捉不到该init进程的退出事件。进一步检查/proc/1/status,发现其State字段一直是S(睡眠),而非Z(僵尸)。
真相浮出水面:该Pod使用了--init容器(即pause容器),而pause容器的源码中,对SIGTERM的处理是忽略(SIG_IGN)。这意味着,kubelet发的SIGTERM,被pause容器吃掉了,根本没传给业务进程!业务进程一直在“假活”,直到30秒后被SIGKILL终结。
解决方案:在Deployment中显式设置shareProcessNamespace: true,并让业务容器作为PID 1,或者改用gcr.io/google_containers/pause-amd64:3.2(该版本已修复SIGTERM透传问题)。
5.3 落地建议:eBPF不是银弹,而是精准手术刀
eBPF能力强大,但绝不应成为日常运维工具。我的建议是:
- 仅在疑难杂症排查时启用:如反复出现的优雅终止失败、探针行为异常、性能毛刺。
- 用标准化脚本封装:将常用观测场景(如“跟踪SIGTERM全链路”、“统计容器启动各阶段耗时”)写成可一键部署的YAML。
- 与现有监控打通:将eBPF采集的关键事件(如
SIGTERM received,process exited)作为自定义指标,推送到Prometheus,纳入统一告警体系。
记住,eBPF的价值不在于炫技,而在于它能帮你回答那个最朴素的问题:“到底发生了什么?” 当所有日志都说“没问题”,而业务却在崩溃时,eBPF就是你唯一的证人。
6. 经验沉淀:一份可直接抄作业的启停配置检查清单
说了这么多原理和案例,最后给你一份我在所有项目上线前,强制执行的配置检查清单。它不是理论,而是从血泪教训中熬出来的“防坑指南”。每一项,都对应一个我亲手填过的坑。
6.1 启动配置黄金五条
Startup Probe必须启用,且failureThreshold × periodSeconds ≥ 应用P99启动耗时 × 2
- 错误示范:
failureThreshold: 3, periodSeconds: 10→ 最长等待30秒,但应用P99耗时45秒 - 正确做法:压测得到P99=45秒,设为
failureThreshold: 10, periodSeconds: 10(100秒宽限期)
- 错误示范:
Readiness Probe的initialDelaySeconds必须 ≤ startupProbe的总宽限期
- 错误示范:startupProbe宽限期100秒,readinessProbe的
initialDelaySeconds: 120→ 流量永远进不来 - 正确做法:
initialDelaySeconds: 5,让readinessProbe在startupProbe成功后立即开始工作
- 错误示范:startupProbe宽限期100秒,readinessProbe的
Readiness Probe的timeoutSeconds必须 ≤ 1秒,且检查逻辑必须包含核心依赖
- 错误示范:
timeoutSeconds: 30,检查/ping(不连DB) - 正确做法:
timeoutSeconds: 1,检查/actuator/health,并在配置中启用db和redisHealthIndicator
- 错误示范:
Liveness Probe的failureThreshold必须 ≥ 3,且检查项严禁包含外部依赖
- 错误示范:
failureThreshold: 1,检查curl -f http://mysql:3306 - 正确做法:
failureThreshold: 5,检查jps -l | grep MyApp(Java)或ps aux | grep node(Node.js)
- 错误示范:
所有Probe的periodSeconds必须是偶数,且避开10秒倍数(如12、14、16)
- 原因:kubelet的默认同步周期是10秒,如果所有Probe都设为10秒,会造成大量并发探测,冲击API Server。设为12秒,能有效错峰。
6.2 终止配置生死三线
terminationGracePeriodSeconds必须显式声明,且值 = (压测P95优雅关闭耗时)+ 15秒
- 错误示范:不声明,用默认30秒
- 正确做法:压测得P95=22秒,设为
terminationGracePeriodSeconds: 37
PreStop Hook必须存在,且类型为exec,内容为
sleep 2(为SIGTERM留出缓冲)- 错误示范:不写preStop,或写
httpGet(网络不稳定) - 正确做法:
command: ["/bin/sh", "-c", "sleep 2"]。这2秒,是给kubelet和容器运行时同步状态的黄金时间。
- 错误示范:不写preStop,或写
应用代码必须注册SIGTERM处理器,并在其中执行所有清理逻辑,且设置超时保护
- 错误示范:Java应用只依赖Spring Boot默认Hook,未配置
spring.lifecycle.timeout-per-shutdown-phase - 正确做法:显式配置
spring.lifecycle.timeout-per-shutdown-phase: 45s,并在Shutdown Hook中加入awaitTermination(45, TimeUnit.SECONDS)。
- 错误示范:Java应用只依赖Spring Boot默认Hook,未配置
6.3 日志与监控的强制要求
- 所有应用必须在启动完成时,打印
Application started in X.X seconds - 所有应用必须在收到SIGTERM时,打印
Received SIGTERM, shutting down... - Prometheus必须采集
kube_pod_container_status_restarts_total和container_last_seen,并配置告警:rate(kube_pod_container_status_restarts_total[1h]) > 0.1(1小时内重启超0.1次/小时)
这份清单,我已经在三个不同技术栈(Java/Spring Boot、Node.js/Express、Python/Flask)的项目中验证过。它不能保证100%不出问题,但能消灭90%以上的“低级优雅失败”。每次上线前,花5分钟对着清单逐条核对,远比半夜爬起来救火轻松得多。
最后分享一个小技巧:把这份清单做成一个Kubernetes Validating Admission Webhook。当任何Deployment被提交到集群时,Webhook自动检查其Probe和Lifecycle配置,不符合清单的,直接拒绝创建,并返回清晰的错误提示(如“readinessProbe initialDelaySeconds (120) > startupProbe total timeout (100)”)。这样,就把“事后补救”变成了“事前拦截”,这才是工程化的优雅。