Основы работы с Nginx: проксирование, балансировка нагрузки, буферизация и кэширование

В этом руководстве мы обсудим возможности HTTP-проксирования веб-сервера Nginx, которые позволяют ему передавать запросы на http-серверы бэкэнда для дальнейшей обработки. Nginx часто настраивается как обратный прокси-сервер, который помогает масштабировать инфраструктуру или передавать запросы другим серверам, которые не предназначены для обработки больших клиентских нагрузок.

Вы научитесь масштабировать свою инфраструктуру, используя встроенные функции балансировки нагрузки Nginx. Также вы узнаете, как с помощью буферизации и кеширования улучшить производительность прокси-операций клиентов.

Основы проксирования

Одной из причин использовать проксирование Nginx является возможность масштабирования инфраструктуры. Nginx умеет одновременно управлять несколькими параллельными соединениями. Это делает его идеальным сервером для контакта с клиентами. Сервер может передавать запросы на любое количество бэкэнд-серверов для обработки основного массива трафика, поступающего в вашу инфраструктуру. Это также обеспечивает гибкость при добавлении или замене бэкэнд-серверов по мере необходимости.

Вторя причина настроить HTTP-проксирование – это наличие в инфраструктуре серверов приложений, которые не могут обрабатывать клиентские запросы напрямую в производственных средах. Многие фреймворки предоставляют встроенные веб-серверы, но большинство из них не столь надежны, как высокопроизводительные серверы, такие как Nginx. Использование обратного прокси Nginx может улучшить пользовательский опыт и повысить безопасность.

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

Nginx может проксировать запросы на серверы, которые обмениваются данными с помощью протоколов http(s), FastCGI, SCGI и uwsgi или memcached через отдельные наборы директив для каждого типа проксирования. В этом мануале мы сосредоточимся на протоколе http. Экземпляр Nginx отвечает за передачу запроса и связь с любым компонентом обмена сообщениями в формате, который может понять upstream сервер.

Директива proxy_pass

Самый простой тип проксирования включает в себя передачу запроса на один сервер, который может связываться с помощью http. Этот тип проксирования известен как proxy pass и обрабатывается одноименной директивой proxy_pass.

Директива proxy_pass в основном встречается в контекстах location. Она также поддерживается блоками if в контексте location и limit_except. Когда запрос совпадает с адресом, указанным в proxy_pass, он пересылается по этому URL-адресу.

Рассмотрим такой пример:

# server context
location /match/here {
proxy_pass http://example.com;
}
. . .

В приведенном выше фрагменте конфигурации в конце блока server в определении proxy_pass не указывается URI. Для определений, соответствующих этому шаблону, запрошенный клиентом URI будет передан на upstream сервер без изменений.

Например, когда этот блок обрабатывает запрос /match/here/please, URI запроса будет отправлен на сервер example.com как http://example.com/match/here/please.

Рассмотрим альтернативный сценарий:

# server context
location /match/here {
proxy_pass http://example.com/new/prefix;
}
. . .

В приведенном выше примере прокси-сервер определяется вместе с сегментом URI в конце (/new/prefix). Когда в определении proxy_pass указывается URI, то часть запроса, которая соответствует определению location, заменяется этим URI.

К примеру, запрос /match/here/please будет передаваться на upstream сервер как http://example.com/new/prefix/please. Префикс /match/here заменяется на /new/prefix. Об этом важно помнить.

Иногда такая замена невозможна. В этих случаях URI в конце определения proxy_pass игнорируется, и на upstream сервер передается исходный URI клиента или URI, измененный другими директивами.

Например, при использованием регулярных выражений Nginx не может определить, какая часть URI соответствует выражению, поэтому он отправляет исходный URI-запрос клиента. Или, например, если директива rewrite используется в одном и том же location, она переписывает URI клиента, но он все же обрабатывается в одном блоке. В этом случае будет передан переписанный URI.

Обработка заголовков в Nginx

Чтобы upstream сервер обработал запрос должным образом, одного URI недостаточно. Запрос, поступающий от имени клиента через Nginx, будет выглядеть иначе, чем запрос, поступающий непосредственно от клиента. Большая часть этого – заголовки, которые согласуются с запросом.

Когда Nginx проксирует запрос, он автоматически вносит некоторые поправки в заголовки, полученные от клиента.

  • Nginx избавляется ото всех пустых заголовков. Нет смысла передавать пустые значения другому серверу; это только усложнит передачу запроса.
  • Все заголовки, которые содержат символы подчеркивания, Nginx по умолчанию рассматривает как недопустимые. Он удалит их из запроса. Если вы хотите, чтобы Nginx интерпретировал их как валидные, вы можете установить в директиве underscores_in_headers значение on, в противном случае такие заголовки никогда не попадут на бэкэнд-сервер.
  • Заголовок Host переписывается значением, определяемым переменной $proxy_host. Это может быть IP-адрес или имя и номер порта upstream сервера, как указано в директиве proxy_pass.
  • Заголовок Connection заменяется значением close. Этот заголовок используется для передачи информации о конкретном соединении, установленном между двумя сторонами. В этом случае Nginx устанавливает это значение, чтобы указать upstream серверу, что это соединение будет закрыто после ответа на исходный запрос. Не следует ожидать, что это upstream соединение будет постоянным.

Первый вывод, который можно сделать из вышеизложенной информации: если вы не хотите передавать тот или иной заголовок, нужно задать ему значение пустой строки. Заголовки с такими значениями полностью удаляются из переданного запроса.

Также следует убедиться, что в нестандартных заголовках нет подчеркиваний, что если ваше бэкэнд-приложение будет обрабатывать такие заголовки. Если вам нужны заголовки, в которых используется символ подчеркивания, вы можете установить директиве underscores_in_headers значение on (это валидно либо в контексте http, либо в контексте объявления server по умолчанию для комбинации IP-адреса/порта). Если вы этого не сделаете, Nginx пометит эти заголовки как некорректные и просто сбросит их, прежде чем перейти к upstream серверу.

Заголовок Host имеет особое значение в большинстве прокси-сценариев. Как указано выше, по умолчанию этот заголовок получит значение переменной $proxy_host, которая содержит домен или IP-адрес и порт, взятые непосредственно из определения proxy_pass. Это поведение определяется по умолчанию, так как это единственный адрес, на который точно отвечает upstream сервер Nginx.

Заголовок Host часто имеет такие значения:

  • $proxy_host: устанавливает в Host домен или IP-адрес и порт из определения proxy_pass. Это значение по умолчанию надежно с точки зрения Nginx, но оно не всегда подходит для правильной обработки запроса.
  • $http_host: устанавливает в Host заголовок Host из клиентского запроса. Заголовки, отправленные клиентом, всегда доступны Nginx в качестве переменных. Переменные начинаются с префикса $http_, после которого устанавливается имя заголовка в нижнем регистре, а все тире заменяются нижним подчеркиванием. Помните, что переменная $http_host не сработает, если в запросе клиента нет валидного заголовка Host.
  • $host: эта переменная может принимать в качестве значений имя хоста из запроса, заголовок host из клиентского запроса или имя сервера соответствующего запроса.

В большинстве случаев нужно установить в заголовке Host переменную $host. Это наиболее гибкий вариант, который обычно обеспечивает точное заполнение заголовка.

Настройка или сброс заголовков

Чтобы настроить или установить заголовки для прокси-соединений, можно использовать директиву proxy_set_header. Например, чтобы изменить заголовок Host и добавить дополнительные заголовки, нужно использовать что-то вроде этого:

# server context
location /match/here {
proxy_set_header HOST $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://example.com/new/prefix;
}
. . .

Здесь заголовок Host получит значение переменной $host, в которой должна содержаться информация о запрошенном исходном хосте. Заголовок X-Forwarded-Proto предоставляет прокси-серверу информацию о схеме исходного запроса клиента (будь то http или https-запрос).

X-Real-IP указывает IP-адрес клиента, чтобы прокси-сервер мог правильно принимать решения или вести лог на основе этой информации. Заголовок X-Forwarded-For – это список, содержащий IP-адрес каждого сервера, по которому проходил запрос. В приведенном выше примере устанавливается значение переменной $proxy_add_x_forwarded_for. Эта переменная принимает значение исходного X-Forwarded-Forheader, извлеченного из клиента, и добавляет IP-адрес сервера Nginx в конец.

Конечно, директиву proxy_set_header стоит переместить в контекст server или http, чтоб иметь возможность ссылаться на нее:

# server context
proxy_set_header HOST $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location /match/here {
proxy_pass http://example.com/new/prefix;
}
location /different/match {
proxy_pass http://example.com;
}

Раздел Upstream для балансировки нагрузки проксируемых соединений

В предыдущих примерах вы увидели, как сделать настроить простое HTTP-прокси соединение на одном сервере. Nginx позволяет легко масштабировать эту конфигурацию, указав целые пулы бэкэнд-серверов, на которые можно передавать запросы.

Это можно сделать с помощью директивы upstream, которая позволяет определить пул серверов. Эта конфигурация предполагает, что любой из перечисленных серверов способен обрабатывать запрос клиента. Это позволяет масштабировать инфраструктуру практически без усилий. Директива upstream должна быть установлена в контексте http конфигурации Nginx.

Рассмотрим простой пример:

# http context
upstream backend_hosts {
server host1.example.com;
server host2.example.com;
server host3.example.com;
}
server {
listen 80;
server_name example.com;
location /proxy-me {
proxy_pass http://backend_hosts;
}
}

В приведенном выше примере был создан контекст upstream под названием backend_hosts. После определения это имя будет доступно в proxy pass как обычный домен. Как вы можете видеть, в блоке server все запросы, сделанные в example.com/proxy-me/…, передаются в пул, который вы определили выше. В этом пуле хост выбирается с помощью настраиваемого алгоритма. По умолчанию это простой процесс round-robin (каждый запрос будет поочередно маршрутизироваться на другой хост).

Изменение алгоритма балансировки в контексте upstream

Настроить алгоритм в пуле upstream можно с помощью таких флагов и директив:

  • round robin: Алгоритм балансировки нагрузки по умолчанию, который используется, если не указано других директив. Каждый запрос будет последовательно передаваться серверам, определенным в контексте upstream.
  • least_conn: Указывает, что новые соединения всегда должны быть привязаны к бэкэнду, который имеет наименьшее количество активных соединений. Это может быть особенно полезно в ситуациях, когда соединения с бэкэндом могут сохраняться в течение некоторого времени.
  • ip_hash: Этот алгоритм балансировки распределяет запросы между серверами на основе IP-адреса клиента. Первые три октета используются в качестве ключа, на основе которого выбирается сервер для обработки запроса. В результате клиенты, как правило, обслуживаются одним и тем же сервером при каждом подключении, что обеспечивает согласованность сеансов.
  • hash: Этот алгоритм балансировки в основном используется с прокси-сервером memcached. Серверы делятся на группы на основе значения произвольно предоставленного хэш-ключа. Ключ может быть текстом, переменной или разными комбинациями. Это единственный метод балансировки, который требует от пользователя предоставить данные (ключ).

При изменении алгоритма блок может выглядеть так:

# http context
upstream backend_hosts {
least_conn;
server host1.example.com;
server host2.example.com;
server host3.example.com;
}
. . .

В приведенном выше примере сервер будет выбран по наименьшему количеству соединений. Можно также добавить директиву ip_hash, чтобы обеспечить «липкость» сессии.

Что касается метода hash, вы должны указать ключ для хэша. Это может быть что угодно:

# http context
upstream backend_hosts {
hash $remote_addr$remote_port consistent;
server host1.example.com;
server host2.example.com;
server host3.example.com;
}
. . .

В приведенном выше примере запросы будут распределяться на основе значений IP-адреса и порта клиента. Также здесь есть опциональный параметр consistent, который реализует алгоритм хэширования ketama consistent. Это означает, что если upstream серверы изменятся, это будет иметь минимальное воздействие на кэш.

Установка веса сервера для балансировки

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

# http context
upstream backend_hosts {
server host1.example.com weight=3;
server host2.example.com;
server host3.example.com;
}
. . .

Теперь host1.example.com будет получать в три раза больше трафика, чем другие два сервера. Вес каждого сервера по умолчанию равен 1.

Использование буферов для освобождения бэкэнд-серверов

Один из главных вопросов при проксировании – насколько изменится скорость работы при добавлении сервера. Увеличение или уменьшение количества серверов можно значительно смягчить с помощью системы буферизации и кэширования Nginx.

При проксировании на другой сервер на опыт клиента влияет скорость двух разных подключений:

  • Подключения клиента к прокси Nginx.
  • И подключения прокси-сервера Nginx к серверу.

Nginx имеет возможность корректировать свое поведение на основе того, какое из этих соединений вы хотите оптимизировать.

Без буферов данные с прокси-сервера сразу же отправляются к клиенту. Если клиентские соединения быстрые, буферизацию можно отключить, чтобы клиент как можно скорее мог получить данные. При использовании буферов прокси-сервер Nginx будет временно хранить ответ бэкэнда, а затем передавать эти данные клиенту. Если клиент работает медленно, это позволит серверу Nginx быстрее закрыть соединение с бэкэндом. Затем он сможет обрабатывать передачу данных клиенту любым возможным способом.

Nginx по умолчанию использует буферизацию, так как скорость соединения, как правило, меняется в зависимости от клиента. Буферизация настраивается с помощью следующих директив. Их можно установить в контексте http, server или location. Важно иметь в виду, что директивы size касаются каждого запроса, поэтому они могут повлиять на производительность серверов при поступлении множества клиентских запросов.

  • proxy_buffering: эта директива определяет, включена ли буферизация для этого контекста и его дочерних контекстов. По умолчанию имеет значение on.
  • proxy_buffers: Эта директива контролирует количество (первый аргумент) и размер (второй аргумент) буферов. По умолчанию используется 8 буферов, размер которых равен одной странице памяти (либо 4k, либо 8k). Увеличение количества буферов позволяет буферизовать дополнительную информацию.
  • proxy_buffer_size: Исходная часть ответа от бэкэнд-сервера, которая содержит заголовки, буферизуется отдельно от остальных данных. Эта директива устанавливает размер буфера для этой части ответа. По умолчанию она будет того же размера, что и proxy_buffers, но поскольку здесь хранятся только заголовки, ее можно уменьшить.
  • proxy_busy_buffers_size: Эта директива устанавливает максимальное количество занятых буферов. Хотя клиент может считывать данные только из одного буфера за раз, буферы помещаются в очередь для отправки фрагментов данных клиенту. Эта директива управляет размером буферного пространства, которое может находиться в этом состоянии.
  • proxy_max_temp_file_size: Это максимальный размер временного файла каждого запроса на диске. Они создаются, если ответ бэкэнда слишком велик и не помещается в буфер.
  • proxy_temp_file_write_size: Это количество данных, которые Nginx будет записывать во временный файл за один раз, если ответ прокси-сервера слишком велик и не помещается в буфер.
  • proxy_temp_path: Это путь к области на диске, где Nginx должен хранить временные файлы, когда ответ upstream сервера не помещается в буфер.

Как вы можете видеть, Nginx предоставляет довольно много разных директив для настройки поведения буферизации. В большинстве случаев вам не придется использовать их, но некоторые из этих значений могут пригодиться. Возможно, наиболее полезными являются proxy_buffers и proxy_buffer_size.

В этом примере увеличивается количество доступных буферов для обработки запросов и уменьшается размер буфера для хранения заголовков:

# server context
proxy_buffering on;
proxy_buffer_size 1k;
proxy_buffers 24 4k;
proxy_busy_buffers_size 8k;
proxy_max_temp_file_size 2048m;
proxy_temp_file_write_size 32k;
location / {
proxy_pass http://example.com;
}

Если у вас есть быстрые клиенты, которым нужно быстро отправить данные, можно полностью отключить буферизацию. Nginx будет по-прежнему использовать буферы, если сервер upstream быстрее, чем клиент, но он попытается немедленно передать данные клиенту. Если клиент работает медленно, это может привести к тому, что upstream соединение останется открытым до тех пор, пока клиент не сможет получить данные. Когда буферизация выключена, будет использоваться только буфер, определенный директивой proxy_buffer_size:

# server context
proxy_buffering off;
proxy_buffer_size 4k;
location / {
proxy_pass http://example.com;
}

Высокая доступность (опционально)

Проксирование Nginx можно сделать более надежным, добавив избыточный набор балансировщиков нагрузки, чтобы создать инфраструктуру высокой доступности.

Настройка высокой доступности – это инфраструктура без единой точки отказа, и балансировщики нагрузки являются частью этой конфигурации. Имея несколько балансировщиков нагрузки, вы сможете предотвратить простои, если один из балансировщиков станет недоступен.

Кэширование и снижение времени ответа

Буферизация помогает освободить сервер бэкэнда для обработки большего количества запросов, но Nginx также может кэшировать контент с бэкэнд-серверов, устраняя необходимость подключения к upstream серверу для обработки запросов.

Настойка прокси-кэша

Для настройки кэширования ответов бэкэнд серверов можно использовать директиву proxy_cache_path, которая определяет пространство для хранения кэша. Её следует задавать в контексте http.

В приведенном ниже примере показано, как использовать эту и некоторые другие директивы для настройки системы кэширования.

# http context
proxy_cache_path /var/lib/nginx/cache levels=1:2 keys_zone=backcache:8m max_size=50m;
proxy_cache_key "$scheme$request_method$host$request_uri$is_args$args";
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;

Директива proxy_cache_path определяет каталог в файловой системе, где нужно хранить кэш. В этом примере это каталог /var/lib/nginx/cache. Если этот каталог не существует, вы можете создать его и определить права доступа к нему:

sudo mkdir -p /var/lib/nginx/cache
sudo chown www-data /var/lib/nginx/cache
sudo chmod 700 /var/lib/nginx/cache

Параметр levels= указывает, как будет организован кэш. Nginx создаст ключ кеша путем хэширования значения ключа (он настраивается ниже). В результате будет создан каталог, имя которого состоит из одного символа (это будет последний символ хешированного значения), и подкаталог с именем из двух символов (следующие два символа в конце хэша). Это помогает Nginx быстро найти соответствующие значения.

Параметр keys_zone= определяет имя зоны кеша (backcache). Здесь также указывается, сколько метаданных можно хранить. В этом случае сервер будет хранить 8 МБ ключей. На 1 мегабайте Nginx может хранить около 8000 записей. Параметр max_size устанавливает максимальный размер кэшированных данных.

Директива proxy_cache_key устанавливает ключ, который будет использоваться для хранения кешированных значений. Этот же ключ используется для проверки того, можно ли запросить данные из кеша. Здесь используется комбинация схемы (http или https), метода HTTP-запроса, а также запрошенного хоста и URI.

Директива proxy_cache_valid может быть указана несколько раз. Она позволяет определить, как долго должны храниться значения в зависимости от кода состояния. В данном примере удачные и переадресованные ответы хранятся в течение 10 минут, а ответы 404 удаляются каждую минуту.

Теперь зона кэширования настроена, но пока что Nginx не знает, когда именно применять кеширование.

Эта информация указывается в контексте location для бекэнд серверов:

# server context
location /proxy-me {
proxy_cache backcache;
proxy_cache_bypass $http_cache_control;
add_header X-Proxy-Cache $upstream_cache_status;
proxy_pass http://backend;
}
. . .

Используя директиву proxy_cache, можно указать, что для этого контекста следует использовать зону кэширования backcache. Nginx проверит запись перед тем, как перейти к серверу.

Директива proxy_chache_bypass принимает значение переменной $http_cache_control. Эта переменная сообщает, запросил ли клиент свежий, не кэшированный ответ. При использовании этой директивы Nginx сможет корректно обрабатывать запросы такого типа. Никаких дальнейших настроек не для этого не требуется.

Мы также добавили дополнительный заголовок X-Proxy-Cache. Этот заголовок принимает значение переменной $upstream_cache_status. Это позволяет увидеть, обработан ли запрос из кэша, этих данных не было в кэше или клиент запросил новый ответ. Это особенно полезно для отладки.

Рекомендации по кэшированию результатов

Кеширование увеличивает скорость прокси-сервера. Но не стоит забывать о нескольких нюансах.

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

Если на сайте есть динамические элементы, им следует уделить внимание. Решение этой проблемы зависит от бекэнд-сервера. Для личных данных используется заголовок Cache-Control со значением no-cache, no-store или private.

  • no-cache: ответ не будет отправлен, пока сервер не проверит данные на бекэнд-сервере. Это значение используется с динамическими данными. Хэшированные метаданные заголовка Etag проверяются при каждом запросе. Если бекэнд вернет теже значения, то данные отправляются из кэша клиенту.
  • no-store: данные не должны храниться в кэше ни при каких обстоятельствах. Это самый безопасный подход при работе с личными данными.
  • private: данные не должны кэшироваться в общем кэше. То есть, к примеру, браузер пользователя может кешировать данные, а прокси-сервер нет.
  • public: данные можно кэшировать везде.

Есть связанный с этим поведением заголовок max-age, который определяет срок хранения кэша в секундах.

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

Если вы используете nginx и на бэкэнде, добавьте директиву expires, которая определяет значение max-age заголовка Cache-Control:

location / {
expires 60m;
}
location /check-me {
expires -1;
}

Первый блок поддерживает кэш в течение часа. Второй блок присваивает заголовку Cache-Control значение no-cache. Для внесения других изменений примените директиву add_header:

location /private {
expires -1;
add_header Cache-Control "no-store";
}

Tags: