Phoenix Framework で WebSocket のチャット

WebSocketでチャットという、遠い昔に流行った題材で、 Phoenix Framework を使ってデプロイまでやってみた。

URL

とりあえず Nginx の背後において動かしてみた。 EC2 の t2.nano インスタンスで動かしてみてる。割とサクサクしてる感じがします。
https://chat.ecpplus.net 2つブラウザ開くか、PCとスマホとかで見ると通信の速度が確認できます。

環境構築

elixir

elixir のインストールは Homebrew とかで出来ます。

$ brew install elixir
$ brew install rebar3 # 手元だと、prod の compile で必要だった
$ elixir -v
Erlang/OTP 19 [erts-8.1] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Elixir 1.3.3
$ node -v
v6.6.0
$ npm -v
3.10.3

vim

vim-elixir, vim-phoenix を入れてみた。2個目はhaml対応してなかったので、fork して使った。 vim-phoenix は、 vim-projectionist で作られてるので、使い勝手は rails.vim と似たような感じになります。 rails.vim ほどは作り込まれていないけど、十分な感じ。 Rails の開発は、 rails.vim のおかげで3倍くらい速くなってる気がするので、 Phoenix Framework のファイル構成が Rails に似てるのもあって、慣れてきたらはやく書けそう。

というのを、dein.toml に追加しました。

# Elixir
[[plugins]]
repo = 'elixir-lang/vim-elixir'

[[plugins]]
repo = 'ecpplus/vim-phoenix'

開発前の準備

  • Static Assets をビルドするデフォルトのツールが brunch なのだが、慣れてるのと情報が多そうなので webpack に変更
  • Templates を記述するのが EEx (like erb)がデフォルトなのだが、閉じタグ書きたくないので haml に変更

という2点をやった。

brunch -> webpack

package.json

Delete brunch-config.js, package.json and initialize package.json.

$ rm brunch-config.js package.json
$ npm init

Install npm packages. I want to use sass, Bootstrap4, es2015, React but anything is OK.

Then I got a package.json!

webpack.config.js

When I compile for production, run

$ NODE_ENV=production node_modules/webpack/bin/webpack.js -p

Don’t forget to put .babelrc.

config/dev.exs

Change watchers from brunch to webpack. npm start is defined in package.json. And add .haml to live reloading hook.

--- a/config/dev.exs
+++ b/config/dev.exs
@@ -11,9 +11,9 @@ config :chat, Chat.Endpoint,
   debug_errors: true,
   code_reloader: true,
   check_origin: false,
-  watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin",
-                    cd: Path.expand("../", __DIR__)]]
-
+  watchers: [
+    npm: ["start", cd: Path.expand("../", __DIR__)]
+  ]

 # Watch static and templates for browser reloading.
 config :chat, Chat.Endpoint,
@@ -22,7 +22,7 @@ config :chat, Chat.Endpoint,
       ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
       ~r{priv/gettext/.*(po)$},
       ~r{web/views/.*(ex)$},
-      ~r{web/templates/.*(eex)$}
+      ~r{web/templates/.*(eex|haml)$}
     ]
   ]

web/static/css/app.sass

Delete app.css, phoenix.css(Bootstrap3). Then add app.sass like following.

I copied default _variables.scss for a template.

$ cp node_modules/bootstrap/scss/_variables.scss web/static/css/bootstrap_variables.sass

That’s it!

実装

コード全然整理されていないけれども、GitHub にコードを上げてあります。
(ecpplus/phoenix-framework-websocket-chat-example)[https://github.com/ecpplus/phoenix-framework-websocket-chat-example]

デプロイ

phoenix.server

Phoenix Framework の公式サイトにあったコマンドをほぼ持ってきただけだが、 Ubuntu で webpack のコンパイルが通らなかったので、 assets を手元で作ってからデプロイした。出来なかった理由は追ってない。

$ MIX_ENV=prod mix phoenix.digest

というので、 assets に hash 値をつけてくれる模様。 priv/static/ 以下に出力されているので、ここを webroot として扱えばOKなようだ。

アプリケーションサーバは、 MIX_ENV=prod mix phoenix.server で十分高速ということだったので、そのまま使ってる。

$ MIX_ENV=prod mix phoenix.server

とするとフォアグラウンドで立ち上がります。

$ MIX_ENV=prod PORT=4001 elixir --detached -S mix do compile, phoenix.server

とするとバックグラウンドで立ち上がります。

多分 elixir より堅牢な supervisor となる(ややこしい) ソフトウェアもない気もするので、これでとりあえずこのままにしてみようと思います。

nginx

TLS は Let’s Encrypt とかでサクッと用意して、それ以外の設定です。 WebSocket は /socket 以下につなぐようにしています。 proxy_http_version は 1.1 にしないと Nginx のエラーになりました。Nginxで動かす場合で調べると、書いてる人みんな同じ設定なので多分それで良いのだと思われるが、あまり調べてないです。 Static assets は Nginx で配信して、それ以外を phoenix.server という感じで動かしました。

負荷テスト

t2.nano インスタンスで、DBアクセスがないページに keepalive 付きで、100並列で20000回アクセスしてみた結果です。
アプリケーションの処理自体はほとんどないページなので、フレームワークが必ず行う処理の速度がこれくらいということだと思います。
この非力なサーバで1514.92リクエスト/秒も処理できてるので、しょぼいVPSでも十分実用的な速度が出そう。
この処理の間で、 beam のプロセスが CPU60% くらい使ってました、メモリはあまり変わらず3%くらいでした。

$ ab -k -c 100 -n 20000 "https://chat.ecpplus.net/"
This is ApacheBench, Version 2.3 <$Revision: 1706008 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking chat.ecpplus.net (be patient)
Completed 2000 requests
Completed 4000 requests
Completed 6000 requests
Completed 8000 requests
Completed 10000 requests
Completed 12000 requests
Completed 14000 requests
Completed 16000 requests
Completed 18000 requests
Completed 20000 requests
Finished 20000 requests


Server Software:        nginx
Server Hostname:        chat.ecpplus.net
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128

Document Path:          /
Document Length:        905 bytes

Concurrency Level:      100
Time taken for tests:   13.202 seconds
Complete requests:      20000
Failed requests:        0
Keep-Alive requests:    19829
Total transferred:      30119145 bytes
HTML transferred:       18100000 bytes
Requests per second:    1514.92 [#/sec] (mean)
Time per request:       66.010 [ms] (mean)
Time per request:       0.660 [ms] (mean, across all concurrent requests)
Transfer rate:          2227.94 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    5  53.5      0     915
Processing:    42   60  21.9     55     808
Waiting:       42   59  21.7     55     808
Total:         42   65  63.4     55     976

Percentage of the requests served within a certain time (ms)
  50%     55
  66%     57
  75%     59
  80%     60
  90%     66
  95%     89
  98%    162
  99%    303
 100%    976 (longest request)

keepalive なしだと、回線かクライアント側の問題で、サーバに全然負荷をかけられませんでした。
Nginx の CPU 使用が20%くらいで、beam はほとんど CPU 使ってない状態でした…。

~ ᐅ ab -c 100 -n 20000 "https://chat.ecpplus.net/"
This is ApacheBench, Version 2.3 <$Revision: 1706008 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking chat.ecpplus.net (be patient)
Completed 2000 requests
Completed 4000 requests
Completed 6000 requests
Completed 8000 requests
Completed 10000 requests
Completed 12000 requests
Completed 14000 requests
Completed 16000 requests
Completed 18000 requests
Completed 20000 requests
Finished 20000 requests


Server Software:        nginx
Server Hostname:        chat.ecpplus.net
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128

Document Path:          /
Document Length:        905 bytes

Concurrency Level:      100
Time taken for tests:   263.151 seconds
Complete requests:      20000
Failed requests:        0
Total transferred:      30020000 bytes
HTML transferred:       18100000 bytes
Requests per second:    76.00 [#/sec] (mean)
Time per request:       1315.753 [ms] (mean)
Time per request:       13.158 [ms] (mean, across all concurrent requests)
Transfer rate:          111.41 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      148 1017 820.6    715   12429
Processing:    42  295 391.6    212   14791
Waiting:       42  235 373.8    160   14791
Total:        233 1313 918.9    991   16536

Percentage of the requests served within a certain time (ms)
  50%    991
  66%   1230
  75%   1468
  80%   1656
  90%   2233
  95%   3289
  98%   4166
  99%   4885
 100%  16536 (longest request)

実は ecpplus.net は t2.nano に置いてるのですが、動的なコンテンツを吐くの恐ろしすぎて WordPress や Sinatra で吐いてる HTML も全ページ Nginx のキャッシュにしてて、静的な以外配信してません。が、 Elixir なら割と行けそうな感じでした。メモリ的にも、フレームワークを起動しただけなら 10MBくらいしか消費しません。 Unicorn で動かしてるほぼ最小構成な Sinatra は6MBくらいなので、Phoenix が特に少ないわけではないかもしれないですが。

次にやりたいこと

  • AWS のサービスとの連携をやってみる
  • NoSQL との連携をやってみる
  • テストを書く
  • 良いデプロイ方法を調べる
  • iOSから叩く何かのAPIを作ってみる
Phoenix Framework で WebSocket のチャット