
Почти каждое падение доступности при релизе — это не «плохой код», а некорректная остановка. Контейнеру прислали SIGTERM — он мгновенно умер, балансировщик ещё шлёт ему трафик, запросы обрываются, очереди теряют подтверждения, клиенты видят 502. Правильное завершение (graceful shutdown) делает обратное: сначала сервис перестаёт брать новый трафик, спокойно доделывает текущие запросы, корректно закрывает соединения и только потом выходит.
Бизнес‑выгоды:
Ключевая идея — сервис сам управляет своей «готовностью» и даёт облачной обвязке время сделать свою работу.
Ниже минимальный HTTP‑сервис, который корректно обрабатывает SIGTERM, имеет /ready и даёт до 30 секунд на завершение запросов.
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"sync/atomic"
"syscall"
"time"
)
var ready atomic.Bool
func main() {
mux := http.NewServeMux()
// Бизнес‑ручка: имитируем работу 200–1500 мс
mux.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(200*time.Millisecond + time.Duration(time.Now().UnixNano()%1300_000_000))
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
// Readiness: 200 только когда готовы принимать трафик
mux.HandleFunc("/ready", func(w http.ResponseWriter, r *http.Request) {
if !ready.Load() {
http.Error(w, "not ready", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ready"))
})
// Liveness: сервис жив, если процесс жив и цикл событий крутится
mux.HandleFunc("/live", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("live"))
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
IdleTimeout: 90 * time.Second, // важен для Keep‑Alive
}
// Сразу после старта считаем себя неготовыми, пока не поднялись зависимости
// Здесь можно проверить подключение к БД/кешу и т.п.
go func() {
time.Sleep(300 * time.Millisecond)
ready.Store(true)
log.Println("service is ready")
}()
// Обработка сигналов
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
go func() {
log.Println("listening on :8080")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %v", err)
}
}()
<-stop
log.Println("signal received, starting graceful shutdown")
// 1) Сразу уходим из готовности — балансировщик перестанет присылать запросы
ready.Store(false)
// 2) Дадим LB время отписать нас (обычно 5–30 с)
drainDelay := 5 * time.Second
time.Sleep(drainDelay)
// 3) Аккуратная остановка HTTP‑сервера с таймаутом
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil { // останавливает приём новых коннектов и ждёт текущие
log.Printf("server shutdown error: %v", err)
}
log.Println("server exited cleanly")
}
Что важно:
В Node нужно явно следить за активными сокетами: server.close перестаёт принимать новые соединения и ждёт существующие, но полезно иметь верхний предел ожидания.
const http = require('http');
let isReady = false;
const sockets = new Set();
const server = http.createServer((req, res) => {
if (req.url === '/ready') {
if (!isReady) {
res.statusCode = 503; return res.end('not ready');
}
return res.end('ready');
}
if (req.url === '/live') return res.end('live');
// Имитация работы
const work = 200 + Math.floor(Math.random() * 1300);
setTimeout(() => { res.end('ok'); }, work);
});
server.keepAliveTimeout = 90_000; // для долговечных коннектов
server.headersTimeout = 95_000; // чуть больше keepAliveTimeout
server.on('connection', (socket) => {
sockets.add(socket);
socket.on('close', () => sockets.delete(socket));
});
server.listen(8080, () => {
setTimeout(() => { isReady = true; console.log('service is ready'); }, 300);
console.log('listening on :8080');
});
function shutdown() {
console.log('signal received, starting graceful shutdown');
isReady = false; // 1) выходим из готовности
const drainDelay = 5000; // 5с на отписку от LB
setTimeout(() => {
server.close(() => {
console.log('server closed cleanly');
process.exit(0);
});
// Жёсткий предел ожидания, чтобы не висеть вечно
const forceKill = setTimeout(() => {
console.warn('force closing lingering sockets');
for (const s of sockets) s.destroy();
process.exit(0);
}, 30000);
forceKill.unref();
}, drainDelay);
}
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
Главные настройки в манифестах:
Пример Deployment с корректными пробами и preStop:
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels: { app: checkout }
template:
metadata:
labels: { app: checkout }
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
image: registry.example.com/checkout:1.2.3
ports:
- containerPort: 8080
readinessProbe:
httpGet: { path: /ready, port: 8080 }
periodSeconds: 2
failureThreshold: 2
successThreshold: 1
timeoutSeconds: 1
livenessProbe:
httpGet: { path: /live, port: 8080 }
initialDelaySeconds: 5
periodSeconds: 5
lifecycle:
preStop:
exec:
# даём LB время «снять» под, затем приложение сна закрывает коннекты
command: ["/bin/sh", "-c", "sleep 5"]
А для защиты от одновременных остановок:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: checkout-pdb
spec:
minAvailable: 3
selector:
matchLabels: { app: checkout }
Правило трёх таймеров:
Что настроить в LB:
Пример для AWS ALB/NLB: установите target group deregistration_delay_seconds=10–15. Для GCP — connection draining timeout. Для Nginx как reverse‑proxy — используйте graceful reload и разумные таймауты:
http {
keepalive_timeout 90s;
send_timeout 30s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
# Грациозный reload без сброса коннектов:
# nginx -s reload
Релизы часто ломают не HTTP, а воркеры.
Принципы:
Эскиз для Kafka на Go с sarama (идея, не привязана к конкретной либе):
// При SIGTERM: закрыть consumer, дождаться onPartitionsRevoked, закоммитить оффсеты и выйти.
// Обработчик должен быть идемпотентен: возможно повторное сообщение после рестарта.
Для SQS/Redis‑очередей:
Cron‑задачи:
Чек‑лист:
Типичные ошибки:
Спринт 1:
Спринт 2:
Итог: релизы перестают быть «событием», а становятся рутиной — без 502, без потерянных заказов и без ночных дежурств.