Перейти к содержимому
RootPoligon

Посредник между клиентом и приложением

Чтение и практика ~75 мин

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

Лаборатория и подготовка — архив один для всех пяти глав. Перед новым сценарием остановите прежний через Ctrl+C.

Что нового

Между клиентом и приложением часто стоит посредник (в настоящих системах — nginx, HAProxy, балансировщик): клиент соединяется с ним, а он сам идёт к приложению (upstream). Путь теперь такой:

клиент ──▶ посредник ──▶ приложение
   (участок A)   (участок B)

Участок A клиент видит сам; участок B — только посредник и его журнал. Поэтому первый вопрос — кто ответил:

  • Страница ошибки посредника — HTML со своей подписью, заголовок Server называет посредника, а заголовка приложения (X-App-Version) нет. Ответ приложения — JSON, в заголовках — X-App-Version. Так устроено в лаборатории; и в настоящем nginx по умолчанию в ответ клиенту идут заголовки апстрима (кроме Date, Server и нескольких служебных — документация nginx, proxy_hide_header), а Server посредник подставляет свой. Это проверено запуском nginx 1.24.0, но настроенный по-другому посредник может вести себя иначе.
  • Здоровый ответ приложения по его собственному адресу доказывает, что приложение живо. Он не доказывает, что посредник его находит и дожидается: это другой участок.

Что означают 502 и 504. В лаборатории lab-proxy отвечает 502, когда не смог получить ответ апстрима (отказ в соединении, непонятный ответ), и 504, когда не дождался (подключение или чтение). Это правила кода лаборатории. В настоящем nginx 1.24.0 при запуске мы наблюдали то же: апстрим не слушает — 502, апстрим молчит дольше proxy_read_timeout — 504; в журнале nginx это строки connect() failed (111: Connection refused) while connecting to upstream и upstream timed out (110: Connection timed out) while reading response header from upstream. Документация модуля upstream такого соответствия кодов не обещает, другие посредники и версии могут отвечать иначе (например, 503). Код — подсказка; причину называет журнал посредника.

Какое время ждёт посредник. По документации nginx, proxy_read_timeout (по умолчанию 60 с) — это время между двумя последовательными чтениями ответа, а не на весь ответ; proxy_connect_timeout (по умолчанию 60 с) — на установление соединения с апстримом. В лаборатории то же понятие названо read_timeout, и он действует на каждое чтение ответа апстрима.

Инструменты владельца посредника (после того, как вы поняли, что нужен именно он):

python3 lab/netlab.py config                 # конфигурация посредника в формате лаборатории
python3 lab/netlab.py logs --source proxy    # журнал посредника: куда пошёл запрос и почему ответ такой
python3 lab/netlab.py logs --source app      # журнал приложения
python3 lab/netlab.py fetch --timing URL     # показывает время подключения, первого байта и всего запроса

Конфигурация записана по образцу nginx, но это не конфигурация nginx. Если вы будете сравнивать с реальным nginx: каталог lab/ содержит nginx_crosscheck.py — он поднимает временный nginx на 127.0.0.1:18081 и печатает его версию.

Подготовка

Из папки курса, в первом окне: python3 lab/netlab.py start ch4. Остальное — во втором окне.

Ситуация

Суббота, 17 октября, 13:00. Служба «Заметки» стоит за посредником, адрес для пользователей прежний: http://notes.lab.test:18080/api/notes. Пользователи видят «502 Bad Gateway». Дежурный Пётр:

Приложение работает, я проверил его по собственному адресу: curl http://127.0.0.1:19000/health — 200. Значит, это посредник «слишком нервный»: давайте поднимем ему тайм-ауты до тридцати секунд.

Воспроизведите симптом:

python3 lab/netlab.py fetch http://notes.lab.test:18080/api/notes

Мини-задача

Задача: выяснить, кто ответил пользователю и на каком участке запрос остановился, и вернуть работоспособный путь через посредника, не потеряв заметки. Готовой цепочки команд нет: инструменты названы выше, порядок выбираете сами.

До любого наблюдения, кроме повтора симптома, запишите:

  1. Кто ответил пользователю — приложение или посредник; по каким трём признакам в выводе вы это скажете.
  2. Не меньше двух гипотез, почему запрос остановился (одна — слова Петра); для каждой — что подтвердило бы её и что бы исключило.
  3. Одно наблюдение, которое различает гипотезы. Выполните его и запишите вывод, а также то, какие гипотезы оно исключило.

Затем: участок (A или B), причина одним предложением и две строки вывода (клиент и посредник или ss).

Разбор мини-задачи

Откройте, когда записали свои ответы

Кто ответил. Ответ — HTTP/1.1 502 Bad Gateway, Server: lab-proxy, тело — HTML-страница «502 Bad Gateway … lab-proxy», заголовка X-App-Version нет. Все три признака: подпись посредника в заголовке, HTML вместо JSON, отсутствие заголовка приложения. Приложение запроса пользователя не видело.

Что проверил Пётр. /health на 127.0.0.1:19000 — 200: приложение живо и порт слушает. Но это участок после посредника, и по собственному адресу приложения; как посредник идёт к нему, эта проверка не показала.

Два верных пути.

  • Со стороны посредника: logs --source proxy —

    [proxy 127.0.0.1:18080] GET /api/notes -> 127.0.0.1:19001/api/notes · 502 Bad Gateway · connect() failed: Connection refused
    

    Посредник пошёл на 127.0.0.1:19001, и там отказ. config подтверждает: upstream 127.0.0.1:19001. ss -ltn: приложение слушает 127.0.0.1:19000, на 19001 — никого.

  • Снаружи внутрь: сравнить путь через посредника (502, страница посредника) и прямой запрос к приложению (curl http://127.0.0.1:19000/api/notes — 200, X-App-Version: 3.0): приложение исправно, неисправен участок B; затем config и ss.

Что это доказывает. Участок B: посредник ходит на 127.0.0.1:19001, где слушателя нет, а приложение слушает 127.0.0.1:19000.

Слова Петра. Тайм-ауты здесь ни при чём: отказ пришёл мгновенно (0 ms в журнале), и ждать было нечего. Поднять proxy_read_timeout до 30 с можно — 502 останется. Гипотеза «приложение не запущено» исключена: ss и прямой запрос.

Развилка: 13:20, что делать

Конфигурацию посредника ведёте вы; приложение — Борис (на связи). Решения (порядок случайный):

  • А. Поднять тайм-аут чтения посредника: python3 lab/netlab.py set proxy_read_timeout=30.
  • Б. Исправить адрес апстрима в посреднике: set proxy_upstream=127.0.0.1:19000.
  • В. Попросить Бориса перенести приложение на порт 19001: set app_port=19001.
  • Г. Перезапустить всё (остановить start и запустить заново).
  • Д. Раздать пользователям адрес приложения напрямую, http://127.0.0.1:19000/api/notes, в обход посредника.
  • Е. Передать разработчикам: «приложение отвечает 502».

Перед ремонтом создайте контрольную заметку напрямую в приложение (fetch --data '{"title": "до ремонта"}' http://127.0.0.1:19000/api/notes) и запишите число заметок.

Для выбранного решения: что увидит пользователь, что сломается у тех, кто ходил по прежнему пути, как вернуть назад.

Последствия
  • А. Бесполезное: 502 остаётся, потому что отказ пришёл сразу (проверено: с тайм-аутом 30 с ответ тот же). Побочный эффект: посредник дольше держит ожидающие соединения, когда что-то и вправду зависнет.
  • Б. Допустимо: устраняет причину на участке, где она найдена. Цена: перезагрузка конфигурации посредника; нужно убедиться, что на 19001 никто не должен был слушать. Откат: вернуть 127.0.0.1:19001. Заметки на месте — настройка применяется без остановки.
  • В. Допустимо, но дороже: чинит расхождение со стороны приложения. Цена: порт 19000 перестаёт отвечать — прямые клиенты и мониторинг, которые ходили на него, останутся без службы; нужен Борис. Откат: set app_port=19000.
  • Г. Бесполезное с риском: конфигурация та же — 502 вернётся; в лабе заметки возвращаются к исходным трём.
  • Д. Частичное и вредное: через посредника по-прежнему 502; те, кто пойдёт напрямую, обходят то, что посредник должен делать (в настоящих системах — шифрование, ограничения, журнал, единый вход), а порт приложения обычно не предназначен для пользователей.
  • Е. Бесполезное: приложение исправно.

Приёмка

  • Записано, кто ответил, и три признака.
  • Гипотезы и предсказания записаны до наблюдений; названо различающее наблюдение и что оно исключило.
  • Участок и причина названы с двумя строками вывода.
  • Выбранное решение называет цену и откат.
  • Запрос пользователя через посредника — fetch http://notes.lab.test:18080/api/notes — 200, X-App-Version: 3.0, JSON; запись через посредника (fetch --data … http://notes.lab.test:18080/api/notes) — 201.
  • Контрольная заметка на месте: число заметок = было + созданные после.
  • Не принято за доказательство: «приложение отвечает по своему адресу».

Подсказки

Подсказка 1 — направление

Пётр проверил приложение. А что проверит запрос пользователя, если приложения он не видит? Кто ему отвечает — и на каком участке пути это произошло?

Подсказка 2 — нужное наблюдение

Спросите у посредника, куда он пошёл (logs --source proxy и config), и сравните с тем, что слушает компьютер (ss -ltn). Если не получается — сравните ответ через посредника и прямой ответ приложения: по каким строкам они различаются?

Подсказка 3 — разбор

Ответил посредник (Server: lab-proxy, HTML, нет X-App-Version). Он ходит на 127.0.0.1:19001 — там никого, отсюда отказ и 502 (журнал: connect() failed: Connection refused); приложение живо на 127.0.0.1:19000. Допустимы Б (исправить адрес апстрима: дёшево, откат — вернуть строку) и В (перенести приложение: дороже, ломает прямых клиентов). А и Г не меняют причину; Д обходит посредника и не выполняет приёмку «через посредника»; Е передаёт исправное приложение.

Второй вариант

Та же суббота, 14:00. Адрес апстрима исправили; 502 исчезли. Но у части пользователей — другое: «504 Gateway Time-out», ровно через две секунды. Сайт ждёт ответа до 10 секунд, мобильное приложение — до 3 секунд (их лимиты — учебная модель; в лабе это fetch --max-time 10 и fetch --max-time 3). Пётр опять проверил приложение: /health — 200 мгновенно. Перезапустите лабу со сценарием ch4b (python3 lab/netlab.py start ch4b) и действуйте так же, как в основной части.

Условия приёмки те же, плюс три:

  1. Объясните, почему исправление адреса апстрима из основной части здесь не помогает — и покажите запуском.
  2. Для временной меры назовите цену и условие отката.
  3. Проверьте данные: запись через посредника при 504 может быть сделана приложением уже после ответа посредника.

Решения (порядок случайный; ключи — для действий после диагностики):

  • А. Повторить прежний ремонт: set proxy_upstream=127.0.0.1:19000.
  • Б. Временно поднять тайм-аут чтения до 6 секунд: set proxy_read_timeout=6.
  • В. Поднять тайм-аут чтения до 60 секунд: set proxy_read_timeout=60.
  • Г. Ускорить приложение (причину медлительности ищет Борис): в лабе — set app_delay=0.
  • Д. При 504 повторять запись заметки, пока не получится.
  • Е. Передать Борису, что /api/notes отвечает за четыре секунды, с выводами fetch --timing и журналов; до ремонта — Б как временная мера.
Подсказка 1 — направление

Пётр опять проверил /health. Одинаково ли быстро отвечают /health и /api/notes? Что показывает fetch --timing и сколько ждёт посредник?

Подсказка 2 — нужное наблюдение

Сравните время прямого запроса к приложению /api/notes с временем ожидания посредника (config, журнал посредника) и посмотрите журнал приложения: сколько времени он говорит про запрос, на который посредник уже ответил 504?

Подсказка 3 — разбор

Адрес апстрима верен (ss, config; повтор А ничего не меняет), приложение отвечает, но /api/notes — за 4,0 с, а посредник ждёт 2 с. 504 — не отказ приложения, а решение посредника перестать ждать; журнал приложения показывает, что запрос всё равно обработан (200 (4000 ms)), и запись при 504 тоже сохраняется (Д создаёт дубли). Допустимы Б как временная мера — она помогает сайту (лимит 10 с), но не мобильному приложению (лимит 3 с, клиент сдаётся первым), — и Г как устойчивый ремонт; Е — вместе с Б. В — частичное и дороже Б: сайту поможет так же, мобильному — нет, а при настоящем зависании приложения каждое соединение будет занято до минуты.

Источники

  • Документация nginx, ngx_http_proxy_module: proxy_read_timeout (по умолчанию 60 с; между двумя последовательными чтениями, не на весь ответ), proxy_connect_timeout (по умолчанию 60 с), proxy_pass (с URI в адресе апстрима совпавшая часть пути заменяется — см. кейс), proxy_hide_header / proxy_pass_header.
  • Соответствие «апстрим не слушает → 502, апстрим молчит → 504» в документации nginx не найдено. Наблюдалось при запуске nginx 1.24.0 (Ubuntu), проверка посредника автором — на этой версии и этой конфигурации.
  • curl 8.5.0: --max-time, -w (time_connect, time_starttransfer, time_total), --resolve.

Теперь расследуйте путь запроса самостоятельно.

Прочитали и разобрались? Отметьте урок — прогресс сохранится в этом браузере.