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を使ってヘルスチェックを行います。

f:id:tomiokasyogo:20190427192957p:plain 引用サイト: 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;
}

全体像としては以下のようになります。 f:id:tomiokasyogo:20190427194951p:plain

このサービスを提供するサーバを実装していきます。記事とは関係がないので内容は省略します。 また、ヘルスチェック用として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_v1standard 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 ProbeReadiness 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 ProbeLiveness 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 ProbeReadiness Probeが設定されていることが確認できます。

NAME                       READY   STATUS    RESTARTS   AGE
gateway-54d7cd4b8c-9rtfd   0/1     Running   0          5s

これは Readiness Probeのヘルスチェックが一度も成功していないからです。少し待つとSTATUS1/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

gRPC - What is gRPC?の和訳 from Qiita

Redisのpub/subを軽く触る

Pub/Subとは

Cloud Pub/Sub とは Pub/Subとは言えばGCPのCloud Pub/Subで有名なメッセージングサービスです。一言でいうと、「Publisher」が送信したメッセージを、「Subscriber」達が受け取ることができるものだと考えてください。 f:id:tomiokasyogo:20190315200033p:plain

引用: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企業で働き始めます。現時点では、配属は確定していませんが、広告系が非常に面白い分野だと思っており、広告関係の部署に行くとこになると思います。エンジニアとしてはサーバーサイドに当たりますが、大学時代にはエンジニアらしいことはほとんどしてなかったので、これから色々と勉強していきます。

最後

特に書くこともなかったので、適当ですが以上です。これから頑張っていきます