第一章:Go信号处理失效:syscall.SIGTERM未触发shutdown,导致K8s滚动更新卡住300秒真相
在 Kubernetes 环境中,滚动更新时 Pod 被优雅终止(graceful termination)依赖于容器进程正确响应 SIGTERM 信号并执行清理逻辑。然而,大量 Go 应用在升级过程中卡住整整 300 秒(即 terminationGracePeriodSeconds 默认值),根本原因常被误判为网络或存储问题,实则源于 Go 运行时对信号的默认屏蔽机制。
Go 默认屏蔽 SIGTERM 的陷阱
Go runtime 在启动时会将 SIGTERM、SIGINT 等信号默认设置为 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 屏蔽除 SIGURG、SIGWINCH 等少数信号外的全部同步信号,确保 goroutine 调度不受干扰。
关键屏蔽策略
- 默认阻塞
SIGALRM、SIGPIPE、SIGHUP等非运行时必需信号 - 仅允许
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 stop 或 kubectl 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+default或time.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.Ignore 和 signal.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状态下触发(非Terminating或Unknown) - 仅由 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 进程实际收到
SIGTERM的signal-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/SIGTERM,select则协调信号通道与超时计时器。
核心控制流
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 128452024-03-12T08:14:22+0800: Go runtime捕获信号,启动shutdown goroutine2024-03-12T08:14:22+0800:srv.Shutdown()开始遍历活跃连接2024-03-12T08:14:22+0800: 长轮询连接收到context.DeadlineExceeded并立即返回HTTP 4992024-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[进程退出] 