process.on('SIGTERM', shutdown) → signal.NotifyContext(ctx, syscall.SIGTERM)
server.close() → httpServer.Shutdown(ctx)
await Promise.all([db.close(), redis.quit()]) → последовательное закрытие с таймаутами
process.exit(0) → os.Exit(0) (но лучше дать defer’ам отработать)
setTimeout(() => process.exit(1), 10000) → context.WithTimeout для всего shutdown
mkdir go-graceful-shutdown && cd go-graceful-shutdown
go mod init go-graceful-shutdown
# Устанавливаем зависимости (для демонстрации)
go get github.com/redis/go-redis/v9
go get github.com/jackc/pgx/v5/pgxpool
go get github.com/rs/zerolog
go mod tidy
# Структура
mkdir -p internal/shutdown
mkdir -p cmd/server
internal/shutdown/shutdown.gopackage shutdown
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
// ╔══════════════════════════════════════════════════════════╗
// ║ КОМПОНЕНТ ДЛЯ ОСТАНОВКИ ║
// ╚══════════════════════════════════════════════════════════╝
// Component — интерфейс для любого компонента, который нужно остановить
type Component interface {
Name() string
Shutdown(ctx context.Context) error
}
// Manager — управляет graceful shutdown всех компонентов
type Manager struct {
components []Component
timeout time.Duration // Общий таймаут на остановку
logger *log.Logger
}
// NewManager — создаёт менеджер
func NewManager(timeout time.Duration, logger *log.Logger) *Manager {
return &Manager{
timeout: timeout,
logger: logger,
}
}
// Register — регистрирует компонент для остановки
// Порядок регистрации ВАЖЕН: останавливаются в обратном порядке!
func (m *Manager) Register(components ...Component) {
m.components = append(m.components, components...)
}
// ShutdownAll — останавливает все компоненты в обратном порядке
func (m *Manager) ShutdownAll(ctx context.Context) error {
m.logger.Println("🛑 Начало graceful shutdown...")
// Создаём контекст с общим таймаутом
shutdownCtx, cancel := context.WithTimeout(ctx, m.timeout)
defer cancel()
var mu sync.Mutex
var errs []error
// Останавливаем в ОБРАТНОМ порядке (последний зарегистрированный — первый останавливается)
for i := len(m.components) - 1; i >= 0; i-- {
comp := m.components[i]
m.logger.Printf(" ⏳ Остановка %s...", comp.Name())
start := time.Now()
err := comp.Shutdown(shutdownCtx)
elapsed := time.Since(start)
if err != nil {
m.logger.Printf(" ❌ %s: ошибка за %v: %v", comp.Name(), elapsed, err)
mu.Lock()
errs = append(errs, fmt.Errorf("%s: %w", comp.Name(), err))
mu.Unlock()
} else {
m.logger.Printf(" ✅ %s: остановлен за %v", comp.Name(), elapsed)
}
}
if len(errs) > 0 {
return fmt.Errorf("shutdown errors: %v", errs)
}
m.logger.Println("✅ Graceful shutdown завершён")
return nil
}
// WaitForSignal — блокируется до получения сигнала на остановку
func WaitForSignal(logger *log.Logger) os.Signal {
sigCh := make(chan os.Signal, 1)
// SIGINT — Ctrl+C
// SIGTERM — kill / docker stop / k8s termination
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
sig := <-sigCh
logger.Printf("📡 Получен сигнал: %v", sig)
return sig
}
// ╔══════════════════════════════════════════════════════════╗
// ║ ГОТОВЫЕ КОМПОНЕНТЫ ║
// ╚══════════════════════════════════════════════════════════╝
// HTTPServer — обёртка для http.Server
type HTTPServer struct {
server *http.Server
name string
}
func NewHTTPServer(name string, srv *http.Server) *HTTPServer {
return &HTTPServer{name: name, server: srv}
}
func (h *HTTPServer) Name() string {
return h.name
}
func (h *HTTPServer) Shutdown(ctx context.Context) error {
// 1. Сначала перестаём принимать НОВЫЕ запросы
// 2. Ждём завершения АКТИВНЫХ запросов (до таймаута)
return h.server.Shutdown(ctx)
}
// GRPCServer — обёртка для gRPC сервера
type GRPCServer struct {
server *grpc.Server
name string
}
func NewGRPCServer(name string, srv *grpc.Server) *GRPCServer {
return &GRPCServer{name: name, server: srv}
}
func (g *GRPCServer) Name() string {
return g.name
}
func (g *GRPCServer) Shutdown(ctx context.Context) error {
// GracefulStop ждёт завершения активных RPC
done := make(chan struct{})
go func() {
g.server.GracefulStop()
close(done)
}()
select {
case <-done:
return nil
case <-ctx.Done():
// Если не успели — жёсткая остановка
g.server.Stop()
return fmt.Errorf("gRPC graceful stop timeout: %w", ctx.Err())
}
}
// DBPool — обёртка для pgxpool
type DBPoolCloser struct {
pool *pgxpool.Pool
name string
}
func NewDBPoolCloser(pool *pgxpool.Pool) *DBPoolCloser {
return &DBPoolCloser{pool: pool, name: "PostgreSQL"}
}
func (d *DBPoolCloser) Name() string {
return d.name
}
func (d *DBPoolCloser) Shutdown(ctx context.Context) error {
d.pool.Close()
return nil
}
// RedisCloser — обёртка для redis.Client
type RedisCloser struct {
client *redis.Client
name string
}
func NewRedisCloser(client *redis.Client) *RedisCloser {
return &RedisCloser{client: client, name: "Redis"}
}
func (r *RedisCloser) Name() string {
return r.name
}
func (r *RedisCloser) Shutdown(ctx context.Context) error {
return r.client.Close()
}
import (
"net/http"
"google.golang.org/grpc"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/redis/go-redis/v9"
)
cmd/server/main.gopackage main
import (
"context"
"log"
"net/http"
"os"
"time"
"go-graceful-shutdown/internal/shutdown"
)
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
log.Println("🚀 Запуск production-сервера...")
// ╔══════════════════════════════════════════════════════╗
// ║ 1. ИНИЦИАЛИЗАЦИЯ КОМПОНЕНТОВ ║
// ╚══════════════════════════════════════════════════════╝
// HTTP-сервер
mux := http.NewServeMux()
mux.HandleFunc("/health", healthHandler)
mux.HandleFunc("/ready", readyHandler)
httpServer := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Имитация других компонентов (в реальности — pgxpool, redis.Client)
// dbPool, _ := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
// redisClient := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
// ╔══════════════════════════════════════════════════════╗
// ║ 2. РЕГИСТРАЦИЯ КОМПОНЕНТОВ В МЕНЕДЖЕРЕ ║
// ╚══════════════════════════════════════════════════════╝
// Создаём менеджер с общим таймаутом 15 секунд
manager := shutdown.NewManager(15*time.Second, log.Default())
// Регистрируем в порядке ИНИЦИАЛИЗАЦИИ
// Останавливаться будут в ОБРАТНОМ порядке:
// 1. HTTP (перестаём принимать запросы)
// 2. БД (закрываем после завершения запросов)
// 3. Redis
manager.Register(
shutdown.NewHTTPServer("HTTP API", httpServer),
// shutdown.NewDBPoolCloser(dbPool), // В реальном коде
// shutdown.NewRedisCloser(redisClient), // В реальном коде
)
// Добавляем фейковые компоненты для демонстрации
manager.Register(&FakeComponent{name: "Kafka Consumer", delay: 2 * time.Second})
manager.Register(&FakeComponent{name: "Metrics Exporter", delay: 500 * time.Millisecond})
// ╔══════════════════════════════════════════════════════╗
// ║ 3. ЗАПУСК СЕРВЕРА ║
// ╚══════════════════════════════════════════════════════╝
go func() {
log.Println("✅ Сервер на :8080")
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Ошибка HTTP: %v", err)
}
}()
// ╔══════════════════════════════════════════════════════╗
// ║ 4. ОЖИДАНИЕ СИГНАЛА И ОСТАНОВКА ║
// ╚══════════════════════════════════════════════════════╝
// Блокируемся до получения SIGINT/SIGTERM
shutdown.WaitForSignal(log.Default())
// Останавливаем все компоненты
ctx := context.Background()
if err := manager.ShutdownAll(ctx); err != nil {
log.Printf("⚠️ Ошибки при остановке: %v", err)
os.Exit(1)
}
log.Println("👋 Приложение остановлено")
}
// ╔══════════════════════════════════════════════════════════╗
// ║ ВСПОМОГАТЕЛЬНЫЕ ОБРАБОТЧИКИ ║
// ╚══════════════════════════════════════════════════════════╝
var isReady atomic.Bool
func init() {
isReady.Store(true)
}
// healthHandler — для liveness probe (жив ли процесс)
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
// readyHandler — для readiness probe (готов ли принимать запросы)
func readyHandler(w http.ResponseWriter, r *http.Request) {
if isReady.Load() {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Ready"))
return
}
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("Not Ready"))
}
// ╔══════════════════════════════════════════════════════════╗
// ║ ФЕЙКОВЫЙ КОМПОНЕНТ (для демонстрации) ║
// ╚══════════════════════════════════════════════════════════╝
type FakeComponent struct {
name string
delay time.Duration
}
func (f *FakeComponent) Name() string {
return f.name
}
func (f *FakeComponent) Shutdown(ctx context.Context) error {
log.Printf(" [%s] имитация остановки (%v)...", f.name, f.delay)
select {
case <-time.After(f.delay):
return nil
case <-ctx.Done():
return fmt.Errorf("timeout: %w", ctx.Err())
}
}
import (
"fmt"
"sync/atomic"
)
# Запуск сервера
go run ./cmd/server/main.go
# В другом терминале:
# Проверка health
curl http://localhost:8080/health
# Проверка readiness
curl http://localhost:8080/ready
# Отправка graceful shutdown (Ctrl+C в первом терминале)
# ИЛИ:
kill -TERM $(pgrep go-graceful)
# Наблюдаем порядок остановки:
# 1. HTTP API (перестаёт принимать запросы)
# 2. Kafka Consumer (ждёт обработки текущих сообщений)
# 3. Metrics Exporter
# 4. БД
# 5. Redis
| Порядок | Компонент | Действие | Причина |
|---|---|---|---|
| 1 | Load Balancer | Отвести трафик | Новые запросы не приходят |
| 2 | Health Check | Readiness → false | K8s перестаёт отправлять запросы |
| 3 | HTTP Server | Shutdown(ctx) | Ждём завершения активных запросов |
| 4 | gRPC Server | GracefulStop() | Ждём завершения RPC |
| 5 | Kafka Consumer | Commit + Close | Фиксируем offset’ы |
| 6 | DB Pool | Close() | Закрываем соединения |
| 7 | Redis | Close() | Закрываем соединения |
| 8 | Tracing | Shutdown(ctx) | Отправляем оставшиеся спаны |
💡 Best practices от сеньоров:
💡 Для Node.js разработчика:
process.on('SIGTERM', async () => { await server.close(); process.exit(0); }).
httpGet: { path: /ready, port: 8080 }.