
就绪探针返回 503 或超时,根本不是代码慢,是连接没建好
Go 应用在 K8s 里就绪(readiness)失败,90% 不是因为业务逻辑卡顿,而是 http.Client 第一次发请求时才初始化 DNS 解析、TLS 握手、TCP 连接——这些耗时叠加起来轻松破秒,而默认 readinessProbe 的 initialDelaySeconds 常设为 5,timeoutSeconds 却只有 1。Pod 启动后立刻被流量打进来,但连接池还是空的,自然 503。
- 别依赖「启动后睡几秒」这种土办法,它掩盖问题且拖慢滚动更新
- 在
main()初始化阶段主动触发一次到自身服务(或下游关键依赖)的健康检查请求,强制预热连接池和 TLS 会话复用 - 用带连接池的
http.Client,禁用http.DefaultClient:它默认MaxIdleConnsPerHost = 2,压测下根本不够 - 示例预热逻辑:
client := &http.Client{Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 100, IdleConnTimeout: 30 * time.Second, }} // 预热:对下游 /health 做一次 HEAD resp, _ := client.Head("https://api.internal/health") if resp != nil { resp.Body.Close() }
goroutine 泄漏让 readiness 探针越来越慢
就绪探针本身是 HTTP 请求,如果 handler 里启了 goroutine 去做异步检查(比如轮询数据库连通性),又没加 context 控制或 recover,一旦下游暂时不可用,goroutine 就卡住不退出。K8s 每 5 秒调一次 readiness,积压几十个卡死的 goroutine 后,整个 HTTP server 线程调度变慢,探针响应从 200ms 慢到 2s+,最终被判定 not ready。
- 所有 readiness handler 必须是同步、无阻塞、有明确超时的
- 避免在 handler 里调
time.Sleep、db.Query、http.Get等未封装超时的操作 - 用
context.WithTimeout(ctx, 500*time.Millisecond)包裹所有外部依赖调用 - 不要自己写「健康检查轮询器」,K8s 的探针就是轮询器——你的 handler 只需回答「此刻是否 ready」
资源预加载踩进 init() 陷阱:config 加载失败,但 Pod 已 Running
很多人把配置加载、证书读取、schema 初始化全塞进 init(),以为“早执行=早准备好”。但 init() 失败会导致 os.Exit(1),容器直接 crashloop;而成功了也不代表 runtime 就 ready——比如证书文件存在,但 tls.LoadX509KeyPair 返回的 *tls.Config 没被注入到 server,或者 config 结构体字段没校验(如空字符串的 DB URL),直到第一个请求进来才 panic。
- 把「可失败」的预加载逻辑移到
main()开头,并显式处理错误:if err != nil { log.Fatal(err) } - 对关键依赖做轻量级运行时验证:比如用
sql.Open+db.PingContext测试 DB 连通性,失败就os.Exit(1),让 K8s 重启而不是留个假 ready 的 Pod - 避免在
init()里做任何需要环境变量、文件系统、网络的初始化——它执行时机早于容器 runtime 环境完全就绪
连接池参数不匹配 K8s Service 的 endpoint 数量
K8s Service 背后可能有 3 个 Pod,但你的 http.Client 设了 MaxIdleConnsPerHost = 10,看起来够用。问题在于:Service 的 DNS 名(如 redis.default.svc.cluster.local)会被 kube-proxy 解析成 ClusterIP,而 Go 的 http.Transport 把这个 IP 当作单一 host。结果所有请求都挤在同一个连接池里,哪怕后端是多个实例,也无法实现连接分散。
立即学习“go语言免费学习笔记(深入)”;
- 若调用的是 headless Service(如
statefulset-0.redis.default.svc.cluster.local),DNS 解析出多个 A 记录,这时MaxIdleConnsPerHost才按每个 endpoint 分配 - 更稳的做法是关闭 keep-alive:
Transport.DisableKeepAlives = true,适用于短生命周期调用(如每秒几次的配置拉取) - 或者用客户端负载均衡:引入
github.com/hashicorp/go-retryablehttp,它支持基于 DNS SRV 的 endpoint 发现 - 检查实际连接数:
lsof -i -n -p $(pgrep myapp) | grep ESTABLISHED | wc -l,对比你设的MaxIdleConns,过低说明没打满,过高说明泄漏
就绪时间优化最易被忽略的一点:不是让应用“更快”,而是让它“每次响应都稳定可控”。连接预热、goroutine 控制、预加载时机、连接池粒度——这四件事没对齐,光调 initialDelaySeconds 只是在给问题盖布。










