# теория · шаг 1 из 5
Как nginx понимает, что сервер лёг
Коротко
В разобранной upstream-группе nginx определяет отказы по результатам пользовательских запросов.
- max_fails неудач за fail_timeout временно исключают сервер; по умолчанию это одна неудача и десять секунд.
- Без zone счётчики у воркеров раздельные. Общая зона позволяет им учитывать результаты вместе.
- Условия повторной попытки влияют на то, какие отказы учитываются.
- Для единственного сервера группы max_fails и fail_timeout игнорируются. Его отказ нельзя проверить как временное исключение одного из нескольких узлов.
Если знаешь, что 500 не считается отказом, и понимаешь роль zone в счётчиках - листай до «Теперь сам».
Сначала ответь сам
Четыре рабочих процесса, группа из трёх серверов, max_fails=1. Один сервер
падает. Сколько неудачных запросов уйдёт в мёртвый сервер, прежде чем nginx
перестанет туда ходить?
Ответ зависит от строки, которой в конфиге обычно нет. Замер на стенде без
zone:
[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; в группе
строка ровно одна.
На что это похоже
Четыре продавца в одном зале и общий список поставщиков. Если список висит на стене, первый же продавец, которому не отгрузили товар, вычеркнет поставщика для всех. Если у каждого свой блокнот, каждому придётся обжечься лично.
Работа в итоге сделается одинаково, разница только в цене обучения - и в том, сколько покупателей подождёт, пока все четверо разберутся.
Механизм: пассивная проверка
Правило
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:
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 на каждый запрос. С умолчаниями:
/fail -> a1 status=502
/fail -> a2 status=502
/fail -> a1 status=502
/fail -> a2 status=502
nginx передал ответ клиенту и ничего не заподозрил. Теперь добавим 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] no live upstreams while connecting to upstream, upstream: "http://app/fail"
Клиент как получал 502, так и получает. Разница в том, что теперь вся группа
выключена, и штатные запросы, на которые бэкенд ответил бы 200, тоже получат
502. Отсюда правило: коды в proxy_next_upstream добавляют, только когда 502
означает «сервер сломан», а не «приложение так отвечает».
Разбор: как выглядит успешный повтор
Когда сервер и правда умер, клиент обычно не видит ничего:
/api/cart -> 10.0.0.11:8000, 10.0.0.12:8000 status=200 ustatus=502, 200
Два адреса через запятую - первая попытка и вторая, успешная. В 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 - две соседние строки с очень разным смыслом:
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,
чтобы падение открывал один воркер, а не каждый по очереди.
Главное
Пассивная проверка замечает отказ на пользовательском запросе. Настраивая исключение узла, проверь, какие ошибки считаются неудачей и где хранятся счётчики. Излишне широкий список кодов способен исключить всю группу из-за штатного ответа приложения. Для обслуживания выводи узел из работы явно.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий