2016-10-27 5 views
14

Мы используем asio в производстве уже много лет, а недавно мы достигли критической точки, когда наши серверы загружаются достаточно, чтобы заметить загадочную проблему.boost :: asio рассуждает за num_implementations для io_service :: strand

В нашей архитектуре каждый отдельный объект, который работает независимо, использует персональный объект strand. Некоторые из объектов могут выполнять долгую работу (чтение из файла, выполнение запроса MySQL и т. Д.). Очевидно, что работа выполняется в обработчиках, завернутых цепочкой. Все звучит красиво и красиво и должно работать безупречно, пока мы не начнем замечать невозможные вещи, такие как таймеры, истекающие секунды после их появления, даже если потоки «ждут работы» и работа останавливается без видимых причин. Похоже, что долгая работа, выполненная внутри пряди, оказала влияние на другие несвязанные нити, не все из них, но большинство.

Бесчисленное количество часов было потрачено, чтобы выявить проблему. Дорожка привела к способу strand объект создан: strand_service::construct (here).

По какой-то причине разработчики решили ограничить количество реализаций strand. Это означает, что некоторые полностью несвязанные объекты будут использовать единую реализацию и, следовательно, будут узкими.

В автономном режиме (non-boost) asio библиотека аналогичный подход используется. Но вместо общих реализаций каждая реализация теперь независима, но может совместно использовать объект mutex с другими реализациями (here).

В чем дело? Я никогда не слышал о ограничениях на количество мьютексов в системе. Или любые накладные расходы, связанные с их созданием/уничтожением. Хотя последняя проблема может быть легко решена путем утилизации мьютексов вместо их уничтожения.

У меня есть простейший тест, чтобы показать, насколько драматичным является снижение производительности:

#include <boost/asio.hpp> 
#include <atomic> 
#include <functional> 
#include <iostream> 
#include <thread> 

std::atomic<bool> running{true}; 
std::atomic<int> counter{0}; 

struct Work 
{ 
    Work(boost::asio::io_service & io_service) 
     : _strand(io_service) 
    { } 

    static void start_the_work(boost::asio::io_service & io_service) 
    { 
     std::shared_ptr<Work> _this(new Work(io_service)); 

     _this->_strand.get_io_service().post(_this->_strand.wrap(std::bind(do_the_work, _this))); 
    } 

    static void do_the_work(std::shared_ptr<Work> _this) 
    { 
     counter.fetch_add(1, std::memory_order_relaxed); 

     if (running.load(std::memory_order_relaxed)) { 
      start_the_work(_this->_strand.get_io_service()); 
     } 
    } 

    boost::asio::strand _strand; 
}; 

struct BlockingWork 
{ 
    BlockingWork(boost::asio::io_service & io_service) 
     : _strand(io_service) 
    { } 

    static void start_the_work(boost::asio::io_service & io_service) 
    { 
     std::shared_ptr<BlockingWork> _this(new BlockingWork(io_service)); 

     _this->_strand.get_io_service().post(_this->_strand.wrap(std::bind(do_the_work, _this))); 
    } 

    static void do_the_work(std::shared_ptr<BlockingWork> _this) 
    { 
     sleep(5); 
    } 

    boost::asio::strand _strand; 
}; 


int main(int argc, char ** argv) 
{ 
    boost::asio::io_service io_service; 
    std::unique_ptr<boost::asio::io_service::work> work{new boost::asio::io_service::work(io_service)}; 

    for (std::size_t i = 0; i < 8; ++i) { 
     Work::start_the_work(io_service); 
    } 

    std::vector<std::thread> workers; 

    for (std::size_t i = 0; i < 8; ++i) { 
     workers.push_back(std::thread([&io_service] { 
      io_service.run(); 
     })); 
    } 

    if (argc > 1) { 
     std::cout << "Spawning a blocking work" << std::endl; 
     workers.push_back(std::thread([&io_service] { 
      io_service.run(); 
     })); 
     BlockingWork::start_the_work(io_service); 
    } 

    sleep(5); 
    running = false; 
    work.reset(); 

    for (auto && worker : workers) { 
     worker.join(); 
    } 

    std::cout << "Work performed:" << counter.load() << std::endl; 
    return 0; 
} 

Построить его, используя следующую команду:

g++ -o asio_strand_test_case -pthread -I/usr/include -std=c++11 asio_strand_test_case.cpp -lboost_system 

Тестовый прогон в обычном порядке:

time ./asio_strand_test_case 
Work performed:6905372 

real 0m5.027s 
user 0m24.688s 
sys  0m12.796s 

Испытание с длительной блокировкой:

time ./asio_strand_test_case 1 
Spawning a blocking work 
Work performed:770 

real 0m5.031s 
user 0m0.044s 
sys  0m0.004s 

Разница драматична. Что происходит, каждая новая неблокирующая работа создает новый объект strand до тех пор, пока он не будет иметь одну и ту же реализацию с strand работы блокировки. Когда это происходит, это тупик, пока не закончится работа.

Edit: Уменьшенных параллельная работа до числа рабочих потоков (от 1000 до 8) и обновленного выхода тестового прогона. Это потому, что, когда оба числа близки, проблема более заметна.

ответ

3

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

Проблема, с которой вы сталкиваетесь, как я понимаю, с внедрением boost, заключается в том, что она по умолчанию создает только ограниченное число strand_impl, 193, как я вижу в моей версии boost (1.59).

Теперь это означает, что большое количество запросов будет в конфликте, поскольку они будут ждать разблокировки блокировки другим обработчиком (используя тот же экземпляр strand_impl).

Мое предположение для того, чтобы делать такую ​​вещь, было бы запретить перегрузку ОС, создав много-много-много мьютексов. Это было бы плохо. Текущая реализация позволяет повторно использовать замки (и настраиваемым, как мы увидим ниже)

В моей установке:

 
MacBook-Pro:asio_test amuralid$ g++ -std=c++14 -O2 -o strand_issue strand_issue.cc -lboost_system -pthread 
MacBook-Pro:asio_test amuralid$ time ./strand_issue 
Work performed:489696 

real 0m5.016s 
user 0m1.620s 
sys 0m4.069s 
MacBook-Pro:asio_test amuralid$ time ./strand_issue 1 
Spawning a blocking work 
Work performed:188480 

real 0m5.031s 
user 0m0.611s 
sys 0m1.495s 

Теперь, есть способ, чтобы изменить это число кэшированных реализаций по настройка макроса BOOST_ASIO_STRAND_IMPLEMENTATIONS.

Ниже результат я получил после того, как установить его в значение 1024:

 
MacBook-Pro:asio_test amuralid$ g++ -std=c++14 -DBOOST_ASIO_STRAND_IMPLEMENTATIONS=1024 -o strand_issue strand_issue.cc -lboost_system -pthread 
MacBook-Pro:asio_test amuralid$ time ./strand_issue 
Work performed:450928 

real 0m5.017s 
user 0m2.708s 
sys 0m3.902s 
MacBook-Pro:asio_test amuralid$ time ./strand_issue 1 
Spawning a blocking work 
Work performed:458603 

real 0m5.027s 
user 0m2.611s 
sys 0m3.902s 

Почти то же самое для обоих случаях! Возможно, вы захотите настроить значение макроса в соответствии с вашими потребностями, чтобы отклонение было небольшим.

+1

* «Мое предположение для того, чтобы делать такую ​​вещь, было бы запретить перегрузку ОС, создав много-много-много мьютексов. Это было бы плохо». * Почему? Какие накладные расходы есть в стороне от небольшой постоянной памяти (на мьютекс)? –

+1

@yurikilochek Это мьютексы. По определению они бесполезны, если они не используются для синхронизации. Это делает одновременным ожидание больших коллекций примитивов синхронизации. ':: WaitForMultipleObjectsEx' может не возражать, но это контекстный переключатель, это не просто несколько байт памяти. На linux нет такого звонка AFAIK. – sehe

+0

@Arunmu Независимо от количества реализаций проблема будет сохраняться, потому что она находится в дизайне. Увеличение числа может выиграть некоторое время, но только в некоторой степени. В режиме реального времени это никогда не будет работать. Попробуйте мой пример с «рабочими объектами», равными числу потоков, то есть '8' вместо' 1000'. В этом случае реализация '1024' едва помогает (' Выполненная работа: 8331'). – GreenScape

2

Автономные ASIO и Boost.ASIO в последние годы стали совершенно изолированными, поскольку автономный ASIO медленно превращается в стандартную реализацию TS для стандартизации. Все «действия» происходят в автономном ASIO, включая основные исправления ошибок. В Boost.ASIO внесены очень незначительные исправления. К настоящему времени между ними существует разница в несколько лет.

Поэтому я предлагаю всем, кто ищет какие-либо проблемы с Boost.ASIO, должен переключиться на автономный ASIO. Преобразование обычно не сложно, посмотрите на многие макроконфигурации для переключения между C++ 11 и Boost в config.hpp. Исторически Boost.ASIO был фактически сгенерирован сценарием из автономного ASIO, это может быть так, что Крис сохранил эти сценарии, и поэтому вы можете восстановить новый блестящий новый Boost.ASIO с последними изменениями. Я подозреваю, что такая сборка не очень хорошо протестирована.

+0

Это интересно @Niall Douglas. Рассматривая примечания к выпуску, последняя версия [standalone asio] (http://think-async.com/), чтобы превратить ее в [boost] (http://www.boost.org/users/news/), была еще в апреле 2015 года. Эта версия была asio 1.10.6, в то время как последняя версия для разработчиков [asio development] (http://think-async.com/asio/asio-1.11.0/doc/asio/history.html#asio.history .asio_1_11_0) показывает 1.10.5 как последний основной релиз, поэтому вы правы, они расходились, а Крис концентрируется на Заявке на сетевую библиотеку, теперь [N4612] (http://open-std.org/JTC1/ SC22/WG21/docs/papers/2016/n4612.pdf) – kenba

+0

К сожалению, стратегия распределения strang_impl не была изменена в автономной версии. В «strand_executor_service» есть некоторая работа в правильном направлении. Я попытался перенести его в vanilla 'strand_service', но не повезло. Текущий дизайн во многом зависит от гарантии того, что 'strand_impl' не разрушен, событие после' strand', что почти невозможно исправить без редизайна. В любом случае, я написал в список рассылки. – GreenScape