---
title: "Kubernetes — terminationGracePeriodSeconds, probes, maxSurge (Go)"
nav_title: "Kubernetes"
excerpt: "K8s-конфигурация graceful shutdown для Go net/http+chi: terminationGracePeriodSeconds 60, preStop sleep 10, раздельные /health/live и /health/ready, maxSurge 1 maxUnavailable 0."
keywords: "terminationGracePeriodSeconds 60, preStop sleep 10, maxSurge maxUnavailable, readinessProbe livenessProbe go, rolling deploy zero downtime, R-SHUT-K8S, kube-proxy go net/http"
focus_keyword: "Kubernetes graceful shutdown Go net/http"
tags:
  - go
  - kubernetes
  - graceful-shutdown
  - net/http
  - chi
---

# Kubernetes — terminationGracePeriodSeconds, probes, maxSurge (Go)

> **Опирается на правила:** `R-SHUT-K8S-1` … `R-SHUT-K8S-3` и `R-SHUT-K8S-X1` … `R-SHUT-K8S-X2` из Graceful Shutdown → [раздел 6. Kubernetes](/standards/backend/graceful-shutdown/#6-kubernetes--r-shut-k8s).

> **Важно знать**
> - **`terminationGracePeriodSeconds: 60`** — явно в Deployment, не k8s default 30.
> - **`preStop sleep 10`** — отдельный бюджет; SIGTERM приходит **после** preStop, не вместе с ним.
> - **`atomic.Bool` в `health.State`** — единственный источник readiness; `SetNotReady()` вызывается **до** `srv.Shutdown`.
> - **`/health/ready`** отвечает 503 при `!appState.IsReady()` — k8s убирает pod из endpoints; **`/health/live`** всегда 200 на shutdown.
> - **`srv.Shutdown(ctx)`**, не `srv.Close()` — Shutdown дожидается in-flight запросов, Close рвёт соединения немедленно.
> - **`maxSurge: 1, maxUnavailable: 0`** — новый pod принимает трафик до того как старый начал shutdown.
> - **Без `preStop`** — гарантированные 502 в окне 5-15 секунд после SIGTERM.

K8s — контекст, в котором живёт graceful shutdown. Go-сервис на net/http+chi может корректно завершать обработчики, но без правильной k8s-конфигурации клиенты увидят 502: kube-proxy не успевает убрать pod из endpoints до того, как SIGTERM добирается до процесса. Три k8s-параметра замыкают цепочку.

## terminationGracePeriodSeconds: 60

`R-SHUT-K8S-1`: total budget для всей shutdown-последовательности.

```yaml
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  template:
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - name: order-service
          image: order-service:2.1.0
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 10"]
```

Sequence завершения pod:

```
T=0s    kubelet начинает shutdown:
          - запускает preStop hook параллельно с убиранием pod из Service endpoints
T=10s   preStop sleep завершён → kubelet отправляет SIGTERM
T=10s   os.Signal-канал в Run() получает SIGTERM:
          - appState.SetNotReady()     // /health/ready → 503
          - cancelConsumerCtx()        // kafka consumer завершает batch
          - consumerWg.Wait()
          - schedulerWg.Wait()         // outbox relay дожимает итерацию
          - srv.Shutdown(shutCtx)      // дожидается in-flight HTTP
          - pool.Close()               // pgxpool последним
T=35s   процесс завершается (25s на shutdown после preStop)
T=70s   если процесс жив → SIGKILL
```

`terminationGracePeriodSeconds: 30` (k8s default) + preStop 10s → Go-процессу остаётся 20s, но `ShutdownTimeout: 25s` уже не помещается. Pod уходит в SIGKILL посередине дрейна HTTP.

Явная запись `60` обязательна — k8s не читает значение из кода приложения.

## Readiness-флаг и health endpoints

`R-SHUT-CFG-3`, `R-SHUT-CFG-4`, `R-SHUT-K8S-2`: на shutdown нужен readiness=503, liveness должна оставаться 200.

```go
// internal/health/state.go
package health

import "sync/atomic"

type State struct{ ready atomic.Bool }

func NewState() *State {
    s := &State{}
    s.ready.Store(true)
    return s
}

func (s *State) SetNotReady() { s.ready.Store(false) }
func (s *State) IsReady() bool { return s.ready.Load() }
```

```go
// internal/server/routes.go
func RegisterHealthRoutes(r chi.Router, state *health.State) {
    r.Get("/health/live", func(w http.ResponseWriter, _ *http.Request) {
        w.WriteHeader(http.StatusOK)
    })
    r.Get("/health/ready", func(w http.ResponseWriter, _ *http.Request) {
        if !state.IsReady() {
            w.WriteHeader(http.StatusServiceUnavailable)
            return
        }
        w.WriteHeader(http.StatusOK)
    })
}
```

На SIGTERM `SetNotReady()` вызывается первым — ещё до `srv.Shutdown`. k8s через `periodSeconds: 5` × `failureThreshold: 2` = 10s видит 503 и убирает pod из endpoints. Новый трафик не поступает; in-flight запросы, принятые до переключения, дожимаются через `srv.Shutdown`.

Liveness возвращает 200 всегда — падение liveness вызывает restart pod, что разрушает graceful.

## Probes в Deployment

`R-SHUT-K8S-2`:

```yaml
containers:
  - name: order-service
    readinessProbe:
      httpGet:
        path: /health/ready
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 5
      timeoutSeconds: 2
      failureThreshold: 2
    livenessProbe:
      httpGet:
        path: /health/live
        port: 8080
      initialDelaySeconds: 15
      periodSeconds: 10
      timeoutSeconds: 2
      failureThreshold: 3
```

| Probe | Endpoint | Действие k8s при fail |
|---|---|---|
| readinessProbe | `/health/ready` | Убрать из endpoints (без restart) |
| livenessProbe | `/health/live` | Перезапустить pod |

Go-сервисы стартуют быстро (< 5s в среднем), поэтому `initialDelaySeconds: 15` на liveness — достаточно. В отличие от JVM-сервисов, где прогрев JIT занимает 30-60s, Go-бинарь готов почти сразу после запуска.

`readinessProbe` начинает работать через 5s — если сервис поднялся, kube-proxy сразу добавит pod в endpoints.

## SIGTERM-handler и порядок shutdown

`R-SHUT-CFG-2`, `R-SHUT-CFG-3`:

```go
// internal/server/server.go
package server

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

    "github.com/example/order-service/internal/health"
)

type Config struct {
    ShutdownTimeout time.Duration // 25s
}

func Run(
    ctx context.Context,
    srv *http.Server,
    cfg Config,
    appState *health.State,
    cancelConsumerCtx context.CancelFunc,
    consumerDone <-chan struct{},
    schedulerDone <-chan struct{},
    closePool func(),
) error {
    sigC := make(chan os.Signal, 1)
    signal.Notify(sigC, syscall.SIGTERM, syscall.SIGINT)
    defer signal.Stop(sigC)

    errC := make(chan error, 1)
    go func() { errC <- srv.ListenAndServe() }()

    select {
    case sig := <-sigC:
        slog.InfoContext(ctx, "получили SIGTERM, начинаем graceful shutdown", "signal", sig.String())
    case err := <-errC:
        return err
    }

    appState.SetNotReady() // R-SHUT-CFG-3: readiness=503 первым

    cancelConsumerCtx()      // kafka consumer: не начинать новый batch
    <-consumerDone           // дождаться завершения текущего batch
    <-schedulerDone          // outbox relay: текущая итерация

    shutCtx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout)
    defer cancel()
    if err := srv.Shutdown(shutCtx); err != nil { // R-SHUT-CFG-1: дожать HTTP
        slog.ErrorContext(ctx, "http shutdown error", "error", err)
    }

    closePool() // R-SHUT-DB-1: pgxpool последним
    return nil
}
```

`context.WithTimeout(context.Background(), ...)` — не родительский `ctx`: на момент shutdown родительский контекст уже может быть отменён, а нам нужно завершить in-flight запросы корректно.

## maxSurge / maxUnavailable

`R-SHUT-K8S-3`: zero-downtime rolling deploy.

```yaml
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
```

Sequence deploy `order-service` v2.1.0 → v2.2.0:

```
Начало: 3 пода v2.1.0
T=0   kubelet создаёт pod v2.2.0 (итого 4 пода)
T=Ns  v2.2.0 проходит readinessProbe → добавлен в endpoints
T=Ns  pod v2.1.0 #1: preStop sleep 10 → SIGTERM → SetNotReady() → Shutdown → exit
      (3 активных пода: 2×v2.1.0 + 1×v2.2.0)
T=Ns  kubelet создаёт pod v2.2.0 #2 (итого 4)
...
```

Без `maxUnavailable: 0` k8s мог бы выключить старый pod до запуска нового — capacity падает, SLO нарушается.

`maxSurge: 1` — минимум для небольших сервисов (3-10 replicas). Для `ProductService` с 50+ replicas — `maxSurge: 25%`.

## Cumulative budget

`R-SHUT-OBS-1`: все фазы должны уложиться в 60s.

```
preStop sleep      10s  (k8s, до SIGTERM)
──────────────────────────────────────────  ← SIGTERM
SetNotReady         ~0s
consumerWg.Wait   ~15s  (kafka: текущий batch)
schedulerWg.Wait  ~10s  (outbox relay: итерация)
srv.Shutdown      ~25s  (HTTP: in-flight)
pool.Close          ~1s
──────────────────────────────────────────
Итого              61s  > 60s → нужно сократить
```

Если budget не помещается — сократить batch (100→20 событий в outbox), не увеличивать `terminationGracePeriodSeconds`. Большой budget скрывает медленные операции; правильное решение — ускорить их.

Структурный лог и метрика (`R-SHUT-OBS-2`, `R-SHUT-OBS-3`):

```go
// обёртка в Run(), до shutdown-последовательности:
start := time.Now()
slog.InfoContext(ctx, "graceful shutdown начат")
defer func() {
    slog.InfoContext(ctx, "graceful shutdown завершён",
        "duration_s", time.Since(start).Seconds(),
    )
    shutdownDuration.Set(time.Since(start).Seconds()) // promauto Gauge
}()
```

## Что запрещено

| Антипаттерн | Правило | Что взамен |
|---|---|---|
| Нет `preStop` в lifecycle | `R-SHUT-K8S-X1` | `exec: ["/bin/sh", "-c", "sleep 10"]` |
| `terminationGracePeriodSeconds: 30` (default) | `R-SHUT-K8S-X2` | `60` явно |
| Один `/health` для обеих probes | `R-SHUT-K8S-2` | отдельные `/health/live` и `/health/ready` |
| `srv.Close()` вместо `srv.Shutdown(ctx)` | `R-SHUT-CFG-1` | `http.Server.Shutdown` с явным таймаутом |
| `var shuttingDown bool` без `atomic.Bool` | `R-SHUT-CFG-X1` | `health.State` с `atomic.Bool` |
| `pool.Close()` до `consumerWg.Wait()` | `R-SHUT-DB-X1` | pgxpool — последним в shutdown-последовательности |
| Liveness зависит от состояния БД | `R-SHUT-K8S-2` | liveness всегда 200; БД-зависимость — в readiness |
| `maxUnavailable: 1` на production | `R-SHUT-K8S-3` | `maxUnavailable: 0` |

## Куда дальше

- [Бюджеты и observability](go/budgets-and-observability.md) — cumulative budget, `app_shutdown_duration_seconds`, структурный лог.
- [HTTP drain](go/http-drain.md) — `srv.Shutdown` vs `srv.Close`, `preStop` детали, долгие эндпоинты.
- [БД и persistence](go/db-and-persistence.md) — порядок закрытия `pgxpool`, транзакции на SIGTERM.
- [Kafka shutdown](go/kafka-shutdown.md) — `kafka-go` consumer через `context.Context`, `CommitMessages`, `writer.Close`.
- [Фоновые задачи и outbox](go/scheduled-async-outbox.md) — `sync.WaitGroup`, outbox relay, критичная секция с `context.Background()`.
- [Идемпотентность in-flight операций](go/idempotency-in-flight.md) — `Idempotency-Key`, `ON CONFLICT DO NOTHING` по `event_id`.
