使用Phoenix和Redis的Pub/Sub来扩展WebSocket的规模
一般而言,使用WebSocket的应用程序通常会通过使用发布/订阅服务器来扩展规模。
我們將使用Redis的發布/訂閱功能來擴展Phoenix的WebSocket。
请参考此链接以了解如何在Phoenix中进行WebSocket通信的方法。
预备知识:关于WebSocket应用程序的扩展性
在常规的Web应用程序中,水平扩展一般是通过增加服务器数量并在负载均衡器上分配请求来实现的。然而,在WebSocket应用程序中,这种方法是无法使用的。因为WebSocket应用程序是基于服务器内部管理连接的有状态结构。如果将请求分散到冗余服务器上,将无法将消息广播到其他连接到其他服务器的客户端,这是一个问题。为了解决这个问题,需要通过某种方式在服务器外部管理连接信息,并使服务器本身保持无状态的状态。
当听到要在外部管理连接信息时,我觉得可能会变得相当困难,但是 Redis 的 Pub/Sub 功能可以很好地解决这个问题。
通过使用这个 Pub/Sub 功能来中继消息的交流,可以实现连接在 Redis 服务器内进行管理,并通过冗余实现扩展性。
我們將在一個設備上啟動兩個帶有不同端口號的 Phoenix 應用程式和一個 Redis 伺服器,以體驗模擬的擴展性。系統配置的概念如下所示。
安装Phoenix应用程序
我要创建一个名为 redis_pubsub_sample 的应用程序。
$ mix phoenix.new redis_pubsub_sample
暂时不干涉依赖关系。
另外,本次我們想要在兩個不同的端口 4000 和 4001 上分別啟動應用程式,以檢查 WebSocket 的行為。
為了能夠在啟動時指定使用的端口,我們將對 config/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文件,并去掉注释。
defmodule PubsubRedisSample.UserSocket do
use Phoenix.Socket
## Channels
# 以下の1行についてコメントアウトを解除
channel "rooms:*", PubsubRedisSample.RoomChannel
## Transports
...
接下来,我们将实现PubsubRedisSample.RoomChannel模块。创建一个名为web/channels/room_channel.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,所以我们还要从 CDN 加载 jQuery 到 app.js 文件中。
<!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 文件。
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/ 时,我想会弹出一个输入表单,让我们尝试输入一些文字并发送。
当您在其他窗口或应用程序中访问 http://localhost:4000/ ,您可以确认通过WebSocket广播消息。
然后,您可以打开另一个终端,然后使用4001端口启动应用程序。
$ PORT=4001 iex -S mix phoenix.server
4000 番ポートと同様に一通り動作を確認したら、最後に 4000 と 4001 の両ポート間でメッセージがやりとりされないことを確認しておきましょう。
(現時点では接続情報が 4000 と 4001 で共有されていないため、メッセージはやりとりできません)
听起来你很有男子气概。
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 這個庫。
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的终端点。
...
# 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之间共享,因此双方应该可以进行消息交流。
好的感觉对吧。
印象
-
- WebSocket もちゃんとスケールアウトできるようで安心した
ステートフルなサービスは不慣れで不安が多い
Redis に Pub/Sub 機能が付いた経緯が知りたい