Выберите продукт

Nginx error_page как защитный барьер: безопасные фолбэки и circuit breaker на уровне прокси

Error_page в nginx — это не только «красивая 50x-страница». При правильной настройке она становится барьером: перехватывает 5xx и тайм-ауты, отдаёт безопасные фолбэки, ограничивает ретраи и включает простой circuit breaker на уровне прокси.
Nginx error_page как защитный барьер: безопасные фолбэки и circuit breaker на уровне прокси

В продакшене нам нужен nginx не просто как обратный прокси, а как защитный барьер, который удержит фронт при сбоях апстримов. Ключ к этому — грамотная работа с error_page: безопасные фолбэки (fallback), единая политика на 5xx/тайм-ауты и даже примитивный circuit breaker на уровне прокси без внешних компонентов. Разберёмся, как связать error_page с proxy_intercept_errors, proxy_next_upstream, тайм-аутами и кешем, чтобы повысить устойчивость и предсказуемость отказов.

Зачем перехватывать ошибки через error_page

По умолчанию nginx просто прокидывает статус апстрима. Это честно, но опасно для UX и безопасности: стек-трейсы приложения попадают пользователю, «шумные» коды меняются от запроса к запросу, а страницы бэкапа выглядят по-разному в зависимости от того, где упало. Используя error_page, мы:

  • Создаём единый контролируемый ответ при сбоях: статус, заголовки, текст или JSON.
  • Избегаем утечек внутренних деталей приложения и стеков.
  • Можем возвращать безопасный контент (статический HTML или предсобранный JSON) без обращения к падающему апстриму.
  • Фиксируем договорённости с клиентами: 503 с Retry-After при техработах, 200 с «degraded» для tolerant-клиентов и т. д.

Принцип: прокси должен деградировать предсказуемо. Пользователь должен видеть стабильный ответ, а не рулетку из 502/504/500 и случайных тел.

Как работает связка proxy_intercept_errors + error_page

Перехват кодов, пришедших от апстрима, включается директивой proxy_intercept_errors on;. Далее правило error_page перенаправляет обработку в заранее подготовленную «безопасную» локацию.

location / {
    proxy_pass http://app;
    proxy_intercept_errors on;
    error_page 500 502 503 504 = @fallback;
}

location @fallback {
    internal;
    add_header Cache-Control "no-store" always;
    add_header X-Proxy-Mode "fallback" always;
    return 503 "Service temporarily unavailable. Please try again later.";
}

Ключевые нюансы:

  • internal; запрещает прямой вызов локации извне.
  • return или отдача статического файла исключают обращение к аппликации.
  • Если нужно сохранить код ответа — используйте форму =503 @fallback в error_page.
  • Во избежание зацикливаний оставьте recursive_error_pages off; (по умолчанию).

Базовые фолбэки: HTML и API

HTML-сайт

Для публичного сайта часто достаточно статической страницы «Технические работы» плюс правильные заголовки. Положите подготовленную страницу на диск и переиспользуйте её во всех серверных блоках.

location @fallback_html {
    internal;
    root /var/www/fallbacks;
    add_header Cache-Control "no-store" always;
    add_header Retry-After "120" always;
    try_files /maintenance.html =503;
}

Такой подход гарантирует единообразие и минимальную латентность при сбоях.

API-сценарий

Клиенты API зачастую готовы к «degraded mode». Мы можем вернуть 200 с простым JSON, чтобы не ломать обработку на стороне клиента, но с явным признаком деградации.

location /api/ {
    proxy_pass http://api_upstream;
    proxy_intercept_errors on;
    error_page 502 504 = @api_fallback;
}

location @api_fallback {
    internal;
    default_type application/json;
    add_header X-Proxy-Mode "degraded" always;
    return 200 '{"status":"degraded","message":"temporary upstream failure"}';
}

Если же важно сохранять семантику отказа для ретраев — верните 503 с Retry-After.

Circuit breaker на уровне прокси

В терминологии сетевых паттернов circuit breaker — это отсечка «плохого» апстрима и быстрый ответ без попыток бесконечно стучаться в неисправный сервис. В nginx мы строим этот паттерн из нескольких кирпичиков:

  • Границы времени: короткие, осмысленные тайм-ауты на соединение и чтение (proxy_connect_timeout, proxy_read_timeout, proxy_send_timeout).
  • Политика ретраев: proxy_next_upstream с ограничениями proxy_next_upstream_tries и proxy_next_upstream_timeout.
  • Маркировка и исключение «битых» бэкендов: max_fails, fail_timeout, опционально slow_start и общий zone для шаринга состояния.
  • Фолбэк через error_page, если все попытки исчерпаны.
upstream app {
    zone app_zone 64k;
    least_conn;

    server 10.0.0.11:8080 max_fails=3 fail_timeout=10s;
    server 10.0.0.12:8080 max_fails=3 fail_timeout=10s;
    server 10.0.0.13:8080 backup;
}

server {
    # тайм-ауты и границы
    proxy_connect_timeout 1s;
    proxy_read_timeout 2s;
    proxy_send_timeout 2s;

    location / {
        proxy_pass http://app;
        proxy_intercept_errors on;
        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
        proxy_next_upstream_tries 2;
        proxy_next_upstream_timeout 3s;
        error_page 500 502 503 504 = @fallback_html;
    }
}

Здесь мы не включаем параметр non_idempotent — это значит, что POST/PATCH/DELETE по умолчанию не будут ретраиться на другой бэкенд, что безопаснее для операций с побочными эффектами. Для GET/HEAD ретраи разрешены. Когда попытки исчерпаны, включается error_page и отдаётся локальный фолбэк.

Плавное восстановление

Если вы возвращаете узел после падения, полезен slow_start в апстриме — он постепенно наращивает вес сервера, чтобы не обрушить его лавиной трафика сразу после восстановления.

upstream app {
    zone app_zone 64k;
    least_conn;
    server 10.0.0.11:8080 max_fails=3 fail_timeout=10s slow_start=10s;
    server 10.0.0.12:8080 max_fails=3 fail_timeout=10s slow_start=10s;
}

Старый контент как fallback: use_stale

Фолбэк не всегда должен быть «плоским». Если вы используете кеш на прокси, можно вернуть «старый, но годный» ответ при ошибке апстрима. Это хороший компромисс для контента, который допустимо показывать с задержкой.

proxy_cache mycache;
proxy_cache_valid 200 301 302 10m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;

Схема: при сбое апстрима nginx отдаёт копию из кеша, а в фоне может пытаться обновить её. В связке с error_page это даёт многоступенчатый план: сначала «stale», затем статический фолбэк, если кеша нет.

Диаграмма потока запросов и failover в Nginx при ошибках апстрима

Maintenance без хаоса

Плановые работы — частный случай управляемой деградации. На входе: 503 всем, плюс Retry-After. Удобно сделать «переключатель» через отдельный конфиг или флаг-файл, чтобы не трогать основную логику.

# include /etc/nginx/flags/maintenance.conf;
# внутри файла может быть: set $maintenance 0; или set $maintenance 1;

map $maintenance $maintenance_on {
    default 0;
    1 1;
}

server {
    if ($maintenance_on) {
        return 503;
    }

    error_page 503 = @maintenance;

    location @maintenance {
        internal;
        add_header Retry-After "120" always;
        root /var/www/fallbacks;
        try_files /maintenance.html =503;
    }
}

Так мы не ломаем апстрим, а управляем статусом на уровне прокси. В журналах это будет видно как 503 с нулевым временем апстрима. Для бэкенда БД на время техработ пригодится материал перевод MySQL в read-only на время работ.

Гигиена заголовков и отсутствие утечек

Фолбэк — это ещё и точка санитизации ответов: не допускайте вытекания заголовков апстрима и метаданных окружения. В фолбэк-локациях задавайте нужные типы (default_type), добавляйте контролируемые Cache-Control/Retry-After и избегайте переноса случайных заголовков с апстрима.

Не перехватывайте 4xx (например, 404) у приложений, если у них есть собственная логика. Для API это особенно важно, чтобы не ломать контракт.

Логирование: что и как смотреть

Для разбора «почему сработал фолбэк» пригодится расширенный формат логов с полями апстрима.

log_format upstream_ext '$remote_addr - $request $status '
                      'up=$upstream_addr us=$upstream_status '
                      'urt=$upstream_response_time uct=$upstream_connect_time '
                      'rt=$request_time mode=$sent_http_x_proxy_mode';
access_log /var/log/nginx/access.log upstream_ext;

Проверяйте, что upstream_status и upstream_response_time осмысленны. Если mode=degraded или mode=fallback, значит, отработал наш барьер. upstream_connect_time поможет отличить сетевые проблемы от неповоротливого приложения.

Таймлайн выдачи устаревшего кеша use_stale в Nginx при сбое апстрима

Тестирование отказов

Перед выкатыванием проверьте локально ключевые сценарии: тайм-аут соединения, медленный ответ, 502/504, исчерпание ретраев.

# ограничьте общее время запроса клиента
curl -I --max-time 2 https://example.test/

# симулируйте медленный апстрим (например, через tc/netem) и смотрите, как срабатывает fallback
# отключите один бэкенд и проверяйте трассировку в логах по $upstream_addr

Если нужен изолированный стенд для проверки сценариев деградации и кеширования, поднимите его на VDS, чтобы не рисковать боевой инфраструктурой.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Ограничение каскадов и защита от штормов

Без ограничений ретраи и длинные тайм-ауты превращают отказ апстрима в лавинообразную деградацию. Держите параметры консервативными: небольшие тайм-ауты, ограничение количества и общего времени ретраев. На горячем трафике включайте keepalive в апстриме и разумные буферы, чтобы не тратить время на каждый коннект. В точке фолбэка выбирайте лёгкую отдачу (короткий текст или небольшой HTML).

Чек-лист безопасного fallback и circuit breaker

  • Включить proxy_intercept_errors on; там, где нужна политика на 5xx/тайм-ауты.
  • Определить error_page для 500 502 503 504 и указывать именованную локацию с internal.
  • На уровне апстрима задать zone, max_fails, fail_timeout, опционально slow_start.
  • Для ретраев: proxy_next_upstream, proxy_next_upstream_tries, proxy_next_upstream_timeout. Не добавлять non_idempotent, если не понимаете риски.
  • Тайм-ауты: короткие proxy_connect_timeout, proxy_read_timeout, proxy_send_timeout в зависимости от SLA.
  • Опционально кеш и proxy_cache_use_stale для «старого, но годного» ответа.
  • Фолбэк для HTML и API держать раздельно (тип ответа, заголовки, тела).
  • Логи с полями $upstream_status, $upstream_response_time, $upstream_addr и признаком режима.
  • Автоматический maintenance-режим: 503 + Retry-After через флаг/инклюд.

Типичные ошибки и подводные камни

  • Забыли proxy_intercept_errors: error_page не сработает для проксированных ответов.
  • Перехватывают 404/401 у приложения: ломает контракт API и UX веб-приложений.
  • Возврат больших фолбэк-страниц: увеличивает латентность и расход CPU/памяти при массовых отказах.
  • Слишком щедрые тайм-ауты и бесконечные ретраи: усиливает шторм; ставьте верхние границы.
  • Прямой доступ к фолбэк-локации: отсутствие internal позволяет дергать её извне.
  • Зацикливание error_page: явно задавайте код через =503 и держите recursive_error_pages off.
  • Случайное кеширование ошибок: ставьте Cache-Control: no-store на фолбэк-ответы, если не хотите их кешировать.

Итог

Одной директивой error_page проблему отказоустойчивости не решить, но в сочетании с proxy_intercept_errors, грамотными тайм-аутами, политикой ретраев и опциональным кешем она превращается в мощный защитный барьер. Вы получаете контролируемые ответы, предсказуемую деградацию и простейший circuit breaker на уровне nginx без дополнительного софта. Начните с базового фолбэка на 5xx/тайм-ауты, разделите HTML и API-сценарии, добавьте «stale» для контента и доведите параметры так, чтобы при падении апстрима пользователи видели стабильный и безопасный ответ.

Поделиться статьей

Вам будет интересно

Debian/Ubuntu: duplicate address detected, DAD failed IPv6 — причины и исправление OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: duplicate address detected, DAD failed IPv6 — причины и исправление

Сообщения duplicate address detected и DAD failed в Debian/Ubuntu означают, что IPv6-адрес не прошёл проверку уникальности в локал ...
Debian/Ubuntu: как исправить swapoff: Device or resource busy при отключении swap OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как исправить swapoff: Device or resource busy при отключении swap

Ошибка swapoff: Device or resource busy в Debian и Ubuntu обычно связана не с «битым» swap, а с нехваткой RAM, zram, автоподключен ...
Debian/Ubuntu: как исправить cloud-init status: error после first boot на VDS OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как исправить cloud-init status: error после first boot на VDS

Если на Debian или Ubuntu после первого запуска или клонирования VDS вы видите cloud-init status: error, причина обычно в datasour ...