0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SOCKET-MANAGERを使った自作サーバー開発の手引き<マルチサーバーの構成について>

Posted at

はじめに

ここで述べているマルチサーバーというのは、親子関係がある複数のサーバー同士でサーバー(プロセス)間通信(IPC)を通して連携し合っているサーバーの事を指しています。

このページでは クライアント⇔サーバー 間と サーバー⇔サーバー 間で並行処理を行いながらプロトコル部とコマンド(ビジネスロジック)部を分割管理する手段を提供します。

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

サーバー構成

マルチサーバーの構成には色々なパターンが考えられると思いますが、大きく分けると以下の2パターンになるかと思います。

  • 一つのホスト上で複数のプロセスを配置する(スケールアップ)
  • 二つ以上のホストで複数のプロセスを分散配置する(スケールアウト)

プロセス間通信だけの利用でソケット通信を使っている場合は、基本的にホスト名とポート番号を使って振り分けができていれば特に問題はありませんが、一つのサーバー上で クライアント⇔サーバー 間と サーバー⇔サーバー 間でやり取りを行う場合、プロトコル部とコマンド部を切り分けて使う事に課題がありました。

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

これを図にすると以下のようになります。

SOCKET-MANAGERのマルチサーバー構成図

プロセスをフォークしているわけではないので"親プロセス/子プロセス"ではなく敢えて"親サーバー/子サーバー"と表記しています。

上の図では、SOCKET-MANAGER Framework のエントリポイントをクライアント通信側とサーバー間通信側に分けた上で、UNITパラメータクラスによるグローバル共有(コンテキスト)を介して通信データを共有できるように構成しています。

また、SocketManager のエントリポイントが分かれる事によってサーバー間通信側と分離した管理が可能になるため、以下のメリットが生まれます。

  • クライアント通信側と同じインターフェースでサーバー間通信の開発が可能
  • INETソケット(オリジナルプロトコルを含む)を使ったサーバー間通信が可能
  • 物理サーバーが分かれていても同じインターフェース(INETソケット通信)が利用できるのでシステム変更が不要
  • 別途モジュールの追加実装を必要としない

結果、単一ソリューションによるアーキテクチャレベルでの実装が容易になり、外部ライブラリや外部メディア(あるいはサーバー)などの管理作業が省略できる事で中長期的な運用の手間が軽減されます。

仮に1つのサーバープロセス上のエントリポイント分割にフォーカスを当てたイメージが以下のようになります。
(UNITパラメータクラスでグローバル共有しています)

クライアント通信用/サーバー間通信用それぞれのメイン処理クラスの構成図

UNITパラメータクラスのグローバル共有の部分にフォーカスを当てると以下のようなイメージになります。

クライアント通信用/サーバー間通信用それぞれのUNITパラメータクラスをグローバル共有としてリソースを交換するイメージ

そして実際にはノンブロッキングループで並走する事になるので以下のようなイメージになります。

クライアント通信用/サーバー間通信用それぞれのノンブロッキングループ並走イメージ

ソースで確認

ここでは上記で説明したサーバー構成を ChatServerForTcpMulti.php のソースを追いながらみていきます。

以下の実装では Websocket 用のUNITパラメータクラス ParameterForWebsocket とマルチサーバー用のUNITパラメータクラス ParameterForTcpMulti のインスタンスを生成し、それぞれの設定メソッドを使って互いのインスタンスを交換設定しています。

デモ用: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 用の実装ではノーマルな初期設定ブロックになっています。

デモ用: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 用と異なるのは、冒頭の SocketManager のインスタンス生成時に条件判断が入っているところです。
マルチサーバーには親子関係がありますから、自身が親サーバーなのか、あるいは子サーバーなのかを判断する必要があります。
また、同じTCP通信同士では Websocket 用とマルチサーバー用で同じポートが使えないので異なるポート番号を使用する必要があります。
(マルチサーバー用のポートをUDP通信で使う場合は Websocket のTCP通信とはリソースが異なるので同じポート番号でも問題ありません)

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

自身が子サーバーだと判断した場合、待ち受けホストもポート番号も共に設定する必要がないので SocketManager の引数を空にしています。

デモ用: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 用の方は Listen するだけでいいのですが、マルチサーバーの場合、親の時は Listen、子の時は Connect にする必要があるので以下のように条件分岐しています。

デモ用: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 用の SocketManager とマルチサーバー用の SocketManager 共に cycleDriven メソッドを呼んで周期ドリブンの処理で並走しています。

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;
    }
}

おわりに

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

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

デモ環境のマルチサーバーの起動方法については以下のページをご覧ください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?