Go 語言實現 gRPC Health 驗證

grpc_square_reverse_4x

本篇教大家如何每隔一段時間驗證 gRPC 服務是否存活,如果想了解什麼是 gRPC 可以參考 這篇『REST 的另一個選擇:gRPC』,這邊就不多介紹 gRPC 了,未來將會是容器的時代, 那該如何檢查容器 Container 是否存活。如果是用 Kubernetes 呢?該如何來撰寫 gRPC 接口搭配 livenessProbe 設定。底下是在 Dockerfile 內可以設定 HEALTHCHECK 來 達到檢查容器是否存活。詳細說明可以參考此連結

1
2
HEALTHCHECK --interval=5m --timeout=3s \
  CMD curl -f http://localhost/ || exit 1

建立 Health Check proto 接口

打開您的 *.proto 檔案,並且寫入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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);
}

存檔後重新產生 Go 程式碼: 檔案存放在 rpc/proto 目錄

1
$ protoc -I rpc/proto rpc/proto/gorush.proto --go_out=plugins=grpc:rpc/proto

或者在 Makefile 內驗證 proto 檔案是否變動才執行:

1
2
3
rpc/proto/gorush.pb.go: rpc/proto/gorush.proto
    protoc -I rpc/proto rpc/proto/gorush.proto \
    --go_out=plugins=grpc:rpc/proto

程式碼連結

建立 Health Interface

如果還有其他接口需要驗證,這就必須建立一個 Health Interface 讓你的服務可以驗證多種 protocol, 建立 health.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package rpc

import (
    "context"
)

// Health defines a health-check connection.
type Health interface {
    // Check returns if server is healthy or not
    Check(c context.Context) (bool, error)
}

程式碼連結

建立 gRPC 服務

首先要定義一個 Server 結構來實現 Check 接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type Server struct {
    mu sync.Mutex
    // statusMap stores the serving status of the services this Server monitors.
    statusMap map[string]proto.HealthCheckResponse_ServingStatus
}

// NewServer returns a new Server.
func NewServer() *Server {
    return &Server{
        statusMap: make(map[string]proto.HealthCheckResponse_ServingStatus),
    }
}

這邊可以看到,gRPC 的狀態可以從 proto 產生的 Go 檔案拿到,打開 *.pb.go,可以找到如下

1
2
3
4
5
6
7
type HealthCheckResponse_ServingStatus int32

const (
    HealthCheckResponse_UNKNOWN     HealthCheckResponse_ServingStatus = 0
    HealthCheckResponse_SERVING     HealthCheckResponse_ServingStatus = 1
    HealthCheckResponse_NOT_SERVING HealthCheckResponse_ServingStatus = 2
)

接著來實現 Check 接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Check implements `service Health`.
func (s *Server) Check(ctx context.Context, in *proto.HealthCheckRequest) (*proto.HealthCheckResponse, error) {
    s.mu.Lock()
    defer s.mu.Unlock()
    if in.Service == "" {
        // check the server overall health status.
        return &proto.HealthCheckResponse{
            Status: proto.HealthCheckResponse_SERVING,
        }, nil
    }
    if status, ok := s.statusMap[in.Service]; ok {
        return &proto.HealthCheckResponse{
            Status: status,
        }, nil
    }
    return nil, status.Error(codes.NotFound, "unknown service")
}

上面可以看到透過帶入 proto.HealthCheckRequest 得到 gRPC 的回覆,這邊通常都是帶空值, gRPC 會自動回 1,最後在啟動 gRPC 服務前把 Health Service 註冊上去

1
2
3
4
5
    s := grpc.NewServer()
    srv := NewServer()
    proto.RegisterHealthServer(s, srv)
    // Register reflection service on gRPC server.
    reflection.Register(s)

這樣大致上完成了 gRPC 伺服器端實作

程式碼連結

建立 Client 套件

一樣可以透過 proto 產生的程式碼來撰寫 Client 驗證,建立 client.go 裡面寫入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package rpc

import (
    "context"

    "github.com/appleboy/gorush/rpc/proto"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
)

// generate protobuffs
//   protoc --go_out=plugins=grpc,import_path=proto:. *.proto

type healthClient struct {
    client proto.HealthClient
    conn   *grpc.ClientConn
}

// NewGrpcHealthClient returns a new grpc Client.
func NewGrpcHealthClient(conn *grpc.ClientConn) Health {
    client := new(healthClient)
    client.client = proto.NewHealthClient(conn)
    client.conn = conn
    return client
}

func (c *healthClient) Close() error {
    return c.conn.Close()
}

func (c *healthClient) Check(ctx context.Context) (bool, error) {
    var res *proto.HealthCheckResponse
    var err error
    req := new(proto.HealthCheckRequest)

    res, err = c.client.Check(ctx, req)
    if err == nil {
        if res.GetStatus() == proto.HealthCheckResponse_SERVING {
            return true, nil
        }
        return false, nil
    }
    switch grpc.Code(err) {
    case
        codes.Aborted,
        codes.DataLoss,
        codes.DeadlineExceeded,
        codes.Internal,
        codes.Unavailable:
        // non-fatal errors
    default:
        return false, err
    }

    return false, err
}

程式碼連結

驗證 gRPC 服務是否存活

上述 Client 寫好後,其他開發者可以直接 import 此 package,就可以直接使用。再建立 一個檔案取名叫 check.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
    "context"
    "log"
    "time"

    "github.com/go-training/grpc-health-check/rpc"

    "google.golang.org/grpc"
)

const (
    address = "localhost:9000"
)

func main() {
    // Set up a connection to the server.
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()

    client := rpc.NewGrpcHealthClient(conn)

    for {
        ok, err := client.Check(context.Background())
        if !ok || err != nil {
            log.Printf("can't connect grpc server: %v, code: %v\n", err, grpc.Code(err))
        } else {
            log.Println("connect the grpc server successfully")
        }

        <-time.After(time.Second)
    }
}

程式碼連結

結論

所有程式碼都可以在這邊找到,假設團隊的 gRPC 服務跟 Web 服務器 綁在同一個 Go 程式的話,可以透過撰寫 /healthz 來同時處理 gRPC 及 Http 服務的驗證。在 Kubernetes 內就可以透過設定 livenessProbe 來驗證 Container 是否存活。

1
2
3
4
5
6
livenessProbe:
  httpGet:
    path: /healthz
    port: 3000
  initialDelaySeconds: 3
  periodSeconds: 3

See also