pub/sub model, msm, and asio
Post on 14-Apr-2017
621 Views
Preview:
TRANSCRIPT
Pub/Sub モデルと msm と asioとTakatoshi Kondo
05/03/2023 1
発表内容
05/03/2023 2
• Pub/Sub モデルとは?• コネクションとスレッド• 2 つのスケーラビリティ• broker の状態管理とイベントの遅延処理• msm の要求する排他制御• io_service の post と実行順序• async_write と strand
自己紹介
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 参加予定
Pub/Sub モデルとは
05/03/2023 4
topic A
publisher 1
subscriber 1topic B
publisher 2
subscriber 2
hello
world
論理的な概念
subscribe
publish
world
client
Pub/Sub モデルとは
05/03/2023 5
broker
clientpublisher subscriber
topictopic
connection
物理的?な配置
node
コネクションとスレッド
05/03/2023 6
broker
client
connection
workerthread
workerthread
workerthread
client client
context switch のコスト増大
コネクションとスレッド
05/03/2023 7
broker
client
connection
boost::asio::io_service on 1 thread
client client
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
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
2 つのスケーラビリティ
05/03/2023 10
• マルチスレッド• マルチノード(マルチサーバ)
マルチスレッドにスケールアウト
05/03/2023 11
broker
client
connection
boost::asio::io_service on 1 thread
client client
コアを有効活用したい
マルチスレッドにスケールアウト
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 の順序とは異なる
マルチスレッドにスケールアウト
05/03/2023 13
broker
client
connection
client client
ios
client
thread threadthread
subscriber
Pub/Sub モデルとロック
05/03/2023 14
subscriber
topic
publisher
subscribers_
subscribesubscribesubscribe
unsubscribe
排他ロック
publish対象の subscriber に配送
共有ロック
webserver
マルチノードにスケールアウト
05/03/2023 15
client client client
load balancer
webserver webserver
毎回コネクションを切断する、 Web サーバなどはスケールアウトがシンプル
broker
brokerbroker
マルチノードにスケールアウト
05/03/2023 16
client client client client
Pub/Sub モデルはコネクション型通信のため、Web サーバのようなリクエスト毎の切断を前提とするロードバランス戦略をとれない
情報の転送が必要
publisher subscriber
load balancer or dispatcher
hello
broker
brokerbroker
マルチノードにスケールアウト
05/03/2023 17
client client client client
ルーティングなどの情報の同期が必要
publisher subscriber
同期中publish/Defer
同期済み
publish/ 配信処理
同期完了イベント処理の遅延
ステートマシンが常に必須とは限らないが、今回は必要であると仮定する。
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 導入前
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 導入後
受信時の処理はアクションに移動
イベントの遅延が可能
イベントを処理すると
現在状態に応じた
アクションが実行される
msm とスレッド
05/03/2023 20
process_event() の呼び出しは serialize されなければならない
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
配信
配信別々の受信でも順番に処理せねばならない
msm とスレッド
05/03/2023 22
排他ロック
共有ロック
排他ロック
共有ロック
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 並行処理が可能
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() に渡した処理が参照するオブジェクトは生存しているか?
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() が並列化され、パフォーマンスの向上が見込め
る
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
並行処理が可能排他ロック 共有ロック
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 到着)
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 到着)
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 の順序とは異なる
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 を返送した後に、この処理が実行されることがある
問題はどこにあるのか?
05/03/2023 31
• 同一コネクションに対する送信の順序を保証したいが、• io_service::post() を使うことで、順序の保証ができなくなっている• しかし、ループ処理の並列化は行いたい
• コネクションとの対応付けを考慮した、処理の post が行えれば良い
boost::asio::async_write
05/03/2023 32
現実的には、このハンドラ内で次の async_write を呼ぶことになる
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 を作るには、自前でキューイングなどの処理を実装する必要がある。
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 でロックするのと何が違うのか?
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 経由で処理する
strand は本当に必要か?
05/03/2023 36
strand しなくても、暗黙的に strand になるケース
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 を経由させることで、順序の入れ替わりを防ぎ、かつ、処理の並列化を実現することができる
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
並行処理が可能かつ同一接続に対してはシリアライズ
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 ループの所要時間は短かった。排他ロックの中で処理を行ってもパフォーマンスは落ちなかった。よってシンプルな実装を採用した。(グレーの部分のコードを削除した)
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
並行処理が可能かつ同一接続に対してはシリアライズ
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
並行処理が可能かつ同一接続に対してはシリアライズ
まとめ
05/03/2023 42
• io_service を複数スレッドで run() することで、コアを有効利用できる• msm の Defer はイベント処理を遅延できて便利• その一方、 msm の状態遷移は排他制御を要求する• post() を利用することで任意の処理を、遅延でき、ロックの最適化が可能となる• post() はコネクションを意識しないので、マルチスレッドの場合、実行順序が保証されない• 通信では同一コネクションに対して、順序を保証したいことがよくある• そんなときは、 async_write() が使える• 好きなタイミングで呼べる async_write() は自分で実装する必要がある• キューイング処理と async_write ハンドラに加え、
async_read() も合わせて strand する必要がある
top related