Симптомы proxy часто приходят пачкой: клиент получает 504, приложение пишет HTTP вместо HTTPS, а журнал видит один и тот же адрес для каждого пользователя. Цена поспешного исправления заметна быстро: таймаут увеличивают для всех endpoint-ов, в код добавляют исключение для «настоящей схемы», а access-log остаётся без полей, которые могли бы отделить подключение от ожидания ответа. Через неделю тот же сбой возвращается, но уже с другим status и без возможности сравнить два случая.
Полезный разбор начинается с ограничения. Здесь нет production-инцидента и нет реального access-log: ниже синтетический сценарий с учебными полями. Его задача — научить отличать сигнал от вывода. Reverse proxy формирует отдельный upstream-hop, поэтому один status не говорит, где именно потеряно время или кто изменил заголовок. В апреле 2020 года достаточно собрать путь одного запроса, безопасный формат лога и короткую таблицу «симптом → причина → проверка → действие», не приписывая себе запуск на живом контуре.
Начинаем с одного маршрута и одного вопроса
Выбираю маршрут, на котором нет персональных данных и который допускает повтор: /health, техническая страница или специальный стендовый endpoint. Вопрос тоже один. Например: «получил ли Nginx заголовки ответа upstream до истечения своего read timeout?» Это лучше, чем вопрос «почему всё медленно». Для ответа нужны совпадающие признаки: путь и безопасный токен в входном запросе, итоговый status и время в access-log, плюс запись приложения, если она уже умеет логировать этот токен. Если последней записи нет, это результат наблюдения, а не повод придумать её содержание.
Следующий вопрос — о границе доверия: «кто установил X-Forwarded-Proto и X-Forwarded-For?» Приложение может видеть адрес Nginx совершенно корректно. Проблемой это становится, когда приложение использует этот peer как пользовательский адрес или принимает forwarded-header от прямого внешнего соединения. Nginx realip module требует отметить trusted sources, а RFC 7239 предупреждает, что forwarded-информация может быть изменена на пути. Поэтому сначала рисуется топология, затем определяется доверенный proxy, и только после этого меняется конфигурация или middleware.
| Симптом | Вероятная граница | Минимальная проверка | Ограниченное действие |
|---|---|---|---|
| 504 на одном endpoint | ожидание proxy между операциями чтения upstream | сопоставить proxy_read_timeout, request_time, upstream_header_time и лог приложения на одном токене | исправить задержку upstream или отдельно пересмотреть лимит именно этого endpoint |
| 502 после смены proxy_pass | маршрут до upstream, URI или ответ upstream | прочитать итоговый location/proxy_pass и status upstream без изменения таймаутов | вернуть один неверный маршрут либо исправить конфигурацию; не лечить 502 read timeout |
| Приложение строит http-ссылку | TLS termination и договор о схеме | сверить внешний маршрут, $scheme на Nginx и поле, которое читает фреймворк | зафиксировать единственный доверенный forwarded-сигнал |
| Все пользователи имеют адрес proxy | peer address и realip boundary | выяснить, существует ли прямой доступ к приложению и какие hop-ы доверены | не парсить первый X-Forwarded-For; задать список доверенных источников отдельно |
В таблице нет строки «перезапустить всё». Перезапуск может совпасть с исчезновением эффекта, но не доказать причину. Если конфигурация изменилась одновременно с приложением, сначала возвращают понятный контроль: один location, один upstream, один запрос. Если симптом связан с timeout, нельзя одновременно увеличивать proxy_read_timeout, менять пул соединений и добавлять retry. Иначе следующая запись в журнале не ответит, какая из трёх правок что изменила.
Учебный access-log должен сохранять развилку
Nginx пишет access-log в формате, который задаётся через log_format. Для proxy-разбора хватает небольшой строки: метод и URI, итоговый status, $request_time, выбранный upstream, его status и времена подключения, заголовков и ответа. Значения upstream-полей иногда остаются пустыми; это тоже сигнал о том, что нужно проверить конкретную ветку, а не маскировать пустоту нулём. Формат не должен писать authorization, cookie, тело или реальный IP пользователя: диагностика не оправдывает сбор лишних данных.
# Учебный log_format; путь журнала и значения подставляются в стенде.
log_format proxy_boundary '$request_method $uri status=$status '
'request=$request_time upstream=$upstream_addr '
'upstream_status=$upstream_status '
'connect=$upstream_connect_time '
'header=$upstream_header_time '
'response=$upstream_response_time';
access_log /path/to/proxy-boundary.log proxy_boundary;
После настройки формата удобнее прочитать одну синтетическую строку, чем десятки настоящих. В примере ниже connect маленький, а header отсутствует до 3.001 секунды. Это поддерживает гипотезу, что Nginx быстро начал upstream-hop, но не получил заголовки ответа до лимита. Это не доказывает, что виновата база, GC, внешняя зависимость или сам framework. Следующий шаг — посмотреть конфигурацию и безопасную запись приложения для того же токена, а не объявить причину по одной цифре.
# Синтетический пример для чтения полей; это не access-log реального сервера.
GET /health status=504 request=3.001 upstream=<backend> upstream_status=-
connect=0.001 header=- response=3.001
# Это формирует гипотезу «proxy не получил заголовки ответа до своего лимита»,
# но не называет причину: её проверяют по конфигурации и логу приложения.
Та же дисциплина применима к 502. Status 502 сообщает, что proxy не смог отдать клиенту корректный upstream-ответ в данной конфигурации, но не заменяет сравнение location, proxy_pass, URL и лога upstream. Если рядом нет upstream_status, это может сузить ветку, но не выбирает её автоматически. Правильная запись расследования звучит короче и честнее: «на учебном маршруте proxy не получил заголовки upstream до указанного лимита; следующий эксперимент — сверить endpoint и лог процесса». В ней нет ни выдуманного запуска, ни обещания, что увеличение таймаута решит всё.
Неверная схема и адрес — отдельные ветки
Схема и адрес часто попадают в один «proxy bug», хотя проверяются по-разному. Для схемы важно место TLS termination и заголовок, который приложение реально читает. На внутреннем HTTP-hop-е $scheme может быть http, даже если внешний пользователь пришёл по HTTPS. Для адреса важно, кто является непосредственным peer и кто имеет право заменить его данными из header. set_real_ip_from описывает доверенную сторону; отсутствие такого знания нельзя компенсировать тем, что приложение возьмёт первый элемент X-Forwarded-For.
Если проект использует стандартный Forwarded, правила его сохранения и расширения должны быть описаны вместе с proxy. Если используется X-Forwarded-*, это не делает решение неверным, но добавляет обязанность назвать формат и источник. Нельзя склеивать значения из Via, Forwarded и X-заголовков так, будто они всегда описывают одну упорядоченную цепочку. RFC 7239 прямо отделяет эти форматы и не обещает, что каждый proxy обновляет их одинаково. Для небольшого сервиса практичнее выбрать один договор и проверить его на одном маршруте.
Порядок проверки без широкого перезапуска
- Зафиксировать один симптом, один URL-шаблон без чувствительных данных и один ожидаемый результат; назвать стоимость, если он повторится.
- Отметить на схеме внешний клиентский hop, Nginx и upstream; отдельно указать, где завершается TLS и есть ли proxy перед Nginx.
- Проверить итоговые директивы proxy_pass, proxy_set_header и timeout для конкретного location, а не фрагмент из другого include-файла.
- Собрать безопасный access-log с request_time и upstream-полями, затем выполнить один учебный запрос на изолированном стенде с диагностическим токеном.
- Сопоставить status и время proxy с записью приложения только для этого токена; если записи приложения нет, зафиксировать неизвестное вместо домысла.
- Изменить одну подтверждённую границу, повторить тот же запрос и записать критерий отката. Если результат не изменился, вернуться к следующей строке таблицы, а не расширять действие.
Пример проверки заголовков без реального endpoint
Ниже показана команда с подстановками. Её назначение — сделать будущий учебный запрос повторяемым: оператор заменяет только URL и Host стенда, а не копирует скрытые cookie или рабочую авторизацию. До запуска нужно договориться, какой ответ безопасно проверять и где появится диагностический токен. Команда не была выполнена и не подтверждает доступность какого-либо адреса.
# Учебный запрос; не выполнялся в browser, staging или production.
# <PROXY_URL> и <HOST> — подстановки для изолированного стенда, не реальные адреса.
curl -i --max-time 5 \
'<PROXY_URL>/health' \
-H 'Host: <HOST>' \
-H 'X-Debug-Token: proxy-study-2020-04'
# Сверяем статус, Location, Set-Cookie и заголовок, который приложение
# записывает как схему или request token. Сам curl не доказывает путь до upstream.
После запуска стендового варианта смотрят не только на 200. Для схемы проверяют, что приложение использовало согласованный forwarded-signal, а не peer socket. Для адреса проверяют путь доверия от входного proxy до кода. Для timeout сравнивают нужный интервал с данными proxy и лога приложения. Если обработчик отдаёт streaming response, отдельной проверкой фиксируют интервал между частями: proxy_read_timeout измеряет паузу между чтениями, а не полную длительность передачи. Это ограничение меняет постановку задачи и должно остаться рядом с выбранным значением.
Что остаётся после исправления
Хороший разбор оставляет не «правильный timeout», а повторяемый маршрут: схема hop-ов, список заголовков с источником, минимальный log_format и один безопасный запрос. В следующий раз это позволяет ответить быстрее: проблема в соединении с upstream, в паузе ответа, в контракте схемы или в недоверенном адресе. Так автор развивает ширину от frontend HTTP к delivery boundary, но не притворяется владельцем большой распределённой платформы. Для этого уровня достаточно видеть свою границу и не скрывать неизвестное.
Реальные Nginx, curl, browser, CI, staging и production в этой статье не запускались. Синтетическая строка лога, значения времени, адрес 127.0.0.1, URL-подстановки и путь файла приведены только для обучения; hostnames, секреты, реальные IP и пользовательские данные не публикуются. Перед применением нужен отдельный прогон в согласованном стенде, проверка версии Nginx, владельца конфигурации, доверенных proxy и политики журналирования.
Проверяемые источники
- Nginx: модуль ngx_http_proxy_module — первичная документация директив proxy_pass, proxy_set_header, proxy_connect_timeout, proxy_send_timeout и proxy_read_timeout
- Nginx: модуль ngx_http_realip_module — модуль меняет адрес клиента только по указанному заголовку и только для источников, явно объявленных доверенными через set_real_ip_from
- Nginx: модуль ngx_http_log_module — справочник log_format, access_log и переменной request_time; формат журнала должен быть частью диагностического контракта
- RFC 7239: Forwarded HTTP Extension — стандартный заголовок Forwarded, его связь с X-Forwarded-* и ограничение: данные заголовка нельзя считать достоверными без доверенной цепочки proxy
- RFC 7230: HTTP/1.1 Message Syntax and Routing — историческая для апреля 2020 года спецификация HTTP/1.1: маршрутизация, Host и границы между получателем и отправителем сообщения