Graceful Shutdown
Зачем знать: Падающий сервис теряет in-flight запросы, оставляет открытые транзакции и не сбрасывает буферы. Graceful shutdown — стандарт production: вы закрываете listener, ждёте завершения текущих запросов, освобождаете ресурсы. В Kubernetes без graceful shutdown ваш rollout даст 503 пользователям. На middle-уровне это must-have.
Содержание
Заголовок раздела «Содержание»- Базовая концепция
- В Go идиоматично
- Gotchas
- Best practices
- Вопросы на собесе
- Practice
- Источники
1. Базовая концепция
Заголовок раздела «1. Базовая концепция»1.1 Что такое graceful shutdown
Заголовок раздела «1.1 Что такое graceful shutdown»Graceful shutdown — завершение работы сервиса в три шага:
- Stop accepting новые запросы (закрыть listener).
- Drain in-flight запросов (дать им завершиться).
- Close resources (БД, очереди, exporters).
Hard shutdown = немедленный os.Exit(1). Это:
- Прерывает HTTP-запросы (клиент видит connection reset);
- Откатывает транзакции БД;
- Теряет буферизованные данные (Kafka producer, OTel spans);
- Оставляет «зомби»-состояния в downstream.
1.2 HTTP-сервер: Shutdown vs Close
Заголовок раздела «1.2 HTTP-сервер: Shutdown vs Close»type Server struct { ... }
// Shutdown — gracefulfunc (s *Server) Shutdown(ctx context.Context) error
// Close — abruptfunc (s *Server) Close() error-
Shutdown(ctx):- Закрывает все listeners (
Acceptвозвращает error); - Закрывает idle connections;
- Ждёт завершения активных request’ов;
- Возвращает, когда всё чисто или
ctxотменился. ListenAndServe()возвращаетhttp.ErrServerClosed.
- Закрывает все listeners (
-
Close():- Немедленно закрывает listener и все connections, включая активные.
- Используется как «emergency stop».
1.3 gRPC-сервер: GracefulStop vs Stop
Заголовок раздела «1.3 gRPC-сервер: GracefulStop vs Stop»type Server struct { ... }
// GracefulStop — gracefulfunc (s *Server) GracefulStop()
// Stop — abruptfunc (s *Server) Stop()GracefulStop():- Закрывает listener;
- Ждёт завершения текущих RPC;
- Блокирующий вызов — без timeout-механизма.
Stop():- Прерывает RPC сразу.
⚠️ У GracefulStop() нет встроенного таймаута. Нужно оборачивать вручную.
1.4 Сигналы
Заголовок раздела «1.4 Сигналы»Linux сигналы, важные для shutdown:
| Сигнал | Источник | Поведение |
|---|---|---|
| SIGINT (2) | Ctrl+C в терминале | можно поймать, типично — graceful shutdown |
| SIGTERM (15) | kill, k8s default | можно поймать, k8s шлёт это для termination |
| SIGKILL (9) | kill -9 | НЕЛЬЗЯ поймать, мгновенный exit |
| SIGHUP (1) | terminal disconnect | традиционно — reload config |
| SIGQUIT (3) | Ctrl+\ | можно поймать; Go runtime по дефолту печатает stack traces |
В k8s lifecycle: PID 1 в контейнере получает SIGTERM, ждёт terminationGracePeriodSeconds, потом SIGKILL.
1.5 signal.NotifyContext (Go 1.16+)
Заголовок раздела «1.5 signal.NotifyContext (Go 1.16+)»Современный pattern — signal.NotifyContext создаёт context, отменяемый сигналом:
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)defer stop()
// сервер работаетgo runServer(ctx)
// блокируемся до сигнала<-ctx.Done()log.Println("shutdown signal received")Когда приходит SIGINT/SIGTERM — ctx.Done() закрывается. Все, кто подписан на этот ctx, узнают об остановке.
1.6 Order of shutdown
Заголовок раздела «1.6 Order of shutdown»Порядок имеет значение:
1. Readiness probe FAIL (LB перестаёт слать новые запросы)2. (опционально) Sleep N (LB обновляет endpoints)3. Stop HTTP/gRPC listener (Accept перестаёт работать)4. Drain in-flight requests (active handlers завершаются)5. Close downstream: - HTTP/gRPC client conn - Kafka producer (flush) - DB pool - Trace exporters (flush spans) - Metrics exporters (flush)6. Logger sync (flush stdout/file)Логика: сначала перестать принимать, потом дать завершиться, потом отрезать downstream.
1.7 Kubernetes lifecycle
Заголовок раздела «1.7 Kubernetes lifecycle»Pod termination sequence:1. kubectl delete pod → pod status: Terminating2. Endpoints controller убирает pod из Service endpoints (НО это асинхронно с шагом 3!)3. PreStop hook (если есть) — выполняется СИНХРОННО4. SIGTERM → PID 15. Wait up to terminationGracePeriodSeconds (default 30s)6. SIGKILL⚠️ Шаги 2 и 3-4 происходят параллельно. Это значит:
- Если приложение быстро остановилось, оно может ещё получать запросы (LB не обновился).
- Решение —
preStop: sleep 5, чтобы дать LB обновить endpoints до SIGTERM.
apiVersion: v1kind: Podspec: terminationGracePeriodSeconds: 30 containers: - name: app lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 5"]2. В Go идиоматично
Заголовок раздела «2. В Go идиоматично»2.1 Минимальный HTTP graceful shutdown
Заголовок раздела «2.1 Минимальный HTTP graceful shutdown»package main
import ( "context" "errors" "log" "net/http" "os/signal" "syscall" "time")
func main() { srv := &http.Server{ Addr: ":8080", Handler: myHandler(), ReadHeaderTimeout: 5 * time.Second, }
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop()
// Запускаем сервер go func() { log.Println("server listening on", srv.Addr) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("server error: %v", err) } }()
// Ждём сигнал <-ctx.Done() log.Println("shutting down...")
// Граничное время на shutdown shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil { log.Printf("shutdown error: %v", err) srv.Close() // hard kill } log.Println("server stopped")}Ключевые моменты:
signal.NotifyContext— один источник для сигнала.ListenAndServeвозвращаетhttp.ErrServerClosedпослеShutdown— это норма.shutdownCtxс таймаутом (например, 30s) гарантирует, что мы не висим вечно.- Fallback
srv.Close()— если graceful не уложился в таймаут.
2.2 gRPC graceful shutdown
Заголовок раздела «2.2 gRPC graceful shutdown»package main
import ( "context" "log" "net" "os/signal" "syscall" "time"
"google.golang.org/grpc")
func main() { lis, err := net.Listen("tcp", ":9090") if err != nil { log.Fatal(err) } s := grpc.NewServer() // ... s.RegisterService(...)
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop()
go func() { log.Println("gRPC listening on", lis.Addr()) if err := s.Serve(lis); err != nil { log.Fatalf("serve: %v", err) } }()
<-ctx.Done() log.Println("gRPC shutting down...")
done := make(chan struct{}) go func() { s.GracefulStop() close(done) }()
select { case <-done: log.Println("gRPC stopped gracefully") case <-time.After(30 * time.Second): log.Println("gRPC shutdown timeout, forcing") s.Stop() }}⚠️ GracefulStop() блокирующий и без таймаута. Оборачиваем в горутину + select с time.After.
2.3 Комбинированный сервис (HTTP + gRPC + worker)
Заголовок раздела «2.3 Комбинированный сервис (HTTP + gRPC + worker)»Типовая ситуация: один процесс держит несколько серверов и фоновых workers.
package main
import ( "context" "errors" "log" "net" "net/http" "os/signal" "sync" "syscall" "time"
"google.golang.org/grpc")
func main() { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop()
// HTTP сервер httpSrv := &http.Server{ Addr: ":8080", Handler: httpHandler(), ReadHeaderTimeout: 5 * time.Second, }
// gRPC сервер grpcLis, _ := net.Listen("tcp", ":9090") grpcSrv := grpc.NewServer() // ... регистрация сервисов
// Worker var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() runWorker(ctx) }()
// Запуск серверов go func() { if err := httpSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Printf("http: %v", err) stop() // триггерим shutdown всех } }() go func() { if err := grpcSrv.Serve(grpcLis); err != nil { log.Printf("grpc: %v", err) stop() } }()
<-ctx.Done() log.Println("shutdown started")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel()
// Параллельно останавливаем HTTP и gRPC var shutdownWg sync.WaitGroup shutdownWg.Add(2) go func() { defer shutdownWg.Done() if err := httpSrv.Shutdown(shutdownCtx); err != nil { log.Printf("http shutdown: %v", err) httpSrv.Close() } }() go func() { defer shutdownWg.Done() stopped := make(chan struct{}) go func() { grpcSrv.GracefulStop() close(stopped) }() select { case <-stopped: case <-shutdownCtx.Done(): log.Println("grpc forced stop") grpcSrv.Stop() } }()
shutdownWg.Wait()
// Ждём worker (он сам реагирует на ctx) wg.Wait()
// Закрываем downstream closeDB() flushKafka() flushTraces(shutdownCtx) log.Println("all done")}
func runWorker(ctx context.Context) { t := time.NewTicker(time.Second) defer t.Stop() for { select { case <-ctx.Done(): log.Println("worker stopped") return case <-t.C: // do work } }}2.4 Worker pool drain
Заголовок раздела «2.4 Worker pool drain»Если у вас pool горутин обрабатывают задачи — нужно дать им завершить in-flight:
type Pool struct { jobs chan Job wg sync.WaitGroup}
func NewPool(n int) *Pool { p := &Pool{jobs: make(chan Job, 100)} for i := 0; i < n; i++ { p.wg.Add(1) go p.worker() } return p}
func (p *Pool) worker() { defer p.wg.Done() for job := range p.jobs { job.Do() }}
func (p *Pool) Submit(j Job) { p.jobs <- j }
// Shutdown — graceful: закрываем jobs, ждёмfunc (p *Pool) Shutdown(ctx context.Context) error { close(p.jobs) done := make(chan struct{}) go func() { p.wg.Wait() close(done) }() select { case <-done: return nil case <-ctx.Done(): return ctx.Err() }}2.5 Close database pool
Заголовок раздела «2.5 Close database pool»import "github.com/jackc/pgx/v5/pgxpool"
pool, _ := pgxpool.New(ctx, dsn)defer pool.Close() // блокирующий: ждёт завершения всех queries
// Если нужен timeout:done := make(chan struct{})go func() { pool.Close() close(done)}()select {case <-done:case <-time.After(10 * time.Second): log.Println("db close timeout")}database/sql:
db.Close() // блокирует, ждёт active queries; idle conns закрывает2.6 Close Kafka producer
Заголовок раздела «2.6 Close Kafka producer»segmentio/kafka-go:
producer := &kafka.Writer{ ... }defer producer.Close() // flush + closeconfluent-kafka-go:
producer := kafka.NewProducer(...)producer.Flush(15 * 1000) // 15 секунд на flushproducer.Close()⚠️ Без flush — buffered messages теряются.
2.7 Close OTel trace exporters
Заголовок раздела «2.7 Close OTel trace exporters»import "go.opentelemetry.io/otel/sdk/trace"
tp := trace.NewTracerProvider(...)// ...shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()if err := tp.Shutdown(shutdownCtx); err != nil { log.Printf("trace shutdown: %v", err)}Без Shutdown — pending spans потеряются.
2.8 errgroup pattern
Заголовок раздела «2.8 errgroup pattern»golang.org/x/sync/errgroup упрощает orchestration:
import "golang.org/x/sync/errgroup"
func runApp(ctx context.Context) error { g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return runHTTPServer(ctx) }) g.Go(func() error { return runGRPCServer(ctx) }) g.Go(func() error { return runWorker(ctx) })
return g.Wait()}Если один сервер падает → ctx отменяется → остальные тоже завершаются.
2.9 Lifecycle abstraction
Заголовок раздела «2.9 Lifecycle abstraction»Если у вас 10 компонентов — удобно их обобщить:
type Component interface { Start(ctx context.Context) error Stop(ctx context.Context) error}
type App struct { components []Component}
func (a *App) Run(parent context.Context) error { ctx, cancel := signal.NotifyContext(parent, syscall.SIGINT, syscall.SIGTERM) defer cancel()
for _, c := range a.components { if err := c.Start(ctx); err != nil { return err } } <-ctx.Done()
shutdownCtx, sc := context.WithTimeout(context.Background(), 30*time.Second) defer sc() // Reverse order for i := len(a.components) - 1; i >= 0; i-- { if err := a.components[i].Stop(shutdownCtx); err != nil { log.Printf("stop %T: %v", a.components[i], err) } } return nil}Этот pattern используют uber-go/fx, go-kit, etc.
2.10 Тестирование graceful shutdown
Заголовок раздела «2.10 Тестирование graceful shutdown»func TestGracefulShutdown(t *testing.T) { srv := newServer() go srv.Run()
// Дёргаем эндпоинт resp, err := http.Get("http://localhost:8080/long-running") require.NoError(t, err)
// Запускаем shutdown proc, _ := os.FindProcess(os.Getpid()) proc.Signal(syscall.SIGTERM)
// Запрос должен завершиться нормально body, err := io.ReadAll(resp.Body) require.NoError(t, err) assert.Equal(t, "ok", string(body))
// Новые запросы должны отбиваться _, err = http.Get("http://localhost:8080/") require.Error(t, err)}Или unit-тест:
func TestServer_Shutdown(t *testing.T) { srv := &http.Server{ Addr: ":0", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(100 * time.Millisecond) // in-flight request w.WriteHeader(200) }), } lis, _ := net.Listen("tcp", ":0") go srv.Serve(lis)
// Делаем долгий запрос var resp *http.Response var err error go func() { resp, err = http.Get("http://" + lis.Addr().String()) }() time.Sleep(10 * time.Millisecond)
// Shutdown ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() require.NoError(t, srv.Shutdown(ctx))
// Старый запрос должен закончиться успешно time.Sleep(150 * time.Millisecond) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode)}3. Gotchas
Заголовок раздела «3. Gotchas»3.1 Запутались в errors после Shutdown
Заголовок раздела «3.1 Запутались в errors после Shutdown»err := srv.ListenAndServe()if err != nil { log.Fatal(err) // ПЛОХО: после Shutdown это http.ErrServerClosed}Правильно:
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatal(err)}3.2 GracefulStop без таймаута
Заголовок раздела «3.2 GracefulStop без таймаута»grpcSrv.GracefulStop() // блокирует НАВЕЧНО, если client держит RPCРешение — обернуть в горутину + select:
done := make(chan struct{})go func() { grpcSrv.GracefulStop(); close(done) }()select {case <-done:case <-time.After(30 * time.Second): grpcSrv.Stop() // force}3.3 SIGKILL — нельзя поймать
Заголовок раздела «3.3 SIGKILL — нельзя поймать»signal.Notify(ch, syscall.SIGKILL) // НЕРАБОТАЕТSIGKILL не доставляется в user space. Если кто-то шлёт kill -9 — вы ничего не можете сделать. В k8s SIGKILL приходит после terminationGracePeriodSeconds, поэтому graceful должен укладываться в этот лимит.
3.4 Long-running handler не уважает ctx
Заголовок раздела «3.4 Long-running handler не уважает ctx»func handler(w http.ResponseWriter, r *http.Request) { time.Sleep(60 * time.Second) // НЕ остановится на shutdown w.WriteHeader(200)}Shutdown() ждёт handler. Если handler 60s, shutdown 60s. Решение:
select {case <-time.After(60 * time.Second):case <-r.Context().Done(): return}3.5 Shutdown в порядке зависимостей
Заголовок раздела «3.5 Shutdown в порядке зависимостей»// ПЛОХО:db.Close() // активные queries обломятсяhttpSrv.Shutdown() // handler пытается обратиться к db → ошибкаПравильный порядок:
- HTTP/gRPC Shutdown (handlers завершаются);
- Worker pool drain;
- DB close;
- Kafka, OTel exporters.
3.6 Kubernetes Endpoints обновляется асинхронно
Заголовок раздела «3.6 Kubernetes Endpoints обновляется асинхронно»Pod получает SIGTERM, но LB ещё 3-5 секунд шлёт ему запросы (пока Endpoints не обновились). Без preStop sleep — те запросы получат connection refused.
lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 5"]Этот sleep блокирует SIGTERM, давая LB время обновить endpoints.
3.7 PreStop hook не учитывает terminationGracePeriodSeconds
Заголовок раздела «3.7 PreStop hook не учитывает terminationGracePeriodSeconds»lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 60"] # 60sterminationGracePeriodSeconds: 30 # 30sЭто БАГ: preStop выполняется внутри grace period. После 30s pod SIGKILL’нется, preStop не завершится. preStop + shutdown ≤ terminationGracePeriodSeconds.
3.8 Readiness probe не сменилась на Failure
Заголовок раздела «3.8 Readiness probe не сменилась на Failure»readinessProbe: httpGet: path: /readyz port: 8080 periodSeconds: 10Если probe каждые 10s, и pod получает SIGTERM — LB ещё 10s может слать запросы. Решение:
- При SIGTERM сразу делайте
/readyzотдавать 503; - Sleep в preStop > periodSeconds;
- Уменьшить periodSeconds.
var ready atomic.Boolready.Store(true)
http.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) { if !ready.Load() { http.Error(w, "not ready", http.StatusServiceUnavailable) return } w.WriteHeader(200)})
// При shutdown<-ctx.Done()ready.Store(false) // probe начнёт failитьtime.Sleep(15 * time.Second) // дать LB обновитьсяsrv.Shutdown(...)3.9 Kafka producer не flush
Заголовок раздела «3.9 Kafka producer не flush»producer := kafka.NewProducer(...)// ... отправили 1000 messagesproducer.Close() // ПЛОХО: без Flush буфер потеряетсяПеред Close — producer.Flush(timeoutMs).
3.10 db.Close на pool с активными tx
Заголовок раздела «3.10 db.Close на pool с активными tx»db.Close() // активные транзакции откатятсяСначала дайте handlers завершиться (HTTP Shutdown), потом db.Close.
3.11 Глобальная переменная srv
Заголовок раздела «3.11 Глобальная переменная srv»var srv *http.Server // ПЛОХО
func main() { srv = &http.Server{...}}
// в другой горутине:srv.Shutdown(...) // race condition: srv может быть nilПередавайте srv через канал или используйте sync.Once.
3.12 Не выходим из main
Заголовок раздела «3.12 Не выходим из main»<-ctx.Done()srv.Shutdown(...)// забыли вернуться из main → процесс виситУбедитесь, что все горутины завершены (через wg.Wait()), потом main возвращает control.
3.13 OS exit без cleanup
Заголовок раздела «3.13 OS exit без cleanup»log.Fatal(err) // os.Exit(1) — defer'ы НЕ выполнятсяlog.Fatal и os.Exit пропускают defer. Используйте только в main() после явного cleanup, или возвращайте error.
3.14 Запрос пришёл в момент shutdown
Заголовок раздела «3.14 Запрос пришёл в момент shutdown»T=0: pod SIGTERMT=100ms: новый запрос прилетелT=101ms: handler начал обработкуT=200ms: Shutdown calledShutdown всё равно дождётся этого handler’а. Это OK, потому что k8s даёт terminationGracePeriodSeconds.
3.15 cgo deps
Заголовок раздела «3.15 cgo deps»Если есть cgo-зависимости (например, на C-библиотеки) — их Cleanup иногда нужно делать вручную, defer не помогает в main. Это редкий edge case.
3.16 Daemonizing (двойной fork)
Заголовок раздела «3.16 Daemonizing (двойной fork)»Не нужно в Go и не нужно в k8s. В контейнере PID 1 — это ваше приложение, и оно должно сразу обрабатывать сигналы.
4. Best practices
Заголовок раздела «4. Best practices»signal.NotifyContext— единый context для shutdown сигнала.- Shutdown с таймаутом (30s типично) для HTTP/gRPC.
- Fallback Close()/Stop() если graceful не уложился.
- Игнорируйте
http.ErrServerClosed— это не ошибка. GracefulStopоборачивайте в select с time.After.- Порядок: listener → handlers → workers → downstream.
/readyz→ 503 при shutdown для удаления из LB.- PreStop sleep 5-10s в k8s для смены Endpoints.
- preStop + shutdown ≤ terminationGracePeriodSeconds.
- Flush Kafka producer перед Close.
- Shutdown OTel TracerProvider для отправки pending spans.
- db.Close после handlers — не наоборот.
- Worker pool drain через close(jobs) + wg.Wait().
- errgroup для orchestration нескольких компонентов.
- Тестируйте shutdown в CI (запустить, послать SIGTERM, проверить exit code).
- Никогда не используйте log.Fatal в shutdown path — пропускает defer.
- Слушайте SIGINT + SIGTERM, остальные сигналы — по необходимости.
- Log shutdown phases для debug в проде.
- Healthcheck различает liveness и readiness.
- Не используйте daemonize в Go или контейнерах.
5. Вопросы на собесе
Заголовок раздела «5. Вопросы на собесе»-
Что такое graceful shutdown? Завершение работы сервиса в 3 шага: stop accepting, drain in-flight, close resources. Без потери данных и обрыва клиентов.
-
Чем
Shutdownотличается отCloseв http.Server?Shutdown(ctx)— graceful (ждёт активные запросы).Close()— abrupt (закрывает всё немедленно). -
Что возвращает
ListenAndServeпослеShutdown?http.ErrServerClosed. Это норма, игнорируем черезerrors.Is. -
Чем
GracefulStopотличается отStopв gRPC?GracefulStopждёт текущие RPC, без встроенного таймаута.Stop— мгновенный. -
Как сделать таймаут на
GracefulStop? Обернуть в горутину + select сtime.After; по таймауту вызватьStop(). -
Какие сигналы важны для shutdown в Linux? SIGINT (Ctrl+C), SIGTERM (k8s, kill). SIGKILL — нельзя поймать.
-
Что такое
signal.NotifyContext? Создаёт context, отменяемый при получении сигнала. Удобный паттерн с Go 1.16+. -
Можно ли поймать SIGKILL? Нет. Это by design — для emergency stop, не доставляется в user space.
-
Что делает Kubernetes при удалении pod’а? (1) убирает из Endpoints, (2) preStop hook, (3) SIGTERM, (4) ждёт terminationGracePeriodSeconds, (5) SIGKILL.
-
Зачем preStop sleep N в k8s? Чтобы дать LB время обновить Endpoints до того, как pod начнёт shutdown — иначе клиенты получат 503.
-
Какой порядок shutdown компонентов? (1) Stop accepting (HTTP/gRPC listener), (2) drain handlers, (3) close workers, (4) close DB/Kafka, (5) flush exporters.
-
Зачем нужен
terminationGracePeriodSeconds? K8s даёт pod’у это время на graceful shutdown. После — SIGKILL. Default 30s. -
Что произойдёт, если shutdown превысит grace period? K8s пришлёт SIGKILL, in-flight requests оборвутся, defer не выполнится.
-
Как обновить readiness probe при shutdown? При получении сигнала atomic-флагом отвечаем 503 на
/readyz. LB перестаёт слать запросы. -
Зачем
defer pool.Close()в main? Освободить connection pool. ⚠️log.Fatalпропускает defer, аккуратно с error handling. -
Что такое drain worker pool? Закрыть jobs channel + дождаться
wg.Wait()— все workers закончат текущие задачи. -
Почему Kafka producer.Close без Flush — баг? Buffered messages, не отправленные ещё в Kafka, теряются. Перед Close —
Flush(timeoutMs). -
Зачем нужен
Shutdownу OTel TracerProvider? Чтобы pending spans отправились в коллектор. Без — span’ы теряются. -
Можно ли
db.Close()сразу после SIGTERM? Нет, сначала дождитесь HTTP/gRPC Shutdown — handlers могут ещё использовать БД. -
Чем
errgroupпомогает graceful shutdown?errgroup.WithContextотменяет ctx при ошибке любой горутины. Все компоненты подписаны на этот ctx и завершаются вместе.
6. Practice
Заголовок раздела «6. Practice»-
Напишите HTTP-сервер с эндпоинтом
/slow(sleep 10s, проверяет ctx.Done). Запустите, дёрните эндпоинт, пошлите SIGTERM. Проверьте, что request завершается, новые отбиваются с connection refused. -
Аналогично — для gRPC. Реализуйте
GracefulStopс таймаутом 5s; продемонстрируйте fallback наStop. -
Сделайте комбинированный сервис: HTTP (8080) + gRPC (9090) + worker (раз в секунду пишет в БД). Один SIGTERM должен остановить всё:
- HTTP graceful
- gRPC graceful
- Worker — реагирует на ctx
- DB close в конце
-
В k8s манифесте настройте:
terminationGracePeriodSeconds: 30;preStop: sleep 5;readinessProbeс periodSeconds: 3. Снимите trace pod termination черезkubectl describe pod.
-
Реализуйте паттерн readiness flag:
atomic.Bool ready. При SIGTERM —ready.Store(false),/readyzначинает отдавать 503. Sleep 15s, потом срабатывает Shutdown. -
Напишите интеграционный тест:
- Запустить сервер;
- В loop’е дёргать эндпоинт;
- Послать SIGTERM;
- Убедиться, что:
- in-flight завершились без ошибок;
- новые запросы получают connection refused;
- процесс завершился в течение 30s.
-
Включите Kafka producer. Покажите: без
Flush— теряются 50 messages при shutdown; сFlush— нет. -
Включите OTel exporter (Jaeger / OTLP). Покажите: без
TracerProvider.Shutdownпропадают spans последнего 1s. -
Протестируйте сценарий: graceful shutdown превышает grace period — pod получает SIGKILL. Какие данные потеряются?
-
Реализуйте Lifecycle Manager: компоненты регистрируются, Start/Stop в правильном порядке (reverse при Stop).
7. Источники
Заголовок раздела «7. Источники»- net/http godoc: Shutdown — https://pkg.go.dev/net/http#Server.Shutdown
- gRPC Go: GracefulStop — https://pkg.go.dev/google.golang.org/grpc#Server.GracefulStop
- signal.NotifyContext — https://pkg.go.dev/os/signal#NotifyContext
- Kubernetes Pod Lifecycle — https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/
- Kubernetes Best Practices: Terminating with grace — https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-terminating-with-grace
- Cindy Sridharan. Graceful shutdowns in Kubernetes — https://medium.com/@copyconstruct/graceful-shutdowns-go-kubernetes-d83cc5e8e1a
- Mat Ryer. Stop, Drain, Done (HTTP server lifecycle) — https://medium.com/@matryer/golang-advent-calendar-day-six-d33b40bd49b
- golang.org/x/sync/errgroup — https://pkg.go.dev/golang.org/x/sync/errgroup
- segmentio/kafka-go — https://github.com/segmentio/kafka-go
- OpenTelemetry Go SDK — https://opentelemetry.io/docs/languages/go/