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

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

Группа серверов вместо адреса

Коротко

upstream - именованная группа серверов. Пишется в контексте http, дальше имя группы ставят в proxy_pass вместо адреса.

  • Сервер выбирается на каждый запрос, а не на клиента: приложение с сессией в памяти процесса за балансировщиком ломается.
  • Host бэкенду по умолчанию станет именем группы (Host: app), а не именем сайта.
  • Счётчики группы по умолчанию у каждого воркера свои. Строка zone кладёт их в общую память, и это меняет поведение отказов.
  • Проверять раскладку надо $upstream_addr в логе, а не догадками.

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

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

/etc/nginx/nginx.confhttp
upstream app {
    server a1:8000;
    server a2:8000;
    server a3:8000;
}
/etc/nginx/conf.d/shop.local.confhttp server
location / { proxy_pass http://app; }

a1, a2, a3 - имена серверов: в Docker это имена контейнеров, на обычной машине - записи из DNS или /etc/hosts. Слово server внутри upstream - не тот блок server { }, что описывает сайт: здесь это просто «адрес в группе», listen и location внутрь не пишут. Имя группы nginx отличает от адреса по тому, что оно объявлено блоком upstream. Какой Host увидит приложение?

Стенд отвечает app - имя группы (подставной бэкенд печатает $http_host). Не имя сайта, не адрес конкретного сервера. Четыре строки заголовков из прошлого раздела нужны здесь ровно так же, и забывают их именно при переезде с одиночного адреса на группу: раньше в Host был хотя бы app-1:8000, теперь там слово, которого не существует нигде.

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

Отдел из трёх сотрудников и общий телефон на входе. Звонящий не выбирает человека - он звонит в отдел, и трубку берёт тот, кто свободен.

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

Механизм: решение принимается на каждый запрос

браузер nginx a1: сессия тут a2: сессии нет a3: сессии нет запрос 1 попал на a1, запрос 2 того же браузера - на a2: корзина пуста
Балансировщик не знает про клиента ничего. Каждый запрос - отдельное решение.

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

Первый путь правильный, второй быстрый. Часто делают второй, чтобы прожить до первого.

Правило

/etc/nginx/nginx.confhttp
upstream app {
    zone app 64k;                  # общая память на всех воркеров
    server a1:8000 weight=3;       # втрое больше запросов; параметры через = без пробелов
    server a2:8000;                # weight=1 по умолчанию
}

Веса нужны, когда серверы разные по мощности. Единственный честный способ подобрать числа - измерить: если $upstream_response_time у «тяжёлого» сервера растёт быстрее, чем у остальных, вес завышен.

Строка zone выглядит служебной, а меняет она главное - где живут счётчики группы. 64k - размер этой общей памяти, имя обычно берут как у группы. Без неё каждый рабочий процесс считает отказы и соединения сам по себе.

Разбор: круг раздаётся не там, где кажется

Метод по умолчанию - круговой перебор. На стенде из трёх серверов при одном рабочем процессе он идеален:

Бэкенды печатают своё имя, worker_processes 1, 30 запросов подряд:

↳  выводсводка по $upstream_addr, 30 запросов, worker_processes 1
a1a2a3a1a2a3a1a2a3a1a2a3a1a2a3a1a2a3a1a2a3a1a2a3a1a2a3a1a2a3

А при четырёх процессах в ту же последовательность вклиниваются повторы:

↳  выводсводка по $upstream_addr, 30 запросов, worker_processes auto
a2a3a1a2a3a1a2a3a1a2a3a1a2a2a3a1a2a3a1a2a3a1a2a3a1a2a3a1a2a2

auto - это столько процессов, сколько ядер (задаётся worker_processes); у тебя число своё. Ничего не сломалось: у каждого воркера свой счётчик круга, и запрос, попавший к другому воркеру, начинает круг с его позиции. На больших числах раскладка выравнивается, а вот на десятке запросов «проверил - неровно» ничего не значит.

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

Разбор: как посмотреть, кто на самом деле отвечает

Догадки тут не работают - в лог добавляют адрес сервера, который обработал запрос:

/etc/nginx/nginx.confhttp
log_format up '$time_iso8601 $request_uri -> $upstream_addr rt=$request_time';

Строка access_log /var/log/nginx/up.log up; пишет этот формат; awk '{print $4}' берёт четвёртое поле (адрес после ->), uniq -c считает одинаковые, sort перед ним обязателен - uniq схлопывает только соседние строки:

↳  выводawk '{print $4}' /var/log/nginx/up.log | sort | uniq -c
     67 10.0.0.11:8000
     66 10.0.0.12:8000
     67 10.0.0.13:8000

Слева количество, справа адрес. Имена a1-a3 в логе превратились в адреса: nginx резолвит их при старте, в замере они легли на 10.0.0.11-13. Ровно - группа работает. Перекос на один сервер обычно значит, что остальные помечены недоступными. А если вместо адреса в логе стоит имя группы - -> app - значит живых серверов не осталось вовсе; разбор этой строки в главе про отказы.

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

Забытый proxy_set_header Host $host в группе даёт не «немного не тот заголовок», а имя, которого нет в DNS: письма со ссылками на http://app/, непройденные проверки источника (приложение сверяет Host с настроенным доменом и отклоняет чужой), мультидоменное приложение, выбравшее сайт наугад.

Второе - тихая потеря половины мощности. Сервер выпал из ротации, клиенты ничего не заметили (nginx сам повторил запрос на живом сервере - когда именно, разбирает урок про отказыРазбирается в разделе 8, глава «Отказы, backup и keepalive»: Как nginx понимает, что сервер лёг), нагрузка на оставшиеся выросла вдвое, и узнаешь ты об этом в час пик. Поэтому раскладку по $upstream_addr стоит держать на графике, а не смотреть руками раз в квартал.

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

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

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

Теперь сам

В группе три сервера, все живые. Разработчик жалуется: «загрузка файла падает через раз». Файл загружается по частям, каждая часть - отдельный запрос. Что происходит и что чинить?

Части уходят на разные серверы, и тот, кто получил вторую часть, не знает про первую. Залипание (привязка клиента к одному серверу, следующий урокРазбирается в разделе 8, глава «Методы балансировки»: Шесть методов и когда какой) тут закроет симптом, но правильный ответ - собирать части в общем хранилище, а не в памяти или локальном каталоге процесса. Быстрая проверка: посмотреть $upstream_addr у запросов одной загрузки - адреса будут разные.

Главное

upstream - именованная группа серверов в контексте http, её имя ставят в proxy_pass вместо адреса. Сервер выбирается на каждый запрос заново, поэтому приложение с состоянием в памяти процесса за группой ломается, и правильное лечение - вынести состояние наружу. Host бэкенду по умолчанию станет именем группы, так что четыре строки заголовков нужны и здесь. Счётчики группы без строки zone у каждого воркера свои - на круговом переборе это видно как повторы в короткой выборке, а на отказах обходится дороже. Раскладку проверяют $upstream_addr в логе.

Комментарии

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

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

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