概要
gRPCサーバのヘルスチェック方法として
- HTTPサーバを別途立ち上げてHTTPでチェック
- tcpでポートがopenしたかチェック
といった方法がありますが、前者はgRPCサーバなのにHTTPサーバを用意しないといけなかったり、後者はtcpのopenは実際にServe開始したタイミングではないといった課題があります。
そこでKubernetesでは3番目の手段として何かしらのヘルスチェックのgRPCをコマンドラインから呼んでチェックする方法を推奨しています。
ref: Health checking gRPC servers on Kubernetes - Kubernetes
環境
- go 1.13.5
- grpc-go 1.27.1
- Kubernetes v1.15.5
GRPC Health Checking Protocol
3番目の手段ですが、実はgRPCの方でヘルスチェック用のプロトコルが定義されています。
grpc/health-checking.md at master · grpc/grpc · GitHub
加えてgrpc-ecosystem/grpc-health-probeという専用のバイナリも用意されています。
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の修正
readinessProbe
とlivenessProbe
を設定します。
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が成功してREADY
が1/1
になり、リクエストを捌ける状態になっています。
サンプルコード
今回のサンプルコードはこちらです。
その他
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