Posted in

Golang服务优雅下线失效真相:SIGTERM处理漏洞、连接池残留、gRPC流未关闭三大致命盲区(附可落地Checklist)

第一章:Golang服务优雅下线失效真相全景透视

Golang服务在Kubernetes滚动更新、手动扩缩容或CI/CD发布过程中频繁出现“请求502/连接拒绝”或“少量请求丢失”,表面归因于“未等请求处理完就退出”,实则暴露了对信号处理、HTTP服务器生命周期及依赖组件协同下线的系统性认知盲区。

信号捕获与默认行为陷阱

Go标准库http.Server默认仅响应os.Interrupt(Ctrl+C)和syscall.Kill,但容器环境主流发送的是SIGTERM。若未显式监听该信号,进程将被内核强制终止,Shutdown()根本无机会执行。正确做法是:

server := &http.Server{Addr: ":8080", Handler: mux}
// 启动服务 goroutine
go func() {
    if err := server.ListenAndServe(); err != http.ErrServerClosed {
        log.Fatal(err)
    }
}()

// 阻塞等待 SIGTERM
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
<-sigChan // 等待信号

// 执行优雅关闭
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
    log.Printf("HTTP server shutdown error: %v", err)
}

中间件与长连接未同步退出

中间件(如JWT鉴权、日志记录)若持有独立goroutine或未实现http.Handler接口的ServeHTTP阻塞等待,会导致Shutdown()返回后仍有活跃协程。必须确保所有中间件在server.Shutdown()前完成清理,例如:

  • 使用sync.WaitGroup跟踪中间件goroutine;
  • 将长轮询/流式响应(SSE、gRPC streaming)的context.Contextserver.Shutdown()绑定;
  • 数据库连接池需调用db.Close()而非仅释放变量。

依赖组件下线时序错位

服务下线流程常忽略下游依赖的退出依赖关系:

组件 关键动作 必须早于
消息队列消费者 调用consumer.Close() http.Server.Shutdown()
Redis连接池 pool.Close() + Wait() HTTP服务器关闭完成
Prometheus注册 prometheus.Unregister() 任意指标上报停止前

若顺序颠倒,可能触发panic(如向已关闭的连接池写入)或监控数据截断。建议使用统一的Shutdowner接口聚合各组件关闭逻辑,并按逆启动顺序执行。

第二章:SIGTERM信号处理的隐性陷阱与加固实践

2.1 Go runtime signal.Notify 机制的生命周期盲区分析

Go 的 signal.Notify 将操作系统信号绑定到 chan os.Signal,但其生命周期完全依赖用户对 channel 的管理——runtime 不持有引用,亦不感知 channel 是否已关闭或被 GC。

信号注册与 goroutine 泄漏风险

ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT)
// 若 ch 未显式关闭且无接收者,Notify 持有 channel 引用 → goroutine 长期阻塞

signal.Notify 内部通过 sig.send 向 channel 发送信号;若 channel 已满且无接收者,发送协程永久阻塞在 send 路径,无法被 GC 回收。

生命周期关键依赖点

  • ✅ 显式调用 signal.Stop(ch) 可解除注册并释放内部 handler
  • ❌ 仅 close(ch) 不触发 Notify 自动注销
  • ⚠️ channel 被 GC 时,runtime 不会自动清理关联的 signal handler(存在内存与 goroutine 泄漏)
场景 是否自动清理 风险
signal.Stop(ch)close(ch) 安全
close(ch) handler 残留 + goroutine 阻塞
ch 被 GC(无强引用) handler 泄漏,SIGINT 等仍被转发
graph TD
    A[signal.Notify(ch, SIGINT)] --> B{ch 是否有活跃接收?}
    B -->|是| C[信号正常送达]
    B -->|否| D[goroutine 在 send 处永久阻塞]
    D --> E[handler 无法 GC,泄漏]

2.2 主goroutine阻塞导致信号丢失的典型场景复现与验证

复现核心逻辑

main goroutine 被 time.Sleep 或系统调用长期阻塞时,Go 运行时无法及时调度信号处理协程(如 signal.Notify 所启动的接收循环),导致 SIGINT/SIGTERM 在阻塞窗口内被内核丢弃。

package main

import (
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    sigCh := make(chan os.Signal, 1) // 缓冲容量为1,仅能暂存1个信号
    signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        sig := <-sigCh
        println("received:", sig.String()) // 实际可能永不执行
    }()

    time.Sleep(5 * time.Second) // 主goroutine阻塞期间发送信号 → 丢失
}

逻辑分析sigCh 容量为 1,若信号在 go func() 启动前或 time.Sleep 期间抵达,将被写入通道;但若主 goroutine 阻塞时信号已送达且通道未被消费(因接收协程尚未调度),而通道满,则后续信号被内核静默丢弃(POSIX 行为)。Go 不保证信号排队。

关键参数说明

  • make(chan os.Signal, 1):缓冲区大小决定可暂存信号数量,(无缓冲)更易丢失
  • signal.Notify:注册后信号由运行时异步转发至通道,依赖 goroutine 调度及时性

信号丢失对比表

场景 主goroutine状态 信号发送时机 是否丢失 原因
正常运行 活跃调度 任意时刻 接收协程可及时消费
time.Sleep(5s) 全阻塞 第2秒 信号写入时通道未就绪或已满
runtime.LockOSThread() + 系统调用 OS线程独占阻塞 阻塞中 Go调度器无法抢占

修复路径示意

graph TD
    A[主goroutine阻塞] --> B{是否启用信号通道?}
    B -->|否| C[信号被内核丢弃]
    B -->|是| D[检查通道缓冲与消费及时性]
    D --> E[增大缓冲/提前启动监听/使用 select default 防阻塞]

2.3 context.WithCancel + signal.Notify 组合模式的正确范式实现

核心设计原则

  • context.WithCancel 提供可主动终止的生命周期控制;
  • signal.Notify 将系统信号(如 SIGINT, SIGTERM)转为 Go 通道事件;
  • 二者需共享 cancel 函数,确保信号触发时所有派生 context 同步失效。

典型实现代码

func runServer() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        <-sigCh
        log.Println("received shutdown signal")
        cancel() // 主动取消,传播至所有子 context
    }()

    // 启动依赖 ctx 的服务(如 HTTP server、worker pool)
    httpServer := &http.Server{Addr: ":8080", Handler: nil}
    go func() {
        if err := httpServer.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()

    // 等待 cancel 或 panic
    <-ctx.Done()
    log.Println("shutting down gracefully...")
    _ = httpServer.Shutdown(context.WithTimeout(context.Background(), 5*time.Second))
}

逻辑分析cancel() 调用后,ctx.Done() 关闭,所有监听该 channel 的 goroutine(如 httpServer.Shutdown)可安全退出。signal.Notify 使用带缓冲通道避免阻塞,defer cancel() 保障异常路径下资源清理。

常见陷阱对照表

错误模式 后果 正确做法
在 signal handler 中直接调用 os.Exit() 跳过 deferShutdown() 仅调用 cancel(),交由主流程统一收尾
多次调用 signal.Notify() 同一 channel 信号重复注册,导致多次 cancel 单次注册,复用同一 channel
graph TD
    A[启动服务] --> B[注册信号通道]
    B --> C[启动 goroutine 监听 sigCh]
    C --> D{收到 SIGINT/SIGTERM?}
    D -->|是| E[调用 cancel()]
    D -->|否| F[继续运行]
    E --> G[ctx.Done() 关闭]
    G --> H[各组件响应 Done 并清理]

2.4 多阶段退出协调:从收到SIGTERM到进程终止的时序建模与压测验证

信号捕获与优雅退场入口

func setupSignalHandler() {
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
    go func() {
        <-sigChan
        log.Info("Received SIGTERM, initiating graceful shutdown")
        shutdownCoordinator.Start() // 触发多阶段协调器
    }()
}

该代码注册异步信号监听,避免阻塞主流程;shutdownCoordinator.Start() 是协调中枢,确保后续阶段按依赖顺序触发。

阶段化退出时序(单位:ms)

阶段 耗时上限 关键动作
连接 draining 3000 拒绝新连接,完成活跃请求
数据同步 5000 刷盘未提交事务、上报指标快照
资源释放 1000 关闭DB连接池、注销服务发现

协调状态流转

graph TD
    A[收到 SIGTERM] --> B[draining 状态]
    B --> C[同步中]
    C --> D[资源释放]
    D --> E[exit 0]

2.5 生产环境SIGTERM响应延迟诊断工具链(pprof+trace+自定义signal logger)

当服务收到 SIGTERM 后迟迟不退出,常因阻塞型清理逻辑或 goroutine 泄漏导致。需协同三类观测能力定位瓶颈。

信号捕获与时间戳打点

func initSignalLogger() {
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGTERM)
    go func() {
        start := time.Now()
        <-sigCh
        log.Printf("✅ SIGTERM received at %s", start.Format(time.RFC3339))
        // 记录至专用 metric 或日志流,供后续关联分析
    }()
}

该代码在进程启动时注册异步信号监听,精确记录 SIGTERM 到达系统时间点(纳秒级精度),避免 time.Now() 被调度延迟污染;make(chan os.Signal, 1) 确保信号不丢失。

三元诊断联动策略

工具 观测维度 关键参数示例
pprof 阻塞 Goroutine http://:6060/debug/pprof/goroutine?debug=2
runtime/trace GC/调度/阻塞事件 trace.Start(w) + trace.Stop() 包裹 shutdown 流程
自定义 logger 信号到达 vs 退出完成耗时 log.WithField("delta_ms", time.Since(start).Milliseconds())

全链路时序归因流程

graph TD
    A[SIGTERM抵达内核] --> B[signal logger打点]
    B --> C[pprof goroutine dump]
    B --> D[trace.Start/shutdown/Stop]
    C & D --> E[对齐时间轴:信号时刻 vs 阻塞调用栈 vs GC STW]
    E --> F[定位延迟根因:如 sync.WaitGroup.Wait 阻塞]

第三章:HTTP/HTTP2连接池残留引发的长连接悬挂问题

3.1 net/http.Server.Close() 与 Shutdown() 的语义差异与误用反模式

核心语义对比

方法 是否等待活跃连接完成 是否接受新连接 是否阻塞调用 适用场景
Close() ❌ 立即终止 ✅ 直接关闭监听 ✅ 同步返回 测试/强制中断
Shutdown() ✅ 可配置超时等待 ❌ 拒绝新请求 ✅ 阻塞至完成 生产环境优雅下线

典型误用反模式

  • ❌ 在 SIGTERM 处理中调用 Close() → 导致正在传输的响应被截断;
  • ❌ 调用 Shutdown() 后未设置 ctx, cancel := context.WithTimeout(...) → 无限期挂起。
// ✅ 正确的优雅关闭流程
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
    log.Printf("server shutdown error: %v", err) // 可能为 context.DeadlineExceeded
}

Shutdown() 依赖上下文控制等待边界;Close() 不触发 Serve() 返回,而 Shutdown() 会令 Serve() 返回 http.ErrServerClosed

3.2 连接池未主动驱逐导致客户端重试失败的真实案例还原

故障现象

某金融系统在数据库主从切换后,部分 HTTP 请求持续返回 500,日志显示 Connection refused,但服务端连接数正常,且新请求可成功建立连接。

根本原因

HikariCP 默认配置下未启用连接存活检测(connection-test-query 已弃用),且 validation-timeoutidle-timeout 不匹配,导致失效连接滞留池中超过 30 分钟。

关键配置对比

参数 当前值 推荐值 说明
max-lifetime 1800000ms (30min) 1200000ms 防止连接被中间件静默回收
keepalive-time 0(禁用) 30000ms 每30秒发送 SELECT 1 探活
connection-init-sql /*+ db-readonly */ SELECT 1 切换后快速暴露只读库不可写

修复代码示例

HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://proxy:3306/app");
config.setKeepaliveTime(30_000); // 启用保活探针
config.setMaxLifetime(1_200_000); // 缩短最大生命周期
config.setConnectionInitSql("SELECT 1"); // 初始化即验证连通性

keepaliveTime 在 HikariCP 4.0+ 中生效,需配合 leak-detection-threshold=60000 使用;maxLifetime 应比 MySQL wait_timeout(默认28800s)至少短 20%,避免连接被服务端单方面关闭。

故障链路还原

graph TD
    A[客户端发起重试] --> B{连接池返回旧连接}
    B --> C[连接指向已下线的旧主库]
    C --> D[TCP RST 或超时]
    D --> E[客户端抛出 SQLException]
    E --> F[重试逻辑未捕获底层连接异常]

3.3 基于 http.Transport.IdleConnTimeout 与 server.SetKeepAlivesEnabled 的协同调优策略

HTTP 连接复用依赖客户端空闲连接管理与服务端保活能力的严格对齐。若 http.Transport.IdleConnTimeout(如30s)短于服务端 Keep-Alive 超时,连接可能在复用前被客户端主动关闭。

客户端与服务端超时对齐原则

  • 客户端 IdleConnTimeout 应 ≤ 服务端 Keep-Alive 实际有效时间
  • server.SetKeepAlivesEnabled(true) 启用后,Go 默认 KeepAlive 超时为 3m,但受 ReadTimeout/WriteTimeout 制约

典型协同配置示例

// 客户端 Transport 配置
tr := &http.Transport{
    IdleConnTimeout: 25 * time.Second, // 留5s安全余量
    MaxIdleConns:        100,
    MaxIdleConnsPerHost: 100,
}

逻辑分析:设服务端实际 KeepAlive 可用窗口为30s(由 ReadTimeout=35s 保障),客户端设为25s可避免“连接已关闭但请求仍尝试复用”的 net/http: HTTP/1.x transport connection broken 错误;MaxIdleConnsPerHost 需 ≥ 并发峰值连接数,否则触发新建连接抖动。

超时关系对照表

组件 参数 推荐值 作用
客户端 IdleConnTimeout ≤ 服务端可用 KeepAlive 时间 控制空闲连接存活上限
服务端 SetKeepAlivesEnabled(true) 必须显式启用 启用 TCP keepalive 及 HTTP Connection: keep-alive
服务端 ReadTimeout > IdleConnTimeout + 预估最大请求耗时 防止连接因读超时被意外中断
graph TD
    A[客户端发起请求] --> B{连接池中存在空闲连接?}
    B -->|是| C[复用连接]
    B -->|否| D[新建TCP连接]
    C --> E[检查 IdleConnTimeout 是否超时]
    E -->|未超时| F[发送请求]
    E -->|已超时| G[关闭旧连接,新建连接]

第四章:gRPC流式服务未优雅终止的深层根因与闭环方案

4.1 gRPC Server.Shutdown() 对活跃Stream的默认行为解析与源码级验证

默认行为:优雅终止,但不强制中断活跃流

Server.Shutdown() 启动优雅关闭流程,等待所有已接受连接完成处理,但对已建立的 streaming RPC(如 stream.Send() 中的长连接)不会主动关闭或取消上下文——除非客户端断开或超时触发。

源码关键路径验证

// server.go: Shutdown()
func (s *Server) Shutdown(ctx context.Context) error {
    s.mu.Lock()
    s.quit = true // 标记不再接受新连接
    s.mu.Unlock()

    // 注意:此处未遍历/取消 active streams
    s.serveWG.Wait() // 等待所有 Serve goroutine 退出
    return nil
}

serveWG 仅等待 Serve() 主循环结束,而每个 stream 的 handleStream 在独立 goroutine 中运行,其生命周期由 stream.Context().Done() 决定。Shutdown() 不调用 cancel(),故活跃 stream 继续运行直至自然结束或客户端关闭。

行为对比表

场景 是否被 Shutdown() 中断 触发条件
新连接请求 ✅ 拒绝 s.quit == true
已建立的 Unary RPC ✅ 等待完成 serveWG 等待 handler 返回
正在发送的 ServerStream ❌ 不中断 依赖 stream ctx 超时或 client close

建议实践

  • 显式监听 ctx.Done() 在 stream 循环中;
  • 使用 WithTimeoutWithCancel 包装 stream 上下文;
  • 配合 GracefulStop()(等价于 Shutdown() + 强制 cancel 所有 stream)。

4.2 Unary 与 Streaming RPC 在上下文取消传播中的不一致性实测对比

实测环境配置

  • gRPC Go v1.63.0,服务端启用 WithBlock(),客户端设置 context.WithTimeout(ctx, 200ms)
  • Unary 调用单次请求;Streaming 使用 client.Send() 后立即 ctx.Cancel()

取消传播行为差异

场景 Unary RPC Streaming RPC
服务端接收 cancel ✅ 立即触发 ctx.Err() == context.Canceled server.Context().Err() 仍为 nil(首消息后才感知)
客户端阻塞退出 rpc error: code = Canceled 即时返回 client.Recv() 可能 hang 直至服务端主动关闭流

关键代码逻辑验证

// 客户端发起取消前发送一个消息(Streaming)
if err := client.Send(&pb.Request{Data: "init"}); err != nil {
    log.Fatal(err) // 若此时 ctx 已 cancel,Send 可能成功但 Recv 不响应
}
cancel() // 此刻 Unary 已中断,Streaming 流仍 open

Send() 不校验上下文取消状态,仅检查流是否 active;而 Recv() 在底层 transport.Stream 中延迟感知 cancel,导致语义断裂。

根本原因图示

graph TD
    A[Client ctx.Cancel()] --> B[Unary: transport.Write → immediate error]
    A --> C[Streaming Send: bypass ctx check]
    C --> D[Streaming Recv: wait for server frame or timeout]
    D --> E[Server must detect cancel via recv loop, not send path]

4.3 流式服务端主动终止逻辑:基于 grpc.StreamServerInterceptor 的超时熔断注入

核心拦截器注册方式

在 gRPC Server 初始化时,通过 grpc.StreamInterceptor() 注入自定义熔断逻辑,优先于业务 handler 执行。

超时上下文注入实现

func timeoutInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
    ctx := ss.Context()
    // 从 metadata 提取 client 声明的 max-duration(单位:秒)
    md, _ := metadata.FromIncomingContext(ctx)
    if durStr := md.Get("x-stream-timeout"); len(durStr) > 0 {
        if dur, err := time.ParseDuration(durStr[0] + "s"); err == nil {
            ctx, cancel := context.WithTimeout(ctx, dur)
            defer cancel()
            wrapped := &timeoutWrappedStream{ss, ctx}
            return handler(srv, wrapped)
        }
    }
    return handler(srv, ss)
}

该拦截器提取 x-stream-timeout 元数据字段,动态构造带超时的 context,并包装原始 ServerStream。关键在于:不修改流数据结构,仅劫持上下文生命周期

熔断触发条件对比

触发场景 是否中断流 是否返回错误码 是否释放资源
Context 超时 ✅ (DEADLINE_EXCEEDED)
客户端主动断连 ❌(无错误)
服务端 panic ✅ (INTERNAL)

数据同步机制

熔断后,服务端立即调用 ss.CloseSend() 并清空缓冲区,避免 goroutine 泄漏。

4.4 客户端侧流重连状态机设计与服务端流生命周期事件监听(via grpc.StreamServerInfo)

核心状态流转逻辑

客户端需在断连后自主决策重试策略,避免雪崩。典型状态包括:IDLECONNECTINGSTREAMINGRECONNECTINGFAILED

type ReconnectState int
const (
    IDLE ReconnectState = iota // 初始空闲
    CONNECTING                 // 正在建立gRPC连接
    STREAMING                  // 流已就绪,接收数据中
    RECONNECTING               // 检测到EOF/Cancel,启动退避重连
)

该枚举定义了轻量级状态标识,配合 time.AfterFunc 实现指数退避(如 1s→2s→4s),RECONNECTING 状态下禁止并发重试。

服务端流生命周期钩子

通过 grpc.StreamServerInfo 获取流元信息,结合拦截器监听关键事件:

事件类型 触发时机 可用字段
OnStreamStart ServerStream.Send() 首次调用前 FullMethod, IsServerStream
OnStreamEnd 流关闭(正常/异常)后 Err, Trailer

状态机驱动流程

graph TD
    A[IDLE] -->|Connect| B[CONNECTING]
    B -->|Success| C[STREAMING]
    C -->|EOF/DeadlineExceeded| D[RECONNECTING]
    D -->|Backoff| A
    D -->|MaxRetries| E[FAILED]

重连时自动注入 grpc.WaitForReady(true)grpc.MaxCallRecvMsgSize 上下文参数,保障流稳定性。

第五章:可落地的Golang服务优雅下线Checklist与演进路线

核心Checklist:生产环境必须验证的12项动作

  • ✅ HTTP Server 调用 Shutdown() 前已关闭监听端口(ln.Close()
  • ✅ gRPC Server 执行 GracefulStop() 并等待 Serve() 返回
  • ✅ 数据库连接池调用 db.Close(),且 sql.DB.Stats().OpenConnections == 0
  • ✅ Redis 客户端执行 client.Close(),确认 client.Ping() 返回 redis.Nil
  • ✅ Kafka 消费者调用 consumer.Close() 后检查 consumer.Closed()true
  • ✅ 定时任务(time.Ticker/cron)已 Stop() 并清空 Stop() 后未触发的 pending tick
  • ✅ Prometheus http.Handler 已从 http.ServeMux 中移除,避免 /metrics 接口残留
  • ✅ OpenTelemetry SDK 调用 shutdown(ctx) 并等待 err == nil
  • ✅ 文件句柄(os.File)全部 Close(),通过 lsof -p $PID | wc -l 验证无异常增长
  • ✅ 自定义信号监听 goroutine 收到 syscall.SIGTERM 后退出并关闭 done channel
  • ✅ 熔断器(如 gobreaker)状态持久化写入完成,避免重启后误判
  • ✅ Kubernetes readiness probe 返回 404 或 503(通过 /healthz?ready=false 显式降级)

典型故障场景与修复对照表

故障现象 根因定位命令 修复代码片段
Pod Terminating 超过30s被强制 Kill kubectl describe pod xxx 查看 Termination Grace PeriodEvents httpServer.RegisterOnShutdown(func() { log.Println("on shutdown hook triggered") })
Kafka 消费位点丢失 kafka-consumer-groups.sh --bootstrap-server x --group y --describe \| grep "CURRENT-OFFSET" signal.Notify(c, syscall.SIGTERM) 后立即 consumer.CommitOffsets()

演进路线:从基础到高可用的三阶段实践

第一阶段(单体服务):仅实现 http.Server.Shutdown() + os.Signal 监听,超时设为10秒。
第二阶段(微服务集群):集成 go.uber.org/fx 生命周期管理,将数据库、消息队列、缓存客户端注册为 fx.Invoke 依赖,确保依赖顺序关闭。
第三阶段(云原生生产环境):在 preStop hook 中注入健康检查降级脚本,配合 Istio Sidecar 的 proxy-status 检查 Envoy 连接数归零后再触发主进程退出。

// 示例:带超时控制的综合关闭流程
func gracefulShutdown(srv *http.Server, db *sql.DB, ch chan os.Signal) {
    <-ch // 等待 SIGTERM
    log.Println("Starting graceful shutdown...")

    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()

    // 并行关闭各组件
    var wg sync.WaitGroup
    wg.Add(3)
    go func() { defer wg.Done(); srv.Shutdown(ctx) }()
    go func() { defer wg.Done(); db.Close() }()
    go func() { defer wg.Done(); closeCustomResources(ctx) }()

    wg.Wait()
    log.Println("All resources released")
}

关键指标监控清单

  • http_server_shutdown_duration_seconds{quantile="0.99"} 必须
  • go_goroutinesSIGTERM 后 5s 内下降至初始值 ±3
  • process_open_fds 曲线在 shutdown 开始后 10s 内收敛至基线
  • Kubernetes Event 中 FailedKillPod 事件周报为 0
flowchart TD
    A[收到 SIGTERM] --> B[切换 readiness probe 状态]
    B --> C[停止接收新请求]
    C --> D[等待活跃 HTTP 连接自然结束]
    D --> E[并发关闭 DB/Redis/Kafka]
    E --> F[Commit offset & flush metrics]
    F --> G[退出进程]

敏捷如猫,静默编码,偶尔输出技术喵喵叫。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注