Pull to refresh

nxweb – HTTP сервер для приложений на Си

Reading time10 min
Views16K
nxweb – это новый встраиваемый высокопроизводительный веб-сервер для приложений на Си. По функциональности это фреймворк для написания обработчиков HTTP запросов. Аналоги: G-WAN/libevent/Mongoose, Apache/mod_<ваш любимый язык>, Tomcat, Node.js. Разработчик – Ярослав Ставничий. Меня проект заинтересовал прежде всего тем, что он представляет реальную альтернативу существующим решениям, каждое из которых обладает своими недостатками. Выбор – это хорошо. Возможно, и вам понравится сочетание особенностей, плюсов и минусов этого сервера.

Под катом подробная информация о проекте из интервью с разработчиком.

Q: Расскажи о себе, чем занимаешься, где работаешь?

Yaroslav: Я давно занимаюсь разработкой софта. Компания Nexoft. Пишу в основном на Java, но и C/C++ периодически вспоминаю, когда нужно что-то скоростное сделать. Например, движок баннерокрутилки для сайта с высокой посещаемостью.

Q: Значит nxweb создавался с целью – крутить баннеры?

Yaroslav: Ну, не только ради баннеров, но это одна из первых задач. Конкретная задача, по крайней мере, не абстрактная.

Q: Высокая посещаемость – это сколько запросов в секунду?

Yaroslav: Сейчас приходит около 100 запросов в секунду. Часть из них отправляется на Tomcat. Но в каждую HTML страницу вставляется до 20 кодов баннеров. Серверу, в принципе, тяжело, поэтому хочется простой задачей отдачи коротких HTML фрагментов сильно его не обременять. Может, конечно, и на перле это всё работало бы, но мы легких путей не ищем.

Собственно, движок такой у меня был давно, написан как модуль для Apache, но в последнее время все проекты переношу на nginx.

Вообще писать модули под Apache и nginx – не самое приятное занятие. Там куча мишуры, навязанной заботой о переносимости. К тому же, они многопроцессны, а это отдельная беда. С потоками (threads) все намного проще. Общая память – бесценна. Я привык к Java, там именно так. Полез внутрь nginx, уж очень все там громоздко, плюс, опять же, shared memory и пр.

Решил попробовать реализовать свое приложение на микро-движке типа Mongoose, натыкался на него в сети ранее. Стал копать, нашел еще несколько альтернатив, в т.ч. G-WAN. Очень всё в нем понравилось, кроме closed-source. Я пробовал G-WAN именно как сервер приложений, а не как отгрузчик статики. Но довольно быстро стал натыкаться на глюки, найти и исправить которые не представляется возможным из-за закрытости кода.

Итак, написал крутилку на Mongoose, стал тестировать быстродействие и обнаружил, что оно уж больно низкое по сравнению с G-WAN. Это уже потом я догадался 2500 тредов запустить, а поначалу думал, что 8-16 хватит. С таким количеством там вообще никак.

Q: Потому что mongoose использует один OS поток на каждый запрос?

Yaroslav: Да, собственно, 2500 тредов – тоже не решение. Памяти они жрут тонну, и, опять же, появится 2501-й запрос – и привет…

Стал копать глубже, нашел microhttpd и libevent. По производительности, если сравнивать с G-WAN, они выглядели бледно. Пока я со всем этим разбирался, копался в исходниках, пришло понимание, что написать веб-сервер не так уж и сложно. В mongoose, например, и так весь HTTP ответ надо вручную писать разработчику модуля.

libevent выглядел наиболее обещающим, но однопоточным. Решил переделать то же на libev (облегченная альтернатива libevent). Так родился nxweb. Главная первоначальная цель – это веб-приложения на C, а не полнофункциональный сервер. Если уж разрабатываешь что-то на C, значит хочешь, чтобы оно работало предельно быстро, а значит, и платформа должна быть скоростной. А то напишешь под mongoose, и, спрашивается, ради чего старался, если он все на тормозах спустит.

Конкурировать у меня ни с кем цели не было. Хотелось приблизиться к G-WAN, как к эталону скорости. Но стоило опубликовать проект, как тут же пошел поток писем. Стали все меня подзадоривать. Вот и пришлось еще несколько дней потратить, попыхтеть, чтобы G-WAN таки обогнать.

Обогнал я его или нет, пока не ясно. На своем компе я его обогнал. Как говорится, на своей территории. Опять же, наверное не во всех режимах.

Кстати, nginx очень порадовал. Скорость его HTTP стека выше, чем у G-WAN (если правильно его настроить, конечно). Просто nginx не кеширует файлы в памяти, если его не просят. С nginx вообще конкурировать бессмысленно. Даже удивляет, как такой функциональный сервер обеспечивает столь высокое быстродействие. Я тут убедился, что элементарное использование sprintf вместо strcat способно посадить быстродействие на 5-10 тыс. запросов в секунду. Т.е. бой идет уже за каждую лишнюю инструкцию CPU.

Q: Хорошо, расскажи об архитектуре nxweb вкратце.

Yaroslav: Основной поток слушает сокет и рассовывает поступающие коннекты по сетевым потокам (у каждого своя очередь, чтобы избежать мьютексов). Сетевые потоки (их имеет смысл запускать по одному на ядро процессора) обеспечивают HTTP протокол. Декодируют запрос, передают его модулю-обработчику. Воркеры – вещь опциональная, задуманы исключительно для поддержки медленных обработчиков, чтобы они не стопорили сетевой поток.

Сейчас у модулей есть коллбек, который вызывается сервером перед входом в основной цикл. Также есть файл main.c, который можно убрать или заменить. Его функция — исключительно сервисная: открыть лог-файл и передать управление в _nxweb_main(). Он также умеет запускать демона. Причем двойного: один демон следит за тем, чтобы второй (где собственно и крутится nxweb) не остановился. Если внутренний демон падает, то первый перезапускает его автоматически. При желании main.c можно заменить на любой другой. Главное, он должен вызвать _nxweb_main().

Q: Почему каждый сетевой поток не делает accept цикл? Тогда OS сможет распределять коннекты и можно сделать несколько accept-ов параллельно на разных ядрах.

Yaroslav: Я пробовал оба варианта, в т.ч. accept сетевым потоком без участия основного. Разницы в быстродействии не обнаружил, вернулся к единому акцептору, так как мне показалось, что это снижает процент ошибок соединения. У nginx и G-WAN часть соединений подвисает, и нагрузки не несет. См. мой комментарий насчет real concurrency на странице бенчмарок.

Q: Понятно. Первая версия использовала libev, но сейчас в wiki написано, что зависимости от libev нет. Что изменилось?

Yaroslav: Я написал свой libev. Это как раз то, что я имел в виду под «попыхтеть, чтобы обогнать G-WAN». Он заточен под мои нужды и самостоятельным проектом не является. Другими словами, я теперь работаю с epoll напрямую. nxweb совершенно не переносим. Он работает только на Linux да еще на ядре >= 2.6.22. Я использую Edge-Triggered epoll, который libev не поддерживает из-за его непереносимости. Да, libev – супер-классная вещь, я бы на нем и остановился, но народ захотел быстрее. Вот пришлось помучиться.

Q: Именно Edge-Triggered позволил получить последний прирост?

Yaroslav: Не только. Ещё пришлось над кодом поработать. В частности, исключить sprintf, новая система резервирования памяти и т.п. Есть мысль прикрутить всё это назад к libev. Может и не хуже будет, но не знаю, соберусь ли. Сделать переносимый код у меня задача не стоит. Хостинг практически всегда на Linux. А libev – это лишняя зависимость, которую надо инсталлировать, чтобы собрать nxweb. тоже минус.

Q: Игорь Сысоев и авторы libev утверждают, что у epoll есть масса проблем. Ты уже сталкивался с ними?

Yaroslav: Есть надежда, что эти проблемы в современном ядре уже вычищены. Всё-таки много лет прошло. Я тестирую на 2.6.32 и 3.0.0, с проблемами не сталкивался.

Q: Что насчёт резервирования памяти? Ты аллоцируешь какой-то большой пул, чтобы делать меньше сисколов?

Yaroslav: Free-list для коннекшенов и свой аналог obstack для формирования ответов.

Q: Ты используешь собственную реализацию парсера HTTP или брал что-то готовое?

Yaroslav: Реализация собственная. Безусловно, я не поддерживаю абсолютно все нюансы HTTP протокола. Хотя, например, 100-continue или chunked-encoding у меня есть. Опять же, отгрузка статики – это дело модуля. Ядро парсит запрос, вызывает обработчик, получает ответ, оборачивает его в HTTP и отправляет клиенту. Т.е. всякие там ETag и Range – это задача модуля. Сейчас мой модуль sendfile.c формирует лишь основные заголовки, хотя наверное скоро сделаю поддержку Range и If-Modified-Since.

Да, наверное есть готовые парсеры, проверенные на ошибки. Но, с другой стороны, в собственном парсере я быстрее ошибку отловлю, если мне о ней сообщат, чем в чужом. Тут реально счёт идёт на CPU-инструкции. А все самостоятельные проекты парсеров настолько обстоятельны, что ни о какой производительности с ними и речи быть не может.

Q: С другой стороны, я бы не дал никому кроме nginx слушать внешний порт боевого сервера. А за ним все некорректные запросы уже отфильтрованы.

Yaroslav: На данный момент (версия 2.0) nxweb не очень готов к тому, чтобы выставлять его наружу. В основном потому, что реальные приложения состоят не только из C-скриптов. Нужно подвязывать и Java и статику и все остальное. Если только речь не идет о реализации каких-нибудь чатов, где важно держать много тысяч одновременных соединений.

Q: Есть ряд библиотек, которые делают в Си userland потоки, одна из них libtask, используется в mongrel2. У каждой корутины свой стек, поэтому библиотека содержит несколько инструкций на ассемблере для переключения стека. Это позволяет писать обычный последовательный код, без колбеков типа on_request, on_success…

Yaroslav: Да, про корутины слышал. Но если я уже написал все колбеками, то выигрыша в производительности переходом на корутины я не получу. Только лишняя память уйдет на хранение стеков. Да еще и проблемы с gdb – тоже не самое приятное. Тут дело вкуса и привычки. К корутинам надо привыкать. Это особая концепция.

Q: Существенный выигрыш может быть в поддерживаемости кода обработчиков, когда люди захотят не просто request-response, а, например, sleep в середине или в базу сходить. Для простой баннерокрутилки, конечно, никакого смысла в них нет.

Yaroslav: Для слипов и в базу ходить – как раз для этого я и предусмотрел воркеров. И проверил первым делом. Если сконфигурировано 100 воркеров и каждый воркер делает sleep 1 сек, то сервер ровно и без запинок отрабатывает 100 запросов в секунду. Ни G-WAN, ни nginx с этим не справляются. Они либо выдают 3-4 запроса в секунду, либо тупо виснут.

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

Q: Как бы ты описал текущий статус nxweb? Стабильность, ошибки?

Yaroslav: Ну, я уже прикрутил его в качестве бекенда к сайту с довольно большой посещаемостью, т.е. запустил в продакшн. Прошел уже месяц, и ни единого перезапуска/сбоя. Хотя, бекенд – это не самое жесткое боевое крещение.

Полагаю, что статус – альфа. В частности, далеко не все функции nxweb тщательно проверены. Например, chunked request encoding. На тестовых примерах работает, но мало ли что всплывет. Также неясно, какова будет стабильность при некорректном поведении клиента. Например, запросы с ошибками могут спровоцировать сбой.

Еще есть проблема на текущем этапе – я довольно активно меняю h-файлы, структуры данных и пр.

Q: Лично мне как потенциальному разработчику под nxweb нужна возможность полностью управлять обработкой всех запросов и толстая библиотека вспомогательных функций (это уместно в виде модулей), типа сравни урл, sendfile, проставь заголовок, распарси куки, запроксируй на бекенд. Именно так я представляю себе программирование критических участков на nxweb.

Yaroslav: Надо сказать, что это примерно и есть мой подход. Создать базовые функции, с помощью которых можно конфигурировать работу сервера. Но конфигурировать на С, а не с помощью какого-то конфиг-файла. Потому что создать аналог конфига, который есть у nginx, это задача очень тяжкая.

Q: Следующий вопрос, скорее для галочки, что ты думаешь по поводу поддержки Windows?

Yaroslav: На 99% исключено. Я вообще холодно смотрю на переносимость кода. По сути, именно из-за забот о переносимости внутренний интерфейс nginx (да и apache) так заморочен. Приходится отказываться от современных технологий в угоду переносимости кода. Я пока хочу сконцентрироваться только на Linux.

Q: В G-WAN есть довольно удобная фича – автокомпиляция приложений. Конечно, тут есть масса открытых вопросов, типа безопасности, флагов компилятора, обновления на лету. Планируешь ли ты добавить такую же фичу в nxweb?

Yaroslav: Да, мне она очень понравилась. Но не планирую пока. Лишний гемор, который никак не приближает меня к тому, чтобы начать полноценно использовать nxweb для своих нужд.

Q: Хорошо, более насущный вопрос, в какой форме ты планируешь распространять nxweb? Дерево исходников? Библиотека? Статическая/динамическая?

Yaroslav: Пока только в том, каком уже распространяю. Открытый репозитарий, откуда можно скачать исходник и выполнить make. На данный момент полная компиляция проекта занимает секунды. Смысла городить библиотеки пока не вижу.

Q: Допустим, я хочу написать своё hello world приложение. Мои действия?

Yaroslav: hello.c – это шаблон модуля. Далее, в Makefile есть небольшие комментарии о подключении своих модулей. Там же есть специальные переменные SRC_MODULES и INC_MODULES. Кроме того надо добавить ссылку на свой модуль в modules.c.

Согласен, что процесс работы с модулями надо как-то получше обустроить, особенно если их станет больше.

Организационно разработка модулей может выглядеть так: делаешь форк nxweb на bitbucket, либо клонируешь nxweb куда-либо еще. Делаешь там свои разработки, коммитишь и т.п. Чтобы обновить nxweb делаешь hg pull .../nxweb

Q: Еще нужна какая-то документация, описание возможных модулей. Примеры решения типичных задач.

Yaroslav: Пока есть пример hello.c, по нему многое понятно. А в остальном, сорри, только исходный код. На bitbucket есть wiki, там какая-то часть документации.

Q: Дальше – где общаться заинтересованным разработчикам. Рассылка/форум. Какое-то место для вопросов и ответов, с историей и поиском.

Yaroslav: На днях я создал две гугл-группы: nxweb и nxweb-ru.

Q: Когда будут стабильные версии, заморозка API?

Yaroslav: Ой, не знаю… Заморозка API нескоро. Хотя, надо сказать, между 1-й и 2-й версией изменения в модулях были минимальны. При том, что внутренности сервера были все переделаны.

Q: Это хороший знак. Какие планы по наращиванию библиотеки полезностей для приложений? Конкретнее: логирование, движок шаблонов, работа с заголовками, куками.

Yaroslav: Заголовки и куки уже парсятся в таблицы и их можно извлекать по именам. Не знаю, что еще с ними можно сделать. access_log – пока не нужен был, в принципе он не сложен, однако существенно замедлит работу. Библиотека полезностей для начала пополнится функциями проксирования, я думаю.

Q: access log как раз не нужен, нужно логирование приложения.

Yaroslav: Логирование есть общее. Так называемый error_log, функция nxweb_log_error("", ...). Он флушится после каждой строки.

Q: Дальше, по планам. Было бы здорово конфигурировать количество тредов автоматически. Я вижу тут два направления: 1) при запуске определить количество ядер и поставить столько потоков, 2) порождать новые потоки пока LA ниже заданного значения, ну и с верхним ограничением, конечно.

Yaroslav: Автоконфигурация – большой вопрос. nxweb в принципе пока не конфигурируется иначе как путем перекомпиляции. Есть ли смысл делать один параметр автоматически конфигурируемым, не знаю. На самом деле несложно запускать при старте любое автоматически определенное число тредов. Чуть сложнее дозапускать или останавливать треды в процессе, хотя тоже реализуемо.

Из практических целей я пока вижу реализацию проксирования. В первую очередь на Java. Это то, с чем я работаю. Потом – SSL, управляемое кеширование. Может быть, интерфейс к БД.

Сейчас запрос и ответ полностью буфферизуются. Это может быть плохо для некоторых задач (например, upload файла). Поэтому я думаю ввести в модулях понятие обработчиков частично полученных данных, а также отправку данных по частям.

Q: Проксировать и SSL, вроде nginx умеет. Не снаружи же ставить nxweb.

Yaroslav: Если не ставить его снаружи, то всё преимущетсво скорости теряется. Я вчера протестировал nxweb позади nginx: 25 тыс. запросов в секунду. Притом что сам он отдает 160 тыс., да и nginx 130 тыс. умеет. Это для текущей стабильной версии nginx 1.0, у которого нет keep-alive для бекендов. В версии 1.1 получается 50 тыс. запросов в секунду – намного быстрее, но, все равно, более, чем трехкратное замедление.

Q: Похоже что use case-ы nxweb можно разделить на две большие категории, с разными потребностями:
  • Много rps для банеров, топлайнов и пр. Тогда его надо ставить наружу, тут без вариантов
  • Нагрузка по запросам небольшая, но очень важно отдавать ответ максимально быстро, поэтому кусок приложения пишется на Си. Тут вполне уместно встать за nginx для удобства админов.


Yaroslav: Полагаю, что с новыми версиями nxweb появятся и новые use case-ы.
Tags:
Hubs:
+14
Comments24

Articles

Change theme settings