1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 1 из 5

Как nginx понимает, что сервер лёг

Коротко

В разобранной upstream-группе nginx определяет отказы по результатам пользовательских запросов.

  • max_fails неудач за fail_timeout временно исключают сервер; по умолчанию это одна неудача и десять секунд.
  • Без zone счётчики у воркеров раздельные. Общая зона позволяет им учитывать результаты вместе.
  • Условия повторной попытки влияют на то, какие отказы учитываются.
  • Для единственного сервера группы max_fails и fail_timeout игнорируются. Его отказ нельзя проверить как временное исключение одного из нескольких узлов.

Если знаешь, что 500 не считается отказом, и понимаешь роль zone в счётчиках - листай до «Теперь сам».

Сначала ответь сам

Четыре рабочих процесса, группа из трёх серверов, max_fails=1. Один сервер падает. Сколько неудачных запросов уйдёт в мёртвый сервер, прежде чем nginx перестанет туда ходить?

Ответ зависит от строки, которой в конфиге обычно нет. Замер на стенде без zone:

↳  выводgrep "temporarily disabled" error.log
[warn] 72#72: *31 upstream server temporarily disabled while connecting to upstream
[warn] 73#73: *44 upstream server temporarily disabled while connecting to upstream
[warn] 74#74: *57 upstream server temporarily disabled while connecting to upstream
[warn] 75#75: *68 upstream server temporarily disabled while connecting to upstream

72#72 - номер процесса и потока (у каждого воркера свой), *31 - номер соединения. Четыре строки от четырёх разных процессов: каждый узнал о падении сам и заплатил за это своим неудачным соединением. Со строкой zone app 64k; в группе строка ровно одна.

На что это похоже

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

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

Механизм: пассивная проверка

время запрос упал неудача 1 max_fails набран сервер выключен прошёл fail_timeout пробуем снова пробный запрос - обычный пользовательский, а не служебный не ответил - снова пауза; ответил - сервер вернулся в ротацию активных проверок в открытой версии нет
nginx учится на живых запросах. Первым после паузы всегда идёт чей-то настоящий запрос.

Правило

/etc/nginx/nginx.confhttp
upstream app {
    zone app 64k;
    server a1:8000 max_fails=3 fail_timeout=15s;
    server a2:8000;
}

Читается так: «если за 15 секунд с сервером случилось 3 неудачи, считать его недоступным на те же 15 секунд». Умолчание - max_fails=1 fail_timeout=10s.

Список того, что считается неудачей, задаёт proxy_next_upstream:

/etc/nginx/conf.d/shop.local.confhttp › server › location /api/
proxy_next_upstream error timeout;
proxy_next_upstream_tries 2;      # всего попыток, считая первую
proxy_next_upstream_timeout 10s;  # позже 10 с новую попытку не начинать

proxy_next_upstream_timeout не обрывает попытку, которая уже идёт: он только запрещает начинать следующую. Замер на 1.31.8: proxy_next_upstream_timeout 100ms, proxy_read_timeout 3s, бэкенд отвечает через 1,3 секунды - клиент получил 200 через 1,30 с. Сколько ждать одну попытку, решают proxy_connect_timeout и proxy_read_timeout; жёсткий срок на весь запрос nginx не ставит.

Умолчание - error timeout: обрыв соединения, отказ в соединении, таймаут. Код ответа приложения в этот список не входит.

Разбор: 502 от приложения - не отказ, и это к лучшему

Стенд, бэкенд отвечает 502 на каждый запрос. С умолчаниями:

↳  выводчетыре запроса, proxy_next_upstream по умолчанию
/fail -> a1  status=502
/fail -> a2  status=502
/fail -> a1  status=502
/fail -> a2  status=502

nginx передал ответ клиенту и ничего не заподозрил. Теперь добавим http_502 в список, как советуют «чтобы клиент не видел ошибок»:

↳  выводчетыре запроса, proxy_next_upstream error timeout http_502
/fail -> a1, a2   status=502  ustatus=502, 502
/fail -> a2, app  status=502  ustatus=502, 502
/fail -> app      status=502  ustatus=502
/fail -> app      status=502  ustatus=502

Первый запрос обошёл оба сервера. Второй нашёл только один живой и добил группу. Дальше в $upstream_addr стоит имя группы app - значит no live upstreams: живых серверов не осталось, и nginx отвечает 502 сам, даже не пробуя соединиться:

↳  выводerror.log
[error] no live upstreams while connecting to upstream, upstream: "http://app/fail"

Клиент как получал 502, так и получает. Разница в том, что теперь вся группа выключена, и штатные запросы, на которые бэкенд ответил бы 200, тоже получат 502. Отсюда правило: коды в proxy_next_upstream добавляют, только когда 502 означает «сервер сломан», а не «приложение так отвечает».

Разбор: как выглядит успешный повтор

Когда сервер и правда умер, клиент обычно не видит ничего:

↳  выводaccess_log
/api/cart -> 10.0.0.11:8000, 10.0.0.12:8000  status=200  ustatus=502, 200

Два адреса через запятую - первая попытка и вторая, успешная. В error_log в этот момент две строки подряд:

↳  выводerror.log
[error] connect() failed (111: Connection refused) while connecting to upstream
[warn] upstream server temporarily disabled while connecting to upstream

Первая говорит, что случилось, вторая - что nginx решил. Такие пары в логе стоит считать: если их десятки в час, у тебя постоянно падающий бэкенд, а пользователи об этом молчат, потому что каждый раз выручает повтор.

Что ломается без этого

non_idempotent добавляют, не подумав. Неидемпотентными nginx считает POST, LOCK и PATCH: это методы, повтор которых может дать второй эффект. PUT и DELETE меняют данные, но идемпотентны - повторённый DELETE удаляет то же самое, что и первый. Флаг non_idempotent разрешает повторять на другом сервере и POST с PATCH, причём запрет касается только запроса, который уже ушёл бэкенду: если соединение не установилось, повтор безопасен и так. Если первый бэкенд успел провести платёж и умер до ответа, повтор проведёт его второй раз. Повтор POST безопасен, только когда приложение умеет идемпотентность по ключу операции.

Сервер, вернувшийся из паузы, встречает пользователя, а не проверку. Между «процесс запустился» и «готов обслуживать» проходит время: прогрев, миграции, подключение к базе. slow_start в открытой версии нет, поэтому вернувшийся сервер сразу получает полную долю; чинится это порядком выкладки из следующего урока, а не настройками группы.

Единственный сервер группы за отказы не выключается. Когда сервер в группе один, max_fails и fail_timeout игнорируются: выключать некого, отвечать стало бы нечем. Поэтому от отказов строки no live upstreams на одиночном бэкенде не бывает - она появится, только если сервер помечен down или упёрся в max_conns, и весь разговор про max_fails начинается со второго сервера. А вот когда серверов несколько, недоступными могут стать все - это и показал разбор http_502 выше: «последний живой» защищён, только пока он буквально единственный в группе.

Зачем это в работе

down и backup - две соседние строки с очень разным смыслом:

/etc/nginx/nginx.confhttp
upstream app {
    zone app 64k;
    server a1:8000 down;                 # выведен руками, на обслуживание
    server a2:8000;
    server maintenance:8080 backup;      # включится, только если все основные легли
}

down - «не трогать вовсе»; так выводят машину на работы, не удаляя строку (удаление перетасует привязки при hash и ip_hash). backup - резерв, который молчит, пока жив хоть один основной сервер. Типичное применение - не второй экземпляр приложения, а маленький сервис со страницей «идут работы»: вежливая заглушка лучше, чем 502.

Теперь сам

В группе два сервера. Мониторинг показывает, что раз в несколько минут появляется пара строк connect() failed и temporarily disabled, но доля 5xx у пользователей нулевая. Стоит ли реагировать?

Стоит. Пользователей выручает повтор на живом сервере, но каждый такой эпизод - это половина мощности, выключенная на fail_timeout, и лишняя задержка у попавших под него запросов. Смотреть надо в бэкенд: обычно это перезапуск по нехватке памяти или исчерпание пула соединений к базе. И заодно добавить zone, чтобы падение открывал один воркер, а не каждый по очереди.

Главное

Пассивная проверка замечает отказ на пользовательском запросе. Настраивая исключение узла, проверь, какие ошибки считаются неудачей и где хранятся счётчики. Излишне широкий список кодов способен исключить всю группу из-за штатного ответа приложения. Для обслуживания выводи узел из работы явно.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.