skills/go-graceful-shutdown/SKILL.md
Go 優雅關機(Graceful Shutdown)模式:Signal 處理、HTTP Server shutdown、gRPC GracefulStop、 Worker/Consumer 停止、Kubernetes 整合、Context 取消機制、資源清理流程。 **適用場景**:實作 HTTP Server 優雅關機、gRPC Server 停止、Background Worker 終止、 Kubernetes 部署配置、處理 SIGTERM/SIGINT、實作 preStop hook、避免請求中斷。
npx skillsauth add vincent119/ai-rules-kit go-graceful-shutdownInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
相關 Skills:本規範建議搭配
go-grpc(gRPC Server)與go-http-advanced(HTTP Server)
優先選擇:vincent119/commons/graceful
核心特性:
HTTPTask)log/slog 結構化日誌errors.Join)安裝:
go get github.com/vincent119/commons/graceful
package main
import (
"context"
"log/slog"
"net/http"
"os"
"time"
"github.com/vincent119/commons/graceful"
)
func main() {
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
srv := &http.Server{Addr: ":8080"}
// 初始化資源
// db, _ := sql.Open(...)
err := graceful.Run(
// 1. 主要任務:HTTPTask 封裝 srv.ListenAndServe
graceful.HTTPTask(srv),
// 2. 設定 Logger
graceful.WithLogger(logger),
// 3. 設定 Shutdown Timeout(預設 30s)
graceful.WithTimeout(10*time.Second),
// 4. 註冊清理函式(LIFO 順序執行)
graceful.WithCleanup(func(ctx context.Context) error {
logger.Info("shutting down server...")
return srv.Shutdown(ctx)
}),
// 5. 註冊 io.Closer 資源(自動呼叫 Close())
// graceful.WithCloser(db),
// graceful.WithClosers(redis, cache), // 批量註冊
)
if err != nil {
logger.Error("application exited with error", "error", err)
os.Exit(1)
}
}
任何符合 func(ctx context.Context) error 的任務都可使用:
func MyWorker(ctx context.Context) error {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
// 收到訊號,優雅退出
return nil
case <-ticker.C:
// 執行工作
if err := doWork(); err != nil {
return err
}
}
}
}
func main() {
graceful.Run(
MyWorker,
graceful.WithTimeout(5*time.Second),
)
}
err := graceful.Run(
task,
// 設定 shutdown timeout(預設 30s)
graceful.WithTimeout(15*time.Second),
// 設定 logger(預設 slog.Default())
graceful.WithLogger(logger),
// 註冊清理函式(LIFO 順序)
graceful.WithCleanup(func(ctx context.Context) error {
// 先註冊的後執行
return db.Close()
}),
graceful.WithCleanup(func(ctx context.Context) error {
// 後註冊的先執行
return srv.Shutdown(ctx)
}),
// 註冊單個 io.Closer
graceful.WithCloser(db),
// 批量註冊多個 io.Closer(按順序關閉)
graceful.WithClosers(redis, cache, queue),
)
重要注意事項:
WithCleanup 採用 LIFO (後進先出)。建議先註冊底層資源(DB),再註冊上層服務(HTTP Server),確保關機時先停止服務再關閉資料庫graceful.Run 使用 errors.Join 返回所有錯誤ctx.Done(),避免阻塞整體關機流程注意:以下為不使用
commons/graceful的手動實作方式,僅供理解原理或特殊場景使用
必須實作優雅關機的元件:
關機流程順序:
SIGINT, SIGTERM)Shutdown / gRPC GracefulStop)禁止行為:
os.Exit()log.Fatal / logger.Fatal(會跳過 defer 與資源收尾)ctx.Done() 造成關機卡死import (
"context"
"os"
"os/signal"
"syscall"
)
func main() {
// 將 OS 訊號轉為可取消的 context
ctx, stop := signal.NotifyContext(
context.Background(),
os.Interrupt, // SIGINT (Ctrl+C)
syscall.SIGTERM, // SIGTERM (Kubernetes 預設)
)
defer stop() // 釋放資源
// 啟動 Server(會阻塞)
if err := runServer(ctx); err != nil {
log.Fatalf("server error: %v", err)
}
}
func runHTTPServer(
srv *http.Server,
shutdownTimeout time.Duration,
closeResources func(ctx context.Context) error, // 關閉 DB/Redis/Scheduler
logger *zap.Logger,
) error {
// 1) 訊號轉 ctx
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
// 2) 監控 server 是否異常退出(避免只等訊號,卻漏掉 server 先掛)
srvErr := make(chan error, 1)
go func() {
// ListenAndServe 正常因 Shutdown/Close 退出會回傳 http.ErrServerClosed
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
srvErr <- err
}
close(srvErr) // 關閉 channel 表示 server goroutine 已結束
}()
// 3) 等待:訊號 or server 異常
select {
case <-ctx.Done(): // 收到關機訊號
logger.Info("received shutdown signal")
case err := <-srvErr: // 服務異常退出
if err != nil {
logger.Error("http server stopped unexpectedly", zap.Error(err))
return err
}
}
// 4) 統一走 graceful shutdown:先停止接新請求,再收尾資源
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
// Shutdown 會等待 in-flight request,若卡住要有最後手段
logger.Error("http server shutdown failed", zap.Error(err))
_ = srv.Close() // 最後手段:避免卡住(可能中斷連線)
}
// 5) 關閉外部資源(scheduler/worker/redis/db...)
var resErr error
if closeResources != nil {
resErr = closeResources(shutdownCtx)
if resErr != nil {
logger.Error("close resources failed", zap.Error(resErr))
}
}
logger.Info("server exited")
return resErr
}
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
srv := &http.Server{
Addr: ":8080",
Handler: r,
}
// 使用通用 runHTTPServer 函式
if err := runHTTPServer(srv, 30*time.Second, nil, logger); err != nil {
log.Fatal(err)
}
}
func runGRPCServer(
grpcServer *grpc.Server,
lis net.Listener,
shutdownTimeout time.Duration,
logger *zap.Logger,
) error {
// 1) 訊號監聽
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
// 2) Server goroutine
srvErr := make(chan error, 1)
go func() {
if err := grpcServer.Serve(lis); err != nil {
srvErr <- err
}
close(srvErr)
}()
// 3) 等待訊號或錯誤
select {
case <-ctx.Done():
logger.Info("received shutdown signal")
case err := <-srvErr:
if err != nil {
logger.Error("grpc server error", zap.Error(err))
return err
}
}
// 4) Graceful Stop(等待進行中的請求完成)
stopped := make(chan struct{})
go func() {
grpcServer.GracefulStop() // 阻塞直到所有請求完成
close(stopped)
}()
// 5) 等待 GracefulStop 或 timeout
select {
case <-stopped:
logger.Info("grpc server stopped gracefully")
case <-time.After(shutdownTimeout):
logger.Warn("graceful stop timeout, forcing shutdown")
grpcServer.Stop() // 強制停止
}
return nil
}
type Worker struct {
logger *zap.Logger
}
func (w *Worker) Run(ctx context.Context) error {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
// 收到取消訊號,停止拉取新任務
w.logger.Info("worker shutting down")
return ctx.Err()
case <-ticker.C:
// 執行週期性任務
if err := w.processTask(ctx); err != nil {
w.logger.Error("task failed", zap.Error(err))
}
}
}
}
func (w *Worker) processTask(ctx context.Context) error {
// 長時間任務需定期檢查 ctx.Done()
for i := 0; i < 100; i++ {
select {
case <-ctx.Done():
w.logger.Warn("task interrupted")
return ctx.Err()
default:
// 執行任務片段
time.Sleep(100 * time.Millisecond)
}
}
return nil
}
type Consumer struct {
queue *Queue
logger *zap.Logger
}
func (c *Consumer) Start(ctx context.Context) error {
for {
select {
case <-ctx.Done():
c.logger.Info("consumer stopping, draining queue...")
// 處理剩餘訊息(或設定 timeout)
drainCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
for c.queue.HasMessages() {
select {
case <-drainCtx.Done():
c.logger.Warn("drain timeout, some messages may be lost")
return drainCtx.Err()
default:
msg, err := c.queue.Receive(drainCtx)
if err != nil {
return err
}
_ = c.processMessage(drainCtx, msg)
}
}
return nil
default:
msg, err := c.queue.Receive(ctx)
if err != nil {
if errors.Is(err, context.Canceled) {
return err
}
c.logger.Error("receive failed", zap.Error(err))
continue
}
if err := c.processMessage(ctx, msg); err != nil {
c.logger.Error("process failed", zap.Error(err))
}
}
}
}
type Resources struct {
db *gorm.DB
redis *redis.Client
scheduler *Scheduler
tracer func() // OTel Shutdown
}
func (r *Resources) Close(ctx context.Context) error {
var errs []error
// 1. 停止 Scheduler
if r.scheduler != nil {
if err := r.scheduler.Stop(ctx); err != nil {
errs = append(errs, fmt.Errorf("stop scheduler: %w", err))
}
}
// 2. 關閉 Tracer(OTel)
if r.tracer != nil {
r.tracer()
}
// 3. 關閉 Redis
if r.redis != nil {
if err := r.redis.Close(); err != nil {
errs = append(errs, fmt.Errorf("close redis: %w", err))
}
}
// 4. 關閉 DB(最後才關閉)
if r.db != nil {
sqlDB, err := r.db.DB()
if err == nil {
if err := sqlDB.Close(); err != nil {
errs = append(errs, fmt.Errorf("close db: %w", err))
}
}
}
// 聚合錯誤
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func main() {
// 初始化資源
resources := &Resources{
db: initDB(),
redis: initRedis(),
scheduler: initScheduler(),
tracer: initTracer(),
}
// 建立 HTTP Server
srv := &http.Server{
Addr: ":8080",
Handler: buildHandler(),
}
// 啟動並等待關機
if err := runHTTPServer(srv, 30*time.Second, resources.Close, logger); err != nil {
log.Fatal(err)
}
}
原則:
terminationGracePeriodSeconds ≥ 應用層 Shutdown timeout + buffer(建議 5-10 秒)範例:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
terminationGracePeriodSeconds: 45 # 應用層 30s + buffer 15s
containers:
- name: app
image: myapp:latest
env:
- name: SHUTDOWN_TIMEOUT
value: "30"
目的:確保 Pod 從 Service Endpoints 移除後再開始 shutdown(避免新請求進來)
lifecycle:
preStop:
exec:
command: ["sleep", "5"] # 等待 Endpoints 更新
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
template:
spec:
terminationGracePeriodSeconds: 45
containers:
- name: app
image: myapp:latest
ports:
- containerPort: 8080
name: http
env:
- name: SHUTDOWN_TIMEOUT
value: "30"
lifecycle:
preStop:
exec:
command: ["sleep", "5"]
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
func TestGracefulShutdown(t *testing.T) {
srv := &http.Server{
Addr: ":0", // 隨機 Port
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second) // 模擬慢請求
w.WriteHeader(200)
}),
}
// 啟動 Server
lis, err := net.Listen("tcp", srv.Addr)
if err != nil {
t.Fatal(err)
}
go func() {
_ = srv.Serve(lis)
}()
// 發送請求
reqDone := make(chan bool)
go func() {
resp, err := http.Get("http://" + lis.Addr().String())
if err != nil {
t.Errorf("request failed: %v", err)
}
resp.Body.Close()
reqDone <- true
}()
// 等待請求開始
time.Sleep(100 * time.Millisecond)
// 觸發關機
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
t.Errorf("shutdown failed: %v", err)
}
// 驗證請求完成
<-reqDone
}
優先推薦
github.com/vincent119/commons/graceful 套件(統一生命週期管理)graceful.Run() 作為主程式入口graceful.HTTPTask() 包裝 HTTP Servergraceful.WithCleanup() 註冊清理函式(LIFO 順序)graceful.WithCloser() / WithClosers() 註冊資源(自動 Close)WithTimeout()(建議 10-30 秒)log/slog 結構化日誌Signal 處理(手動實作)
signal.NotifyContext 監聽 SIGINT 與 SIGTERMos.Exit()log.Fatal()HTTP Server
srv.Shutdown(ctx) 而非 srv.Close()http.ErrServerClosed(正常退出)srv.Close()gRPC Server
grpcServer.GracefulStop() 而非 Stop()grpcServer.Stop()Background Worker
ctx.Done() 停止拉取新任務ctx.Done()資源清理
errors.Join 聚合清理錯誤context.Context(支持 timeout)Kubernetes
terminationGracePeriodSeconds ≥ shutdown timeout + bufferpreStop hook(sleep 5s)/readyz endpoint(關機時回傳 503)測試
graceful)tools
基於 SLA/SLO 量化評估事故影響的計算模型與業務影響矩陣。適用於「SLA 影響」、「SLO 違反」、「影響評估」、「營收損失估算」、「Error Budget」、「可用性計算」、「事故成本評估」等量化事故業務影響的任務。強化 impact-assessor 的評估能力。注意:事故原因分析與改善規劃不在此技能範圍內。
research
根因分析(RCA)方法論詳細指南。提供 5 Whys、Fishbone 圖、Fault Tree Analysis、變更分析等結構化 RCA 技術,以及認知偏誤防範清單。適用於「根因分析」、「RCA」、「5 Whys」、「魚骨圖」、「Fault Tree」、「原因分析方法論」、「變更分析」等事故原因分析任務。強化 root-cause-investigator 的分析能力。注意:時間軸重建與改善規劃不在此技能範圍內。
testing
事故事後分析(Postmortem)完整流程。協調 7 個執行階段:資訊收集 → 時間軸重建 → 根因分析 → 影響評估 → 改善規劃 → 報告審查 → 整合報告,最終產出完整的 Postmortem 報告。適用於「寫事故報告」、「post-incident 分析」、「RCA 報告」、「事故時間軸整理」、「建立改善措施」等請求。注意:即時 Incident Response(on-call)、監控系統設定、告警配置不在此技能範圍內。
content-media
投影片版面模式庫。提供 20 種投影片類型的最佳版面配置、格線系統、色彩與字型設計 Token。適用於「投影片版面」、「Slide Layout」、「設計系統」、「格線」、「字型」、「色彩規範」等投影片視覺設計任務。強化 visual-designer 的設計能力。注意:PPT/Keynote 檔案直接輸出不在此技能範圍內。