Carpe Diem

備忘録

gRPCのヘルスチェック

概要

gRPCサーバのヘルスチェック方法として

  • HTTPサーバを別途立ち上げてHTTPでチェック
  • tcpでポートがopenしたかチェック

といった方法がありますが、前者はgRPCサーバなのにHTTPサーバを用意しないといけなかったり、後者はtcpのopenは実際にServe開始したタイミングではないといった課題があります。

そこでKubernetesでは3番目の手段として何かしらのヘルスチェックのgRPCをコマンドラインから呼んでチェックする方法を推奨しています。

f:id:quoll00:20200212233923p:plain

ref: Health checking gRPC servers on Kubernetes - Kubernetes

環境

GRPC Health Checking Protocol

3番目の手段ですが、実はgRPCの方でヘルスチェック用のプロトコルが定義されています。

grpc/health-checking.md at master · grpc/grpc · GitHub

加えてgrpc-ecosystem/grpc-health-probeという専用のバイナリも用意されています。

f:id:quoll00:20200212234438p:plain

ref: Health checking gRPC servers on Kubernetes - Kubernetes

なのでこちらを使ってヘルスチェック機構を作ります。

grpc-health-probeを使ってみる

導入には以下の対応を行います。

  • goコードの修正(ヘルスチェックprotocolを実装)
  • Dockerfileの修正(ヘルスチェックバイナリを入れる)
  • Kubernetes Manifestの修正(probeの種類をcommandに)

goコードの修正

grpc_health_v1 - GoDocを実装します。

package main

import (
    "context"

    "google.golang.org/grpc/codes"
    health "google.golang.org/grpc/health/grpc_health_v1"  // here
    "google.golang.org/grpc/status"

    pb "github.com/jun06t/grpc-sample/unary/proto"
)

type helloHandler struct{}

func (h *helloHandler) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

type healthHandler struct {
}

// here
func (h *healthHandler) Check(context.Context, *health.HealthCheckRequest) (*health.HealthCheckResponse, error) {
    return &health.HealthCheckResponse{
        Status: health.HealthCheckResponse_SERVING,
    }, nil
}

// here
func (h *healthHandler) Watch(*health.HealthCheckRequest, health.Health_WatchServer) error {
    return status.Error(codes.Unimplemented, "watch is not implemented.")
}

Watch()の方はstreamingでチェックするメソッドで、今回は不要なので中身は実装しません。

実装したstructをRegisterします。

func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatal(err)
    }

    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &helloHandler{})
    health.RegisterHealthServer(s, &healthHandler{})  // here
    err = s.Serve(lis)
    if err != nil {
        log.Fatal(err)
    }
}

Dockerfileの修正

grpc-health-probeのバイナリをdocker imageに入れます。
今回はalpine linuxなのでlinux-amd64を使います。

FROM golang:alpine3.10

...

RUN GRPC_HEALTH_PROBE_VERSION=v0.3.1 && \
  wget -qO/bin/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64 && \
  chmod +x /bin/grpc_health_probe

...

Kubernetes Manifestの修正

readinessProbelivenessProbeを設定します。

spec:
  containers:
  - name: probe
    image: "jun06t/probe"
    ports:
    - containerPort: 8080
    readinessProbe:
      exec:
        command: ["/bin/grpc_health_probe", "-addr=:8080"]
      initialDelaySeconds: 5
    livenessProbe:
      exec:
        command: ["/bin/grpc_health_probe", "-addr=:8080"]
      initialDelaySeconds: 10

readinessProbeとlivenessProbeの区別

大まかに区別すると以下です。

probe 役割
livenessProbe Podが正常に動作しているか
readinessProbe Podがサービスインしているか
(リクエストをさばいたりできる状態か)

ではreadinessProbeだけで十分ではないか?と感じますが、挙動が異なります。

probe 成功するとどうなるか 失敗するとどうなるか
livenessProbe 何もなし Podを再起動する
readinessProbe READYステータスが満たされリクエストがPodへ流れる READYステータスが満たされずリクエストがPodへ流れない

例えばずっと起動しているとメモリリークなどで処理ができなくなるPodが出てくることもあります。
そういった時はPodを再起動すれば自動的に回復できます。これはreadinessProbeでは実現できません。

「それならreadinessProbeが再起動もしてくれれば良いのに」と思うかもしれませんが、再起動というのは時間がかかる処理なのでその間他のPodへトラフィックが集中してしまい連鎖的にPodが落ちるケースもあります。

そういったユースケースに対応するため、livenessProbeとreadinessProbeを使い分ける必要があります。

動作検証

applyしてみます。

probe $ kubectl get po
NAME                     READY   STATUS    RESTARTS   AGE
probe-8644f7f8c8-b92n6   0/1     Running   0          12s
probe-8644f7f8c8-x6tjb   0/1     Running   0          12s
...
probe $ kubectl get po
NAME                     READY   STATUS    RESTARTS   AGE
probe-8644f7f8c8-b92n6   0/1     Running   0          12s
probe-8644f7f8c8-x6tjb   1/1     Running   0          12s
...
probe $ kubectl get po
NAME                     READY   STATUS    RESTARTS   AGE
probe-8644f7f8c8-b92n6   1/1     Running   0          17s
probe-8644f7f8c8-x6tjb   1/1     Running   0          17s

readinessProbeが成功してREADY1/1になり、リクエストを捌ける状態になっています。

サンプルコード

今回のサンプルコードはこちらです。

github.com

その他

KubernetesがgRPCヘルスチェックをサポートする可能性は?

以下のissueを読む限り、Kubernetesではサポートされなそうです。

We're not adding grpc any time soon. HTTP2 should be a different topic entirely.

Support gRPC health checks · Issue #21493 · kubernetes/kubernetes · GitHub