Go初心者が始める！gRPCサーバーのヘルスチェックの実装

# 自己紹介

はじめまして！数年前に家電メーカーからSIerに転職したエンジニアです。

転職後に初めてGo言語やgRPC、Kubernetes、マイクロサービスの開発に携わり、日々四苦八苦しながら働いています。

この記事は、学習の一環として自分が調べた内容をまとめたものです。同じようにgRPCやKubernetesの初心者の方に少しでも役立つ情報を提供できればと思います！

# ヘルスチェックとは何か

ヘルスチェックはサーバーが正常にレスポンスを返せる状態かを判断するためのAPIです。
gRPCサーバーのヘルスチェックのRPCのインターフェースは標準のサービスAPIとして下記のように規定されています。
詳細はgRPCの公式ドキュメントをご覧ください: [gRPC Health Checking](https://grpc.io/docs/guides/health-checking/)

```proto
syntax = "proto3";

package grpc.health.v1;

message HealthCheckRequest {
  string service = 1;
}

message HealthCheckResponse {
  enum ServingStatus {
    UNKNOWN = 0;
    SERVING = 1;
    NOT_SERVING = 2;
    SERVICE_UNKNOWN = 3;  // Used only by the Watch method.
  }
  ServingStatus status = 1;
}

service Health {
  rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
  rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}
```

# gRPCサーバーを実装する

## 1. プロジェクトの準備

最初に、プロジェクトのディレクトリを作成します。

```bash
git init grpc-health-service
cd grpc-health-service
```

その後、`grpc_health_v1` をインストールします。

```bash
go mod init grpc-health-service
go get google.golang.org/grpc google.golang.org/grpc/health/grpc_health_v1 google.golang.org/grpc/examples/features/proto/echo
```

`grpc_health_v1` パッケージはgRPCのヘルスチェックのサービスの実装に必要なコードを提供しています。

## 2. サーバーの実装

実際にgRPCサーバーを実装する場合、メインの機能を提供するサービスに加えて、ヘルスチェックのサービスを登録することになります。この記事ではメインの機能を提供するサービスの代わりとして、Echoサービスを登録しています。

### main.go

```go
package main

import (
	"context"
	"google.golang.org/grpc/reflection"
	"log"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/examples/features/proto/echo"
	"google.golang.org/grpc/health/grpc_health_v1"
)

// HealthService implements the gRPC health checking service.
type HealthService struct{}

// Check メソッドの実装
func (h *HealthService) Check(ctx context.Context, req *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) {
	// 商用サービスなら、ここでデータベースに接続できるかなどをチェックする
	return &grpc_health_v1.HealthCheckResponse{
		// サーバーが正常な状態でなければ NOT_SERVING を返すようにする
		Status: grpc_health_v1.HealthCheckResponse_SERVING,
	}, nil
}

// Watch メソッドの実装
func (h *HealthService) Watch(req *grpc_health_v1.HealthCheckRequest, srv grpc_health_v1.Health_WatchServer) error {
	// ここでは常に SERVING と NOT_SERVING を連続して返し続けてい
	// 本来はサーバーの状態に応じてステータスが変わったときだけ返すようにする
	for {
		if err := srv.Send(&grpc_health_v1.HealthCheckResponse{
			Status: grpc_health_v1.HealthCheckResponse_SERVING,
		}); err != nil {
			return err
		}

		if err := srv.Send(&grpc_health_v1.HealthCheckResponse{
			Status: grpc_health_v1.HealthCheckResponse_NOT_SERVING,
		}); err != nil {
			return err
		}
	}
}

// EchoService implements the Echo service defined in the proto file.
type EchoService struct {
	echo.UnimplementedEchoServer
}

// Echo returns the same message sent by the client.
func (e *EchoService) Echo(ctx context.Context, req *echo.EchoRequest) (*echo.EchoResponse, error) {
	return &echo.EchoResponse{Message: req.GetMessage()}, nil
}

func main() {
	listener, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	server := grpc.NewServer()

	// メインのサービスの登録
	echoServer := &EchoService{}
	echo.RegisterEchoServer(server, echoServer)

	// ヘルスサービスの登録
	healthServer := &HealthService{}
	grpc_health_v1.RegisterHealthServer(server, healthServer)

	// grpcurl で proto ファイルを指定せずにサービスを呼び出すため
	reflection.Register(server)

	log.Println("gRPC server is running on port 50051")
	if err := server.Serve(listener); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}

	// 商用サービスならグレースフルシャットダウンなどの実装が必要
}
```

gRPC のヘルスサービスには Check と Watch の２つのメソッドを実装できます。

### CheckメソッドとWatchメソッドの違い

- **Checkメソッド**: クライアントが現在のサーバーの状態を**1回だけ**確認するためのメソッド（Unary RPC）です。ロードバランサーからのサーバーの死活監視や、k8sのliveness probeなどで使用されます
- **Watchメソッド**: サーバーの状態の変化を連続して確認するためのメソッド（Streamin RPC）です。gRPCのドキュメントでは `client side health check` で使うとされていますが、私は使ったことがないので詳細はよくわからないです...

## 3. サーバーの動作確認

クライアントを使ってサーバーが正常に動作するかを確認します。

### grpcurl

`grpcurl` を使用してヘルスチェックを実行します。

```bash
$ go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
$ grpcurl -plaintext localhost:50051 grpc.health.v1.Health/Check
{
  "status": "SERVING"
}
$ grpcurl -plaintext localhost:50051 grpc.health.v1.Health/Watch
{
  "status": "SERVING"
}
{
  "status": "NOT_SERVING"
}
// ずっと繰り返す
```

## 4. KubernetesでのgRPC liveness probe

Kubernetesの v1.27 以降では、gRPCサーバーのヘルスチェックを使用してPodのLiveness Probeを設定することが可能です。
この設定をすると、サーバーが正常に動作しているかをKubernetesが監視し、正常に動作していない場合はpodを再起動してくれます。

以下は、`livenessProbe` の設定例です。

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: grpc-server
spec:
  containers:
  - name: grpc-server
    image: grpc-server-image
    ports:
    - containerPort: 50051
    livenessProbe:
      grpc:
        port: 50051
      initialDelaySeconds: 5
```

# おわりに

この記事では、Goを使用したgRPCヘルスサービスの実装方法を簡単にまとめしました。これからも引き続き、勉強したことをまとめていきたいと思います！