【マルチサーバーの構成】

はじめに

ここで述べているマルチサーバーというのは、親子関係がある複数のサーバー同士でサーバー(プロセス)間通信を通して連携し合っているサーバーの事を指しています。
このページではクライアント⇔サーバー間とサーバー⇔サーバー間で並行処理を行いながらプロトコルとコマンド処理を分割管理する手段を提供します。

以降ではデモのソースを例に挙げて話を進めます。

サーバー構成

マルチサーバーの構成には色々なパターンが考えられると思いますが、大きく分けると以下の2パターンになるかと思います。
  • 一つのホスト上で複数のプロセスを配置する
  • 二つ以上のホストで複数のプロセスを分散配置する
プロセス間通信だけの利用でソケット通信を使っている場合は、基本的にホスト名とポート番号を使って振り分けができていれば特に問題はありませんが、一つのサーバー上でクライアント⇔サーバー間とサーバー⇔サーバー間でやり取りを行う場合、プロトコルとコマンド処理を切り分けて使う事に課題がありました。

そこでデモ用として使っている今回のマルチサーバーには、一つのサーバープロセスの中で待ち受けポートをWebsocket用とマルチサーバー用の2つに分けて実装(SocketManagerモジュールを2つ使用)する事で切り分けができるようにしています。

これを図にすると以下のようになります。
※プロセスをフォークしているわけではないので"親プロセス/子プロセス"ではなく敢えて"親サーバー/子サーバー"と表記しています。

アーキテクチャページのメイン処理クラスの表記を代用し、1つのサーバーにフォーカスを当てたイメージが以下のようになります。
(UNITパラメータクラスでグローバル共有しています)
UNITパラメータクラスのグローバル共有の部分にフォーカスを当てると以下のようなイメージになります。
そして実際にはノンブロッキングループで並走する事になるので以下のようなイメージになります。

ソースで確認

ここでは上記で説明したサーバー構成をChatServerForTcpMulti.phpのソースを追いながらみていきます。
デモ用:app/MainClass/ChatServerForTcpMulti.php内のグローバルエリア交換設定
// 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用のUNITパラメータクラスParameterForWebsocketとマルチサーバー用のUNITパラメータクラスParameterForTcpMultiのインスタンスを生成し、それぞれの設定メソッドを使って互いのインスタンスを交換設定しています。
デモ用:app/MainClass/ChatServerForTcpMulti.php内の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用の方はノーマルな初期設定ブロックになっています。
デモ用:app/MainClass/ChatServerForTcpMulti.php内のマルチサーバー用の初期設定ブロック
// 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用のSocketManagerと異なるのは冒頭のSocketManagerのインスタンス生成時に条件判断が入っているところです。
マルチサーバーには親子関係がありますから、自身が親サーバーなのか、あるいは子サーバーなのかを判断する必要があります。
また、同じTCP通信同士ではWebsocket用とマルチサーバー用で同じポートが使えないので異なるポート番号を使用する必要があります。
(マルチサーバー用のポートをUDP通信で使う場合はWebsocketのTCP通信とはリソースが異なるので同じポート番号でも問題ありません)

そこでこのデモではWebsocket用で使うポート番号+10の値を親サーバー用のポート番号として使うというルールにしています。
つまりコマンドライン引数の指定でWebsocket用のポート番号が10000、親サーバーのポート番号10010で指定されていれば、自身は親サーバーだと判断するようにしています。
(UDP通信の場合はWebsocket用のポート番号と親サーバー用のポート番号の指定が同じであれば親だと判断しています)

自身が子サーバーだと判断した場合、待ち受けホストもポート番号も共に設定する必要がないのでSocketManagerの引数を空にしています。
デモ用:app/MainClass/ChatServerForTcpMulti.php内のポート設定ブロック
$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用の方はListenするだけでいいのですが、マルチサーバーの場合は親の時はListen、子の時はConnectにする必要があるので条件分岐しています。
デモ用:app/MainClass/ChatServerForTcpMulti.php内のノンブロッキングループブロック
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;
    }
}
                    

ここではWebsocket用のSocketManagerとマルチサーバー用のSocketManager共にcycleDrivenメソッドを呼んで周期ドリブンの処理で並走しています。

おわりに

サーバーをまたがってユーザーを検索するようなケースではデータベースで串刺し検索を行った方が早いように思えますが、トランザクションロックを起こす可能性がある限り同時接続している他のユーザーに影響が出る可能性を考慮しないといけません。

データの永続化が必要な場合は、起動時に必要な情報を読み込んでおくようにしたり、今回のようなマルチサーバーでデータベースアクセス専用のサーバーを起ててリクエストを投げるだけにするなど、工夫次第でパフォーマンスへの影響を軽減できる方法はあると思います。
その上でデータベース上のデータの共有が必要であればサーバー間通信で賄えばいいでしょう。

サーバーに障害が発生した時はデータがなくなってしまうかもしれませんが、それはデータベースサーバーで障害が発生しても同じ事が言えるので、データを分散化したりREDISを活用したりインフラサイドのフォローがどれだけ考慮されているかに尽きると思います。

マルチサーバーの起動方法は▶デモサーバーの種類⇒マルチサーバーのページをご覧ください。