tkak's tech blog

This is my technological memo.

sinatraとsidekiqを組み合わせて、簡単なWeb APIを作ってみた

sidekiqは、複数のjobを非同期実行させることができるrubyライブラリです。割と簡単にjobの並列処理がrubyで書けて、便利そうです。今回、年末から年始にかけて、ゆる〜くsinatraとsidekiqを組み合わせて、非同期型のjob実行Web APIを作ってみたので、まとめておきます。

背景

GUIしか存在しない、いにしえのツールをWeb API化(poltergeistを使ってブラウザ操作部分を自動化し、sinatraとsidekiqを組み合わせてAPIを作成)したかったからです。

なお、今回作ったものはGitHubにあげました。

github.com

開発環境

今回Dockerも触ってみたかったので、ローカル開発環境をDockerで作りました。

  • OS X 10.11.1 (15B42)
  • Docker version 1.8.2, build 0a8c2e3
  • docker-machine version 0.4.1 (e2c88d6)
  • docker-compose version: 1.4.2

docker-machine

docker-machineでDocker環境(VirtualBox上にDocker用VM)を作ります。

$ docker-machine start dev

docker-machine envの結果を環境変数に設定します。

$ eval "$(docker-machine env dev)"

application用のDockerfile

sinatraとsidekiqを動かすapplication用コンテナのDockerfileを、以下のように作ります。一度/tmpにGemfileとGemfile.lockをコピーしてbundle installすることで、docker runするたびにgem installが動くことを回避し、うまくDockerのキャッシュを使えるようにしています。また、sinatraで使うポート5000をEXPOSEし、外からアクセスできるようにします。

FROM ruby:2.2.4
RUN gem install bundler

WORKDIR /tmp
ADD Gemfile Gemfile
ADD Gemfile.lock Gemfile.lock
RUN bundle install

WORKDIR /opt/sinatra-sidekiq-example

EXPOSE 5000

docker-compose

sidekiqは、jobのキューをためるのにredisを使っているので、applicationコンテナとは別にredisコンテナを立ち上げます。ただ、applicationコンテナとredisコンテナ、複数のコンテナを立ち上げようとすると、linkオプションを使わないといけなかったり、コンテナの起動順序を意識したりと、dockerコマンドだけで管理しようとすると極端に辛くなってきます。そこでdocker-composeを使います。docker-compose.ymlファイルにコンテナの定義を書くことで、コンテナを管理しやすくします。コンテナが多くなってくると、コマンドラインで短縮化されたオプションを書きまくるより、設定ファイルでより直感的に管理できる方がいいですからね。

docker-composeのヘルプはこんな感じ。

$ docker-compose -h
Define and run multi-container applications with Docker.

Usage:
  docker-compose [options] [COMMAND] [ARGS...]
  docker-compose -h|--help

Options:
  -f, --file FILE           Specify an alternate compose file (default: docker-compose.yml)
  -p, --project-name NAME   Specify an alternate project name (default: directory name)
  --verbose                 Show more output
  -v, --version             Print version and exit

Commands:
  build              Build or rebuild services
  help               Get help on a command
  kill               Kill containers
  logs               View output from containers
  port               Print the public port for a port binding
  ps                 List containers
  pull               Pulls service images
  restart            Restart services
  rm                 Remove stopped containers
  run                Run a one-off command
  scale              Set number of containers for a service
  start              Start services
  stop               Stop services
  up                 Create and start containers
  migrate-to-labels  Recreate containers to add labels
  version            Show the Docker-Compose version information

今回のdocker-compose.ymlファイルはこんな感じにしました。

web:
  build: .
  command: foreman start
  ports:
    - "5000:5000"
  volumes:
    - .:/opt/sinatra-sidekiq-example
  links:
    - redis
  environment:
    - REDIS_HOST=redis
    - REDIS_PORT=6379
redis:
  image: redis

buildでカレントディレクトリを指定してますが、そうすることでカレントディレクトリからDockerfileを探してきて勝手に読み込んでくれます。commandは、コンテナを起動するときに実行するコマンドです。portsはポートフォワーディングの設定です。volumesは、コンテナの内部とカレントディレクトリを共有するために設定します。linksは、コンテナ同士の関連付けです。environmentはコンテナ内部の環境変数設定です。redisコンテナは特にカスタマイズせずに公式のredisイメージを立ち上げるだけにしてます。

Dockerfileからコンテナをビルドします。

$ docker-compose build

コンテナを起動させます。

$ docker-compose up -d

コンテナの情報を確認します。

$ docker-compose ps
            Name                          Command             State           Ports
--------------------------------------------------------------------------------------------
sinatrasidekiqexample_redis_1   /entrypoint.sh redis-server   Up      6379/tcp
sinatrasidekiqexample_web_1     foreman start                 Up      0.0.0.0:5000->5000/tcp

コンテナの中に入ります。

## application用 (web)
$ docker-compose run web /bin/bash

## redis用
$ docker-compose run redis /bin/bash

参考

sidekiq

開発環境が整ったら、ようやくsidekiqです。Gemfileにsidekiqを指定し、bundle installします。

gem 'sidekiq'

sidekiqを使った実装は、jobをキューに入れるところと、jobを実行するところの2つです。

redisの接続設定

sidekiqはバックエンドにredisを使うので、redisに接続するための設定を記述します。configure_clientとconfigure_server、両方必要です。

## jobをキューにためるのに必要
Sidekiq.configure_client do |config|
  config.redis = { url: "redis://#{ENV['REDIS_HOST']}:#{ENV['REDIS_PORT']}" }
end

## jobをキューから取得するのに必要
Sidekiq.configure_server do |config|
  config.redis = { url: "redis://#{ENV['REDIS_HOST']}:#{ENV['REDIS_PORT']}" }
end

workerの実装

jobを実行するworkerの実装は、以下のような感じです。Sidekiq::Workerモジュールをincludeしたclassを作り、performメソッド内にjobを記述します。ドキュメント読む限り、performメソッドの引数は、redisの容量を少なくするためになるべく少なくした方がいいようです。

require 'sidekiq'

class SomeWorker
  include Sidekiq::Worker

  def perform(id)
    thing = $redis.hget('things', id)
    sleep 10
    p "The job is done: #{thing}"
  end
end

キューにためたjobを実行するためには、sidekiqコマンドを使います。

$ bundle exec sidekiq -h
2016-01-02T15:21:45.078Z 7 TID-origl9uvo INFO: sidekiq [options]
    -c, --concurrency INT            processor threads to use
    -d, --daemon                     Daemonize process
    -e, --environment ENV            Application environment
    -g, --tag TAG                    Process tag for procline
    -i, --index INT                  unique process index on this machine
    -q, --queue QUEUE[,WEIGHT]       Queues to process with optional weights
    -r, --require [PATH|DIR]         Location of Rails application with workers or file to require
    -t, --timeout NUM                Shutdown timeout
    -v, --verbose                    Print more verbose output
    -C, --config PATH                path to YAML config file
    -L, --logfile PATH               path to writable logfile
    -P, --pidfile PATH               path to pidfile
    -V, --version                    Print version and exit
    -h, --help                       Show help

## 実行例
$ bundle exec sidekiq -r ./app.rb -C ./sidekiq.yml

オプションを色々と指定することもできますが、yml形式の設定ファイルを使うこともできます。sidekiq.ymlはこんな感じ。

:verbose: true
:pidfile: /var/run/sidekiq.pid
:logfile: /var/log/sidekiq.log
:concurrency: 10
:queues:
    - default

sidekiqのプロセス数は、concurrencyで指定します。デフォルトは、25です。また、-dオプションでプロセスをデーモン化できますが、本番環境ではsystemdなどを使った方がいいとドキュメントに書いてあります。

jobをキューに登録

非同期で実行したいjobをキューに登録するのには、perform_asyncクラスメソッドを呼びます。

    SomeWorker.perform_async(req['id'])

5分後に実行したいjobは以下のように、perform_inを使います。

    SomeWorker.perform_in(5.minutes, req['id'])

dashboard

f:id:keepkeptkept:20160103102740p:plain

sidekiqは、jobのステータスをブラウザから確認するためのdashboardを提供してます。ドキュメントには、railsと一緒に使う方法しか書いてないんですが、sinatraと一緒に使う場合は、以下のようにすれば使えました。

require './app'
require 'sidekiq/web'

run Rack::URLMap.new('/' => SomeApp, '/sidekiq' => Sidekiq::Web)

参考

foreman

foremanは複数のプロセスを管理するツールです。今回sinatraとsidekiqのプロセス管理をforemanでやってみました。以下のようにProcfileにプロセスの定義を書きます。

web: bundle exec rackup -p $PORT --host 0.0.0.0
worker: bundle exec sidekiq -r ./app.rb -C sidekiq.yml

起動は以下のコマンド。

$ foreman start

参考

まとめ

sinatraとsidekiqを組み合わせることで、簡単に非同期型のjob実行Web APIが書けました。半分くらいDockerで遊んで得た知見ですが、これでひとまずやりたいことが実現できそうなことが分かってよかったです。

プログラマのためのDocker教科書 インフラの基礎知識&コードによる環境構築の自動化

プログラマのためのDocker教科書 インフラの基礎知識&コードによる環境構築の自動化

Docker実践ガイド

Docker実践ガイド