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 のチャット

Nginx のレスポンスヘッダの Server のバージョンを非表示にする

Nginx のデフォルトの設定だと、レスポンスヘッダに Nginxのバージョンが含まれているので、セキュリティ的には外した方がベターです。

Server nginx/0.8.53

みたいになっています。

設定で、下記のように server_token off; と書けばバージョンが外れて

Server nginx

という表記になります。デフォルトの4xx, 5xx のページの下部にあるNginxの表記からもバージョンが外れます。古いバージョンだと、エラーページのバージョンは外れないかも。

http {
    server_tokens off;
    ...
}

とすれば全体で有効になり、server ディレクティブの中に記載すれば、その server のみで非表示になります。

Nginx のレスポンスヘッダの Server のバージョンを非表示にする

Nginx で IPアドレスによるアクセス制限

 Nginx がついに version1.0.0 でリリースされましたね!Nginx は速いのと設定ファイルが書きやすいので大好きです。

 Nginx で特定IPアドレスからのアクセスを許可するには、allow, deny を使う。初めにマッチしたところで評価が終了するようなので、deny all; と先頭に書いてしまうと誰も見られなくなってしまうので注意。サブネットでの指定も、単一IPアドレスでの指定もいけます。

server {
    listen       80;
    server_name  ecpplus.net;
 
    allow 192.168.1/24;
    allow 111.111.111.111;
    deny all;
    ...
}

のようにします。location ディレクティブの中に入れても大丈夫です。 /admin 以下のみ制限を掛けたい場合は、下記のようにします。

server {
    listen       80;
    server_name  ecpplus.net;
 
    location ~^/admin {
        allow 192.168.1.10;
        deny all;
    }
    ...
}
Nginx で IPアドレスによるアクセス制限

Nginx の X-Accel-Redirect を Rails で使う

 Nginx で認証つきで大容量ファイルをダウンロードさせたい時は、X-Accel-Redirect を使います。Rails のファイル操作を介さず、Nginxで直接クライアントへデータを送れます。Lighttpd とか Apache でいうところの、X-sendfile と同じような感じでしょうか。
Lighttpd の場合はこちら -> Rails&Lighttpdで大容量ファイルをダウンロード【変なヤバいもんログ内】

 ヘッダに、X-Accel-Redirect でパスを指定して、そのパスを Nginx 側で受けるだけですinternal って付けるのがポイントです。そうすると、このように内部からしか参照できなくなります。外からそのパスを叩いても見れないということになります。

 Rails2以前の場合は自分でヘッダに値を設定する必要があり、Rails3以降は設定を変更するだけでいけるようです。

# Rails2以前
def download
  # 認証処理などを挟むことができます。  
  path = "/bigfiles/long_movie.mp4"
  response.headers['X-Accel-Redirect'] = path
  response.headers['Content-Type'] = "application/force-download" 
  response.headers['Content-Disposition'] = "attachment; filename=\"#{File.basename(path)}\"" 
  response.headers['Content-length'] = File.size(RAILS_ROOT + path)
  render :nothing => true
end
 
# Rails3
def download
  # 認証処理などを挟むことができます。  
  path = "/bigfiles/long_movie.mp4"
  send_file path, 
             :type => "application/force-download",
             :disposition => "attachment; filename=\"#{File.basename(path)}\""
end
 
# config/environments/production.rb
# 下記行をコメントアウト (Apache, lighttpd の場合はこの行を使う)
# config.action_dispatch.x_sendfile_header = "X-Sendfile"
# 下記の行を有効にする (nginxの場合はこの行を使う)
config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'
location / {
    client_max_body_size    200m;
    proxy_read_timeout 60;
    proxy_connect_timeout 60;
    proxy_pass http://railsapp;
}

location /bigfiles {
    # -> /path/to/railsroot/bigfiles
    root /path/to/railsroot/;
    internal;
}

gist に上げてあります。https://gist.github.com/708959

補足

 逆に、ちょっとしたアプリを動かすときに、いちいち nginx の設定とかせずに Unicorn とかから直接ファイルを送りたい場合もあります。Rails3になってから、config/environments/productioin.rb がデフォルトのままだと send_file すると 0bytes になってしまいます。

 下記の2行をコメントアウトしておくと、Unicorn なりで直接ファイル送信ができるようになります。

# config.action_dispatch.x_sendfile_header = "X-Sendfile"
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'

 ついでに、静的ファイルも Unicorn なりで表示したいような手抜き構成のときは、下記の設定もお忘れなく。

config.serve_static_assets = false
Nginx の X-Accel-Redirect を Rails で使う

nginx で SSL振り分け

 ログインページや管理画面など、セキュアにしたいページにだけSSLをかける。というのを nginx でやる方法。

 たとえば、/admin 以下、 /login 以下をSSLにして、それ意外のページは非SSLにしたい場合です。細かい設定は端折って、upstream appserver でバックエンドのサーバが設定されてるときです。

    # HTTP setting
    server {
        listen       80;
        server_name  localhost;
 
        location ~ ^/(admin|login) {
            rewrite ^(.*) https://$http_host$1;
            break;
        }
 
        location / {
            proxy_pass http://appserver;
        }
    }
 
    # HTTPS setting
    server {
        listen       443;
        server_name  localhost;
 
        ssl                  on;
        ssl_certificate      server.pem;
        ssl_certificate_key  server.key;
        ssl_session_timeout  5m;
 
        ssl_protocols  SSLv2 SSLv3 TLSv1;
        ssl_ciphers  ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
        ssl_prefer_server_ciphers   on;
 
        location ~ ^/(admin|login) {
            proxy_pass http://appserver;
            break;
        }
 
        location / {
            rewrite ^(.*) http://$http_host$1;
        }
    }

 こんな感じにすると、http://host/login とかでアクセスが来ると、 https://host/login にリダイレクトされます。逆に、 https://host/top でアクセスが来ると、http://host/top にリダイレクトされます。

 break; とすると、それ以降のマッチングは行われないので、書いとくといいですね。

nginx で SSL振り分け

nginx の後ろで thin を unix socket で動かす

Railsを起動するとき、いつもPoundの後ろで複数ポートで振り分けっていうのをよくやっていたのだけど、unix socket で nginx の後ろで thin を動かすと早いらしいとのことで、試してみた。

環境はOSX

thinのインストール

$ sudo gem install thin
$ sudo gem install eventmachine --source http://code.macournoyer.com

2行目は、thinをsocket指定して動かす時に eventmachin の 0.11.0 以上が必要とのことで、OSXでデフォルトで入らなかったので入れた。

インストールはこれで完了。

nginxのインストール

Portで入るのが 0.5.1 ?とかで、ちょっと古かったので公式サイト(http://nginx.net/)から現時点で安定版の 0.6.31を取得してインストール

$ wget http://sysoev.ru/nginx/nginx-0.6.31.tar.gz
$ tar xvzf nginx-0.6.31
$ cd nginx-0.6.31
$ ./configure --prefix=/usr/local
$ make
# make install

で、無事インストールが終わるはず。configure で pcre チェックされてたので、 pcre とか入ってない場合はいれたほうがよさげ。VirtualHostの設定するときに使われるのかな?

nginxの設定

configure のオプションで –prefix=/usr/local とした場合、設定ファイルは /usr/local/conf/nginx.conf に生成されていて、デフォルトでこれを読みに行きます。

編集します。最低限の設定だけ。

worker_processes  1;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
 
    sendfile        on;
    keepalive_timeout  0;
 
    upstream backend {
        server unix:/tmp/thin.0.sock;
        server unix:/tmp/thin.1.sock;
        server unix:/tmp/thin.2.sock;
        server unix:/tmp/thin.3.sock;
    }
 
    server {
        listen       80;
        location / {
            proxy_pass http://backend;
 
        }
    }
}

これで、80番ポートにきたものを、unix socket の /tmp/thin.[0-3].sock で動いてる何かに転送出来るようになりました。

以下のように、-t をつけて設定ファイルの文法チェックをします。OKなら以下のようなメッセージが出ました。

$ sudo /usr/local/sbin/nginx -t
2008/06/25 01:05:12 [info] 14511#0: the configuration file /usr/local/conf/nginx.conf syntax is ok
2008/06/25 01:05:12 [info] 14511#0: the configuration file /usr/local/conf/nginx.conf was tested successfully

thinの設定(起動)

Rails でしか使ったことないので、以下 Rails アプリのディレクトリ直下で叩いてくだしあ。コマンドに引数渡すだけだけど、先ほど nginx.conf で指定した unix socket で動かす。

* s は サーバ4つで、–socket で /tmp/thin.sock とするとサーバの数だけ /tmp/thin.n.sock が出来るようだ。

$ thin start -s 4 --socket /tmp/thin.sock

nginxの起動

以下のコマンドで起動します。ぼくの環境だと、何も標準出力されずに起動しました。起動したら lsof -i:80 とかして80番で立ち上がってることを確認します。

$ sudo /usr/local/sbin/nginx

これで動いてるようだ。

この状態で、裏を切ってみる。Rails だと、thin の pid ファイルが tmp/pids/ 以下に出来ますんで、今 /tmp/thin.[0-2].sock で動いてるところを切ってみます。

$ kill cat `tmp/pids/thin.0.pid`
$ kill cat `tmp/pids/thin.1.pid`
$ kill cat `tmp/pids/thin.2.pid`

これで3だけが動いてる状態になりましたが、正常につながっているようです。ちゃんと振分けられてるみたいですねー。

Pound に比べていいなって思うところが、バックエンドが落ちたときの挙動です。Pound は Alive で指定した秒数でポーリングでバックエンドを死活監視をしてるみたいなので、後ろが落ちちゃったあと次のポーリングまではエラー画面が出てしまいます。

nginx で今回試してみた感じ、後ろが落ちても前にエラーが表示されることはないみたい?全部落ちると、もちろんエラー画面が出ますけど。

参考URL: http://wiki.codemongers.com/Main (nginx の wiki)

中国行ってるとき、家のサーバ落ちたら終わりかと(‘A`)

nginx の後ろで thin を unix socket で動かす