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

UnixForum





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

Библиотека Warp

Глава 11 из книги "Производительность приложений с открытым исходным кодом".

Оригинал: Warp
Авторы: Kazu Yamamoto, Michael Snoyman, Andreas Voellmy
Перевод: Н.Ромоданов

Ключевые принципы

При реализации нашего высокопроизводительного сервера на языке Haskell мы придерживались следующих четырех ключевых принципов:

  1. Сводить к минимуму использование системных вызовов настолько, насколько это возможно
  2. Использовать специальные реализации функций и избегать повторных вычислений
  3. Избегать блокировок
  4. Использовать надлежащие структуры данных

Использование минимально возможного количества системных вызовов

Хотя системные вызовы в большинстве современных операционных систем, как правило, не слишком ресурсоемки, они, если их вызывать часто, могут быть причиной существенной вычислительной нагрузки. Действительно, в Warp при обслуживании каждого запроса выполняется несколько системных вызовов, в том числе recv(), send() и sendfile() (системный вызов, который позволяет выполнять копирование нулей в файл). Благодаря механизму кэширования, который описан в разделе «Таймеры для файловых дескрипторов», часто при обработке отдельного запроса можно не пользоваться некоторыми системными вызовами, например, вызовами open(), stat() и close().

Чтобы увидеть, какие системные вызовы действительно используются, мы можем воспользоваться командой strace. Когда мы с помощью команды strace наблюдали за поведением сервера nginx, мы заметили, что он использовал вызов accept4(), о котором в тот момент мы не знали.

С помощью стандартной сетевой библиотеки языка Haskell создается слушающий сокет с набором неблокирующих флагов. Когда из слушающего сокета происходит новое подключение, то также необходимо установить соответствующий сокет в неблокирующий режим. В сетевой библиотеке это реализуется при помощи двукратного вызова функции fcntl(): один раз для того, чтобы получить текущие флаги и второй раз для того, чтобы установить флаги в неблокирующий режим.

В Linux флаг отключения блокирования подключенного сокета всегда сбрасывается даже в том случае, если сокет неблокируемый. В Linux системный вызов accept4() является расширенной версией вызова accept(). Он может, при приеме, устанавливать флаг неблокирующего режима. Так что, если мы используем вызов accept4(), то мы можем избежать двух ненужных обращений к функции fcntl(). Наш патч, в котором на Linux используется вызов accept4(), уже добавлен в сетевую библиотеку.

Специализированные функции и попытка избежать перевычислений

В компиляторе GHC предоставляется средство профилирования, но у него есть ограничение: правильно профилирования возможно, только в случае, если программа работает не в фоновом режиме и не порождает дочерних процессов. Если мы хотим профилировать реальную активность серверов, нам нужно обратить на это особое внимание.

В сервере mighty есть такой механизм. Предположим, что N является количеством рабочих процессов, указываемых в конфигурационном файле сервера mighty. Если N больше или равно 2, то mighty создает N дочерних процессов и родительский процесс просто работает для того, чтобы передавать сигналы. Однако, если N = 1, то mighty не создает каких-либо дочерних процессов. Вместо этого выполняемый процесс самостоятельно обслуживает запрос HTTP. Кроме того, mighty в случае, если включен режим отладки, может работать в своем собственном терминале.

Когда мы выполнили профилирование сервера mighty, мы были удивлены тем, что большую часть процессорного времени потребляет стандартная функция форматирования строкового представления дат. Сервер HTTP, как известно большинству, должен в полях заголовка, например, в Date, Last-Modified и т.д. возвращать строковые даты в формате GMT:

Date: Mon, 01 Oct 2012 07:38:50 GMT

Поэтому для генерации строк с датой в формате GMT мы реализовали специальный форматер. Сравнение с помощью бенчмарка criterion нашей специализированной функции и стандартной реализации языка Haskell показало, что наша реализация работала намного быстрее. Но если сервер HTTP принимает более одного запроса в секунду, то он снова и снова выполняет одно и то же форматирование. Поэтому для строковых дат мы также добавили механизм кэширования.

Мы также объясняем использование специализированных функций и отказ от перевычислений значений в разделах «Пишем парсер» и «Композер ответа HTTP-заголовка».

Попытка избежать блокировок

Ненужные блокировки являются злом для программирования. В нашем коде иногда неявно применяются ненужные блокировки из-за того, что эти блокировки используются внутри системы или библиотеки на этапе выполнения. Чтобы реализовать высокопроизводительные сервера, нам нужно выявлять такие блокировки и избегать их, если это возможно. Стоит отметить, что блокировки становятся намного более критичными, когда происходит управление с помощью менеджера параллельного ввода/вывода. Мы поговорим о том, как определить и избежать блокировок в разделе «Таймеры подключений» и «Выделение памяти».

Использование надлежащих структур данных

Стандартной структурой данных в языке Haskell для строк является структура String, которая представляет собой связанный список символов Unicode. Поскольку программирование списков является сердцем функционального программирования, структура String удобна для многих целей. Но для высокопроизводительных серверов, структура списка является слишком медленной, а Unicode слишком сложным, поскольку протокол HTTP основывается на потоках байтов byte. Поэтому вместо этой структуры мы для представления строк (или буферов) пользуемся структурой ByteString. ByteString является массивом байтов с метаданными. Благодаря этим метаданным, можно пользоваться этой структурой данных из нескольких процессов без ее копирования. Это подробно описывается в разделе «Пишем парсер».

Другими примерами правильных структур данных являются Builder и сдвоенная ссылка IORef. О них рассказывается в разделах «Композер заголовка HTTP-ответа» и «Таймеры подключений», соответственно.


Продолжение статьи: Парсер HTTP-запроса.