{ "index": 34, "slug": "editorial-2027-01-field-debugging-decade", "title": "502 без догадок: как восстановить цепочку запроса по логам", "excerpt": "Практический разбор 502 по access и application log: как установить границу отказа, проверить корреляцию по request ID и не объявить приложение причиной без подтверждения.", "contentHtml": "
Клиент получает 502, а в журнале приложения не находится запись с тем же запросом. Самый дорогой ответ в этой ситуации — открыть последний релиз и начать исправлять код. 502 мог сформировать proxy после таймаута, ошибки соединения или недействительного ответа upstream. Могла потеряться и сама запись: sampling, collector или неверное поле корреляции оставляют ту же картину.
\nЦена неверной атрибуции измеряется не только часами. Команда откатывает исправный релиз, повышает таймаут без проверки границы или добавляет повторные запросы к уже перегруженной зависимости. Следующий дежурный получает уверенную формулировку «упало приложение» и повторяет тот же маршрут расследования.
\nНадёжный разбор начинается с вопроса «кто сформировал этот статус?». Сначала фиксируем событие на границе, затем связываем его с попыткой в приложении по идентификатору. Только найденное и согласованное событие разрешает перейти к зависимости. Если запись не найдена, это результат наблюдения, а не доказательство того, что запрос не дошёл.
\nRFC 9110 определяет 502 как статус, который сервер в роли gateway или proxy возвращает после недействительного ответа от входного сервера, к которому он обращался для выполнения запроса. В этой формулировке нет имени конкретного виноватого сервиса. Статус сообщает о проблеме на участке между посредником и upstream либо о том, что посредник не смог принять его ответ.
\nОтделяйте 502 от 504. 502 говорит о недействительном ответе, а 504 — об отсутствии своевременного ответа от upstream или другой вышестоящей системы. На практике конкретный proxy может использовать собственные детали диагностики и маппинг ошибок, поэтому RFC объясняет семантику статуса, но не заменяет документацию вашего узла.
\nВ access-событии зафиксируйте субъект результата: edge.status=502, edge.name и upstream.name, если последний известен. Добавьте route, method, точное время, duration_ms, request_id, trace_id и attempt. Набор полей не универсален, но без него внешний лог показывает симптом и почти не помогает выбрать следующий запрос.
Для одной попытки нужны как минимум access event на границе и application event в сервисе. Ищите по точному request_id или по корректному trace context. Маршрут и временное окно — вторичные признаки: два одинаковых запроса могут прийти одновременно, а часы сервисов могут иметь небольшой сдвиг.
W3C Trace Context задаёт формат заголовка traceparent для передачи идентификаторов между HTTP-границами. Это полезный транспорт связи, но не обещание полной записи: sampled-флаг не гарантирует, что трасса будет сохранена, а промежуточный узел может создать новый контекст при невалидном входе. Поэтому проверяйте и сам заголовок, и фактическое событие в каждом важном слое.
Корреляция считается подтверждённой, если совпали не только ID, но и операция: маршрут, время, номер попытки и ожидаемый слой. Одного одинакового ID мало. При retry ищите дочерние span или отдельные значения attempt; иначе можно принять ответ первой попытки за результат второй.
| Что найдено | Что это подтверждает | Что ещё не доказано | Следующий запрос |
|---|---|---|---|
| Edge 502; application event отсутствует | На границе зафиксирован 502 | Неизвестно, дошёл ли запрос до приложения | Проверить timeout, маршрут, collector и формат ID |
| Edge 502; application 500 с тем же ID и attempt | Приложение обработало эту попытку с ошибкой | Неизвестна первопричина внутри приложения или зависимости | Сверить dependency event, длительность и лимиты |
| Edge 502; application 200 с тем же ID | Приложение завершило свою операцию успешно | Не объяснено расхождение с клиентским статусом | Проверить retry, cache и mapping ответа на proxy |
| В edge нет request ID | Схема наблюдения неполна | Нельзя надёжно связать слои по времени | Исправить генерацию и передачу ID до следующего разбора |
У таблицы есть важная асимметрия. Строка «application event отсутствует» допускает несколько причин: запрос мог остановиться до приложения, запись могла не попасть в хранилище, а идентификатор мог измениться по дороге. Поэтому формулировка должна оставаться отрицательной: «событие не найдено в проверенном источнике и окне», а не «приложение не получило запрос».
\nНиже — самостоятельный пример на JavaScript. События вымышлены и нужны только для проверки корреляции. Функция не пытается угадать первопричину: она различает наличие согласованного application event и оставляет отдельный статус для отсутствующей записи.
\nconst edgeEvents = [\n { requestId: 'r-1', attempt: 1, status: 502, durationMs: 3000 },\n { requestId: 'r-2', attempt: 1, status: 502, durationMs: 420 },\n];\n\nconst applicationEvents = [\n { requestId: 'r-2', attempt: 1, status: 500, durationMs: 180, error: 'dependency unavailable' },\n];\n\nfunction classify(edgeEvent, appEvents) {\n const match = appEvents.find((event) =>\n event.requestId === edgeEvent.requestId\n && event.attempt === edgeEvent.attempt\n );\n\n if (!match) return 'application-event-not-found';\n if (match.status >= 500) return 'application-error-observed';\n return 'application-success-edge-failure-needs-check';\n}\n\nconsole.log(edgeEvents.map((event) => ({\n requestId: event.requestId,\n result: classify(event, applicationEvents),\n})));\n// r-1: application-event-not-found\n// r-2: application-error-observed\nДля r-1 результат ограничен: в переданном массиве нет совпадения по двум полям. Он не различает timeout, потерю записи и ошибку маршрутизации. Для r-2 наблюдается application 500 с теми же ID и попыткой. Это подтверждает обработку запроса приложением, но не доказывает, что строка dependency unavailable — первопричина; её надо сопоставить с журналом зависимости.
В рабочей системе добавьте проверку схемы до классификации: ID не должен быть пустым, attempt — неотрицательным целым, а время — разбираться однозначно. Храните число найденных событий и источник поиска. Не помещайте в общий лог токены, тело формы, email или сырые заголовки; для закрытой корреляции используйте разрешённый идентификатор и действующие правила хранения.
Метод требует хотя бы одного надёжного события на границе. Если access log сам неполон, расследование начинается с восстановления его схемы, а не с чтения application stack trace. Sampling, буферизация и задержка доставки могут удалить или переставить события. Близкое время не заменяет ID, а найденный ID не гарантирует полноту цепочки.
\nRetry меняет картину. Прокси может повторить запрос, приложение — создать новый span, а пользователь — отправить его ещё раз. Один request ID иногда живёт дольше одной попытки, иногда меняется на границе. Нужны attempt, span ID и правила, по которым именно ваша система связывает повторы.
Структурированный лог повышает разбираемость, но не делает данные истинными автоматически. RFC 5424 описывает structured data как parseable-формат и допускает, что collector проигнорирует некорректный элемент. Это означает практическую границу: схему полей надо тестировать на реальном транспорте, а не только на примере конфигурации.
\nРазбор одной карточки не заменяет анализ нагрузки. Если 502 появляется только при насыщении пула, нужны распределение задержек, число retry, состояние очередей и лимиты соединений. Если проблема связана с TLS, DNS, HTTP/2 или конкретным форматом ответа, потребуется проверка соответствующего протокола. Приведённый алгоритм выбирает границу следующего теста, но не обещает одну причину для всех 502.
\nРасследование можно закрывать, когда для повторённой попытки показаны access event и application event либо явно зафиксирован разрыв наблюдения; совпадают маршрут, время и attempt; зависимость проверена там, где это разрешает цепочка; назван следующий измеримый результат. Исправление подтверждено только после повторного запроса: ожидаемый статус получен, цепочка снова связывается по ID, а отрицательный путь остаётся различимым.
\n