少し遅くなったけど2017年にElixirを使った振り返りをしてみる。(年末年始ダラダラしてて書くの遅れた
)
以下のElixir環境まとめエントリーに触発されました
- Elixir のチームでの開発環境について - Qiita
- エムスリーでの Elixir 開発環境 ver.2017 #m3dev - エムスリーテックブログ
- Elixir 開発環境 2017 in ACCESS - Qiita
Elixirは2016年ごろから触っていたのだけど2016年後半から2017年は仕事としてもほぼフルタイムで使っていたため、使ってるうちによかった点や辛かった点、開発環境など振り返ってみる。
よかったところ
堅牢性(robustness)
これは本当に頑強でErlang VMが直接的な原因でサーバが落ちたということは本番運用も含めて1年ぐらい稼働させてる中でなかったと思う。可用性nine nines(99.9999999%)はガチ。逆にやることなさすぎて暇になるやつ
エコシステム
ここ1, 2年で大分充実した。Hex.pmに登録されているパッケージの数は2016年2月の段階で1455個だったのが2018年1月現在5702個になった。約1年10ヶ月で約380%の増加。以前は主要なWebサービスでもAPIクライアントがないというようなこともあったけど最近はほぼないんじゃないんだろうか。
またPhoenix 1.3がリリースされた. ディレクトリ構造が変わったりContextという概念の導入などもあり若干脱Railsっぽくなってきている。既存のディレクトリ構造のままアップデートも可能なのでそのままアップデートして新しい機能を使うもよし、Phoenix 1.3 styleで始めるもよし。
以前言われていたほどパッケージが少ない問題はなくなってきていると思うのでそこが懸念点になる場合も今はあまりないと思う。
コミュニティ
ここ1年半ほどでゆるやかに成長してきた。
以下のグラフはRedditの言語別subredditのsubscriber数の統計だが、恐らく同じようなパラダイムで領域もそこそこ被ってると思われるClojureやScalaと比較して後発ながら伸び率も悪くない。
急に人口が増えたら増えたでバックグラウンドの違うステークホルダーが増えてコミュニティが混乱する気がするのでこのぐらいの伸び率でちょうどいいと思う。
生産性
最初は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の多いファイルを確認出来るようになるのでCIなどでチェックしてあげるといいと思います
参照: Understanding Elixir’s recompilation
他の言語を使った時
デメリットというかパターンマッチングがないと辛くなるようになってしまった(?)
開発環境
コーディング規約
Elixir 1.6のcode formatterを使っている。1.6.0-devの開発版に既に入っていたのでローカルとCIでのチェック用に1.6.0正式版に先行して導入したけど最高だったのでみんな使いましょう。コードレビューでコーディング規約に関する指摘をしなくてよくなるだけで大分脳の負荷が違います。
テスト
特に特別なことはしてないけどExUnitとhoundでE2Eテストをしている
PhantomJSをバックエンドにE2Eテストしているけど挙動が微妙にブラウザと違ったりdeprecatedになっているのでそろそろHeadless Chromeに置き換えたい
カバレッジは取ってないけど体感大体8割くらいはテスト書いてる
デプロイ
ローカル: docker + docker-compose
本番: ECS + Distributed Erlang
たぶんElixirで一番悩むところがデプロイだと思うけど、開発/本番一致の原則のためDockerでデプロイしている。ベースイメージはpublicでDockerHubに公開しローカルと本番で共通のベースイメージを使っている。
CIはCircleCIでテスト、コンパイル、docker imageの作成とプライベートなDockerHubのリポジトリへのpush、ECSのタスク更新を行う。
ちなみにDistilleryは使っていない。Dockerを使っている以上リリースのたびにプロセスが再起動されるので長期間に渡って起動されるようなプロセスもなくremote consoleを使ってプロセスのstateまで閲覧してデバッグしないといけないような状況も発生しづらくメリットも薄いと考えたからだ。OTP releaseは使わずコンパイル済みのソースを含めたイメージ内でmix
でサーバを起動している。今のところ起動中のプロセスのstateまで確認しないと分からないようなエッジケースには出会ったことはないが、そのようなケースになった時はremote console用にOTP releaseにしてみたい。
あとしばらくPhoenixのChannelのPubSubのアダプターとしてRedisを使っていたが無駄に単一障害点や管理ポイントを増やしたくないので分散Erlangクラスタを組んでPG2をアダプターとして使っている。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でカスタムのディスカバリを作成しredisにexpire付きでEC2のmetadataから取得した自身のホストの(コンテナではない)プライベートIPアドレスを保存して定期的にホストの一覧を走査して取得している。
なぜこうしているかというとEC2上ではmulticast udpが基本使えず(Weaveなどで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個に達したので感想など
cdn
Elixirで初めて作成したパッケージ。
Laravelのcdnというパッケージをportした。
S3に特定のディレクトリ(priv/assets
とか)を更新時や差分等を考慮してアップロード出来る。またCloudFrontから配布するためのパスを生成出来る 例: cdn(static_path(conn, "/css/main.css"))
plug_rate_limit_redis
rate limitをredisをデータストアにplugで実現するパッケージ
Usage
defmodule MyApp do
plug RateLimit, interval_seconds: 60, max_requests: 30
def index(conn, _params) do
conn
|> render(:index)
end
end
- 自作plugの作り方を学ぶ
payjp
PAY.JPのAPIクライアント。公式のクライアントライブラリがなかったので作成。 Stripeのクライアントを参考にした。
Usage
customer = [
email: "[email protected]",
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を使うとリクエストを再現して実際叩かないようにしてくれるので便利
paidy
PaidyのAPIクライアント。同じく公式にクライアントライブラリがなかったので作成。
ex_line_pay
LINE PAYのAPIクライアント。同じく類似パッケージが(ry
APIクライアントは一度ベースを作るとあとはエンドポイントとモジュールをちょっと調整すれば大体似通った作りになるので楽ですね。
fcmex
FCM(Firebase Cloud Messaging)のAPIクライアント
FCM側にRate Limitがあり1リクエストあたり1000件までしかデバイスTokenを送信出来ないのと、短時間で大量のプッシュ通知を送信出来るようにFlowで流量を考慮しつつ並列度、CPU効率を考えて送信出来るようにした。
activity_log
ロギング周りの実装でActivity Streams風のスキーマを定義したくて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
特定のリクエストパスに対するレスポンスをキャッシュする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
メンテナンス状態を取得して503 Service Not Available
のHTTP Statusを返すplug
plug_robots
robots.txtを平文で返すplug。作った理由忘れたけどたぶんrobots.txtをapplicationサーバから返したいみたいな感じだったと思う
と、こんな感じで微力ながらElixirコミュニティに何らかのContributionが出来ればと思ったのと必要性にかられて何個か作成したけど、やっぱり言語を学習するのになんらかのパッケージを作るのは一番の近道だなと。
お世話になってるパッケージ
普段お世話になってるパッケージ
ecto
- みんな大好き. Composed Queryとして書くと再利用性も高くシンプルな書き方が出来るのがすき。
ja_serializer
- JSON-API形式でAPIレスポンスを返すため
hound
- E2Eでのテストをするため。PhantomJSをバックエンドとして使っていたけどdeprecatedになってしまったのでそろそろHeadless Chromeに移行したい
ex_machina
- テストデータのFactoryに
canary
- Authorizationに
sentry
- Sentry公式でElixirのクライアントライブラリが提供されているのでエラートラッカーはこれを使っている
benchee
- 実装に困ったらマイクロベンチマークで適宜ベンチマークを測って指標にする
phoenix_swagger
- Swagger形式でAPIドキュメントを出力するため。そろそろOpenAPI 3.0仕様に準拠したい
logster
- アクセスログをワンライナーでJSONで出力出来る
- アクセスログをCloudwatch LogsからKinesis, S3, Athenaなどに送り込むため最初からJSON形式でログを出力したかったのでこれを使っている
- ちなみにJSON形式でログを出力するとCloudWatch Logsのフィルタで
{ $.type = 'foo' }
のような形で検索ワードを指定出来て便利
scrivener
- ページングライブラリ。最初自前でページャなどを書いたけど辛かったので早く知りたかった
まとめ
最初は社内で自分一人だけだったElixir開発者も、実績がたまったおかげで他プロジェクトでも使われるようになって社内で5人ほど使うようになったり、周りで使われている会社も増えてきたりでなんだかんだゆるやかな成長を感じる。
少し前はミーハーでHypeな感じもあったけど最近は落ち着いて実際使う人は粛々と使ってる感じで個人的には居心地がいいです。