- Описание
- Архитектура
- Установка
- Конфигурация
- Обратное проксирование и FastCGI
- Статика
- Failover и балансировка
- Кеширование
- Оптимизация nginx
- Пример конфигурации
- Прочее
- Полезные ссылки
nginx (Engine X) — веб-сервер и почтовый прокси-сервер, работающий на Unix-подобных операционных системах.
nginx позиционируется производителем как простой, быстрый и надежный сервер, не перегруженный функциями.
Применение nginx целесообразно прежде всего для статических веб-сайтов и как обратного прокси-сервера перед динамическими сайтами.
Основные функции:
- HTTP-сервер
- Обслуживание неизменяемых запросов, индексных файлов, автоматическое создание списка файлов, кеш дескрипторов открытых файлов
- Акселерированное проксирование без кэширования, простое распределение нагрузки и отказоустойчивость
- Поддержка кеширования при акселерированном проксировании и FastCGI
- Акселерированная поддержка FastCGI и memcached серверов, простое распределение нагрузки и отказоустойчивость
- Модульность, фильтры, в том числе сжатие (gzip), byte-ranges (докачка), chunked ответы, HTTP-аутентификация, SSI-фильтр
- Несколько подзапросов на одной странице, обрабатываемые в SSI-фильтре через прокси или FastCGI, выполняются параллельно
- Поддержка SSL
- Поддержка PSGI, WSGI
- Экспериментальная поддержка встроенного Perl
- SMTP/IMAP/POP3-прокси сервер
- Перенаправление пользователя на SMTP/IMAP/POP3-бэкенд с использованием внешнего HTTP-сервера аутентификации
- Простая аутентификация (LOGIN, USER/PASS)
- Поддержка SSL и STARTTLS
В nginx рабочие процессы обслуживают одновременно множество соединений, мультиплексируя их вызовами операционной системы select
, epoll
(Linux) и kqueue
(FreeBSD). Рабочие процессы выполняют цикл обработки событий от дескрипторов. Полученные от клиента данные разбираются с помощью конечного автомата. Разобранный запрос последовательно обрабатывается цепочкой модулей, задаваемой конфигурацией. Ответ клиенту формируется в буферах, которые хранят данные либо в памяти, либо указывают на отрезок файла. Буферы объединяются в цепочки, определяющие последовательность, в которой данные будут переданы клиенту. Если операционная система поддерживает эффективные операции ввода-вывода, такие как writev
и sendfile
, то nginx применяет их по возможности.
Алгоритм работы HTTP сервера выглядит следующим образом:
- получить очередной дескриптор из
kevent
- прочитать данные из файла и записать в socket, используя либо
write(2)
/read(2)
, либо используя системный вызовsendfile(2)
, выполняющий те же действия, но в пространстве ядра - перейти к шагу 1
Конфигурация HTTP-сервера nginx разделяется на виртуальные серверы (директива server
). Виртуальные серверы разделяются на location’ы (location
). Для виртуального сервера возможно задать адреса и порты, на которых будут приниматься соединения, а также имена, которые могут включать *
для обозначения произвольной последовательности в первой и последней части, либо задаваться регулярным выражением.
location’ы могут задаваться точным URI, частью URI, либо регулярным выражением. location’ы могут быть сконфигурированы для обслуживания запросов из статического файла, проксирования на fastcgi / memcached сервер.
Для эффективного управления памятью nginx использует пулы. Пул — это последовательность предварительно выделенных блоков динамической памяти. Длина блока варьируется от 1 до 16 килобайт. Изначально под пул выделяется только один блок. Блок разделяется на занятую область и незанятую. Выделение мелких объектов выполняется путем продвижения указателя на незанятую область с учетом выравнивания. Если незанятой области во всех блоках не хватает для выделения нового объекта, то выделяется новый блок. Если размер выделяемого объекта превышает значение константы NGX_MAX_ALLOC_FROM_POOL
либо длину блока, то он полностью выделяется из кучи.
Таким образом, мелкие объекты выделяются очень быстро и имеют накладные расходы только на выравнивание.
nginx содержит модуль географической классификации клиентов по IP-адресу. В его основу входит база данных соответствия IP-адресов географическому региону, представленная в виде radix tree (сжатое префиксное дерево или сжатый лес) в оперативной памяти. nginx предварительно распределяет первые несколько уровней дерева, таким образом, чтобы они занимали ровно 1 страницу памяти. Это гарантирует, что при поиске IP-адреса для первых нескольких узлов при трансляции адреса всегда найдется запись в TLB.
# Ubuntu
sudo apt-get update
sudo apt-get install nginx
# macOS
brew install nginx
Конфигурационный файл nginx очень удобен и интуитивно понятен. Называется он обычно nginx.conf
и располагается в etc/nginx/
если расположение не было переопределено при компиляции.
Структура конфигурационного файла:
# Имя пользователя, с правами которого будет запускаться nginx
user nginx;
# Количество рабочих процессов
worker_processes 1;
events {
<...> # в этом блоке указывается механизм поллинга который будет использоваться
# и максимальное количество возможных подключений
}
http {
<глобальные директивы http-сервера, например настройки таймаутов и т.п.>;
<почти все из них можно переопределить для отдельного виртуального хоста или локейшена>;
# Описание серверов (это то что в apache называется VirtualHost)
server {
# Адрес и имя сервера
listen *:80;
server_name aaa.bbb;
<Директивы сервера. Здесь обычно указывают расположение докуменов (root),
редиректы и переопределяют глобальные настройки>;
# Вот так можно определить location, для которого можно также переопределить
# практически все директивы указаные на более глобальных уровнях
location /abcd/ {
<директивы>;
}
# Кроме того, можно сделать location по регулярному выражению, например так:
location ~ \.php$ {
<директивы>;
}
}
# другой сервер
server {
listen *:80;
server_name ccc.bbb;
<директивы>
}
}
Стоит обратить внимание на то, что каждая директива должна оканчиваться точкой с запятой.
Реализация обратного проксирования в nginx:
location / {
proxy_pass http://1.2.3.4:8080;
}
В этом примере все запросы попадающие в location /
будут проксироваться на сервер 1.2.3.4
порт 8080
. Это может быть как apache, так и любой другой http-сервер.
Однако тут есть несколько тонкостей, связанных с тем, что приложение будет считать, что, во-первых, все запросы приходят к нему с одного IP-адреса (что может быть расценено, например, как попытка DDoS-атаки или подбора пароля), а во-вторых, считать, что оно запущено на хосте 1.2.3.4
и порту 8080
(соответственно, генерировать неправильные редиректы и абсолютные ссылки). Чтобы избежать этих проблем без необходимости переписывания приложения, необходимо изменить конфигурацию.
nginx слушает внешний интерфейс на порту 80. Если backend (допустим, Apache) расположен на том же хосте, что и nginx, то он "слушает" порт 80
на 127.0.0.1
или другом внутреннем IP-адресе. Конфигурация nginx в таком случае выглядит следующим образом:
server {
listen 4.3.2.1:80;
# Устанавливаем заголовок "Host" и "X-Real-IP" к каждому запросу отправляемому на backend
# Или "proxy_set_header Host $host;", если приложение будет дописывать :80 ко всем ссылкам
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host:$proxy_port;
}
Для того, чтобы приложение различало IP-адреса посетителей, нужно либо поставить модуль mod_extract_forwarded
(если оно исполняется сервером Apache), либо модифицировать приложение так, чтобы оно брало информацию о IP-адресе пользователя из HTTP-заголовка X-Real-IP
.
Другой вариант backend — это использование FastCGI. В этом случае конфигурация nginx будет выглядеть примерно так:
server {
<...>
# location, в который будут попадать запросы на php-скрипты
location ~ .php$ {
# Определяем адрес и порт FastCGI-сервера,
fastcgi_pass 127.0.0.1:8888;
# ...индексный файл
fastcgi_index index.php;
# и некоторые параметры, которые нужно передать серверу FastCGI, чтобы он
# понял какой скрипт и с какими параметрами выполнять:
# имя скрипта
fastcgi_param SCRIPT_FILENAME /usr/www/html$fastcgi_script_name;
# строка запроса
fastcgi_param QUERY_STRING $query_string;
# и параметры запроса
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
}
# Благодяря тому что location с регулярными выражениями обладают
# большим "приоритетом", сюда будут попадать все не-php запросы
location / {
root /var/www/html/
}
}
Для того, чтобы меньше нагружать backend, статические файлы лучше отдавать только через nginx — он, с этой задачей справляется лучше, т.к. на каждый запрос он тратит существенно меньше ресурсов (не надо порождать новый процесс, да и процесс nginx как правило потребляет меньше памяти, а обслуживать может множество соединений).
В конфигурационном файле это выглядит примерно так:
server {
listen *:80;
server_name myserver.com;
location / {
proxy_pass http://127.0.0.1:80;
}
# Предположим что все статичные файлы лежат в /files
location /files/ {
# Указываем путь на фс
root /var/www/html/;
# Добавляем заголовок "Expires":
expires 14d;
# А если файл не найден, отправляем его в именованный location "@back"
error_page 404 = @back;
}
# Запросы из "/files", для которых не было найдено файла отправляем на backend,
# а он может либо сгенерировать нужный файл, либо показать красивое сообщение об ошибке
location @back {
proxy_pass http://127.0.0.1:80;
}
}
Если вся статика не помещена в какой-то определенный каталог, то можно воспользоваться регулярным выражением:
location ~* ^.+\.(jpg|jpeg|gif|png|ico|css|zip|tgz|gz|rar|bz2|doc|xls|exe|pdf|ppt|txt|tar|wav|bmp|rtf|js)$ {
# Аналогично тому что выше, только в этот location будут попадать все запросы
# оканчивающиеся на одно из указаных суффиксов
root /var/www/html/;
error_page 404 = @back;
}
К сожалению, в nginx не реализована асинхронная работа с файлами. Иными словами, nginx worker блокируется на операциях ввода-вывода. Так что если у вас очень много статических файлов и, в особенности, если они читаются с разных дисков, лучше увеличивать количество рабочих процессов (до числа, которое в 2-3 раза больше, чем суммарное число головок на диске). Это, конечно, ведет к увеличению нагрузки на ОС, но в целом производительность увеличивается. Для работы с типичным количеством статики (не очень большое количество сравнительно небольших файлов: CSS, JavaScript, изображения) вполне хватает одного-двух рабочих процессов.
Крупные проекты редко состоят из одного сервера приложений. Часто их два или больше, и возникает задача балансировки клиентов по этим серверам, а также выполнения failover — необходимо чтобы выход из строя одного из серверов не был заметен для клиентов. Простейший способ решить эту задачу — dns round-robin, т.е. назначение доменному имени нескольких IP-адресов. Но это решение имеет ряд недостатков, и гораздо лучше выглядит решение балансировки запросов по backend'ам на frontend nginx. В конфигурационном файле выглядит это примерно так:
# Объявляем upstream — список backend'ов
upstream backend {
# Перечисляем dns-имена или IP-адреса серверов и их "вес"
server web1 weight=5;
server 1.2.3.4:8080 weight=5;
# А так можно подключаться к backend через unix-сокет
server unix:/tmp/backend3 weight=1;
}
# Конфигурация виртуального сервера
server {
listen <...>;
server_name myserver.com;
# Отправляем все запросы из location "/" в upstream
location / {
proxy_pass http://backend;
}
}
Запросы, приходящие к nginx, распределяются по backend'ам соответственно указанному весу. Кроме того, можно сделать так, чтобы запросы с одних и тех же IP-адресов отправлялись на одни и те же серверы (для этого в upstream нужно указать директиву ip_hash
). Так можно решить проблему с сессиями, но все же лучше найти какой-нибудь способ их репликации. В случае, если один из серверов откажется принимать соединения или соединение к нему отключится по таймауту, он на некоторое время будет исключен из upstream.
Суть серверного кеширования в том, чтобы не генерировать постоянно одни и те же скрипты (например, ленту постов Wordpress), что может иногда занимать целые секунды. Вместо этого, приложение генерирует страницу один раз, и результат сохраняется в память. Когда посетитель запросит ту же страницу второй раз, генерации уже не будет, а клиент получит сохраненную в памяти версию. Раз в какое-то время (называемое TTL), эта сохраненная версия будет удаляться и генерироваться новая, чтобы поддерживать актуальность данных.
Прежде всего нужно определить максимальный размер кеша (общий размер всех страниц в кеше будет не более этого размера). Это делается в секции http
:
http {
...
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=all:32m max_size=1g;
...
}
Предварительно необходимо создать папку для кеша:
sudo mkdir /var/cache/nginx
Чтобы кеширование заработало, мы должны создать новый хост, который будет слушать 80
порт. А основной хост перенести на какой-то другой порт (например, 81
). Кеширующий хост будет посылать запросы на основной либо отдавать данные из кеша.
Кеширующий хост:
server {
listen 80;
location / {
proxy_pass http://127.0.0.1:81/;
proxy_cache all;
# Каждая страница будет сохраняться в кеш на 1 час
proxy_cache_valid any 1h;
}
}
Основной хост:
server {
# Обычный конфиг только на 81 порту
listen 81;
location / {
# fpm и т.п.
}
}
Многие сайты используют различные персональные блоки на страницах. Технология SSI позволяет реализовать продвинутое кеширование в случаях большого количество персонализированных блоков. В простом случае, мы можем просто отключать кеш, если у пользователя установлены какие-то Cookies.
server {
listen 80;
location / {
if ($http_cookie ~* ".+" ) {
set $do_not_cache 1;
}
proxy_cache_bypass $do_not_cache;
proxy_pass http://127.0.0.1:81/;
proxy_cache all;
proxy_cache_valid any 1h;
}
}
Имеет смысл также включить кеширование ошибочных запросов на какое-то короткое время. Это позволит избежать частых повторных попыток обратиться к неработающей части сайта.
server {
listen 80;
location / {
if ($http_cookie ~* ".+" ) {
set $do_not_cache 1;
}
proxy_cache_bypass $do_not_cache;
proxy_pass http://127.0.0.1:81/;
proxy_cache all;
proxy_cache_valid 404 502 503 1m;
proxy_cache_valid any 1h;
}
}
Nginx позволяет кешировать ответы от FastCGI. Для включения этого кеша, необходимо также объявить его параметры в секции http
:
# Установим максимальный размер кеша в 1GB
fastcgi_cache_path /var/cache/fpm levels=1:2 keys_zone=fcgi:32m max_size=1g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
Создаем папку для кеша:
sudo mkdir /var/cache/fpm
В конфигурации основного хоста, добавляем правила кеширования:
server {
listen 80;
location ~ \.php$ {
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_cache fcgi;
# В данном случае мы будем кешировать ответы с кодом 200 на 60 минут
fastcgi_cache_valid 200 60m;
}
}
Для хранения принятых запросов и еще не отданных ответов nginx использует буферы в памяти, а если запрос или ответ не помещается в них, nginx записывает его во временный файл (и пишет при этом предупреждение в log-файл). Поэтому необходимо установить такие размеры, чтобы в большинстве случаев не требовалось обращаться к временному файлу, а с другой стороны, чтобы буферы не использовали слишком много памяти.
Для этого используются следующие параметры:
client_body_buffer_size
(по умолчанию: 8k/16k в зависимости от архитектуры) — задает размер буфера для чтения тела запроса клиента. Обычно стандартного значения хватает, его требуется повышать, только если ваше приложение устанавливает огромные cookiesproxy_buffer_size
(по умолчанию: 4k/8k) — задает размер буфера, в который будет читаться первая часть ответа, получаемого от проксируемого сервера. В этой части ответа находится, как правило, небольшой заголовок ответа. Стандартного значения обычно хватаетproxy_buffers
(по умолчанию: 8 4k/8k) — задает число и размер буферов для одного соединения, в которые будет читаться ответ, получаемый от проксируемого сервера. Установите этот параметр так, чтобы большинство ответов от backend помещалось в буферы.
Есть одна тонкость, касающаяся механизма обработки соединений, а именно способ получения информации о событиях на сокетах. Существуют следующие методы:
select
— стандартный метод. На большой нагрузке сильно нагружает процессорpoll
— стандартный метод. Также сильно нагружает процессорkqueue
— эффективный метод, используемый в операционных системах FreeBSD 4.1+, OpenBSD 2.9+, NetBSD 2.0 и Mac OSXepoll
— эффективный метод, используемый в Linux 2.6+rtsig
— real time signals, эффективный метод, используемый в Linux 2.2.19+. При больших количествах одновременных соединений (более 1024) с ним могут быть проблемы/dev/poll
— эффективный метод, используемый в Solaris 7 11/99+, HP/UX 11.22+ (eventport), IRIX 6.5.15+ и Tru64 UNIX 5.1A+
При компиляции nginx автоматически выбирается максимально эффективный найденый метод, однако скрипту configure
можно указать какой метод использовать.
Включение gzip
позволяет сжимать ответ, отправляемый клиенту, что положительно сказывается на удовлетворенности пользователя, но требует больше времени CPU. Gzip включается директивой gzip (on|off)
. Кроме того, стоит обратить на следующие важные директивы модуля gzip:
gzip_comp_level 1..9
— устанавливает уровень сжатия. Опытным путем выявлено, что оптимальные значения лежат в промежутке от 3 до 5, большие значения дают маленький выигрыш, но создают существенно большую нагрузку на процессор, меньшие — дают слишком маленький коэффициент сжатияgzip_min_length
(по умолчанию, 0) — минимальный размер ответа, который будет сжиматься. Имеет смысл поставить этот параметр в 1024, чтобы слишком маленькие файлы не сжимались (т.к. эффективность этого будет мала).gzip_types mime-тип [mime-тип ...]
— разрешает сжатие ответа методом gzip для указанных MIME-типов в дополнение кtext/html
.text/html
сжимается всегда. Имеет смысл добавить такие mime-типы какtext/css
,text/javascript
и подобные. Сжимать gif, jpg и прочие уже компрессированные форматы не имеет смысла
Кроме того, существует модуль gzip_static
, который позволяет раздавать уже сжатые статические файлы. В конфигурационном файле это выглядит так:
location /files/ {
gzip on;
gzip_min_length 1024;
gzip_types text/css text/javascript;
gzip_comp_level 5;
gzip_static on;
}
При использовании такой конфигурации в случае запроса /files/test.html
nginx будет проверять наличие /files/test.html.gz
, и, если этот файл существует и дата его последнего изменения больше, чем дата последнего изменения файла test.html
, будет отдан уже сжатый файл, что сохранит ресурсы процессора, которые потребовались бы для сжатия оригинального файла.
Пример конфигурации для Django для production. Внутри Docker-контейнера:
user nginx;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
client_max_body_size 100m;
upstream backend {
server backend:8000;
}
server {
listen 80;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
send_timeout 600;
root /dist/;
index index.html;
# Frontend
location / {
try_files $uri $uri/ @rewrites;
}
location @rewrites {
rewrite ^(.+)$ /index.html last;
}
# Backend
location ~ ^/(admin|api) {
proxy_redirect off;
proxy_pass http://backend;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto 'https';
proxy_set_header Host $http_host;
}
# Backend static
location ~ ^/(staticfiles|mediafiles)/(.*)$ {
alias /$1/$2;
}
# Some basic cache-control for static files to be sent to the browser
location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
expires max;
add_header Pragma public;
add_header Cache-Control "public, must-revalidate, proxy-revalidate";
}
}
}
На основном хосте:
server {
server_name my.domain.ru;
listen 443 ssl;
ssl on;
ssl_certificate /etc/letsencrypt/live/my.domain.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/my.domain.ru/privkey.pem;
client_max_body_size 50M;
proxy_connect_timeout 6000;
proxy_send_timeout 6000;
proxy_read_timeout 6000;
send_timeout 6000;
gzip on;
gzip_comp_level 5;
gzip_proxied any;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon;
location / {
proxy_redirect off;
proxy_pass http://127.0.0.1:8000/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
}
Создать ключ:
sudo apt install apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd_myfile admin
Изменить файл конфигурации:
location / {
satisfy any;
# Разрешить заходить на location с выбранного IP без авторизации
allow 10.193.10.24;
deny all;
auth_basic "Page Title";
auth_basic_user_file /etc/nginx/.htpasswd_myfile;
...
}