ラベル sidekiq の投稿を表示しています。 すべての投稿を表示
ラベル sidekiq の投稿を表示しています。 すべての投稿を表示

2015/09/28

unicornとsidekiqをユーザレベルのsystemdで自動起動

lxcコンテナでRailsアプリである録画システムを動かすことに成功したので、永続化のためにコンテナの再起動時に自動起動するようにしたいわけです。

今までどおりなら、/etc/init.dに複雑な起動と停止のスクリプトを書くか、/etc/init.にupstart用のスクリプトファイルを置くのでしょうが、せっかく Ubuntu15.04 から systemd がデフォルトになったので、これを期にRailsアプリのデーモンをsystemdで扱えるようにしようと。

systemd/ユーザ
こちらを読むと、ユーザがログインした時に起動して、ログアウトで停止するようなサービスを定義できるらしい。更にそれをブート時に起動して、ユーザのログイン・ログアウトに依存しない永続化ができるらしい。これだぁ。

lxcコンテナでも、systemd --user デーモンがちゃんと稼働している。

今回デーモン起動したいサービスは、Railsのunicornとsidekiq。
これをsystemdのユーザサービスとして起動できるようにする。

systemdのサービスUnit定義ファイルを、ユーザのホームディレクトリの所定の場所に記述

SidekiqサービスUnitファイル .config/systemd/user/sidekiq.service

[Unit]
Description=Recman Sidekiq

[Service]
Type=forking
WorkingDirectory=%h/recman
ExecStart=/usr/local/bin/sidekiq -C ./config/sidekiq.yml -d

[Install]
WantedBy=default.target

UnicornサービスUnitファイル .config/systemd/user/unicorn.service

[Unit]
Description=Recman Unicorn

[Service]
Type=forking
WorkingDirectory=%h/recman
ExecStart=/usr/local/bin/unicorn_rails -D -c ./config/unicorn.rb -E development

[Install]
WantedBy=default.target

ExecStartは、デーモン起動したらすぐに終わるコマンドなので、Typeはforking。
Railsアプリのディレクトリに移ってから実行する必要があるので、WorkingDirectoryで指定する。
%hはユーザのホームディレクトリを示す変数。
インストール先は、default.target。systemd --userによって、defaut.targetユニットが予め存在するらしい。

とにかく、すごいシンプル!こんなでいいんだ。って感じ。

これらを登録して自動起動設定するまでの流れ
以降の操作はすべてユーザ権限で行える。systemctlに'--user'を付ける。

ユーザUnitファイルを読みこませる
$ systemctl --user daemon-reload

ちゃんと認識したか確認

$ systemctl --user list-unit-files

sidekiq.serviceとunicorn.serviceが、disableの状態でリストされる
ここまで行けば、手動での起動と停止ができるようになる

手動起動
$ systemctl --user start sidekiq
$ systemctl --user start unicorn

停止
$ systemctl --user stop sidekiq
$ systemctl --user stop unicorn

ExecStopを記述しなくても、停止させることが出来る。楽ちん。

自動起動するように有効化する

$ systemctl --user enable sidekiq
$ systemctl --user enable unicorn

再度確認してみた時の結果
ubuntu@recman:~$ systemctl --user list-unit-files
UNIT FILE            STATE   
sidekiq.service      enabled 
systemd-exit.service static  
unicorn.service      enabled 
basic.target         static  
bluetooth.target     static  
default.target       static  
exit.target          disabled
paths.target         static  
printer.target       static  
shutdown.target      static  
smartcard.target     static  
sockets.target       static  
sound.target         static  
timers.target        static

ubuntu@recman:~$ systemctl --user status unicorn
unicorn.service - Recman Unicorn
   Loaded: loaded (/home/ubuntu/.config/systemd/user/unicorn.service; enabled; vendor preset: enabled)
   Active: active (running) since 日 2015-09-27 17:59:14 JST; 15min ago
 Main PID: 1575 (ruby2.1)
   CGroup: /lxc/recman-1/user.slice/user-1000.slice/user@1000.service/unicorn.service

ubuntu@recman:~$ systemctl --user status sidekiq
sidekiq.service - Recman Sidekiq
   Loaded: loaded (/home/ubuntu/.config/systemd/user/sidekiq.service; enabled; vendor preset: enabled)
   Active: active (running) since 日 2015-09-27 17:59:12 JST; 15min ago
 Main PID: 1562 (ruby2.1)
   CGroup: /lxc/recman-1/user.slice/user-1000.slice/user@1000.service/sidekiq.service


永続化

ここまでだと、ログアウトと同時にユーザサービスは停止してしまうので、ブート起動して、ログイン/ログアウトに依存しない状態にする

$ sudo loginctl enable-linger

これでログアウトしても停止しなくなるし、lxcコンテナ起動時にサービスが開始される。
解除したい場合は、disable-lingerする。

$ sudo loginctl disable-linger

正直なところ、このloginctlとlingerがよく理解できていない。
enable-lingerするとセッションが壊れる危険性?があるということだが、どういうことなのか・・・

だけれど、とにかく、こうしたいっていう目的は達成である。

2015/07/23

録画予約をActiveJob with sidekiq で

前回の記事で、録画ジョブをActiveJobでやる話を書きました。
実際にやってみて、これまた色々ハマったので今回はそのへんの話を書きたいと思います。

要件
1.EPG情報から直近7日間中の何時何分に実行という予約ができること。
その間、システムリブートしても予約が消滅しないこと。キューの永続化。
2.ジョブが失敗してもリトライしない。スケジュールした時間に意味あるので。
4.予約したものを途中キャンセルできること。


RecordJob という ActiveJob::Baseの派生クラスを作成
$ rails g job record
すると、app/jobs/record_job.rbファイルが作られます。最初は以下の様な感じ。
class RecordJob < ActiveJob::Base
  queue_as :default

  def perform(*args)
    # Do something later
  end
end
このperform関数内に録画処理をずらずらと書いておくわけですが、その内容とは関係ない周辺処理でいくつかハマってしまいました。

1.指定日時分に実行するジョブ登録と実行(perform)

登録自体は簡単です。
  # 予約
  def reserve
    start = self.program.start_at - 30
    job = RecordJob.set(wait_until: start).perform_later(self)
    update_attribute(:job_id, job.job_id)
  end
このなメソッドを、ビデオレコードモデル models/video.rb に書いておいて、予約時に video.reserveを呼ぶだけです。
RecordJob.set(wait_until: start).perform_later(self) がActiveJobを通してSidekiqにstartで指定した日時分に実行するスケジュールが登録されます。redisにスケジュールは永続化するので、途中システムリブートしても大丈夫。

なぜ perform_later(video.id)ではなく、perform_later(self) なのか?

ネット記事を読みながらやると、オブジェクト渡しはシリアライズが入ってよろしくないからID渡しをするのだ的な内容が多かったので、最初は自分もそうしてみました。
 ところが!
最初のperform実行ではちゃんとvideo.idが渡って滞り無く処理されるんですが、不思議なことに2回めのジョブ(もちろんIDは異なる)でも最初のIDでperformが実行されてしまいました。
perform(video_id)で受けるのをやめてみて perform(*args) で受けてみても結果同じでした。

↑後で気づいたんだけど、whereではなくfind_byを使ったためと思われる。perform(id)ができないというわけではないようだ。

その後知りましたが、現在のActiveJobのperformへはオブジェクト渡しは悪い方法ではないらしい。シリアライズ負荷がどうのという懸念は解消しているらしい。

ただオブジェクト渡しだと、ジョブ実行前にそのオブジェクトレコードが削除された場合は事前に調べることが出来ず、例外エラーとなってしまう。これはちゃんとレコード削除とジョブキャンセルを同期させれば問題ないか。

2.失敗してもリトライしないように

ネット上は色々なソリューションがあるようです。

ActiveJobオプションをジョブクラスに記述する。
job_options retry: false

Sidekiqオプションを記述する。
sidekiq_options :retry => false

ふむふむ? どっちも効きませんけど・・・
結局自分は、以下のコードを、initializers/sidekiq.rb に加えました。
Sidekiq.configure_server do |config|
   config.server_middleware do |chain|
     chain.add Sidekiq::Middleware::Server::RetryJobs, :max_retries => 0
   end
end

3.SidekiqスケジュールをRailsアプリからキャンセルする

放っておいてもジョブ失敗するだけでシステムが死ぬわけじゃないんで支障はない
のだけれど、ログが汚くなるし、sidekiq_webで見た時にわけわからなくなるんで、
予約キャンセルでジョブスケジュールも削除したいわけです。

探してみましたが、ActiveJobからキュー状態を調べたり削除したりすることが出来ない?

perform_laterが返すJobのjob_idは、SidekiqのjobIDとは異なる? むむぅ。調べた結果、
Sidekiqスケジュールのアーギュメント内にActiveJobのjob_idが記録されている。
ということで以下のコードをレコード削除メッソド内に記述することで解決。
jobs = Sidekiq::ScheduledSet.new.select do |job|
  job.args[0]['job_id'] == @video.job_id
end
jobs.each(&:delete)
冒頭のreserveメソッドでActiveJobのjob_idをレコード上に覚えておくのは、このため。
以上が、やりたいこと以外のやらなくてはならなかったこと。結構苦労した。

2015/07/19

録画処理をActiveJobで

実際の録画ジョブをどこでどうやって動かすか?

過去のアプローチとしては、Linuxの'at'コマンドでOS的なジョブ登録で回していた。
'at'は、お手軽でいいんだけれど、ジョブ管理が結構面倒だった。

その後、Jenkinsでジョブ管理するようにした。これはこれで良かったが、Jenkins自体がメモリとCPU食いなので、1台で全部やらせようとするとキャパ的にきつかった。

今回完全リニューアルということで、調べなおしてみたところ、Rails4以降では、ActiveJob というものが標準化したらしい。ならば、使ってみようじゃないですかと。

ActiveJobはただのフロントAPIであって、実際のジョブは別のジョブキューイングシステムに任せるようだ。その辺の解説は、http://blog.chopschips.net/blog/2015/02/26/active-job/がいいです。

Sucker Punch

バックエンドに簡単そうな、Sucker Punch を使ってやってみることにした。作者の記事もなかなか面白いです → Why I Wrote the Sucker Punch Gem

いつものようにGemfileに、gem 'sucker_punch' を追記して、$ bundle しておく。
config/initializers/sucker_punch.rbファイル作成して
Rails.application.configure do
  config.active_job.queue_adapter = :sucker_punch
end
と書いて、ActiveJobのバックエンドとしてSuckerPunchを使うようにして準備完了。
簡単にできるはずが・・・

実は色々ハマりました
SuckerPunchは実は Celluloid のラッパーに等しい。このCelluloidの挙動がむずい。

1.予約録画モデルのVideoモデルがジョブキューを持てばいいんじゃないか?
と、いきなりSuckerPunchを直接使ってしまおうと思った。そこで、Videoモデルに
include SuckerPunch::Job
を入れてみたら、Videoモデル全体がCelluloidでラップされてしまい、.createとか、.first_or_createとかが全然動かなくなってしまった。newしてsaveすれば?と書き方を変えて粘ってもみたが、 全く太刀打ち出来無かった・・・

気持ちを新たに、rails g job RecordJob で、RecordJobというActiveJobを作ってからのこと・・・

2.perform_laterと10秒制限?
RecordJob.perform_laterでキュー登録してみた。performメソッド内で録画タイミングを図るためにsleepをさせていた。perform_laterからperformはコールされるが、perform内は10秒以内の処理じゃないと強制的にアボートしてしまうようだ。はぁ?って感じ。

 RecordJob.set(wait_until: video.program.start_at).perform_later(video)
気持ちとしては、上記の1行で予約時にキュー登録したいのだが、試しにやってみたら、wait_untilはSuckerPuchでは実装されていなかった・・・

残念ながら、SuckerPunchは目的には合致しなかったようだ。撃沈。

やっぱり普通にSidekiqか

sidekiqをバックエンドに使用することにしたので redis 入れる。


$ apt-get install redis-server

Gemfileにgem 'sidekiq'を追加して、$ bundle実行。
sidekiqは、$ sidekiq で直接動かしてテスト実行。

今度は、
 RecordJob.set(wait_until: 録画開始時間).perform_later(video)
が普通に動いた。
後は、sidekiqをサーバ自動起動化して、タスク環境はよしとしよう。