Посредник между клиентом и приложением
Чтение и практика ~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
Мини-задача
Задача: выяснить, кто ответил пользователю и на каком участке запрос остановился, и вернуть работоспособный путь через посредника, не потеряв заметки. Готовой цепочки команд нет: инструменты названы выше, порядок выбираете сами.
До любого наблюдения, кроме повтора симптома, запишите:
- Кто ответил пользователю — приложение или посредник; по каким трём признакам в выводе вы это скажете.
- Не меньше двух гипотез, почему запрос остановился (одна — слова Петра); для каждой — что подтвердило бы её и что бы исключило.
- Одно наблюдение, которое различает гипотезы. Выполните его и запишите вывод, а также то, какие гипотезы оно исключило.
Затем: участок (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) и действуйте так же, как в основной части.
Условия приёмки те же, плюс три:
- Объясните, почему исправление адреса апстрима из основной части здесь не помогает — и покажите запуском.
- Для временной меры назовите цену и условие отката.
- Проверьте данные: запись через посредника при 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), проверка посредника автором — на этой версии и этой конфигурации.
curl8.5.0:--max-time,-w(time_connect,time_starttransfer,time_total),--resolve.
Прочитали и разобрались? Отметьте урок — прогресс сохранится в этом браузере.