Используем nginx для выполнения интересных и нестандартных задач
Nginx стремительными темпами набирает популярность, превращаясь из просто ускорителя отдачи статики для Apache в полнофункциональный и развитый веб-сервер, который все чаще применяется обособленно. В этой статье мы поговорим об интересных и нестандартных сценариях использования nginx, которые позволят выжать из веб-сервера максимум.
Почтовый прокси
Начнем с самого очевидного — со способности nginx выступать в роли почтового прокси. Эта функция есть в nginx изначально, а вот используется в продакшн она почему-то крайне редко, некоторые так и вообще не догадываются о ее существовании. Как бы там ни было, nginx поддерживает проксирование протоколов POP3, IMAP и SMTP с разными методами аутентификации, включая SSL и StartTLS, причем делает это очень быстро.
Зачем это нужно? Есть как минимум два применения данной функциональности. Первая: использовать nginx в качестве щита от назойливых спамеров, пытающихся отправить мусорные письма через наш SMTP-сервер. Обычно спамеры не создают много проблем, так как быстро отшибаются на этапе аутентификации, однако, когда их становится действительно много, nginx поможет сэкономить процессорные ресурсы. Вторая: использовать nginx для перенаправления пользователей на несколько почтовых POP3/IMAP-серверов. С этим, конечно, мог бы справиться и другой почтовый прокси, но зачем городить огород серверов, если на фронтенде уже установлен nginx для отдачи статики по HTTP, например?
Почтовый прокси-сервер в nginx сделан не совсем стандартно. Он использует дополнительный слой аутентификации, реализованный средствами HTTP, и, только если пользователь проходит этот барьер, он пропускается дальше. Обеспечивается такая функциональность путем создания страницы/скрипта, которой nginx отдает данные пользователя, а она/он возвращает ответ в виде стандартных OK или причины отказа (типа «Invalid login or password»). Скрипт запускается со следующими заголовками:
Входные данные скрипта аутентификации HTTP_AUTH_USER: юзер HTTP_AUTH_PASS: пароль HTTP_AUTH_PROTOCOL: почтовый протокол (IMAP, POP3 или SMTP)
А возвращает такие:
Выходные данные скрипта аутентификации HTTP_AUTH_STATUS: OK или причина отказа HTTP_AUTH_SERVER: реальный почтовый сервер для перенаправления HTTP_AUTH_PORT: порт сервера
Замечательная особенность такого подхода в том, что его можно использовать вовсе не для самой аутентификации, а чтобы раскидать пользователей по разным внутренним серверам, в зависимости от имени юзера, данных о текущих нагрузках на почтовые серверы либо вообще организовав простейшую балансировку нагрузки с помощью round-robin. Впрочем, если требуется всего лишь перекинуть пользователей на внутренний почтовый сервер, можно использовать вместо реального скрипта заглушку, реализованную самим nginx. Например, простейший SMTP- и IMAP-прокси в конфиге nginx будет выглядеть следующим образом:
# vi /etc/nginx/nginx.conf mail { # Адрес скрипта аутентификации auth_http localhost:8080/auth; # Отключаем команду XCLIENT, некоторые почтовые серверы ее не понимают xclient off; # IMAP-сервер server { listen 143; protocol imap; proxy on; } # SMTP-сервер server { listen 25; protocol smtp; proxy on; } }
Далее в секцию http конфига добавляем следующее:
# vi /etc/nginx/nginx.conf http { # Маппинг на нужный порт почтового сервера в зависимости от порта, отправленного в заголовке HTTP_AUTH_PROTOCOL map $http_auth_protocol $mailport { default 25; smtp 25; imap 143; } # Реализация «скрипта» аутентификации — всегда возвращает OK и перекидывает пользователя на внутренний почтовый сервер, выставляя нужный порт с помощью приведенного выше маппинга server { listen 8080; location /auth { add_header "Auth-Status" "OK"; add_header "Auth-Server" "192.168.0.1"; add_header "Auth-Port" $mailport; return 200; } } }
Это все. Такая конфигурация позволяет прозрачно перенаправлять пользователей на внутренний почтовый сервер, не создавая оверхеда в виде ненужного в данном случае скрипта. Применив скрипт, такую конфигурацию можно существенно расширить: настроить балансировку нагрузки, проверять пользователей по базе LDAP и выполнять другие операции. Написание скрипта выходит за рамки этой статьи, однако его очень легко реализовать, даже имея лишь поверхностные знания о PHP и Python.
Потоковое вещание видео
Поднять обычный видеохостинг на базе nginx легко. Достаточно только выложить перекодированное видео в доступный серверу каталог, прописать его в конфиг и настроить флеш- или HTML5-проигрыватель так, чтобы он брал видео из этого каталога. Однако, если требуется настроить непрерывное вещание видео из какого-то внешнего источника или веб-камеры, такая схема не сработает, и придется смотреть в сторону специальных потоковых протоколов.
Есть несколько протоколов, решающих эту задачу, наиболее эффективный и поддерживаемый из них RTMP. Беда только в том, что почти все реализации RTMP-сервера страдают от проблем. Официальный Adobe Flash Media Server платный. Red5 и Wowza написаны на Java, а потому не дают нужной производительности, еще одна реализация, Erlyvideo, написана на Erlang, что хорошо в случае настройки кластера, но не так эффективно для одиночного сервера.
Я же предлагаю другой подход — воспользоваться модулем RTMP для nginx. Он обладает превосходной производительностью и к тому же позволит использовать один сервер для отдачи как веб-интерфейса сайта, так и видеопотока. Проблема только в том, что модуль этот неофициальный, поэтому nginx с его поддержкой придется собрать самостоятельно. Благо сборка осуществляется стандартным способом:
$ sudo apt-get remove nginx $ cd /tmp $ wget http://bit.ly/VyK0lU -O nginx-rtmp.zip $ unzip nginx-rtmp.zip $ wget http://nginx.org/download/nginx-1.2.6.tar.gz $ tar -xzf nginx-1.2.6.tar.gz $ cd nginx-1.2.6 $ ./configure --add-module=/tmp/nginx-rtmp-module-master $ make $ sudo make install
Теперь модуль нужно настроить. Делается это, как обычно, через конфиг nginx:
rtmp { # Активируем сервер вещания на порту 1935 по адресу сайт/rtmp server { listen 1935; application rtmp { live on; } } }
Модуль RTMP не умеет работать в многопоточной конфигурации, поэтому количество рабочих процессов nginx придется сократить до одного (позже я расскажу, как обойти эту проблему):
worker_processes 1;
Теперь можно сохранить файл и заставить nginx перечитать конфигурацию. Настройка nginx завершена, но самого видеопотока у нас еще нет, поэтому его нужно где-то взять. Для примера пусть это будет файл video.avi из текущего каталога. Чтобы превратить его в поток и завернуть в наш RTMP-вещатель, воспользуемся старым добрым FFmpeg:
# ffmpeg -re -i ~/video.avi -c copy -f flv rtmp://localhost/rtmp/stream
В том случае, если видеофайл представлен не в формате H264, его следует перекодировать. Это можно сделать на лету с помощью все того же FFmpeg:
# ffmpeg -re -i ~/video.avi -c:v libx264 -c:a libfaac -ar 44100 -ac 2 -f flv rtmp://localhost/rtmp/stream
Поток также можно захватить прямо с веб-камеры:
# ffmpeg -f video4linux2 -i /dev/video0 -c:v libx264 -an -f flv rtmp://localhost/rtmp/stream
Чтобы просмотреть поток на клиентской стороне, можно воспользоваться любым проигрывателем с поддержкой RTMP, например mplayer:
$ mplayer rmtp://example.com/rtmp/stream
Или встроить проигрыватель прямо в веб-страницу, которая отдается тем же nginx (пример из официальной документации):
Простейший веб-проигрыватель RTMP
<script type="text/javascript" src="/jwplayer/jwplayer.js"></script> <script type="text/javascript"> jwplayer("container").setup({ modes: [{ type: "flash", src: "/jwplayer/player.swf", config: { bufferlength: 1, file: "stream", streamer: "rtmp://localhost/rtmp", provider: "rtmp", } }] }); </script>
Важных строки тут всего две: «file: “stream”», указывающая на RTMP-поток, и «streamer: “rtmp://localhost/rtmp”», в которой указан адрес RTMP-стримера. Для большинства задач таких настроек будет вполне достаточно. По одному адресу можно пустить несколько разных потоков, а nginx будет эффективно их мультиплексировать между клиентами. Но это далеко не все, на что способен RTMP-модуль. С его помощью, например, можно организовать ретрансляцию видеопотока с другого сервера. Сервер FFmpeg для этого вообще не нужен, достаточно добавить следующие строки в конфиг:
# vi /etc/nginx/nginx.conf application rtmp { live on; pull rtmp://rtmp.example.com; }
Если требуется создать несколько потоков в разном качестве, можно вызвать перекодировщик FFmpeg прямо из nginx:
# vi /etc/nginx/nginx.conf application rtmp { live on; exec ffmpeg -i rtmp://localhost/rtmp/$name -c:v flv -c:a -s 320x240 -f flv rtmp://localhost/rtmp-320x240/$name; } application rtmp-320x240 { live on; }
С помощью такой конфигурации мы получим сразу два вещателя, один из которых будет доступен по адресу rtmp://сайт/rtmp, а второй, вещающий в качестве 320 x 240, — по адресу rtmp://сайт/rtmp–320x240. Далее на сайт можно повесить флеш-плеер и кнопки выбора качества, которые будут подсовывать плееру тот или иной адрес вещателя.
Ну и напоследок пример вещания музыки в сеть:
while true; do ffmpeg -re -i "`find /var/music -type f -name '*.mp3'|sort -R|head -n 1`" -vn -c:a libfaac -ar 44100 -ac 2 -f flv rtmp://localhost/rtmp/stream; done
Git-прокси
Система контроля версий Git способна обеспечивать доступ к репозиториям не только по протоколам Git и SSH, но и по HTTP. Когда-то реализация доступа по HTTP была примитивной и неспособной обеспечить полноценную работу с репозиторием. С версии 1.6.6 ситуация изменилась, и сегодня этот протокол можно использовать, чтобы, например, обойти ограничения брандмауэров как с той, так и с другой стороны соединения либо для создания собственного Git-хостинга с веб-интерфейсом.
К сожалению, официальная документация рассказывает только об организации доступа к Git средствами веб-сервера Apache, но, так как сама реализация представляет собой внешнее приложение со стандартным CGI-интерфейсом, ее можно прикрутить практически к любому другому серверу, включая lighttpd и, конечно же, nginx. Для этого не потребуется ничего, кроме самого сервера, установленного Git и небольшого FastCGI-сервера fcgiwrap, который нужен, потому что nginx не умеет работать с CGI напрямую, но умеет вызывать скрипты с помощью протокола FastCGI.
Вся схема работы будет выглядеть следующим образом. Сервер fcgiwrap будет висеть в фоне и ждать запроса на исполнение CGI-приложения. Nginx, в свою очередь, будет сконфигурирован на запрос исполнения CGI-бинарника git-http-backend через FastCGI-интерфейс каждый раз при обращении к указанному нами адресу. Получив запрос, fcgiwrap исполняет git-http-backend с указанными CGI-аргументами, переданными GIT-клиентом, и возвращает результат.
Чтобы реализовать такую схему, сначала установим fcgiwrap:
$ sudo apt-get install fcgiwrap
Настраивать его не нужно, все параметры передаются по протоколу FastCGI. Запущен он будет тоже автоматически. Поэтому остается только настроить nginx. Для этого создаем файл /etc/nginx/sites-enabled/git (если такого каталога нет, можно писать в основной конфиг) и пишем в него следующее:
# vi /etc/nginx/sites-enabled/git server { # Висим на порту 8080 listen 8080; # Адрес нашего сервера (не забудь добавить запись в DNS) server_name git.example.ru; # Логи access_log /var/log/nginx/git-http-backend.access.log; error_log /var/log/nginx/git-http-backend.error.log; # Основной адрес для анонимного доступа location / { # При попытке загрузки отправляем юзера на приватный адрес if ($arg_service ~* "git-receive-pack") { rewrite ^ /private$uri last; } include /etc/nginx/fastcgi_params; # Адрес нашего git-http-backend fastcgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend; # Адрес Git-репозитория fastcgi_param GIT_PROJECT_ROOT /srv/git; # Адрес файла fastcgi_param PATH_INFO $uri; # Адрес сервера fcgiwrap fastcgi_pass 127.0.0.1:9001; } # Адрес для доступа на запись location ~/private(/.*)$ { # Полномочия юзера auth_basic "git anonymous read-only, authenticated write"; # HTTP-аутентификация на основе htpasswd auth_basic_user_file /etc/nginx/htpasswd; # Настройки FastCGI include /etc/nginx/fastcgi_params; fastcgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend; fastcgi_param GIT_PROJECT_ROOT /srv/git; fastcgi_param PATH_INFO $1; fastcgi_pass 127.0.0.1:9001; } }
Этот конфиг предполагает три важные вещи:
- Адресом репозитория будет /srv/git, поэтому выставляем соответствующие права доступа:
$ sudo chown -R www-data:www-data /srv/git
- Сам репозиторий должен быть открыт на чтение анонимусами и позволять аплоад по HTTP:
$ cd /srv/git $ git config core.sharedrepository true $ git config http.receivepack true
- Аутентификация осуществляется с помощью файла htpasswd, нужно его создать и добавить в него пользователей:
$ sudo apt-get install apache2-utils $ htpasswd -c /etc/nginx/htpasswd user1 $ htpasswd /etc/nginx/htpasswd user2 ...
На этом все, перезагружаем nginx:
$ sudo service nginx restart
Далее можно подключиться к репозиторию с помощью клиента Git.
Микрокеширование
Представим себе ситуацию с динамичным, часто обновляемым сайтом, который вдруг начинает получать очень большие нагрузки (ну попал он на страницу одного из крупнейших новостных сайтов) и перестает справляться с отдачей контента. Грамотная оптимизация и реализация правильной схемы кеширования займет долгое время, а проблемы нужно решать уже сейчас. Что мы можем сделать?
Есть несколько способов выйти из этой ситуации с наименьшими потерями, однако наиболее интересную идею предложил Фенн Бэйли (Fenn Bailey, fennb.com). Идея в том, чтобы просто поставить перед сервером nginx и заставить его кешировать весь передаваемый контент, но не просто кешировать, а всего на одну секунду. Изюминка здесь в том, что сотни и тысячи посетителей сайта в секунду, по сути, будут генерировать всего одно обращение к бэкенду, получая в большинстве своем кешированную страницу. При этом разницу вряд ли кто-то заметит, потому что даже на динамичном сайте одна секунда обычно ничего не значит.
Конфиг с реализацией этой идеи будет выглядеть не так уж и сложно:
# vi /etc/nginx/sites-enabled/cache-proxy # Настройка кеша proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=microcache:5m max_size=1000m; server { listen 80; server_name example.com; # Кешируемый адрес location / { # Кеш включен по умолчанию set $no_cache ""; # Отключаем кеш для всех методов, кроме GET и HEAD if ($request_method !~ ^(GET|HEAD)$) { set $no_cache "1"; } # В случае если клиент загружает контент на сайт (no_cache = 1), делаем так, чтобы отдаваемые ему данные не кешировались в течение двух секунд и он смог увидеть результат загрузки if ($no_cache = "1") { add_header Set-Cookie "_mcnc=1; Max-Age=2; Path=/"; add_header X-Microcachable "0"; } if ($http_cookie ~* "_mcnc") { set $no_cache "1"; } # Включаем/отключаем кеш в зависимости от состояния переменной no_cache proxy_no_cache $no_cache; proxy_cache_bypass $no_cache; # Проксируем запросы на реальный сервер proxy_pass http://appserver.example.ru; proxy_cache microcache; proxy_cache_key $scheme$host$request_method$request_uri; proxy_cache_valid 200 1s; # Защита от проблемы Thundering herd proxy_cache_use_stale updating; # Добавляем стандартные хидеры proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Не кешируем файлы размером больше 1 Мб proxy_max_temp_file_size 1M; } }
Особое место в этом конфиге занимает строка «proxy_cache_use_stale updating;», без которой мы бы получили периодические всплески нагрузки на бэкенд-сервер из-за запросов, пришедших во время обновления кеша. В остальном все стандартно и должно быть понятно без лишних объяснений.
Приближение прокси к ЦА
Несмотря на повсеместное глобальное увеличение скоростей интернета, физическая удаленность сервера от целевой аудитории все равно продолжает играть свою роль. Это значит, что, если русский сайт крутится на сервере, расположенном где-нибудь в Америке, скорость доступа к нему будет априори медленнее, чем с российского сервера с такой же шириной канала (естественно, если закрыть глаза на все остальные факторы). Другое дело, что размещать серверы за рубежом зачастую выгоднее, в том числе и в плане обслуживания. Поэтому для получения профита, в виде более высоких скоростей отдачи, придется идти на хитрость.
Один из возможных вариантов: разместить основной производительный сервер на Западе, а не слишком требовательный к ресурсам фронтенд, отдающий статику, развернуть на территории России. Это позволит без серьезных затрат выиграть в скорости. Конфиг nginx для фронтенда в этом случае будет простой и всем нам знакомой реализацией прокси:
# vi /etc/nginx/sites-enabled/proxy # Храним кеш 30 дней в 100 Гб хранилище proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=static:32m inactive=30d max_size=100g; server { listen 80; server_name example.com; # Собственно, наш прокси location ~* .(jpg|jpeg|gif|png|ico|css|midi|wav|bmp|js|swf|flv|avi|djvu|mp3)$ { # Адрес бэкенда proxy_pass back.example.com:80; proxy_redirect off; 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_buffer_size 16k; proxy_buffers 32 16k; proxy_cache static; proxy_cache_valid 30d; proxy_ignore_headers "Cache-Control" "Expires"; proxy_cache_key "$uri$is_args$args"; proxy_cache_lock on; } }
Выводы
Сегодня с помощью nginx можно решить множество самых разных задач, многие из которых вообще не связаны с веб-сервером и протоколом HTTP. Почтовый прокси, сервер потокового вещания и интерфейс Git — это только часть таких задач.