Skip to content

Instantly share code, notes, and snippets.

@ssig33
Last active March 18, 2022 02:46
Show Gist options
  • Save ssig33/9382101 to your computer and use it in GitHub Desktop.
Save ssig33/9382101 to your computer and use it in GitHub Desktop.
Resque がぶっ壊れる話

起きたこと

Resque が謎の頓死を遂げる。 25 個とか起動してたはずのワーカーが気付いたら 0 個になってる。

対策

Resque のワーカー減らして daemon-spawn をやめて foreman + daemontools で resque を監視するようにした。

というだけではなんのこっちゃなので細かい話

Resque のワーカーがとにかく死にまくるのでなんなんだと思い調べていた。最初は monit とかで再起動させまくるかみたいに判断をする。この時点で Resque は daemon-spawn 経由で起動していた。

daemon-spawn 経由で起動している Resque のワーカーを monit で監視するというのは、大変に都合が悪い。複数あるワーカーのうちどれか一つが死んだみたいな場合でもまるごと再起動みたいな話になる。ならないかもしれないけど調べる時間の余裕がない(サービスに不具合があって金銭的な損害を垂れ流している状況なので)。

というわけで落ちている原因を調べる。

どうやら Redis が発狂して諸々お亡くなりになれられるという話のようだった。なぜ Redis が発狂するかというとどうもコネクションを張りすぎてるとかそんな話っぽい(それで発狂するのかよ!!)。なんでそんなことになってるかというと Resque のワーカーはなんとこの時点で 25 個も起動していた!!!なぜなら Resque がなんか遅いから。

それに関しては Resque の使い方が悪いという話であった。何も考えないと Resque はジョブの実行が終わってから次のジョブを探しにいくまで 5 秒間待つとのこと。

Resque::Worker.new('default').work(0.1)

とかするとインターバル 0.1 秒になって無駄な待ちが発生しなくなって超絶速くなったので 25 個のワーカーの大リストラが行なわれ、 3 個に減らされる。

あんま本題と関係無い話なのだが、 Resque の標準での待ち時間 5 秒というのは以下のような理由によるのだと思う。

  1. Resque はジョブを実行するごとに worker を fork するスタイルである
  2. 故に小さいジョブを大量に実行する場合 fork のコストがシャレにならない
  3. なので小さいジョブを大量に実行するのがそもそも向いていない
  4. 5 秒という待ち時間でも問題ないような長大なジョブを実行するのに使いなさい

というような。僕が今回やっていたことは

  1. 外部のサーバーから画像を取得
  2. RMagick で画像を加工

というものだった。これは同期処理をすると外部のサーバーの通信という予測出来ないコストと、画像の処理という予測は可能だがとにかく時間のかかる処理があるので好ましくない。もともと delayed_job という骨董品で非同期化していた。しかしながら RMagick が ImageMagick の中という Ruby からは手の出せない世界で死亡しまくるという問題が発生するに至った。そこでプロセスを実行ごとに fork して、ジョブが一個頓死したところで問題の発生しない Resque を採用するに至った。 fork のコストなどこの際議論してはいられなかった。

そんなこんなでワーカーの数が減ったので総じてクラッシュは減ったのだが、ついでに daemon-spawn も廃絶しようみたいになった(実際のところ思いついたことを全部平行してやってたのでそんなシリアルに分かりやすく作業が行なわれたわけではない)。

プロセスそのものをデーモン化するというアプローチはそもそも監視がしづらい。スーパーバイザにプロセスを実行させ監視させるほうが望ましいと思う。この辺りいろいろと思想はあると思うが僕はそのように思っている。

もともと今回の件では Web 側は unicorn を foreman に実行させていた。 foreman は daemontools によって監視されていた。そこで foreman に Resque まで面倒を見させることにする。

以下のようなファイル ./script/resque を作成する。

#!/usr/bin/env ruby  
  
require File.expand_path('../../config/application', __FILE__)  
Rails.application.require_environment!  
 
def work
  worker = Resque::Worker.new('default')
  worker.work(0.1)
end

work

Procfile を以下のようにする

web: bundle exec unicorn_rails -E $RAILS_ENV -c unicorn.conf
worker: ./script/resque

そして damontools の run ファイルを以下のようにする。

foreman start worker=3,web=1

実のところこれは問題がある。 foreman は実行しているプロセスにシグナルが届くとどうやら foreman 本体にまで到達してしまう。なので Resque のワーカーに QUIT とかが届くと foreman で実行しているものが丸々死亡してしまう。 daemontools で再起動されるにせよ再起動されるまで数秒とかダウンタイムが発生してしまう。ただしこれは利点でもある。 unicorn を QUIT でぶっ殺して daemontools で再起動させることで新しいコードをデプロイするみたいな方法が取られていた場合、 unicorn を殺せば worker も一緒に死んでくれて楽である。

今回の場合そのような野蛮なデプロイ手段がとられていたこと、そしてワーカーにシグナルを送りつけるなどというのを想定していないことを考慮し、そのまま野蛮な手段が継続された。蛮族ではないという場合は

foreman start worker=3
foreman start web=1

というふうに foreman を二つ起動すればよい。

終わりに

問題自体は Resque 起動しすぎてたという話であり、本質的にその問題と関係の無いことも行なわれている感は否めないが、 foreman と daemontools に何もかも集約されてすっきりして気持ちがいいという話です。

@ytkg
Copy link

ytkg commented Mar 18, 2022

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment