Бенчмарк HTTP-серверов (С/C++) в FreeBSD

в 9:02, , рубрики: c++, c++ библиотеки, freebsd, высокая производительность, Сетевые технологии, метки: , , ,

Бенчмарк HTTP серверов (С/C++) в FreeBSD

Проведено сравнение производительности ядер HTTP-серверов, построенных с использованием семи C/C++ библиотек, а также (в познавательных целях) — других готовых решений в этой области (nginx и node.js).

HTTP-сервер — это сложный и интересный механизм. Есть мнение, что плох программист, не написавший свой компилятор, я бы заменил «компилятор» на «HTTP-сервер»: это и парсер, и работа с сетью, и асинхронность с многопоточностью и много чего еще....

Тесты по всем возможным параметрам (отдача статики, динамики, всевозможные модули шифрования, прокси и т.п.) — задача не одного месяца кропотливой работы, поэтому задача упрощена: будем сравнивать производительность ядер. Ядро HTTP-сервера (как и любого сетевого приложения) — это диспетчер событий сокетов и некий первичный механизм их обработки (реализованный в виде пула потоков, процессов и т.п.). Сюда же можно отнести парсер HTTP-пакетов и генератор ответов. На первый взгляд, все должно свестись к тестированию возможностей того или иного системного механизма обработки асинхронных событий (select, epoll и т.п.), их мета-обёрток (libev, boost.asio и др.) и ядра ОС, однако конкретная реализация в виде готового решения дает существенную разницу в производительности.

Был реализован свой вариант HTTP-сервера на libev. Конечно, реализована поддержка небольшого подмножества требований пресловутого rfc2616 (вряд ли ее полностью реализует хоть один HTTP-сервер), лишь необходимый минимум для соответствия требованиям, предъявляемым к участникам данного тестирования,

  1. Слушать запросы на 8000-ом порту;
  2. Проверять метод (GET);
  3. Проверять путь в запросе (/answer);
  4. Ответ должен содержать:
                HTTP/1.1 200 OK
                Server: bench
                Connection: keep-alive
                Content-Type: text/plain
                Content-Length: 2
                42
            

  5. На любой другой методпуть — должен возвращаться ответ с кодом ошибки 404 (страница не найдена).

Как видите — никаких расширений, обращений к файлам на диске, интерфейсов шлюза и т.п. — все максимально упрощено.
В случаях, когда сервер не поддерживает keep-alive соединения (кстати, этим отличился только cpp-netlib), тестирование проводилось в соотв. режиме.

Предыстория

Изначально стояла задача реализовать HTTP-сервер с нагрузкой в сотни миллионов обращений в сутки. Предполагалось, что будет относительно небольшое кол-во клиентов, генерирующих 90% запросов, и большое число клиентов, генерирующих оставшиеся 10%. Каждый запрос нужно отправлять дальше, на несколько других серверов, собирать ответы и возвращать результат клиенту. От скорости и качества ответа зависел весь успех проекта. Поэтому я не мог просто взять и использовать первое попавшееся готовое решение. Нужно было получить ответы на следующие вопросы:

  1. Стоит ли изобретать свой велосипед или же использовать существующие решения?
  2. Подходит ли node.js для высоконагруженных проектов? Если да, то выкинуть заросли С++ кода и переписать все в 30 строк на JS

На повестке стояли и менее значимые вопросы, например, влияет ли HTTP keep-alive на производительность? (спустя год ответ был озвучен здесь — влияет, и весьма существенно).

Разумеется, сначала был изобретён свой велосипед, затем появился node.js (узнал про него два года назад), ну а потом захотелось узнать: насколько существующие решения эффективнее собственного, не зря ли было потрачено время? Собственно, так и появился данный пост.

Подготовка

Железо

  • Процессор: CPU: AMD FX(tm)-8120 Eight-Core Processor
  • Cеть: localhost (почему — см. в TODO)

Софт

  • ОС: FreeBSD 9.1-RELEASE-p7

Тюнинг
Обычно в нагрузочном тестировании сетевых приложений принято изменять следующий стандартный набор настроек:

/etc/sysctl.conf

kern.ipc.somaxconn=65535
net.inet.tcp.blackhole=2
net.inet.udp.blackhole=1
net.inet.ip.portrange.randomized=0
net.inet.ip.portrange.first=1024
net.inet.ip.portrange.last=65535
net.inet.icmp.icmplim=1000

/boot/loader.conf
kern.ipc.semmni=256
kern.ipc.semmns=512
kern.ipc.semmnu=256
kern.ipc.maxsockets=999999
kern.ipc.nmbclusters=65535
kern.ipc.somaxconn=65535
kern.maxfiles=999999
kern.maxfilesperproc=999999
kern.maxvnodes=999999
net.inet.tcp.fast_finwait2_recycle=1

Однако в моем тестировании они не приводили к повышению производительности, а в некоторых случаях даже приводили к значительному замедлению, поэтому в финальных тестах никаких изменений настроек в системе не проводилось (т.е. все настройки по умолчанию, ядро GENERIC).

Участники

Библиотечные

Имя Версия События Поддержка keep-alive Механизм
cpp-netlib 0.10.1 Boost.Asio нет многопоточный
hand-made 1.11.30 libev да многопроцессный (один поток на процесс), асинхронный
libevent 2.0.21 libevent да однопоточный*, асинхронный
mongoose 4.1** poll да многопоточный (отдельный поток для входящих соединений), с очередью (подробнее)
onion 0.5 epoll да многопоточный
Pion Network Library 0.5.4 Boost.Asio да многопоточный
POCO C++ Libraries 1.4.3 epoll/select да многопоточный (отдельный поток для входящих соединений), с очередью (подробнее)

Готовые решения

Имя Версия События Поддержка keep-alive Механизм
Node.js 0.10.17 libuv да модуль cluster (многопроцессная обработка)
nginx 1.4.4 epoll, select, kqueue да многопроцессная обработка

*для тестов переделан по схеме «многопроцессный — один процесс один поток»
**Авторы mongoose буквально на днях выкатили 5 версию, в которой практически с нуля переделана архитектура. Как раз когда все тесты с 4.2 были закончены. Но новая версия все ещё сырая, keep-alive поломан, поэтому решено было остаться на 4.2. Но не тут то было. Создатели решили убрать отовсюду версию 4.2. Максимально доступная стабильная версия стала 4.1. Пришлось откатиться (и переделывать под нее код).

Дисквалифицированы

Имя Причина
nxweb только Linux
g-wan только Linux (и вообще...)
libmicrohttpd постоянные падения при нагрузках
yield ошибки компиляции
EHS ошибки компиляции
libhttpd синхронный, HTTP/1.0, не дает поменять заголовки
libebb ошибки компиляции, падения

В качестве клиента использовалось приложение от разработчиков lighttpd — weighttpd. Изначально планировалось использовать httperf, как более гибкий инструмент, но он постоянно падает. Кроме того, weighttpd основан на libev, который гораздо лучше подходит для FreeBSD, чем httperf с его линуксовым select-ом. В качестве главного тестового скрипта (обертки над weighttpd с подсчётом расхода ресурсов и пр.) рассматривался gwan-овский ab.c, переделанный под FreeBSD, но в последствии был переписан с нуля на Пайтоне (bench.py в приложении).

Клиент и сервер запускались на одной и той же физической машине.
В качестве переменных значений использовались:

  • Количество серверных потоков (1 и 8).
    Исключение — mongoose. У него кол-во потоков сервера всегда равно количеству открытых соединений. При попытках задать макс. кол-во потоков больше 50 (значение по умолчанию) — mongoose начинал падать.
  • Количество параллельно открытых запросов клиентов (10, 50, 100, 200, 400, 800)

В каждой конфигурации выполнялось по 5 итераций (всего — 2*6*5=60 итераций для каждого сервера). Ожидалось, что разные сервера покажут себя лучше в разных конфигурациях.

Результаты

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

Для 1 млн. запросов имеем:
Бенчмарк HTTP серверов (С/C++) в FreeBSD

Место Имя Потоков Время Запросов
Серверных Клиентских Польз. Сист. Успешных (в сек.) Неуспешных (%)
1 hand-made 8 200 23.82987 8.13092 168947 0
2 mongoose 50 200 8.690878 14.812137 154340 0
3 libevent 8 400 24.15859 14.827379 139770 0
4 nginx 8 100 8.379323 9.861154 136485 0
5 POCO 8 50 40.928956 15.933258 86716 0
6 onion 8 50 9.55005 6.030781 64403 0
7 pion 8 400 81.543614 17.370284 48359 0
8 node.js 8 100 121.474981 12.551338 46595 0
9 cpp-netlib 1 100 87.590467 29.244296 16673 0

График потерь (запросы, завершенные ошибками либо закрытием соединения для таблицы выше):
Бенчмарк HTTP серверов (С/C++) в FreeBSD

cpp-netlib показал странный результат: производительность одного серверного потока оказалась выше, чем восьми. Мало того что он единственный не поддерживал HTTP keep-alive соединения, так ещё и падал где-то в недрах boost-а, было проблематично выполнить все 60 итераций подряд, но в итоге получилось. Однозначно, решение сырое, документация — устаревшая. Законное последнее место.

С победителями (первые 4 места показали приблизительно одинаковую скорость) не все так однозначно. Если посмотреть подробный отчёт запусков (bench_res.csv в приложении) то увидим следующее:

  • В hand-made решении в режиме «1 поток сервера на 800 потоков клиента» (самый «экстремальный» режим работы) процент потерянных запросов колеблется от 15% до 18%. Это плохо.
  • У mongoose-а проблема с большим количеством соединений (вызвано ограничением макс. кол-ва открытых соединений сервера — 50). Потери доходят до 58%. И это наблюдается практически во всех режимах «400 клиентских потоков и выше».
  • У libevent и nginx потерь нет. На все запросы всегда приходят ответы.

Сравним libevent и nginx. Несмотря на то, что по скорости они практически одинаковы, nginx значительно проигрывает libevent-у в режимах с 400 клиентскими потоками и незначительно в режимах с 800 клиентскими потоками. Поэтому, именно libevent (доработанный до схемы «создаём несколько процессов на одном порте») я бы назвал победителем соревнований, но решать вам.

Не буду хвалить свою реализацию — в общем зачёте она держит 10 первых мест. Несмотря на этот факт, это лишь наколенная имитация HTTP-сервера. Данная реализация более 2-х лет обрабатывает сотни миллионов HTTP запросов в день от реальных HTTP-клиентов, собирает ответы с более сотни различных HTTP-серверов и не нуждается в оправданиях и попытках приравнять ее к полноценным решениям.

node.js, откровенно говоря, разочаровал (не так категорично, как выразился товарищ здесь, но V8 еще пилить и пилить. Что это за high-load решение, которое даже без полезной нагрузки так жадно потребляет ресурсы и выдаёт 1/4 производительности топовых участников тестирования? Можно добавить, что при создании полезной нагрузки (например, использования модуля async) процессорное время начинает убиваться ещё в 9000 раз больше, при этом памяти каждый процесс из cluster-а отъедает более 500 Мб. Возможно, это проявляется лишь на FreeBSD, но я уверен, что проблема в node.js.

По-поводу HTTP keep-alive on/off: если в посте разница доходила до x2 раз, то в моих тестах разница доходит до х10.

TODO

  • бенчмарк в режиме «клиент и сервер на разных машинах». Многие придерживаются теории, что де именно такой режим покажет реальную картину. С другой стороны очевидно, что все упрётся в сетевые железки, причём не только модели сетевых карт, а свичей, роутеров и т.п. — всю инфраструктуру между реальными машинами. В подключении же напрямую смысла не больше, чем через localhost;
  • тестирование клиентской HTTP API (организовать в виде сервера и прокси). Проблема в том, что далеко не все библиотеки предоставляют API для реализации HTTP-клиента. С другой стороны, некоторые популярные библиотеки (libcurl, например) предоставляют исключительно клиентский набор API;
  • использование других HTTP-клиентов. httperf не использовался по указанным выше причинам, ab — по многим отзывам устарел и не держит реальных нагрузок. Но здесь представлены пара десятков решений, какие-то из них стоило бы сравнить;
  • аналогичный бенчмарк в Linux-среде. Вот это должна быть интересная тема (как минимум — новая волна для холиварных обсуждений);
  • прогнать тесты на топовом Intel Xeon с кучей ядер.

По последним двум пунктам — хотел попросить сообщество предоставить доступ к подобному готовому стенду.

Ссылки

Stress-testing httperf, siege, apache benchmark, and pronk — HTTP-клиенты для нагрузочного тестирования серверов.
Performance Testing with Httperf — советы и рекомендации о проведении бенчмарков.
ApacheBench & HTTPerf — описание процесса бенчмарка от G-WAN.
Warp — еще один high-load HTTP-сервер с претензией, Haskell.

Приложение

В приложении вы найдёте исходники и результаты всех итераций тестирования, а также подробные сведения по сборке и установке HTTP-серверов.

Автор: robert_ayrapetyan

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js