16 Авг

Балансируем в nginx

За последние пару недель приходилось 4 раза настраивать балансировку нагрузки между несколькими бекендами средствами nginx и еще больше раз консультировать по данному вопросу.
Не знаю, почему так вырос интерес у людей в данном вопросе и что их побуждает докупать дополнительные сервера для обеспечения стабильности работы своих ресурсов (популярность найдет везде?), но факт остается фактом.
Всвязи с этим, набросаю небольшой эпос, чтобы избавить многих начинающих специалистов от изобретений очередных велосипедов.

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

Запросы от посетителей Nginx проксирует бекендам (apache, fpm, fastcgi, etc…) случайным или конкретно определенным образом, распределяя этим самым все запросы между несколькими серверами.
Допустим, у нас есть 3 бекенда, между которыми нам необходимо распределить нагрузку и перебрасывать все запросы. Назовем эти сервера, допустим, backend1.srv.net, backend2.srv.net и backend3.srv.net соответственно (можно и просто IP-адреса указать). Теперь лезем в любимый nginx.conf и описываем эти сервера следующим образом:

upstream backendCloud {
  server backend1.srv.net;
  server backend2.srv.net;
  server backend3.srv.net;
}

Этим самым мы дали имя нашему апстриму из трех серверов backendCloud. Самое главное, что теперь мы без проблем можем использовать это имя в параметре proxy_pass, чтобы перебрасывать все запросы туда. Работает это по принципу round-robin — nginx будет перемешивать случайным образом список серверов в апстриме каждый раз при запросе к нему, отсылая все запросы на них случайным образом.
В конфиге это будет выглядить примерно так:

server {
  listen x.x.x.x:80;
  location / {
    proxy_pass http://backendCloud;
  }
}

На этом, пожалуй, можно и остановиться (что многие и делают, почему-то), но не все так радужно.
Дело в том, что в такой схеме есть немало минусов — конечно, все зависит от конкретного проекта, но я рассмотрю пару частых случаев.
Допустим, в клиентском проекте существует система авторизации, что подразумевает работу сессий. Что произойдет в этом случае? А произойдет то, что сперва вы авторизуетесь на первом бекенде, который создаст у себя все необходимые сессии, а затем следующим запросом nginx перебросит вас на другой бекенд, который про ваши сессии ничего не знает. И вам придется авторизовываться снова. Бред? Бред! Но Nginx имеет возможность привязывать клиентов по их ip-адресам (ну ладно, по ip-хешам) к определенному бекенду до окончания сессии. Делается это очень просто одним параметром ip_hash:

upstream backendCloud {
  ip_hash;
  server backend1.srv.net;
  server backend2.srv.net;
  server backend3.srv.net;
}

В моем недавнем случае бекенды были не одинаковыми серверами и очень сильно отличалиь друг от друга в плане системных ресурсов. Опытным путем было решено, что нет смысла пытаться распределять между ними нагрузку равномерно, иначе один сервер буден задействован на долю возможностей, а другой, наоборот, будет пыхтеть на пределе своих сил. Курение доков обрадовали — в описании апстрима можно указывать своего рода «вес» бекендам. То есть, указывать nginx’у, на какие бекенды запросы можно отправлять чаще, а на какие реже, а какие вообще временно исключить из апстрима (например, на момент тестирования и прочих проверок). Пример:

upstream backendCloud {
  ip_hash;
  server backend1.srv.net;
  server backend2.srv.net weight=3;
  server backend3.srv.net down;
  server backend4.srv.net weight=2;
}

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

Во всей этой схеме есть один нюанс, который обязательно стоит учесть — представим, что внезапно один из бекендов упал и более недоступен. Если nginx не смог до него достучаться, он отправит запрос на обработку следующему бекенду. Небольшая задержка — ничего страшного, но проблема в том, что она будет до тех пор, пока не станет доступен упавший сервер, поскольку nginx по составленным правилам постоянно будет пытаться его проверять, что в итоге при большой посещаемости просто будет раздражать как клиентов, так и сам балансировщик, загружая его ненужными постоянными проверками. Обходится эта проблема очень просто и красиво. При помощи директивы max_fails можно указать максимально-допустимое количество неудачных попыток подключения к бекенду, после чего он будет считаться нерабочим и запросы к нему прекратятся. По умолчанию значение этого параметра равно единице, то есть после одной неудачной попытки Nginx на определённое время прекратит попытки новых подключений с нерабочему серверу. Это время определяется параметром fail_timeout, по умолчанию значение которого равно 10 секундам.

upstream backendCloud {
  ip_hash;
  server backend1.srv.net max_fails=3 fail_timeout=120;
  server backend2.srv.net weight=3;
  server backend3.srv.net down;
  server backend4.srv.net weight=2;
  server backend5.srv.net backup;
}

Попутно я указал пятый бекенд — в моем случае это был просто самый слабый сервер. Директивой backup я указывал nginx’у, что на него он может отправлять запросы исключительно в том случае, если все остальные бекенды оказались недоступны. Этим самым, просто сделал пятый бекенд резервным.