Go微服务优雅关机实战:从信号捕获到连接排空的完整实现方案

微服务直接关机的风险场景

在Kubernetes环境中滚动更新或手动缩容时,Pod收到SIGTERM信号后有30秒的宽限期(terminationGracePeriodSeconds)。如果服务在收到信号后直接退出,正在处理的请求会中断,数据库事务会处于未提交状态,与消息中间件的连接会被强制断开可能导致消息丢失或重复消费。优雅关机的目标是在退出前完成三件事:停止接收新请求、排空正在处理的请求、关闭所有外部连接。

信号捕获与关机流程编排

Go语言中优雅关机的基础是正确捕获系统信号并编排关机流程。以下是生产级实现:

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    server := &http.Server{
        Addr:    ":8080",
        Handler: newRouter(),
    }
    go func() {
        if err := server.ListenAndServe();
           err != http.ErrServerClosed {
            log.Fatalf("HTTP异常: %v", err)
        }
    }()
    quit := make(chan os.Signal, 1)
    signal.Notify(quit,
        syscall.SIGINT, syscall.SIGTERM)
    sig := <-quit
    log.Printf("收到信号 %v,开始关机", sig)
    ctx, cancel := context.WithTimeout(
        context.Background(),
        25*time.Second,
    )
    defer cancel()
    if err := server.Shutdown(ctx); err != nil {
        log.Printf("关机超时: %v", err)
    }
    closeExternalConnections()
    log.Println("服务已关闭")
}

注意Shutdown的context超时设置为25秒而非30秒,留出5秒作为安全余量,避免被K8s在宽限期结束时SIGKILL强制终止。

连接排空:让正在处理的请求自然完成

server.Shutdown()会关闭监听器停止接收新连接,但已建立的HTTP连接会继续处理。对于长连接场景(WebSocket、gRPC流),需要额外的连接排空逻辑:

type ConnectionTracker struct {
    wg sync.WaitGroup
}

func (ct *ConnectionTracker) Add() {
    ct.wg.Add(1)
}

func (ct *ConnectionTracker) Done() {
    ct.wg.Done()
}

func (ct *ConnectionTracker)
    WaitWithTimeout(timeout time.Duration) bool {
    done := make(chan struct{})
    go func() {
        ct.wg.Wait()
        close(done)
    }()
    select {
    case <-done:
        return true
    case <-time.After(timeout):
        return false
    }
}

func (ct *ConnectionTracker)
    Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(
      func(w http.ResponseWriter, r *http.Request) {
        ct.Add()
        defer ct.Done()
        next.ServeHTTP(w, r)
    })
}

数据库连接与消息中间件的优雅关闭

数据库连接池和消息中间件连接的关闭顺序很重要。原则是先关闭消息消费者(不再消费新消息),再等待正在处理的消息完成,然后关闭数据库连接(确保事务提交),最后关闭消息生产者:

func closeExternalConnections() {
    log.Println("关闭消息消费者...")
    consumer.Close()
    log.Println("等待消息处理完成...")
    messageWg.Wait()
    log.Println("关闭数据库连接...")
    if err := db.Close(); err != nil {
        log.Printf("数据库关闭异常: %v", err)
    }
    log.Println("关闭消息生产者...")
    producer.Close()
}

gRPC服务的优雅关机

gRPC服务的优雅关机比HTTP更复杂,因为需要处理流式RPC:

func startGRPCServer() {
    lis, _ := net.Listen("tcp", ":9090")
    grpcServer := grpc.NewServer()
    pb.RegisterYourServiceServer(
        grpcServer, &yourServiceImpl{})
    go func() {
        grpcServer.Serve(lis)
    }()
    quit := make(chan os.Signal, 1)
    signal.Notify(quit,
        syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("gRPC开始关机")
    done := make(chan struct{})
    go func() {
        grpcServer.GracefulStop()
        close(done)
    }()
    select {
    case <-done:
        log.Println("gRPC已关闭")
    case <-time.After(25 * time.Second):
        log.Println("gRPC关机超时")
        grpcServer.Stop()
    }
}

Kubernetes环境下的健康检查配合

在K8s中优雅关机还需要配合readiness探针。当Pod收到SIGTERM后,应该立即将readiness探针标记为不健康,使Service不再将新流量路由到该Pod:

var ready atomic.Bool

func readinessHandler(
    w http.ResponseWriter, r *http.Request) {
    if ready.Load() {
        w.WriteHeader(http.StatusOK)
    } else {
        w.WriteHeader(
          http.StatusServiceUnavailable)
    }
}

func onShutdown() {
    ready.Store(false)
    log.Println("停止接收新流量")
}

完整的优雅关机不是简单的信号捕获加server.Shutdown(),而是需要将HTTP连接排空、数据库事务收尾、消息中间件消费者停止、gRPC流式RPC完成这些操作按正确顺序编排,并与K8s的readiness探针和terminationGracePeriodSeconds协调配合。每个环节的超时设置都要留出安全余量,避免被K8s强制SIGKILL。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-wei-fu-wu-you-ya-guan-ji-shi-zhan-cong-xin-hao-bu-huo/

(0)
小编小编
上一篇 15小时前
下一篇 15小时前

相关推荐