はじめに
ここで述べているマルチサーバーというのは、親子関係がある複数のサーバー同士でサーバー(プロセス)間通信(IPC)を通して連携し合っているサーバーの事を指しています。
このページでは クライアント⇔サーバー
間と サーバー⇔サーバー
間で並行処理を行いながらプロトコル部とコマンド(ビジネスロジック)部を分割管理する手段を提供します。
以降では以下のデモのソースを例に挙げて話を進めます。
サーバー構成
マルチサーバーの構成には色々なパターンが考えられると思いますが、大きく分けると以下の2パターンになるかと思います。
- 一つのホスト上で複数のプロセスを配置する(スケールアップ)
- 二つ以上のホストで複数のプロセスを分散配置する(スケールアウト)
プロセス間通信だけの利用でソケット通信を使っている場合は、基本的にホスト名とポート番号を使って振り分けができていれば特に問題はありませんが、一つのサーバー上で クライアント⇔サーバー
間と サーバー⇔サーバー
間でやり取りを行う場合、プロトコル部とコマンド部を切り分けて使う事に課題がありました。
そこでデモ用として使っている今回のマルチサーバーには、一つのサーバープロセスの中で待ち受けポートを Websocket 用とマルチサーバー用の2つに分けて実装( SocketManager モジュールを2つ使用)する事で切り分けができるようにしています。
これを図にすると以下のようになります。
プロセスをフォークしているわけではないので"親プロセス/子プロセス"ではなく敢えて"親サーバー/子サーバー"と表記しています。
上の図では、SOCKET-MANAGER Framework のエントリポイントをクライアント通信側とサーバー間通信側に分けた上で、UNITパラメータクラスによるグローバル共有(コンテキスト)を介して通信データを共有できるように構成しています。
また、SocketManager
のエントリポイントが分かれる事によってサーバー間通信側と分離した管理が可能になるため、以下のメリットが生まれます。
- クライアント通信側と同じインターフェースでサーバー間通信の開発が可能
- INETソケット(オリジナルプロトコルを含む)を使ったサーバー間通信が可能
- 物理サーバーが分かれていても同じインターフェース(INETソケット通信)が利用できるのでシステム変更が不要
- 別途モジュールの追加実装を必要としない
結果、単一ソリューションによるアーキテクチャレベルでの実装が容易になり、外部ライブラリや外部メディア(あるいはサーバー)などの管理作業が省略できる事で中長期的な運用の手間が軽減されます。
仮に1つのサーバープロセス上のエントリポイント分割にフォーカスを当てたイメージが以下のようになります。
(UNITパラメータクラスでグローバル共有しています)
UNITパラメータクラスのグローバル共有の部分にフォーカスを当てると以下のようなイメージになります。
そして実際にはノンブロッキングループで並走する事になるので以下のようなイメージになります。
ソースで確認
ここでは上記で説明したサーバー構成を ChatServerForTcpMulti.php
のソースを追いながらみていきます。
以下の実装では Websocket 用のUNITパラメータクラス ParameterForWebsocket
とマルチサーバー用のUNITパラメータクラス ParameterForTcpMulti
のインスタンスを生成し、それぞれの設定メソッドを使って互いのインスタンスを交換設定しています。
// UNITパラメータのインスタンス化
$websocket_param = new ParameterForWebsocket(); // Websocket用
$tcpmulti_param = new ParameterForTcpMulti(); // サーバー間通信(TCP)用
// UNITパラメータの交換設定
$websocket_param->setChatParameterForServer($tcpmulti_param); // サーバー間通信(TCP)用インスタンスをWebsocket用インスタンスへ
$tcpmulti_param->setChatParameterForWebsocket($websocket_param); // Websocket用インスタンスをサーバー間通信(TCP)用インスタンスへ
以下の Websocket 用の実装ではノーマルな初期設定ブロックになっています。
// SocketManagerのインスタンス設定
$websocket_manager = new SocketManager($this->host, $this->port);
// SocketManagerの初期設定
$init = new InitForWebsocket($websocket_param, $this->port);
$websocket_manager->setInitSocketManager($init);
// プロトコルUNITの設定
$protocol_units = new ProtocolForWebsocket();
$websocket_manager->setProtocolUnits($protocol_units);
// コマンドUNITの設定
$command_units = new CommandForWebsocket();
$websocket_manager->setCommandUnits($command_units);
以下のマルチサーバー用の実装が Websocket 用と異なるのは、冒頭の SocketManager
のインスタンス生成時に条件判断が入っているところです。
マルチサーバーには親子関係がありますから、自身が親サーバーなのか、あるいは子サーバーなのかを判断する必要があります。
また、同じTCP通信同士では Websocket 用とマルチサーバー用で同じポートが使えないので異なるポート番号を使用する必要があります。
(マルチサーバー用のポートをUDP通信で使う場合は Websocket のTCP通信とはリソースが異なるので同じポート番号でも問題ありません)
そこでこのデモでは Websocket 用で使うポート番号+10の値を親サーバー用のポート番号として使うというルールにしています。
つまりコマンドライン引数の指定で Websocket 用のポート番号が 10000、親サーバーのポート番号 10010 で指定されていれば、自身は親サーバーだと判断するようにしています。
(UDP通信の場合は Websocket 用のポート番号と親サーバー用のポート番号の指定が同じであれば親だと判断しています)
自身が子サーバーだと判断した場合、待ち受けホストもポート番号も共に設定する必要がないので SocketManager
の引数を空にしています。
// SocketManagerのインスタンス設定
$tcpmulti_manager = null;
if($this->parent === true)
{
$tcpmulti_manager = new SocketManager($this->host, $this->parent_port);
}
else
{
$tcpmulti_manager = new SocketManager();
}
// SocketManagerの初期設定
$init = new InitForTcpMulti($tcpmulti_param, $this->port, $this->parent, $this->parent_port);
$tcpmulti_manager->setInitSocketManager($init);
// プロトコルUNITの設定
$protocol_units = new ProtocolForTcpMulti();
$tcpmulti_manager->setProtocolUnits($protocol_units);
// コマンドUNITの設定
$command_units = new CommandForTcpMulti();
$tcpmulti_manager->setCommandUnits($command_units);
Websocket 用の方は Listen するだけでいいのですが、マルチサーバーの場合、親の時は Listen、子の時は Connect にする必要があるので以下のように条件分岐しています。
$ret = $websocket_manager->listen();
if($ret === false)
{
goto finish; // リッスン失敗
}
if($this->parent === true)
{
$ret = $tcpmulti_manager->listen();
if($ret === false)
{
goto finish; // リッスン失敗
}
}
else
{
$w_ret = $tcpmulti_manager->connect($this->host, $this->parent_port);
if($w_ret === false)
{
goto finish; // 接続失敗
}
}
以下のソースでは Websocket 用の SocketManager
とマルチサーバー用の SocketManager
共に cycleDriven
メソッドを呼んで周期ドリブンの処理で並走しています。
while(true)
{
// 周期ドリブン
$ret = $websocket_manager->cycleDriven($this->cycle_interval, $this->alive_interval);
if($ret === false)
{
goto finish;
}
$ret = $tcpmulti_manager->cycleDriven($this->cycle_interval);
if($ret === false)
{
goto finish;
}
}
おわりに
サーバーをまたがってユーザーを検索するようなケースではデータベースで串刺し検索を行った方が早いように思えますが、トランザクションロックを起こす可能性がある限り同時接続している他のユーザーに影響が出る可能性を考慮しないといけません。
データの永続化が必要な場合は、起動時に必要な情報を読み込んでおくようにしたり、今回のようなマルチサーバーでデータベースアクセス専用のサーバーを起ててリクエストを投げるだけにするなど、工夫次第でパフォーマンスへの影響を軽減できる方法はあると思います。
その上でデータベース上のデータ共有が必要であればサーバー間通信で賄えばいいでしょう。
デモ環境のマルチサーバーの起動方法については以下のページをご覧ください。