Pull to refresh

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

Reading time6 min
Views34K


Проведено сравнение производительности ядер 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 5.0 select да однопоточный, асинхронный, со списком (подробнее)
onion 0.5 libev да многопоточный
Pion Network Library 0.5.4 Boost.Asio да многопоточный
POCO C++ Libraries 1.4.3 select да многопоточный (отдельный поток для входящих соединений), с очередью (подробнее)

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

*для тестов переделан по схеме «многопроцессный — один процесс один поток»

Дисквалифицированы
Имя Причина
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, 2 и 3)
  • Количество параллельно открытых запросов клиентов (10, 100, 200, 400, 800)

В каждой конфигурации выполнялось по 20-30 итераций, 2 млн. запросов за итерацию.

Результаты


В первой версии статьи были допущены грубые нарушения в методике тестировании, на что было указано в комментариях пользователями VBart и wentout. Так, в частности, не использовалось строгое разделение задач по ядрам процессора, общее кол-во потоков сервера\клиента превышало допустимые нормы. Также не были отключены опции, влияющие на результаты измерений (AMD Turbo Core), не были указаны погрешности измерений. В текущей версии статьи используется подход, описанный здесь.

Для серверов, запущенных в однопоточном режиме, получены следующие результаты (взяты максимальные медианы по комбинациям серверных/клиентских потоков):
Место Имя Клиентск. потоков Проц. время Запросов
Польз. Сист. Успешных (в сек.) Неуспешных (%)
1 nginx 400 10 10 101210 0
2 mongoose 200 12 15 53255 0
3 libevent 200 16 33 39882 0
4 hand-made 100 20 32 38550 0
5 onion 10 22 33 29230 0
6 POCO 10 25 50 20943 0
7 pion 10 24 83 16526 0
8 node.js 10 23 173 9374 0
9 cpp-netlib 10 100 183 5362 0

Масштабируемость:

В теории, если бы ядер было больше, мы бы наблюдали линейный рост производительности. К сожалению, проверить теорию не представляется возможным — ядер не хватает.

nginx, откровенно говоря, удивил — ведь по сути это готовое, многофункциональное, модульное решение, а результаты на порядок превзошли узкоспециализированные библиотеки. Респект.

mongoose пока сыроват, версия 5.0 не обкатана и ветка находится в активной стадии разработки.

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

node.js уже ругали здесь, не буду так категоричен, но V8 еще пилить и пилить. Что это за high-load решение, которое даже без полезной нагрузки так жадно потребляет ресурсы и выдаёт 10-20% производительности топовых участников тестирования?

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

Погрешность по ministat: No difference proven at 95.0% confidence.

TODO


  • бенчмарк в режиме «клиент и сервер на разных машинах». Нужно быть осторожным — все может упереться в сетевые железки, причём не только модели сетевых карт, а свичей, роутеров и т.п. — всю инфраструктуру между реальными машинами. Для начала можно попробовать прямое подключение;
  • тестирование клиентской 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-серверов.
Tags:
Hubs:
+34
Comments54

Articles