はじめに
これは 【その1】ドリコム Advent Calendar 2015 の19日目の記事です。
18日目はYさんの記事でした。
【その2】ドリコム Advent Calendar 2015の18日目はwasbi01さんの記事でした。
寺社自社で開発/運用している、Elixirを利用した広告配信システムについて紹介したいと思います。
自己紹介
@ohrdev
普段は写経(仏教的な意味で)や仏像彫り、寺社仏閣巡りをしています。
空いた時間はドリコムという会社で広告周りのシステムの開発をしています。
好きなbehaviourはgen_eventです。
Elixirについて
ElixirはErlangのVM上で動作する比較的新しいプログラミング言語です。
Erlangで実装されている為、分散、耐障害性、ソフトリアルタイムといった(Erlangの)特徴を兼ね備え、Elixir独自の、マクロ、メタプログラミング、ポリモーフィズムといった拡張機能を持っています。
採用背景
実装言語はいくつか候補(Ruby,Go,Scala,etc)がありましたが、API周りは特に要件として、
- 無停止稼動が可能
- 障害耐性がある
- スケールし易い(スケールの程度が予想し辛い)
があった為、「ああ、あれだな」「あいつだ」「例のアレね」という事で、Erlangを採用する事となりました。
が、Erlangを導入するとなった時はだいたい、
- Erlangのシンタックスがつらい(LL畑の人からの評判はあまりよくない印象です)
- そもそもErlangエンジニアの確保ができない
- 知見/情報が少ない
みたいな意見が出るのではないでしょうか。(実際、同様の意見が出ました)
ですが、
-
Erlangのシンタックスがつらい
|> Elixirのバージョン1.0がリリースされた
|> Erlangのシンタックス以外で(ある程度安定した言語で)書ける -
そもそもErlangのエンジニアの確保ができない
|> じゃあ他の言語のエキスパートが確保できるのか?
|> 結局言語の問題では「無い」 -
知見/情報が少ない
|> 英語ならそれなりにある
|> 日本語じゃなくておk -
性能面は大丈夫?
|> ベンチを計測した
|> ErlangとElixirでほぼ性能差は無い
というのがElixirを採用した理由です。
使い方
APIサーバー部分でElixirを利用しています。
管理画面はRails、非同期処理部分はSidekiqによる実装です。
APIサーバー
小さな(軽量な)リクエストを大量に捌く用途で、大きな(重い)リクエストはあまりありません。(重いリクエストはざっくりと全体の3%程度です)
maruというgrapeのElixir実装(APIのDSL)を利用して実装しています。
バックエンドDBはRedisで、コネクション管理はErlangのpoolboyを使って自前実装しています。
重い処理はどうしている?
軽量な処理はAPIサーバーで処理し、複雑なビジネスロジックが絡む重たい処理はElixirからExqを使ってSidekiqに直接ジョブをenqueueしています。
Exq
ExqはSidekiqのElixir実装です。
ExqはResque/Sidekiqと互換性があるので、ExqからResque/Sidekiqにジョブのenqueueができます。
Enqueue側
enqueueはExqのExq.Enqueuer.enqueue
を使って行います。
defmodule MyApp.Job do
@pid :exq_enqueuer # Exqのプロセス名
@queue_name "my_queue" # Enqueue対象のキュー名
@worker "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
def enqueue(args) do
Exq.Enqueuer.enqueue(@pid, @queue_name, @worker, args)
end
end
@pid
は後述のExqのプロセス起動時にsupervisorで指定するプロセス名です。
@queue_name
はenqueue対象とするキュー名です。
@worker
はActiveJobのAdapterのクラス名です。
SupervisorでExqを起動する際、以下の様になります。
defmodule MyApp.Supervisor do
use Supervisor
def start_link do
Supervisor.start_link(__MODULE__, [], name: :my_app)
end
def init([]) do
children = [[
host: "myappredis.com", # host
port: 6379, # port
namespace: "my_job", # namespace
queues: ["my_queue", "another_my_queue"], # target queues
name: :exq_enqueuer]] # process name
supervise(
children,
strategy: :one_for_one,
max_retstarts: 30,
max_seconds: 5)
end
end
host
はバックエンドDBのRedisのホスト名です。
port
はバックエンドDBのRedisのポート名です。
namespace
はバックエンドDBのRedisのネームスペースです。
queues
はExqで処理対象とするキュー名のリストです。
name
はExqのプロセス名です。
Dequeue側
ExqでenqueueされたJobは、Sidekiqで以下のクラスでdequeueされ処理が行われます。
queue_asで、MyApp.Jobで指定されたMyApp.Job
のキュー名を指定します。
class MyAppJob << ActiveJob::Base
queue_as :my_queue # MyApp.Jobの@queue_nameで指定されたキュー名
def perform(args)
# my awesome heavey job
end
end
また、Redisの設定を以下の様に指定しています。
...
# connection設定
redis_conn = proc do
Redis::Namespace.new(
"my_job", # Supervisorのnamespaceで指定したnamespace
redis: Redis.new(url: "myappredis.com:6379/0"), # Supervisorのhost,portで指定したRedisのURL
)
end
# server設定
Sidekiq.configure_server do |config|
config.redis = ConnectionPool.new(size: ENV['REDIS_SERVER_POOL_SIZE'], &redis_conn)
...
end
# client設定
Sidekiq.configure_client do |config|
config.redis = ConnectionPool.new(size: ENV["REDIS_CLIENT_POOL_SIZE"], &redis_conn)
...
end
...
redis_conn
のnamespace、urlには、ExqのSupervisorで指定した namespace
、host
、port
に対応したパラメータ値を設定します。
ちなみに、SidekiqからExqへのジョブのenqueueも当然できます。(が必要がなかったのでやっていませんが)
使ってみた所感は?
4ヶ月ほど運用してみましたが、今の所大きな問題は出ていません。
Sidekiqのダッシュボードは以下の様になっています。
4ヶ月で処理した処理が重めのジョブ数はこんな感じです。これらは、ほぼ全てがExqからenqueueされたものです。
半年の遷移を見ると、リリース時に比べるとかなり増えている事が見て取れます。
スケールに関してですが、リリース日から今現在までで、サービスの規模がDAUベースで30倍程になっていますが、その為の構成変更や特別なチューニング等は行っておらず、単純にサーバーのコア数や台数の変更のみで対応しました。
まとめ
広告の配信システムのAPI部分をElixirで実装/運用しました。
Exqを使うことでSidekiqとキューのやりとりを実現しました。
参考資料
地獄のElixir入門
Elixirを本番環境で使ってみたという事例紹介
おまけ
来年、2016/1/11にElixir Meetup #1 in Drecomのイベントを企画しています。
そこで、Elixirを使ってアプリを実際にプロダクション運用した際の地雷とその処理内容について話したいと思います。
次は
【その1】ドリコム Advent Calendar 2015 の20日目はNarazakaさんの記事です。
【その2】ドリコム Advent Calendar 2015の20日目はjizojpさんの記事です。