7839

雑草魂エンジニアブログ

【Rails】SideKiqを用いて非同期処理(バックグラウンドジョブ)を実行する

今回、Rails のプロジェクトで、バックグラウンド処理を実装したくなり、SideKiq を用いて実装を行ったので、備忘録として残しておく。

ジョブ管理

Rails アプリケーションで非同期的に処理を実行したい場合などがある。
例えば、以下のように、API 経由でメールを送信する場合。

def send_email
  # recieve request
  mail.send()  # 処理が終わるまで待機する
  head(:ok) && return
end

同期処理の場合、mail.send() の処理が終わるまで、レスポンスを返すことができず、最悪の場合、リクエスタイムアウトになってしまう。こういう場合に、ジョブ管理を使うと、ジョブをキューに登録する処理だけ実行し、(並列処理をすることで)レスポンスを即座に返すことができる。

Rails5 には、ジョブ管理のフレームワークである Active Job がある。 ただし、Rails 自身が提供するのは、ジョブをメモリに保持するインプロセスのキューイングシステムだけであり、再起動などするとジョブは全て失われてしまう。

そのため、(Railsアプリケーションの起動とは別に)ジョブを処理するためにライブラリ固有のキューイングサービスを起動しておく必要がある。

詳細は、以下の公式ドキュメントを参考にして欲しい。

railsguides.jp

今回、Active Job とアダプタとして Sidekiq を使ってバックグラウンドジョブを実行することもできたが、Sidekiq 単体でも同様の処理が実行できたため、Sidekiq 単体での実装にすることとした。

Sidekiq とは

github.com

Simple, efficient background processing for Ruby.

Sidekiq はスレッドを使用して、同じプロセスで同時に多くのジョブを処理することができる。

Sidekiq を使うためには Client、Redis、Server の 3 つが必要となる。

  • Client:Railsアプリケーションサーバー(ジョブを登録する)
  • Redis:ジョブをキューイングするために必要(ジョブ管理をする)
  • Server:並列処理をする(ジョブを実行する)ために、Railsアプリケーションとは別プロセスとして起動しておく(bundle exec sidekiq で起動することができる)

Redis をインストールする

今回、開発環境を Docker としていたので、ruby の環境下に Redis を追加することにした。

FROM ruby:2.6.3-alpine

ENV LANG C.UTF-8
ENV WORKSPACE=/app

# 基本モジュールインストール
RUN apk update && \
  apk add --no-cache tzdata libxml2-dev curl-dev make gcc libc-dev build-base libxslt-dev g++ mariadb-dev less git && \
  rm -rf /usr/local/bundle/cache/* /usr/local/share/.cache/* /var/cache/* /tmp/* && \
  apk del libxml2-dev curl-dev make gcc libc-dev g++

# node + yarn
# python2はnodeインストール時の依存関係
RUN apk add --no-cache alpine-sdk \
  nodejs-current \
  nodejs-npm \
  yarn \
  python2

# install Redis
RUN apk add --update --no-cache redis

RUN mkdir -p $WORKSPACE
WORKDIR $WORKSPACE

ADD Gemfile $WORKSPACE/Gemfile
ADD Gemfile.lock $WORKSPACE/Gemfile.lock

RUN bundle install

COPY . $WORKSPACE

Redis を起動する

$ redis-server --appendonly yes --daemonize yes

Sidekiq を実装する

1. gemを追加する

gem 'sidekiq'
gem 'sinatra', require: false # ダッシュボードのため

bundle install を実行して、gem をインストールする。

2. 起動時の設定を追加する
サーバー側とクライアント側の2種類の設定を追記しておく。

Sidekiq.configure_server do |config|
    config.redis = { url: 'redis://localhost:6379' }
end

Sidekiq.configure_client do |config|
    config.redis = { url: 'redis://localhost:6379' }
end

3. 設定ファイルを作成する

:verbose: false
:pidfile: ./tmp/pids/sidekiq.pid
:logfile: ./log/sidekiq.log
:concurrency: 10
:queues:
  - default

4. Woker作成(ジョブを登録する) 今回は、Testという名前のジョブを作成する。

$ bundle exec rails g sidekiq:worker Test
      create  app/workers/testd_worker.rb
      create  test/workers/test_worker_test.rb
class TestWorker < ApplicationController
  include Sidekiq::Worker
  def perform()
    p 'Hello SideKiq'
  end
end

5. sidekiqを起動する

$ bundle exec sidekiq -C config/sidekiq.yml

6. ジョブを実行してみる Controller などから呼び出して実行することもできるが、試験的に Railsコンソールで実行してみる。(sidekiq を起動しいるシェルとは別に新しいシェルで実行する。)

$ bundle exec rails c
> TestWorker.perform_async()

sidekiq 側で、「Hello SideKiq」が表示され、ジョブを実行することが確認できた。

7. 管理画面を追加する

require 'sidekiq/web'
mount Sidekiq::Web, at: "/sidekiq"

再度、Rails サーバーを起動して、http://localhost:3000/sidekiq にアクセスすると、管理画面が表示される。

(おまけ)AWS EC2へのデプロイ

最後に、諸々の機能を実装して、本番のAWSにデプロイする際に、追加で実施した処理について残しておく。

Redis のインストール

$ amazon-linux-extras
6  postgresql10     available  [ =10  =stable ]
8  redis4.0             available  [ =4.0.5  =4.0.10  =stable ]
9  R3.4                   available  [ =3.4.3  =stable ]

$ sudo amazon-linux-extras install redis4.0

Redis と Sidekiq のデーモン化

今回、Redis と Sidekiq のデーモン化は、前回紹介した Supervisor で行うことにした。 (前回の記事:【Linux】Supervisor でプロセス制御(Rails + Puma / Capistrano) 参照)

それぞれの設定ファイルを以下のように準備する。

[program:redis]
command=/bin/redis-server /etc/redis.conf
autostart=true
autorestart=true
user=redis
[program:sidekiq]
directory=/srv/monoai-ranger-manager/current
command=/bin/bash -c "/opt/rbenv/shims/bundle exec sidekiq -e production -C config/sidekiq.yml"
autostart=true
autorestart=true
user=ec2-user
startsecs=10
stdout_logfile=/srv/monoai-ranger-manager/shared/log/sidekiq.stderr.log
logfile_maxbytes=1MB
logfile_backups=10
redirect_stderr=true

(注意)
-e production のオプションを付けないと、default が development になっているので、本番環境で環境変数の不一致が発生しているエラーが発生した。

supervisord を再起動させて、実行させる。

$ supervisorctl reload
Restarted supervisord

$ supervisorctl status
redis                  RUNNING   pid 17430, uptime 0:00:13
sidekiq              RUNNING   pid 17431, uptime 0:00:13

Capistrano 設定追加

以前は、Rails アプリケーションのみであったが、今回 Redis と Sidekiq が増えたので、Deploy 前後で全てのプロセスを再起動するように変更した。これで、deployコマンドのみでプロセスの再起動まで完結するようにできた。

~~ 省略 ~~
namespace :console do
  namespace :all do
    desc 'start service all'
    task :start do
      on roles(:app) do |_host|
        execute 'supervisorctl start all'
      end
    end

    desc 'stop service all'
    task :stop do
      on roles(:app) do |_host|
        execute 'supervisorctl stop all'
      end
    end
  end
end

before 'deploy:starting', 'console:all:stop'
after 'deploy:finished', 'console:all:start'

(おまけ)redis と sidekiq を別コンテナで起動する

Sidekiqは、Railsで非同期処理を行うためのライブラリであり、実行の際にはRailsのプロセスとは別に実行する必要がある。負荷を分散するために、SidekiqをRailsアプリケーションとは別のコンテナで動かしたいことがあるかもしれない。その場合は、以下の設定にすることで、簡単に実現することができる。

version: '3'
services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
      TZ: Asia/Tokyo
    ports:
      - 3306:3306
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - mysql-data:/var/lib/mysql
  redis:
    image: redis:alpine
    ports:
      - 6379:6379
  app: &app_base
    build:
      context: .
    volumes:
      - .:/app
      - gem-data:/app/vendor/bundle
      - log-data:/app/log
    ports:
      - 3000:3000
    depends_on:
      - db
      - redis
    stdin_open: true
    tty: true
  sidekiq:
    <<: *app_base
    command: ["bundle", "exec", "sidekiq", "-C", "config/sidekiq.yml"]
    ports:
      - 3001:3001
volumes:
  mysql-data:
  gem-data:
  log-data:

また、以下のライブラリを追加する。

〜省略〜
gem 'redis-namespace'

SideKiq の設定を以下のように変更する。

Sidekiq.configure_server do |config|
  config.redis = { url: 'redis://redis:6379', namespace: 'sidekiq' }
end
Sidekiq.configure_client do |config|
  config.redis = { url: 'redis://redis:6379', namespace: 'sidekiq' }
end

これで、コンテナを分離することができた。 (実際に、コンテナを分離することで、ログなどがそれぞれのコンテナに記載されてしまうので、正直ログを確認するのが面倒になってしまったので、そこあたりの設定をしっかりとしていきたい。)

まとめ

今回は、SideKiq を用いてジョブ管理をすることができた。定期処理や日時指定での実行も可能のため、今後も使っていきたいと思えた。

それでは、ステキな開発ライフを。

関連記事

serip39.hatenablog.com