Веб-приложение на C++, или укрощение демона FastCGI

В настоящее время, благодаря таким инструментам как NodeJS, создание веб-приложения — сущий пустяк. Скачал бинарник, сваял js в 5 строчек кода и можно хвастаться. А если подключить express и добавить ещё 5 строчек, то получим полноценное веб-приложение с роутингом, шаблонами, сессиями и другими прелестями. Так просто, что даже скучно. И стало мне интересно: как обстоят дела у моего старого знакомого С++, с которым уже 5 лет не виделся. В своё время прельстил меня ActionScript и прочий JavaScript, а о добром друге, который не раз выручал, совсем позабыл. В свете недавних статей о Configurable Omnipotent Custom Applications Integrated Network Engine (сокращено Cocaine), попался мне на глаза проект под названием Fastcgi Daemon, на основе которого функционирует HTTP-интерфейс Cocaine. И так, знакомьтесь

Fastcgi Daemon — Yandex's opensource framework for design highload FastCGI applications on C++.

То есть

Fastcgi Daemon — это фреймворк с открытым исходным кодом, разработанный в Яндексе и предназначенный для создания высоконагруженных FastCGI-приложений на C++.

К сожалению, это всё, что вы найдёте в README из официального репозитория.
Больше документации доступно здесь github.com/lmovsesjan/Fastcgi-Daemon/wiki/_pages, но для полноценной работы и её недостаточно. Например, вся установка там занимает одну строку:

sudo apt-get install fastcgi-daemon2-init libfastcgi-daemon2-dev libfastcgi2-syslog

Но в официальных репозиториях Ubuntu, да и в каких-либо других (может плохо искал), эти пакеты обнаружить не удалось. В этой статье я решил собрать свои изыскания, связанные с установкой, настройкой и использованием этого инструмента. Все приведённые ниже исходники, а также готовые deb-файлы можно забрать здесь github.com/nickalie/HelloFastCGI

Установка


Проект поддерживается для Ubuntu, поэтому все операции ниже я производил на Ubuntu 12.04 64 bit со свежими обновлениями.
Для начала установим все необходимые зависимости. Сделать это можно при помощи следующей команды:

sudo apt-get install -y build-essential git debhelper automake1.9 autotools-dev libboost-dev libboost-thread-dev libfcgi-dev libxml2-dev libboost-regex-dev libtool libssl-dev autoconf-archive

Теперь клонируем репозиторий с Fastcgi Daemon. Тут у нас на выбор 2 варианта:

Я выбрал первый вариант

git clone https://github.com/golubtsov/Fastcgi-Daemon.git

Переходим в папку со свежескачанным проектом

cd Fastcgi-Daemon

и запускаем сборку

dpkg-buildpackage -rfakeroot

На выходе получаем готовые к установке deb-файлы, которые находятся в родительской директории. Делаем

cd ..

и

sudo dpkg -i ./libfastcgi-daemon2-dev_2.10-13_amd64.deb \
  ./libfastcgi-daemon2_2.10-13_amd64.deb \
  ./fastcgi-daemon2-init_2.10-13_amd64.deb \
  ./fastcgi-daemon2_2.10-13_amd64.deb \
  ./libfastcgi2-syslog_2.10-13_amd64.deb

Также нам понадобится веб-сервер, который умеет работать с FastCGI. Я для своих нужд использую nginx, тоже самое советуют и в документации

sudo apt-get install nginx

Если хочется использовать свежайшую версию этого веб-сервера, то перед этим делаем

sudo add-apt-repository ppa:nginx/stable && sudo apt-get update

На этом с установкой покончено. Переходим к нашему первому приложению.

Приложение


Создаём файл HelloFastCGI.cpp и помещаем в него следующий код:

#include <fastcgi2/component.h>
#include <fastcgi2/component_factory.h>
#include <fastcgi2/handler.h>
#include <fastcgi2/request.h>

#include <iostream>
#include <sstream>

class HelloFastCGI : virtual public fastcgi::Component, virtual public fastcgi::Handler
{
    public:
        HelloFastCGI(fastcgi::ComponentContext *context) :
                fastcgi::Component(context)
        {

        }

        virtual void onLoad()
        {

        }

        virtual void onUnload()
        {

        }

        virtual void handleRequest(fastcgi::Request *request, fastcgi::HandlerContext *context)
        {
                request->setContentType("text/plain");
                std::stringbuf buffer("Hello " + (request->hasArg("name") ? request->getArg("name") : "stranger"));
                request->write(&buffer);
        }
};

FCGIDAEMON_REGISTER_FACTORIES_BEGIN()
FCGIDAEMON_ADD_DEFAULT_FACTORY("HelloFastCGIFactory", HelloFastCGI)
FCGIDAEMON_REGISTER_FACTORIES_END()

Метод, который нас больше всего интересует — handleRequest. Именно он занимается обработкой запроса. Надеюсь из кода понятно, что происходит в этом методе, но на всякий случай поясню. Если в запросе (POST или GET) есть параметр “name”, то выводим текcт “Hello %name%”, иначе “Hello stranger”.

Класс fastcgi::Request отвечает одновременно и за запрос, и за ответ, хотя обычно эта функциональность разделяется на 2 класса или объекта, как, например, в том же NodeJS.
Уже “из коробки” мы можем работать c cookies, выставлять произвольные HTTP-статусы, заголовки и т. п. В общем, нам доступен джентельменский набор для разработки веб-сервисов и веб-приложений. Разве что по умолчанию не реализованы сессии, но они прикручиваются в 2 счёта. Об этом расскажу в следующей раз.

Вернёмся же к нашим баранам. Теперь нам нужно скомпилировать этот класс в shared-библиотеку:

g++ HelloFastCGI.cpp -O2 -fPIC -lfastcgi-daemon2 -shared -o libHelloFastCGI.so

Затем следует подготовить конфигурационный файл HelloFastCGI.conf (важно, чтобы расширение было “conf”):

<?xml version="1.0"?>
<fastcgi xmlns:xi="http://www.w3.org/2001/XInclude">
<pools>
    <pool name="main" threads="1" queue="5000"/>
</pools>
<handlers>
    <handler pool="main" url="/hellofascgi">
        <component name="HelloFastCGIComponent"/>
    </handler>
</handlers>
<components>
    <component name="HelloFastCGIComponent" type="MainModule:HelloFastCGIFactory"/>
    <component name="daemon-logger" type="logger:logger">
       <level>INFO</level>
       <ident>hellofastcgi</ident>
    </component>
</components>
<modules>
    <module name="MainModule" path="./libHelloFastCGI.so"/>
    <module name="logger" path="/usr/lib/fastcgi2/fastcgi2-syslog.so"/>
</modules>
<daemon>
    <logger component="daemon-logger"/>
    <endpoint>
        <backlog>128</backlog>
        <socket>/tmp/fastcgi_daemon.sock</socket>
        <threads>1</threads>
    </endpoint>
    <pidfile>/var/run/fastcgi2/HelloFastCGI.pid</pidfile>
    <monitor_port>20012</monitor_port>
</daemon>
</fastcgi>


У нас есть обработчик (handler) запросов, которые приходят на “/hellofastcgi” (например, www.somedomain.com/hellofastcgi). Этим обработчиком является компонент HelloFastCGIComponent, который лежит в модуле MainModule. Точнее в модуле лежит фабрика HelloFastCGIFactory, которая позволяет получить необходимый компонент. В свою очередь MainModule черпает свои ресурсы из недавно скомпилированной libHelloFastCGI.so. Также стоит обратить внимание на содержимое тэга “socket” — это ни что иное как unix-сокет, который нам вскоре понадобиться указывать в настройках nginx. “pidfile” — важен при демонизации FastCGI-Daemon. Его имя должно совпадать с именем conf-файла, различие только в расширениях. Чтобы демон был в состоянии делать start/stop/restart, pid должен лежать именно в “/var/run/fastcgi2/”.

Самое время настроить веб-сервер. Так как всё это я проделываю на свежеустановленном nginx, то, не мудрствуя лукаво, правлю /etc/nginx/sites-available/default. Содержание должно быть примерно следующим:

server {
  listen 80;

  location / {
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $fastcgi_script_name;
    fastcgi_pass unix:/tmp/fastcgi_daemon.sock;
}
}

Перезапускаем nginx

sudo service nginx restart

Запускаем наше приложение при помощи

fastcgi-daemon2 --config=HelloFastCGI.conf

Если всё было сделано правильно, то открыв в браузере localhost/hellofastcgi, вы должны увидеть

Hello stranger

Добавим аргумент localhost/hellofastcgi?name=nick и получим

Hello nick

Ура, работает! Надеюсь, у вас тоже.

Однако, сейчас Fastcgi Daemon запускается в консоли как обычное приложение. Где-же обещанная демонизация? Об этом поговорим ниже.

Настройка daemon


К счастью Fastcgi Daemon на то и Daemon, что его очень легко демонизировать (прошу прощения за тавтологию).

Берём ранее созданный HelloFastCGI.conf, заменяем в нём относительный путь к libHelloFastCGI.so на абсолютный и кладём в /etc/fastcgi2/available.

Теперь можно запускать/останавливать/перезапускать демона привычным способом:

sudo service fascgi-daemon2 start/stop/restart <appname>/all

В нашем случае это будет

sudo service fastcgi-daemon2 start HelloFastCGI

Чтобы запустить все доступные в /etc/fastcgi2/available приложения используем ключевое слово “all”

sudo service fastcgi-daemon2 start all

Немаловажным также является то, что в случае непредвиденного падения вашего приложения, оно будет автоматически перезапущено.

Бенчмарки


Любопытства ради решил сравнить производительность Fastcgi Daemon и NodeJS. Для этого набросал аналогичное приложение на JS:

var http = require('http');
var url = require('url');

http.createServer(function (req, res)
{
	res.writeHead(200, {'Content-Type': 'text/plain'});
	var query = url.parse(req.url, true).query;
	res.end('Hello ' + (query.name ? query.name : 'stranger'));

}).listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

Для чистоты эксперимента настроил proxy_pass в nginx для работы с node.
Тестировал при помощи Apache Bench:

ab -c 100 -n 20000 http://IPorURL/hellofastcgi?name=Nikolay

и

ab -c 100 -n 20000 http://IPorURL/?name=Nikolay

Результаты:

Fastcgi Daemon

Concurrency Level:      100
Time taken for tests:   15.181 seconds
Complete requests:      20000
Failed requests:        0
Write errors:           0
Non-2xx responses:      20000
Total transferred:      6460000 bytes
HTML transferred:       3440000 bytes
Requests per second:    1317.45 [#/sec] (mean)
Time per request:       75.904 [ms] (mean)
Time per request:       0.759 [ms] (mean, across all concurrent requests)
Transfer rate:          415.56 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.5      0       4
Processing:    12   75  31.2     68     474
Waiting:        9   73  31.4     66     471
Total:         12   76  31.3     68     475

Percentage of the requests served within a certain time (ms)
  50%     68
  66%     80
  75%     85
  80%     88
  90%     96
  95%    106
  98%    114
  99%    125
 100%    475 (longest request)


NodeJS

Concurrency Level:      100
Time taken for tests:   23.038 seconds
Complete requests:      20000
Failed requests:        0
Write errors:           0
Total transferred:      2700000 bytes
HTML transferred:       260000 bytes
Requests per second:    868.12 [#/sec] (mean)
Time per request:       115.192 [ms] (mean)
Time per request:       1.152 [ms] (mean, across all concurrent requests)
Transfer rate:          114.45 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.6      0      52
Processing:    39  114  21.7    109     306
Waiting:       28  112  21.5    107     305
Total:         40  115  21.7    109     306

Percentage of the requests served within a certain time (ms)
  50%    109
  66%    117
  75%    125
  80%    130
  90%    145
  95%    155
  98%    168
  99%    186
 100%    306 (longest request)

Стоит упомянуть, что всё это крутится на VirtualBox, которому выделено одно ядро от Core i7.
В итоге получили разницу в полтора раза в пользу Fastcgi Daemon, что, как мне кажется, не так уж и плохо для NodeJS. Вот только потребление процессора у NodeJS доходило до 50%, а памяти — до 45 MБ (5.6 МБ в состоянии покоя). В то время как Fastcgi Daemon отъедал не более 20% процессора и 9.5 МБ RAM (4.5 МБ в состоянии покоя). То есть к ресурсам последний более дружелюбен, что неудивительно. Ясное дело, что сравнение получилось совсем сферическим в вакууме. По-хорошему, надо делать более насыщенный код, подключать БД, запускать оба приложения в многопоточном режиме. Но для начала и этого хватит.

Вместо заключения


На мой взгляд, очень интересный проект вышел из недр Яндекса. Помимо того, что Fastcgi Daemon значительно упрощает написание веб-приложений на C++, так ещё и содержит в себе необходимые init.d-скрипты для удобного управления уже готовыми приложениями. В следующий раз я опишу создание сервиса для авторизации на основе Fastcgi Daemon с сессиями, БД и шаблонами.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 19
  • +3
    Стоит упомянуть, что onLoad() и onUnload() компонента вызываются в контексте основного потока, а handleRequest() — в контексте потока из тред-пула. Это означает, что подключение к базе, например, выполняется один раз, и надо обязательно помнить про многопоточность доступа и контексты. Например, в случае с MySQL, придётся делать пул коннектов.
    • 0
      Не надеюсь на ответ, спустя больше года, но все же попытаюсь. А зачем вызывать подключение к базе в onLoad() компонента? Можно ведь коннектиться в handleRequest() каждый раз. В случае PostgreSQL через какой-нибудь пуллер-коннектов, тот же PgBouncer, например. Тогда не будет накладных расходов на дорогостоящее подключение к самой базе, а будут «легкие» коннекты к PgBouncer. Пытаюсь разобраться с этим фреймворком, потому интересуюсь. Заранее спасибо за ответ, если он последует 8)
      • 0
        Много подключений делать плохо как минимум по причине того, что при использовании TCP сокеты (не fd, а именно структуры под сокеты в ядре) могут тупо кончиться. Ещё вариант: при подключении делаются некие настройки соединения. Если подключаться в handleRequest(), то их каждый раз нужно будет делать.

        А так, конечно, прям требованием подключение к базе в onLoad() не является. Делайте так, как лучше/быстрее в вашем случае. Своим комментарием я хотел подчеркнуть то, что onLoad() выполняется не в контексте тредпула, а из главного потока FastcgiDaemon при его инициализации.
    • +1
      Странно почему так медленно, у меня C (scgi) + nginx выдают 20 тыс в секунду при 1 тыс конкурентных. При том что там есть еще логика.
      • +3
        В подобном тесте fastcgi дает большие накладные расходы. Ведь NodeJS не был спрятан за http сервером? Если сравнивать C++ приложение, работающее в качестве http сервера, результаты будут значительно лучше. Сравнил с моей поделкой.

        NodeJS:

        Concurrency Level:      100
        Time taken for tests:   1.877 seconds
        Complete requests:      10000
        Failed requests:        0
        Write errors:           0
        Total transferred:      1150000 bytes
        HTML transferred:       140000 bytes
        Requests per second:    5328.73 [#/sec] (mean)
        Time per request:       18.766 [ms] (mean)
        Time per request:       0.188 [ms] (mean, across all concurrent requests)
        Transfer rate:          598.44 [Kbytes/sec] received
        
        Connection Times (ms)
                      min  mean[+/-sd] median   max
        Connect:        0    0   0.2      0       6
        Processing:     1   19   3.4     17      33
        Waiting:        1   19   3.4     17      32
        Total:          4   19   3.4     17      33
        
        Percentage of the requests served within a certain time (ms)
          50%     17
          66%     18
          75%     18
          80%     19
          90%     25
          95%     26
          98%     30
          99%     31
         100%     33 (longest request)
        


        C++ приложение:

        Concurrency Level:      100
        Time taken for tests:   0.493 seconds
        Complete requests:      10000
        Failed requests:        0
        Write errors:           0
        Total transferred:      1370000 bytes
        HTML transferred:       210000 bytes
        Requests per second:    20287.80 [#/sec] (mean)
        Time per request:       4.929 [ms] (mean)
        Time per request:       0.049 [ms] (mean, across all concurrent requests)
        Transfer rate:          2714.29 [Kbytes/sec] received
        
        Connection Times (ms)
                      min  mean[+/-sd] median   max
        Connect:        0    2   0.6      2       7
        Processing:     1    3   0.8      3       8
        Waiting:        1    2   0.7      2       7
        Total:          3    5   0.8      5      11
        
        Percentage of the requests served within a certain time (ms)
          50%      5
          66%      5
          75%      5
          80%      5
          90%      6
          95%      6
          98%      9
          99%      9
         100%     11 (longest request)


        Хотя мой код далеко не оптимальный, если сравнивать с nginx.
        • +2
          И добавлю то же C++ приложение, но в режиме fastcgi через nginx:

          Concurrency Level:      100
          Time taken for tests:   1.208 seconds
          Complete requests:      10000
          Failed requests:        0
          Write errors:           0
          Total transferred:      1560000 bytes
          HTML transferred:       210000 bytes
          Requests per second:    8275.49 [#/sec] (mean)
          Time per request:       12.084 [ms] (mean)
          Time per request:       0.121 [ms] (mean, across all concurrent requests)
          Transfer rate:          1260.72 [Kbytes/sec] received
          
          Connection Times (ms)
                        min  mean[+/-sd] median   max
          Connect:        0    0   0.9      0       7
          Processing:     3   12   2.5     12      32
          Waiting:        2   12   2.7     12      32
          Total:          3   12   2.2     12      32
          
          Percentage of the requests served within a certain time (ms)
            50%     12
            66%     12
            75%     12
            80%     13
            90%     13
            95%     16
            98%     19
            99%     21
           100%     32 (longest request)
          


          Но как правильно указал автор, не совсем корректно сравнивать ничего не делающее приложение.
          • 0
            а что у Вас в качестве веб-сервера используется?
            • 0
              Своя библиотечка, также сделанная для разработки web приложений на C++.
              • 0
                Чистые сокеты и собственный парсинг HTTP?
                Почему не используете Mongoose, Boost или POCO?
                • 0
                  Кто сказал, что там чистые сокеты? Где в boost реализация HTTP и FastCGI? Где в POCO и Mongoose поддержка FastCGI?
                  • 0
                    Я имел в виду часть веб-сервера:
                    C++ приложение, работающее в качестве http сервера
                    • +2
                      Потому, что так сложилось. Давайте мы не будем обсуждать мою бибилотеку, она не имеет к статье никакого отношения. Вот когда я напишу статью о ней, к этому вопросу можно будет вернутся :-)
          • +1
            Я тоже ожидал больших результатов от fastcgi. Но скорее всего медлительность связана с моим конкретным окружением: VirtualBox, одно ядро, дефолтные настройки как nginx, так и Fastcgi Daemon.
          • 0
            Тоже используем FastCGI при построении C++ бэкендов. За основу взяли стандартную libfcgi-dev и написали вокруг неё небольшую обвязку на С++. По дизайну библиотека получилась очень похожей на описываемую в статье. Померил предлагаемый в статье пример — получается 2-5 мс. Измерения проводил на рабочей машине (i7, 8GB). Бэкенд работал так же в связке с Nginx, для тестирования использовался тот же ab. Однако, тестирование проводилось через loopback. Считаю такой метод измерений более показательным, т.к. все накладные расходы на tcp-handshake и прочее сводятся к минимуму, измеряется именно эффективность связки Nginx + FastCGI бэкенд. Предлагаю автору попробовать измерить без виртуализации и через loopback:)
            • 0
              Попробуйте лучше через unix socket тогда, а не просто tcp через loopback.
              • 0
                loopback имелся ввиду в связке ab — Ngnix. Для взаимодействия Nginx — FastCGI используем как раз unix socket.
            • 0
              пять лет назад я делал что-то похожее libscgi
              пока заказчики не жаловались…
              • 0
                У меня вначале получилось по производительности хуже, чем на php, но по совету одного местного хаброжителя — уменьшил уровень логгирования — количество обрабатываемых запросов в секунду выросло в разы по сравнению с конфигурацией по умолчанию.
                • 0
                  Спасибо за статью. Буду ждать продолжения.
                  А кто-нибудь уже пробовал CppCMS — Web фреймворк от создателя Boost.Locale? Было бы интересно сравнить его с FastCGI-Deamon.

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