combined-upstreams: Модуль NGINX Combined Upstreams
Установка
Вы можете установить этот модуль в любой дистрибутив на базе RHEL, включая, но не ограничиваясь:
- RedHat Enterprise Linux 7, 8, 9 и 10
- CentOS 7, 8, 9
- AlmaLinux 8, 9
- Rocky Linux 8, 9
- Amazon Linux 2 и Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install nginx-module-combined-upstreams
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install nginx-module-combined-upstreams
Включите модуль, добавив следующее в верхней части файла /etc/nginx/nginx.conf:
load_module modules/ngx_http_combined_upstreams_module.so;
Этот документ описывает nginx-module-combined-upstreams v2.3.1, выпущенный 15 апреля 2025 года.
The module introduces three directives add_upstream, combine_server_singlets, and extend_single_peers available inside upstream configuration blocks, and a new configuration block upstrand for building super-layers of upstreams. Additionally, directive dynamic_upstrand is introduced for choosing upstrands in run-time.
Директива add_upstream
Заполняет хост-апстрим серверами, перечисленными в уже определённом апстриме, указанном обязательным 1-м параметром директивы. Атрибуты сервера, такие как веса, max_fails и другие, сохраняются в хост-апстриме. Необязательные параметры могут включать значение backup, чтобы пометить все серверы исходного апстрима как резервные, и weight=N, чтобы откалибровать веса серверов исходного апстрима, умножив их на коэффициент N.
Пример
upstream combined {
add_upstream upstream1; # исходный апстрим 1
add_upstream upstream2 weight=2; # исходный апстрим 2
server some_another_server; # если необходимо
add_upstream upstream3 backup; # исходный апстрим 3
}
Директива combine_server_singlets
Создаёт несколько singlet upstreams из серверов, определённых в хост-апстриме. Singlet upstream содержит только один активный сервер, в то время как другие серверы помечены как резервные или недоступные. Если параметры не были переданы, то singlet upstreams будут иметь имена хост-апстрима с добавленным порядковым номером активного сервера в хост-апстриме. Необязательные 2 параметра могут быть использованы для настройки их имён. 1-й параметр — это суффикс, добавляемый после имени хост-апстрима и перед порядковым номером. 2-й параметр должен быть целым числом, которое определяет нулевое выравнивание порядкового номера. Например, если он равен 2, то порядковые номера могут быть '01', '02', ..., '10', ... '100' ....
Чтобы пометить вторичные серверы как недоступные, а не резервные, используйте другой необязательный параметр nobackup. Этот параметр должен быть указан в конце, после всех других параметров.
Пример
upstream uhost {
server s1;
server s2;
server s3 backup;
server s4;
# создаёт singlet upstreams uhost_single_01,
# uhost_single_02, uhost_single_03 и uhost_single_04
combine_server_singlets _single_ 2;
server s5;
}
Почему числа, а не имена?
В приведённом выше примере singlet upstreams будут иметь имена, такие как uhost_single_01, но имена, содержащие имена серверов, такие как uhost_single_s1, выглядели бы лучше и удобнее. Почему бы не использовать их вместо порядковых номеров? К сожалению, Nginx не запоминает имена серверов после того, как сервер был добавлен в апстрим, поэтому мы не можем просто их извлечь.
Обновление. Есть хорошие новости! Начиная с версии 1.7.2, Nginx запоминает имена серверов в данных апстрима, и теперь мы можем использовать их, ссылаясь на специальное ключевое слово byname. Например,
combine_server_singlets byname;
# или
combine_server_singlets _single_ byname;
Все двоеточия (:) в именах серверов заменяются на подчеркивания (_).
Где это может быть полезно
Singlet upstream ведёт себя как один сервер с режимом резервирования. Это может быть использовано для управления "липкими" HTTP-сессиями, когда серверы на заднем плане идентифицируют себя с помощью соответствующего механизма, такого как HTTP cookies.
upstream uhost {
server s1;
server s2;
combine_server_singlets;
}
server {
listen 8010;
server_name main;
location / {
proxy_pass http://uhost$cookie_rt;
}
}
server {
listen 8020;
server_name server1;
location / {
add_header Set-Cookie "rt=1";
echo "Передано на $server_name";
}
}
server {
listen 8030;
server_name server2;
location / {
add_header Set-Cookie "rt=2";
echo "Передано на $server_name";
}
}
В этой конфигурации первый запрос клиента будет выбирать сервер на заднем плане случайным образом, выбранный сервер установит cookie rt на предопределённое значение (1 или 2), и все последующие запросы от этого клиента будут автоматически проксироваться на выбранный сервер, пока он не станет недоступен. Допустим, это был server1, тогда, когда он станет недоступен, cookie rt на стороне клиента всё ещё будет 1. Директива proxy_pass направит следующий клиентский запрос на singlet upstream uhost1, где server1 объявлен активным, а server2 — резервным. Как только server1 станет недоступным, Nginx направит запрос на server2, который перепишет cookie rt, и все последующие клиентские запросы будут проксироваться на server2, пока он не станет недоступным.
Директива extend_single_peers
Пиры в апстримах выходят из строя в соответствии с правилами, перечисленными в директиве proxy_next_upstream. Если у апстрима есть только один пир в его основной или резервной части, то этот пир никогда не выйдет из строя. Это может быть серьёзной проблемой при написании пользовательского алгоритма для активных проверок состояния пирсов апстрима. Директива extend_single_peers, объявленная в блоке апстрима, добавляет фиктивный пир, помеченный как down, в основной или резервной части апстрима, если в части изначально содержится только один пир. Это заставляет Nginx пометить оригинальный единственный пир как вышедший из строя, когда он не проходит правила proxy_next_upstream, как это происходит в общем случае с несколькими пирами.
Пример
upstream upstream1 {
server s1;
extend_single_peers;
}
upstream upstream2 {
server s1;
server s2;
server s3 backup;
extend_single_peers;
}
Обратите внимание, что если часть (основная или резервная) апстрима содержит более одного пира (как основная часть в upstream2 из примера), то директива не имеет эффекта: в частности, в upstream2 она влияет только на резервную часть апстрима.
Блок upstrand
Предназначен для настройки супер-слоя апстримов, которые не теряют свои идентичности. Принимает ряд директив, включая upstream, order, next_upstream_statuses и другие. Апстримы с именами, начинающимися с тильды (~), соответствуют регулярному выражению. Только апстримы, которые уже были объявлены до определения блока upstrand, рассматриваются как кандидаты.
Пример
upstrand us1 {
upstream ~^u0 blacklist_interval=60s;
upstream b01 backup;
order start_random;
next_upstream_statuses error timeout non_idempotent 204 5xx;
next_upstream_timeout 60s;
intercept_statuses 5xx /Internal/failover;
}
Upstrand us1 будет объединять все апстримы, имена которых начинаются с u0, и апстрим b01 как резервный. Резервные апстримы проверяются, если все нормальные апстримы выходят из строя. Выход из строя означает, что все апстримы в нормальных или резервных циклах ответили статусами, перечисленными в директиве next_upstream_statuses, или были внесены в черный список. Здесь ответ апстрима означает статус, возвращаемый последним сервером апстрима, который сильно зависит от значения директивы proxy_next_upstream. Апстрим помечается как внесённый в черный список, когда у него есть параметр blacklist_interval и он отвечает статусом, перечисленным в next_upstream_statuses. Состояние черного списка не делится между рабочими процессами Nginx.
Следующие четыре директивы upstrand аналогичны тем, что из модуля прокси Nginx.
Директива next_upstream_statuses принимает нотацию статусов 4xx и 5xx и значения error и timeout, чтобы различать случаи, когда ошибки происходят с соединениями пирсов апстрима, от тех случаев, когда бэкенды отправляют статусы 502 или 504 (простые значения 502 и 504, а также 5xx относятся к обоим случаям). Она также принимает значение non_idempotent, чтобы разрешить дальнейшую обработку неидемпотентных запросов, когда они были отвечены последним сервером из апстрима, но не прошли по другим статусам, перечисленным в директиве. Запросы считаются неидемпотентными, когда их методы POST, LOCK или PATCH, как и в директиве proxy_next_upstream.
Директива next_upstream_timeout ограничивает общее время, в течение которого upstrand проходит через все свои апстримы. Если время истекает, пока upstrand готов перейти к следующему апстриму, возвращается результат последнего цикла апстрима.
Директива intercept_statuses позволяет осуществлять failover upstrand путем перехвата окончательного ответа в location, который соответствует данному URI. Перехваты должны происходить даже тогда, когда upstrand превышает время ожидания. Также обратите внимание, что обход апстримов в upstrand и URI failover upstrand не подлежат перехвату. Говоря более общо, любое внутреннее перенаправление (по error_page, proxy_intercept_errors, X-Accel-Redirect и т.д.) нарушит вложенные подзапросы, на которых основана реализация upstrand, что приведет к возврату пустых ответов. Это крайне плохие случаи, и именно поэтому обход апстримов был защищён от перехватов. URI failover upstrand более подвержен этому, так как реализация имеет меньше контроля над его местоположением. В частности, failover upstrand имеет защиту только от перехватов по error_page и proxy_intercept_errors. Это означает, что местоположение URI failover upstrand должно быть как можно более простым (например, с использованием простых директив, таких как return или echo).
Сказав это, есть достойное решение проблемы с местоположениями failover upstrand и внутренними перенаправлениями в них. Как именно внутренние перенаправления нарушают подзапросы? Что ж, они стирают контексты подзапросов, необходимые в фильтрах ответов модуля. Таким образом, если мы сможем сделать контекст подзапроса постоянным, решим ли мы проблему? Ответ — да! Модуль Nginx nginx-easy-context позволяет создавать постоянные контексты запросов. Upstrands могут извлечь выгоду из них, включив переключатель в файле config и построив оба модуля. Смотрите детали в разделе Сборка и тестирование.
Директива order в настоящее время принимает только одно значение start_random, что означает, что начальные апстримы в нормальных и резервных циклах после запуска рабочего процесса будут выбраны случайным образом. Начальные апстримы в последующих запросах будут проходить в круговом порядке. Кроме того, модификатор per_request также принимается в директиве order: он отключает глобальный круговой цикл по рабочему процессу. Сочетание per_request и start_random делает так, что начальный апстрим в каждом новом запросе выбирается случайным образом.
Такой failover между статусами failure может быть достигнут во время одного запроса, передавая специальную переменную, начинающуюся с upstrand_, в директиву proxy_pass следующим образом:
location /us1 {
proxy_pass http://$upstrand_us1;
}
Будьте осторожны при доступе к этой переменной из других директив! Это запускает механизм подзапросов, что может быть нежелательно во многих случаях.
Переменные статуса upstrand
Существует несколько доступных переменных статуса upstrand: upstrand_addr, upstrand_cache_status, upstrand_connect_time, upstrand_header_time, upstrand_response_length, upstrand_response_time и upstrand_status. Все они являются аналогами соответствующих переменных upstream и содержат значения последних для всех апстримов, прошедших через запрос и все подзапросы хронологически. Переменная upstrand_path содержит путь всех апстримов, посещённых во время запроса.
Где это может быть полезно
upstrand выглядит очень похоже на простой комбинированный апстрим, но у него есть ключевое отличие: апстримы внутри upstrand не сглаживаются и сохраняют свои идентичности. Это даёт возможность настроить статус failover для группы серверов, связанных с одним апстримом, без необходимости проверять их всех по очереди. В приведённом выше примере upstrand us1 может содержать список апстримов, таких как u01, u02 и т.д. Представьте, что апстрим u01 содержит 10 серверов внутри и представляет собой часть географически распределённой системы бэкенда. Пусть upstrand us1 объединит все такие части в одно целое, и давайте запустим клиентское приложение, которое опрашивает части для выполнения некоторых задач. Пусть бэкенды отправляют HTTP статус 204, если у них нет новых задач. В плоском комбинированном апстриме все 10 серверов могут быть опрошены, прежде чем приложение наконец получит новую задачу от другого апстрима. Upstrand us1 позволяет пропустить к следующему апстриму после проверки первого сервера в апстриме, который не имеет задач. Эта механика, очевидно, подходит для апстримного вещания, когда сообщения отправляются всем апстримам в upstrand.
Примеры выше показывают, что upstrand можно рассматривать как двумерный апстрим, который включает в себя несколько кластеров, представляющих естественные апстримы и позволяет быстро переключаться между ними.
Чтобы проиллюстрировать это, давайте смоделируем апстрим без балансировки по круговому принципу. Каждый новый клиентский запрос будет начинаться с проксирования на первый сервер в списке апстрима, а затем переходить к следующему серверу.
upstream u1 {
server localhost:8020;
server localhost:8030;
combine_server_singlets _single_ nobackup;
}
upstrand us1 {
upstream ~^u1 blacklist_interval=60s;
order per_request;
next_upstream_statuses error timeout non_idempotent 5xx;
intercept_statuses 5xx /Internal/failover;
}
Директива combine_server_singlets в апстриме u1 генерирует два singlet апстрима u1_single_1 и u1_single_2, чтобы населить upstrand us1. Благодаря порядку per_request внутри upstrand, два апстрима будут проходиться в порядке u1_single_1 → u1_single_2 в каждом клиентском запросе.
Директива dynamic_upstrand
Позволяет выбирать upstrand из переданных переменных во время выполнения. Директива может быть установлена в сервере, location и clauses location-if.
В следующей конфигурации
upstrand us1 {
upstream ~^u0;
upstream b01 backup;
order start_random;
next_upstream_statuses 5xx;
}
upstrand us2 {
upstream ~^u0;
upstream b02 backup;
order start_random;
next_upstream_statuses 5xx;
}
server {
listen 8010;
server_name main;
dynamic_upstrand $dus1 $arg_a us2;
location / {
dynamic_upstrand $dus2 $arg_b;
if ($arg_b) {
proxy_pass http://$dus2;
break;
}
proxy_pass http://$dus1;
}
}
upstrands, возвращаемые в переменных dus1 и dus2, должны быть выбраны из значений переменных arg_a и arg_b. Если arg_b установлено, то клиентский запрос будет отправлен в upstrand с именем, равным значению arg_b. Если upstrand с этим именем не существует, то dus2 будет пустым, и proxy_pass вернёт HTTP статус 500. Чтобы предотвратить инициализацию переменной динамического upstrand пустым значением, её объявление должно заканчиваться буквальным именем, соответствующим существующему upstrand. В этом примере переменная динамического upstrand dus1 будет инициализирована upstrand us2, если arg_a пусто или не установлено. В целом, если arg_b не установлено или пусто, и arg_a установлено и имеет значение, равное существующему upstrand, запрос будет отправлен в этот upstrand, в противном случае (если arg_b не установлено или пусто, а arg_a установлено, но не ссылается на существующий upstrand) proxy_pass скорее всего вернёт HTTP статус 500 (если только не существует переменной, составленной из буквальной строки upstrand_ и значения arg_a, указывающей на действительное назначение), в противном случае (если обе arg_b и arg_a не установлены или пусты) запрос будет отправлен в upstrand us2.
См. также
Существует несколько статей о модуле в моём блоге, в хронологическом порядке:
- Простой модуль nginx для создания комбинированных апстримов (на русском). Обширная статья, раскрывающая детали реализации директивы add_upstream, которая также может рассматриваться как небольшое руководство по разработке модулей Nginx.
- nginx upstrand для настройки супер-слоёв апстримов. Обзор использования блока upstrand и некоторые детали его реализации.
- Не такой уж простой модуль nginx для создания комбинированных апстримов (на русском). Обзор всех возможностей модуля с примерами конфигурации и образцами тестовых сессий.
GitHub
Вы можете найти дополнительные советы по конфигурации и документацию для этого модуля в репозитории GitHub для nginx-module-combined-upstreams.