Kubernetesクラスタ上で動かすSidekiqに対してヘルスチェックを導入した話

こんにちは。
マネーフォワード クラウドのアカウント基盤のプラットフォームをプロダクトオーナー兼バックエンドエンジニアとして開発しているkamillleです。
普段はRailsやk8s manifest、Terraformなどを書いています。

今回はKubernetesクラスタ上で動かしているSidekiq(※)プロセスに対してヘルスチェックを導入して、より安全にデプロイが行えるようになった話をご紹介しようと思います。

(※) Railsのアプリケーションで非同期処理を行うためのライブラリ

 

ヘルスチェックで満たしたい要件

今回ヘルスチェックで満たしたかったゴールは下記の2つです。

  1. デプロイ時に新しいSidekiq用のPodが起動したが内部でプロセスが立ち上がらなかった場合は既存Podのterminateを行わない(いわゆるゼロダウンタイムデプロイを高いレベルで実現したい)
  2. Sidekiqのプロセスは生きているがなんらかの理由でジョブを捌けなくなったら検知して再起動する

 
ゴール実現のためにそれぞれ以下の状態を検知できるようにすることが必要と考えました。

  1. Sidekiqが起動し、Redisのキューを捌けるようになった
  2. Sidekiqのプロセスは生きているが処理が止まってしまっている

 
これをKubernetesのヘルスチェック(Readiness, Liveness, Startup Probe)に当てはめると下記になると考えました。

  • Startup Probe、もしくはReadiness Probe(※)で 1. Sidekiqが起動し、Redisのキューを捌けるようになったを検知する
  • Liveness Probeで 2. Sidekiqのプロセスは生きているが処理が止まってしまっているを検知する

(※)Startup ProbeとReadiness Probe、どちらを使うべきかは後述します。

 

どのようにやるか

概要

下記の2つを一つのコードで検知しようと思います。

  1. Sidekiqが起動し、Redisのキューを捌けるようになった
  2. Sidekiqのプロセスは生きているが処理が止まってしまっている

Sidekiqのプロセスは5秒に一度、自身のホスト名やプロセスIDなどの情報をRedisに書き込んでおり(以後、ハートビート)、起動時も同様の挙動が実行され続けます(FYI)。
ハートビートのレコードがRedisに存在するかをチェックし、存在する場合は 1. Sidekiqが起動し、Redisのキューを捌けるようになった を満たしているとみなして良いと考えました。

このハートビートのレコードにはEXPIREが設定されており、60秒で揮発するようになっています(FYI)。
そのため 2. Sidekiqのプロセスは生きているが処理が止まってしまっている という状態が起きた際は60秒後にハートビートのレコードが消え、Sidekiqのプロセスはあるがハートビートが存在しない状態ができます。
これを検知することで 2. Sidekiqのプロセスは生きているが処理が止まってしまっている に気づくことが可能になります。
実際に60秒後にハートビートが揮発することは自分の環境でも確かめてみました。

上記に書いてきたように今回のヘルスチェックのゴールを満たすにはSidekiqがRedisに書き込むハートビートを検証するのが良いと思います。
ただし、注意しなければならないのは下記図のようにSidekiqを複数プロセスで動かすケースです。下記図のようにハートビートはSidekiqのプロセスごとにRedisに書き込まれます。
そのためヘルスチェックをしたいSidekiqのプロセスが書き込んだハートビートのレコードがRedisに存在するかどうかを確認する必要があります。

ハートビートのレコードは下記のような構造になっていて、Sidekiqが動いているサーバーのhostnameが書き込まれるようになっているため、ヘルスチェックしたいSidekiqのプロセスが動いているサーバーのhostnameが含まれたハートビートレコードがあるかどうかをチェックすることが大事です。

# 実際は下記のようなハッシュをJSONに変換したオブジェクトが入っています
{
  "hostname": "sidekiq-b478d66b9-8jrdh",
  "started_at": 1609830466.260034,
  "pid": 25,
  "tag": "app",
  "concurrency": 5,
  "queues":["default"],
  "labels":[],
  "identity":"sidekiq-b478d66b9-8jrdh:25:1efaf2b7ac34"
}

The default hostname for a pod is defined by a pod’s metadata.name value.
https://medium.com/kubernetes-tutorials/kubernetes-dns-for-services-and-pods-664804211501

とあるように、KubernetesのPodのホスト名はデフォルトではPod名と同じになるようになっています。
もしReplicaSetで同一Podを複数作成する定義をしている場合は {hostname}-{UniqueId} という形式で同じ名前のPodが作成されないようにしてくれます。
そのため各Sidekiq用のPodごとにハートビートのレコードがRedisに存在することになります。

ヘルスチェックのコード

Readiness/Liveness/Startup Probeはコマンドを使ったヘルスチェックをサポートしています
SidekiqはHTTPリクエストを受けることはできないので今回はハートビートのレコードの存在有無を検証するコマンドを用意し、それを実行しようと思います。

まずDeploymentの定義は下記のようになりました。
livenessProbe.exec.commandreadinessProbe.exec.command にヘルスチェックで利用するコマンドを記載します。
./bin/sidekiq_health_check がヘルスチェック用のコマンドで、これはSidekiqで起動するRailsアプリケーションのリポジトリに実装しました。

startup Probeを使わない理由

ヘルスチェックを実装した2021/01では、自分が開発しているアプリケーションはEKSのKubernetesクラスタで動いていました。
Kubernetesのバージョンは1.18.9だったのですが、このEKSでは serviceAccountName とstartupProbeの両方をmanifestに記載した場合、startupProbeの定義がPodに当たらないというバグがあり(FYI)、これによってstartupProbeが使えない状態であったため代わりにreadinessProbeを使っています。

2021/02にEKSがKubernetes1.19に対応したためバージョンアップが完了したらstartupProbeを使おうと考えています。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sidekiq
spec:
  template:
    spec:
      serviceAccountName: sidekiq # For IRSA
      containers:
      - name: sidekiq
        command:
        - bundle
        - exec
        - sidekiq
        - -C
        - config/sidekiq.yml
        livenessProbe:
          exec:
            command:
            - ./bin/sidekiq_health_check
          initialDelaySeconds: 60 # Readiness以降にLivenessが実行されてほしいのでこっちは60秒待機する
          periodSeconds: 10
          timeoutSeconds: 10
          successThreshold: 1
          failureThreshold: 3
        readinessProbe:
          exec:
            command:
            - ./bin/sidekiq_health_check
          initialDelaySeconds: 30
          periodSeconds: 60
          timeoutSeconds: 10
          successThreshold: 1
          failureThreshold: 3

./bin/sidekiq_health_check ではrails runnerを利用してヘルスチェック用のRubyのコードを実行しています。rake taskで実行するでもいいと思います。

#!/bin/sh

./bin/rails runner "require Rails.root.join('lib/kubernetes/sidekiq_health_checker.rb'); Kubernetes::SidekiqHealthChecker.new.call!"

Kubernetes::SidekiqHealthChecker#call! では当該Podのhostnameが書き込まれたハートビートがRedisに存在するかどうかをチェックしています。
存在する場合は exit 0 を、存在しない場合は exit 1 を投げます。
KubernetesのProbeは終了コードが 0 の場合、当該コンテナがhealthyと判断し、0 以外の場合はunhealthyと判断してくれます。

Kubernetes::SidekiqHealthChecker はinitialize時に @hostname に当該サーバーのhostnameと、Redisに書き込んでいるハートビートを取得することができる Sidekiq::ProcessSet のインスタンスをバインドしています。
Sidekiq::ProcessSet のインスタンスはRedisから全てのハートビートのレコードを取得し、それをEnumerableなオブジェクトとして提供してくれます。
Sidekiq::ProcessSet の定義はこちらです。

Kubernetes::SidekiqHealthChecker#exit_status メソッドでは Sidekiq::ProcessSet のインスタンスが返したハートビートのレコードの中に当該サーバーのhostnameを含んだレコードが存在するかどうかを検証しています。

# frozen_string_literal: true

module Kubernetes
  # k8sのProbeでSidekiqのヘルスチェックを行うためのクラス
  # Sidekiqはheartbeatといって5秒に一度Redisにホスト名やプロセスIDなどを書き込んでおり、
  # 更新されないと60秒で消えるようになっているため、heartbeatの有無でヘルスチェックが可能になっている
  class SidekiqHealthChecker
    attr_reader :hostname, :process_set

    # :nodoc:
    def initialize
      @hostname = Socket.gethostname
      @process_set = Sidekiq::ProcessSet.new
    end

    # rubyのexitは実行時点でrubyのプロセスが終了するためexitに渡した終了コードをrspecで補足することができない
    # そのためexitを実行するメソッドと終了コードを決めるメソッドを分けている。
    # @see: https://pocke.hatenablog.com/entry/2016/07/17/085928
    # @raise [SystemExit]
    def call!
      exit(exit_status)
    end

    # @return [Integer] 0 or 1
    def exit_status
      # Sidekiqのプロセスが複数存在するためハートビートも複数あることを考慮し #any? を使用する
      if process_set.any? { |process| process['hostname'] == hostname }
        0
      else
        1
      end
    end
  end
end

 

まとめ

今回紹介したヘルスチェックを導入したことでデプロイによって作られた新しいPodが起動はしたがSidekiqのプロセスが立ち上がらないまま古いPodがterminateされジョブを捌けるプロセスがいない状態になってしまったり、なんらかの理由で処理がハングしたSidekiqのプロセスがあれば自動で再起動がかかるようになりました。
これによって安心してデプロイを行うことができるようになったり、何かあった時に手動で復旧させる必要性が低くなったので運用面のコストが下げられてよかったと感じています。

引き続き開発者が安心安全に運用を行える仕組みや運用コストの削減に取り組み、自分だけではなく周囲のエンジニアメンバーがユーザーの課題解決に取り組むための時間を増やせるような活動をしていければと思っています。


マネーフォワードでは、エンジニアを募集しています。
ご応募お待ちしています。

【サイトのご案内】
マネーフォワード採用サイト
Wantedly
京都開発拠点

【プロダクトのご紹介】
お金の見える化サービス 『マネーフォワード ME』 iPhone,iPad Android

ビジネス向けバックオフィス向け業務効率化ソリューション 『マネーフォワード クラウド』

おつり貯金アプリ 『しらたま』

お金の悩みを無料で相談 『マネーフォワード お金の相談』

だれでも貯まって増える お金の体質改善サービス 『マネーフォワード おかねせんせい』

金融商品の比較・申し込みサイト 『Money Forward Mall』

くらしの経済メディア 『MONEY PLUS』

Pocket