最近、Kubernetesのヘルスチェックについての質問をよく見ています。ここでヘルスチェックの種類の違いや、どう使うか説明してみます。

Liveness Probe

Kubernetesのヘルスチェックは2種類があって、一つ目はlivenessProbeと、2つ目はreadinessProbeというやつです。livenessProbeの役目はアプリケーションが生きてるかどうかをチェックすること。普段、エラーが起きた時に、アプリがクラッシュで終了して、Kubernetesがそれを見て、再起動してくれるんですけど、livenessProbeはアプリが終了せずに動かなくなったり、デッドロックしたりする場合にもアプリを再起動して直すために存在する。アプリがちゃんと動いているだけをチェックしているので、単純にHTTPレスポンスを返せばいいはず。

簡単な例として、以下はGoアプリのlivenessProbeの実装。

http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("OK"))
}
http.ListenAndServe(":8080", nil)

Deploymentのほうはこんな感じ

livenessProbe:
  # an http probe
  httpGet:
    path: /healthz
    port: 8080
    initialDelaySeconds: 15
    timeoutSeconds: 1

このlivenessProbeはアプリケーションが生きているだけをチェックします。initialDelaySecondsはアプリを起動してから、何秒後にヘルスチェックを始めるかを示している。例えば、起動するまで時間かかるようなアプリケーションだとこの設定を指定すると便利。timeoutSecondsはヘルスチェックのレスポンスを何秒待つかを示す。livenessProbeの場合はこれをできるだけ短くしたほうが早く検知するので復活が速い。でも、注意すべき点があって、負荷がかかっている状態でも適切なタイムアウトを設定しないと、一番忙しい時なのにアプリが再起動されてしまったり、パフォーマンスに影響がでる。なので、適切なtimeoutSecondsを指定するのが大事。

アプリケーションのlivenessProbeに接続できなかったり、HTTPエラーコードが返ってきた場合、Kubernetesはコンテナを素早く再起動しますし、アプリケーションの障害の可能性になるので、livenessProbeでは複雑な処理などをしないほうが良い。

Readiness Probe

readinesProbelivelinessProbeと似ているものだけど、readinessProbeが失敗した結果が違います。readinessProbeはアプリケーションが生きているかどうかじゃなくて、トラフィックを受けられるかどうかを確認するためのヘルスチェック。livelinessProbeとは微妙に違います。例えば、アプリケーションがデータベースや、memcachedに依存している場合、この二つのサービスが動いていて、接続もできて、さらにアプリケーションも大丈夫な状態じゃないとトラフィックを受けられない。

readinessProbeが失敗した場合、そのパッドがサービスのエンドポイントから外される。そうすると、Kubernetesのサービスディスカバリー機能でトラフィックを受けられないポッドにトラフィックを転送しない。例えば、ローリングアップデートの時や、スケールアップした時、新しいポッドが機能されていたんだけど、まだトラフィック受けられないタイミングでリクエストが来たら、困るので、readinessProbeでそういうことを防ぐ。

readinessProbeの書き方はlivenessProbeと同じ。Deploymentの中に書く:

readinessProbe:
  # an http probe
  httpGet:
    path: /readiness
    port: 8080
    initialDelaySeconds: 20
    timeoutSeconds: 5

readinesProbeの実装では、アプリケーションの依存サービスに接続できるかどうかをチェックする。一つの例として、データベースとmemcachedに依存するアプリケーションのreadinessProbeを実装する。

以下のハンドラーのような感じになります。ここでは memcachedへ書き込みができるかどうか、かつ、データベースへの接続ができるかどうかをチェックします。どっちかがダメな場合は503を返す。

http.HandleFunc("/readiness", func(w http.ResponseWriter, r *http.Request) {
  ok := true
  errMsg = ""

  // Check memcache
  if mc != nil {
    err := mc.Set(&memcache.Item{Key: "healthz", Value: []byte("test")})
  }
  if mc == nil || err != nil {
    ok = false
    errMsg += "Memcached not ok.¥n"
  }

  // Check database
  if db != nil {
    _, err := db.Query("SELECT 1;")
  }
  if db == nil || err != nil {
    ok = false
    errMsg += "Database not ok.¥n"
  } 

  if ok {
    w.Write([]byte("OK"))
  } else {
    // Send 503
    http.Error(w, errMsg, http.StatusServiceUnavailable)
  }
})
http.ListenAndServe(":8080", nil)

より安定性のあるアプリケーション

livenessProbereadinessProbeはどっちもアプリケーションの安定性に助かる機能。トラフィックを受けられるコンテナだけにトラフィックを転送しないようにしてくれるし、クラッシュしたコンテナや、固まったコンテナを再起動してくれるし、非常に便利。そして、私の同僚のKelsey Hightowerが解説した 12 Fractured Appsの解決策でもある。ヘルスチェックがあれば、依存するサービスの起動を待ったりする複雑なEntrypointスクリプトはいらない。トラフィックを受けられる時だけ受けるし、ローリングアップデートやスケールアップがスムーズに動きます。