Elixirを使ってSidekiqを操作する

![ex-logo.png](https://qiita-image-store.s3.amazonaws.com/0/28174/13b7ea17-aac8-1168-f6f1-c63e88b6d4ae.png)![side-logo.png](https://qiita-image-store.s3.amazonaws.com/0/28174/9f39519c-df87-0334-b6ad-5dc6a2eca3d6.png)

# はじめに
これは [【その1】ドリコム Advent Calendar 2015](http://www.adventar.org/calendars/1043) の19日目の記事です。
18日目は[Yさんの記事](http://qiita.com/yuichi8/items/c701d755b066425487b0)でした。
[【その2】ドリコム Advent Calendar 2015](http://www.adventar.org/calendars/1044)の18日目は[wasbi01さんの記事](http://wasbi01.hatenablog.com/entry/2015/12/18/112558)でした。

~~寺社~~自社で開発/運用している、Elixirを利用した広告配信システムについて紹介したいと思います。

# 自己紹介
[@ohrdev](https://twitter.com/ohrdev)
普段は[写経(仏教的な意味で)](http://qiita.com/ohr486/items/bcd2c0513c82e909c3ef)や仏像彫り、寺社仏閣巡りをしています。
空いた時間はドリコムという会社で広告周りのシステムの開発をしています。
好きなbehaviourは[gen_event](http://www.erlang.org/doc/design_principles/events.html)です。

# Elixirについて
[Elixir](http://elixir-lang.org)は[Erlang](http://www.erlang.org)の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](http://sidekiq.org/)による実装です。

## APIサーバー
小さな(軽量な)リクエストを大量に捌く用途で、大きな(重い)リクエストはあまりありません。(重いリクエストはざっくりと全体の3%程度です）
[maru](https://github.com/falood/maru)という[grape](http://www.ruby-grape.org/)のElixir実装(APIのDSL)を利用して実装しています。
バックエンドDBはRedisで、コネクション管理はErlangの[poolboy](https://github.com/devinus/poolboy)を使って自前実装しています。

## 重い処理はどうしている？
軽量な処理はAPIサーバーで処理し、複雑なビジネスロジックが絡む重たい処理はElixirからExqを使ってSidekiqに直接ジョブをenqueueしています。

![image2.png](https://qiita-image-store.s3.amazonaws.com/0/28174/b2b5b61c-bdc9-5f58-3086-2e3c39a99bd4.png)

## Exq
[Exq](https://github.com/akira/exq)はSidekiqのElixir実装です。
ExqはResque/Sidekiqと互換性があるので、ExqからResque/Sidekiqにジョブのenqueueができます。

### Enqueue側

enqueueはExqの```Exq.Enqueuer.enqueue```を使って行います。

```ex: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を起動する際、以下の様になります。

```ex:Supervisor
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```のキュー名を指定します。

```rb:dequeue処理
class MyAppJob << ActiveJob::Base
  queue_as :my_queue # MyApp.Jobの@queue_nameで指定されたキュー名

  def perform(args)
    # my awesome heavey job
  end
end
```

また、Redisの設定を以下の様に指定しています。

```rb:initializerでの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のダッシュボードは以下の様になっています。

![sidekiq1.png](https://qiita-image-store.s3.amazonaws.com/0/28174/883af3a8-4e88-6ce3-9dcd-c4c2dcecd281.png)
4ヶ月で処理した処理が重めのジョブ数はこんな感じです。これらは、ほぼ全てがExqからenqueueされたものです。

![sidekiq2.png](https://qiita-image-store.s3.amazonaws.com/0/28174/40d30507-1d31-2139-ee98-0846446b8d66.png)
半年の遷移を見ると、リリース時に比べるとかなり増えている事が見て取れます。

スケールに関してですが、リリース日から今現在までで、サービスの規模が[DAU](http://www.sophia-it.com/content/DAU)ベースで30倍程になっていますが、その為の構成変更や特別なチューニング等は行っておらず、単純にサーバーのコア数や台数の変更のみで対応しました。

# まとめ

広告の配信システムのAPI部分をElixirで実装/運用しました。
Exqを使うことでSidekiqとキューのやりとりを実現しました。

# 参考資料
[地獄のElixir入門](http://www.slideshare.net/ohr486/elixir-52044515)
[Elixirを本番環境で使ってみたという事例紹介](http://www.slideshare.net/ohr486/shibuyaex-1-elixir)

# おまけ
来年、2016/1/11に[Elixir Meetup #1 in Drecom](http://beam-lang.connpass.com/event/23013/)のイベントを企画しています。
そこで、Elixirを使ってアプリを実際にプロダクション運用した際の地雷とその処理内容について話したいと思います。

# 次は

 [【その1】ドリコム Advent Calendar 2015](http://www.adventar.org/calendars/1043) の20日目は[Narazakaさん]()の記事です。
[【その2】ドリコム Advent Calendar 2015](http://www.adventar.org/calendars/1044)の20日目は[jizojpさん]()の記事です。