🛠️

Sidekiqの導入からECSデプロイまで

2021/02/28に公開

概要

  • Sidekiqを導入する際に最低限必要な作業をまとめてみました

背景

メール送信のように時間がかかる処理や、重たい処理を分割して並列実行したいときに非同期ジョブを使いたくなることがありますよね。
RailsにはActiveJobというジョブ管理のAPIが用意されていますので、これを使わない手はないです。

Railsガイドのジョブを実行するに書かれている通り、本番環境で使うには何らかのキューイングバックエンドが必要です。

production環境でのジョブのキュー登録と実行では、キューイングのバックエンドを用意しておく必要があります。具体的には、Railsで使うべきサードパーティのキューイングライブラリを決める必要があります。 Rails自身が提供するのは、ジョブをメモリに保持するインプロセスのキューイングシステムだけです。 プロセスがクラッシュしたりコンピュータをリセットしたりすると、デフォルトの非同期バックエンドの振る舞いによって主要なジョブが失われてしまいます。アプリケーションが小規模な場合やミッションクリティカルでないジョブであればこれでも構いませんが、多くのproductionでは永続的なバックエンドを選ぶ必要があります。

そこでジョブ管理に便利なのがSidekiqです。

しかし、実際に導入しようするとGemを入れるだけではなく、開発環境のdocker-composeを用意したり[1]、CIでもジョブのテストが出来るようにする必要があります。
さらに、ステージング・本番での実行環境の用意も必要です。
Sidekiqを使ったことがなかった筆者にはやることが多くて大変でした。
次に導入する誰かの役に立つように、やったことのまとめを書きます。
内容は以下の通りです。

  • Sidekiqの基礎知識
  • 開発環境(Dockerfile, docker-compose)の用意
  • CIの設定(CodeBuild)
  • ECS(ステージング・本番環境)へのSidekiqコンテナのデプロイ
  • Elasticacheの設定

Sidekiqの基礎知識

SidekiqではジョブのことをWorkerと呼びます。ActiveJobでは、スレッドのことをワーカーと呼ぶので初めは混乱しました。
下図のように、appのコンテナとsidekiqのコンテナはredisを介してenqueue, dequeueを行います。sidekiqコンテナでジョブを実行します。

開発環境(Dockerfile, docker-compose)の用意

redisコンテナ

version: "3.7"
services:
 redis:
   image: redis:6
   ports:
     - 6379:6379
   volumes:
     - ./redis/redis.conf:/usr/local/etc/redis/redis.conf
   command: redis-server /usr/local/etc/redis/redis.conf
   sysctls:
     - net.core.somaxconn=512

ジョブを登録するためにredisが必要です。
redisの設定ファイルをコンテナにコピーしておきます。
redis.confは以下のようにしました。

# The filename where to dump the DB
dbfilename dump.rdb

これはコンテナ起動時の下記のエラーへの対応です。

Redis::CommandError (MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.)

redisのデータをパックアップするファイルを指定してあげると、解消しました。
commandでredis-serverの起動時に、この設定ファイルが参照されるようにします。

また、sysctlsでsomaxconnを512にしておきます。これは下記のエラーへの対応です。

# WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.

Redisの紹介の記事に以下のようにありました。

TCPバックログ(待ち受け可能なTCPセッション数)のOSのデフォルト値は128ですが、Redisのデフォルトでは511が指定されているため、OSのバックログを拡張します。

somaxconnとは、socket max connectionsの略のようで、Redisのデフォルトの指定値を下回っていると怒られるようでした。

sidekiqコンテナ

version: "3.7"
services:
 redis: # 省略

  sidekiq:
   depends_on:
   - db
   - redis
   build:
     context: .
     target: test
     dockerfile: ./dockerfiles/Dockerfile
   command: bundle exec sidekiq -C config/sidekiq.yml
   environment:
     REDIS_URL_SIDEKIQ: redis://redis:6379/5/sidekiq
     DB_HOST: hoge
     DB_NAME: hoge
     RAILS_ENV: development

appと同じDockerfileを使ってbuildしました。
マルチステージビルドしていたので、そのまま使うとRAILS_ENVがproductionになったままのコンテナを使ってしまいます。
それを防ぐためにtarget: testとして、暫定的にテスト用のステージのコンテナイメージを使用しました。
commandは設定ファイルの指定して起動していますが、config/sidekiq.ymlがデフォルトのファイルパスのようなので書かなくても読み込まれるようです。私は設定ファイルがあることを明示するためにあえて書きました。
環境変数としてredisの接続先を指定します。docker-composeはサービス名で指定してネットワーキングすることが出来るのでredis://としています。
また、sidekiqコンテナではDBからジョブに関するデータを取得したり、DBに書き込むジョブもあると思ったのでDBの接続情報も渡しました。

sidekiq.ymlは以下のようにしました。queuesにmailersを追加していおかないと、メール送信されないので注意して下さい。

 ---
:queues:
  - default
  - mailers

公式の設定例はここにありました。

CIの設定(CodeBuild)

build:
  commands:
    - docker-compose -f docker-compose.yaml build
    # redisを起動する
    - docker-compose -f docker-compose.yaml up -d db redis
    - docker-compose -f docker-compose.yaml -f run app bundle exec rspec

ジョブのテストを行うためにCIの設定も必要になります。
buildspecで以下のようにテスト実行の前にredisを起動しておく必要があります。
また、gemのmock_redisのようなダミーのKVSを使うのも1つの手です。

ECSへのSidekiqコンテナのデプロイ

appとnginxのコンテナを同一のECSタスクにしていましたが、このタスクにsidekiqコンテナを追加することはしませんでした。
ソースコードは同一のGithubリポジトリで管理しているので、appと同時にsidekiqコンテナをデプロイ出来ることはメリットです。しかし、appコンテナが起動しなかった場合は、タスクが停止するのでsidekiqコンテナを一緒に停止してしまいます。
別タスクにしておくと、この問題を回避出来ますし、sidekiqコンテナだけの増減も簡単です。さらに、ジョブでのみ必要になる計算資源に合わせて、CPUやメモリを設定出来るのでサーバーリソースの使用も効率的です。
Dockerイメージはappと同様のものを使いました。ECSへのデプロイにhakoを使っているので、hako定義のjsonnetファイルでデプロイ時にbundle exec sidekiq -C config/sidekiq.ymlでコマンドを上書きしました。

  app: {
    image: '[aws-account-id].dkr.ecr.ap-northeast-1.amazonaws.com/[repo-name]',
    tag: 'latest',
    command: [
      "bundle",
      "exec",
      "sidekiq",
      "-C",
      "config/sidekiq.yml"
    ],
    cpu: '256',
    memory: '512',
  },

Elasticacheの設定

Sidekiqコンテナ起動時に以下のようなWarningが出ていました。

WARNING: Your Redis instance will evict Sidekiq data under heavy load.
The 'noeviction' maxmemory policy is recommended (current policy: 'volatile-lru').

これは、redisのメモリが溢れた時の挙動をどうするかの設定のことでした。redisの日本語ドキュメントに各種ポリシーの挙動が書かれています。

  • noeviction(既存のキーバリューを追い出さない、新規書き込み拒否)
  • volatile-lru(最も最近使用されていないKey、ただしexpire setが設定されているKeyを先に削除)

CloudFormationで以下のように指定して、noevictionに変更しました。

Resources:
  ParameterGroup:
    Type: AWS::ElastiCache::ParameterGroup
    Properties:
      Properties: {
        "maxmemory-policy" : !Ref MaxMemoryPolicy # noeviction
      }

Propertiesのバリューにネストして、さらにPropertiesを指定する必要がありました、こちらの公式ドキュメントを参照しました。

Warning対応として設定は変えましたが、実際の運用においてはredisサーバーのメモリ状況を監視していれば、それほど気にする必要はなさそうでした。

以上がSidekiqの導入から本番環境へのデプロイまでにやったことでした。
CloudFormationでのIaC、hakoのようなECSデプロイツールは使わない場合も多いと思うので、適宜読み替えて参考にして頂けたらと思います。

脚注
  1. ローカルサーバーでも実行可能ですが、デプロイ時にはコンテナで動かすのならコンテナを用意しておいた方がいいと思います ↩︎

Discussion