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 で Let’s Encrypt 使う時の設定

Using Free SSL/TLS Certificates from Let’s Encrypt with NGINX

これに比べると Caddyを使う場合 が簡単すぎて泣ける。

追記

と思っていたが、 Let’s Encrypt は Certbot でものすごく簡単に設定できるようになった。

公式を見たら手順が分かりやすい。

インストール

wget https://dl.eff.org/certbot-auto
chmod a+x certbot-auto
./certbot-auto

とすると、必要なものが全部インストールされる。

証明書の発行

Nginx の場合、

./path/to/certbot-auto certonly

で、聞かれることに答えていくと、自動的に認証されて証明書が発行される。
この時、証明書を取得するドメインの80番ポートが空いていることが必要で、 webroot を指定する必要があるので事前に調べておくと良い。

もしくは、Nginxなどを使わない場合、スタンドアロンで80番ポートをリッスンするHTTPサーバになって認証をしてくれる模様。

./path/to/certbot-auto certonly --standalone -d example.com -d www.example.com

Let’s Encrypt は認証用の情報を、証明書取得予定のドメインの80番ポートの特定URLにおいて、Let’s Encrypt本家からそのURLにアクセスしにきて、認証OKとなったら証明書を発行してくれる仕組みのよう。

Nginx の設定

例えばこの ecpplus.net ドメインの場合、デフォルトでは下記の場所に証明書が出来る。(発行時に標準出力されてるメッセージを見れば書いてある)

server {
        listen 443 ssl http2;
        server_name ecpplus.net;

        ssl on;
        ssl_certificate     /etc/letsencrypt/live/ecpplus.net/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/ecpplus.net/privkey.pem;
...

cron の設定

一応90日の有効期限があるが、注意書きに1日2回の更新で、分をランダムにするように。というのがおすすめされてるので、そのように設定する。更新の必要がないときは、何もしないって書いてあった。

21 */12 * * * /usr/local/bin/certbot-auto renew --quiet --no-self-upgrade
22 */12 * * * /etc/init.d/nginx reload
Nginx で Let’s Encrypt 使う時の設定

Let’s Encrypt を Caddy で簡単に使う

Caddy という web server がデフォルトTLSというポリシーで、Let’s Encrypt を簡単に設定出来て、証明書の自動更新をしてくれるようだったので使ってみた。Let’s Encrypt は無料でTLS証明書使えるサービスです。

VultrのVPSを使ってみたら、DocumentsにCaddyのインストールについて載ってたので知りました。Vultr の Tokyo リージョン良さそうですね。

Caddy のインストール

https://caddyserver.com/download から、ダウンロードするとバイナリが落ちてくるので、それを実行するだけです。バイナリ生成時に、Select Features として、追加機能を入れる事ができます。

Caddy の設定

/path/to/caddy.conf

hoge.example.com {
  tls hoge@example.com
}

最小構成だと、ドメインに対して自分のメールアドレスを設定したら動くみたいです。ecpplus.net のドメインだと動いたけれど、 ecp.plus のドメインだと動かなかった。あまり調べていないけれど、Let’s Encrypt 側で何かやる必要がありそう。

Caddy の起動

80, 443 ポートをListenするので、setcap しておくか、sudo での実行が必要です。

sudo setcap cap_net_bind_service=+ep /path/to/caddy
/path/to/caddy --conf /path/to/caddy.conf -agree=true

-agree は、Agree to Let’s Encrypt Subscriber Agreement で、-emailは Default Let’s Encrypt account email address とのことです。

これで、 https://hoge.example.com としてサーバが立ち上がります。http でアクセスしても自動的に https に遷移します。

Caddy のコマンド引数

$ caddy --help
Usage of caddy:
  -agree=false: Agree to Let's Encrypt Subscriber Agreement
  -ca="https://acme-v01.api.letsencrypt.org/directory": Certificate authority ACME server
  -conf="": Configuration file to use (default=Caddyfile)
  -cpu="100%": CPU cap
  -email="": Default Let's Encrypt account email address
  -grace=5s: Maximum duration of graceful shutdown
  -host="": Default host
  -http2=true: HTTP/2 support
  -log="": Process log file
  -pidfile="": Path to write pid file
  -port="2015": Default port
  -quiet=false: Quiet mode (no initialization output)
  -revoke="": Hostname for which to revoke the certificate
  -root=".": Root path to default site
  -version=false: Show version

Caddy の機能

https://caddyserver.com/docs に、ドキュメントがあります。

気になった機能

  • markdown で、Caddy 自体が Markdown を HTML に変換してくれる。テンプレートの指定もできる。デフォルトで入ってるのは面白いかも。
  • gitで、git push で自動的にサイト更新出来るので、markdown と合わせてお手軽なサイトに便利そう。
  • gzip は使えるぽいですが、キャッシュ系の機能はまだないっぽいので、現状だと別で用意する必要がありそうです。
  • fastcgi で指定すれば、裏でWordpressとか動かしても大丈夫そうです。
  • proxy でリバースプロキシの設定もできるので、Rails とか裏においても大丈夫そうです。
  • websocket で、WebSocket のコネクションが張られたときに実行するコマンドが指定できる。
  • startup, shutdown で、起動・停止時に任意のコマンドを実行できる。例えば Rails のアプリケーションサーバを立ち上げるとか。

安いTLS証明書使ってたけど、失効したら乗り換えても良いかもという感じがしました。

Rails5 で使う場合

ActionCable 試してないけど、Websocket の設定も出来るので多分いけそう。とりあえず、 /assets /system だけ Caddy で返して、バックエンドは Unicorn とか Puma とかを 9292 番ポート動かしてる場合の設定例。proxy_header を適切に設定しないと、 redirect_to とかさせたときにおかしなことになるので注意。

example.com {
  gzip
  tls caddy@example.com
  log /var/log/access.log

  root /var/www/rails_root/current/public

  proxy / localhost:9292 {
    proxy_header Host {host}
    proxy_header X-Real-IP {remote}
    proxy_header X-Forwarded-Proto {scheme}
    except /assets /system
  }
}

Daemonize

caddy 自身ではデーモンとして起動出来ないようだ。 supervisord を使って daemon にしてみた。

/etc/supervisord.d/caddy.ini

[program:caddy]
command=/usr/local/bin/caddy -conf="/etc/Caddyfile" -agree=true
directory=/var/www/rails_root/current/public
autostart=true
user=root
redirect_stderr=true
stdout_logfile=/var/log/caddy.log
stderr_logfile=/var/log/caddyerr.log

/etc/supervisord.conf

[supervisord]
minfds=4096

caddy を起動するとき、少なくとも ulimit -n 4096 にしろと言われるので、supervisord の方で指定しておくと良い。

Let’s Encrypt を Caddy で簡単に使う

デバイスを使用してるプロセスを調べる

/home2 を umount しようとしたら、デバイスは使用中です。って出て umount 出来なかった。

という状態になったら lsof で調べられます。

# /usr/sbin/lsof /home2

とすると、 /home2 を使用しているプロセスの一覧が出ます。

他にも lsof は、ポートを使用しているプロセスを調べるのにも使えます。

たとえば、22番ポートを listen しているプロセスを調べるときは下記のようにすると分かります。

# /usr/sbin/lsof -i:22
デバイスを使用してるプロセスを調べる

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振り分け

monit5.1.1のインストール

 CentOS5.6 で、monit5.1.1 をインストールしようとしたら、make でエラーが出てしまった。

% ./configure --prefix=/usr/local
% make
** 中略 **
/usr/bin/ld: skipping incompatible /usr/lib/libpthread.so when searching for -lpthread
/usr/bin/ld: skipping incompatible /usr/lib/libpthread.a when searching for -lpthread
/usr/bin/ld: skipping incompatible /usr/lib/libpam.so when searching for -lpam
/usr/bin/ld: skipping incompatible /usr/lib/libcrypt.so when searching for -lcrypt
/usr/bin/ld: skipping incompatible /usr/lib/libcrypt.a when searching for -lcrypt
/usr/bin/ld: skipping incompatible /usr/lib/libresolv.so when searching for -lresolv
/usr/bin/ld: skipping incompatible /usr/lib/libresolv.a when searching for -lresolv
/usr/bin/ld: skipping incompatible /usr/lib/libnsl.so when searching for -lnsl
/usr/bin/ld: skipping incompatible /usr/lib/libnsl.a when searching for -lnsl
/usr/bin/ld: skipping incompatible /usr/lib/libssl.so when searching for -lssl
/usr/bin/ld: skipping incompatible /usr/lib/libssl.a when searching for -lssl
/usr/bin/ld: skipping incompatible /usr/lib/libcrypto.so when searching for -lcrypto
/usr/bin/ld: skipping incompatible /usr/lib/libcrypto.a when searching for -lcrypto
/usr/bin/ld: skipping incompatible /usr/lib/libc.so when searching for -lc
/usr/bin/ld: skipping incompatible /usr/lib/libc.a when searching for -lc

どうも、32bit と 64bit がごっちゃになってることが原因らしい。32bitのライブラリで64bitのコンパイルをしようとしている?のかな…?

Makefile をいじることで回避しました。

% ./configure --prefix=/usr/local
% vi Makefile

LIB = -lfl -lpthread -lpam -lcrypt -lresolv -lnsl -L/usr/lib -lssl -lcrypto

LIB = -lfl -lpthread -lpam -lcrypt -lresolv -lnsl -L/usr/lib64 -lssl -lcrypto

として、/usr/lib ではなく /usr/lib64 を使うように変更しました。これで無事コンパイルが通りました。

monit5.1.1のインストール