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

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

Шесть методов и когда какой

Коротко

Основных методов шесть, и выбирают между ними по одному вопросу: что должно быть равномерным - число запросов, загрузка серверов или привязка клиента. Седьмой, random, нужен в особом случае - о нём в конце.

  • round-robin равняет число запросов, least_conn - занятость. Замер: пять долгих запросов заняли серверы неравномерно, и все лёгкие ушли на свободный.
  • ip_hash считает по первым трём октетам: смена последнего привязку не рвёт, смена третьего рвёт.
  • hash ... consistent при добавлении сервера переносит 26% ключей вместо 76%, но раскладывает их менее ровно.
  • sticky cookie с 1.29.6 есть в открытой версии и работает по-настоящему: метка в куке, а не совпадение хеша.

Если различаешь least_conn и round-robin на практике и знаешь про sticky в открытой версии - листай до «Теперь сам».

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

Три бэкенда, round-robin. Половина запросов лёгкие (карточка товара, 20 мс), половина тяжёлые (отчёт, четыре секунды). Что произойдёт через несколько минут?

Запросов каждый сервер получит поровну, а тяжёлых - как повезёт. Вот замер: пять долгих запросов легли на серверы неровно (a1 достался один, соседям по два), и следом пошли шесть лёгких.

↳  выводсводка по $upstream_addr из access_log, метод round-robin
/light1 a3   /light2 a1   /light3 a2
/light4 a3   /light5 a1   /light6 a2

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

↳  выводсводка по $upstream_addr из access_log, метод least_conn
/light1 a1   /light2 a1   /light3 a1
/light4 a1   /light5 a1   /light6 a1

Все шесть ушли на свободный сервер. Равное число запросов не означает равную нагрузку.

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

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

Второй способ - отправлять к кассе с самой короткой очередью. Ровно то же самое делает least_conn, и по той же причине так устроены очереди в аэропорту.

Механизм: шесть способов выбрать сервер

round-robin по кругу с весами равняет число запросов least_conn меньше активных соединений равняет занятость least_time меньше среднее время ответа мерит, а не верит весам ip_hash хеш трёх октетов адреса привязка по совпадению hash key хеш любой переменной привязка по совпадению sticky cookie метка сервера в куке настоящая привязка метод пишется первой строкой группы и действует на всю группу
Первые три равняют нагрузку, последние три привязывают клиента к серверу. Седьмой, random, - для нескольких независимых балансировщиков.

Правило

/etc/nginx/nginx.confhttp
upstream app {
    least_conn;                 # метод - первой строкой группы
    server a1:8000;
    server a2:8000;
}
Ситуация Метод
Запросы примерно одинаковые, серверы равные round-robin (ничего не пишем)
Стоимость запросов сильно разная least_conn
Серверы с разным железом, хочется мерить least_time header (открыт с 1.31; header - равнять по времени до первого байта, есть ещё last_byte)
Приложение хранит сессию в памяти процесса sticky cookie
Кеширующий слой, важна попадаемость по ключу hash $request_uri consistent
Балансировщиков несколько, друг о друге не знают random two least_conn (седьмой метод: берёт два сервера наугад и из них — менее занятый)

Разбор: ip_hash считает не весь адрес

Стенд, группа из трёх серверов, адрес клиента подменяется через realip:

адрес клиента сервер
10.0.0.1 a1
10.0.0.2 a1
10.0.0.77 a1
10.0.0.254 a1
10.0.1.1 a2
10.0.2.1 a3
10.0.3.1 a1

Для IPv4 ключом служат первые три октета: смена последнего привязку не рвёт - это сделано ради клиентов, у которых адрес плавает внутри подсети провайдера. Для IPv6 берётся весь адрес.

Отсюда же главная беда метода: за NAT весь офис или вся сотовая сеть выглядят одним адресом и оседают на одном сервере. А если перед nginx стоит CDN, без realip из прошлого раздела ip_hash вообще бессмыслен - он увидит два-три адреса узлов CDN.

Разбор: чем платит consistent

Параметр consistent включает ketama-хеширование. Обещание - при добавлении сервера переезжает примерно 1/N ключей, а не почти все. Замер на 300 разных ключах, группа растёт с трёх серверов до четырёх:

переехало ключей раскладка после
hash $request_uri 228 из 300 (76%) 77 / 73 / 73 / 77
hash $request_uri consistent 79 из 300 (26%) 94 / 65 / 62 / 79

Обещание выполняется: 26% против 76%. Но видна и цена - раскладка стала заметно менее ровной. Для кеширующего слоя это правильный размен: узел, перегруженный на четверть (94 против среднего 75), дешевле, чем три четверти промахов мимо кеша. Для раскидывания нагрузки - неправильный.

sticky: настоящая привязка, а не совпадение хеша

ip_hash и hash привязывают «по совпадению»: одинаковый ключ даёт одинаковый сервер, пока состав группы не менялся. С 1.29.6 в открытой версии есть директива, которая закрепляет клиента честно. Метод балансировки (least_conn, ip_hash) пишут первой строкой группы, а sticky - директива привязки, и её ставят среди server, не обязательно первой:

/etc/nginx/nginx.confhttp
upstream app {
    server a1:8000;
    server a2:8000;
    sticky cookie srv_id expires=1h path=/ httponly;
}

Проверено на стенде: первый ответ приносит куку, дальше клиент не сходит с сервера.

$  команда
curl -si http://shop.local/ | grep -i set-cookie
↳  выводcurl -si http://shop.local/ | grep -i set-cookie
Set-Cookie: srv_id=c69a4fd523c3ca0ca67cda2f009f1ea6; expires=Fri, 11-Sep-26 12:26:19 GMT; max-age=3600; httponly; path=/
↳  выводсводка по $upstream_addr: пять запросов с сохранением куки и пять без
с кукой:  a1 a1 a1 a1 a1
без куки: a2 a1 a2 a1 a2

Отличий от хеша два, и оба важные: привязка не разъезжается при добавлении сервера в группу и не ломается за NAT - метка у каждого браузера своя.

Рядом два параметра, ради которых всё и затевалось:

/etc/nginx/nginx.confhttp upstream app
server a1:8000 route=a;
server a2:8000 route=b drain;

route задаёт метку явно - удобно, когда её выставляет балансировщик выше по цепочке. drain переводит сервер в режим «новых не принимать, старых дообслужить»: закреплённые клиенты продолжают ходить на него, новые уходят на соседей. Это штатный способ вывести узел из работы, не оборвав сессии.

Половина статей в интернете всё ещё пишет «sticky - это только NGINX Plus». С 1.29.6 это неправда; в 1.31.5 на стенде работают все три формы - cookie, route и learn.

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

backup не работает с hash, ip_hash и random. Конфиг не соберётся вовсе:

↳  выводnginx -t
nginx: [emerg] balancing method does not support parameter "backup" in /etc/nginx/nginx.conf:11
nginx: configuration file /etc/nginx/nginx.conf test failed

С least_conn и sticky резервный сервер разрешён. Если нужны и привязка, и резерв - выбирать придётся между ними либо делать приложение stateless.

Убирать сервер из группы нельзя - только помечать down. Список участвует в вычислении хеша, поэтому удаление строки перетасует привязки всех клиентов, а не только тех, кто сидел на убранном сервере.

slow_start в открытой версии нет. Строка server a1:8000 slow_start=30s; роняет конфиг с invalid parameter. Вернувшийся сервер получает полную долю трафика сразу, и если он ещё прогревается, первые пользователи это почувствуют.

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

Выбор метода - это ответ на вопрос про приложение, а не про nginx. Стоимость запросов ровная - оставь умолчание, оно предсказуемее всех. Разброс большой - least_conn. Железо разное - least_time header, он мерит время ответа вместо того, чтобы верить весам в конфиге.

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

Теперь сам

Кеширующий слой из четырёх серверов, ключ hash $request_uri consistent. Добавили пятый сервер, и в первый час нагрузка на базу выросла втрое, хотя переехать должно было около пятой части ключей. Что не сходится?

Сходится. Переехало и правда примерно 20% ключей, но каждый переехавший ключ - это промах мимо кеша и поход в базу, пока новый узел не прогрелся. Замер выше объясняет и порядок величины: без consistent переехало бы три четверти, и рост был бы не втрое, а на порядок. Лечится не выбором метода, а прогревом нового узла до включения в группу.

Главное

Метод пишется первой строкой группы. round-robin равняет число запросов, least_conn - занятость (на замере он отдал все лёгкие запросы свободному серверу, пока круг продолжал грузить занятых), least_time мерит время ответа. ip_hash считает по первым трём октетам и ломается за NAT и CDN; hash ... consistent переносит при росте группы 26% ключей вместо 76%, но раскладывает менее ровно. Настоящую привязку даёт sticky cookie - с 1.29.6 он есть в открытой версии, а drain рядом выводит сервер из работы, дообслуживая закреплённых. backup несовместим с hash, ip_hash и random, а slow_start в открытой версии нет вовсе.

Комментарии

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

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

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