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-ы.

Комментарии (23)


  1. mwizard
    26.07.2015 14:27
    +1

    К сожалению, из-за форматирования, т.е. отсутствия оного, статья нечитабельная :(


    1. ainu
      26.07.2015 16:43

      Если вчитываться, то читабельная.
      По теме — жаль, текущие кейсы и задачи не генерируют условий «к вам сейчас зайдёт 100000 посетителей, каждому надо отдать баннер».


  1. voidnugget
    26.07.2015 18:37

    rwasa повеселее для подобных задач будет.


    1. yaroslav2
      26.07.2015 20:32

      Сейчас nxweb намного более функционален. Хотя, если кто-то пишет веб-приложения на ассемблере, то rwasa – самое то.


  1. yaroslav2
    26.07.2015 20:29
    +8

    Я – разработчик nxweb. Данная статья была написана в декабре 2011 года, но почему-то только сейчас вышла из песочницы. Через три с половиной года. Попытаюсь немного освежить информацию.

    За прошедшее время nxweb оброс немалым функционалом:

    • http-proxy (удобно для Java)
    • встроенный Python
    • шаблоны страниц с наследованием блоков
    • SSL
    • файловый кеш ответов бекенда
    • кешируемое gzip-сжатие
    • масштабирование изображений
    • конфиг-файл в формате json
    • подгружаемые модули
    • access log
    • ...


    При этом он достаточно стабилен, давно использую его как фронтенд. На некоторых сайтах работает год и больше без перезапуска.

    Кстати, вот официальный сайт: nxweb.org. Работает на nxweb + своя CMS на Python. Там, правда, в русской версии практически ничего не написано, но в английской есть несколько статей для быстрого старта.


    1. ibKpoxa
      27.07.2015 12:17

      Какая в проекте текущая ситуация с возможностью написания comet сервера?


      1. yaroslav2
        27.07.2015 12:25

        Из обработчика, работающего в отдельном потоке (worker thread), в т.ч. Python, это пока не реализовано. Worker должен вернуть серверу весь ответ целиком.

        Для асинхронных модулей на C такого ограничения нет, но написание асинхронных модулей – задача нетривиальная. Нужно интегрироваться в event loop сервера. В т.ч. нужно асинхронно мониторить источник информации, сигнализирующий о необходимости отправки клиентам новых пакетов данных.


    1. viatoriche
      27.07.2015 15:55

      Встроенный python. Интересно. Означает ли это, что nxweb может запускать django приложения? Если да, то через какой handler?


      1. yaroslav2
        27.07.2015 16:22

        Да, Django работает.

        Вот тут посмотрите пример для Flask, Django будет отличаться лишь параметром:

        "wsgi_application": "hello.app" // full python name of WSGI entry point
        


    1. Moxa
      28.07.2015 00:47

      а keep-alive поддерживается? что-то nxweb.org с ним не дружит =(


      1. yaroslav2
        28.07.2015 00:52

        Не может быть. Как проверяли?


        1. Moxa
          28.07.2015 01:16

          ab -n 100 -k -c 10 nxweb.org
          This is ApacheBench, Version 2.3 <$Revision: 1528965 $>
          Copyright 1996 Adam Twiss, Zeus Technology Ltd, www.zeustech.net
          Licensed to The Apache Software Foundation, www.apache.org

          Benchmarking nxweb.org (be patient)...apr_pollset_poll: The timeout specified has expired (70007)
          Total of 10 requests completed

          если вырубить, не дожидаясь таймаута:

          ab -n 100 -k -c 10 nxweb.org
          ab -n 100 -k -c 10 nxweb.org
          This is ApacheBench, Version 2.3 <$Revision: 1528965 $>
          Copyright 1996 Adam Twiss, Zeus Technology Ltd, www.zeustech.net
          Licensed to The Apache Software Foundation, www.apache.org

          Benchmarking nxweb.org (be patient)...^C

          Server Software: nxweb/3.3.0-dev
          Server Hostname: nxweb.org
          Server Port: 80

          Document Path: /
          Document Length: 1199 bytes

          Concurrency Level: 10
          Time taken for tests: 3.558 seconds
          Complete requests: 10
          Failed requests: 2
          (Connect: 0, Receive: 0, Length: 2, Exceptions: 0)
          Non-2xx responses: 10
          Keep-Alive requests: 10
          Total transferred: 236160 bytes
          HTML transferred: 231922 bytes
          Requests per second: 2.81 [#/sec] (mean)
          Time per request: 3557.915 [ms] (mean)
          Time per request: 355.791 [ms] (mean, across all concurrent requests)
          Transfer rate: 64.82 [Kbytes/sec] received

          Connection Times (ms)
          min mean[±sd] median max
          Connect: 3 3 0.2 3 4
          Processing: 14 39 22.7 58 64
          Waiting: 14 39 22.7 58 64
          Total: 17 42 22.7 62 67


          1. yaroslav2
            28.07.2015 03:37

            ApacheBench стар как мир, он использует HTTP 1.0 и, похоже, не понимает chunked-encoding.

            Строго говоря, nxweb не должен был бы использовать chunked encoding для клиента, обращающегося по HTTP 1.0, так как chunked encoding – это особенность HTTP 1.1. Пожалуй, это недочет. Не уверен, правда, что мне хочется его исправлять. Ведь живые клиенты, использующие HTTP 1.0, сейчас уже практически не встречаются.

            Хотя, я еще подумаю над этим.

            nxweb (да и другие серверы) использует chunked encoding не всегда, а только в случаях, когда размер ответа не известен заранее. Обычно это означает, что контент формируется на лету с помощью SSI или шаблонов.

            Попробуйте вот так:

            ab -n 100 -c 10 -k http://nxweb.org/i/images/wait.gif
            


  1. Ivan_83
    27.07.2015 13:51
    +2

    На самом деле собственных «движков» типа libev и http серверов и не только полно.
    Тема интересная, но вот здесь технических деталей совсем нет.

    У меня тоже есть собственный движок и http сервер с боку :)
    www.netlab.linkpc.net/wiki/ru:software:msd:lite
    www.netlab.linkpc.net/wiki/ru:software:ssdpd:index
    Движок в обоих проектах один, хттп сервер есть в первом.
    Есть ещё msd, «сервер лицензий» и «прокси для торрент трекеров», но это не публичные проекты, однако движок у них один.

    Изначально движок был под kqueue() и однопоточный.

    Позднее добавил epoll() (с дикими матами по поводу его никакой относительно epoll() функциональности).

    Пришлось пойти на ряд компромиссов, в целом epoll() работает также, но есть особенности:
    — текущая реализация не работает одновременно с отправкой и получением на одном дескрипторе
    — на каждый таймер тратится доволнительный файловый дискритоп
    — на каждую операцию чтения доп сискол пытающийся получить размер того что доступно для чтения
    — вероятно что то ещё, просто я уже забыл :)

    Потом пришла многопоточность.
    Это был тяжёлый выбор между:
    — один kqueue()/epoll() и куча потоков
    — один поток — один kqueue()/epoll()
    По граблям первого варианта я уже находился до этого в винде с IOCP(), потому выбрал второй. В принципе он примерно эквивалентен тому как тот же nginx форкается.

    С многопоточностью пришло понимание что потокам нужно общаться, и лучше это делать без локов совсем.
    В случае kqueue() один поток просто отправляет EVFILT_USER, помещая в data адрес колбэк функции а в udata аргумент.
    Для epoll() пришлось городить отправку сообщений через pipe(): отправляющий поток записывает в пайп принимающего адрес функции, аргумент и меджик на всякий случай.
    Это всё дало возможность отправлять юникастовые и броадкастовые сообщения между потоками.

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

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

    Мысль об первой схеме, где один kqueue()/epoll() и куча потоков меня все же не оставляла равнодушным: «а что если будет такой сокет с которого нужно будет что то тяжёлое обрабатывать?»…
    Добавил я «виртуальный поток», по сути ещё один kqueue()/epoll() который добавлен в kqueue()/epoll() всех потоков.
    Те если на виртуальном kqueue()/epoll() случается событие, то все потоки его получают, вернее пытаются, ибо получает его только первый, остальные отрабатывают в холостую.
    Для лёгких обработчиков оверхэд от таких пустых срабатываний будет существенным.
    Другой вариант — он в HTTP сервере реализован, это когда принимающий соединение поток ставит сокет с заданием на чтение в другой поток. Но тут тоже нюансы есть — нужно как то балансировать нагрузку между потоками. Я дальше roundrobin думать не стал, у меня обычно или connection-close сразу или сокет у вебсервера отбирается навсегда.

    Я считаю что апи движка уже давно стабилизировалось, и менять там особо нечего.
    Последнее что я дописывал — оно дополняло а не меняло.

    Поверх движка работают различные обработчики, например «приёмщик соединений», который только и делает что принимает соединения и для каждого вызывает указанный при создании коллбэк.
    На линухе как то вылез забавный баг.
    Было сделано так: реад эвент от ядра, в обработчике вызывается accept() и ждём ещё эвента (те вернули управление ядру).
    Но соединения иногда залипали, оказалось что ядро может в очередь поставить хоть 100 штук, а эвент придёт один.
    Пришлось добавить цикл и вызывать accept() пока он не вернёт ошибку.

    И другие обработчики, для приёма пакетов, отправки, подключения, отправки файла и тп…
    Им скармливаешь сокет/файл, буфер и они сами заботятся о том чтобы он полностью весь отправился/принялся, по завершении дёргают колбэк и всё.

    Тут тоже апи стабилизировалось, последний раз добавлял sendfile() в виде доп функций, внутри оно практически те же стандартные обработчики.

    Собственно сам HTTP сервер это по сути уже третий уровень абстракции, который работает поверх этих обработчиков.
    Те весь код это функции реагирующие на события:
    — «пришло новое соединение»
    — «клиент чего то прислал» — проверяем, вдруг там запрос?)
    — «отправили клиенту» — слили ответ…

    Недавно прочитал про SO_REUSEPORT в NGINX 1.9.1, и тоже себе добавил.
    Нужно было всего то вместо одного сокета для обработчика входящих соединений создать несколько, и обработки создавать на разных потоках пула. Короче строчек 20-40 кода всего, не более. (включая чтение из конфига)
    Ещё из низкоуровневых вещей в HTTP сервере используется SO_ACCEPTFILTER(httpready) / TCP_DEFER_ACCEPT.

    HTTP сервер я бы сказал очень мало функциональный, его обязанности это принять соединение, принять запрос, почекать его на предмет валидности (основные проверки), распарсить запрос в структуру, выборочно проверить «host» и скормить это в клбэк который был указан для сервера при инициализации или для конкретного клиента, отвечающий за обработку запросов.
    Для каждого клиента можно кастомизировать обработчики «запрос пришёл», «ответ отправлен», «клиент отключился».
    Это очень удобно, я сейчас как раз дописываю авторизацию через радиус, на время авторизации эти обработчики перезаписываются с основных на те что в модуле авторизации. По сути система плагинов или как субклассинг в винде.
    И сам сервер написан так, чтобы при необходимости у него можно было легко забрать сокет навсегда, потому что тот же msd/msd_lite на запросы каналов начинает лить бесконечно поток.

    С HTTP серверами есть один нюанс.
    Клиент может отправить запрос и сразу же сделать сделать half-close (вызвать shutdown(skt, SHUT_WR)), если сервер не знает про такой финт то может подумать что клиент прислал запрос и отключился, и просто закрыть сокет.
    В коде это выглядит как уведомление на чтение и выставленный EOF. Вот этот EOF часто и считают признаком что клиент отключился, а это не всегда так.

    Сам хттп сервер умеет грузить свои настройки из xml файла, хотя там и настроек то не сильно много.
    Апи почти стабилизировалось, ещё пару моментов перебрать и всё.

    Вот как то так получилось, практически без взаимных блокировок потоков.
    Веб севером для приложений я это назвать не рискну — кода слишком много, скорее это приложения на си для движка с вебсервером :)
    Тот же ssdp — собственного кода там кот наплакал, всё остальное движок.

    Производительность именно http сервера не тестил, «торрент ретрекер прокси» на старом однопоточном ядре под линухом держал 100+ запросов в секунду, выедая 5-10% проца. Но там как бы обработчики тяжёлые, в том смысле что суть программы в том, чтобы один запрос переправить на несколько трекеров, а ответы собрать в месте и вернуть клиенту.
    За счёт sendfile() и ещё кое каких трюков msd_lite раздаёт по 20г с одного тазика и половина или более проца свободны.


    1. yaroslav2
      27.07.2015 14:32

      Принцип работы веб-сервера прост, и реализовать его несложно. Есть работающие примеры буквально из 100 строк кода. Но, как хорошо видно из Вашего поста, нюансов там несметное множество. А если захочется сделать на собственном сервере полноценный веб-сайт (со статикой, бекендом, SSI, gzip-компрессией, кешированием, ...) то придется много еще чего доделать. Вот так и я, начал с самого простого, в итоге за несколько лет пришел к тому, что есть сейчас.


      1. voidnugget
        27.07.2015 14:37

        О безопасности история умалчивает.


        1. yaroslav2
          27.07.2015 14:55

          Безопасность – штука многогранная…

          Сайты, работающие на nxweb, уже не раз бывали под DDoS'ом – успешно его отбивали (хотя DDoS тоже бывает разный), в т.ч. благодаря кешированию. Не знаю, пытались ли их взломать, но и не слышал о том, чтобы это кому-то удалось.

          Разумеется, я думал о безопасности, когда писал код, но уязвимости можно найти всегда в любом коде. Обязательно их устраню, когда обнаружатся.


      1. Ivan_83
        30.07.2015 02:15

        И у меня оно было простым, в начале я хотел свой HAVP но по шустрее. Потом оно начало работать но в теме я разочаровался и забросил.
        Через год понадобился на работе «прокси для торрент трекеров» но чтобы и на линухе работал, тогда я вернулся к наработкам, хотя там вряд ли хотя бы одна строчка уцелела :)

        Цели получить полноценный веб сервер никогда не было, всегда нужен был некий минимум для взаимодействия.

        2 voidnugget:
        Как и о не безопасности.

        2 Alesh:
        Кто!? Я!?
        Нет, не думал :)

        1. Много и так уже описал :)
        Дальше только исходники перечитывать и вспоминать где было мучительно больно :)

        2. «libev, libevent, libuv» — я их бегло смотрел, в целом они все представляли нечто вроде epoll() апи, а такое мне не интересно в виду слишком большой урезанности.
        Те превратить kqueue() в epoll() может любой дурак, тут ума совсем не надо.
        Работа под виндой — я думал, ядро в принципе «портировать» можно заюзав в качестве очереди — очередь сообщений невидимого окна (select(), который под вендой лепят все вместо poll()/epoll()/kqueue(), мне решительно не нравится, IOCP слишком другая идеология, остальное вроде ещё хуже) — и чисто ядро и веб сервер мне там не нужны, те довольно много функционала потеряется, и тот же msd если и будет работать в целом, то множество крутилок в нём работать не будут. SSDP — тоже не всё однозначно.
        Те сделать то можно, но зачем мне что то запускать под виндой не понятно. SSDPd мог бы быть интересен вин юзерам в целом, но к нему нужен nginx+php, настроить это всё мало кто осилит.

        Был ещё libkqueue — типа под линухом/виндой/соляркой/андройдом kqueue() проги запускать.
        Полузаброшенное решение, видимо авторы добились что с их программами оно работает.
        Сразу увидел что там заглушка для модификации таймера под линуксом, либо они его пересоздают либо оно у них не реализовано.
        Для винды у них интересно сделано, можно втянуть себе, если приспичит, или сразу собрать с их либой — вдруг повезёт и будет работать :)

        Соответственно и производительность я с ними не сравнивал. На kqueue() у меня в любом случае будет не хуже, на epoll() возможно что и хуже, из за того что у меня при ошибках ядро пытается код ошибки узнать: getsockopt(udata->ident, SOL_SOCKET, SO_ERROR,...).
        Ещё может быть просадка из за таймеров: я везде использую ядреные таймеры. В планах уже давно было начать использовать один ядерный таймер и организовать собственную очередь таймеров, но руки не дошли с одной стороны, с другой таймеры активно использовались в msd и «прокси для торрент трекеров». В msd я почти все таймеры убрал: раньше для отправки каждому клиенту был таймер для таймаута и ещё на всякие действия, теперь там один таймер тикающий раз в секунду, который рассылается броадкастом по потокам, и они в его обработчике заодно и проверяют все места где таймаут был возможен. А прокси не оч нужен сейчас.

        3. Тут же все под хайлоадом хттп понимают хххххххх запросов к вебсерверу в секунду, а я ушёл по другому пути: у меня запросов не очень то и много, но вот отдачи много: 2-16 мегабит в одно соединение. И куча таких вот соединение, «проксирование» IPTV потоков один-к-многим.
        И в целом я поэтому могу себе позволить каждому новому клиенту сразу же тюнить сокет под мои цели, а потом ещё раз его перенастраивать в зависимости от того что он запросил.
        Если гнаться за CPS (connections per second) то это непозволительная роскошь.
        Да и собственно не веб сервер у меня, в традиционном понимании, ибо сам по себе он не умеет отдавать даже файлы, да и просто запросы обрабатывать (он только на валидность проверяет и парсит малость). Не говоря о всяких fast-cgi и тп.
        Это именно встройка, типа как был winsock в вижал бейсике: кинул его в проект и он тебе эвенты только шлёт.

        Писанина публикации это труд (в отличии от комента её оформлять нужно, картинку с котиками для привлечения внимания искать, ошибки вычитывать, общую читабельность и интересность поддерживать, может тесты и графики делать....).
        Короче говоря, у меня проблема с мотивацией: за плюсики я тут при таком отношении к авторам писать ничего не собираюсь больше, пусть дальше с песочницы таскают, да за деньги комм блоги продают пиарастам (искл.: нод32 — интересно и по делу пишут, иногда интел).
        Для каких то моих личных интересов — вот именно указанные три направления ничего мне не дадут, ну кроме плюсиков, которые и нахер не сдались.
        Были наброски публикации с тестированием производительности udpxy, astra, msd_lite и описанием почему так, но там без зубодробительных дебрей.


        1. dimoclus
          30.07.2015 19:42

          > Те превратить kqueue() в epoll() может любой дурак, тут ума совсем не надо.
          В современном ядре Linux epoll обладает практически всеми фичами kqueue (за исключением, пожалуй EVFILT_PROC). Конечно, иногда это происходит ценой написания большего количества кода, но результат получается весьма достойным.
          Кстати, использование timerfd для таймеров говорит о том, что не каждый дурак способен способен реализовать таймеры эффективно: в реальных приложениях, как правило, нужны таймеры, срабатывающие через равные промежутки времени, что можно сделать за O(1) в отличие от бездумного использования heap'ов для всего и вся.

          >но вот отдачи много: 2-16 мегабит в одно соединение. И куча таких вот соединение, «проксирование» IPTV потоков один-к-многим
          Я абсолютно не сведущ в FreeBSD-специфичных API, но как раз для этого в Linux завезли: a) splice (-30% cpu load при проксировании «один к одному») б) vmsplice (для отдачи кучи мегабит из одного соединения, для мультиплексирования) в) tee (для мультиплексирования «с трюкачествами»)


          1. Ivan_83
            31.07.2015 02:16

            Не обладает.
            Или я читал man от какого то другого ядра линукса, где epoll() совсем примитивный.
            1. kqueue() позволяет иметь раздельно фильтры для чтения и записи, те udata будет для каждого свой.
            2. kqueue() возвращает кроме udata ещё и сам описатель файла/сокета/..., количество байт, код ошибки, тип фильтра, флаги и тп, всё что есть в ядре полезного по данному описателю+операции.
            3. Таймеры они прямо в kqueue() без лишних костылей/описателей.
            4. Пользовательский тип фильтра, и другие типы фильтров…
            5. kqueue() может за один сискол поставить в очередь сразу пачку фильтров или/и прочитать пачку фильров для которых событие наступило. (хотя я этим пока не пользуюсь)

            В epoll() ничего нет. Он простой как poll(), умеет на событие возвращать только udata которое привязано к описателю (но не сам описатель — сокет/файл) и флаги из которых можно понять какое событие + EOF, ERROR. Всё.
            Я не спорю с тем, что к epoll() можно прикостылить много чего.
            Меня вот напрягло что запись+чтение могут придти разом, и udata для них никак не разделяется, этот код я так и не отладил, оставив рабочим вариантом когда один описатель — один тип событий.

            Про timerfd и heap~ы не понял, можно развернуть мысль?

            «Я абсолютно не сведущ в FreeBSD-специфичных API, но как раз для этого в Linux завезли: a) splice (-30% cpu load при проксировании «один к одному») б) vmsplice (для отдачи кучи мегабит из одного соединения, для мультиплексирования) в) tee (для мультиплексирования «с трюкачествами»)» —
            Это всё хорошо, и может быть даже замечательно, но всё это работает только с «потоковыми» описателями, те tcp сокет/файл — да, а udp — нет.
            Вот на это я и наступил когда хотел сделать msd_lite — совсем лёгким, а оно не заработало. В итоге пришлось из msd взять то что работает.
            В основном нужно делать как udpxy только с наворотами: принять мультикаст и раздать по хттп. Бывает что и хттп принять нужно и раздать так же, но это реже и всё равно желательна дополнительная обработка потока.
            Разницы между sendfile() и всем этим — вроде особой нет.
            Кстати, sendfile() в линухе тоже весь такой обрезанный: не умеет данные до/после файла из буферов посылать, притом что во фре и в виндовом TransmitFile() это можно. А во фрёвом sendfile() ещё и флаги вкусные есть: SF_NODISKIO, SF_MNOWAIT, SF_SYNC.
            Вот с линукса, который принимает 650+ мегабит мультикаста и отдаёт 50 мегабит на 14 клиентов:
            CPU usage system: 27,22%
            CPU usage user: 7,50%
            CPU usage total: 34,72%

            Когда высасывал с него гиг то нагрузка не сильно больше была, и растёт в основном систем.

            PS: единственный плюс линукса который я пока увидел это работа SO_REUSEPORT, о которой недавно писали в новостях про nginx.


            1. dimoclus
              16.08.2015 13:07

              >Про timerfd и heap~ы не понял, можно развернуть мысль?
              Как правило, все таймеры можно объединить в heap (по-русски это вроде «пирамидой» называют). Однако, если таймерами управляет приложение, оно может объединить равноинтервальные таймеры в связанный список, привязанный к единственному элементу heap-а. В итоге rearm таймера будет иметь сложность O(1) вместо логарифмических вставки/удаления в heap. Именно поэтому я весьма скептически отношусь к помещению десятков тысяч таймеров в kqueue.

              >Меня вот напрягло что запись+чтение могут придти разом, и udata для них никак не разделяется, этот код я так и не отладил, оставив рабочим вариантом когда один описатель — один тип событий.
              Хранить в udata файловый дескриптор, а в userspace к файловому дескриптору привешивать цепочки watcher'ов, как это сделано в libev

              >Кстати, sendfile() в линухе тоже весь такой обрезанный: не умеет данные до/после файла из буферов посылать
              Но зачем? TCP_CORK + send/writev. Зачем плодить лишние сущности которые делают то же, что уже существующие? Цена syscall-а не такая высокая, как это многим кажется.


    1. Alesh
      29.07.2015 22:18

      Не думали написать полноценную статью на эту тему? Похоже вы прилично успели в этом покапаться.

      • epoll/kqueue — возможности, особенности, etc
      • libev, libevent, libuv — сравнение, производительность, etc
      • в целом разработка хайлоад сетевого сервиса: архитектуры, «подводные камни», etc

      Мало кто копал в этой теме, инфа скудная. Каких-то сравнительных тестов или анализа разных подходов, как по типам архитектур серверов, так и по используемым библиотекам и системным возможностям — просто нет.


  1. fish9370
    29.07.2015 08:26

    спасибо за статью, она подвигла меня задуматься об оптимизации своего кода