NGINX: HTTP-прокси, Load Balancing, буферы и кеширование

Автор: | 03/24/2016
 

nginx_logoNGINX часто используется в роли реверс-прокси для облегчения задач масштабируемости инфрастуктуры или для проксирования запросов сервисам, которые сами не могут обработать большое количество клиентов.

Оригинал и полная версия — тут>>>.

General Proxying Information

Проксирование в NGINX заключается в обработке запросов, которые получает NGINX и передача их другим серверам для обработки. Результат обработки передается обратно NGINX-у, который возвращает его клиенту. Сервера, которым NGINX передает запросы, могут быть как локальными сервисами так и удаленными серверами, и в контексте NGINX-а называются «upstreams«.

NGINX может проксировать запросы по HTTP(S), FastCGI, SCGI, uWSGI, Memcached, используя различные директивы для каждого типа прокси. В данном посте будет в основном рассматриваться HTTP.

HTTP Proxy Pass

Наиболее простой тип прокси заключается в пркссировании запросов к одному серверу по HTTP. Тако тип проксирования называется «proxy pass» и описывается директивой proxy_pass.

Как правило — proxy_pass используется в контексте location. Допустимо его указание в блоках if в location и в контексте limit_except.

Когда запрос совпадает с location, в котором указан proxy_pass — он перенаправляется на заданный в директиве URL.

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

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

В этом примере для proxy_pass не указан URI для передачи запроса, поэтому все запросы, попавшие под данный location будут переданы upstream-серверу (или «бекенду«) в том виде, в каком они придут самому NGINX-у.

Например, запрос вида «/match/here/please» будет перенаправлен серверу example.com в виде http://example.com/match/here/please.

Давайте рассмотрим еще один пример:

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

Тут бекенду example.com указывается URI  в виде /new/prefix. Когда NGINX получит запрос, попадающий под действие данного location — часть запроса, которая соответсвует определению location (/match/here в данном примере) будет заменена на URI, указанный для прокси.

Например, запрос вида /match/here/please к самому NGINX-у будет передан бекенду в виде http://example.com/new/prefix/please: часть запроса /match/here меняется на /new/prefix.

Заголовки и Nginx

Еще один немаловаждный момент, который необходимо учитывать для того, что бы ваш бекенд корректно обрабатывал запросы от фронтеда с NGINX — это то, что передавать требуется не только сам URI.

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

Когда NGINX проксирует запрос от клиента к бекенду — он вносит некоторые корректировки в его заголовки:

  • Удаляются любые пустые заголовки, т.к. нет никакого смысла в их передаче — это только увеличивает размер запроса.
  • По умолчанию — все заголовки со знаком нижнего подчеркивания считаются некорректными и удаляются из запроса. Если требуется их использовать — добавьте директиву underscores_in_headers со значением «on«.
  • Заголовок «Host» перезаписывается значением переменной $proxy_host, которая содержит IP или имя и порт бекенда, как он указан в директиве proxy_pass.
  • Заголовок «Connection» будет изменен на «close«. «Connection» используется передачи информации о состоянии каждого соединения. NGINX устанавливает значение «close«, что бы сообщить бекенду о  том, что данное соединение будет закрыто после отправки ответа — бекенд не должен ожидать, что данное соединение будет постоянным. Почитать можно тут>>> и тут>>>.

Заголовок «Host» имеет особое значение при проксировании запросов. По умолчанию NGINX устанавливает его раным значению переменной $proxy_host, которая содержит доменное имя, IP адрес и/или порт бекенда. Такое значение используется по умолчанию, т.к. это единственные данные, используя которые NGINX может быть уверен, что получит ответ от бекенда — так как он получает эти данные напрямую из настроек проксирования.

Наиболее используемые значения для «Host» бывают следующие:

  • $proxy_host: значение по умолчанию — имя или IP бекенда из директивы proxy_pass. Хотя это значение считается наиболее приемлимым с точки зрения самого NGINX — оно может быть не подходящим для самого бекенда для корректной обработки запроса.
  • $http_host: устанавливает значение «Host» равным значению заголовка «Host» из запроса клиента. Заголовки, полученные от клиента всегда доступны NGINX-у и начинаются с префикса $http_, за которым следует имя заголовка строчными буквами и заменой тире на нижнее подчеркивание. Хотя значение переменной $http_host является подходящим в большинстве случаев — иногда запрос клиента может не содержать его вообще, что приведет к проблемам при проксировании.
  • $host: может содержать имя хоста из Request-Line запроса, значение заголовка «Host» из запроса клиента или имя сервера или имя сервера, который обрабатывает запрос, если заголовок «Host» недоступен.

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

Добавление и изменение заголовков

Для изменения или добавления заголовков при проксировании соединений можно использовать директиву proxy_set_header.

Например, для изменения заголовка «Host» и добавления некоторых других заголовков, которые часто используются при проксировании, вы можете использовать такую конфигурацию:

...
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 (XFF) — передает информацию об IP адресах всех серверов, через которые проходит соединение до настоящего момента. В примере выше тут используется значение переменной $proxy_add_x_forwarded_for, которое формируется из предыдущего значения X-Forwarded-For, полученного от клиента, к которому добавляется IP сервера с самим NGINX-ом.

proxy_set_header можно разместить как в контексте location — так и выше, в блоке server {} или http {}, что позволит использовать их для всех location:

server {
    ...
    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(S) запросов к одному бекенд-серверу.

Кроме этого — с помощью  директивы upstream NGINX позволяет создавать пул таких серверов, что позволяет легко управлять масштабируемостью проекта.

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

Директива upstream должна быть указана в блоке http {}, например:

http {
...
    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 как обычное доменное имя.

В данном примере мы проксируем все запросы к http://example.com/proxy-me/* к пулу серверов. Внутри самого пула NGINX выбирает один из серверов, в зависимости его настроек. По умолчанию NGINX использует механизм «round-robin» — каждый новый запрос передается следующему серверу в списке пула.

Changing the Upstream Balancing Algorithm

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

  • round-robin: механизм, используемый по умолчанию. Каждый запрос переадресовывается следующему бекенд серверу в списке пула, пока не будет достигнут последний из них, после чего будет выбран первый сервер из пула и круг начнется сначала.
  • least_conn: определяет, что новый запрос должен быть направлен к тому бекенду, который в настоящий момент имеет наименьшее количество активных соединений. Бывает особенно полезно в случаях, когда некоторые соединения с бекендом активны в течении относительно длительного периода времени.
  • ip_hash: механизм, распределяющий запросы по разным бекендам, основываясь на IP адресе клиента. Первые три октета адреса клиента используются как ключ для определения бекенда, которому будет направлен запрос. Данная реализация позволяет стремиться к обаработке запросов клиента на одном и том же бекенде.
  • hash: как правило используется при проксированнии через Memcached — бекенд для обработки запроса выбирается на основе данных, полученных от клиента. Это может быть текст, значение переменной или их комбинация. Позволяет поддерживать целостность сессии клиента, обрабатывая его запросы на одном и том же сервере, пока он в состоянии его обработать.

Переопределить механизм балансировщика можно так:

...
upstream backend_hosts {

    least_conn;

    server host1.example.com;
    server host2.example.com;
    server host3.example.com;
}
...

Тут будет выбран бекенд, у которого в настоящий момент наименьшее количество активных соединений. Точно так же можно указать ip_hash.

А определение hash могло бы выглядеть так:

...
upstream backend_hosts {

    hash $remote_addr$remote_port consistent;

    server host1.example.com;
    server host2.example.com;
    server host3.example.com;
}
...

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

Setting Server Weight for Balancing

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

Однако — вы моежет переопределить это поведение, изменив вес бекенда/ов, например:

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

В этом случае сервер host1.example.com будет получать в три раза больше запросов, что второй и третий. По умолчанию — каждый бекенд имеет weight=1.

Буферы в NGINX

Для улучшения производительности при использовании дополнительных серверов — NGINX поддерживает буферизацию (proxy_buffering) и кеширование (proxy_cache).

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

  1. от клиента к прокси-серверу NGINX;
  2. от прокси-сервера к бекенду.

NGINX имеет возможность оптимизации для обоих соединений.

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

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

По умолчанию буферизцаия включена и NGINX предоставляет несколько директив для ее настройки. Эти директивы могут располагаться в блоках server {} или http {}, или location. Важно учитывать факт того, что размеры в директивах применяются к каждому запросу и увеличение значений для них сверх необходимого может повлиять на производительность всего сервера, если таких запросов будет много.

Наиболее используемые директивы буферизации:

  • proxy_buffering: включение или отключение буферизации для контекста и его дочерних контекстов. По умолчанию «on«.
  • proxy_buffers: определяет количество (первый агрумент) и размер (второй аргумент) буферов для проксированных ответов. По умолчанию используется 8 буферов, каждый размером в одну страницу памяти (4 или 8 кб, в зависимости от системы). Увеличение количества буферов позволяет вместить больше информации.
  • proxy_buffer_size: первоначальная часть ответа от бекенда клиенту, которая включает в себя заголовки, буферизируется отдельно от остальной части ответа. Эта директива позволяет менять размер буфера для этой части. По умолчанию его значение равно директиве proxy_buffers, но так как он используется для передачи заголовков — тут можно указать меньшее значение.
  • proxy_busy_buffers_size:  определяет максимальный размер буферов, которые могут быть отмечены как «client-ready«, т.е. «заняты». Так как клиент может в один момент времени получать только один буфер — буферы располагаются группой в очереди для передачи их клиенту. Эта директива определяет общий размер буферов, которые могут быть в данном состоянии.
  • proxy_max_temp_file_size: максимальный размер временного файла на диске на один запрос. Такие файлы создаются в том случае, если размер ответа слишком большой и не помещается в буфер.
  • proxy_temp_file_write_size: размер данных, которые будут записываться NGINX-ом во временный файл, когда размер ответа слишком велик для буфера.
  • proxy_temp_path: путь к разделу или директории, где NGINX должен хранить временные файлы, когда ответ бекенда слишком большой для буферизации.

Как правило — вы можете не волноваться по поводу всех этих директив, однако может оказатсья весьма полезным изменить значение некоторых из них. Наиболее полезными являются директивы proxy_buffers и proxy_buffer_size.

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

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

Если клиент будет принимать данные слишком медленно — отключение буферизации может привести к простою бекенда, так как он будет вынужден держать открытым соденине до тех пор, пока не завершится передача данных.

При отключении буферизации — будет применяться только директива proxy_buffer_size.

Например:

...
proxy_buffering off;
proxy_buffer_size 4k;

location / {
    proxy_pass http://example.com;
}
...

High Availability

Для создания более устойчивой инфрастуктуры вы так же можете создать несколько балансировщиков нагрузки из нескольких серверов NGINX.

Такой подход — High Availability (HA) представляет собой отказоустойчивую инфрастуктуру без единой «точки входа». Для этого используется несколько серверов в роли балансировщика, каждый из которых может заменить любой другой, вышедший из строя, балансировщик.

Замечательная гифка, наглядно отображающая подобный подход:

ha-diagram-animated

NGINX proxy и кеширование

Для кеширования проксируемого контента можно использовать директиву proxy_cache_path, которая указывает на каталог или раздел для хранения закешированных данных, полученных от бекендов. proxy_cache_path должна располагаться в контексте http {}.

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

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

Параметр levels определяет то, как кеш будет организован. NGINX будет создавать ключ кеша хешируя значения, указанные в директиве proxy_cache_key. «Уровни», которые мы указали тут указывают на создание первого каталога внутри /var/lib/nginx/cache с именем из одного символа, который берется из последнего символа хеша, и двухсимволного каталога внутри него, имя для которого представляет собой два предыдущих символа.

На практике это выглядит так:

# tree /var/lib/nginx/cache/
/var/lib/nginx/cache/
└── b
    └── 35
        └── 027f860f1a0eb88523bb95b2cd6d035b

Параметр key_zone указывает на имя этой зоны кеширования, в данном случае — мы его назвали backcache. Так же тут определяется как много метаданных хранить в ней, тут используется 8МБ под ключи. В каждом мегабайте NGINX может хранить 8000 записей. Параметр max_size определяет максимальное значение для всей закешированной информации.

Следующая директива, которую мы тут использовали — proxy_cache_key. Она используется для создания ключадля кеширвоания информации. Этот же ключ используется для проверки того, может ли запрос быть обработан из кеша или нет. Тут ключ создается из значений типа протокола (HTTP, HTTPS), тип HTTP запроса а так же запрошенного хоста и URI.

Директива proxy_cache_valid позволяет настроить время хранения данных в кеше, в зависимости от статуса. Например, мы указали что данные с кодами 200 (OK) и 301 (redirect) будут храниться 10 минут, а данные с кодом 404 — очищать каждую минуту.

Теперь, когда мы настроили зону кеширования, нам необходимо указать NGINX-у когда его применять.

В контекстах location, которые используют проксирование, мы можем добавить такие настройки:

...
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 мы указали, что для контекста /proxy-me необходимо использовать зону кеширования «backcache«.

Директива proxy_cache_bypass установлена равной значению переменной $http_cache_control, которая содержит информацию о том, запрашивает ли клиент некешированную информацию.

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

Notes about Caching Results

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

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

Если же ваш сайт включает в себя динамические элементы с приватными данными — вы можете установить заголовок Cache-Control со значением «no-cache«, «no-store» или «private«, в заисимости от типа ваших данных:

  • no-cache: указывает на то, что ответ не должен быть передан клиенту без проверки обновления данных на бекенде.
  • 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";
}
...

Ссылки  по теме

Load Balancing with NGINX and NGINX Plus, Part 1
Alphabetical index of variables
How To Set Up Nginx Load Balancing with SSL Termination
HTTP caching with Nginx and Memcached
NGINX proxy buffering
NGINX LOAD BALANCING – HTTP LOAD BALANCER
A Guide to Caching with NGINX