当你的服务被 kill 时,别像被拔插头的电脑——要像谢幕的摇滚明星 🎸
🤯 一、为什么“拔插头式停机”很危险?
你肯定见过这些“尸体现场”:
| 现象 | 根本原因 |
|---|---|
❌ MySQL 报 Too many connections |
服务挂了,连接没 close |
| ❌ Kafka 消费位点回滚 1000 条 | 消息处理到一半被中断 |
| ❌ Prometheus 指标突降归零 | 暴力退出,没上报最后 10 秒数据 |
| ❌ K8s Pod 重启 5 分钟 | 因无健康探针响应被反复 kill |
💡 真相:
Ctrl + C发的是SIGINT,kubectl delete发的是SIGTERM——
它们不是“立即死刑”,而是 “请准备谢幕” 的提示音 🎵
✅ 二、Go 的优雅停机 = 3 步谢幕流程
🎯 核心思想:收尾 → 等待 → 撤退
// 代码来自 https://pawelgrzybek.com/graceful-shutdown-in-go/
// 已简化 + 注释增强,新手也能秒懂
package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
ctx := context.Background()
if err := run(ctx); err != nil {
log.Fatal(err) // 只有真实错误才 exit(1)
}
// 优雅退出 → exit(0) 👑
}
func run(ctx context.Context) error {
// 1️⃣ 启动 HTTP 服务(放 goroutine 里)
srv := &http.Server{
Addr: ":8080", Handler: http.HandlerFunc(ok)}
errCh := make(chan error, 1)
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
errCh <- err // 真报错才传出去
}
close(errCh)
}()
// 2️⃣ 监听系统信号(SIGINT/Ctrl+C, SIGTERM/k8s)
ctxSig, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer stop()
select {
case <-ctxSig.Done():
log.Println("🎯 收到退场信号!开始谢幕流程...")
case err := <-errCh:
return err // 服务自己崩了,直接报错
}
// 3️⃣ 优雅退场:给 5 秒关门时间
ctxTimeout, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctxTimeout); err != nil {
log.Printf("⚠️ 退场超时!强制离场: %v", err)
return srv.Close() // 最后手段:拔网线式关闭
}
log.Println("✨ 优雅谢幕完成!掌声送给这位靠谱服务 👏")
return nil
}
func ok(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte("I'm alive and ready to exit gracefully!"))
}
🧠 三、代码拆解:每一行都在“防坑”
| 代码片段 | 作用 | 幽默解读 |
|---|---|---|
signal.NotifyContext |
注册“谢幕提示器” | 📻 相当于后台 DJ:“注意!下一首是最后一曲” |
select {} |
等待:信号 or 服务自身崩溃 | 🪑 坐在后台:“等 cue 我就上,自己摔了也算” |
srv.Shutdown(ctx) |
拒绝新请求 + 等待旧请求处理完 | 🚪 关大门:“进来的都服务完,门外的请改天” |
WithTimeout(5s) |
设置最长谢幕时间 | ⏱️ 导演喊:“5 秒内不下台,我拉闸了啊!” |
srv.Close() |
最后手段:暴力清场 | 🔌 保镖登场:“这位先生,谢幕时间到了” |
✅ 效果对比:
| 操作 | 暴力 kill | 优雅停机 |
|------|----------|---------|
| 正在处理的请求 | 直接丢弃 → 用户 5xx | 完成后才断开 → 用户 200 ✅ |
| DB 连接池 | 悬空泄漏 | 自动释放 |
| Prometheus 指标 | 瞬间归零 | 平滑下降 |
| K8s 重启耗时 | 2min+(反复探针失败) | <10s(一次通过) |
🧪 四、实测:看它如何“救场”
# 1. 启动服务
$ go run main.go
2025/11/27 10:00:00 Starting server on :8080
# 2. 另开终端,发一个慢请求(模拟耗时操作)
$ curl "localhost:8080?sleep=3" & # 后台运行
[1] 12345
# 3. 立刻 Ctrl+C 停服务
^C2025/11/27 10:00:02 🎯 收到退场信号!开始谢幕流程...
2025/11/27 10:00:05 ✨ 优雅谢幕完成!掌声送给这位靠谱服务 👏
# 4. 查看 curl 结果 → 居然成功了!
$ # 等 1 秒后...
OK
[1]+ Done curl "localhost:8080?sleep=3"
🎯 关键:
即使你在请求中途Ctrl+C,Go 也会等它跑完再退出——
用户无感知,运维不背锅。
🛠️ 五、进阶:适配你的业务场景
场景 1:有后台 Goroutine(如 Kafka 消费者)
// 在 Shutdown 前加一行:
waitGroup.Wait() // 等所有 worker 处理完当前消息再退
场景 2:想自定义“退场动作”
// 在 srv.Shutdown 之前:
log.Println("💾 正在刷盘缓存...")
cache.Flush()
log.Println("📤 上报最后心跳...")
metrics.Report()
场景 3:K8s 用户必加!
# deployment.yaml
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
# 关键:加 terminationGracePeriodSeconds!
terminationGracePeriodSeconds: 30 # 给足 30s 优雅时间
🎁 六、一句话总结
优雅停机不是“功能”,是服务的“基本礼仪”。
就像摇滚明星——
- 暴力 kill:摔吉他走人 → 粉丝骂街
- 优雅停机:鞠躬 + 飞吻 + 抛 pick → 全场起立鼓掌 🎸👏