Posted in

Go信号处理失效:syscall.SIGTERM未触发shutdown,导致K8s滚动更新卡住300秒真相

第一章:Go信号处理失效:syscall.SIGTERM未触发shutdown,导致K8s滚动更新卡住300秒真相

在 Kubernetes 环境中,滚动更新时 Pod 被优雅终止(graceful termination)依赖于容器进程正确响应 SIGTERM 信号并执行清理逻辑。然而,大量 Go 应用在升级过程中卡住整整 300 秒(即 terminationGracePeriodSeconds 默认值),根本原因常被误判为网络或存储问题,实则源于 Go 运行时对信号的默认屏蔽机制。

Go 默认屏蔽 SIGTERM 的陷阱

Go runtime 在启动时会将 SIGTERMSIGINT 等信号默认设置为 SIG_IGN(忽略),即使你显式调用 signal.Notify(c, syscall.SIGTERM),若未在 goroutine 中持续接收,信号仍会被丢弃。典型错误写法如下:

// ❌ 危险:Notify 后未阻塞接收,main goroutine 退出导致程序立即终止
signal.Notify(signalCh, syscall.SIGTERM)
// 缺少 <-signalCh 或 select{} —— 信号通道从未被消费!

验证信号是否真正被捕获

在容器内执行以下命令,确认进程是否注册了 SIGTERM 处理器:

# 查看目标进程(如 PID=1)的信号掩码
cat /proc/1/status | grep SigCgt
# 输出示例:SigCgt: 0000000000000002 → 表示仅捕获 SIG1(SIGHUP),未设 SIG15(SIGTERM)

SigCgt 低位第15位为 (即十六进制 0x8000 对应位未置1),说明 SIGTERM 未被 Go 进程注册监听。

正确的信号处理模式

必须确保信号接收 goroutine 持续运行,并在收到 SIGTERM 后触发 shutdown 流程:

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

    // 启动 HTTP 服务等业务逻辑
    srv := &http.Server{Addr: ":8080"}
    go func() { http.ListenAndServe(":8080", nil) }()

    // ✅ 关键:阻塞等待信号,且 shutdown 必须在信号接收后立即触发
    sig := <-sigCh
    log.Printf("Received %v, starting graceful shutdown...", sig)
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    srv.Shutdown(ctx) // 阻塞直到所有连接关闭或超时
}

Kubernetes 侧关键配置对照表

配置项 推荐值 说明
terminationGracePeriodSeconds 30 避免默认 300 秒,与 Go shutdown 超时匹配
livenessProbe.initialDelaySeconds ≥ 服务冷启动时间 + 5s 防止 probe 误杀正在 shutdown 的进程
preStop hook sleep 2 && kill -TERM $PID 不推荐——Go 进程已能直接响应 SIGTERM,额外 hook 反而引发竞争

SIGTERM 处理缺失时,K8s 发送信号后无响应,强制等待 terminationGracePeriodSeconds 后发送 SIGKILL,造成服务中断与更新延迟。修复核心仅两点:显式 signal.Notify + 永久阻塞接收。

第二章:Go进程信号机制的底层原理与常见陷阱

2.1 Go runtime对POSIX信号的封装与屏蔽策略

Go runtime 在启动时即接管信号处理,通过 sigprocmask 屏蔽除 SIGURGSIGWINCH 等少数信号外的全部同步信号,确保 goroutine 调度不受干扰。

关键屏蔽策略

  • 默认阻塞 SIGALRMSIGPIPESIGHUP 等非运行时必需信号
  • 仅允许 SIGQUIT(触发栈dump)、SIGUSR1(调试)由 runtime 显式注册 handler
  • 所有用户级 signal.Notify 均基于 sigsend 机制异步投递至内部 signal channel

信号转发流程

// src/runtime/signal_unix.go 片段
func sigtramp() {
    // 汇总寄存器状态 → 触发 sigsend(sig, info, ctxt)
}

sigtramp 是汇编入口,不直接调用 C handler;sigsend 将信号封装为 sigNote 并唤醒等待的 sighandler goroutine。

信号类型 runtime 处理方式 用户可捕获性
SIGQUIT 同步打印 goroutine 栈 ❌(被 runtime 独占)
SIGUSR1 触发 pprof HTTP server ✅(需显式 Notify)
SIGPIPE 静默忽略(避免崩溃) ❌(始终屏蔽)
graph TD
    A[内核发送信号] --> B{runtime sigtramp}
    B --> C[解析 siginfo_t]
    C --> D[sigsend→sigNote队列]
    D --> E[sighandler goroutine]
    E --> F[分发至 runtime 或 user channel]

2.2 syscall.SIGTERM在Linux内核、容器运行时与Go程序间的传递链路分析

信号触发源头

docker stopkubectl delete pod 执行时,容器运行时(如 containerd)向容器 init 进程(PID 1)发送 SIGTERM,依据 POSIX 标准,该信号不可被忽略且默认终止进程

内核级传递路径

// Go 程序中捕获 SIGTERM 的典型模式
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
<-sigChan // 阻塞等待信号

此代码注册了信号接收通道;signal.Notify 依赖 rt_sigaction() 系统调用注册用户态 handler,内核通过 do_signal() 在进程返回用户态前注入信号。

容器环境特殊性

组件 行为说明
Linux 内核 SIGTERM 发送给 PID 1(非 init 命名空间则为容器主进程)
containerd 调用 runc kill --signal TERM 触发 kill(2) 系统调用
Go runtime 自动将 SIGTERM 转为 channel 接收事件,不中断 goroutine
graph TD
    A[containerd] -->|kill -TERM /proc/1| B[Linux kernel]
    B -->|do_signal → userspace return| C[Go runtime signal mask]
    C --> D[signal.Notify channel]

2.3 signal.Notify阻塞模型与goroutine调度冲突的实证复现

复现场景构建

以下最小化示例可稳定触发 signal.Notify 在无信号到达时阻塞主 goroutine,进而干扰调度器对其他 goroutine 的公平调度:

package main

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

func main() {
    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs) // ❗未指定信号列表 → 阻塞式监听所有信号(含 SIGURG、SIGCHLD 等)

    go func() {
        for i := 0; i < 5; i++ {
            println("worker:", i)
            time.Sleep(100 * time.Millisecond)
        }
    }()

    // 主 goroutine 在此永久阻塞,无法让出 P,导致 worker goroutine 饥饿
    <-sigs // 死锁诱因:无信号发送,且未设超时/非阻塞机制
}

逻辑分析signal.Notify(sigs) 若不传入具体信号(如 os.Interrupt, syscall.SIGTERM),Go 运行时会注册全量信号集,并在内部调用 sigwaitinfo 或类似系统调用——该调用在无信号时不可中断、不释放 M/P,使当前 goroutine 占用 OS 线程且无法被调度器抢占。runtime.GOMAXPROCS(1) 下 worker 几乎无法执行。

关键参数说明

  • sigs 通道容量为 1:缓冲不足加剧阻塞风险;
  • 缺失信号列表:触发底层全信号监听模式,显著提升阻塞概率;
  • select + defaulttime.After:丧失非阻塞退避能力。

调度影响对比(GOMAXPROCS=1)

场景 主 goroutine 状态 worker 执行次数 是否发生饥饿
signal.Notify(sigs, os.Interrupt) 可被中断唤醒 ≥4 次
signal.Notify(sigs) 永久内核态阻塞 0 次
graph TD
    A[main goroutine] -->|调用 signal.Notify\\无信号参数| B[进入 sigwaitinfo 系统调用]
    B --> C[OS 内核挂起线程]
    C --> D[Go 调度器无法回收 P]
    D --> E[worker goroutine 无法获得 P 运行]

2.4 默认信号处理器与自定义handler的竞争条件调试实践

SIGINT 同时触发默认终止行为与用户注册的 sigaction handler,内核交付顺序未定义,易引发竞态。

竞态复现场景

#include <signal.h>
#include <unistd.h>
volatile sig_atomic_t flag = 0;

void handler(int sig) {
    flag = 1;           // 非原子写入(实际中应使用 sig_atomic_t)
    write(1, "HANDLED\n", 9); // 可能被默认终止中断
}

int main() {
    struct sigaction sa = {.sa_handler = handler};
    sigaction(SIGINT, &sa, NULL);
    pause(); // 等待信号
}

write() 非异步信号安全函数;若在执行中被第二次 SIGINT 中断,可能破坏 stdout 缓冲区状态。flag 虽为 sig_atomic_t,但多线程+信号混合场景下仍需 sigprocmask 配合屏蔽。

常见竞态类型对比

场景 是否可重入 安全调用方式
printf() 禁止在 handler 中调用
sigprocmask() 用于临时阻塞信号
write()(fd=1) 仅限固定长度、无格式

修复路径示意

graph TD
    A[收到 SIGINT] --> B{信号掩码检查}
    B -->|未屏蔽| C[执行自定义 handler]
    B -->|已屏蔽| D[排队至 pending]
    C --> E[恢复掩码并调用 sigreturn]

2.5 Go 1.16+ signal.Ignore与signal.Reset行为变更对优雅退出的影响

Go 1.16 起,signal.Ignoresignal.Reset 的语义发生关键变化:它们不再影响已注册的信号处理器,仅作用于当前 goroutine 的信号接收行为

行为差异对比

操作 Go Go ≥ 1.16 行为
signal.Ignore(os.Interrupt) 全局禁用 SIGINT(含所有 goroutine) 仅忽略当前 goroutine 的 sigch 接收
signal.Reset(os.Interrupt) 清除全局处理器并恢复默认行为 仅重置当前 goroutine 的信号接收队列

典型误用代码示例

func badGracefulShutdown() {
    sigch := make(chan os.Signal, 1)
    signal.Notify(sigch, os.Interrupt)
    signal.Ignore(os.Interrupt) // ❌ 无效:Notify 已注册,Ignore 不取消它
    <-sigch // 仍会接收到信号
}

逻辑分析signal.Ignore 在 Go 1.16+ 中不解除 signal.Notify 的注册关系;它仅让当前 goroutine 忽略向 sigch 的投递。参数 os.Interrupt 仅指定信号类型,不改变注册状态。

正确做法

  • 使用 signal.Stop(sigch) 显式注销;
  • 或关闭通道后调用 signal.Reset() 配合新 Notify
graph TD
    A[启动 Notify] --> B[信号抵达内核]
    B --> C{Go ≥ 1.16?}
    C -->|是| D[投递至所有注册 chan]
    C -->|否| E[受 Ignore/Reset 全局抑制]

第三章:Kubernetes滚动更新中SIGTERM生命周期的真实时序解构

3.1 kubelet发送SIGTERM前的PreStop Hook执行边界与超时约束

PreStop Hook 是 Pod 终止流程中关键的同步屏障,其执行严格限定在 terminationGracePeriodSeconds 倒计时开始后、kubelet 发送 SIGTERM 之前。

执行时机边界

  • 必须在容器 Running 状态下触发(非 TerminatingUnknown
  • 仅由 kubelet 主动调用,不支持跨容器广播
  • 若 Hook 失败或未返回,kubelet 仍继续后续终止流程(不阻塞)

超时约束机制

参数 默认值 说明
exec/httpGet 超时 30s(不可配置) 由 kubelet 内部硬编码限制,超时即终止 Hook 进程
terminationGracePeriodSeconds 30s 全局窗口上限;PreStop + SIGTERM + SIGKILL 总耗时不得超过此值
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 25 && curl -X POST http://localhost:8080/shutdown"]

此例中 sleep 25 占用大部分窗口,若 curl 因网络延迟超过 5s,将被 kubelet 强制中断——因剩余 grace 时间不足,且 Hook 自身无独立 timeout 字段。

执行失败处理流程

graph TD
  A[Pod 删除请求] --> B{PreStop Hook 启动}
  B --> C[执行中]
  C --> D{是否超时或失败?}
  D -->|是| E[记录事件 event: PreStopHookTimeout]
  D -->|否| F[等待 Hook 成功退出]
  E & F --> G[发送 SIGTERM]

3.2 容器runtime(containerd/cri-o)信号转发延迟的可观测性验证

核心观测维度

需同时捕获三类时间戳:

  • 宿主机 kill -TERM 发起时刻(t₀
  • containerd CRI 接口接收到 StopPodSandbox 请求时刻(t₁
  • 容器 init 进程实际收到 SIGTERMsignal-delivery 事件(t₂

实时抓取示例(eBPF)

# 使用 trace-cmd 抓取内核 signal delivery 事件
trace-cmd record -e signal:signal_deliver \
  -F "comm ~ 'runc:*' || comm ~ 'conmon'" \
  -M 512 # 缓冲区大小(MB)

逻辑说明:signal_deliver 事件在内核 do_send_sig_info() 中触发;-F 过滤仅关注容器运行时进程;-M 512 防止高负载下事件丢失,确保 t₂ 精确捕获。

延迟分解对照表

阶段 典型延迟 触发点
CRI → containerd 0.5–3 ms StopPodSandbox gRPC 解析完成
containerd → runc 1–8 ms runc kill --all IPC 调用返回
runc → 容器init 0.1–2 ms kill(1, SIGTERM) 系统调用执行

数据同步机制

graph TD
  A[Host kill -TERM] --> B[containerd CRI Server]
  B --> C[runc kill --all]
  C --> D[conmon proxy]
  D --> E[PID 1 in container]

3.3 Pod Phase Transition中Terminating状态与进程实际存活时间的偏差归因

核心偏差来源:SIGTERM传播延迟与容器运行时解耦

Kubernetes 将 Terminating 状态置为 True 后,仅触发 preStop 钩子和发送 SIGTERM不等待进程实际退出

lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 2 && nginx -s quit"]  # 延迟优雅关闭

此配置使容器在收到 SIGTERM 后仍存活约 2 秒,而 kubelet 已将 Pod phase 更新为 Terminating,造成状态与实际生命周期错位。

关键影响因子对比

因子 默认行为 实际影响
terminationGracePeriodSeconds 30s(可设为0) 超时后强制 SIGKILL,但 Terminating 状态不反映剩余宽限期
容器运行时(如 containerd)信号投递延迟 ~10–100ms 进程可能未及时捕获 SIGTERM
应用未监听 SIGTERM 或忽略信号 进程持续运行 Terminating 状态已生效,但进程仍在服务请求

状态演进时序(简化)

graph TD
  A[Pod deletion initiated] --> B[API Server 更新 phase=Terminating]
  B --> C[kubelet 发送 SIGTERM + 执行 preStop]
  C --> D{进程是否响应?}
  D -->|是| E[graceful shutdown]
  D -->|否| F[等待 terminationGracePeriodSeconds 后 SIGKILL]

偏差本质是 声明式状态更新(快)进程级生命周期控制(慢且不可观测) 的固有割裂。

第四章:构建高可靠Go服务优雅退出的工程化方案

4.1 基于context.Context与sync.WaitGroup的shutdown协调器设计与压测验证

核心协调器结构

Shutdown协调器需同时响应取消信号(context.Context)与等待所有goroutine退出(sync.WaitGroup),确保服务优雅终止。

type ShutdownCoordinator struct {
    ctx    context.Context
    cancel context.CancelFunc
    wg     sync.WaitGroup
    mu     sync.RWMutex
    done   bool
}

func NewShutdownCoordinator(timeout time.Duration) *ShutdownCoordinator {
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    return &ShutdownCoordinator{ctx: ctx, cancel: cancel}
}

context.WithTimeout 提供可中断的生命周期控制;cancel 用于主动触发终止;wg 后续用于注册/等待工作协程;done 标志位避免重复关闭。

压测关键指标对比(1000并发请求,5s超时)

指标 无协调器 仅WaitGroup Context+WaitGroup
平均shutdown耗时 820ms 410ms 290ms
goroutine泄漏率 12.3% 0.1% 0.0%

协调流程示意

graph TD
    A[收到SIGTERM] --> B[调用cancel()]
    B --> C[Context Done通道关闭]
    C --> D[各worker检测ctx.Err()并退出]
    D --> E[worker调用wg.Done()]
    E --> F[wg.Wait()返回 → shutdown完成]

4.2 使用os.Signal + select超时组合实现可中断的graceful shutdown流程

在高可用服务中,优雅关闭需同时响应外部信号与内部超时约束。os.Signal监听SIGINT/SIGTERMselect则协调信号通道与超时计时器。

核心控制流

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

go func() {
    done <- srv.Shutdown(context.Background()) // 执行HTTP graceful shutdown
}()

select {
case <-sigCh:
    log.Println("received shutdown signal")
case <-time.After(30 * time.Second):
    log.Println("shutdown timeout, forcing exit")
case err := <-done:
    if err != nil {
        log.Printf("shutdown error: %v", err)
    }
}

逻辑分析:sigCh接收系统终止信号;time.After提供硬性超时兜底;done通道捕获Shutdown()完成状态。三者通过select非阻塞择一触发,确保任意条件满足即退出主循环。

关键参数说明

参数 含义 建议值
time.After(30s) 最大等待服务连接自然断开时长 15–60s(依业务负载)
srv.Shutdown(ctx) 阻塞至所有请求完成或ctx取消 使用带超时的context避免永久挂起
graph TD
    A[启动服务] --> B[注册信号监听]
    B --> C[启动Shutdown goroutine]
    C --> D{select等待}
    D -->|收到SIGTERM| E[触发Shutdown]
    D -->|30s超时| F[强制终止]
    D -->|Shutdown完成| G[正常退出]

4.3 Prometheus指标埋点与SIGTERM接收/处理耗时的实时监控实践

为精准观测服务优雅退出过程,需在应用生命周期关键节点埋点并暴露至Prometheus。

核心埋点设计

  • app_shutdown_latency_seconds:直方图指标,记录从SIGTERM到达至进程终止的完整耗时
  • app_shutdown_stage_total:计数器,按received/graceful_start/cleanup_done阶段打标

SIGTERM处理与指标更新代码

var shutdownLatency = prometheus.NewHistogramVec(
    prometheus.HistogramOpts{
        Name:    "app_shutdown_latency_seconds",
        Help:    "Latency of graceful shutdown process",
        Buckets: prometheus.ExponentialBuckets(0.1, 2, 8), // 0.1s ~ 12.8s
    },
    []string{"stage"},
)

func init() {
    prometheus.MustRegister(shutdownLatency)
}

func handleSigterm() {
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGTERM)

    start := time.Now()
    <-sigChan

    shutdownLatency.WithLabelValues("received").Observe(time.Since(start).Seconds())

    // 执行清理逻辑(如DB连接关闭、队列刷盘等)
    cleanup()

    shutdownLatency.WithLabelValues("cleanup_done").Observe(time.Since(start).Seconds())
}

该代码在信号捕获瞬间打点received,并在所有清理完成后打点cleanup_done,直方图分桶覆盖典型优雅停机时间范围(0.1–12.8秒),支持P95/P99延迟分析。

指标采集链路

graph TD
    A[Go App] -->|Exposes /metrics| B[Prometheus Scraping]
    B --> C[Alert on shutdown_latency_seconds{stage=\"cleanup_done\"} > 5s]
阶段标签 含义 监控意义
received SIGTERM被Go runtime捕获时刻 验证系统是否及时响应OS信号
cleanup_done 所有资源释放完成时刻 衡量实际停机窗口上限

4.4 Kubernetes livenessProbe与readinessProbe协同shutdown状态的反模式规避

当应用收到 SIGTERM 后进入优雅关闭流程,若 livenessProbe 仍持续失败并触发重启,将中断 shutdown,造成数据丢失或连接中断——这是典型反模式。

常见错误配置

livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 5  # shutdown 耗时 >5s 时即被误杀
readinessProbe:
  httpGet:
    path: /readyz
    port: 8080
  periodSeconds: 3

⚠️ 问题:livenessProbe 未感知 shutdown 状态,且 periodSeconds 小于应用实际关闭耗时,导致容器在 PreStop 执行中被强制 kill。

推荐协同策略

  • readinessProbe 在收到 SIGTERM 后立即返回 503(下线流量);
  • livenessProbe 在 shutdown 阶段放宽超时或切换为轻量检查(如文件存在性);
  • 设置 terminationGracePeriodSeconds ≥ 最大 shutdown 时间。
探针类型 shutdown 期间行为 依据
readinessProbe 返回失败(HTTP 503) 停止新流量接入
livenessProbe 检查 /tmp/shutting-down 文件 避免误重启
graph TD
  A[Pod 收到 SIGTERM] --> B[执行 PreStop hook]
  B --> C[标记 /tmp/shutting-down]
  C --> D[readinessProbe 返回 503]
  C --> E[livenessProbe 检查文件存在]
  E --> F[等待 graceful shutdown 完成]

第五章:从300秒卡顿到毫秒级终止——一次生产级Go服务信号治理的闭环总结

问题现场还原

某金融风控网关服务在K8s集群中频繁触发PreStop超时(300s),导致滚动更新失败率高达12%。kubectl describe pod显示Terminating状态长期滞留,strace -p $(pgrep -f 'main')捕获到大量restart_syscall阻塞在epoll_wait,而lsof -p $PID | wc -l显示连接数稳定在47,排除连接泄漏。根本症结在于SIGTERM到达后,http.Server.Shutdown()被阻塞在未完成的长轮询请求上——该服务承载客户端心跳保活,单次超时设为290秒。

信号注册与上下文传播改造

原代码仅监听os.Interrupt,未处理容器标准信号流:

// 改造前(危险)
signal.Notify(c, os.Interrupt)

// 改造后(生产就绪)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
go func() {
    <-sigChan
    log.Info("Received SIGTERM, initiating graceful shutdown")
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()
    if err := srv.Shutdown(shutdownCtx); err != nil {
        log.Warn("HTTP server forced shutdown", "error", err)
    }
}()

连接生命周期精准控制

为解决长轮询阻塞,重写http.Handler注入可取消上下文:

func (h *HeartbeatHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // 从父上下文继承取消能力,而非使用r.Context()
    ctx, cancel := context.WithTimeout(r.Context(), 285*time.Second)
    defer cancel()

    // 关键:将cancel显式传递给业务逻辑
    h.handleWithCancel(ctx, w, r)
}

治理效果对比数据

指标 治理前 治理后 变化幅度
平均终止耗时 298.3s 127ms ↓99.96%
滚动更新成功率 88.2% 99.97% ↑11.77pp
SIGTERM到Shutdown调用延迟 3.2s 41ms ↓98.7%

真实故障复盘时间线

  • 2024-03-12T08:14:22+0800: K8s发起kill -TERM 12845
  • 2024-03-12T08:14:22+0800: Go runtime捕获信号,启动shutdown goroutine
  • 2024-03-12T08:14:22+0800: srv.Shutdown()开始遍历活跃连接
  • 2024-03-12T08:14:22+0800: 长轮询连接收到context.DeadlineExceeded并立即返回HTTP 499
  • 2024-03-12T08:14:22+0800: 所有连接关闭,srv.Close()返回

监控埋点验证方案

Shutdown()前后插入OpenTelemetry计时器:

start := time.Now()
err := srv.Shutdown(ctx)
otel.Record("http.server.shutdown.duration", time.Since(start).Milliseconds())

Prometheus抓取指标http_server_shutdown_duration_milliseconds_bucket,确认P99≤150ms。

容器化部署加固项

# deployment.yaml 片段
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 0.1 && kill -TERM $PPID"]
readinessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10

信号链路全路径可视化

graph LR
A[Pod Terminating] --> B[K8s kubelet send SIGTERM]
B --> C[Go runtime signal.Notify]
C --> D[启动Shutdown goroutine]
D --> E[调用http.Server.Shutdown]
E --> F[遍历connList并设置read deadline]
F --> G[长轮询连接主动close]
G --> H[所有conn.Close()返回]
H --> I[调用srv.Close()]
I --> J[进程退出]

以代码为修行,在 Go 的世界里静心沉淀。

发表回复

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