リアクターソフトウェア設計パターンは、多数の潜在的なサービス要求に同時に対応できるイベント処理戦略です。このパターンの主要コンポーネントは、単一のスレッドまたはプロセスで実行されるイベントループであり、受信した要求を分離して適切な要求ハンドラにディスパッチします。[ 1 ]
ブロッキング I/Oやマルチスレッドではなくイベントベースのメカニズムに依存することで、リアクターは最小限の遅延で多数の同時I/O バウンド要求を処理できます。 [ 2 ] また、リアクターは特定の要求ハンドラールーチンを簡単に変更または拡張することもできますが、このパターンにはいくつかの欠点と制限があります。[ 1 ]
シンプルさと拡張性のバランスが取れたリアクターは、いくつかのサーバーアプリケーションやネットワーク用ソフトウェアフレームワークにおいて中心的なアーキテクチャ要素となっています。マルチリアクターやプロアクターなどの派生型は、より高いスループット、パフォーマンス、または要求の複雑さが求められる特殊なケースにも存在します。[ 1 ] [ 2 ] [ 3 ] [ 4 ]
大規模ネットワークにおけるクライアント・サーバーモデルの実際的な考慮事項、例えばウェブサーバーのC10k問題などが、リアクターパターンの本来の動機であった。[ 5 ]
ネットワークソケットやファイルディスクリプタなど、多数の潜在的なエンドポイントからのサービス要求を処理する単純なアプローチは、イベントループ内で新しい要求をリッスンし、最も早い要求をすぐに読み込むことです。要求全体が読み込まれたら、適切なハンドラを直接呼び出すことで処理して転送できます。このように、イベントループの反復ごとに1つの要求を最初から最後まで処理する、完全に「反復的」なサーバーは論理的には有効です。しかし、複数の要求が連続して受信されると、処理が遅れてしまいます。反復的なアプローチは、要求の読み取りによってサーバーの唯一のスレッドが完全な要求を受信するまでブロックされ、I/O操作は通常、他の計算よりもはるかに遅いため、スケーラブルではありません。[ 2 ]
この制限を克服する戦略の1つはマルチスレッドです。新しいリクエストをそれぞれ独自のワーカー スレッドにすぐに分割することで、最初のリクエストがイベント ループをブロックしなくなり、イベント ループはすぐに反復して別のリクエストを処理できるようになります。この「接続ごとにスレッド」設計は、純粋な反復設計よりもスケーラブルですが、依然として複数の非効率性があり、ある時点を超えると問題が発生します。基盤となるシステム リソースの観点から見ると、新しいスレッドまたはプロセスごとに、メモリと処理時間 (コンテキスト スイッチングのため) にオーバーヘッドコストが発生します。各スレッドが I/O の完了を待つという根本的な非効率性も解決されません。[ 1 ] [ 2 ]
設計上の観点から見ると、どちらのアプローチも汎用デマルチプレクサと特定の要求ハンドラを密接に結合させているため、サーバーコードが脆弱になり、変更が困難になります。これらの点を考慮すると、いくつかの重要な設計上の決定事項が考えられます。
これらの知見を組み合わせると、シングルスレッドの利点と高スループットおよびスケーラビリティのバランスが取れたリアクターパターンが生まれる。[ 1 ] [ 2 ]
リアクターパターンは、並行処理やイベント処理に関するあらゆる問題に対する優れた出発点となり得ます。このパターンはネットワークソケットに限定されるものではなく、ハードウェアI/O、ファイルシステムやデータベースへのアクセス、プロセス間通信、さらには抽象的なメッセージパッシングシステムなどにも適用可能です。
しかし、リアクターパターンには制限があり、その主な制限はコールバックの使用です。コールバックはプログラムの解析とデバッグを難しくし、これは反転制御を持つ設計に共通する問題です。[ 1 ]よりシンプルな接続ごとのスレッドと完全な反復アプローチはこれを回避し、スケーラビリティや高スループットが要求されない場合には有効なソリューションとなり得ます。[ a ]
シングルスレッドは、最大限のスループットが求められるユースケースや、要求に大量の処理が含まれる場合には欠点となる可能性があります。さまざまなマルチスレッド設計によってこれらの制限を克服でき、実際、イベントやI/Oを処理するためのサブコンポーネントとしてリアクターパターンを使用しているものもあります。[ 1 ]
リアクターパターン(またはその派生形)は、多くのWebサーバー、アプリケーションサーバー、およびネットワークフレームワークで採用されている。
リアクティブアプリケーションは複数の可動部分で構成され、いくつかのサポートメカニズムに依存します。[ 1 ]
標準的な原子炉構成は多くの用途には十分だが、特に要求の厳しい用途では、改良を加えることで複雑さが増すものの、さらに高い出力を得ることができる。
基本的な変更点の1つは、並行性を高めるためにイベントハンドラを独自の別スレッドで呼び出すことです。必要に応じて新しいスレッドを起動するのではなく、スレッドプールでハンドラを実行することで、マルチスレッド処理がさらに簡素化され、オーバーヘッドが最小限に抑えられます。これにより、スレッドプールは多くのユースケースでリアクターパターンの自然な補完となります。[ 2 ]
スループットを最大化するもう一つの方法は、「接続ごとにスレッド」方式のサーバーを部分的に再導入し、複製されたディスパッチャ/イベントループを並行して実行することです。ただし、接続数ではなく、ディスパッチャの数を基盤となるハードウェアの利用可能なCPUコア数に合わせて設定します。
マルチリアクターと呼ばれるこの方式は、専用サーバーがハードウェアの処理能力を最大限に活用することを保証します。個別のスレッドは長時間実行されるイベントループであるため、スレッドの作成と破棄のオーバーヘッドはサーバーの起動とシャットダウンに限定されます。リクエストが独立したディスパッチャに分散されるため、マルチリアクターは可用性と堅牢性も向上します。エラーが発生して単一のディスパッチャが故障した場合でも、そのイベントループに割り当てられたリクエストのみが中断されます。[ 3 ] [ 4 ]
同期要求と非同期要求を組み合わせる必要がある特に複雑なサービスの場合、もう1つの選択肢はプロアクターパターンです。このパターンはリアクターよりも複雑で、独自のエンジニアリングの詳細がありますが、ブロッキングI/Oの問題を解決するためにリアクターサブコンポーネントを使用します。[ 3 ]
関連パターン:
具体的な用途:
実装例: