こんにちは。 マネーフォワード クラウドのアカウント基盤のプラットフォームをプロダクトオーナー兼バックエンドエンジニアとして開発しているkamillleです。 普段はRailsやk8s manifest、Terraformなどを書いています。
今回はKubernetesクラスタ上で動かしているSidekiq(※)プロセスに対してヘルスチェックを導入して、より安全にデプロイが行えるようになった話をご紹介しようと思います。
(※) Railsのアプリケーションで非同期処理を行うためのライブラリ
ヘルスチェックで満たしたい要件
今回ヘルスチェックで満たしたかったゴールは下記の2つです。
- デプロイ時に新しいSidekiq用のPodが起動したが内部でプロセスが立ち上がらなかった場合は既存Podのterminateを行わない(いわゆるゼロダウンタイムデプロイを高いレベルで実現したい)
- Sidekiqのプロセスは生きているがなんらかの理由でジョブを捌けなくなったら検知して再起動する
ゴール実現のためにそれぞれ以下の状態を検知できるようにすることが必要と考えました。
- Sidekiqが起動し、Redisのキューを捌けるようになった
- Sidekiqのプロセスは生きているが処理が止まってしまっている
これをKubernetesのヘルスチェック(Readiness, Liveness, Startup Probe)に当てはめると下記になると考えました。
- Startup Probe、もしくはReadiness Probe(※)で
1. Sidekiqが起動し、Redisのキューを捌けるようになった
を検知する - Liveness Probeで
2. Sidekiqのプロセスは生きているが処理が止まってしまっている
を検知する
(※)Startup ProbeとReadiness Probe、どちらを使うべきかは後述します。
どのようにやるか
概要
下記の2つを一つのコードで検知しようと思います。
- Sidekiqが起動し、Redisのキューを捌けるようになった
- 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.command
と readinessProbe.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
■ビジネス向けバックオフィス向け業務効率化ソリューション 『マネーフォワード クラウド』
■だれでも貯まって増える お金の体質改善サービス 『マネーフォワード おかねせんせい』