Проведено сравнение производительности ядер HTTP-серверов, построенных с использованием семи C/C++ библиотек, а также (в познавательных целях) — других готовых решений в этой области (nginx и node.js).
HTTP-сервер — это сложный и интересный механизм. Есть мнение, что плох программист, не написавший свой компилятор, я бы заменил «компилятор» на «HTTP-сервер»: это и парсер, и работа с сетью, и асинхронность с многопоточностью и много чего еще....
Тесты по всем возможным параметрам (отдача статики, динамики, всевозможные модули шифрования, прокси и т.п.) — задача не одного месяца кропотливой работы, поэтому задача упрощена: будем сравнивать производительность ядер. Ядро HTTP-сервера (как и любого сетевого приложения) — это диспетчер событий сокетов и некий первичный механизм их обработки (реализованный в виде пула потоков, процессов и т.п.). Сюда же можно отнести парсер HTTP-пакетов и генератор ответов. На первый взгляд, все должно свестись к тестированию возможностей того или иного системного механизма обработки асинхронных событий (select, epoll и т.п.), их мета-обёрток (libev, boost.asio и др.) и ядра ОС, однако конкретная реализация в виде готового решения дает существенную разницу в производительности.
Был реализован свой вариант HTTP-сервера на libev. Конечно, реализована поддержка небольшого подмножества требований пресловутого rfc2616 (вряд ли ее полностью реализует хоть один HTTP-сервер), лишь необходимый минимум для соответствия требованиям, предъявляемым к участникам данного тестирования,
- Слушать запросы на 8000-ом порту;
- Проверять метод (GET);
- Проверять путь в запросе (/answer);
- Ответ должен содержать:
HTTP/1.1 200 OK Server: bench Connection: keep-alive Content-Type: text/plain Content-Length: 2 42
- На любой другой методпуть — должен возвращаться ответ с кодом ошибки 404 (страница не найдена).
Как видите — никаких расширений, обращений к файлам на диске, интерфейсов шлюза и т.п. — все максимально упрощено.
В случаях, когда сервер не поддерживает keep-alive соединения (кстати, этим отличился только cpp-netlib), тестирование проводилось в соотв. режиме.
Предыстория
Изначально стояла задача реализовать HTTP-сервер с нагрузкой в сотни миллионов обращений в сутки. Предполагалось, что будет относительно небольшое кол-во клиентов, генерирующих 90% запросов, и большое число клиентов, генерирующих оставшиеся 10%. Каждый запрос нужно отправлять дальше, на несколько других серверов, собирать ответы и возвращать результат клиенту. От скорости и качества ответа зависел весь успех проекта. Поэтому я не мог просто взять и использовать первое попавшееся готовое решение. Нужно было получить ответы на следующие вопросы:
- Стоит ли изобретать свой велосипед или же использовать существующие решения?
- Подходит ли 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
Тюнинг
Обычно в нагрузочном тестировании сетевых приложений принято изменять следующий стандартный набор настроек:
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
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 млн. запросов имеем:
Место | Имя | Потоков | Время | Запросов | |||
---|---|---|---|---|---|---|---|
Серверных | Клиентских | Польз. | Сист. | Успешных (в сек.) | Неуспешных (%) | ||
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 |
График потерь (запросы, завершенные ошибками либо закрытием соединения для таблицы выше):
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