【アーキテクチャ】
はじめに
ここではシステム特有の用語を含めたメカニズムや考え方を説明しています。
このシステムではノンブロッキングモードによる実装を前提としており、TCPやUDP通信を使った接続中の個別の挙動部分については、待ちを発生させないタイムシェアリングによる並行動作を可能にしているため、動的なプロセスのフォークやスレッドの生成を行わなくても実装できるようにしています。
こうする事でプロセスの絶対数を管理する事ができ、サーバー間通信を伴うマルチサーバーの構築も可能なので、サーバーリソースを極力抑えつつパフォーマンスを向上させ、スケーラビリティを考慮しながら高速に動作できるように設計しています。
プロセスの絶対数を管理する部分の考え方については、FastCGIを使ったAPサーバーのプロセス管理手法と一部似通っています。
このシステムではノンブロッキングモードによる実装を前提としており、TCPやUDP通信を使った接続中の個別の挙動部分については、待ちを発生させないタイムシェアリングによる並行動作を可能にしているため、動的なプロセスのフォークやスレッドの生成を行わなくても実装できるようにしています。
こうする事でプロセスの絶対数を管理する事ができ、サーバー間通信を伴うマルチサーバーの構築も可能なので、サーバーリソースを極力抑えつつパフォーマンスを向上させ、スケーラビリティを考慮しながら高速に動作できるように設計しています。
プロセスの絶対数を管理する部分の考え方については、FastCGIを使ったAPサーバーのプロセス管理手法と一部似通っています。
レイヤー概念図
サーバーのアプリケーションの部分はプロトコル部とコマンド部に大きく分かれています。
最初にそれらをライブラリ内の
プロトコル部とコマンド部に分けているのは、それぞれを自由に入れ替える事ができるようにするためです。
作成したクライアントのプロトコルに合わせてプロトコル部を入れ替えたり、サーバーサイドのコンテンツを切り替えるためにコマンド部を入れ替えたりする事で様々な組み合わせのサーバーを構築する事が可能です。
最初にそれらをライブラリ内の
CycleDrivenManager
のプロトコル部とコマンド部へ取り込ませてから使う事になります。プロトコル部とコマンド部に分けているのは、それぞれを自由に入れ替える事ができるようにするためです。
作成したクライアントのプロトコルに合わせてプロトコル部を入れ替えたり、サーバーサイドのコンテンツを切り替えるためにコマンド部を入れ替えたりする事で様々な組み合わせのサーバーを構築する事が可能です。
キューとUNITの関係
まずは
図にすると以下のようなイメージになります。 通信データの送受信の特徴として送りたいデータ、あるいは欲しいデータが一度に全て送信/受信できるとは限らないためこのような構成になっています。
また、WAITが発生するような処理(送信/受信をセットにしている動作など)をUNITで分ける事で、キューの処理を待たせる事なく次のUNIT処理へと進む事ができるようになります。
例えばWebsocketのopeningハンドシェイクの場合はクライアントからヘッダ情報を受信した後は同じようにヘッダ情報をサーバーから送り返さないといけませんが、これをブロッキングモードで送信が完了するまで待っていると他の接続も巻き込んで処理が待たされる事になってしまいます。
この回避策として当ライブラリでは以下のUNIT処理のようにノンブロッキングモードで送信が行えるようにしています。
UNIT処理の一部を抜粋
実際にデータを送信する
※
※
受信の場合も同じように受信サイズを設定するメソッド
このようにUNITの処理の実装は極力処理の停滞(ブロッキング)がおきないように構成する事が求められます。
また、UNITステータスはリターン値によって遷移していきます。そしてキューの処理が全て完了したらnullを返すルールとなっています。
処理の流れはリターン値で制御できるので、同じような処理を繰り返し行ったり、分岐したいケースがある場合には処理を構造化しながらコントロールする事が可能になります。
このようなキューとステータスUNITをコントロールする役割を
ステータスUNIT内で
CycleDrivenManager
の動きを理解するためにキューとステータスUNITの関係を知る必要があります。図にすると以下のようなイメージになります。 通信データの送受信の特徴として送りたいデータ、あるいは欲しいデータが一度に全て送信/受信できるとは限らないためこのような構成になっています。
また、WAITが発生するような処理(送信/受信をセットにしている動作など)をUNITで分ける事で、キューの処理を待たせる事なく次のUNIT処理へと進む事ができるようになります。
例えばWebsocketのopeningハンドシェイクの場合はクライアントからヘッダ情報を受信した後は同じようにヘッダ情報をサーバーから送り返さないといけませんが、これをブロッキングモードで送信が完了するまで待っていると他の接続も巻き込んで処理が待たされる事になってしまいます。
この回避策として当ライブラリでは以下のUNIT処理のようにノンブロッキングモードで送信が行えるようにしています。
UNIT処理の一部を抜粋
// CREATEステータスのUNIT { // 送信データの設定 $p_param->protocol()->setSendingData(<送信データ>); return ProtocolStatusEnumForWebsocket::SEND->value; } // SENDステータスのUNIT { // データ送信 $w_ret = $p_param->protocol()->sending(); // 送信中の場合は再実行 if($w_ret === null) { $sta = $p_param->getStatusName(); return $sta; } return null; }
実際にデータを送信する
SEND
ステータスのひとつ前のUNIT処理(CREATE
ステータス)であらかじめ送信したいデータを設定しておき、送信を実行するUNIT処理(SEND
ステータス)でsending
メソッドの戻り値がnull以外になるまで繰り返しています。※
sending
メソッドは設定された送信データを送信しきるまでnullを返す処理です。※
getStatusName
メソッドは現在実行中のステータス名を取得するものです。受信の場合も同じように受信サイズを設定するメソッド
setReceivingSize
と受信しきるまでnullを返すメソッドreceiving
に分けています。このようにUNITの処理の実装は極力処理の停滞(ブロッキング)がおきないように構成する事が求められます。
また、UNITステータスはリターン値によって遷移していきます。そしてキューの処理が全て完了したらnullを返すルールとなっています。
処理の流れはリターン値で制御できるので、同じような処理を繰り返し行ったり、分岐したいケースがある場合には処理を構造化しながらコントロールする事が可能になります。
このようなキューとステータスUNITをコントロールする役割を
CycleDrivenManager
が担っています。throwブレイク
このシステムでのthrowブレイクは、UNITのステータスを維持したまま処理を中断して待ち受け状態に戻る事を意味します。ステータスUNIT内で
UnitException
クラスを用いて例外(throw)を発行する事でthrowブレイクとなります。
UNIT定義クラス
UNITの処理はプロトコル/コマンド部を問わず
プロトコル部を作成する時は
生成されるファイルの種類は以下の通り。
プロトコル部のキュー名は
コマンド部のキュー名の定義は自由です。UNIT定義クラスの実装に合わせてキュー名を追加していく事になります。
IEntryUnits
インタフェースに従って実装する必要があります。プロトコル部を作成する時は
craft:protocol
コマンドを、コマンド部を作成する時はcraft:command
コマンドを実行してファイルを生成します。生成されるファイルの種類は以下の通り。
- UNIT定義クラス
- キュー名定義のEnum
- ステータス名定義のEnum
プロトコル部のキュー名は
ProtocolQueueEnum
で予約されているので、作成されたEnumファイルにはこの予約されたEnum値がエイリアス名として定義されます。コマンド部のキュー名の定義は自由です。UNIT定義クラスの実装に合わせてキュー名を追加していく事になります。
- ■getQueueListメソッドの実装
- プロトコル部、コマンド部それぞれで定義されたキュー名のEnum値をピックアップしてリストに追加します。
- ■getUnitListメソッドの実装
- 引数で渡されたキュー名と一致するリストを返します。プロトコル部、コマンド部それぞれで定義されたステータス名定義のEnum値をピックアップしてステータス名に対応するUNITメソッドとセットでリストを作成します。各キューの処理は
START
ステータスから始まるので必ず含める必要があります。
初期化クラス
初期化クラスは
インタフェース実装に必要な内容は以下の通りです。
このインスタンスはUNIT処理の引数として渡されるもので、
プロトコル部・コマンド部・コマンドディスパッチャー間で同じグローバルエリアの利用が可能です。
接続子単位でのグローバル管理が必要なデータに関しては、UNITパラメータクラス内の
これを図にすると以下のようになります。
初期化クラス内の
プロトコル部ではUNITパラメータクラス内でインプリメントされた
これに対してコマンド部ではUNITパラメータクラス内でインプリメントされた
このような構成になっているのは以下の処理を前提としているからです。
※protocolメソッドをコマンド部で呼び出すと
※具体的にどんなメソッドが存在するのかについては
送受信スタックエリア内ではシリアライズ化されたデータが保持されるため、データ取得時にはアンシリアライザーが、データ設定時にはシリアライザーが呼び出されます。
これを図にすると以下のようになります。
コマンドディスパッチャーはその受信データを解析してコマンド部に処理を振り分ける役割を担っています。
コマンド部から送信するデータはUNITパラメータクラスの
送信時はコマンドディスパッチャーの介入はなく、送信スタックに溜まったものをプロトコル部を経由して順次クライアントへ送信されます。
これを図にすると以下のようになります。
アプリケーションレイヤーからはUNITパラメータクラスを通して呼ばれます。
IInitSocketManager
インタフェースに従って実装する必要があります。インタフェース実装に必要な内容は以下の通りです。
UNITパラメータクラス
SocketManagerParameter
クラスやそれを継承しているクラスの事をUNITパラメータクラスと呼びます。このインスタンスはUNIT処理の引数として渡されるもので、
SocketManager
との橋渡し役を担っています。SocketManagerParameter
クラスを継承する事でデータの送受信やディスクリプタ(クライアント接続子)の操作を行うための機能を提供すると同時に、アプリケーションで利用するグローバルエリアを管理する役割も担っています。プロトコル部・コマンド部・コマンドディスパッチャー間で同じグローバルエリアの利用が可能です。
接続子単位でのグローバル管理が必要なデータに関しては、UNITパラメータクラス内の
getTempBuff/setTempBuff
メソッドで取得・設定が可能です。これを図にすると以下のようになります。
初期化クラス内の
getUnitParameterメソッド
でインスタンスを返す必要がありますが、インスタンス化するタイミングは初期化クラスの内側/外側を問いません。プロトコル部ではUNITパラメータクラス内でインプリメントされた
IProtocolParameter
インタフェースを返すprotocol
メソッドを介して送受信用のメソッドを使う必要があります。これに対してコマンド部ではUNITパラメータクラス内でインプリメントされた
getRecvData
メソッドを使って受信データを取得し、setSendStack
メソッドを使って送信データを設定する流れが基本となります。このような構成になっているのは以下の処理を前提としているからです。
- ・受信処理
- 受信データはプロトコル部で組み立てたものをコマンド部に渡します(コマンド部では
getRecvData
メソッドを通して受信データを取得します)。 - ・送信処理
- コマンド部で組み立てた送信データはプロトコル部で必要に応じて分解され、送信されます(コマンド部では
setSendStack
メソッドを通して送信データを引き渡します)。
※protocolメソッドをコマンド部で呼び出すと
UnitExceptionEnum::ECODE_METHOD_CALL_FAIL
例外が発生してエラー終了しますのでご注意ください。※具体的にどんなメソッドが存在するのかについては
SocketManagerParameter
クラスの>> Referenceをご覧ください。シリアライザー/アンシリアライザー
この処理はディスクリプタ内で管理された先入れ先出し方式である送受信スタックエリア(メモリ上のスタック領域とは無関係です)と深く関わっています。送受信スタックエリア内ではシリアライズ化されたデータが保持されるため、データ取得時にはアンシリアライザーが、データ設定時にはシリアライザーが呼び出されます。
これを図にすると以下のようになります。
コマンドディスパッチャー
プロトコル部で受信したデータはUNITパラメータクラスのsetRecvStack
メソッドを使って受信スタックへ格納する必要があります。コマンドディスパッチャーはその受信データを解析してコマンド部に処理を振り分ける役割を担っています。
コマンド部から送信するデータはUNITパラメータクラスの
setSendStack
メソッドを使って送信スタックへ格納する必要があります。送信時はコマンドディスパッチャーの介入はなく、送信スタックに溜まったものをプロトコル部を経由して順次クライアントへ送信されます。
これを図にすると以下のようになります。
緊急停止時のコールバック
以下の場面で呼び出されます。- アライブチェック処理のタイムアウト
- コマンドディスパッチャーで例外キャッチ時
- 相手先による強制切断
ログライター
ライブラリ内を含めアプリケーションレイヤー内からも呼ばれるログ出力ハンドラーです。アプリケーションレイヤーからはUNITパラメータクラスを通して呼ばれます。
メイン処理クラス
実装イメージは以下の通りです。
このうち初期設定ブロックは
ポート設定ブロックではマルチサーバーの子サーバーとして起動する場合、親サーバーへの接続を行うための
ノンブロッキングループブロックは以下のイメージで動作します。
1つの接続に対してプロトコル部とコマンド部が連携し合って動作します。
このうち1回の周期(ループ)で実行されるのはプロトコル/コマンド部のそれぞれ1UNITずつとなります。
これが複数の接続になると以下のイメージになり、1つのサーバーで処理する単位になります。
そして複数のサーバーを起動している場合は以下のイメージになり、プロセスが順当に割り当てられると1つのサーバープロセスがCPUの各コア(このケースではコアが4つの場合)に割り当てられてデュアルで動作します。
※実際にサーバーをスケーリングする際には上記のイメージを基に計算/設計していく事になりますが、サーバー上には多数のプロセスが存在するため必ずしも
このうち初期設定ブロックは
SocketManager
の準備処理として必要な部分です。ポート設定ブロックではマルチサーバーの子サーバーとして起動する場合、親サーバーへの接続を行うための
connect
メソッドが使用される事があります。ノンブロッキングループブロックは以下のイメージで動作します。
1つの接続に対してプロトコル部とコマンド部が連携し合って動作します。
このうち1回の周期(ループ)で実行されるのはプロトコル/コマンド部のそれぞれ1UNITずつとなります。
これが複数の接続になると以下のイメージになり、1つのサーバーで処理する単位になります。
そして複数のサーバーを起動している場合は以下のイメージになり、プロセスが順当に割り当てられると1つのサーバープロセスがCPUの各コア(このケースではコアが4つの場合)に割り当てられてデュアルで動作します。
※実際にサーバーをスケーリングする際には上記のイメージを基に計算/設計していく事になりますが、サーバー上には多数のプロセスが存在するため必ずしも
SocketManager
のプロセスが4つとも常に割り当てられるとは限らないので、リソースや処理時間等の実測値を見ながら調整していく事になります。おわりに
ここでご紹介した内容は特に新規プロジェクト開発をする時に必要になる情報ばかりですが、Websocket開発環境でもより高度な実装をするために必要な情報でもあります。
サーバー間通信を伴うマルチサーバーを構築する際の基礎的な内容でもありますので▶マルチサーバーの構成のページも合わせてご覧ください(マルチサーバーの構築を予定していない場合は読み飛ばしてもらっても構いません)。
サーバー間通信を伴うマルチサーバーを構築する際の基礎的な内容でもありますので▶マルチサーバーの構成のページも合わせてご覧ください(マルチサーバーの構築を予定していない場合は読み飛ばしてもらっても構いません)。