はじめに
株式会社diffeasyの政栄です。主にバックエンド担当です。(Rails)
弊社では「大会運営 向上心」という大会運営をサポートするwebアプリを開発・運営しています。
今回はその「大会運営 向上心」のトーナメント表示部分にてリアルタイムに更新されるようにしてみました。
※まだテスト段階で、リリース日は未定です。
実装概要
RailsのAction Cableを使用して、Websocket通信させて、リアルタイム更新を実現します!
また、Action CableやWebsocket関連の知識について未熟ですので、間違い等ありましたら、教えてください。
トーナメント表示にリアルタイム更新機能実装しよう!
トーナメント表示機能の説明
トーナメント機能では、トーナメントを見る人の画面と、トーナメント結果入力者用の画面があります。
現在は、結果入力が実施されたとき、閲覧者自が"最新の状態に更新"ボタンをクリックして更新があるかどうか確認しなければいけない状態です。
そのため、Action Cableを使用して、サーバー側から更新できるような仕組みにいこうというのが今回の試みです。
実装結果はこんな感じです。右が結果入力する人の画面で、左側が閲覧者の画面です。
トーナメントの右上の五郎さんが決勝へ進み、決勝戦で太郎さんが勝っています。
リアルタイム更新について
今回採用したAction CableではWebsocketという技術をつかっています。
実装前に使用されている技術について調べたことを載せます。
Websocketとは??
- TCP上で低コストでの双方向通信
低コストとは、昔の技術で、リアルタイム更新のような機能を実現しようとすると何秒毎に更新する(ポーリング)とかをして負荷をかけてた。websocketは不必要な更新しない。 - websocketの通信手順
- HTTP(厳密にはそれをwebsocketにupgradeしたもの)でクライアントとサーバー間で情報をやり取りしてコネクション確立
- 確立されたコネクション上で、低コストな双方向通信。コネクションが確立されることをハンドシェイクという。握手
Railsでのwebsocket
Rails標準ではredisを使ってるメッセージキューをredisに保存
アクセス多いサイトになってきたら、別途redisサーバーを用意したりできる
Pub/Subについて
Pub/Subはパブリッシャ-サブスクライバ(pub/sub)型モデルとも呼ばれる、メッセージキューのパラダイムです。パブリッシャ側(Publisher)が、サブスクライバ側(Subscriber)の抽象クラスに情報を送信します。 このとき、個別の受信者を指定しません。Action Cableは、サーバーと多数のクライアント間の通信にこのアプローチを採用しています。
実装方法
アプリケーションの環境を載せます。
- Rails (5.0.6)
- ※Action CableはRails5からの機能です。
- Nuxt.js (1.3.0)
バックエンド側の処理から
consoleにて
rails g channel message
チャンネルの設定
app/channels/message_channel.rb
# チャンネル接続時に呼ばれる
def subscribed
stream_from "message_#{params[:room]}"
end
# def unsubscribed
# # Any cleanup needed when channel is unsubscribed
# end
# メッセージをブロードキャストするためのアクション
# def speak(data)
# ActionCable.server.broadcast "message_#{params[:room]}", message: data['message']
# end
end
- speakの関数部分は、DBを介さずにリアルタイムでメッセージのやり取りをするときに使えます。
- トーナメント更新のようにDB反映を元にブロードキャスト(配信)したいときはspeakは使いません。
トーナメントの勝ち上がり処理をトリガーにします
def win
ActiveRecord::Base.transaction do
@tournament_bracket.win
ActionCable.server.broadcast "message_#{@tournament_bracket.game_event_id}", message: @tournament_bracket.player_name
end
render json: {status: :success}
rescue => e
render json: {status: :error}
end
つまり、バックエンドのアクションの後に、
ActionCable.server.broadcast "message_#{@tournament_bracket.game_event_id}", message: @tournament_bracket.player_name
を書くことで配信ができます。
- game_event_idとは、種目のことで、該当種目のページを開いている人にのみ、ブロードキャストします。
ルーティングの設定
Rails.application.routes.draw do
# 以下を追記
mount ActionCable.server => '/cable'
end
フロント側
パッケージのインストール
npm install actioncable
websocketとの接続処理
.envにバックエンドでマウントしたやつを書く
BACK_WSS_URL=ws://localhost:4000/cable
utilsディレクトリ内にcable.jsを作成する
cable.jsはバックエンド側と接続するためのチャンネルを作成するものです。
また、重複してのチャンネル接続がないように、すでに接続しているチャンネルがある場合、クリアするようにしています。
import cable from 'actioncable'
let consumer
function createChannel (...args) {
if (!consumer) {
consumer = cable.createConsumer(process.env.BACK_WSS_URL)
} else {
consumer.subscriptions['subscriptions'].shift()
}
return consumer.subscriptions.create(...args)
}
export default createChannel
トーナメントを描画しているページで実際にチャンネルを接続する
まずは先程作ったcable.jsを読み込む
import createChannel from '~/utils/cable'
create時に実際に接続し、双方間通信状態とする
async created () {
this.messageChannel = createChannel({channel: 'MessageChannel', room: this.gameEventId}, {
received: (data) => {
// ブロードキャストされたときにトーナメント情報をサーチしにいくapiを投げる
this.searchTournamentBrackets({gameEventId: this.gameEventId})
// 通知を表示する
this.$notify.info({
title: 'トーナメント更新情報',
message: data.message
})
}
})
}
room: this.gameEventId
部分が種目部分で種目のidにて接続しているので、ここで指定したid以外でのブロードキャストは受け取りません。
また通知部分に関してはelementUIを使用しています。
本番環境で使用する際は下記設定が要ります。
- nginxの設定
location /cable {
proxy_pass http://app/cable;
proxy_http_version 1.1;
proxy_set_header Upgrade websocket;
proxy_set_header Connection Upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
- actioncableの設定を変える(バックエンド側)
actioncableは指定のURL以外websocketを繋がない設定だからアクセス元の許可をしてあげます。
config/environments/production.rb
URLがhttps://example.com
の場合
config.action_cable.url = 'wss://example.com/cable'
config.action_cable.allowed_request_origins = [ 'https://example.com/', /https:\/\/example.*/ ]
- websocketにはws:で始まるものとwss:で始まるものとあります。httpとhttpsの関係と同じですので、それぞれの環境に合わせる必要があります。
負荷テストしてみました
- 同時接続数が多いときにリアルタイム更新処理をさばけるか
- サーバーに対してどのような負荷がかかるのか
今回のテストの環境
- 弊社staging環境を使用
- 勤務しているメンバーに複数タブにて同じ画面を開いてもらいました!
接続数MAX 515件
負荷テスト結果は無事更新処理できました。
- websocketでの接続と、pub/subについては、Redis(kvs)を使用しているので、メモリがやばいかなと思っていたけど、実際負荷がかかったのはCPUでした。
- 500接続でもリアルタイム更新できました。
- 500接続というのは、同じ種目のトーナメントを同時に見ている人(サイトへのアクセス総数ではない)
更新時のメモリ
更新時のCPU
課題
- 弊社ではGCPを使用してるのですが、GCPにてロードバランサを設定しています。
その際にロードバランサの外側はhttps通信をしていますが、ロードバランサ内はhttpでやりとりをしているため、リアルタイム更新ができなかった。
→インフラの調整中です - トーナメントを表示したままスマホをロックした場合に更新させることができない。
→解決できるのでしょうか??
まとめ
- ActionCableを使用することでリアルタイム更新可能なwebアプリケーションを作成することができました。
- 更新ボタンを押して貰う必要がなくなるため、ユーザービリティの向上につながったと思います。
- 自分が未熟なため、リアルタイム処理の実装についてのベストプラクティスがわかりません。
- Action cableの情報についてもチャットアプリを作って見た系はある程度。
以上です。ありがとうございました。