LoginSignup
106
108

More than 5 years have passed since last update.

Phoenix で Redis の Pub/Sub を使って WebSocket をスケールアウトさせる

Last updated at Posted at 2015-09-21

WebSocket を利用するアプリケーションは Pub/Sub サーバを使ってスケールアウトさせるのが一般的です。

今回は Redis の Pub/Sub 機能を使って Phoenix の WebSocket をスケールアウトさせてみます。

Phoenix で WebSocket 通信をさせる方法はコチラをご参照ください。

事前知識: WebSocket アプリケーションのスケールアウトについて

通常の Web アプリケーションにおけるスケールアウトは、サーバ台数を増やしてロードバランサでリクエストを分散させる、というのが一般的ですが、WebSocket アプリケーションではこの手法が使えません。
なぜかというと、WebSocket アプリケーションはコネクションをサーバ内で管理するステートフルな作りになっているためです。冗長化させたサーバにリクエストを分散させてしまうと、他サーバで接続されたクライアントに対してメッセージのブロードキャストができないという問題が生じてしまいます。これを解決するためには、接続情報をなんらかの方法でサーバ外部で管理して、サーバ自体はステートレスな状態にする必要があります。

接続情報を外部で管理...と聞くと結構大変な気がしますが、この辺りをよろしく解決してくれるのが Redis の Pub/Sub 機能 です。
この Pub/Sub 機能を中継してメッセージをやりとりすることで、コネクションが Redis サーバ内で管理されるようになり、冗長化によるスケールアウトが実現されます。

今回は1つの端末内にポート番号を分けた Phoenix アプリケーションを2つと Redis サーバを起動させて、擬似的なスケールアウトを体験してみようと思います。
システム構成のイメージは以下のような感じです。

pubsub_websocket.png

Phoenix アプリケーションをセットアップする

redis_pubsub_sample というアプリケーションを作ります。

$ mix phoenix.new redis_pubsub_sample

ひとまずは依存関係は弄らないでおきます。

なお、今回はアプリケーションを 40004001 の2つのポートで別々に起動させて、WebSocket の挙動を確認してみたいと思います。
利用するポートを起動時に指定できるよう、config/dev.exs に以下の改修を加えます。

dev.exs
use Mix.Config
...
config :pubsub_redis_sample, PubsubRedisSample.Endpoint,
  # http: [port: 4000],
  # ポート番号を環境変数 PORT から取得するようにする
  http: [port: System.get_env("PORT")],
  debug_errors: true,
  code_reloader: true,
  cache_static_lookup: false,
...

以上でセットアップは完了です。

簡易的なチャットアプリを作成する

動作確認用に簡易的なチャットアプリを構築します。

web/channels/user_socket.ex を開き、コメントアウトを外します。

user_socket.ex
defmodule PubsubRedisSample.UserSocket do
  use Phoenix.Socket

  ## Channels
  # 以下の1行についてコメントアウトを解除
  channel "rooms:*", PubsubRedisSample.RoomChannel

  ## Transports
...

次に、PubsubRedisSample.RoomChannel モジュールを実装します。
web/channels/room_channel.ex というファイルを作成し、以下のように記述します。

room_cahnnel.ex
defmodule PubsubRedisSample.RoomChannel do
  use Phoenix.Channel

  def join("rooms:lobby", _auth_msg, socket) do
    {:ok, socket}
  end
  def join("rooms:" <> _private_room_id, _auth_msg, socket) do
    {:error, %{reason: "unauthorized"}}
  end

  def handle_in("send_message", %{"message" => message}, socket) do
    broadcast! socket, "receive_message", %{message: message}
    {:noreply, socket}
  end
end

以上でサーバサイドは完了で、次にクライアントサイドを作ります。
例のごとく web/templates/layout/app.html.eex を直に書き換えていきましょう。
メッセージ送信用のフォームと、受け取ったメッセージを表示するコンテナを作ります。
また、今回は jQuery を利用しますので、app.js に加えて jQuery を CDN から読み込ませています。

app.html.eex
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Phoenix PubSub Sample</title>
    <link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
    <style>
      .received-message {
        color: #555;
        font-style: italic;
        padding: 10px 0 5px;
        border-bottom: 1px solid #ddd;
      }
      .received-message:first-child {
        font-size: 2em;
      }
    </style>
  </head>

  <body>
    <div class="container" role="main">
      <form class="form-inline">
        <div class="form-group">
          <input type="text" class="form-control" id="input-send-message">
        </div>
        <button type="submit" class="btn btn-default">Send</button>
      </form>
      <div id="received-messages">
      </div>
    </div>

    <script src="//code.jquery.com/jquery-2.1.4.min.js"></script>
    <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
  </body>
</html>

最後に app.js を書き換えます。

app.js
import {Socket} from "deps/phoenix/web/static/js/phoenix/"

var socket = new Socket("/socket");
socket.connect();
var channel = socket.channel("rooms:lobby", {});
channel.join();

$("form").submit(function(e) {
    e.preventDefault();
    channel.push("send_message", {message: $("#input-send-message").val()});
    $("#input-send-message").val("");
});

channel.on("receive_message", function(dt) {
    var div = $("<div></div>", {"class": "received-message"})
        .text(dt.message);
    $("#received-messages").prepend(div);
});

フォームが送信されたタイミングでチャンネルに対してプッシュし、メッセージが受信されたタイミングでコンテナにメッセージを表示させています。

以上で実装は完了です。

動作を確認する その一

まずはこのチャットアプリ単体の動作を確認してみましょう。
以下のコマンドで、4000 番ポートでアプリケーションを起動させます。

$ PORT=4000 iex -S mix phoenix.server

プラウザで http://localhost:4000/ にアクセスすると入力フォームが出ると思いますので、いくつか文字を入力して送信してみましょう。

スクリーンショット 2015-09-21 19.20.08.png

別ウィンドウなどで http://localhost:4000/ にアクセスすると、WebSocket によるメッセージのブロードキャストが確認できると思います。

スクリーンショット 2015-09-21 19.22.08.png

次に、ターミナルをもう1つ起動させるなどして、今度は 4001 番ポートでアプリケーションを起動させます。

$ PORT=4001 iex -S mix phoenix.server

4000 番ポートと同様に一通り動作を確認したら、最後に 4000 と 4001 の両ポート間でメッセージがやりとりされないことを確認しておきましょう。
(現時点では接続情報が 4000 と 4001 で共有されていないため、メッセージはやりとりできません)

スクリーンショット 2015-09-21 19.24.18.png

大丈夫そうですね。

Redis を準備する

Phoenix と Redis を連携させる前に、まずは Redis 自体を準備しましょう。
と言っても、端末に Redis をインストールして起動しておくだけで OK です。
Mac な方は homebrew でサクッとインストール可能です。

$ brew install redis
$ redis-server /usr/local/etc/redis.conf

これで 6379 ポート(Redis のデフォルト)で立ち上がります。

Redis 連携をセットアップする

さて、いよいよ Redis と連携させます。
今までは Phoenix 自身の PubSub を使ってメッセージをやりとりしていましたが、今後は先程起動させた Redis の PubSub サーバを中継させるようにします。これにより、4000 と 4001 の両ポートでメッセージのやりとりができるようになるハズです。

まずは依存関係の追加をします。使うライブラリは Phoenix.PubSub.Redis です。

mix.exs
defmodule PubsubRedisSample.Mixfile do
  ...
  def application do
    [mod: {PubsubRedisSample, []},
     applications: [:phoenix, :phoenix_html, :cowboy, :logger,
                    :phoenix_ecto, :postgrex,
                    # phoenix_pubsub_redis を追加
                    :phoenix_pubsub_redis]]
  end
  ...
  defp deps do
    [{:phoenix, "~> 1.0.0"},
     {:phoenix_ecto, "~> 1.1"},
     {:postgrex, ">= 0.0.0"},
     {:phoenix_html, "~> 2.1"},
     {:phoenix_live_reload, "~> 1.0", only: :dev},
     {:cowboy, "~> 1.0"},
     # phoenix_pubsub_redis を追加
     {:phoenix_pubsub_redis, "~> 1.0.0"}]
  end
end

依存関係をダウンロードします。

$ mix deps.get

次に、config/config.exs を編集して PubSub のエンドポイントを変更します。

config.exs
...
# Configures the endpoint
config :pubsub_redis_sample, PubsubRedisSample.Endpoint,
  url: [host: "localhost"],
  root: Path.dirname(__DIR__),
  secret_key_base: "gu6oPHeuSxfTrcoUg22GFBBmKFGnn2AebGrRe/tUY4g2drnpxZWKgraTbztTpJwp",
  render_errors: [accepts: ~w(html json)],
# 従来の設定はコメントアウトしておきます
#  pubsub: [name: PubsubRedisSample.PubSub,
#           adapter: Phoenix.PubSub.PG2]
  pubsub: [name: PubsubRedisSample.PubSub,
           adapter: Phoenix.PubSub.Redis,
           host: "localhost"]
...

以上で Redis 連携のセットアップは完了です。

動作を確認する その二

さて、先程と同様に 4000 番ポートと 4001 番ポートでそれぞれアプリケーションを起動して、ブラウザからメッセージのやりとりをしてみましょう。
今回は中継サーバを入れたために 4000 と 4001 で接続情報が共有され、お互いにメッセージのやりとりが可能となっているはずです。

スクリーンショット 2015-09-21 19.52.40.png

いい感じですね。

感想

  • WebSocket もちゃんとスケールアウトできるようで安心した
    • ステートフルなサービスは不慣れで不安が多い
  • Redis に Pub/Sub 機能が付いた経緯が知りたい
106
108
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
106
108