リアルタイムウェブな観点からElixir / Phoenix について

  • 638
    いいね
  • 3
    コメント

ここ最近、
[翻訳] Elixir - 次に来る大物Web言語 - Qiita とか 超高速なJSON APIをElixirフレームワークのPhoenixでビルドしてテストしよう | POSTD を読んで気になっていたので、勉強していた。

前提: 自分はシングルページアプリケーションに特化したフロントエンドエンジニアであり、サーバサイドのプロダクション運用にはそこまで強くない。あとこれはここ2日の勉強した日記でもあり誤解や勘違いも多々あると思う。

リアルタイムウェブアプリケーションのためのサーバー

Railsの次の時代、リアルタイムウェブの為のウェブフレームワークがあるとしたら、次のような特長をもつと思う。

  • HTTP, HTTP/2. WebSocket等のプロトコル対応と抽象化
  • JSON APIに特化
  • 認証系
  • キャッシュ管理
  • Viewに関心がない

リアルタイムウェブは、その言葉をどう定義するかによるが、SPAでイニシャルビューから動的に動くコンテンツを配信する用途として使うならば、その結果サーバーサイドレンダリングは最小限、というかむしろindex.htmlを返却するだけになるのではないか。

複雑なフロントエンドを前提にするなら、フロントエンドはサーバーサイドで行われたレンダリング過程について知りようがなく再現もできないので、サーバーサイドでHTMLは組み立てない方がよい。むしろ邪魔であるとも言える。

エコシステムの刷新

SPAが前提になる時、これはいくつかの「メジャーではない」言語にとって追い風になる。実装コストが高く、かつメンテナンスも難しいView層をすっ飛ばしてJSONを喋っていればいいので、アプリケーションとしてのコアドメインに注力できる。ここ近年のマイクロサービスに移行する傾向も同様に追い風だろう。

その分フロントに押し付けてフロントが苦しむことになるが、各々が得意なドメインを担当することで苦しみの総量は減るはずだ、という見通しがある。サーバーサイドの人間はどんどんフロント側にViewの仕事を押し付けて欲しい。フロントエンドは苦しむがよい。それは本来お前がやるべきだった仕事だ。

node.jsとリアルタイムウェブ

本来、「リアルタイムウェブ」という目的はnode.jsがクリアすべき問題ではあった。

現状のnode.jsの最大の問題は、言語特性上の問題として安定運用に向いた言語ではないことにあると思う。c10kはクリアできるかもしれないが、c10k以外の問題の対処には、他の言語より優れた点があるとは言いがたい。他によく言われるメリットの、JavaScriptを「書ける」人間の採用・調達は一件楽にみえるが、サーバーサイド言語としての要求に応えられるようなJavaScriptを書けるように教育するには かなりの時間がかかる。

フロントエンドでJavaScriptを使うのは、選択肢がないので仕方ないとはいえ、他の選択肢がある中でサーバーサイドで選ぶかというと、javascriptが得意な自分でもかなり微妙な選択肢になる。一昔前は socket.ioというキラーアプリがあったが、モダンブラウザはwebsocketを一通り実装し終えており、年々、現時点でのsocket.ioの必要性は弱まっている。

ここで気にすべきはサーバサイドレンダリングだが、GoogleはJavaScriptによってレンダリングされたものもクロールしはじめているので、将来的にはまったく不要になるかもしれない。現在でもクローラの機嫌次第だがちゃんとインデックスされはじめている。とはいえ完全にGoogle頼みなので歯がゆさはあるが…。

じゃあ何を選ぶか

求めるものは、堅牢性、応答性、そして規模が膨らんだ時のメンテナンス性だ。ウェブアプリケーションフレームワークという特性からして最終的なボトルネックはインフラ側に起因するので、言語そのものの速度はそこまで重要ではない(が、無視するわけではない)。

オンメモリでの応答性、そして障害耐性という面で、ここ数日ElixirのPhoenixが選択肢になるんじゃないかと思って検証していた。

  • disposableなプロセスモデル
  • Erlang/OTP
  • 高い信頼性
  • それなりに速いサーバー(cowboy)

Erlangの堅牢性についてはよく言及されるので解説は他に譲るが、Erlang自体はあまり一般的ではないやや特異な文法をもつ言語なので、そしてそれが普及を妨げていると思われる。で、その欠点をカバーするのがElixirである、という認識をしている。

ElixirはRuby風の文法のガワを被せ、かつErlangに特徴的なパターンマッチを自然に折り込み、またマクロも使えてその筋の人にも楽しめる。ElixirはErlangVMで動くコードにコンパイルされ、そして自然にErlangの機能を呼び出せる。

ここ数日勉強した限り、Elixir, というか Erlang の基本的なスタイルは、何をやるにも軽量プロセスを作って、そのプロセス同士で通信する。軽量プロセスは簡単にクラッシュさせてもよく、そしてプロセスプールから再起動戦略に応じて新しいプロセスを生成するというもの。GCはプロセス単位で張り付いている、というのがよく言われるErlangVMの安全性の根拠だろう。

ErlangとGolangを比較してみる - Qiita

型はない。静的解析するツールはあった(dialyzer)が、どこまで信用していいか自分では判断がつかなかった。型安全よりもプロセスの安全性を重視した言語、というかVMのデザインなのだろう。

Phoenix

PhoenixはElixirで実装されたウェブアプリケーションフレームワークである。

Phoenix

雛形から生成したPhoenixのコードをみればわかるが、露骨にRailsインスパイアなフレームワークである。作者はRailsコミッタでもあるらしい。SPA的な観点で言えば不要なのだが、Scaffoldingもある。

雛形がこんな感じ

.
├── Procfile
├── README.md
├── config
│   ├── config.exs
│   ├── dev.exs
│   ├── prod.exs
│   ├── prod.secret.exs
│   └── test.exs
├── lib
│   ├── chat
│   │   ├── endpoint.ex
│   │   └── repo.ex
│   └── chat.ex
├── mix.exs
├── mix.lock
├── test
│   ├── chat_test.exs
│   └── test_helper.exs
└── web
    ├── channels
    │   └── room_channel.ex
    ├── controllers
    │   └── page_controller.ex
    ├── router.ex
    ├── templates
    │   ├── layout
    │   └── page
    ├── views
    │   ├── error_view.ex
    │   ├── layout_view.ex
    │   └── page_view.ex
    └── web.ex

で、ここからが言いたかったことなのだけど、Phoenixは0.10からChannelという機能があって、専用のJSと組み合わせて簡単にwebsocketによる通信が実現できる。node.jsにおけるsocket.ioにあたるレイヤーものと思えばよい。

channelを使うのは、たぶん最小構成に近いものだと次のコードのようになると思う。

room_channel.ex

defmodule Chat.RoomChannel do
  use Phoenix.Channel
  require Logger
  def join("rooms:lobby", message, socket) do
    Process.flag(:trap_exit, true)
    :timer.send_interval(5000, :ping)
    {:ok, %{messages: "joined"}, socket}
  end

  def handle_info({:after_join, msg}, socket) do
    Logger.debug "> join #{socket.topic}"
    broadcast! socket, "user:entered", %{user: msg["user"]}
    push socket, "join", %{status: "connected"}
    {:noreply, socket}
  end

  def handle_info(:ping, socket) do
    push socket, "new:msg", %{user: "SYSTEM", body: "ping"}
    {:noreply, socket}
  end

  def handle_in("new:msg", msg, socket) do
    broadcast! socket, "new:msg", %{user: msg["user"], body: msg["body"]}
    {:reply, {:ok, msg["body"]}, assign(socket, :user, msg["user"])}
  end

  def terminate(reason, socket) do
    Logger.debug"> leave #{inspect reason}"
    :ok
  end
end

JS側

let socket = new phoenix.Socket("ws://" + location.host +  "/ws")
socket.connect();
socket.chan("rooms:lobby", {})
.join("rooms:lobby", {})
.receive("ok", messages => {
  chan.push("msg:new", {user: 'foo', body: 'body'})
  chan.on("msg", ({message})=>{
    console.log('receive', message);
  });
});

chrismccord/phoenix_chat_example
を参考にしてコードを書いていたのだけど、babelを使ってるのは公式の例のままで、たぶんes6のDestructiveAssignmentのパターンマッチングがerlangユーザーと相性が良いのだと思う。

brunch.ioというフロントエンドでは最近ちょっと下火なビルドツールを公式で押してるみたいなんだけど、自分はgulpで書きなおした。のが下。↑のchat exampleはphoenix v0.11なんだけど、ついでにphoenix v0.13で動くように書きなおしてある。

mizchi-sandbox/phoenix-channel-example

Elixirに足りないもの

ライブラリ。メジャーなものはあるが、個人の趣味を吸収できるほどのものはない。

とはいえ、たぶん、サーバーサイド用途以外に興味をもつべきではないのだろう。実践 Erlang/OTP コトハジメ 2014.11 でも ErlangはネットワークサーバーのDSLと捉えるべきだ、と書かれていて、納得感があった。

とはいえ足りないものは自分で作ることになるんだが、elixirに付属するmixというパッケージ管理がよくできていて、いわゆるgemやnpmのようにモジュール管理を行う。Hex というリポジトリに登録されたものや、erlangのコードを依存にいれても呼び出せてそのまま便利だった。実際に使っていこうとすると、自分はライブラリをpublishしまくることになるだろう。

自分が他に試したこととして、erlang_v8を使ったReactのサーバーサイドレンダリングも問題なかった。そりゃまあv8がうごけば後は何でもできるんだけども。

最後に

これから全てのサーバーがSPAの為のサーバーになるとは言わないし、これまでと同じような用途ではRailsなりPlayなりを使えば良い。

とはいえある程度以上のリアルタイムウェブとそれによるUXを実現したければ、こういう選択肢を突き詰めるほうが快適だろうな、という気はしている。

これから先Elixirを続けるかはもうちょっと使ってから考える。仕事でちょうど一大websocketサーバー必要になる見通しなので、プロトで試すぐらいはしたい。