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–320×240. Далее на сайт можно повесить флеш-плеер и кнопки выбора качества, которые будут подсовывать плееру тот или иной адрес вещателя.

Ну и напоследок пример вещания музыки в сеть:

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;
    }
}

Этот конфиг предполагает три важные вещи:

  1. Адресом репозитория будет /srv/git, поэтому выставляем соответствующие права доступа:
    $ sudo chown -R www-data:www-data /srv/git
    
  2. Сам репозиторий должен быть открыт на чтение анонимусами и позволять аплоад по HTTP:
    $ cd /srv/git
    $ git config core.sharedrepository true
    $ git config http.receivepack true
    
  3. Аутентификация осуществляется с помощью файла htpasswd, нужно его создать и добавить в него пользователей:
    $ sudo apt-get install apache2-utils
    $ htpasswd -c /etc/nginx/htpasswd user1
    $ htpasswd /etc/nginx/htpasswd user2
    ...
    

На этом все, перезагружаем nginx:

$ sudo service nginx restart

Далее можно подключиться к репозиторию с помощью клиента Git.

Настраиваем Git-прокси
Настраиваем 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 — это только часть таких задач.

By Ruslan Novikov

Интернет-предприниматель. Фулстек разработчик. Маркетолог. Наставник.