kubernetes上で動くgRPCサーバーのヘルスチェック: Health checking of gRPC server on kubernetes
TL;DR
- KubernetesのLiveness & Readiness Probeを使って、Pod内のコンテナ、プロセスのヘルスチェックが行える
grpc-health-probe
で簡単に実装できる
gRPCについて
gRPCはGoogleによって開発されたRPCシステムです。Protocol Buffersをインターフェースの定義に使用しています。gRPCについては以下の公式サイトで詳細に記述されています。 2017年からCNCFにホストされており、パフォーマンスの面や異なる言語間でもprotoファイルによってインターフェースを定義できるといった特定から、マイクロサービスにおいてよく使用されています。
gRPCサーバーのヘルスチェックについて
gRPCを使ったサーバーを開発し、Kubernetes上で稼働させる際にヘルスチェックを導入しました。しかし、その調査をしていた際に、gRPCサーバーのヘルスチェックの手法に関して日本語での記事が少なかったので今回記事にしました。 Kubernetes上でのgRPCサーバのヘルスチェックに関しては公式サイトにて提案がされています。公式サイトにもあるように2018/10時点ではKubernetesはgRPCサーバのhealth checkingをサポートしていないため、開発者が用意する必要があります。
If you’re unfamiliar, Kubernetes health checks (liveness and readiness probes) is what’s keeping your applications available while you’re sleeping. They detect unresponsive pods, mark them unhealthy, and cause these pods to be restarted or rescheduled. Kubernetes does not support gRPC health checks natively. This leaves the gRPC developers with the following three approaches when they deploy to Kubernetes:
公式サイトでは4つの手法が提案されていますが、本記事では、その中でもおすすめされているgrpc-health-probe
を使ってヘルスチェックを行います。
引用サイト: Health checking gRPC servers on Kubernetes - Kubernetes
図にあるようにcontainer内にstandard health checking protocolを利用してgRPCのサーバのヘルスチェックを行うprobeを導入することでヘルスチェックを行います。ヘルスチェックによってserverのstatusをチェックし、SERVING
が返ってくればOK、そうでなければUnhealthy として判断します。
デモ
今回はGo言語を使用してサーバを実装します。protoファイルは以下のようになっており、名前を送信するとメッセージが返ってくるだけの至極シンプルなサービスです。クライアントとやりとりを行うGateway
サービスと実際のメッセージを生成するBackend
サービスを用意しました。
追記:2019/4/28 ソースコードはこちらに置いておきます
syntax = "proto3"; package proto; service GreetingServer { rpc Greeting(GreetingRequest) returns (GreetingResponse) {} } message GreetingRequest { string name = 1; } message GreetingResponse { string message = 1; }
全体像としては以下のようになります。
このサービスを提供するサーバを実装していきます。記事とは関係がないので内容は省略します。
また、ヘルスチェック用としてstandard health checking protocol
を満たすサーバを実装する必要があります。実際のprotoとしては公式サイトにもあるように以下のprotoで定義されています。
syntax = "proto3"; package grpc.health.v1; message HealthCheckRequest { string service = 1; } message HealthCheckResponse { enum ServingStatus { UNKNOWN = 0; SERVING = 1; NOT_SERVING = 2; } ServingStatus status = 1; } service Health { rpc Check(HealthCheckRequest) returns (HealthCheckResponse); }
実際のサービスの場合は、protoc
などでprotoファイルを元にコードを生成し、interfeceを満たすようにサーバの中身を実装しますが、golangではgoogle.golang.org/grpc/health/grpc_health_v1
にstandard health checking protocol
をカバーしたものがあるので今回はこれを利用します。自分でprotoからファイルを生成しても問題はないと思います。
実装したヘルスチェック用のサーバは以下のようになります。
package server import ( "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/codes" health "google.golang.org/grpc/health/grpc_health_v1" <- ここ "google.golang.org/grpc/status" ) func RegisterHeathCheck(s *grpc.Server){ health.RegisterHealthServer(s, &healthServer{}) } type healthServer struct{} func(h *healthServer)Check(context.Context, *health.HealthCheckRequest) (*health.HealthCheckResponse, error){ return &health.HealthCheckResponse{ Status:health.HealthCheckResponse_SERVING, <- "SERVING"を返す },nil } func (h *healthServer) Watch(*health.HealthCheckRequest, health.Health_WatchServer) error { return status.Error(codes.Unimplemented, "service watch is not implemented current version.") }
standard health checking protocol
にはなかったWatch
メソッドがありますが、これはもう少し発展的なヘルスチェック用に使うことができます。今回は特に使わないのでUNIMPLEMENTED
で返しておきます。他にもgoogle.golang.org/grpc/codes
の中で様々なstatusが定義されており使用することができます。
一旦これでヘルスチェックを通すことができるようになりました。一旦このアプリケーションをapplyしてgrpc-health-probe
コマンドを使用して手動でヘルスチェックをしてみます。すると以下のようにSERVING
status が返ってくることが確認できました。
$ > grpc-health-probe -addr=localhost:XXX status: SERVING
Liveness & Readiness Probe
Kubernetesではクラスタ内のPodの正常判断を行うための機構がLiveness Probe
とReadiness Probe
です。この二種類のヘルスチェック機構はそれぞれ役割と失敗した際の挙動が異なります。
Linevess Probe
Podが正常に動作しているのかどうかをチェックするために使用する。失敗した際にはPodを再起動する
Readiness Probe
Podがサービスを提供できる状態にあるのかどうかをチェックする。失敗した際には該当のPodに対してトラフィックを流さないがPodは再起動しない。
本記事ではこれらの機構を使用してヘルスチェックを行います。手順に関してはgrpc-health-probeのREADME.mdに記述があります。まずはgrpc_health-probe
を使用できるようにDockerfile
に以下を追加します。
RUN GRPC_HEALTH_PROBE_VERSION=v0.2.0 && \ 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のPodマニフェストにReadiness Probe
とLiveness Probe
に関する記述を追加します。
containers: - name: server image: "[YOUR-DOCKER-IMAGE]" ports: - containerPort: 5000 readinessProbe: exec: command: ["/bin/grpc_health_probe", "-addr=:5000"] initialDelaySeconds: 5 livenessProbe: exec: command: ["/bin/grpc_health_probe", "-addr=:5000"] initialDelaySeconds: 10
ヘルスチェックの機構を追加したマニフェストをapplyします。
そうすると、最初はPodのStatusがRunnning
にもかかわらず、READY
が0になっていることが確認できます。
Probe
に関するデータを探すとLiveness Probe
とReadiness Probe
が設定されていることが確認できます。
NAME READY STATUS RESTARTS AGE gateway-54d7cd4b8c-9rtfd 0/1 Running 0 5s
これは Readiness Probe
のヘルスチェックが一度も成功していないからです。少し待つとSTATUS
も1/1
になってPodに対してトラフィックを流せる状態になります。
NAME READY STATUS RESTARTS AGE gateway-54d7cd4b8c-9rtfd 1/1 Running 0 18s
podの設定を確認すると正しくprobeが設定されていることがわかります。細かいオプションや条件に関しては公式サイトに詳しく掲載されています。
$> kd pod | grep probe Liveness: exec [/bin/grpc_health_probe -addr=:50001] delay=10s timeout=1s period=10s #success=1 #failure=3 Readiness: exec [/bin/grpc_health_probe -addr=:50001] delay=5s timeout=1s period=10s #success=1 #failure=3
Readiness probe を失敗させてみる
せっかくですのでわざと、ヘルスチェックが失敗するようにしてみます。Readiness probe
がヘルスチェックを行なっているポート番号を適当に変更して失敗するようにしてapplyしてみます。
そうすると下にあるようにPodは起動するが、一分ほど待ってもREADY
が0のままです。Readiness probe
の紹介の通りの挙動をしていることが確認できます。
NAME READY STATUS RESTARTS AGE gateway-6867dfdcdf-tl7pc 0/1 Running 0 1m
Liveness probe を失敗させてみる
次にLiveness probe
が失敗するようにしてみます。そうすると何回もRESTARTS
が行われ、CrashLoopBackOffになっていることが確認できます。これもLiveness probe
によって正しくヘルスチェックが行えています。
NAME READY STATUS RESTARTS AGE gateway-bc555785b-8vg9l 0/1 CrashLoopBackOff 6 7m
Conculusion
思ってたより簡単にヘルスチェックができました。 ただし、このやり方だとアプリケーション側のコードにも変更が必要だったり、マイクロサービスが大きくなった時に管理が大変だったりしそうなので、IstioやLinkerdなどのコントロールプレーンでヘルスチェックもコントロールできるようになる(もうなってる?)と思うのであくまで現状での解決方に過ぎないと思います。 質問、意見、修正などあれば気軽にコメント、連絡お願いします。
Twitter ID: @tomiokasyogo
参考資料、サイト
https://grpc.io/docs/guides/concepts/ https://www.cncf.io/blog/2017/03/01/cloud-native-computing-foundation-host-grpc-google/ https://grpc.io/docs/guides/concepts/ https://cloud.google.com/blog/products/gcp/kubernetes-best-practices-setting-up-health-checks-with-readiness-and-liveness-probes https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/ https://github.com/grpc-ecosystem/grpc-health-probe
Redisのpub/subを軽く触る
Pub/Subとは
Cloud Pub/Sub とは Pub/Subとは言えばGCPのCloud Pub/Subで有名なメッセージングサービスです。一言でいうと、「Publisher」が送信したメッセージを、「Subscriber」達が受け取ることができるものだと考えてください。
引用:https://cloud.google.com/pubsub/docs/overview
PublisherとSubscriberは両方複数登録することができるので、一対多、多対多の非同期メッセージングを実現することができます。 また、PublisherとSubscriberで処理速度や負荷の違いが有るときに緩衝材として機能させることができます。
Redis-Pub/Sub
KVSで有名なRedisにもPub/Sub機能があります。今回は、RedisのPub/Subを使ってメッセージのやりとりを行なっていきます。言語はGo言語を使います。
実践
今回はPublisher、Subscriberが共に一つだけの場合を考えます。 go-redis/redisを使うと、以下のようにPublisher、Subscriberを構造体として表せます。
//Publisherの構造体.redisのクライアントやPub/Subに関するデータをもつ type Publisher struct{ redis *redis.Client channel string pubsub *redis.PubSub } func NewPublisher(channel string) *Publisher { client := NewRedis() return &Publisher{ redis:client, channel:channel, pubsub:client.Subscribe(channel), } } //SubScriber用のチャネルを生成 func (p Publisher) SubChannel() <-chan *redis.Message{ _, err := p.pubsub.Receive() if err != nil { panic(err) } return p.pubsub.Channel() } func (p Publisher)Close() error { err := p.pubsub.Close() return err } //メッセージの送信 func (p Publisher) Publish(message string) error { err := p.redis.Publish(p.channel,message).Err() return err }
//SubScriberはメッセージを受信するチャネルを持つ type Subscriber struct { ch <-chan *redis.Message } func (s Subscriber)RecieveMessage() { for msg:= range s.ch{ fmt.Println("recieve: ",msg) }
Publisherは、Redisのクライアントをもち、Pub/Sub通信を行う部屋の名前とredisのPubSub用の構造体を持っています。PublishメソッドによってPub/Subにメッセージを送信します。 Subscriberに関しては、Go言語には「チャネル」という非同期通信をするのに便利な機能があり、今回はメッセージの受信に受信専用のチャネルを使用しています。
実際のメッセージのやりとり
実際にPublisherとSubscriberを準備し、メッセージのやりとりを行います
publisher := NewPublisher("channel1") subscriber := Subscriber{ ch: publisher.SubChannel(), } go func() { for i := 0; i < 10; i++ { time.Sleep(1 * time.Second) //時間の送信 err := publisher.Publish(time.Now().String()) if err != nil { log.Fatal(err) } } publisher.Close() }() // messageの受信 for msg := range subscriber.ch { fmt.Println(msg.Payload) }
Publisherは10秒間の間、その時の時間を送信します。SubScriberは最後のfor文でチャネルがCloseされるまでの間、チャネルから送信されたデータを読み込みます。 そうすると以下のような結果が得られます。
2019-03-15 18:43:01.059707 +0900 JST m=+1.013542296 2019-03-15 18:43:02.066162 +0900 JST m=+2.019999777 2019-03-15 18:43:03.073749 +0900 JST m=+3.027588299 2019-03-15 18:43:04.080888 +0900 JST m=+4.034729674 2019-03-15 18:43:05.087476 +0900 JST m=+5.041319857 2019-03-15 18:43:06.092702 +0900 JST m=+6.046548033 2019-03-15 18:43:07.096567 +0900 JST m=+7.050414716 2019-03-15 18:43:08.10268 +0900 JST m=+8.056530136 2019-03-15 18:43:09.109732 +0900 JST m=+9.063583712 2019-03-15 18:43:10.115859 +0900 JST m=+10.069712490
単純な例ですが、これでPub/Sub通信をすることができました。
最後に
最近ではマイクロサービス化が進んでいるので、様々なサービスの間で通信が行われることが想定されますが、そういったサービス間の通信の一つとしてPub/Sub通信は非常に便利な機能なので使う機会があればぜひ使っていきましょう
東京に引っ越した
渋谷にお引越ししました
TL;DR
京都の高専を卒業して、四年間通った愛知県の大学院を修了して四月から渋谷のIT企業で働き始めます。 技術的な記事はQiitaに書いてましたが、せっかくなのではてなの方にも技術系の記事を書いて行く予定です。 最近はGo、kuberntes関連の勉強中なのでその辺をやってきます。
お前は誰だ?
京都生まれ京都育ちの新卒エンジニアです。 京都の舞鶴高等専門学校(通称:高専)を出て、愛知の豊橋技術科学大学の三年次に編入して、大学院の博士前期まで在籍していました。 高専時代は電気・情報、大学、大学院からは、CS系の学科に所属していました。 高専時代は野球しか頭になかった野球少年でしたが、大学に編入してからは、量子化学計算系の研究室に在籍し、アルツハイマー病の原因になっているとされているタンパク質の研究なんかをしていました。
なにする人?
今年(2019年)の四月から渋谷にある緑色のIT企業で働き始めます。現時点では、配属は確定していませんが、広告系が非常に面白い分野だと思っており、広告関係の部署に行くとこになると思います。エンジニアとしてはサーバーサイドに当たりますが、大学時代にはエンジニアらしいことはほとんどしてなかったので、これから色々と勉強していきます。
最後
特に書くこともなかったので、適当ですが以上です。これから頑張っていきます