2009-04-24

Введение в nginx, часть 1

Данная статья была опубликована в электронном приложением к журналу "Системный администратор"- "Open Source #041 (27.03.2009)"

Введение


nginx (engine x) — это HTTP-сервер и IMAP/POP3 прокси-сервер для UNIX-подобных платформ (FreeBSD и GNU/Linux). Nginx начал разрабатываться Игорем Сысоевым, сотрудником компании Рамблер весной 2002 года, а осенью 2004 года появился первый публично доступный релиз. Он, как и все последующие, распространяется под лицензией BSD.
На данный момент nginx работает на большом количестве высоконагруженных сайтов (среди них — Рамблер, Яндекс, В Контакте, wordpress.com, Wrike и другие). Текущая версия, 0.6.x, рассматривается как стабильная с точки зрения надежности, а релизы из ветки 0.7 считаются нестабильными. При этом важно заметить, что функциональность некоторых модулей будет меняться, вследствие чего могут меняться и директивы, поэтому обратной совместимости в nginx до версии 1.0.0 не гарантируется.

Чем же nginx так хорош и почему его так любят администраторы высоконагруженных проектов? Почему бы просто не использовать Apache?



Почему Apache — плохо?


Для начала нужно объяснить, как вообще работают сетевые серверы. Те, кто знаком с сетевым программированием, знают, что по сути существуют три модели работы сервера:
  1. Последовательная. Сервер открывает слушающий сокет и ждет, когда появится соединение (во время ожидания он находится в заблокированном состоянии). Когда приходит соединение, сервер обрабатывает его в том же контексте, закрывает соединение и снова ждет соединения. Очевидно, это далеко не самый лучший способ, особенно когда работа с клиентом ведется достаточно долго и подключений много. Кроме того, у последовательной модели есть еще много недостатков (например, невозможность использования нескольких процессоров), и в реальных условиях она практически не используется.
  2. Многопроцессная (многопоточная). Сервер открывает слушающий сокет. Когда приходит соединение, он принимает его, после чего создает (или берет из пула заранее созданных) новый процесс или поток, который может сколь угодно долго работать с соединением, а по окончании работы завершиться или вернуться в пул. Главный поток тем временем готов принять новое соединение. Это наиболее популярная модель, потому что она относительно просто реализуется, позволяет выполнять сложные и долгие вычисления для каждого клиента и использовать все доступные процессоры. Пример ее использования — Web-сервер Apache. Однако у этого подхода есть и недостатки: при большом количестве одновременных подключений создается очень много потоков (или, что еще хуже, процессов), и операционная система тратит много ресурсов на переключения контекста. Особенно плохо, когда клиенты очень медленно принимают контент. Получаются сотни потоков или процессов, занятых только отправкой данных медленным клиентам, что создает дополнительную нагрузку на планировщик ОС, увеличивает число прерываний и потребляет достаточно много памяти.
  3. Неблокируемые сокеты/конечный автомат. Сервер работает в рамках одного потока, но использует неблокируемые сокеты и механизм поллинга. Т.е. сервер на каждой итерации бесконечного цикла выбирает из всех сокетов тот, что готов для приема/отправки данных с помощью вызова select(). После того, как сокет выбран, сервер отправляет на него данные или читает их, но не ждет подтверждения, а переходит в начальное состояние и ждет события на другом сокете или же обрабатывает следующий, в котором событие произошло во время обработки предыдущего. Данная модель очень эффективно использует процессор и память, но достаточно сложна в реализации. Кроме того, в рамках этой модели обработка события на сокете должна происходить очень быстро — иначе в очереди будет скапливаться много событий, и в конце концов она переполнится. Именно по такой модели работает nginx. Кроме того, он позволяет запускать несколько рабочих процессов (так называемых workers), т.е. может использовать несколько процессоров.

Итак, представим следующую ситуацию: на HTTP-сервер с каналом в 1 Гбит/с подключается 200 клиентов с каналом по 256 Кбит/с:

Что происходит в случае Apache? Создается 200 потоков/процессов, которые относительно быстро генерируют контент (это могут быть как динамические страницы, так и статические файлы, читаемые с диска), но медленно отдают его клиентам. Операционная система вынуждена справляться с кучей потоков и блокировок ввода/вывода.
Nginx в такой ситуации затрачивает на каждый коннект на порядок меньше ресурсов ОС и памяти. Однако тут выявляется ограничение сетевой модели nginx: он не может генерировать динамический контент внутри себя, т.к. это приведет к блокировкам внутри nginx. Естественно, решение есть: nginx умеет проксировать такие запросы (на генерирование контента) на любой другой веб-сервер (например, все тот же Apache) или на FastCGI-сервер.

Рассмотрим механизм работы связки nginx в качестве «главного» сервера и Apache в качестве сервера для генерации динамического контента:

Nginx принимает соединение от клиента и читает от него весь запрос. Тут следует отметить, что пока nginx не прочитал весь запрос, он не отдает его на «обработку». Из-за этого обычно «ломаются» практически все индикаторы прогресса закачки файлов — впрочем, существует возможность починить их с помощью стороннего модуля upload_progress (это потребует модификации приложения).
После того, как nginx прочитал весь ответ, он открывает соединение к Apache. Последний выполняет свою работу (генерирует динамический контент), после чего отдает свой ответ nginx, который его буферизует в памяти или временном файле. Тем временем, Apache освобождает ресурсы.
Далее nginx медленно отдает контент клиенту, тратя при этом на порядки меньше ресурсов, чем Apache.

Такая схема называется фронтэнд + бэкенд (frontend + backend) и применяется очень часто.


Установка


Т.к. nginx только начинает завоевывать популярность, имеются некоторые проблемы с бинарными пакетами, так что будьте готовы к тому, что его придется компилировать самостоятельно. С этим обычно не возникает проблем, надо лишь внимательно прочитать вывод команды ./configure --help и выбрать необходимые вам опции компиляции, например такие:
./configure \
    --prefix=/opt/nginx-0.6.x \ # префикс установки
    --conf-path=/etc/nginx/nginx.conf \ # расположение конфигурационного файла
    --pid-path=/var/run/nginx.pid \ # ... и pid-файла
    --user=nginx \ # имя пользователя под которым будет запускаться nginx
    --with-http_ssl_module --with-http_gzip_static_module  --with-http_stub_status_module \ # список нужных
    --without-http_ssi_module --without-http_userid_module --without-http_autoindex_module --without-http_geo_module --without-http_referer_module --without-http_memcached_module --without-http_limit_zone_module # ... и не нужных модулей

После конфигурирования стоит запустить стандартный make && make install, после чего можно пользоваться nginx.
Кроме того в Gentoo вы можете воспользоваться ebuild'ом из стандартного дерева портов; в RHEL/CentOS репозиторием epel (в нем расположени nginx 0.6.x) или srpm для версии 0.7, который можно скачать отсюда: http://blogs.mail.ru/community/nginx; в Debian можно воспользоваться пакетом nginx из ветки unstable.

Конфигурационный файл


Конфигурационный файл nginx очень удобен и интуитивно понятен. Называется он обычно nginx.conf и распологается в $prefix/conf/ если расположение не было переопределено при компиляции. Я люблю класть его в /etc/nginx/, также делают и разработчики всех пакетов упомянутых выше.
Структура конфигурационного файла такова:

user nginx; # имя пользователя, с правами которого будет запускаться 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;

    <директивы>
  }
}

Обратите внимание на то, что каждая директива должна оканчиваться точкой с запятой.
Обратное проксирование и FastCGI

Итак, выше мы рассмотрели преимущества схемы frontend + backend, разобрались с установкой, структурой и синтаксисом конфигурационного файла, рассмотрим тепеть как реализовать обратное проксирование в 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.

Если бэкенд (допустим, Apache) расположен на том же хосте, что и nginx, то он «слушает» порт 80 на 127.0.0.1 или другом внутреннем IP-адресе.

Конфигурация nginx в таком случае выглядит следующим образом:
server {
  listen 4.3.2.1:80;
  # устанавливаем заголовок Host и X-Real-IP: к каждому запросу отправляемому на backend
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header Host $host:$proxy_port; 
# или «proxy_set_header Host $host;», если приложение будет дописывать :80 ко всем ссылкам
}

Для того, чтобы приложение различало IP-адреса посетителей, нужно либо поставить модуль mod_extract_forwarded (если оно исполняется сервером Apache), либо модифицировать приложение так, чтобы оно брало информацию о IP-адресе пользователя из HTTP-заголовка X-Real-IP.

Другой вариант бэкенд — это использование FastCGI. В этом случае конфигурация nginx будет выглядеть примерно так:

server {
  <...>

# location, в который будут попадать запросы на php-скрипты
location ~ .php$ {
  fastcgi_pass   127.0.0.1:8888; # определяем адрес и порт fastcgi-сервера,
  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;
}

# благодяря тому что локейшены с регулярными выражениями обладают большим «приоритетом», сюда будут попадать все не-php запросы.
location / {
   root /var/www/html/
}

Статика


Для того, чтобы меньше нагружать бэкенд, статические файлы лучше отдавать только через 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 14d; # добавляем заголовок Expires:
    error_page   404  =  @back; # а если файл не найден, отправляем его в именованный локейшн @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, изображения) вполне хватает одного-двух рабочих процессов.

To be continued


Продолжение статьи уже можно прочитать на этом сайте или в "OpenSource" #042.

Ссылки


Тут можно найти дополнительноую информацию о nginx:

официальный сайт nginx
сайт Игоря Сысоева
Nginx Wiki
nginx с дополнительными сторонними модулями
src.rpm для последних версий nginx
еще несколько хороших ссылок
список рассылки nginx-ru