根本原因是服务依赖未就绪时 readinessProbe 已开始探查,导致反复503;应使用原子变量标记预热完成状态,/readyz 仅读取该标志,避免每次探针都执行耗时检查。

Go 服务在 Kubernetes 里启动慢,光调大 initialDelaySeconds 是治标不治本——真正卡住的是依赖初始化(DB 连接池、gRPC client、配置热加载)没做完,而 readinessProbe 已经开始探查,结果反复 503、滚动更新卡死、Endpoint 始终为空。
为什么 readinessProbe 一配就失败?
常见现象是 Pod 处于 Running 状态但长期不进 Ready,kubectl get pods 显示 0/1,kubectl describe pod 里出现 Readiness probe failed。根本原因不是探针写错了,而是:服务还没准备好,探针就已经在问了。
- DB 连接池 warmup 要 8s,但
readinessProbe.initialDelaySeconds: 5—— 第一次探查时连接池甚至没建好 - gRPC client 初始化含重试逻辑,最长可能耗时 6s,但探针超时设成
timeoutSeconds: 2,直接判定失败 - 用
http.HandleFunc("/readyz", ...)注册,但 handler 里每次调db.PingContext()都新建连接,fd 耗尽或 goroutine 堆积导致响应卡住
/readyz 必须用原子状态 + 预热完成信号
别让每次探针都去“现场查”,而是让服务自己记下“我好了没”。启动流程中关键依赖就绪后,用原子变量置位,/readyz handler 只读这个标记。
- 声明全局状态:
var isReady int32,初始为 0 - 所有预热逻辑(DB ping、Redis auth、配置校验)完成后,调
atomic.StoreInt32(&isReady, 1) -
/readyzhandler 中只做:if atomic.LoadInt32(&isReady) == 0 { http.Error(w, "not ready", http.StatusServiceUnavailable); return } - 这样探针响应稳定在亚毫秒级,完全规避 I/O 和锁竞争
livenessProbe 别和 readinessProbe 共用路径
两个探针语义不同,混用同一 handler 是高频翻车点。比如把 livenessProbe 和 readinessProbe 都指向 /healthz,一旦 DB 挂了,/healthz 返回 503 → readiness 摘流量 ✅,但 liveness 也失败 → K8s 杀 Pod 重启 ❌,新 Pod 启动后照样连不上 DB,陷入 CrashLoopBackOff。
立即学习“go语言免费学习笔记(深入)”;
-
livenessProbe应指向轻量端点如/livez,只检查进程是否僵死(例如runtime.NumGoroutine()是否异常、主 channel 是否可 send) -
readinessProbe必须用/readyz,且只响应原子变量状态,绝不做任何阻塞操作 - Kubernetes 不会自动等你“准备好了再开探针”,它只按 YAML 里写的
initialDelaySeconds开始发请求——所以延迟值必须 ≥ 所有预热步骤的 p95 耗时
容器启动慢,要从镜像和启动顺序一起压
即使健康检查逻辑没问题,镜像太大、二进制初始化慢、init 函数干了重活,也会拖长整体就绪时间。
- 用
scratch或distroless/static基础镜像,禁用 CGO:CGO_ENABLED=0 go build -ldflags="-s -w" - 把数据库连接、gRPC dial、Redis client 初始化全部移到
main()开头,不要藏在某个 HTTP handler 里“懒加载” - 避免在
init()函数里做网络请求或文件读取;改用sync.Once控制单次初始化 - 如果预热确实无法压缩到 3s 内(比如要加载 GB 级模型),考虑用 Init Container 提前拉取/验证依赖服务可达性,主容器专注快速启动
最易被忽略的点:Kubernetes 的探针不是“等待机制”,它是“采样机制”。你不能指望它理解你的启动逻辑,只能靠你把状态暴露得足够早、足够稳、足够轻——否则所有配置调优都是在给故障留窗口。


















