理解して使うsidekiq-worker-killer

# 1. 概要
sidekiqプロセスのメモリ使用率が元に戻らない悩みを解消するためのイチ手段として、 
**gem「[sidekiq-worker-killer](https://github.com/klaxit/sidekiq-worker-killer)」を使う** 
がある。

残念ながら、上記GitHubのReadmeのみでは、その詳細な挙動を知ることは難しい。
そこで本記事では、sidekiq-worker-killerのソースコード(ver.1.0.0)、およびその他関連情報を調査し、sidekiq-worker-killerの設定項目・挙動について全体的にまとめた。

- sidekiq-worker-killerの設定の意味・設定方法
- sidekiq-worker-killerの処理開始タイミング
- sidekiq-worker-killerの処理開始後の挙動

# 2. sidekiq-worker-killerの設定の意味・設定方法
sidekiq-worker-killerのオプション設定について、それぞれざっくり解説する。
それぞれのオプションがいつ・どのように使われるのか？の詳細は「4. sidekiq-worker-killerの処理開始後の挙動」の項を参照。

## 2.1. 設定オプション一覧

オプション|ざっくり説明(+見解・関連情報)
---|---
max_rss|sidekiqプロセスのメモリ使用量上限値(MB)。<br> この値を超えていると、sidekiq-worker-killerの処理が実行される。<br>メモリ使用量は、[get_process_mem](https://github.com/schneems/get_process_mem)というgemを使って取得している。
gc|sidekiqプロセスのメモリ使用量が、「max_rss」を超えた時、garbage_colletion(GC)を実行する。デフォルト設定はtrue(実行する)
skip_shutdown_if|GC実行後、「skip_shutdown_if」で指定したブロック内の処理を実行する。<br>ブロック内の処理結果が「false or nilでない」場合、sidekiq-worker-killerの処理は中断される。 <br>**このブロック内で、これからsidekiqプロセスキルしますよー！のslack通知やらなんやら、色々させることができる。gem利用者にとって最も挙動をカスタマイズできる可能性がある機能だと思う**
grace_time|「skip_shutdown_if」の処理実行後、sidekiq-worker-killerは、TSTPシグナルを送って新しいジョブを受け付けないようにした後、 「実行中ジョブがあるならば、最大grace_time(秒)待って」sidekiqプロセスのキルを試みる(TERMシグナルを送る)。Float::INFINITYが指定されると、「実行中ジョブの終了を永遠に待つ」になる。
kill_signal|sidekiq-worker-killerが、sidekiqプロセスのキルを試みた(=TERMシグナルを送って★、gem「sidekiq」にプロセスキル処理を任せた)後、それでもだめだったときに「kill_signal」で設定したシグナルをsidekiqプロセスに送信する。<br>ざっくりいうとlinuxコマンドの<br>**``kill -<kill_signal> <sidekiqのPID> ``**<br>を実行しているような形。<br>**デフォルト設定は"SIGKILL"であり、この場合は問答無用でsidekiqプロセスが強制終了される**。
shutdown_wait|★実行後、「kill_signal]をsidekiqプロセスに送るまでの猶予時間(秒)


## 2.2. 設定オプションの記述

sidekiqの設定ファイルで、以下のように書いてやればいい(設定全てを施す場合)。

```
require 'sidekiq/worker_killer'

Sidekiq.configure_server do |config|
  config.server_middleware do |chain|
    chain.add Sidekiq::WorkerKiller, max_rss: <任意のmax_rss>, grace_time: <任意のgrace_time>, shutdown_wait: <任意の値>, kill_signal: <任意の値>, gc: <任意の値>, skip_shutdown_if: ->(worker, job, queue) do
      <任意の処理. slackへの通知だったりもここへ書けばOK>
    end
  end
end
```

# 3. sidekiq-worker-killerの処理開始タイミング
**sidekiqでの各ジョブの実行が完了した後にそれぞれで実行される** 
※動作確認済み

この挙動になる理由は、以下の参考記事を組みあわせればわかる。

- [Sidekiq ミドルウェア [翻訳]](https://qiita.com/ts-3156/items/909932e5f7a7023a2d47)
- [Module: Sidekiq::Middleware](https://www.rubydoc.info/github/mperham/sidekiq/Sidekiq/Middleware)
- [sidekiq-worker-killer(ver.1.0.0)のcallメソッドの実装](https://github.com/klaxit/sidekiq-worker-killer/blob/v1.0.0/lib/sidekiq/worker_killer.rb)


ざっくりいうと、以下の感じ

```
①sidekiqミドルウェアの設定を施すと以下のタイミングで実行される
unicorn (CLミドルウェア実行)
→ redis (ジョブ登録)
→ sidekiq(ジョブ実行前(SVミドルウェア実行★)
→ 実行中
→ 実行後(SVミドルウェア実行★))

②★についてSVミドルウェアの実行タイミングは、そのミドルウェアクラスで実装されたcallメソッドのコードによる。
(yieldの前に処理が記述されているならジョブ実行前、後ならジョブ実行後に実行される)

③sidekiq-worker-killerもsidekiqミドルウェアの一種。
そのcallメソッドの実装を確認すると、ジョブの実行後にミドルウェア処理が走るタイプだとわかる。
```



# 4. sidekiq-worker-killerの処理開始後の挙動
## 4.1. 概要
上記オプションでの設定を踏まえ、「sidekiq-worker-killer」はざっくり以下のように動作する。

**① 現在のメモリ使用量がオプション「max_rss」の値以上かをチェックし、該当するなら処理継続**

**② オプション「gc」がtrueならGCを実行してメモリ解放がんばってみる**

**③ 再度、現在のメモリ使用量がオプション「max_rss」の値以上かをチェックし、該当するなら処理継続**

**④ オプション「skip_shutdown_if」の処理を実行。結果が「false or nil」なら】処理継続**

**⑤ sidekiqプロセスを「stopping(=Redisからの新しいジョブの取得を受け付けない)」状態にする**

**⑥ 【sidekiqプロセスがstopping状態である】または【⑤からオプション「grace_time」秒経過している】ならば(sidekiqの機能を利用して)sidekiqプロセスの終了をする**

**⑦ ⑥からオプション「shutdown_wait」秒経過したならば、(sidekiqの機能を利用せずに)sidekiqプロセスの強制終了をする**

※あくまでsidekiqプロセスをキルするだけなので、再起動させたいなら、別途手段を用意する(monit使ったり)必要あり


## 4.2. 詳細
※[ver1.0.0](https://github.com/klaxit/sidekiq-worker-killer/blob/v1.0.0/lib/sidekiq/worker_killer.rb)のコードを読み解いた結果です。間違ってたらごめんなさいmm

No.|処理概要|補足
---|---|---
1|オプション「max_rss」の設定値が0以下である場合、sidekiq-worker-killerの処理終了|
2|現在のメモリ使用量 > max_rss でないなら処理終了|
3|オプション「gc」がtrueである場合、即座にgarbage collection(現時点で不要なオブジェクト全てを削除 & 同期処理)を実行 |
4|現在のメモリ使用量 > max_rss でないなら処理終了|
5|オプション「skip_shutdown_if」の処理結果がtrueならば、sidekiqログ「メモリ使用量がmax_rss超えてるけどsidekiqプロセスキルはやめたよ！」を吐いて処理終了|実際のログ内容：<br>``current RSS <現在のメモリ使用量> exeeds maximum RSS <max_rss>, however shutdown will be ignored``
6|sidekiqログ「メモリ使用量がmax_rss超えてるよ！」を吐く|実際のログ内容：<br>``current RSS <現在のメモリ使用量> of <システムのホスト名>:<sidekiqプロセスのPID>:<乱数> exceeds maximum RSS <max_rss>``
7|request_shutdownメソッドを呼び出す|

**request_shutdownメソッド**
スレッドを生成して、非同期でshutdownメソッドを実行する

**shutdownメソッド**

No.|処理概要|補足
---|---|---
1|sidekiqログ「sidekiqプロセスにquietしろ(stopping状態 = redisから新しいジョブを取得しない状態)というよ」を吐く|実際のログ内容：<br>``sending quiet to <システムのホスト名>:<sidekiqプロセスのPID>:<乱数>``
2|sidekiqプロセスをstopping状態にする(sidekiqプロセスにTSTPシグナルを送る)|
3|5秒スリープ|
4|sidekiqログ「grace_time秒後にsidekiqプロセスを強制終了するよ！」を吐く|実際のログ内容：<br>``shutting down <システムのホスト名>:<sidekiqプロセスのPID>:<乱数> in <grace_time> seconds``
5|【現在日時からオプション「grace_time」秒経過した】　or 【sidekiqプロセスがstopping状態になる】まで待機する|
6|sidekiqログ「sidekiqプロセスがstopping状態だよ！」を吐く|実際のログ内容：<br>``stopping <システムのホスト名>:<sidekiqプロセスのPID>:<乱数>``
7|sidekiqプロセスで処理中の全ジョブを、sidekiqに設定されているタイムアウト値分経過したら強制終了して、Redisに記録し、sidekiqプロセスをキルする(sidkiqプロセスにTERMシグナルを送る)|
8|sidekiqログ「kill_signalをsidekiqプロセスへ送る前にshutdown_wait待つよ！」を吐く|実際のログ内容：<br>``waiting <shutdown_wait> seconds before sending <kill_signal> to <システムのホスト名>:<sidekiqプロセスのPID>:<乱数>``
9|オプション「shutdown_wait」秒待つ|待っている間にsidekiqプロセスがキルされたら以降の処理は実行されない|
10|sidekiqログ「sidekiqプロセスへkill_signalをおくるよ！」を吐く|実際のログ内容：<br>``sending <kill_signal> to <システムのホスト名>:<sidekiqプロセスのPID>:<乱数>``
11|Linuxコマンドでいうところの、``kill -9 <sidekiqプロセスのPID>``を実行する|gem「sidekiq」の機能外部からのプロセスキル実行になる。保険的な実装と思われる|

**参考**

- [Quiet vs Stop in Sidekiq](https://stackoverflow.com/questions/44022068/quiet-vs-stop-in-sidekiq)
- [Signals · mperham/sidekiq Wiki](https://github.com/mperham/sidekiq/wiki/Signals) ※sidekiqプロセスに送る強制終了系シグナル一覧

## 4.3. メモ: RubyのGCの仕組み
### 4.3.1. 概要
- メモリにRubyのオブジェクトを割り当てる。
- メモリが足りなくなったら実行中のプログラムを停止して、使ってないオブジェクトを削除する。

参考: https://dev.classmethod.jp/articles/about_processor_and_gc_in_ruby/

### 4.3.2. 詳細
[GC.start](https://docs.ruby-lang.org/ja/latest/class/GC.html#S_START)メソッドでGCを実行する。
※sidekiq-worker-killer内で「full_gc:true, immediate_sweep: true」で実行されているメソッド。

#### 4.3.2.1. 引数
- full_gc
  - falseのときは、前回のGC実行後に生成されたオブジェクトのみを対象としたマイナーGCを実行する。
  - マイナーGCだと、前回GC時点では、生存していたオブジェクトには、マークがついて削除されたわけだが、今回GC時点では、もう不要になったオブジェクトなのに、前回GC以前に作られていたオブジェクトが存在するわけで、これは削除対象にならないといったことが発生しそう。
- immediate_sweep 
  - マークされたオブジェクトについて、true: その削除を全て即座にやる、false:必要に応じてちょっとずつ削除していく(lazy sweepというらしいが詳細はよくわからない・・・)の設定らしい。lazy sweepについては論文らしきものも出てくる

## 5. 実際の利用例
sidekiq-worker-killerに完全にプロセスキルを任せると、
shutdownメソッド - No.7 の通り、Redisに現在実行中のジョブをPushしてプロセスが終了する。
この場合、**sidekiqプロセスを復活させると、Redisから実行中のジョブを取得して再実行される。**

導入したプロジェクトでは、**実行中のジョブもきれいさっぱり消し飛ばしてほしかったため**、
**sidekiq-worker-killerには、「上限RSSの超過判定とGCだけ実行してもらい、プロセスのキルは、SIGKILLを送って、RedisへのPushが行われないようにキルする」** ように実装した。

```
Sidekiq.configure_server do |config|
  config.server_middleware do |chain|
    max_rss = <任意の値>
    kill_signal = "SIGKILL"
    gc = true

    # sidekiq-worker-kilerには、メモリ上限到達判定とGC実行だけやらせる
    chain.add Sidekiq::WorkerKiller, max_rss: max_rss, gc: gc, skip_shutdown_if: ->(worker, job, queue) do
      <Slackへの通知処理>
      # Linuxコマンド「kill -9 <sidekiqのPID>」実行相当
      ::Process.kill(kill_signal, ::Process.pid) 
      true
    end
  end
end
```