В высоконагруженных веб-сервисах перезапуск приложений без деградации доступности (Zero-Downtime Deployment) является базовым требованием надежности (SRE). Развертывание новой версии SSR-приложения на базе Next.js 15 в оркестраторах контейнеров (Kubernetes, Docker Swarm) сопряжено с рисками разрыва активных сетевых соединений, генерации ошибок 502 Bad Gateway и повреждения сессионного контекста.
При стандартной остановке контейнера операционная система посылает процессу сигнал SIGTERM. Если приложение завершает выполнение немедленно, все запросы, находящиеся в процессе обработки (In-Flight Requests), аварийно прерываются. В распределенной сети оркестратора ситуация усугубляется асинхронностью обновления таблиц маршрутизации:
Terminating и одновременно инициирует удаление его IP-адреса из ресурса EndpointSlice.kube-proxy на каждом узле кластера с задержкой в 1–3 секунды обновляет правила iptables или IPVS.502 Bad Gateway или ECONNREFUSED.Для устранения этих сбоев архитектура доставки кода опирается на три фундаментальные стратегии:
maxSurge, maxUnavailable) и синхронизацией готовности через зонды readinessProbe и хуки preStop.Жизненный цикл сетевого пакета при смене поколений подов и весовой маршрутизации Canary иллюстрируется следующей схемой:
[ Клиентский HTTP-трафик ]
│
▼
[ Ingress Controller (Nginx / Envoy) ]
│ │
(90% Трафика) (10% Трафика)
│ │
▼ ▼
[ Service: production-v1 ] [ Service: production-v2-canary ]
│ │
┌─────────┴─────────┐ │
▼ ▼ ▼
[ Pod v1 ] [ Pod v1 ] [ Pod v2 ]
(Active) (Active) (Canary Validation)
│ │ │
└───────┬───────────┘ │
▼ ▼
[ Database: Schema Contract v1/v2 (Backward Compatible) ]Деплой манифеста canary-deployment с новой версией контейнера и прохождением startupProbe
Успешный ответ readinessProbe подключает IP-адреса canary к изолированному сервису
Применение аннотаций canary-weight для перенаправления 1/10 входящего трафика
Автоматический опрос Prometheus на предмет ошибок 5xx, тайм-аутов и деградации задержек p99
Обновление версии образа в stable deployment до canary-версии и демонтаж canary ingress
Почему при остановке Pod в Kubernetes клиенты могут получать ошибки 502 Bad Gateway даже при корректной обработке приложением сигнала SIGTERM?
maxUnavailable: 0 и предварительному прогреву кэшей SSR.X-Feature-Group для верификации нового движка рекомендаций на 5% аудитории перед глобальным релизом.Для реализации отказоустойчивой доставки Next.js 15 в production-окружение необходимо скоординировать три слоя: программный перехват SIGTERM в среде Node.js, декларативную конфигурацию жизненного цикла пода в манифестах Kubernetes и правила весовой маршрутизации L7 Ingress.
В режиме output: 'standalone' Next.js генерирует минималистичный HTTP-сервер Node.js. Сервер обязан перехватывать сигналы операционной системы, прекращать прием новых TCP-соединений и дожидаться завершения активных запросов в пределах установленного тайм-аута:
import http from 'node:http';
import next from 'next';
const dev = process.env.NODE_ENV !== 'production';
const hostname = '0.0.0.0';
const port = parseInt(process.env.PORT || '3000', 10);
const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const server = http.createServer((req, res) => {
handle(req, res);
});
// Отслеживание открытых TCP-сокетов для форсированного закрытия при превышении лимита
const connections = new Set<import('node:net').Socket>();
server.on('connection', (socket) => {
connections.add(socket);
socket.once('close', () => connections.delete(socket));
});
let isShuttingDown = false;
// Liveness & Readiness эндпоинт проверки жизнеспособности
server.on('request', (req, res) => {
if (req.url === '/api/healthz') {
if (isShuttingDown) {
res.writeHead(503, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'terminating' }));
return;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', uptime: process.uptime() }));
return;
}
});
server.listen(port, () => {
console.log(`[Next.js Runtime] Server ready on http://${hostname}:${port}`);
});
async function handleGracefulShutdown(signal: string) {
if (isShuttingDown) return;
isShuttingDown = true;
console.log(`[Next.js Runtime] Received ${signal}. Starting graceful shutdown sequence...`);
// 1. Прекращаем прием новых клиентских соединений
server.close((err) => {
if (err) {
console.error('[Next.js Runtime] Error during HTTP server close:', err);
process.exit(1);
}
console.log('[Next.js Runtime] Closed all active HTTP connections successfully.');
process.exit(0);
});
// 2. Устанавливаем жесткий дедлайн на слив inflight-запросов (10 секунд)
setTimeout(() => {
console.warn('[Next.js Runtime] Forcing shutdown: active connections exceeded timeout limit.');
for (const socket of connections) {
socket.destroy();
}
process.exit(1);
}, 10000).unref();
}
process.on('SIGTERM', () => handleGracefulShutdown('SIGTERM'));
process.on('SIGINT', () => handleGracefulShutdown('SIGINT'));
});Формирование неизменяемого Docker-образа с тэгом git commit SHA, прохождение тестов и публикация в GHCR.
Запуск параллельного набора подов v2 с минимальным числом реплик. Срабатывают startupProbe и readinessProbe.
Ingress-контроллер направляет 10% входящих запросов на Canary Service на основе алгоритма weighted round-robin.
Мониторинг квантилей задержек (p99 latency) и соотношения HTTP 5xx к 2xx. При отсутствии аномалий вес увеличивается до 100%.
Основной Deployment v1 обновляется до v2. Canary Ingress удаляется. Старые поды плавно завершают работу через preStop.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nextjs-web-stable
namespace: production
labels:
app: nextjs-web
track: stable
spec:
replicas: 4
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
# Запрещаем уменьшать число работающих подов ниже 100% от replicas
maxUnavailable: 0
# Запускаем не более 1 дополнительного пода сверх нормы во время ротации
maxSurge: 1
selector:
matchLabels:
app: nextjs-web
track: stable
template:
metadata:
labels:
app: nextjs-web
track: stable
spec:
terminationGracePeriodSeconds: 30
containers:
- name: nextjs-app
image: ghcr.io/enterprise/nextjs-web:v1.24.0
imagePullPolicy: IfNotPresent
lifecycle:
preStop:
exec:
# Пауза перед SIGTERM дает kube-proxy и Ingress время удалить IP из upstream
command: ["/bin/sh", "-c", "sleep 15"]
ports:
- containerPort: 3000
name: http
env:
- name: NODE_ENV
value: "production"
- name: PORT
value: "3000"
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "1536Mi"
# Защита от преждевременного убийства пода во время долгой компиляции/гидратации
startupProbe:
httpGet:
path: /api/healthz
port: 3000
initialDelaySeconds: 2
periodSeconds: 3
failureThreshold: 20
# Подключение в балансировку только после полной готовности к обработке трафика
readinessProbe:
httpGet:
path: /api/healthz
port: 3000
periodSeconds: 5
successThreshold: 1
failureThreshold: 2
# Перезапуск зависшего процесса
livenessProbe:
httpGet:
path: /api/healthz
port: 3000
periodSeconds: 10
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: nextjs-service-stable
namespace: production
spec:
type: ClusterIP
selector:
app: nextjs-web
track: stable
ports:
- name: http
port: 80
targetPort: 3000
---
# Canary Deployment: развертывание следующей версии v1.25.0 для 10% трафика
apiVersion: apps/v1
kind: Deployment
metadata:
name: nextjs-web-canary
namespace: production
labels:
app: nextjs-web
track: canary
spec:
replicas: 2
selector:
matchLabels:
app: nextjs-web
track: canary
template:
metadata:
labels:
app: nextjs-web
track: canary
spec:
terminationGracePeriodSeconds: 30
containers:
- name: nextjs-app
image: ghcr.io/enterprise/nextjs-web:v1.25.0
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
ports:
- containerPort: 3000
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "1536Mi"
startupProbe:
httpGet:
path: /api/healthz
port: 3000
failureThreshold: 20
periodSeconds: 3
readinessProbe:
httpGet:
path: /api/healthz
port: 3000
periodSeconds: 5
livenessProbe:
httpGet:
path: /api/healthz
port: 3000
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: nextjs-service-canary
namespace: production
spec:
type: ClusterIP
selector:
app: nextjs-web
track: canary
ports:
- name: http
port: 80
targetPort: 3000
---
# Основной стабильный Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nextjs-ingress-stable
namespace: production
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
rules:
- host: app.enterprise.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nextjs-service-stable
port:
number: 80
---
# Canary Ingress: перехват 10% весового трафика на версию v1.25.0
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nextjs-ingress-canary
namespace: production
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
# Дополнительная возможность принудительной маршрутизации для QA по заголовку
nginx.ingress.kubernetes.io/canary-by-header: "X-Canary-Internal"
spec:
rules:
- host: app.enterprise.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nextjs-service-canary
port:
number: 80Какая комбинация настроек rollingUpdate и схемы базы данных гарантирует нулевой даунтайм при высоких пиковых нагрузках?
По умолчанию при удалении пода оркестратор одновременно шлет сигнал SIGTERM в контейнер и запускает процесс вывода IP-адреса из EndpointSlice. Из-за сетевых задержек обновления iptables на воркер-нодах (1-3 секунды) L7-балансировщик продолжает пересылать пользовательские запросы на завершающийся под, генерируя лавину ошибок 502 Bad Gateway. Всегда добавляйте preStop: exec: command: ["/bin/sh", "-c", "sleep 15"] в конфигурацию контейнера.
Если в конфигурации rollingUpdate указан параметр maxUnavailable: 25% или maxUnavailable: 1 при общем пуле всего из двух подов, емкость кластера мгновенно падает на 50%. В моменты пиковой нагрузки оставшийся под получает двойной объем запросов, исчерпывает лимиты памяти (OOMKilled) и вызывает каскадный отказ всего сервиса. Для критичных систем устанавливайте maxUnavailable: 0.
Удаление или переименование столбца в таблице PostgreSQL до деплоя новой версии кода приводит к немедленному падению работающей стабильной версии v1, которая продолжает читать старый столбец. В Blue-Green и Canary архитектурах база данных всегда должна опережать код: сначала добавляется новое поле (Expand), затем выкатывается код с поддержкой обоих полей, и лишь после полного завершения ротации старый столбец удаляется отдельным релизом (Contract).
Если приложение Next.js компилирует статические чанки или инициализирует кэш в памяти в течение 15–20 секунд при старте, стандартная агрессивная проверка livenessProbe (с порогом failureThreshold: 3 и интервалом 5 секунд) убьет контейнер до того, как он успеет запуститься. Возникает бесконечный цикл CrashLoopBackOff. Выделяйте длительный бюджет запуска через специализированный startupProbe.