четверг, 24 июля 2014 г.

[prog.c++] Задышал новый диспетчер в SObjectizer: агент может работать сразу на нескольких нитях

Сегодня в SObjectizer-5.4.0 начал работать еще один диспетчер, под условным названием adv_thread_pool. Он позволяет параллельно запустить несколько событий агента на разных нитях.

Для чего такой диспетчер понадобился? Ведь раньше все штатные диспетчеры гарантировали, что в любой момент времени у агента может работать только один обработчик события и происходит это на контексте только одной нити. Такое ограничение является очень естественным и понятным, снимающим с прикладного разработчика кучу геморроя. Не в малой степени именно благодаря такому ограничению разработка многопоточных приложений на SObjectizer оказывается гораздо проще, чем с ручным использованием нитей и мутексов. Тем не менее, иногда это ограничение хочется обойти.

Было в мой практике несколько агентов, которые, по сути, являлись stateless преобразователями трафика. Т.е. они получали сообщение, как-то преобразовывали его, выбирали следующего адресата и отсылали сообщение дальше. Один из таких агентов, работая в самом нагруженном на тот момент приложении, свободно прокачивал через себя порядка 7-8 тысяч прикладных сообщений в секунду (и это на SO-4, который работает медленнее SO-5). Запас по производительности еще был, но ради одной амбициозной задачи могло потребоваться увеличить пропускную способность этого агента в 4-5 раз. Даже не смотря на то, что агент был написан на C++ и работал весьма эффективно, при такой нагрузке явно можно было бы упереться в потолок производительности на одной нити. Поэтому естественным решением выглядело бы распараллеливание обработки трафика на несколько ядер процессора. Т.е. нужно было научить агента распараллеливать свою работу.

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

Вот из таких соображений в версии 5.4.0 и появился adv_thread_pool-диспетчер. Т.е. продвинутый диспетчер с пулом рабочих потоков. Его продвинутость заключается в том, что он может определить, какие события каких агентов можно запустить одновременно на нескольких нитях, а для каких агентов этого делать нельзя. Так что, если агент умеет работать сразу на нескольких нитях, то будучи привязанным к adv_thread_pool-диспетчеру агент получит распараллеливание и балансировку нагрузки между несколькими рабочими потоками от SObjectizer-а.

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

Возможность распараллеливания агент задает на уровне своих событий оформляя подписку на сообщения. Если он подписывается обычным способом:

so_subscribe(mbox).event(&router::evt_reconfigure);

То такой обработчик будет считаться однопоточным (not thread safe в терминологии SObjectizer). И SObjectizer гарантирует, что к какому штатному диспетчеру не был привязан агент router, его событие evt_reconfigure будет запущено только на одной нити, и пока оно не завершится, никакие другие события агента запущены не будут.

Но, если пользователь передаст в event() еще один аргумент:

so_subscribe(mbox).event(&router::evt_route_message, so_5::thread_safe);

То SObjectizer поймет, что такой обработчик является безопасным для параллельного запуска на нескольких нитях (thread safe), и эта возможность будет задействована на тех диспетчерах, которые это поддерживают. Т.е. сейчас это adv_thread_pool, может со временем еще что-то добавится. Остальные же диспетчеры, которые предоставляют агенту всего лишь одну рабочую нить, не будут делать различия между thread safe и not thread safe обработчиками.

У такого подхода оказалось очень важное преимущество. Агент может сочетать thread safe и not thread safe обработчики. Например, обработчик сообщения о переконфигурировании агента объявляется однопоточным. И тогда adv_thread_pool сам обеспечит, чтобы все текущие обработчики завершились перед запуском обработчика evt_reconfigure. А когда evt_reconfigure завершит свою работу, adv_thread_pool сможет запустить несколько обработчиков evt_route_message сразу впараллель на нескольких рабочих нитях. Т.е. и здесь разработчик получает помощь от SObjectizer для обеспечения целостности агента в многопоточном окружении.

На данный момент у adv_thread_pool-а есть та же детская болезнь, что и у thread_pool-диспетчера: очень простая реализация disp_queue, из-за которой распределение заявок по нескольким рабочим нитям обходится дороже, чем этого хотелось бы. Поэтому мне предстоит погрузиться в изучение различных механизмов организации multi-producer/multi-consumer очередей. Правда, у SObjectizer есть одна важная особенность: когда есть работа, он должен разбираться с ней с наименьшими накладными расходами, но когда работы нет, SObjectizer должен спать не не жрать вычислительные ресурсы. Так что активное ожидание на spinlock-ах или атомиках в чистом виде не пройдет, нужно как-то задействовать и тяжелые примитивы синхронизации, вроде mutex-ов и condition_variable. Для multi-producer/single-consumer сценария такое решение нашлось и воплотилось в реальный код. Нужно теперь сварганить что-то подобное для MPMC-варианта. Так что буду изучать, сравнивать, думать, пробовать.

После чего, по большому счету, в версии 5.4.0 останется всего одно важное нововведение. Но, к сожалению, пока толком не проработанное. Хотя лично у меня есть ощущение, что что-то подобное уже давно нужно внедрить в SObjectizer и далее смотреть, что из этого получается. Речь идет о проблеме защиты агентов от перегрузки (overload control, если по-умному). Сейчас никакой защиты на уровне SObjectizer нет. Если разработчик ошибается в своих предположениях и агент не успевает разгребать то, что к нему валится, то очереди неограниченно пухнут, память отжирается, производительность падает, память может уйти в своп и т.д. На практике, к счастью, такое встречалось считанные разы за все 12 лет существования SObjectizer-4. Но, если позиционировать SObjectizer как серьезный рабочий инструмент, то что-то в этом направлении делать нужно. Некоторые предварительные мысли на сей счет есть, но далеко не уверен, что в реализацию пойдет именно такой вариант. Если кому-то эта тема интересно, то подключайтесь к обсуждению.

Ну вот как-то так. Работы много, едва-едва разгребаюсь. Когда будет релиз 5.4.0 точно не известно, как только, так сразу. Хорошо бы в августе, но тут уж как получится.

Комментариев нет: