← 中文资源

简体中文版本

Go 信号驱动的优雅停机

监听 SIGINT 与 SIGTERM,停止接收新连接并在限定时间内排空进行中的 HTTP 请求。

阅读英文原文

功能说明

main 用 signal.NotifyContext 把中断信号转换为 context,在 127.0.0.1:0 上启动 http.Server,并并发发起一个耗时 300ms 的请求。50ms 后调用 stop 模拟收到 SIGTERM,再用带 2 秒超时的 context 执行 Shutdown。Shutdown 会停止监听、等待进行中的 handler 完成,最后通过 serveErr 区分正常关闭与真实错误。

关键代码

Serve 在独立 goroutine 中运行,主流程不会被阻塞。server 设置了 ReadHeaderTimeout 与 IdleTimeout,减少慢速连接占用。Shutdown 返回后检查 http.ErrServerClosed,这是 Serve 在正常关闭时的预期错误。clientDone 确保客户端读取完成后再退出程序,避免日志丢失。示例用 stop() 代替真实信号,便于在测试和本地直接运行。

使用注意

Shutdown 只等待已经进入的请求,不会中断它们;超过 grace period 后应调用 server.Close 强制关闭。负载均衡器需要先摘除实例,否则停机期间仍可能收到新请求。生产环境还应关闭后台 worker、数据库连接池和消息消费者,并为整个关闭流程设置统一预算。Windows 下信号语义不同,容器中通常收到 SIGTERM,本地调试可能是 Ctrl+C。

验证方式

运行程序可看到客户端收到 finished,随后输出已停止接收新连接并排空请求。把 handler 延迟改为 5 秒、grace period 保持 2 秒,可验证 Shutdown 返回 deadline exceeded。再用真实进程发送 SIGTERM,或在测试中取消 NotifyContext 的父 context,检查 Serve 返回 ErrServerClosed 且没有 goroutine 泄漏。

代码示例

package main

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

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	mux := http.NewServeMux()
	mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
		select {
		case <-time.After(300 * time.Millisecond):
			fmt.Fprintln(w, "finished")
		case <-r.Context().Done():
			log.Printf("client went away: %v", r.Context().Err())
		}
	})
	mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		io.WriteString(w, "ok")
	})

	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		log.Fatal(err)
	}

	server := &http.Server{
		Handler:           mux,
		ReadHeaderTimeout: 2 * time.Second,
		IdleTimeout:       30 * time.Second,
	}

	serveErr := make(chan error, 1)
	go func() {
		serveErr <- server.Serve(listener)
	}()

	clientDone := make(chan struct{})
	go func() {
		defer close(clientDone)
		resp, err := http.Get("http://" + listener.Addr().String() + "/slow")
		if err != nil {
			log.Printf("request error: %v", err)
			return
		}
		defer resp.Body.Close()
		body, _ := io.ReadAll(resp.Body)
		log.Printf("client got %q", body)
	}()

	time.Sleep(50 * time.Millisecond)
	stop() // Simulates the signal after the request is already in flight.

	shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	if err := server.Shutdown(shutdownCtx); err != nil {
		log.Printf("graceful shutdown failed: %v", err)
	} else {
		log.Println("stopped accepting new connections and drained in-flight work")
	}

	<-clientDone
	if err := <-serveErr; err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Printf("serve error: %v", err)
	}
}