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

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

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

Коротко

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

  • round-robin равняет число запросов, least_conn - занятость. Замер: пять долгих запросов заняли серверы неравномерно, и все лёгкие ушли на свободный.
  • ip_hash считает по первым трём октетам адреса IPv4 (у IPv6 берётся адрес целиком): смена последнего октета привязку не рвёт, смена третьего рвёт.

Если различаешь 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 переехало бы три четверти, и рост был бы не втрое, а на порядок. Лечится не выбором метода, а прогревом нового узла до включения в группу.

Главное

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

Комментарии

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

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

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