pub/sub model, msm, and asio

42
Pub/Sub モモモモ msm モ asio モ Takatoshi Kondo 06/21/2022 1

Upload: takatoshi-kondo

Post on 14-Apr-2017

623 views

Category:

Software


0 download

TRANSCRIPT

Page 1: Pub/Sub model, msm, and asio

Pub/Sub モデルと msm と asioとTakatoshi Kondo

05/03/2023 1

Page 2: Pub/Sub model, msm, and asio

発表内容

05/03/2023 2

• Pub/Sub モデルとは?• コネクションとスレッド• 2 つのスケーラビリティ• broker の状態管理とイベントの遅延処理• msm の要求する排他制御• io_service の post と実行順序• async_write と strand

Page 3: Pub/Sub model, msm, and asio

自己紹介

05/03/2023 3

• 近藤 貴俊• ハンドルネーム redboltz• msgpack-c コミッタ– https://github.com/msgpack/msgpack-c

• MQTT の C++ クライアント mqtt_client_cpp 開発– https://github.com/redboltz/mqtt_client_cpp

• MQTT を拡張したスケーラブルなbroker を仕事で開発中

• CppCon 2016 参加予定

Page 4: Pub/Sub model, msm, and asio

Pub/Sub モデルとは

05/03/2023 4

topic A

publisher 1

subscriber 1topic B

publisher 2

subscriber 2

hello

world

論理的な概念

subscribe

publish

world

Page 5: Pub/Sub model, msm, and asio

client

Pub/Sub モデルとは

05/03/2023 5

broker

clientpublisher subscriber

topictopic

connection

物理的?な配置

node

Page 6: Pub/Sub model, msm, and asio

コネクションとスレッド

05/03/2023 6

broker

client

connection

workerthread

workerthread

workerthread

client client

context switch のコスト増大

Page 7: Pub/Sub model, msm, and asio

コネクションとスレッド

05/03/2023 7

broker

client

connection

boost::asio::io_service on 1 thread

client client

Page 8: Pub/Sub model, msm, and asio

io_service

05/03/2023 8

#include <iostream>#include <boost/asio.hpp>

int main() { boost::asio::io_service ios; ios.post([]{ std::cout << __LINE__ << std::endl; }); ios.post([]{ std::cout << __LINE__ << std::endl; }); ios.post([]{ std::cout << __LINE__ << std::endl; }); ios.post([]{ std::cout << __LINE__ << std::endl; }); ios.post([]{ std::cout << __LINE__ << std::endl; }); ios.run();}

http://melpon.org/wandbox/permlink/MzfsrLNdJjfAeV15

678910

1 2 3 4 5 6 7 8 9101112

様々な処理(ネットワーク、タイマ、シリアルポート、シグナルハンドル、 etc )を io_service に post 。

イベントが無くなるまで処理を実行

http://www.boost.org/doc/html/boost_asio/reference.html

Page 9: Pub/Sub model, msm, and asio

io_service

05/03/2023 9

#include <iostream>#include <boost/asio.hpp>

int main() { boost::asio::io_service ios; ios.post([&ios]{ std::cout << __LINE__ << std::endl; ios.post([&ios]{ std::cout << __LINE__ << std::endl; ios.post([&ios]{ std::cout << __LINE__ << std::endl; }); }); }); ios.run();}

http://melpon.org/wandbox/permlink/lXbFTVurVNUXM8BZ

7911

1 2 3 4 5 6 7 8 910111213141516

処理の中で次のリクエストを post

Page 10: Pub/Sub model, msm, and asio

2 つのスケーラビリティ

05/03/2023 10

• マルチスレッド• マルチノード(マルチサーバ)

Page 11: Pub/Sub model, msm, and asio

マルチスレッドにスケールアウト

05/03/2023 11

broker

client

connection

boost::asio::io_service on 1 thread

client client

コアを有効活用したい

Page 12: Pub/Sub model, msm, and asio

マルチスレッドにスケールアウト

05/03/2023 12

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

int main() { boost::asio::io_service ios; ios.post([]{ std::cout << __LINE__ << std::endl; }); ios.post([]{ std::cout << __LINE__ << std::endl; }); ios.post([]{ std::cout << __LINE__ << std::endl; }); ios.post([]{ std::cout << __LINE__ << std::endl; }); ios.post([]{ std::cout << __LINE__ << std::endl; }); std::vector<std::thread> ths; ths.emplace_back([&ios]{ ios.run(); }); ths.emplace_back([&ios]{ ios.run(); }); ths.emplace_back([&ios]{ ios.run(); }); ths.emplace_back([&ios]{ ios.run(); }); ths.emplace_back([&ios]{ ios.run(); }); for (auto& t : ths) t.join(); std::cout << "finished" << std::endl;}

http://melpon.org/wandbox/permlink/z5bQJYgO23tvM9XF

8910117finished

1 2 3 4 5 6 7 8 91011121314151617181920

実行順序は post の順序とは異なる

Page 13: Pub/Sub model, msm, and asio

マルチスレッドにスケールアウト

05/03/2023 13

broker

client

connection

client client

ios

client

thread threadthread

Page 14: Pub/Sub model, msm, and asio

subscriber

Pub/Sub モデルとロック

05/03/2023 14

subscriber

topic

publisher

subscribers_

subscribesubscribesubscribe

unsubscribe

排他ロック

publish対象の subscriber に配送

共有ロック

Page 15: Pub/Sub model, msm, and asio

webserver

マルチノードにスケールアウト

05/03/2023 15

client client client

load balancer

webserver webserver

毎回コネクションを切断する、 Web サーバなどはスケールアウトがシンプル

Page 16: Pub/Sub model, msm, and asio

broker

brokerbroker

マルチノードにスケールアウト

05/03/2023 16

client client client client

Pub/Sub モデルはコネクション型通信のため、Web サーバのようなリクエスト毎の切断を前提とするロードバランス戦略をとれない

情報の転送が必要

publisher subscriber

load balancer or dispatcher

hello

Page 17: Pub/Sub model, msm, and asio

broker

brokerbroker

マルチノードにスケールアウト

05/03/2023 17

client client client client

ルーティングなどの情報の同期が必要

publisher subscriber

同期中publish/Defer

同期済み

publish/ 配信処理

同期完了イベント処理の遅延

ステートマシンが常に必須とは限らないが、今回は必要であると仮定する。

Page 18: Pub/Sub model, msm, and asio

msm と asio の組み合わせ

05/03/2023 18

boost::asio::async_read( socket_, boost::asio::buffer(payload_), [this]( boost::system::error_code const& ec, std::size_t bytes_transferred){ // error checking ... // 受信時の処理 });

boost::shared_lock<mutex> guard(mtx_subscribers_);auto& idx = subscribers_.get<tag_topic>();auto r = idx.equal_range(topic);for (; r.first != r.second; ++r.first) { auto& socket = r.first->socket; boost::asio::write(socket, boost::asio::buffer(payload_));}

全ての subscriber に対してpublish 内容を配信

msm 導入前

Page 19: Pub/Sub model, msm, and asio

msm と asio の組み合わせ

05/03/2023 19

struct transition_table:mpl::vector< // Start Event Next Action Guard msmf::Row < s_normal, e_pub, msmf::none, a_pub, msmf::none >, msmf::Row < s_sync, e_pub, msmf::none, msmf::Defer, msmf::none >> {};

struct a_pub { template <typename Event, typename Fsm, typename Source, typename Target> void operator()(Event const& e, Fsm& f, Source&, Target&) const { boost::shared_lock<mutex> guard(f.mtx_subscribers_); auto& idx = f.subscribers_.get<tag_topic>(); auto r = idx.equal_range(e.topic); for (; r.first != r.second; ++r.first) { auto& socket = r.first->socket; boost::asio::write(socket, boost::asio::buffer(e.payload)); } }};

// boost::asio::async_read ハンドラ内にてprocess_event(e_pub(topic, payload));

msm 導入後

受信時の処理はアクションに移動

イベントの遅延が可能

イベントを処理すると

現在状態に応じた

アクションが実行される

Page 20: Pub/Sub model, msm, and asio

msm とスレッド

05/03/2023 20

process_event() の呼び出しは serialize されなければならない

Page 21: Pub/Sub model, msm, and asio

msm とスレッド

05/03/2023 21

同期中publish/Defer

同期済み

publish/ 配信処理

同期完了

process_event() の呼び出しは serialize されなければならない

複数のスレッドで同時に状態遷移が起こると、msm の内部状態がおかしくなるのであろう

// boost::asio::async_read ハンドラ内にてprocess_event(e_pub(topic, payload)); ここに排他ロックが必要となる

subscribersubscriberpublish 受信 subscriber

subscribersubscriberpublish 受信 subscriber

配信

配信別々の受信でも順番に処理せねばならない

Page 22: Pub/Sub model, msm, and asio

msm とスレッド

05/03/2023 22

排他ロック

共有ロック

排他ロック

共有ロック

Page 23: Pub/Sub model, msm, and asio

msm と asio の組み合わせ

05/03/2023 23

struct a_pub { template <typename Event, typename Fsm, typename Source, typename Target> void operator()(Event const& e, Fsm& f, Source&, Target&) const { ios.post([&f, e]{ boost::shared_lock<mutex> guard(f.mtx_subscribers_); auto& idx = f.subscribers_.get<tag_topic>(); auto r = idx.equal_range(e.topic); for (; r.first != r.second; ++r.first) { auto& socket = r.first->socket; boost::asio::write(socket, boost::asio::buffer(e.payload)); } }); }};

排他ロックの必要な範囲では、 ios.post() のみ行い、ios.post() に渡した処理が呼び出されるところで、共有ロックを行う

subscribersubscriberpublish 受信 subscriber

subscribersubscriberpublish 受信 subscriber

post

postpost のみ serialize 並行処理が可能

Page 24: Pub/Sub model, msm, and asio

msm と asio の組み合わせ

05/03/2023 24

struct a_pub { template <typename Event, typename Fsm, typename Source, typename Target> void operator()(Event const& e, Fsm& f, Source&, Target&) const { ios.post([&f, e]{ boost::shared_lock<mutex> guard(f.mtx_subscribers_); auto& idx = f.subscribers_.get<tag_topic>(); auto r = idx.equal_range(e.topic); for (; r.first != r.second; ++r.first) { auto& socket = r.first->socket; boost::asio::write(socket, boost::asio::buffer(e.payload)); } }); }};

排他ロックの必要な範囲では、 ios.post() のみ行い、ios.post() に渡した処理が呼び出されるところで、共有ロックを行う

注意点・処理の遅延に問題は無いか?・ ios.post() に渡した処理が参照するオブジェクトは生存しているか?

Page 25: Pub/Sub model, msm, and asio

for ループの処理も post すれば。。。

05/03/2023 25

struct a_pub { template <typename Event, typename Fsm, typename Source, typename Target> void operator()(Event const& e, Fsm& f, Source&, Target&) const { ios.post([&f, e]{ boost::shared_lock<mutex> guard(f.mtx_subscribers_); auto& idx = f.subscribers_.get<tag_topic>(); auto r = idx.equal_range(e.topic); for (; r.first != r.second; ++r.first) { auto& socket = r.first->socket; ios.post([&socket, e]{ boost::asio::write(socket, boost::asio::buffer(e.payload)); }); } }); }}; ループの中で行われる write() が並列化され、パフォーマンスの向上が見込め

Page 26: Pub/Sub model, msm, and asio

struct a_pub { template <typename Event, typename Fsm, typename Source, typename Target> void operator()(Event const& e, Fsm& f, Source&, Target&) const { ios.post([&f, e]{ boost::shared_lock<mutex> guard(f.mtx_subscribers_); auto& idx = f.subscribers_.get<tag_topic>(); auto r = idx.equal_range(e.topic); for (; r.first != r.second; ++r.first) { auto& socket = r.first->socket; ios.post([&socket, e]{ boost::asio::write(socket, boost::asio::buffer(e.payload)); }); } }); }};

for ループの処理も post すれば。。。

05/03/2023 26

publish 受信 subscriberpost

post のみ serialize

並行処理が可能post

subscriber

subscriber

publish 受信 subscriberpost

並行処理が可能post

subscriber

subscriber

並行処理が可能排他ロック 共有ロック

Page 27: Pub/Sub model, msm, and asio

broker

for ループの処理も post すれば。。。

05/03/2023 27

client

client

publisher

subscriber

1. subscribe

2. ack

3. publish(data)

4. data

1 と 3 がほぼ同時に発生した場合、 subscriber から見て許容される振る舞いは、

2, 4 の順で受信 ( 1 が 3 よりも先に broker で処理された場合)または

2 のみ受信 ( 1 が 3 よりも後に broker で処理された場合)

4, 2 の順で受信が発生してはならない。 ( ack の前に data 到着)

Page 28: Pub/Sub model, msm, and asio

broker

for ループの処理も post すれば。。。

05/03/2023 28

client

client

publisher

subscriber

1. unsubscribe

2. data

3. publish(data)

4. ack

1 と 3 がほぼ同時に発生した場合、 subscriber から見て許容される振る舞いは、

2, 4 の順で受信 ( 1 が 3 よりも先に broker で処理された場合)または

4 のみ受信 ( 1 が 3 よりも後に broker で処理された場合)

4, 2 の順で受信が発生してはならない。 ( ack の後に data 到着)

Page 29: Pub/Sub model, msm, and asio

for ループの処理も post すれば。。。

05/03/2023 29

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

int main() { boost::asio::io_service ios; ios.post([]{ std::cout << __LINE__ << std::endl; }); ios.post([]{ std::cout << __LINE__ << std::endl; }); ios.post([]{ std::cout << __LINE__ << std::endl; }); ios.post([]{ std::cout << __LINE__ << std::endl; }); ios.post([]{ std::cout << __LINE__ << std::endl; }); std::vector<std::thread> ths; ths.emplace_back([&ios]{ ios.run(); }); ths.emplace_back([&ios]{ ios.run(); }); ths.emplace_back([&ios]{ ios.run(); }); ths.emplace_back([&ios]{ ios.run(); }); ths.emplace_back([&ios]{ ios.run(); }); for (auto& t : ths) t.join(); std::cout << "finished" << std::endl;}

http://melpon.org/wandbox/permlink/z5bQJYgO23tvM9XF

8910117finished

1 2 3 4 5 6 7 8 91011121314151617181920

実行順序は post の順序とは異なる

Page 30: Pub/Sub model, msm, and asio

for ループの処理も post すれば。。。

05/03/2023 30

struct a_pub { template <typename Event, typename Fsm, typename Source, typename Target> void operator()(Event const& e, Fsm& f, Source&, Target&) const { ios.post([&f, e]{ boost::shared_lock<mutex> guard(f.mtx_subscribers_); auto& idx = f.subscribers_.get<tag_topic>(); auto r = idx.equal_range(e.topic); for (; r.first != r.second; ++r.first) { auto& socket = r.first->socket; ios.post([&socket, e]{ boost::asio::write(socket, boost::asio::buffer(e.payload)); }); } }); }};

unsubscribe 処理を行い、 ack を返送した後に、この処理が実行されることがある

Page 31: Pub/Sub model, msm, and asio

問題はどこにあるのか?

05/03/2023 31

• 同一コネクションに対する送信の順序を保証したいが、• io_service::post() を使うことで、順序の保証ができなくなっている• しかし、ループ処理の並列化は行いたい

• コネクションとの対応付けを考慮した、処理の post が行えれば良い

Page 32: Pub/Sub model, msm, and asio

boost::asio::async_write

05/03/2023 32

現実的には、このハンドラ内で次の async_write を呼ぶことになる

Page 33: Pub/Sub model, msm, and asio

boost::asio::async_write

05/03/2023 33

template <typename F>void my_async_write( std::shared_ptr<std::string> const& buf, F const& func) { strand_.post( [this, buf, func] () { queue_.emplace_back(buf, func); if (queue_.size() > 1) return; my_async_write_imp(); } );}

まず enqueデータは、バッファと完了ハンドラ

未完了の async_write があるなら何もせず終了

async_write の呼び出し処理

制約無く、いつでも呼べる、 async_write を作るには、自前でキューイングなどの処理を実装する必要がある。

Page 34: Pub/Sub model, msm, and asio

boost::asio::async_write

05/03/2023 34

void my_async_write_imp() { auto& elem = queue_.front(); auto const& func = elem.handler(); as::async_write( socket_, as::buffer(elem.ptr(), elem.size()), strand_.wrap( [this, func] (boost::system::error_code const& ec, std::size_t bytes_transferred) { func(ec); queue_.pop_front(); if (!queue_.empty()) { my_async_write_imp(); } } ) );}

queue からデータを取り出して、async_write

まだ queue にデータがあれば、再び async_write

queue からデータを消去し

strand_.post() および strand_.wrap() を用いて、排他制御を行っている

queue_ だけ mutex でロックするのと何が違うのか?

Page 35: Pub/Sub model, msm, and asio

async_read も strand wrap する

05/03/2023 35

boost::asio::async_read( socket_, boost::asio::buffer(payload_), strand_.wrap( [this]( boost::system::error_code const& ec, std::size_t bytes_transferred){ // error checking ... // 受信時の処理 } ));

async_read も strand 経由で処理する

Page 36: Pub/Sub model, msm, and asio

strand は本当に必要か?

05/03/2023 36

strand しなくても、暗黙的に strand になるケース

Page 37: Pub/Sub model, msm, and asio

publish 処理

05/03/2023 37

struct a_pub { template <typename Event, typename Fsm, typename Source, typename Target> void operator()(Event const& e, Fsm& f, Source&, Target&) const { ios.post([&f, e]{ boost::shared_lock<mutex> guard(f.mtx_subscribers_); auto& idx = f.subscribers_.get<tag_topic>(); auto r = idx.equal_range(e.topic); for (; r.first != r.second; ++r.first) { auto& socket = r.first->socket; socket.my_async_write(boost::asio::buffer(e.payload), 完了ハンドラ ); } }); }};

自前の非同期 write を呼び出す

subscribe / unsubscribe の ack 送信処理も、同様に、自前の非同期 write を経由させることで、順序の入れ替わりを防ぎ、かつ、処理の並列化を実現することができる

Page 38: Pub/Sub model, msm, and asio

publish 処理

05/03/2023 38

struct a_pub { template <typename Event, typename Fsm, typename Source, typename Target> void operator()(Event const& e, Fsm& f, Source&, Target&) const { ios.post([&f, e]{ boost::shared_lock<mutex> guard(f.mtx_subscribers_); auto& idx = f.subscribers_.get<tag_topic>(); auto r = idx.equal_range(e.topic); for (; r.first != r.second; ++r.first) { auto& socket = r.first->socket; socket.my_async_write(boost::asio::buffer(e.payload), 完了ハンドラ ); } }); }};

自前の非同期 write を呼び出す

publish 受信 subscriberpost

post のみ serialize

並行処理が可能かつ同一接続に対してはシリアライズ

my_async_write

subscriber

subscriber

publish 受信 subscriberpost

subscriber

subscriber

並行処理が可能排他ロック 共有ロック

my_async_write

並行処理が可能かつ同一接続に対してはシリアライズ

Page 39: Pub/Sub model, msm, and asio

publish 処理

05/03/2023 39

struct a_pub { template <typename Event, typename Fsm, typename Source, typename Target> void operator()(Event const& e, Fsm& f, Source&, Target&) const { ios.post([&f, e]{ boost::shared_lock<mutex> guard(f.mtx_subscribers_); auto& idx = f.subscribers_.get<tag_topic>(); auto r = idx.equal_range(e.topic); for (; r.first != r.second; ++r.first) { auto& socket = r.first->socket; socket.my_async_write(boost::asio::buffer(e.payload), 完了ハンドラ ); } }); }};

自前の非同期 write を呼び出す

非同期 write は十分に軽量であるため、 for ループの所要時間は短かった。排他ロックの中で処理を行ってもパフォーマンスは落ちなかった。よってシンプルな実装を採用した。(グレーの部分のコードを削除した)

Page 40: Pub/Sub model, msm, and asio

publish 処理

05/03/2023 40

struct a_pub { template <typename Event, typename Fsm, typename Source, typename Target> void operator()(Event const& e, Fsm& f, Source&, Target&) const { ios.post([&f, e]{ boost::shared_lock<mutex> guard(f.mtx_subscribers_); auto& idx = f.subscribers_.get<tag_topic>(); auto r = idx.equal_range(e.topic); for (; r.first != r.second; ++r.first) { auto& socket = r.first->socket; socket.my_async_write(boost::asio::buffer(e.payload), 完了ハンドラ ); } }); }};

自前の非同期 write を呼び出す

publish 受信 subscriberpost

post のみ serialize

並行処理が可能かつ同一接続に対してはシリアライズ

my_async_write

subscriber

subscriber

publish 受信 subscriberpost

subscriber

subscriber

並行処理が可能排他ロック 共有ロック

my_async_write

並行処理が可能かつ同一接続に対してはシリアライズ

Page 41: Pub/Sub model, msm, and asio

publish 処理

05/03/2023 41

struct a_pub { template <typename Event, typename Fsm, typename Source, typename Target> void operator()(Event const& e, Fsm& f, Source&, Target&) const { ios.post([&f, e]{ boost::shared_lock<mutex> guard(f.mtx_subscribers_); auto& idx = f.subscribers_.get<tag_topic>(); auto r = idx.equal_range(e.topic); for (; r.first != r.second; ++r.first) { auto& socket = r.first->socket; socket.my_async_write(boost::asio::buffer(e.payload), 完了ハンドラ ); } }); }};

自前の非同期 write を呼び出す

publish 受信 subscriber

my_async_write のみ serialize

並行処理が可能かつ同一接続に対してはシリアライズ

my_async_write

subscriber

subscriber

publish 受信 subscriber

subscriber

subscriber

排他ロック

my_async_write

並行処理が可能かつ同一接続に対してはシリアライズ

Page 42: Pub/Sub model, msm, and asio

まとめ

05/03/2023 42

• io_service を複数スレッドで run() することで、コアを有効利用できる• msm の Defer はイベント処理を遅延できて便利• その一方、 msm の状態遷移は排他制御を要求する• post() を利用することで任意の処理を、遅延でき、ロックの最適化が可能となる• post() はコネクションを意識しないので、マルチスレッドの場合、実行順序が保証されない• 通信では同一コネクションに対して、順序を保証したいことがよくある• そんなときは、 async_write() が使える• 好きなタイミングで呼べる async_write() は自分で実装する必要がある• キューイング処理と async_write ハンドラに加え、

async_read() も合わせて strand する必要がある