nginx
Nginx - популярный веб сервер, отличающийся своей скоростью при работе с большим количествеом соединений. Одновременно: прекрасно отдает статику, работает в качестве reverse proxy и load balancer
Архитектура.
Nginx изначально был спроектирован на базе асинхронных неблокирующих event-driven алгоритмов.
Nginx активно использует мультиплексирование и нотификации событий, назначая конкретные задачи отдельным процессам. Соединения обрабатываются с помощью эффективного цикла выполнения с помощью определенного количества однопоточных процессов, называемых worker’ами. Внутри каждого worker nginx может обрабатывать многие тысячи одновременных соединений и запросов в секунду.
Каждое соединение, обрабатываемое воркером, помещается в event loop вместе с другими соединениями. В этом цикле события обрабатываются асинхронно, позволяя обрабатывать задачи в неблокирующей манере. Когда соединение закрывается оно удаляется из цикла.
Этот подход к обработке соединений позволяет Nginx невероятно масштабироваться при ограниченных ресурсах. Поскольку сервер однопоточный и он не создает процессы под каждое соединение, использование памяти и CPU относительно равномерно, даже при высоких нагрузках.
Worker в nginx включает ядро и функциональные модули. Ядро nginx отвечает за поддержание цикла выполнения и исполнения подходящих секций кода модулей на каждом шаге обработки процесса. Модули предоставляют большую часть функциональности уровня приложений. Также модули читают и пишут в сеть и хранилище, трансформируют контент, осуществляют исходящую фильтрацию и, в случае работы в режиме прокси, передают запросы вышестоящим серверам.
Модульная архитектура nginx позволяет разработчикам расширять набор функций веб-сервера без необходимости модификации кода его ядра. Существует несколько разновидностей модулей nginx — модули ядра, модули событий, фазовые обработчики, протоколы, фильтры, балансировщики нагрузки, обработчики переменных и т.п.

Поскольку nginx не создает процессы и потоки для каждого соединения, в подавляющем большинстве случаев веб-сервер очень консервативно и крайне эффективно работает с памятью. Кроме того он сохраняет циклы процессора, поскольку в случае nginx отсутствует паттерн постоянного создания и уничтожения процессов и потоков. Nginx проверяет состояние сети и хранилища, инициализирует новые соединения, добавляет их в цикл выполнения, а затем асинхронно обрабатывает до «победного конца», после чего соединение деактивируется и исключается из цикла. Благодаря этому механизму, а также вдумчивому использованию системных вызовов и качественной реализации поддерживающих интерфейсов вроде распределителей памяти (pool и slab), nginx позволяет добиться низкой или средней загрузки CPU даже в случае экстремальных нагрузок.
Использование нескольких worker-процессов для обработки соединений также делает веб-сервер хорошо масштабируемым для работы с несколькими ядрами. Эффективное использование многоядерных архитектур обеспечивается созданием одного worker-процесса для каждого ядра, а также позволяет избежать блокировок и трешинга потоков. Механизмы контроля ресурсов изолированы внутри однопоточных worker-процессов — такая модель также способствует более эффективному масштабированию физических устройств хранения, позволяет добиваться более высокой утилизации дисков и избегать блокирования дискового ввода/вывода. В итоге ресурсы сервера используются эффективнее, а нагрузка распределяется между несколькими worker-процессами.
Конфигурация nginx
Конфигурация nginx хранится в нескольких текстовых файлах, которые обычно располагаются в директориях /usr/local/etc/nginx или /etc/nginx. Главный конфигурационный файл обычно называется /etc/nginx/nginx.conf. Чтобы сделать его более читабельным, части конфигурации можно разнести по разным файлам, которые затем включаются в главном.
Изначальное чтение и проверка конфигурационных файлов осуществляется master-процессом. Скомпилированная форма конфигурации для чтения доступна worker-процессам после их выделения из master-процесса. Конфигурационные структуры автоматически разделяются механизмами управления виртуальной памятью.
/etc/nginx/
├── nginx.conf # основной файл
├── sites-available/ # конфиги сайтов
├── sites-enabled/ # активные сайты (через symlink)
├── conf.d/ # дополнительные конфиги
└── snippets/ # переиспользуемые куски
user www-data;
worker_processes auto; #количество процессов, определяется количеством ядер
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
access_log /var/log/nginx/access.log; # логи доступа
error_log /var/log/nginx/error.log; # логи ошибок
include /etc/nginx/conf.d/*.conf; # дополнительные файлы конфигурации
include /etc/nginx/sites-enabled/*; # конфиги для конкретных виртуальных сайтов
}
Каждая строка в данном файле является директивой, определяющей логику работы. Кроме простых директив, которые отделяются друг от друга символом ;, существуют и блоки директив, которые могут располагаться лишь в конкретном отношении друг к другу. Некоторые блоки могут повторяться, некоторые же могут встречаться лишь один раз. В первом приближении уровни конфигурационного файла выглядят следующим образом:
main
├── events
├── http
│ ├── server
│ │ └── location
│ └── server
│ └── location
└── mail
├── server
└── server
log directives
log_format
Директива log_format позволяет описать, что будет содержаться в журнале запросов - какие поля и в какой очередности.
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
По аналогии с bash, все, что начинается со спецсимвола $, - это переменные, большинство из которых доступно по умолчанию. Однако, как мы увидим далее, возможно задать новые переменные, которые также можно отдавать в логах, описав их новый формат.
access_log
Форматы логов используются в директиве access_log следующим образом - access_log ${LOG_PATH} ${LOG_FORMAT};.
Пример использования - access_log /var/log/nginx/access.log combined;.
Возможно в одной директиве использовать несколько access_log, чтобы писать в разные файлы с разными форматами. Это бывает полезно, когда нам нужны разные уровни детализации либо логи далее используются каким-то еще сервисом.
error_log
Кроме access_log, существует еще один лог - error_log. Он используется для записи ошибок работы nginx. В отличие от директивы access_log, в error_log передается не формат логов, а минимальный уровень детализации. По умолчанию используется уровень error, но существуют и другие уровни, вплоть до debug.
Пример использования - error_log /var/log/nginx/error.log debug;.
include
Директива include позволяет подключать другие файлы вместо директивы. Выглядит это так, что в ходе запуска сервиса nginx компилирует конфигурационный файл, вставляя вместо этой директивы содержимое тех файлов, которые подключаются, и применяет директивы одну за одной (поэтому последовательность директив важна).
Важно то, что в ходе такого подключения с использованием * подключение файлов производится в алфавитном порядке, поэтому, если вам нужно какой-то конфигурационный файл поставить первым для выполнения, его можно назвать с использованием числа первым символом имени файла (например: 10-base.conf, 20-addition.conf)
http {
include /etc/nginx/conf.d/*.conf; # дополнительные файлы конфигурации
include /etc/nginx/sites-enabled/*; # конфиги для конкретных виртуальных сайтов
}
server
Директива server определяет так называемый виртуальный хост, что позволяет на одном IP-адресе обрабатывать несколько доменов. С точки зрения внутренней логики, определение производится по HTTP заголовку Host, который выставляется, когда вы обращаетесь к конкретному серверу по DNS-имени. Каждый server может быть запущен на своем адресе и порту, либо обрабатывать конкретное (или несколько конкретных) DNS-имен. Кроме того, можно создать server, который привязан не к конкретному доменному имени, а только к порту.
server {
listen 80;
server_name example.com;
location / {
root /var/www/html;
index index.html;
}
error_page 404 /404.html;
location = /404.html {
root /var/www/errors/;
}
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
}
server_name
При помощи директивы server_name можно как раз указать, какие DNS-имена будет обрабатывать конкретный server. Так, к примеру, если мы хотим определить, чтобы сервер обрабатывал только домен example.com, то директива должна выглядеть просто как server_name example.com;.
Если же требуется обрабатывать несколько доменных имен, то они указываются друг за другом и разделяются пробелами,
к примеру, server_name example.com www.example.com;.
Кроме этого, директива позволяет устанавливать wildcard правила, которые, к примеру, позволяют обрабатывать все субдомены -
server_name example.com *.example.com;.
Отдельным случаем является использование пустого server_name "" - он определяет, что запросы без Host (к примеру, по IP-адресу) будут обрабатываться этим server.
listen
Данная директива позволяет определить, где слушает конкретный сервер - какой порт, какой IP-адрес или вообще Unix сокет.
Самый распространенный случай использования - это определение порта, на котором слушает сервер и частые опции - http2 и ssl. Такой кейс в виде директивы выглядит как listen 443 http2 ssl. Также популярен и, пусть и простой, но очень важный формат, который позволяет повесить сервер на конкретный порт без SSL - listen 8080. Эти два сценария использования данной директивы наиболее распространены и встречаются наиболее часто.
Примеры директивы server_name:
server_name ""; listen 80 default_server;server_name example.com; listen 80;server_name ""; listen 127.0.0.1:80 default_server;
Каждый из них имеет право жить и будет обрабатывать разные запросы.
Опция default_server директивы listen
Опцию default_server рекомендуется использовать с высшим приоритетом над остальными конфигами и, по возможности, не перегружать ее логикой.
# Отказ от запроса со стороны клиента со специальным HTTP кодом 444, который просто закрывает соединение.
server {
listen 80 default_server;
server_name "";
return 444;
}
# Рекомендуется держать данный сервер первым в очереди, чтобы по случайности не было обращения к другим server.
Как nginx обрабатывает запросы
location
Устанавливает конфигурацию в зависимости от URI запроса.
Алгоритм выбора location в Nginx
autoindex
Директива autoindex в Nginx используется для автоматической генерации списка файлов в директории, когда клиент обращается к URL, который указывает на эту директорию. Это может быть полезно, например, когда вы хотите предоставить доступ к содержимому директории через веб-браузер.
Модуль ngx_http_autoindex_module
Красивый листинг файлов и директорий в nginx
root
Директива root задает корневой каталог для запросов. Относительно него определяется, к какому файлу обращаться при запросе. Представим, что у нас есть директория /var/www/static, в которой у нас есть изображения. В этом случае мы можем установить директиву root /var/www/static, и тогда при обращении к пути /image.png мы, по факту, обратимся к файлу /var/www/static/image.png.
root - полный путь складывается из дериктории указанной в
root+ директории указанной вlocation. В целом, документация рекомендует не использовать директивуrootвнутриlocation, а выносить ее вконтекст server, но бывает, что без этого не обойтись.
location /i/ {
root /data/w3;
}
# в ответ на запрос “/i/top.gif” будет отдан файл /data/w3/i/top.gif.
alias
В случае же директивы alias мы создаем некую символическую ссылку, и обращение будет производиться к файлу без добавления location к пути.
alias - полный путь складывается только лишь из дериктории указанной в
alias
location /i/ {
alias /data/w3/images/;
}
# на запрос “/i/top.gif” будет отдан файл /data/w3/images/top.gif
client_max_body_size
Задаёт максимально допустимый размер тела запроса клиента. Если размер больше заданного, то клиенту возвращается ошибка 413 (Request Entity Too Large). Следует иметь в виду, что браузеры не умеют корректно показывать эту ошибку. Установка параметра размер в 0 отключает проверку размера тела запроса клиента.
server {
listen 80;
server_name example.com;
# Установка максимального размера тела запроса в 10 мегабайт
client_max_body_size 10M;
location /upload {
# В этой локации будет применен максимальный размер тела запроса, установленный выше для сервера
}
}
allow/deny
Директивы allow и deny используются в контексте location, server, или http в файле конфигурации Nginx для управления доступом к ресурсам на сервере. Они определяют правила, разрешающие или запрещающие доступ к определенным ресурсам для указанных IP-адресов или диапазонов IP-адресов. Можно использовать как своеобразный firewall по ограничению доступа к вашим ресурсам, однако, в отличие от Firewall, nginx при подключении все равно производит установку соединения, то есть открывает TCP-соединение, что требует ресурсов.
server {
listen 80;
server_name example.com;
location /private {
# Разрешаем доступ только с определенных IP-адресов
allow 192.168.1.0/24;
allow 10.0.0.1;
deny all;
}
location /public {
# Запрещаем доступ с определенных IP-адресов
deny 192.168.1.0/24;
allow all;
}
}
auth_basic
Директива auth_basic используется в блоке location в файле конфигурации Nginx для настройки HTTP-аутентификации базового уровня (Basic Authentication). Эта директива определяет сообщение, которое будет отображаться в диалоговом окне аутентификации браузера, и файл, содержащий учетные данные пользователей.
server {
listen 80;
server_name example.com;
location /private {
# Включение базовой аутентификации для этой локации
auth_basic "Restricted Access";
auth_basic_user_file /etc/nginx/.htpasswd;
}
location /public {
# В этой локации аутентификация не требуется
}
}
auth_basic- позволяет либо отключить аутентификацию (для этого используется специальное значение off), либо задать текст, который выводится при входе на страницу (к примеру, "Welcome to Area 52");auth_basic_user_file- определяет путь, где хранится файл с данными для входа.
auth_basic_user_file
Этот файл должен содержать пары $USERNAME:$PASSWORD_WITH_HASH_TYPE, где "хэш_пароля" обычно представляет собой результат хэширования пароля. Формат этого файла и используемый алгоритм хэширования зависят от метода хранения паролей. Существует несколько вариантов получить хэшированный пароль:
# Утилита htpasswd
htpasswd -c /etc/nginx/htpasswd $USERNAME
# данная команда создаст (за это отвечает ключ -c) новый файл и запишет в него пароль,
# который запросит в интерактивном режиме утилита для пользователя с ником $USERNAME.
htpasswd /etc/nginx/htpasswd $USERNAME_NEW.
# добавит новую запись в уже существующий файл
# Утилита openssl
echo "sammy:$(openssl passwd -apr1)" >> /etc/nginx/htpasswd
# пароль для команды будет запрошен в интерактивном режиме и в файл будет добавлена новая запись - не важно, существовал файл ранее или нет
# можно записывать пароль в нехешированном виде. Для этого используется формат из RFC-2307,
# который позволяет описать формат хеширования в фигурных скобках.
# Так, для хранения пароля в открытом виде используется следующий формат:
test:{PLAIN}test
return
Данная директива либо возвращает какой-то текст (может использоваться для отладки), либо делает редирект на другую ссылку (при использовании HTTP кодов из семейства 300-х).
server {
listen 80;
server_name example.com;
location /old-url {
return 301 https://example.com/new-url;
}
}
server {
listen 80;
server_name example.com;
location /hello {
return 200 "hello from nginx";
}
}
server {
listen 80;
server_name example.com;
location / {
return 301 https://$host$request_uri;
}
}
proxy_pass
Директива proxy_pass используется в блоке location в файле конфигурации Nginx для настройки обратного прокси-сервера. Эта директива определяет адрес, на который будут перенаправляться запросы, и может указывать на другой сервер или локальный порт, где работает другое приложение.
server {
listen 80;
server_name example.com;
location / {
# Проксируем запросы на другой сервер
proxy_pass http://backend-server;
}
}
Манипулирование заголовками
Nginx, кроме перенаправления трафика, умеет еще и управлять HTTP заголовками разными путями. Это бывает полезно как для передачи дополнительных сервисных данных для сервиса, так и для клиента.
В nginx существует несколько основных директив, которые позволяют взаимодействовать с заголовками:
-
add_header - позволяет добавить заголовок с необходимым значением, которое будет возвращаться клиенту. Пример использования - возвращение сервисных заголовков, которые могут отвечать за корректную работу определенной функциональности в браузерах, такую как Cross-Origin Resource Sharing, позволяющей сервисам из одного домена обращаться к сервисам в другом домене. Пример использования директивы - add_header X-Example RebrainMe;. Для того чтобы удалить заголовок из ответа, можно просто установить его пустым, передав пустую строку ("").
-
proxy_set_header - позволяет задать заголовок, который будет передан через proxy_pass. Пример использования - proxy_set_header Host $proxy_host;.
-
proxy_hide_header - позоволяет скрыть конкретный заголовок, который передал сервер клиенту обратно. Пример - proxy_hide_header Content-Secure-Policy;.
Во всех этих директивах можно использовать переменные, ровно как и обычные строки и даже строить другие строки из переменных. К примеру, add_header X-Forwarded-For $host;.
Проксирование запросов в nginx с помощью proxy_pass
upstream
Директива upstream используется в блоке http в файле конфигурации Nginx для определения группы серверов (пула серверов), которые могут быть использованы для балансировки нагрузки или проксирования запросов. Обычно эта директива используется вместе с proxy_pass для проксирования запросов на несколько серверов.
http {
upstream backend {
server backend1.example.com;
server backend2.example.com weigh=3; # weight - позволяет задать для сервера вес,
# то есть то, какую долю запросов может обрабатывать сервер, изменяя алгоритм балансировки на weighted round-robin.
server backend3.example.com backup; # backup - отмечает сервер как запасной.
# Суть данной директивы в том, что на backup server запросы отправляются только тогда, когда основной сервер не отвечает.
}
server {
listen 80;
server_name example.com;
location / {
# Проксируем запросы на сервера из группы "backend"
proxy_pass http://backend;
}
}
}
# Важно обратить внимание на то, что протокол, по которому производится обращение к серверам,
# определяется именно на уровне proxy_pass, а не upstream.server.
http {
resolver 8.8.8.8 ipv6=off;
upstream my_example {
server example.com:443;
}
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
server {
location /example/ {
proxy_ssl_server_name on;
proxy_pass https://my_example/;
proxy_set_header Host example.com;
proxy_buffering off;
proxy_read_timeout 90;
proxy_connect_timeout 90;
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true;
proxy_ssl_verify off;
proxy_ssl_session_reuse on;
}
}
map
Директива map в Nginx используется для создания ассоциативных массивов (map), которые позволяют сопоставлять значения одних переменных с другими значениями или выражениями. Это может быть полезно для решения различных задач, таких как перенаправление запросов, управление кэшированием или изменение конфигурации на основе определенных условий.
Директива map позволяет задать переменную, которая в зависимости от установленного значения в момент обращения к map возвращает определенное значение. В целом, map стоит воспринимать как своеобразную переменную типа ключ-значение, у которой ключ всегда берется из другой переменной.
# Вот пример использования директивы map для перенаправления запросов на разные URL в зависимости от значения переменной $request_uri:
http {
map $request_uri $redirect_url {
/page1 https://example.com/page1;
/page2 https://example.com/page2;
/page3 https://example.com/page3;
default https://example.com/default;
}
server {
listen 80;
server_name example.com;
location / {
return 301 $redirect_url;
}
}
}
# В этом примере, в зависимости от значения переменной $request_uri, определяется значение переменной $redirect_url,
# которая затем используется для перенаправления запросов на соответствующие URL.
# Если $request_uri не совпадает ни с одним из перечисленных значений, будет использован URL https://example.com/default.
# Ключом же может выступать не только конкретная переменная, но и вообще какая-то строка, которая составлена из переменных:
map "$browser-$request_method" $var_two {
"firefox-POST" "ff-post";
"firefox-GET" "ff-get";
}
# При помощи спецключа default можно установить значение, возвращаемое по умолчанию, расширяя наш пример:
map "$browser-$request_method" $var_two {
"firefox-POST" "ff-post";
"firefox-GET" "ff-get";
default "ie-any";
}
stream
Модуль ngx_stream_core_module (stream) в Nginx (доступен с 1.9.0) предназначен для проксирования и балансировки TCP/UDP-трафика, работая вне контекста HTTP. Он позволяет обрабатывать потоковые данные, обеспечивая высокую производительность для баз данных, почтовых серверов и VPN
Общий синтаксис блока stream: в upstream-ах указываются бэкенды на которые проксируются запросы, в server-ах обозначаются порты, протоколы, адреса которые будет слушать nginx.
stream {
# группа backend серверов
upstream backend {
server backend1.example.com:12345 weight=5;
server 127.0.0.1:12345 max_fails=3 fail_timeout=30s;
server unix:/tmp/backend3;
}
# группа dns серверов
upstream dns {
server 192.168.0.1:53535;
server dns.example.com:53;
}
# группа syslog серверов
upstream syslog_tcp {
server 127.0.0.1:5140;
}
server {
listen 12345;
proxy_connect_timeout 1s;
proxy_timeout 3s;
proxy_pass backend;
}
server {
listen 127.0.0.1:53 udp reuseport;
proxy_timeout 20s;
proxy_pass dns;
}
server {
listen [::1]:12345;
proxy_pass unix:/tmp/stream.socket;
}
server {
listen 5140;
proxy_pass syslog_tcp;
}
}
ssl_preread on
Опция ssl_preread on в stream-контексте NGINX включает предварительное чтение TLS-рукопожатия (ClientHello) без расшифровки трафика.
ssl_preread позволяет получить переменные:
- $ssl_preread_server_name - домен из SNI
- $ssl_preread_protocol - версия TLS
- $ssl_preread_alpn_protocols
Когда клиент подключается по HTTPS/TLS, в ClientHello есть:
- SNI (Server Name Indication) — доменное имя (mystatic.site.ru)
- версия TLS
- список cipher suites
- ALPN (h2 / http1.1)
Пример использования массива map в модуле stream с опцией ssl_preread. Данный блок конфигурации принимает трафик на 443 порту по SSL, ssl_preread запоминает первые байты TLS-соединения, чтобы узнать метаданные и решить, куда проксировать соединение. Если в ClientHello есть запрашиваемое доменное имя (SNI), то запрос будет проксирован на соответствуйший виртуальный хост, если SNI отсутствует (у OpenVPN его нет), запрос переправляется на default, далее на upstream и на хокальный порт на котором висит VPN сервер.
stream {
map $ssl_preread_server_name $backend {
mystatic.site.ru static;
mywordpress.ru wordpress;
git.akulovs.ru git;
sfpz.akulovs.ru sfpz;
default vpn;
}
upstream ststic {
server 127.0.0.1:8443;
}
upstream wordpress {
server 127.0.0.1:8443;
}
upstream vpn {
server 127.0.0.1:1194; # OpenVPN TCP
}
server {
listen 443 reuseport;
proxy_pass $backend;
ssl_preread on;
}
}
ssl_preread:
- работает только для TCP
- не расшифровывает TLS
- не требует сертификатов в nginx
- подходит для L4-балансировки HTTPS