performance: boost::asio vs sync sockets
От: Аноним  
Дата: 13.01.14 18:11
Оценка: -1
Есть простой сервер на winsock, который был реализован в спешке на коленке, где на каждое соединение создается отдельный поток, все вызовы, естественно, блокирующие. Сейчас пытаюсь как-то все это дело оптимизировать, решил попоробовать использовать boost.asio, но возникли проблемы на одном из тестовых сценариев.

Итак, реализовал сервер на основе одного из бустовских примеров. Создаю пул потоков, в каждом из потоков вызываю io_service->run(), перед этим создаю acceptor, вызываю async_accept(). Здесь проблем нет, все соединения приходят, все ОК. После создания соединения схема простая:
делаю один вызов async_read(), на который вызывается handle_read(). Читаю из handle_read() данные с помощью socket->read_some(), если неполные данные вызываю опять async_read() один раз, если полные — вызываю async_send() и отправляю ответ клиенту, в handle_write() — обработчике, снова вызываю async_read(). И так процесс продолжается N-раз, потом соединение закрывается. То есть схема взаимодейтсвия абсолютно стандартная:
client-(connect)->server-(connection established)->client-(request)->server-(response)->client->...

В принципе, все работает, но не устраивает производительность в некоторых сценариях.
Итак, сценарий такой: запускаю одновременно 10 клиентов на другой машине, каждый клиент шлет 50000 сообщений размером в 2Кбайт, сервер отвечает на каждое сообщение таким же по размеру сообщением. Сеть 1Гигабит. Так вот, старый сервер отрабатывает где-то за 25 секунд, а бустовский в районе 40. А если запустить все это на одном компе, то там вообще старая реализация рвет бустовскую как тузик грелку. При этом, если увеличивать размер сообщения, то ситуация выравнивается. Профайлер особо ничего не дал. Точнее, что дал, то уже пофиксил. Сейчас 50% — буствоский код, 50% — мой обработчик.
Псмотрел в performance monitor, там вот такая хрень:


Для старой реализации все ОК:


Собственно вопрос в том, почему может быть такой высокий уровень CPU interrupts и как вообще boost.asio "дружит" с большим кол-вом небольших сообщений, реально ли добиться более высокой производительности?
Re: performance: boost::asio vs sync sockets
От: smeeld  
Дата: 13.01.14 19:34
Оценка:
Здравствуйте, Аноним, Вы писали:

У меня тоже свой http сервис на C/epoll тоже рвёт http сервис на boost::asio, и тоже в районе 3:2.
К тому же для функционирования boost::asio при 10K/ps пришлось добавлять размер стека, иначе прога сегфолтилась.
Не пойму только зачем изначально создавать пул потоков, почему не создавать их по мере появления соединений.
И непонятно зачем для каждого потока свой io_service поток, у меня увеличения их количества более одного
производительность не повышала, повышалась только загрузка процессора.
Re[2]: performance: boost::asio vs sync sockets
От: Аноним  
Дата: 14.01.14 00:29
Оценка:
Здравствуйте, smeeld, Вы писали:

S>Здравствуйте, Аноним, Вы писали:


S>У меня тоже свой http сервис на C/epoll тоже рвёт http сервис на boost::asio, и тоже в районе 3:2.

S>К тому же для функционирования boost::asio при 10K/ps пришлось добавлять размер стека, иначе прога сегфолтилась.
S>Не пойму только зачем изначально создавать пул потоков, почему не создавать их по мере появления соединений.
Да можно, просто сейчас это не особо принципиально.
S>И непонятно зачем для каждого потока свой io_service поток, у меня увеличения их количества более одного
S>производительность не повышала, повышалась только загрузка процессора.
Не, у меня один io_service на весь пул, но толку тоже не много. Кроме того, это синтетический тест, в реале будет гораздо
больше клиентов, но интенсивность каждого ниже, поэтому результаты будут другими, но все равно хотелось бы сравнимых результатов со старой реализацией, тем более, что там парсинг гораздо менее эффективен. А главное, не понятно почему так много хардварных прерываний в сравнении со старой реализацией — 54тыс в секунду против 3тыс в секунду.
Re: performance: boost::asio vs sync sockets
От: kaa.python Ниоткуда РСДН профессионально мёртв и завален ватой.
Дата: 14.01.14 05:07
Оценка: +1
По моим ощущениям, использовать ASIO для чего-то серьезного не вариант. Он крайне "overarchitectured", как и 50% BOOST, впрочем, и для высоких нагрузок не очень подходит. Лучше посмотреть на в сторону libuv, если хочется чего-то легкого или ACE, если хочется простой но мощный "всемогутор".
Re[3]: performance: boost::asio vs sync sockets
От: SkyDance Земля  
Дата: 14.01.14 05:35
Оценка:
А>А главное, не понятно почему так много хардварных прерываний в сравнении со старой реализацией — 54тыс в секунду против 3тыс в секунду.

Надо смотреть ваш код. Что за прерывания и откуда они берутся. Вероятно, где-то ошибка в вызовах, например, чтение/запись из/в сокета, который еще не готов.
Re[4]: performance: boost::asio vs sync sockets
От: Аноним  
Дата: 14.01.14 09:04
Оценка:
Здравствуйте, SkyDance, Вы писали:

А>>А главное, не понятно почему так много хардварных прерываний в сравнении со старой реализацией — 54тыс в секунду против 3тыс в секунду.


SD>Надо смотреть ваш код. Что за прерывания и откуда они берутся. Вероятно, где-то ошибка в вызовах, например, чтение/запись из/в сокета, который еще не готов.

На уровне моего кода и на уровне буста никаких ошибок не видно, все проходит прозрачно, отрабатывает "как должно быть", все данные отсылаются/принимаются, как правило, за один вызов, никаких исключений нет (не путать с прерываниями). Код записи такой:
(буфер хранится на уровне инстанса класса, state — POD тип, пуляется по значению в handle_read)
void Connection::async_read(const ReadingState &state)
{
    try
    {
       ...
        char *buf = &this->recv_buf[state.received_data_size];
        size_t buf_size = this->recv_buf.size() - state.received_data_size;

        this->socket->async_receive(
            boost::asio::buffer(buf, buf_size),
            boost::bind(
                &Connection::handle_read, 
                this,
                boost::asio::placeholders::error,
                boost::asio::placeholders::bytes_transferred,
                state));
    }
    catch (const std::exception &e)
    {
        // close connection and log err
    }
    catch (...)
    {
       // close connection and log err
    }
}

void Connection::handle_read(const boost::system::error_code &err, size_t bytes_transferred, ReadingState state)
{
    ...

    if (err)
    {
        // close connection, log err, and signal to server
        return;
    }

    ...

    state.received_data_size += bytes_transferred;

    if (state.received_data_size < state.msg_size)
    {
        this->async_read(state);
        return;
    }

    try
    {
        // process message and call message handler
        ...

        this->async_write(response);
    }
    catch ()
    {
     ...
    }


Статистику по прерываниям я вижу только по логам perfmon'a (см картинки в первом сообщении) и, как я понимаю, генерируются они сетевой картой и их высоких уровень под нагрузкой — это тоже нормально. Я просто не понимаю, почему такая большая разница между двумя реализациями на одном и том же входном трафике.
Re[2]: performance: boost::asio vs sync sockets
От: Аноним  
Дата: 14.01.14 09:32
Оценка:
Здравствуйте, kaa.python, Вы писали:

KP>По моим ощущениям, использовать ASIO для чего-то серьезного не вариант. Он крайне "overarchitectured", как и 50% BOOST, впрочем, и для высоких нагрузок не очень подходит. Лучше посмотреть на в сторону libuv, если хочется чего-то легкого или ACE, если хочется простой но мощный "всемогутор".

ОК, libav посмотрю, но склоняюсь к мысли написать сетевую часть на голом API, там я хоть смогу контролировать весь код. Заодно и попробую новые плюшки от Microsoft: What's New for Windows Sockets.
Re: performance: boost::asio vs sync sockets
От: Evgeny.Panasyuk Россия  
Дата: 14.01.14 11:22
Оценка:
Здравствуйте, Аноним, Вы писали:

А>делаю один вызов async_read(), на который вызывается handle_read(). Читаю из handle_read() данные с помощью socket->read_some(), если неполные данные вызываю опять async_read() один раз, если полные — вызываю async_send() и отправляю ответ клиенту, в handle_write() — обработчике, снова вызываю async_read().


Это опечатка? read_some — блокирующий.
Re[2]: performance: boost::asio vs sync sockets
От: Evgeny.Panasyuk Россия  
Дата: 14.01.14 11:25
Оценка: +1
Здравствуйте, kaa.python, Вы писали:

KP>По моим ощущениям, использовать ASIO для чего-то серьезного не вариант. Он крайне "overarchitectured", как и 50% BOOST,


Где же Asio "overarchitectured"? Достаточно простой и неинтрузивный интерфейс.

KP>впрочем, и для высоких нагрузок не очень подходит.


Почему? Есть какие-то принципиальные моменты?
Re: performance: boost::asio vs sync sockets
От: Evgeny.Panasyuk Россия  
Дата: 14.01.14 11:30
Оценка:
Здравствуйте, Аноним, Вы писали:

А>client-(connect)->server-(connection established)->client-(request)->server-(response)->client->...


Кстати, а asio_handler_allocate/asio_handler_deallocate как делается?
Re[3]: performance: boost::asio vs sync sockets
От: kaa.python Ниоткуда РСДН профессионально мёртв и завален ватой.
Дата: 14.01.14 11:34
Оценка:
Здравствуйте, Evgeny.Panasyuk, Вы писали:

EP>Где же Asio "overarchitectured"? Достаточно простой и неинтрузивный интерфейс.


Видимо, вернее не видимо, а однозначно судя по твоим постам в C++, у нас разное понимание overarchitectured.

KP>>впрочем, и для высоких нагрузок не очень подходит.

EP>Почему? Есть какие-то принципиальные моменты?

Есть. 1) Overarchitectured и как следствие малопригоден для исправления ошибок в типичной разнородной команде. 2) На больших нагрузках начинают возникать странности с поведением (точнее начинал, когда в последний раз им пользовался). В то же время на чем-то небольшом и не нагруженном довольно уместен.
Re[2]: performance: boost::asio vs sync sockets
От: Аноним  
Дата: 14.01.14 11:43
Оценка:
Здравствуйте, Evgeny.Panasyuk, Вы писали:

EP>Здравствуйте, Аноним, Вы писали:


А>>делаю один вызов async_read(), на который вызывается handle_read(). Читаю из handle_read() данные с помощью socket->read_some(), если неполные данные вызываю опять async_read() один раз, если полные — вызываю async_send() и отправляю ответ клиенту, в handle_write() — обработчике, снова вызываю async_read().


EP>Это опечатка? read_some — блокирующий.

Да, описка, read_some на клиенте используется. На сервере async_receive. Я привел немного кода здесь
Автор:
Дата: 14.01.14
.
Re[4]: performance: boost::asio vs sync sockets
От: Evgeny.Panasyuk Россия  
Дата: 14.01.14 11:51
Оценка:
Здравствуйте, kaa.python, Вы писали:

EP>>Где же Asio "overarchitectured"? Достаточно простой и неинтрузивный интерфейс.

KP>Видимо, вернее не видимо, а однозначно судя по твоим постам в C++, у нас разное понимание overarchitectured.

Всё же интересно где именно он "overarchitectured".
Что конкретно не нравится, как это сделать по-другому, в каких местах есть лишняя "архитектура"?

KP>На больших нагрузках начинают возникать странности с поведением (точнее начинал, когда в последний раз им пользовался).


Такие места нужно выявлять и отправлять в trac.
Re[2]: performance: boost::asio vs sync sockets
От: Аноним  
Дата: 14.01.14 11:55
Оценка:
Здравствуйте, Evgeny.Panasyuk, Вы писали:

EP>Здравствуйте, Аноним, Вы писали:


А>>client-(connect)->server-(connection established)->client-(request)->server-(response)->client->...


EP>Кстати, а asio_handler_allocate/asio_handler_deallocate как делается?

Никак, сейчас поискал, я так понимаю, что на каждый чих (вызов async_write/async_send) вызываются по-умолчанию new/delete для хранения объекта обработчика, right? Видимо, тогда имеет смысл что-то кастомное сделать.
Re[4]: performance: boost::asio vs sync sockets
От: Abyx Россия  
Дата: 14.01.14 12:00
Оценка:
Здравствуйте, kaa.python, Вы писали:

KP>Overarchitectured и как следствие малопригоден для исправления ошибок в типичной разнородной команде.

не понял, asio малопригоден для исправления ошибок? это как?
In Zen We Trust
Re[3]: performance: boost::asio vs sync sockets
От: Evgeny.Panasyuk Россия  
Дата: 14.01.14 12:04
Оценка:
Здравствуйте, Аноним, Вы писали:

А>>>client-(connect)->server-(connection established)->client-(request)->server-(response)->client->...

EP>>Кстати, а asio_handler_allocate/asio_handler_deallocate как делается?
А>Никак, сейчас поискал, я так понимаю, что на каждый чих (вызов async_write/async_send) вызываются по-умолчанию new/delete для хранения объекта обработчика, right?

Да, правильно.

А>Видимо, тогда имеет смысл что-то кастомное сделать.


Вот тут пример.
Re[5]: performance: boost::asio vs sync sockets
От: SkyDance Земля  
Дата: 14.01.14 22:37
Оценка: +1
А>(буфер хранится на уровне инстанса класса, state — POD тип, пуляется по значению в handle_read)

А> char *buf = &this->recv_buf[state.received_data_size];

А> size_t buf_size = this->recv_buf.size() — state.received_data_size;

Какой размер буфера? Он растёт до нужного размера? Есть подозрение, что из сокета не вычитывается весь поток, из-за чего asio сигналит постоянно (поди и загрузка CPU 100% по всем ядрам).
Еще я не заметил синхронизации. У вас один io_service на все потоки. Хендлеры надо оборачивать в strand, иначе там битва за сокеты случается. С непредсказуемым поведением и 100% загрузкой процессора.

А> catch (...)

А> {

Это, конечно же, опечатка, так? Или вы в самом деле ловите (...)? И что, туда что-то падает? ЧТО?!

А> if (state.received_data_size < state.msg_size)

А> {
this->>async_read(state);
А> return;
А> }

Вы читаете протокол вида "frame_size->frame_body"? Попробуйте переписать по такому принципу:

// Start the first asynchronous operation for the connection.
void Connection::start()
{
    <... ssl code removed...>
    // start reading now
    socket_.async_read_some(boost::asio::buffer(&frame_size_, sizeof(frame_size_)),
            strand_.wrap(std::bind(&Connection::handle_read_frame_size, shared_from_this(),
        std::placeholders::_1, std::placeholders::_2)));
}

void Connection::handle_read_frame_size(const boost::system::error_code& _ec,
            std::size_t _bytes)
{
    if (!_ec && _bytes == sizeof(frame_size_))
    {
                // allocate storage for frame body using frame_size_
                <...>
        socket_.async_read_some(boost::asio::buffer(frame_body_, _bytes),
            strand_.wrap(std::bind(&Connection::handle_read_frame_body, shared_from_this(),
            std::placeholders::_1, std::placeholders::_2)));
    }else
    {
        connection_manager_.stop(shared_from_this());
    }
}

void Connection::handle_read_frame_body(const boost::system::error_code& _ec,
            std::size_t _bytes)
{
    if (!_ec)
    {
        if (_bytes != frame_size_)
            boost::throw_exception(ConnectionError());

        // parse complete frame and form response
        std::string response = session_.handle((const char*)frame_body_.data(), frame_size_);

        // send response
        auto self(shared_from_this());
        boost::asio::async_write(socket_, boost::asio::buffer(response.c_str(), response.size()),
            [this, self](boost::system::error_code _ec, std::size_t)
            {
                // if connection is to terminate (or already broken), drop it now
                if ( _ec)
                    connection_manager_.stop(self);
            }
    );
    }else
    {
        connection_manager_.stop(shared_from_this());
    }
}


Это из реального проекта с нагрузкой огого, правда, не на Windows, но должно и там работать. Лямбды можно заменить на методы класса + bind.

А>Статистику по прерываниям я вижу только по логам perfmon'a (см картинки в первом сообщении) и, как я понимаю, генерируются они сетевой картой и их высоких уровень под нагрузкой — это тоже нормально. Я просто не понимаю, почему такая большая разница между двумя реализациями на одном и том же входном трафике.


В том и дело, что такая разница в количестве прерываний может говорить о двух вероятных проблемах: либо где-то в тесты вкралась ошибка (например, asio-вариант тестируется с на порядок большей нагрузкой), либо где-то есть лишние вызовы системных функций, проваливающихся в ядро. Что случается, например, при некорректном размере буфера, когда пришедший большой фрейм читается не за один раз целиком, а 100 раз по 10 байт.
Re[6]: performance: boost::asio vs sync sockets
От: SkyDance Земля  
Дата: 14.01.14 23:06
Оценка:
SD> boost::asio::async_write(socket_, boost::asio::buffer(response.c_str(), response.size()),
SD> [this, self](boost::system::error_code _ec, std::size_t)

Прошу прощения, здесь, конечно же, тоже через strand. В процессе выкусывания кода из проекта (response там отправляется worker threads асинхронно через post) и убирания лишних слоёв сам собой выкусился и strand. Упражнение — добавьте самостоятельно, не промахнитесь.
Re: performance: boost::asio vs sync sockets
От: Аноним  
Дата: 14.01.14 23:40
Оценка: 19 (4)
Здравствуйте, Аноним, Вы писали:

А>Собственно вопрос в том, почему может быть такой высокий уровень CPU interrupts и как вообще boost.asio "дружит" с большим кол-вом небольших сообщений, реально ли добиться более высокой производительности?


Всё, всем спасибо, проблема решена. Затык был в конфигурации сетевой карты, а именно в установленной опции Receive Side Scaling Queues, которая была равна 1. Что по сути было эквивалентно выключенной опции Receive Side Scaling (RSS), соответственно обработка трафика сетевой картой не скейлилась по ядрам. В случае же с блокирующим вызовом, похоже, что драйвер использовал то ядро, на котором был блокирующий вызов, поэтому там производительность была выше.
Re[6]: performance: boost::asio vs sync sockets
От: Аноним  
Дата: 15.01.14 00:01
Оценка:
Здравствуйте, SkyDance, Вы писали:

SD>Какой размер буфера? Он растёт до нужного размера? Есть подозрение, что из сокета не вычитывается весь поток, из-за чего asio сигналит постоянно (поди и загрузка CPU 100% по всем ядрам).

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

SD>Еще я не заметил синхронизации. У вас один io_service на все потоки. Хендлеры надо оборачивать в strand, иначе там битва за сокеты случается. С непредсказуемым поведением и 100% загрузкой процессора.

За какие сокеты битва? У меня handler на каждый инстанс, то есть один сокет -> один хэндлер. Оборачивать его нужно, как я понимаю, для защиты своих данных, но у меня там стоит гард на критической секции (в код не попало), так что не вижу никакого смысла еще и strand использовать.
А>> catch (...)
А>> {

SD>Это, конечно же, опечатка, так? Или вы в самом деле ловите (...)? И что, туда что-то падает? ЧТО?!

В чем проблема то? В винде, кстати, catch(...) может ловить и SEH.


SD>Это из реального проекта с нагрузкой огого, правда, не на Windows, но должно и там работать. Лямбды можно заменить на методы класса + bind.

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