Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

NGINX

Глава 14 из книги "Архитектура приложений с открытым исходным кодом", том 2.
Оригинал: The Architecture of Open Source Applications: nginx
Автор: Andrew Alexeev,
Перевод: А.Кикоть

14.4. Внутреннее устройство nginx

Как уже упоминалось ранее, nginx состоит из ядра и некоторого количества модулей. Ядро nginx отвечает за базовый функционал web-сервера и функционал обратного проксирования web и электронной почты, что позволяет предоставлять доступ к реализованным в ядре сетевым протоколам, создавать необходимые среды исполнения и обеспечивать "бесшовное" взаимодействие между модулями. Тем не менее, большинство функций, специфичных для протоклов и приложений, реализуется модулями, а не ядром.

Внутри себя nginx обрабатывает соединения с помощью каналов (pipeline), цепочек команд (chain) или модулей. Другими словами, для каждой операции находится модуль, который и выполняет соответствующую работу (например, сжатие, преобразование данных, выполнение серверных сценариев, взаимодействие с вышестоящими серверами приложений с применением FastCGI или uwsgi протоколов, взаимодействие с memcached).

Есть два модуля, размещённые где-то между ядром и модулями с реальным функционалом. Это модули http и mail. Эти модули обеспечивают дополнительный уровень абстракции между ядром и низкоуровневыми компонентами. В них реализована обработка последовательностей событий, связанная с протоколами прикладного уровня HTTP, SMTP или IMAP. Вместе с ядром эти модули обеспечивают поддержание правильного порядка вызовов соответствующих функциональных модулей. В то время как протокол HTTP реализован в виде части модуля http, уже есть планы по реализации его в виде отдельных функциональных модулей в связи с необходимостью поддержки других протоколов, например, SPDY (смотрите "SPDY: An experimental protocol for a faster web").

Функциональные модули можно разделить на следующие типы: модули обработки событий, модули обработки фазы, выходные фильтры, модули обработки именованных переменных, модули реализации протоколов, модули взаимодействия с вышестоящими серверами и балансировщики нагрузки. Большинство из этих модулей дополняют HTTP-функциональность nginx, хотя модули обработки событий и реализации протоколов также используются и в модуле mail. Модули обработки событий реализуют зависимые от операционной системы механизмы извещения о событиях, например, kqueue или epoll. Модуль обработки событий, используемый в nginx, зависит от операционной системы, на которой он запущен, и настроек. Модули поддержки протоколов позволяют nginx взаимодействовать с использованием HTTP, TLS/SSL, SMTP, POP3 и IMAP.

Типовой цикл обработки HTTP-запроса выглядит следующим образом:
  1. Клиент посылает HTTP-запрос
  2. Ядро nginx выбирает соответствующий обработчик фазы на основе сопоставления содержимого запроса и настроенных корневых каталогов web-сайтов (location)
  3. Если location настроен в качестве балансировщика нагрузки, то nginx выбирает вышестоящщий сервер для проксирования
  4. Обработчик фазы выполняет свою работу и каждый выходной буфер от него подаётся на вход первого фильтра
  5. Обработчик фазы первого фильтра подаёт данные на второй фильтр
  6. Обработчик фазы второго фильтра подёт данные на третий и т.д.
  7. Подготовленный ответ отсылается клиенту

Порядок вызова модулей в nginx чрезвычайно настраиваемый. Он осуществляется с помощью ряда функций обратного вызова с указателями на исполняемые функции. Однако, недостатоком такого подхода является высокий входной порог для программистов, которые хотели бы писать свои собственные модули, так как им потребуется разобраться в этом и точно указывать как и когда модуль должен работать. Для облегчения этой нагрузки постоянно улучшаются API и документация для разработчиков.

Несколько примеров мест, где могут быть подключены модули:
  • перед считыванием и применением конфигурационного файла
  • для каждой директивы location и server везде, где она встретится
  • момент применения основной конфигурации
  • момент инициализации сервера (например, хоста или порта)
  • момент объединения конфигурационных файлов с основной конфигурацией
  • момент инициализации секции location или её объединения с конфигурацией "родительского" сервера
  • момент запуска и останова управляющего процесса
  • момент запуска или завершения исполнителя
  • момент обработки запроса
  • момент фильтрации заголовка или содержимого ответа
  • момент выбора вышестоящего сервера, инициирования запроса к нему и повторного запроса
  • момент обработки ответа от вышестоящего сервера
  • момент завершения взаимодействия с вышестоящим сервером
Последовательность действий цикла генерации ответа внутри исполнителя выглядит следующим образом:
  1. Запуск цикла ngx_worker_process_cycle()
  2. Обработка событий с использованием зависимых от операционной системы механизмов (таких, как epoll или kqueue)
  3. Приём событий и управление выполнением соответствующих им действий
  4. Обработка или проксирование заголовка или содержимого запроса
  5. Генерация содержимого ответа (заголовок и данные) и передача его клиенту
  6. Завершение обработки запроса
  7. Сброс таймеров и событий

Шаги 5 и 6 выполнения цикла обработки запроса обеспечивают поэтапную генерацию ответа и передачу клиенту.

Более детальное описание процесса обработки HTTP-запроса может выглядеть следующим образом:
  1. Инициализация процесса обработки
  2. Обработка заголовка
  3. Обработка данных запроса
  4. Вызов соответствующего обработчика
  5. Переход от фазы к фазе при обработке

Зачем нужны фазы. При обработке HTTP-запроса nginx проводит его через серию фаз обработки. С каждой фазой могут быть ассоциированы и вызваны обработчики. Таким образом, обработчики, ассоциированные с фазами, выполняют обработку запроса и формирование соответствующего ответа. Соответствие фаз и обработчиков задаётся в конфигурационном файле.

Обработчики фазы обычно выполняют четыре задачи: считывают расположение корневого каталога web-сайта (location), генерируют соответствующие ответы на запросы, выполняют отправку заголовков и данных ответа. Обработчик имеет всего один аргумент: структура с описанием запроса. В структуре описания запроса определяется много полезной информации о запросе клиента: метод запроса, URI и заголовок.

Во время чтения заголовка HTTP-запроса nginx выполняет поиск соответствующего виртуального сервера согласно конфигурации. Если виртуальный сервер найден, то запрос проходит шесть фаз:
  1. преобразование URI на уровне сервера
  2. поиск конфигурации в которой будет обрабатываться запрос
  3. преобразование URI на уровне location (что может привести к запросу на возврат к предыдущей фазе)
  4. проверка доступа
  5. обработка директив try_files
  6. журналирование (запись лога)

Для создания ответа на запрос nginx попытается передать его подходящему обработчику генерирования ответа. В зависимости от настроек location nginx может попробовать безусловные обработчики perl, proxy_pass, flv, mp4 и т.д. Если запрос не соответствует ни одному из вышеперечисленных обработчиков, то будут последовательно перебраны следующие модули в указанном порядке: random index, index, autoindex, gzip_static, static.

Более подробное описание модулей index приведено в документации nginx. Эти модули отвечают за обработку запросов на адреса, завершающиеся слешем. Если специализированные модули не подходят (например, mp4 или autoindex), то запрашиваемые данные считаются файлом или каталогом (то есть, статическими данными) и обрабатываются обработчиком static. Для каталога URI будет автоматически преобразован в адрес, завершающийся слешем, и затем выполнится перенаправление HTTP-запроса.

Данные, сгенерированные обработчиком, передаются на фильтры. Задание ассоциаций для фильтров также выполняется на уровне location с возможностью настройки сразу нескольких фильтров. Фильтры выполняют задачу дополнительной обработки сгенерированного обработчиками потока. Порядок запуска фильтра определяется на этапе компиляции. Все фильтры, как поставляемые вместе с nginx, так и сторонние, могут быть настроены на этапе сборки. В текущей реализации nginx фильтры могут быть применены только к выходным данным. Механизма применения фильтров к входным данным в настоящее время не существует. Применение фильтров к входному потоку появится в будущих версиях nginx.

Фильтры строятся согласно определённому шаблону проектирования. Фильтр вызывается, начинает работу, вызывает следующий фильтр и так до тех пор, пока не будет вызван последний фильтр в этой цепочке. После этого nginx завершает подготовку ответа. Следующему фильтру не обязательно ждать, пока предыдущий завершит работу. Следующий фильтр может начинать работу сразу, как только появится порция обработанных данных предыдущим фильтром (по аналогии с каналами (pipeline) Unix). В свою очередь, генерируемый на выходе последнего фильтра ответ может быть передан клиенту до получения полного ответа от вышестоящего сервера.

Фильтры разделяются на фильтры заголовков и данных. nginx при подготовке ответа на запрос подаёт заголовки и данные на соответствующие фильтры раздельно.

Фильтрация заголовка состоит из трёх основных шагов:
  1. Принятие решения о реакции на запрос
  2. Обработка ответа
  3. Вызов следующего фильтра
Фильтры данных преобразуют содержимое ответа. Могут быть приведены следующие примеры фильтров:
  • серверные включения
  • фильтрация XSLT
  • фильтрация изображений (например, изменение размера "на лету")
  • конвертирование кодировки
  • сжатие gzip
  • передача данных с использованием механизма Chunked transfer encoding

После завершения работы цепочки фильтров ответ передаётся на отправку. Одновременно с отправкой могут работать два специальных фильтра copy и postpone. Фильтр copy отвечает за заполнение буферов в оперативной памяти соответствующим ответом, который может храниться во временном каталоге прокси. Фильтр postpone используется для выполнения под-запросов.

Подзапросы являются очень важным механизмом обработки по типу "запрос-ответ". Подзапросы также являются одним из самых мощных механизмов nginx. В результате применения подзапросов nginx может вернуть в качестве результата другой URL, а не тот, что запрашивал клиент. В некоторых библиотеках разработки (frameworks) это называется внутренним перенаправлением. Однако, nginx идёт ещё дальше - помимо выполнения нескольких подзапросов с несколькими фильтрами и объединением всего этого в один ответ, подзапросы могут быть вложенными и выстроенными в иерархическую структуру. Подзапросы могут выполнять свои под-подзапросы, которые в свою очередь могут инициировать под-под-подзапросы. Подзапросы можно связать с файлами на диске, другими обработчиками или вышестоящими серверами. Подзапросы - это очень полезный механизм, позволяющий дополнять ответ, выполняя под-запросы на основе данных подготовленного ранее ответа. Например, модуль SSI (серверные включения) использует фильтр для анализа содержимого возвращаемого документа, а затем подменяет директивы include к конкретным URL. Или может быть приведён ещё такой пример: использование фильтра для извлечения документа по указанной ссылке URL, его обработка, сохранение и возвращение ссылки URL на уже обработанный документ.

Перенаправление запросов (upstream) и проксирование также заслуживают упоминания. Перенаправление можно описать как совокупность обработчика данных запроса и обратного прокси (proxy_pass). Upstream-модули предназначены в основном для отправки запросов на вышестоящий сервер (или встроенную подсистему обработки - backend) и получения ответов от него. Здесь вызов фильтров не предусмотрен. Что точно делает upstream-модуль, так это запускает функции обратного вызова, когда вышестоящий сервер готов для записи или чтения. В nginx реализованы следующие функции обратного вызова:
  1. создание буфера запросов (или цепочек из них), который будет направлен в вышестоящий сервер
  2. повторная инициализация или сброс соединения с вышестоящим сервером (что происходит непосредственно перед повторным запросом)
  3. обработка первых битов ответа вышестоящего сервера и сохранение указателей на полученные от него данные
  4. прерывание запросов (что происходит при преждевременном завершении работы клиента)
  5. завершение обработки запроса при получении всех данных от вышестоящего сервера
  6. удаление части ответа (например, завершающей части пакета - trailer)

Модули балансировки нагрузки дополняют обработчик proxy_pass и предоставляют возможность выбора вышестоящего сервера в случае доступности нескольких. Балансировщик нагрузки включается соответствующей дерективой в конфигурационном файле, обеспечивает дополнительный функционал при инициализации вышестоящего сервера (разрешение вышестоящего сервера по DNS-имени и т.д.), инициализирует структуры для описания соединения, принимает решение о выборе вышестоящего сервера для перенаправления запроса и обновляет статистику. В настоящее время nginx поддерживает два способа балансировки нагрузки: циклический (round-robin) и IP-хеш (IP-hash).

Upstream-модули и модули балансировки включают в себя механизм обнаружения сбоев вышестоящих серверов и автоматического перенаправления запросов на оставшиеся серверы. В будущих версиях планируется много работы для дальнейшего развития этого функционала. В частности, планируется значительно улучшить механизмы проверки состояния вышестоящих серверов и распределение нагрузки между ними в соответствии с полученной оценкой при проверке.

Также есть несколько других интересных модулей, обеспечивающих применение дополнительного набора именованных переменных (variables) в конфигурационном файле. В то время, как в различных модулях создаются и обновляются наборы именованных переменных, есть два модуля, полностью посвящённых именованным переменным geo и map. Модуль geo используется для облегчения отслеживания клиентов по их IP-адресам. Этот модуль может создавать различные именованные переменные в зависимости от IP-адреса клиента. Модуль map позволяет создавать именованные переменные на основе других именованных переменных, предоставляя, по-сути, гибкий механизм отображения имён хостов и динамических переменных. Этот тип модулей может быть назван - "обработчик именованных переменных".

В некоторой степени Apache повлиял на создание в nginx механизма выделения памяти внутри исполнителя. В общем виде описание механизма управления памятью в nginx имеет следующий порядок: для каждого соединения динамически выделяются необходимые буферы; для них устанавливается соответствие с соединением; буферы используются для хранения, обработки заголовка и данных запроса, отправки ответа; после завершения соединения память освобождается. Важно отметить, что nginx пытается избежать копирования данных в оперативной памяти и максимально возможно использует передачу по указателю на значение вместо вызова функции memcpy.

Если немного подробнее, то после генерирования ответа с помощью модуля полученный результат помещается в буфер памяти, который добавляется в цепочку буферов, ассоциированных с соединением. Последующие обработки работают с этой же цепочкой буферов. Цепочки буферов в nginx устроены довольно сложно, так как есть несколько сценариев их работы, зависящих от типа модуля. Например, может оказаться довольно сложным управление буферами при реализации модуля фильтрации данных запроса. Этот модуль должен работать только с одним буфером (звеном цепочки) и в то же время он должен решить, следует ли перезаписать входной буфер, заменить его на новый или вставить в цепочку другой буфер перед или после него. Может быть ещё сложнее: иногда модуль получает несколько буферов и ему приходится обрабатывать неполную цепочку буферов. Тем не менее, nginx в настоящее время для работы с цепочками буферов предоставляет только низкоуровневый API. Поэтому для реализации сторонних модулей разработчик должен очень хорошо ориентироваться в этой мистической части nginx.

Отметим, что всё время жизни соединения ему соответствует ряд буферов памяти. То есть, длительными соединениями блокируется некоторая часть оперативной памяти. В то же время, на поддержание простаивающего соединения nginx тратит всего 550 байт оперативной памяти. В качестве оптимизации в будущих версиях nginx может быть реализовано повторное и совместное использование буферов длительных соединений.

Задача управления оперативной памятью в nginx решается распределителем пула (pool allocator). Области разделяемой памяти используются для организации мьютексов, кеша метаданных, кеша SSL-сессий и хранения информации об управлении пропускной способностью (лимиты). В nginx для управления разделяемой памятью используется slab-распределитель. Для потокобезопасного использования разделяемой памяти используются механизмы блокировки доступа (мьютексы и семафоры). Также при организации сложных структур данных в nginx используются красно-чёрные деревья (red-black tree). Красно-чёрные деревья используются для хранения кеша метаданных в разделяемой памяти, отслеживания нерегулярных значений location и некоторых других задач.

К сожалению, всё вышеперечисленное не было документировано в последовательной и простой манере, что делает разработку сторонних модулей для nginx довольно сложной. Тем не менее, некоторые хорошие материалы существуют. Например, материалы, изданные Эваном Миллером (Evan Miller). Такие материалы требуют огромной работы по обратному инжинирингу и изучению работы модулей, что для многих сравнимо с чёрной магией.

Несмотря на определённые трудности, связанные с разработкой сторонних модулей, в сообществе пользователей nginx существует большое количество полезных модулей. Например, встроенный интерпретатор с языка Lua, дополнительные модули балансировки нагрузки, модуль полной поддержки WebDAV, модуль расширенного управления кешем и другие интересные модули, что авторы этой главы поощряют и будут поддерживать в будущем.


Это произведение распространяется в соответствии с лицензией Creative Commons Attribution 3.0 Unported license. Для получения более детальной информации, пожалуйста, ознакомьтесь с полным описанием лицензии.


Далее: 14.5. Выводы