Библиотека сайта rus-linux.net
Библиотека Warp
Глава 11 из книги "Производительность приложений с открытым исходным кодом".
Оригинал: Warp
Авторы: Kazu Yamamoto, Michael Snoyman, Andreas Voellmy
Перевод: Н.Ромоданов
Ключевые принципы
При реализации нашего высокопроизводительного сервера на языке Haskell мы придерживались следующих четырех ключевых принципов:
- Сводить к минимуму использование системных вызовов настолько, насколько это возможно
- Использовать специальные реализации функций и избегать повторных вычислений
- Избегать блокировок
- Использовать надлежащие структуры данных
Использование минимально возможного количества системных вызовов
Хотя системные вызовы в большинстве современных операционных систем, как правило, не слишком ресурсоемки, они, если их вызывать часто, могут быть причиной существенной вычислительной нагрузки. Действительно, в 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-запроса.