2017年にElixirを仕事で使った振り返り

少し遅くなったけど2017年にElixirを使った振り返りをしてみる。(年末年始ダラダラしてて書くの遅れた )

以下のElixir環境まとめエントリーに触発されました

Elixirは2016年ごろから触っていた (opens new window)のだけど2016年後半から2017年は仕事としてもほぼフルタイムで使っていたため、使ってるうちによかった点や辛かった点、開発環境など振り返ってみる。

# よかったところ

# 堅牢性(robustness)

これは本当に頑強でErlang VMが直接的な原因でサーバが落ちたということは本番運用も含めて1年ぐらい稼働させてる中でなかったと思う。可用性nine nines(99.9999999%)はガチ。逆にやることなさすぎて暇になるやつ

# エコシステム

ここ1, 2年で大分充実した。Hex.pmに登録されているパッケージの数は2016年2月の段階で1455個 (opens new window)だったのが2018年1月現在5702個になった。約1年10ヶ月で約380%の増加。以前は主要なWebサービスでもAPIクライアントがないというようなこともあったけど最近はほぼないんじゃないんだろうか。

またPhoenix 1.3 (opens new window)がリリースされた. ディレクトリ構造が変わったりContextという概念の導入 (opens new window)などもあり若干脱Railsっぽくなってきている。既存のディレクトリ構造のままアップデートも可能なのでそのままアップデートして新しい機能を使うもよし、Phoenix 1.3 styleで始めるもよし。

以前言われていたほどパッケージが少ない問題はなくなってきていると思うのでそこが懸念点になる場合も今はあまりないと思う。

# コミュニティ

ここ1年半ほどでゆるやかに成長してきた。

以下のグラフはRedditの言語別subredditのsubscriber数の統計だが、恐らく同じようなパラダイムで領域もそこそこ被ってると思われるClojureやScalaと比較して後発ながら伸び率も悪くない。

Imgur

急に人口が増えたら増えたでバックグラウンドの違うステークホルダーが増えてコミュニティが混乱する気がするのでこのぐらいの伸び率でちょうどいいと思う。

# 生産性

最初はElixirを部分的に使って管理系の画面はPHPなどで作ろうかと考えていたけどPhoenixの生産性がほぼ他のFWと変わらなかったので結局管理系も含めて全てElixirで作っているぐらいには問題ない

# 楽しい

Elixirはたのしい(語彙力)

# つらかったところ

逆に使っているうちにつらかった点など

# コンパイル時間

通常Elixirはコンパイル時に変更のあったファイルのみ再コンパイルされるのだけど、変更のあったファイル以外にも依存のあるファイルが再コンパイルされる。小さなライブラリレベルでは問題ないのだけどPhoenixを利用したWeb系の開発ではファイル数も多くなってきてコンパイル時間が大きくなるのが地味に効いてくる。

ちなみに再コンパイルの基準はcompile-time dependencyが存在するかどうかで決まる。

例えば

  • import, require を使用したとき
  • Structを使用したとき

compile-time dependencyが追加され再コンパイル対象が増える。

Elixir 1.6でmix xref graph --format statsでcompile-time dependencyの多いファイルを確認出来るようになる (opens new window)のでCIなどでチェックしてあげるといいと思います

参照: Understanding Elixir's recompilation (opens new window)

# 他の言語を使った時

デメリットというかパターンマッチングがないと辛くなるようになってしまった(?)

# 開発環境

# コーディング規約

Elixir 1.6のcode formatter (opens new window)を使っている。1.6.0-devの開発版に既に入っていたのでローカルとCIでのチェック用に1.6.0正式版に先行して導入したけど最高だったのでみんな使いましょう。コードレビューでコーディング規約に関する指摘をしなくてよくなるだけで大分脳の負荷が違います。

# テスト

特に特別なことはしてないけどExUnitとhound (opens new window)でE2Eテストをしている

PhantomJSをバックエンドにE2Eテストしているけど挙動が微妙にブラウザと違ったりdeprecatedになっているのでそろそろHeadless Chromeに置き換えたい

カバレッジは取ってないけど体感大体8割くらいはテスト書いてる

# デプロイ

ローカル: docker + docker-compose

本番: ECS + Distributed Erlang

たぶんElixirで一番悩むところがデプロイだと思うけど、開発/本番一致 (opens new window)の原則のためDockerでデプロイしている。ベースイメージ (opens new window)はpublicでDockerHubに公開しローカルと本番で共通のベースイメージを使っている。

CIはCircleCIでテスト、コンパイル、docker imageの作成とプライベートなDockerHubのリポジトリへのpush、ECSのタスク更新を行う。

ちなみにDistillery (opens new window)は使っていない。Dockerを使っている以上リリースのたびにプロセスが再起動されるので長期間に渡って起動されるようなプロセスもなくremote consoleを使ってプロセスのstateまで閲覧してデバッグしないといけないような状況も発生しづらくメリットも薄いと考えたからだ。OTP releaseは使わずコンパイル済みのソースを含めたイメージ内でmixでサーバを起動している。今のところ起動中のプロセスのstateまで確認しないと分からないようなエッジケースには出会ったことはないが、そのようなケースになった時はremote console用にOTP releaseにしてみたい。

あとしばらくPhoenixのChannelのPubSubのアダプターとしてRedisを使っていたが無駄に単一障害点や管理ポイントを増やしたくないので分散Erlangクラスタを組んでPG2 (opens new window)をアダプターとして使っている。RedisをPubSubサーバとして使っていた頃は少しレイテンシがあったりRedisサーバの負荷によってはPubSubが不安定になったりしたけどPG2にしてからは全くそういうのがなくなった。

余計な不確実性を持ち込まないという意味でも言語のランタイムレベルで問題を解決出来るというのは精神衛生的にも良い。

コンテナ起動時にどのようにしてコンテナ内のプロセスをDistributed Erlang(Elixir)クラスタに参加させるかについては、ERL_AFLAGSでVM間通信に使うポートを固定しコンテナ起動時のポートマッピングで固定したポートへのudp, tcpパケットを通している

ERL_AFLAGS="-name app@$ERL_HOST \
  -setcookie $ERL_COOKIE \
  -kernel inet_dist_listen_min 4370 \
          inet_dist_listen_max 4370"

以下はECS task definition。4369はepmdが固定で使うポートで4370はepmdが動的に割り当てるポートを固定で指定したもの。

〜中略〜
"portMappings": [
	{
		"containerPort": 4369,
		"hostPort": 4369,
		"protocol": "tcp"
	},
	{
		"containerPort": 4369,
		"hostPort": 4369,
		"protocol": "udp"
	},
	{
		"containerPort": 4370,
		"hostPort": 4370,
		"protocol": "tcp"
	},
	{
		"containerPort": 4370,
		"hostPort": 4370,
		"protocol": "udp"
	}
]
〜中略〜

サービスディスカバリはpeerage (opens new window)でカスタムのディスカバリを作成しredisにexpire付きでEC2のmetadataから取得した自身のホストの(コンテナではない)プライベートIPアドレスを保存して定期的にホストの一覧を走査して取得している。

なぜこうしているかというとEC2上ではmulticast udpが基本使えず(Weave (opens new window)などでoverlay networkを構築する方法もあるがオーバーヘッドが大きい)またgossipプロトコルを使うにもdockerコンテナのnetwork modeをhostにしなければならない→network=hostにするとlinkオプションが使えない→分離したnginxコンテナからlink出来ないという状態になるので、半自動的なクラスタリングはせずに地道にサービス一覧を取得し、それぞれのホストに対して定期的にNode.connetct/1するようなディスカバリを書いている

long-running processを作れたりHot Upgrade出来るところがBEAMの強みでもあるけどdockerを使うことでデプロイの度にコンテナは破棄されるのでその利点は失われることは覚悟しないといけない。とはいえ実際にその機能は切り札のようなものでHot Upgrade自体のテストやロールバック等も考えると大半の場合は無停止でのアップグレードなどはインフラを含めたアプリケーション全体のアーキテクチャで吸収した方がいいとは思うのでElixirは基本コンテナの起動・停止で影響の出るようなステートを保持しない方針でdockerでワンバイナリのように扱っている。

# 作ったパッケージ

分量が少ないので1年程使う中でお仕事的に必要になったり個人的な興味で作ったパッケージが10個 (opens new window)に達したので感想など

# cdn (opens new window)

Elixirで初めて作成したパッケージ。

Laravelのcdn (opens new window)というパッケージをportした。 S3に特定のディレクトリ(priv/assetsとか)を更新時や差分等を考慮してアップロード出来る。またCloudFrontから配布するためのパスを生成出来る 例: cdn(static_path(conn, "/css/main.css"))

# plug_rate_limit_redis (opens new window)

rate limitをredisをデータストアにplug (opens new window)で実現するパッケージ

# Usage

defmodule MyApp do

  plug RateLimit, interval_seconds: 60, max_requests: 30

  def index(conn, _params) do
	conn
	|> render(:index)
  end
end
  • 自作plugの作り方を学ぶ

# payjp (opens new window)

PAY.JP (opens new window)のAPIクライアント。公式のクライアントライブラリがなかったので作成。 Stripe (opens new window)のクライアントを参考にした。

# Usage

customer = [
  email: "test@test.com",
  description: "An Elixir Test Account",
  metadata: [
    app_attr1: "xyz"
  ],
  card: [
    number: "4242424242424242",
    exp_month: 01,
    exp_year: 2020,
    cvc: 123,
    name: "Joe Test User"
  ]
]

Payjp.Customers.create(customer)
  • APIクライアントの作り方を学ぶ
  • 外部APIのテストにはExVCR (opens new window)を使うとリクエストを再現して実際叩かないようにしてくれるので便利

# paidy (opens new window)

Paidy (opens new window)のAPIクライアント。同じく公式にクライアントライブラリがなかったので作成。

# ex_line_pay (opens new window)

LINE PAY (opens new window)のAPIクライアント。同じく類似パッケージが(ry

APIクライアントは一度ベースを作るとあとはエンドポイントとモジュールをちょっと調整すれば大体似通った作りになるので楽ですね。

# fcmex (opens new window)

FCM (opens new window)(Firebase Cloud Messaging)のAPIクライアント

FCM側にRate Limitがあり1リクエストあたり1000件までしかデバイスTokenを送信出来ないのと、短時間で大量のプッシュ通知を送信出来るようにFlow (opens new window)で流量を考慮しつつ並列度、CPU効率を考えて送信出来るようにした。

# activity_log (opens new window)

ロギング周りの実装でActivity Streams (opens new window)風のスキーマを定義したくてDSLが欲しくなったのでmacroで実装。 EctoのSchema風にしたかったのでEctoを参考にしたりした。

# Usage

# スキーマ定義
defmodule MyApp.Activity.Article do
  use ActivityLog

  activity "create" do
    actor  :user
    object :article
  end

  def name(actor, object), do: "#{actor.name} created #{object.name}"
end

# ログ出力
iex> alias MyApp.Activity.Article
iex> activity = %Article{actor: %Article.Actor{id: 1, name: "foo"}, object: %Article.Object{id: 1, name: "My article"}}
iex> ActivityLog.add(activity)
# outputs
05:29:32.128 [info]  {"type":"create","target":null,"object":{"type":"article","name":"My article","id":1},"name":"foo createed My article","actor":{"type":"user","name":"foo","id":1},"@timestamp":"2017-10-15T20:29:32.128192Z","@context":"https://github.com/shufo/activity_log"}
:ok
  • Macroの強力さと諸刃の剣さを理解
  • でもやっぱDSL便利

# plug_cache (opens new window)

特定のリクエストパスに対するレスポンスをキャッシュするplug。 ETSでインメモリでキャッシュを保存するからキャッシュサーバ等は不要。分散Erlangクラスタを組んでいる場合は分散キャッシュを使ってクラスタ全体で一意なキャッシュのinvalidationなども出来る。

# Usage

defmodule MyApp.PageController do

  plug PlugCache, ttl: 86400 when action in [:index]

  def index(conn, _params) do
    conn
    |> render(:index)
  end
end
  • ETS便利

# plug_maintenance (opens new window)

メンテナンス状態を取得して503 Service Not AvailableのHTTP Statusを返すplug

# plug_robots (opens new window)

robots.txtを平文で返すplug。作った理由忘れたけどたぶんrobots.txtをapplicationサーバから返したいみたいな感じだったと思う

と、こんな感じで微力ながらElixirコミュニティに何らかのContributionが出来ればと思ったのと必要性にかられて何個か作成したけど、やっぱり言語を学習するのになんらかのパッケージを作るのは一番の近道だなと。

# お世話になってるパッケージ

普段お世話になってるパッケージ

# ecto (opens new window)

# ja_serializer (opens new window)

# hound (opens new window)

  • E2Eでのテストをするため。PhantomJSをバックエンドとして使っていたけどdeprecatedになってしまったのでそろそろHeadless Chromeに移行したい

# ex_machina (opens new window)

  • テストデータのFactoryに

# canary (opens new window)

  • Authorizationに

# sentry (opens new window)

  • Sentry (opens new window)公式でElixirのクライアントライブラリが提供されているのでエラートラッカーはこれを使っている

# benchee (opens new window)

  • 実装に困ったらマイクロベンチマークで適宜ベンチマークを測って指標にする

# phoenix_swagger (opens new window)

# logster (opens new window)

  • アクセスログをワンライナーでJSONで出力出来る
  • アクセスログをCloudwatch LogsからKinesis, S3, Athenaなどに送り込むため最初からJSON形式でログを出力したかったのでこれを使っている
  • ちなみにJSON形式でログを出力するとCloudWatch Logsのフィルタで{ $.type = 'foo' }のような形で検索ワードを指定出来て便利

# scrivener (opens new window)

  • ページングライブラリ。最初自前でページャなどを書いたけど辛かったので早く知りたかった

# まとめ

最初は社内で自分一人だけだったElixir開発者も、実績がたまったおかげで他プロジェクトでも使われるようになって社内で5人ほど使うようになったり、周りで使われている会社も増えてきたりでなんだかんだゆるやかな成長を感じる。

少し前はミーハーでHypeな感じもあったけど最近は落ち着いて実際使う人は粛々と使ってる感じで個人的には居心地がいいです。