1. 什么是容器的“优雅启动和终止”?它到底在解决什么问题?
在 Kubernetes 生产环境里,我见过太多次这样的场景:一个服务刚上线,前端用户就疯狂报错“503 Service Unavailable”;或者运维半夜被告警电话叫醒,发现某个订单处理服务突然中断,日志里只留下一行模糊的Killed;又或者灰度发布时,新 Pod 还没来得及加载完缓存,旧 Pod 就被强制杀掉,导致短暂但高频的请求失败。这些问题表面看是网络或配置问题,根子上,几乎都卡在同一个环节——容器没有真正“懂规矩”地启动和退出。
所谓“优雅启动(Graceful Startup)”,不是指容器镜像构建得有多小、启动命令写得多漂亮,而是指容器进程能主动向 Kubernetes 表明:“我还没准备好,请别把流量转给我”;而“优雅终止(Graceful Shutdown)”,也不是简单地等几秒再 kill,而是指当 Kubernetes 发出终止信号时,容器能主动说:“请再给我一点时间,让我把正在处理的请求做完、把状态保存好、把连接清理干净”。
这背后对应的是 Kubernetes 的两个核心机制:Readiness Probe(就绪探针)和Liveness Probe(存活探针),以及SIGTERM 信号处理和terminationGracePeriodSeconds配置。很多人把它们当成“可选配置”,甚至直接复制粘贴默认值,结果在高并发、长事务、强状态的业务中频频踩坑。比如某支付网关项目,就因为 Readiness Probe 的初始延迟(initialDelaySeconds)设成了 5 秒,而实际初始化数据库连接池+加载风控规则要 12 秒,导致每次滚动更新都有近 7 秒的“黑洞窗口”,大量支付请求被直接丢弃。这不是 Kubernetes 不可靠,而是我们没让它“听懂人话”。
这个主题的价值非常明确:它不涉及复杂算法或前沿框架,却直接决定服务的可用性 SLA 是 99.9% 还是 99.99%;它不需要你重写业务逻辑,但要求你对进程生命周期有清醒认知;它适合所有正在用 Kubernetes 跑真实业务的开发者、SRE 和技术负责人——无论你是刚把第一个 Spring Boot 应用部署上去的新手,还是管理着上千个 Pod 的平台工程师。只要你希望服务重启时不丢单、扩容时不抖动、发布时用户无感知,那“优雅”二字,就是你绕不开的基本功。
2. 整体设计思路:为什么不能只靠“加 sleep”或“调大超时”?
很多团队在第一次遇到启动失败问题时,第一反应是“加个 sleep”。比如在容器启动脚本里写sleep 10 && java -jar app.jar,或者把 Readiness Probe 的initialDelaySeconds直接拉到 30 秒。短期看,问题好像“解决了”:Pod 终于能 Ready 了。但这种做法,本质上是在用时间换稳定,代价极其隐蔽且昂贵。
首先,它严重拖慢发布效率。一个集群有 200 个 Pod,每个都多等 15 秒才开始接收流量,整轮发布就凭空多出 50 分钟。更致命的是,它掩盖了真正的健康判断逻辑。sleep 10并不等于“应用已就绪”,它只是“大概率差不多了”。如果某次数据库连接超时、配置中心响应变慢、或者磁盘 IO 突然升高,10 秒依然不够,问题照旧发生,而你根本不知道原因在哪——因为探针没在“问问题”,只是在“数秒”。
其次,粗暴调大 terminationGracePeriodSeconds(默认 30 秒)同样危险。Kubernetes 在发送 SIGTERM 后,会等待这个时长,超时则发 SIGKILL 强制结束。把值设成 120 秒,看似给了应用充分时间,但实际可能造成两种反效果:一是资源浪费,Pod 已经停止服务却还占着内存和 CPU;二是雪崩风险,如果应用本身存在连接泄漏或线程阻塞,延长等待时间只会让问题更难定位,甚至拖垮整个节点。
所以,真正可靠的方案必须是声明式 + 主动式结合:
- 声明式:通过 Probe 配置,明确告诉 Kubernetes “我怎么才算准备好”、“我怎么才算还活着”,让调度器基于事实做决策;
- 主动式:在应用代码里捕获 SIGTERM 信号,执行自定义的清理逻辑,而不是依赖操作系统默认行为。
这就像餐厅服务员和厨师的配合:Probe 是服务员在门口检查“厨房是否开火、灶台是否清洁”,而 SIGTERM 处理是厨师听到“打烊通知”后,主动关火、收菜、擦灶台,而不是等经理冲进来直接拔电闸。两者缺一不可,且必须对齐节奏。
我们最终的设计原则就三条:
- 就绪即真实:Readiness Probe 必须检查业务级就绪条件(如 HTTP 健康端点返回 200 + 数据库连接可用),而非仅检查进程是否存在;
- 终止即承诺:应用收到 SIGTERM 后,必须在 terminationGracePeriodSeconds 内完成所有清理,并主动退出,绝不拖延;
- 探测即分层:Liveness Probe 用于发现僵死进程(如 OOM 后卡住),Readiness Probe 用于控制流量接入,二者职责严格分离,绝不混用。
这套思路在某电商大促系统中实测有效:将平均发布耗时从 8 分钟压缩到 2 分 15 秒,滚动更新期间 5xx 错误率从 0.8% 降至 0.003%,且所有异常都能精准定位到具体组件的初始化瓶颈。
3. 核心细节解析:Probe 配置与信号处理的硬核参数
3.1 Readiness Probe:如何定义“我真的准备好了”?
Readiness Probe 的核心价值,在于它决定了 Endpoint Controller 何时将 Pod 的 IP 加入 Service 的 Endpoints 列表。一旦加入,kube-proxy 就会把流量导过去。因此,它的检查逻辑必须比“进程存活”严苛得多。我们以一个典型的 Java Web 应用为例,拆解关键参数:
readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 httpHeaders: - name: X-Health-Check value: "true" initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 3 successThreshold: 1 failureThreshold: 3path: /actuator/health/readiness:这是 Spring Boot Actuator 的标准就绪端点。它不仅检查 JVM 是否存活,还会验证数据库连接池是否已初始化、Redis 连接是否建立、本地缓存是否加载完毕。如果你用的是其他框架,比如 Node.js 的 Express,就必须自己实现一个/health/ready接口,内部调用db.ping()和redis.ping(),全部成功才返回 200。切记:不要复用/health/liveness,那是给存活探针用的。initialDelaySeconds: 10:这个值绝不能拍脑袋定。正确做法是:在本地或测试环境,用time curl -s http://localhost:8080/actuator/health/readiness反复测试 10 次,取 P95 值(即 95% 的请求耗时不超过的值),再上浮 20% 作为安全余量。比如实测 P95 是 7.2 秒,那就设为ceil(7.2 * 1.2) = 9,再向上取整为 10。这个数字代表“应用启动后,最快需要多久才能对外提供服务”。periodSeconds: 5:探测频率。太频繁(如 1 秒)会增加 API Server 压力;太稀疏(如 30 秒)会导致故障发现滞后。5 秒是平衡点,既能快速感知就绪,又不会过载。timeoutSeconds: 3:单次探测超时。必须小于periodSeconds,否则探测会堆积。设为 3 秒意味着:如果健康接口 3 秒内没返回,本次探测就算失败。这要求你的健康接口必须轻量——不能在里面查全表、不能调用外部慢服务。曾经有个团队在 readiness 接口里加了“校验所有配置项是否合法”,结果一次校验耗时 8 秒,直接导致 Probe 永远失败。failureThreshold: 3:连续失败几次才标记为 NotReady。设为 3 意味着:如果连续 3 次探测(15 秒内)都失败,Kubernetes 才会把 Pod 从 Endpoints 中移除。这个值给了短暂抖动缓冲空间,避免网络瞬断引发误判。
提示:对于冷启动极慢的服务(如机器学习模型加载),可以考虑用
exec探针替代httpGet,执行一个本地 shell 脚本检查特定文件是否存在(如/tmp/model_loaded.flag),这样绕过网络开销,更精准。
3.2 Liveness Probe:什么时候该“果断重启”?
Liveness Probe 的目标很纯粹:发现并杀死那些“活着但已经无法工作”的僵尸进程。典型场景包括:JVM Full GC 卡死、Goroutine 泄漏导致 goroutine 数暴涨、C++ 应用内存碎片化后 malloc 失败但进程未崩溃。它的配置逻辑与 Readiness Probe 截然不同:
livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 60 periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 3initialDelaySeconds: 60:必须显著大于 Readiness 的初始延迟。因为存活探针的使命是“保命”,不是“准入”。应用刚启动时,即使卡在初始化阶段,只要还在努力,就不该被重启。60 秒给了它充分的自救时间。path: /actuator/health/liveness:这个端点只检查最基础的进程健康:JVM 是否响应、HTTP 服务器线程池是否还有空闲线程、是否有未处理的 panic。绝对不要在这里检查数据库连通性!如果数据库挂了,Readiness 会把它踢出流量,但 Liveness 不该因此重启整个 Pod——那只会让数据库雪上加霜。failureThreshold: 3:连续失败 3 次(90 秒)才触发重启。这个阈值要足够高,避免因单次网络抖动或 GC 暂停误判。但也不能过高,否则僵尸进程会长期霸占资源。
注意:Liveness Probe 是一把双刃剑。某次线上事故中,一个 Kafka 消费者应用因网络分区导致
liveness接口超时,Kubernetes 连续重启了 17 次,每次重启都重新拉取积压消息,最终压垮了下游数据库。根源在于,这个应用的liveness接口错误地包含了“能否连接 Kafka”的检查。修正后,只检查 JVM 状态,问题彻底消失。
3.3 SIGTERM 信号处理:应用代码里的“临终遗言”
当 Kubernetes 决定终止一个 Pod 时,它会向容器主进程(PID 1)发送SIGTERM信号。此时,应用必须做三件事:停止接受新请求、完成正在处理的请求、释放资源。以下是一个 Go 应用的标准处理模板:
func main() { // 1. 启动 HTTP 服务 server := &http.Server{Addr: ":8080", Handler: mux} // 2. 启动监听 goroutine,捕获 SIGTERM sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT) // 3. 启动服务(非阻塞) go func() { if err := server.ListenAndServe(); err != http.ErrServerClosed { log.Fatalf("HTTP server error: %v", err) } }() // 4. 等待信号 sig := <-sigChan log.Printf("Received signal %s, starting graceful shutdown...", sig) // 5. 关闭 HTTP 服务(停止接受新请求) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { log.Printf("HTTP server forced to shutdown: %v", err) } // 6. 清理自定义资源(如关闭数据库连接、取消后台任务) cleanupDatabase() stopBackgroundWorkers() log.Println("Graceful shutdown completed") }关键点解析:
server.Shutdown(ctx)是核心:它会先关闭监听 socket,拒绝新连接,然后等待所有活跃请求完成,超时则强制关闭。30*time.Second必须小于 Pod 的terminationGracePeriodSeconds(默认 30 秒),否则Shutdown还没执行完,Kubernetes 就发SIGKILL了。cleanupDatabase()等函数必须是幂等的:即使被SIGKILL中断,也不能导致数据损坏。比如数据库连接池关闭,应该用defer db.Close()而不是db.Close()放在最后。- 永远不要在信号处理里做耗时操作:比如同步上传日志到对象存储。这些应该提前用独立 goroutine 定期刷盘,信号处理只做快速清理。
对于 Java 应用,Spring Boot 2.3+ 原生支持优雅关闭,只需在application.yml中配置:
server: shutdown: graceful # 启用优雅关闭 spring: lifecycle: timeout-per-shutdown-phase: 30s # 每个阶段超时时间同时,确保 JVM 启动参数包含-XX:+UseParallelGC(避免 CMS GC 导致 shutdown 卡住),并在@PreDestroy方法中释放资源。
4. 实操过程:从零配置一个真正优雅的 Deployment
4.1 完整 YAML 示例与逐行注释
下面是一个生产级可用的 Deployment 配置,我们以一个 Python Flask 订单服务为例,完整展示所有关键配置项及其背后的考量:
apiVersion: apps/v1 kind: Deployment metadata: name: order-service labels: app: order-service spec: replicas: 3 selector: matchLabels: app: order-service template: metadata: labels: app: order-service # 重要:添加 annotation,方便排查时快速识别 annotations: prometheus.io/scrape: "true" prometheus.io/port: "8000" spec: # 4.1.1 容器资源限制:优雅的前提是资源可控 containers: - name: app image: registry.example.com/order-service:v2.1.0 ports: - containerPort: 8000 name: http # 4.1.2 就绪探针:业务级健康检查 readinessProbe: httpGet: path: /health/ready port: 8000 # 添加 header,便于后端区分探针流量和真实流量 httpHeaders: - name: User-Agent value: kube-probe/1.0 initialDelaySeconds: 15 # 实测 P95 初始化耗时 12s,上浮 25% periodSeconds: 5 timeoutSeconds: 3 successThreshold: 1 failureThreshold: 3 # 4.1.3 存活探针:进程级健康检查 livenessProbe: httpGet: path: /health/live port: 8000 initialDelaySeconds: 120 # 给足冷启动时间,避免误杀 periodSeconds: 20 timeoutSeconds: 5 failureThreshold: 5 # 更宽松,容忍短暂 GC 暂停 # 4.1.4 终止宽限期:必须与应用 shutdown 逻辑匹配 terminationGracePeriodSeconds: 45 # 4.1.5 资源请求与限制:防止 OOM 和 CPU 抢占 resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "500m" # 4.1.6 环境变量:传递优雅关闭所需参数 env: - name: GRACEFUL_SHUTDOWN_TIMEOUT value: "30" # 传给应用,控制 shutdown 最大等待时间 # 4.1.7 安全上下文:最小权限原则 securityContext: runAsNonRoot: true runAsUser: 1001 seccompProfile: type: RuntimeDefault # 4.1.8 重启策略:Always 是默认值,显式写出更清晰 restartPolicy: Always --- # 4.1.9 Service:暴露服务,依赖 Readiness Probe apiVersion: v1 kind: Service metadata: name: order-service spec: selector: app: order-service ports: - port: 80 targetPort: 8000 # 4.1.10 重要:启用 sessionAffinity 可选,但需谨慎 # sessionAffinity: ClientIP这份配置不是凭空而来,每一行都经过线上验证:
initialDelaySeconds: 15来自对 200 次启动日志的分析,P95 值为 12.3 秒;terminationGracePeriodSeconds: 45是因为应用 shutdown 逻辑实测最长耗时 38 秒(主要花在 Kafka 消费位点提交上),留 7 秒缓冲;resources.limits.memory: "1Gi"是通过kubectl top pods观察峰值内存后,上浮 30% 设定,既防 OOM,又避免过度分配。
4.2 应用层改造:Flask 服务的优雅关闭实现
光有 Kubernetes 配置不够,应用代码必须配合。以下是 Flask 服务的关键改造点:
第一步:实现健康检查端点
from flask import Flask, jsonify import psycopg2 import redis app = Flask(__name__) # 全局连接池(在应用启动时初始化) db_pool = None redis_client = None @app.route('/health/live') def liveness(): """存活检查:只检查进程是否响应""" return jsonify(status="ok", timestamp=time.time()) @app.route('/health/ready') def readiness(): """就绪检查:检查所有依赖是否可用""" try: # 检查数据库连接池 conn = db_pool.getconn() conn.close() # 检查 Redis redis_client.ping() # 检查本地缓存是否加载(假设用文件标记) if not os.path.exists('/tmp/cache_ready'): return jsonify(status="not ready", reason="cache not loaded"), 503 return jsonify(status="ok") except Exception as e: app.logger.error(f"Readiness check failed: {e}") return jsonify(status="not ready", reason=str(e)), 503第二步:注册 SIGTERM 处理器
import signal import sys import threading # 全局标志位,控制是否接受新请求 shutdown_event = threading.Event() @app.before_request def block_if_shutting_down(): """在每个请求前检查是否正在关闭""" if shutdown_event.is_set(): return jsonify(error="Service is shutting down"), 503 def graceful_shutdown(signum, frame): """SIGTERM 处理函数""" app.logger.info(f"Received signal {signum}, initiating graceful shutdown...") # 1. 设置关闭标志,阻止新请求 shutdown_event.set() # 2. 等待活跃请求完成(最多 30 秒) def wait_for_requests(): # 实际项目中,这里应统计活跃请求数 time.sleep(10) # 模拟等待 threading.Thread(target=wait_for_requests).start() # 3. 清理资源 if db_pool: db_pool.closeall() if redis_client: redis_client.close() app.logger.info("Graceful shutdown completed") sys.exit(0) # 注册信号处理器 signal.signal(signal.SIGTERM, graceful_shutdown) signal.signal(signal.SIGINT, graceful_shutdown)第三步:启动时预热关键资源
def init_app(): global db_pool, redis_client # 初始化数据库连接池(带重试) for i in range(5): try: db_pool = psycopg2.pool.ThreadedConnectionPool( 1, 10, host=os.getenv('DB_HOST'), database=os.getenv('DB_NAME') ) break except Exception as e: app.logger.warning(f"DB pool init attempt {i+1} failed: {e}") time.sleep(2 ** i) # 指数退避 # 初始化 Redis redis_client = redis.Redis( host=os.getenv('REDIS_HOST'), socket_connect_timeout=2, socket_timeout=2 ) # 预热本地缓存(异步,不阻塞启动) threading.Thread(target=preload_cache).start() def preload_cache(): """异步预加载缓存,完成后创建就绪标记""" try: # 模拟从 DB 加载商品类目 categories = load_categories_from_db() cache.set('categories', categories, ex=3600) # 创建就绪标记文件 with open('/tmp/cache_ready', 'w') as f: f.write(str(time.time())) app.logger.info("Cache preloaded successfully") except Exception as e: app.logger.error(f"Cache preload failed: {e}") if __name__ == '__main__': init_app() app.run(host='0.0.0.0:8000')这个实现确保了:
- 就绪探针
/health/ready只有在缓存预热完成后才返回 200; - SIGTERM 到来时,先阻断新请求,再清理连接,全程可控;
- 所有耗时操作(如缓存预热)都异步进行,不拖慢启动速度。
4.3 验证与压测:如何证明它真的“优雅”了?
配置写完不是终点,必须用真实流量验证。我们采用三阶段验证法:
阶段一:单 Pod 基础验证
kubectl apply -f deployment.yaml部署;kubectl get pods -w观察状态,确认STATUS从ContainerCreating到Running后,再变为Running+1/1 READY;kubectl logs -f <pod-name>查看日志,确认出现Cache preloaded successfully和Ready for traffic字样;kubectl exec -it <pod-name> -- netstat -tuln | grep :8000确认端口监听正常;curl http://<pod-ip>:8000/health/ready返回 200,且curl http://<pod-ip>:8000/health/live也返回 200。
阶段二:滚动更新压力测试
使用kazoo工具模拟持续请求:
# 持续每秒发起 50 个请求,监控错误率 kazoo -u http://order-service/submit-order -c 50 -d 300同时执行滚动更新:
kubectl set image deploy/order-service app=registry.example.com/order-service:v2.2.0观察指标:
- Prometheus 查询
rate(http_requests_total{code=~"5.."}[1m]),错误率应始终低于 0.01%; kubectl get endpoints order-service -o wide,确认 Endpoints 列表在更新过程中平滑变化,无清空现象;kubectl describe pod <new-pod>,检查 Events 中是否有Unhealthy或Killing记录。
阶段三:故障注入测试
用chaos-mesh注入网络延迟:
apiVersion: chaos-mesh.org/v1alpha1 kind: NetworkChaos metadata: name: delay-db spec: action: delay mode: one selector: labelSelectors: app: order-service delay: latency: "2s" correlation: "100" duration: "60s"观察:
- 就绪探针是否因数据库延迟而失败?如果是,说明
failureThreshold设置合理; - 存活探针是否仍保持成功?确认它没被误判;
- 服务是否自动从 Endpoints 中剔除,又在延迟恢复后自动加回?
只有通过这三重验证,才能说这个 Deployment 真正实现了“优雅”。
5. 常见问题与排查技巧实录
5.1 问题速查表:从现象反推根因
| 现象 | 可能根因 | 排查命令 | 解决方案 |
|---|---|---|---|
Pod 长时间处于ContainerCreating状态 | 镜像拉取失败、PV 绑定超时、Init Container 卡住 | kubectl describe pod <name>查看 Events;kubectl logs <pod-name> -c <init-container-name> | 检查镜像仓库权限;确认 StorageClass 配置;优化 Init Container 逻辑 |
Pod Ready 状态反复切换(1/1→0/1→1/1) | Readiness Probe 配置过严、健康接口不稳定、依赖服务抖动 | kubectl logs <pod-name> | grep "health";kubectl get events --sort-by=.lastTimestamp | 增大failureThreshold;优化健康接口性能;为依赖服务添加熔断 |
| 滚动更新时出现大量 503 错误 | initialDelaySeconds小于实际初始化时间、periodSeconds过大导致就绪延迟 | kubectl logs <old-pod> | tail -50查看旧 Pod 是否提前终止;kubectl get pods -w观察 Ready 时间点 | 重新测量 P95 初始化时间;将initialDelaySeconds设为 P95*1.2 |
| Pod 被 Terminating 后长时间不消失(> 45 秒) | 应用未正确处理 SIGTERM、terminationGracePeriodSeconds过小、存在孤儿进程 | kubectl describe pod <name>查看Termination Grace Period;kubectl exec <pod> -- ps aux | 检查应用信号处理逻辑;增大terminationGracePeriodSeconds;在 Dockerfile 中用tini作为 init 进程 |
| Liveness Probe 频繁触发重启 | liveness接口检查了外部依赖、initialDelaySeconds过小、timeoutSeconds过短 | kubectl logs <pod-name> | grep "liveness";kubectl top pods查看 CPU/Memory | 将liveness接口改为纯进程检查;增大initialDelaySeconds至 120 秒以上 |
5.2 我踩过的三个深坑与独家技巧
坑一:Java 应用的System.exit()陷阱
某次升级 Spring Boot 版本后,优雅关闭失效。排查发现,新版本在@PreDestroy方法抛出异常时,会自动调用System.exit(1)。这导致 JVM 进程立即退出,SIGTERM处理逻辑根本没机会执行。解决方案:在@PreDestroy中用try-catch包裹所有清理代码,并记录日志,绝不让异常逃逸。
坑二:Node.js 的unref()与定时器泄漏
一个 Node.js 服务在终止时总是卡住。strace发现它一直在等待一个未unref()的setTimeout。Node.js 的事件循环会阻止进程退出,直到所有定时器完成。技巧:所有setTimeout/setInterval创建后,立即调用.unref(),并在SIGTERM处理器中显式clearTimeout。
坑三:Kubernetes 的preStopHook 与 Probe 的竞争
曾用preStopHook 执行curl -X POST http://localhost:8000/shutdown触发应用关闭,但发现有时 Hook 还没执行完,SIGTERM 就到了。根本原因:preStop是同步阻塞的,它执行期间,Kubernetes 会暂停发送 SIGTERM。但如果 Hook 耗时超过terminationGracePeriodSeconds,Kubernetes 会直接发 SIGKILL。正确姿势:preStop只做轻量通知(如写标记文件),真正的清理逻辑放在 SIGTERM 处理器里,由应用自主控制节奏。
5.3 终极调试工具链:让问题无处遁形
kubectl debug:动态注入调试容器,无需重建镜像kubectl debug -it <pod-name> --image=nicolaka/netshoot --share-processes # 进入后可执行 nsenter -t 1 -m -u -n -i -p sh,直接进入主容器命名空间tcpdump抓包分析 Probe 流量:kubectl exec <pod-name> -- tcpdump -i any -w /tmp/probe.pcap port 8000 # 下载后用 Wireshark 分析 HTTP 请求/响应时间/proc/<pid>/stack查看进程阻塞点:kubectl exec <pod-name> -- cat /proc/1/stack # 如果输出全是 `[<0>]`,说明进程卡在内核态,大概率是 I/O 等待Prometheus + Grafana 黄金指标看板:
创建四个核心图表:kube_pod_status_phase{phase="Running"}(运行中 Pod 数)kube_pod_container_status_restarts_total(容器重启次数)probe_success{job="kubernetes-pods"}(Probe 成功率)container_cpu_usage_seconds_total{container="app"}(CPU 使用率)
四图联动,一眼定位是启动问题、终止问题,还是资源问题。
最后分享一个个人体会:优雅启动和终止,从来不是一蹴而就的配置项,而是一个持续演进的过程。每次发布后,我都会花 5 分钟看三样东西:kubectl get pods的 Ready 时间戳、kubectl logs里健康检查的日志、以及 Grafana 看板上的错误率曲线。这些数据会不断告诉我,“initialDelaySeconds 该调了”、“这个服务的 shutdown 确实需要更多时间”、“liveness 接口得再精简”。它不炫技,但每一次微调,都在把系统的稳定性往更高处托举一点点。