У високонавантажених виробничих системах перезапуск застосунків без деградації доступності (Zero-Downtime Deployment) є фундаментальною вимогою Site Reliability Engineering (SRE). Розгортання нової версії SSR-застосунку на базі Next.js 15 в оркестраторах контейнерів (Kubernetes, Docker Swarm) супроводжується ризиками розриву активних клієнтських TCP-з'єднань, появи помилок HTTP 502 Bad Gateway та пошкодження транзакційного стану під час обробки.
Коли оркестратор ініціює зупинку контейнера, операційна система надсилає процесу сигнал SIGTERM. Якщо застосунок завершує роботу негайно, усі клієнтські HTTP-запити, що перебувають у процесі виконання (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 ]
(Активний) (Активний) (Canary Валідація)
│ │ │
└───────┬───────────┘ │
▼ ▼
[ База даних: Схема Expand-Contract (Зворотна сумісність) ]maxUnavailable: 0 та прогріву кешів SSR.X-Feature-Group для оцінки нового механізму оформлення замовлення на 5% клієнтів до початку масштабного оновлення.Створення відмовостійкої системи оновлення Next.js 15 у продакшені потребує координації трьох компонентів: перехоплення сигналу SIGTERM на рівні середовища Node.js, конфігурації життєвого циклу контейнера в маніфестах Kubernetes та правил зваженої маршрутизації в L7 Ingress.
У режимі output: 'standalone' Next.js збирає компактний сервер 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] Сервер готовий до роботи на http://${hostname}:${port}`);
});
async function handleGracefulShutdown(signal: string) {
if (isShuttingDown) return;
isShuttingDown = true;
console.log(`[Next.js Runtime] Отримано сигнал ${signal}. Запуск процедури плавного вимкнення...`);
// 1. Припиняємо прийом нових вхідних TCP-з'єднань
server.close((err) => {
if (err) {
console.error('[Next.js Runtime] Помилка під час закриття HTTP-сервера:', err);
process.exit(1);
}
console.log('[Next.js Runtime] Усі активні HTTP-з\'єднання успішно закрито.');
process.exit(0);
});
// 2. Встановлюємо жорсткий дедлайн на доопрацювання активних запитів (10 секунд)
setTimeout(() => {
console.warn('[Next.js Runtime] Примусова зупинка: час очікування обробки з\'єднань вичерпано.');
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, проходження перевірок безпеки та завантаження до реєстру.
Запуск ізольованого деплойменту v2 з мінімальною кількістю реплік. Успішне проходження startupProbe та readinessProbe.
Ingress-контролер спрямовує 10% вхідного трафіку на Canary Service за алгоритмом weighted round-robin.
Моніторинг затримок p99 та співвідношення помилок HTTP 5xx. Якщо показники в нормі, частка трафіку збільшується до 100%.
Оновлення стабільного деплойменту до версії v2. Видалення canary Ingress. Плавне завершення старих подів через preStop.
Нижче наведено повний продакшен-маніфест Kubernetes із налаштуванням безперебійного rolling update, затримки preStop, глибинних зондів стану та зваженого Ingress для канареєчного релізу.
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:
# Пауза дає час kube-proxy та Ingress видалити IP-адресу з пулів до сигналу SIGTERM
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
# Перезапускає завислий Event Loop або заблокований процес
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За замовчуванням Kubernetes одночасно надсилає контейнеру сигнал SIGTERM та починає видаляти його IP-адресу з EndpointSlice. Оскільки оновлення таблиць ядра (iptables/IPVS) на всіх вузлах займає 1–3 секунди, Ingress-контролери продовжують маршрутизувати нові запити на под, що завершує роботу, спричиняючи шквал помилок HTTP 502 Bad Gateway. Обов'язково декларуйте preStop: exec: command: ["/bin/sh", "-c", "sleep 15"] у специфікації контейнера.
Значення maxUnavailable: 1 або maxUnavailable: 25% у кластері з двома подами зменшує робочу ємність на 50% під час оновлення. Під піковим навантаженням єдиний екземпляр, що залишився, перевантажується або аварійно завершується через OOMKilled, спричиняючи повну відмову системи. Для критичних сервісів конфігуруйте maxUnavailable: 0 та використовуйте maxSurge.
Видалення або зміна типу стовпця в PostgreSQL до релізу нового коду негайно виводить з ладу стабільну версію (v1), яка продовжує надсилати запити до видаленого стовпця. У контурах Blue-Green та Canary міграції бази даних повинні передувати релізу: спершу додаються нові поля (Expand), розгортається код із підтримкою обох версій схеми, і лише після вимкнення старих подів застарілі поля видаляються (Contract).
Якщо Next.js під час запуску компілює чанки сторінок або прогріває кеш пам'яті впродовж 15–20 секунд, стандартна сувора перевірка livenessProbe (наприклад, поріг помилок 3 з інтервалом 5 секунд) примусово перезапустить контейнер до завершення ініціалізації. Це спричиняє нескінченний цикл CrashLoopBackOff. Надайте достатній час на старт за допомогою startupProbe.
Застосування маніфесту canary-deployment з новою версією образу; перевірка успішного проходження startupProbe.
Успішна відповідь readinessProbe підключає IP-адреси canary-подів до виділеного Service.
Застосування анотацій canary-weight для перенаправлення десятої частини вхідного трафіку.
Моніторинг метрик Prometheus щодо появи помилок 5xx, тайм-аутів та зростання затримок p99.
Оновлення стабільного деплойменту до версії v2, видалення canary Ingress та плавна зупинка старих подів.