1、grpc健康检查
在 gRPC 中使用健康检查,在负载均衡前通过健康检查,只对健康的 Subchannel 发起请求,保证请求的成功率。
通过使用健康库,客户端可以在遇到问题时优雅地避免使用服务器。大多数语言提供开箱即用的实现,使其在系统
之间可互操作。
gRPC提供了一个健康库来向其客户端传达系统的健康状况,它通过Health/v1 api
提供服务定义:
https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto
1.1 客户端
客户端有两种方法来监控服务器的运行状况,一个适用于单次请求的 check
方法,另一个是适用于Stream流的
watch
方法。
在大多数情况下,客户端不需要直接检查后端服务器。如果当服务配置中指定了健康检查配置时,他们可以透明地
执行此操作。此配置指示建立连接时应检查哪个后端serviceName
。空字符串 (""
) 通常表示应报告服务器的整体
运行状况。更多详情查看:
https://github.com/grpc/proposal/blob/master/A17-client-side-health-checking.md
// import grpc/health to enable transparent client side checking
import _ "google.golang.org/grpc/health"
// set up appropriate service config
serviceConfig := grpc.WithDefaultServiceConfig(`{
"loadBalancingPolicy": "round_robin",
"healthCheckConfig": {
"serviceName": ""
}
}`)
conn, err := grpc.Dial(..., serviceConfig)
1.2 服务端
服务器控制其服务状态,它们通过检查相关系统来做到这一点,然后相应地更新自己的状态。健康服务器可以返回
四种状态之一:UNKNOWN
,SERVING
,NOT_SERVING
和SERVICE_UNKNOWN
。
UNKNOWN
:表示当前状态尚不清楚,这种状态通常在服务器实例启动时出现。
SERVING
:表示系统是健康的,并准备好为请求提供服务。相反,NOT_SERVING
表示系统当时无法为请求提供服
务。
SERVICE_UNKNOWN
:表示服务器不知道客户端请求的serviceName
,只有·Watch()`调用才会报告此状态。
服务器可以使用healthServer.SetServingStatus("serviceName", servingStatus)
切换其运行状况。
1.3 健康检查案例1
1.3.1 proto编写和编译
syntax = "proto3";
option go_package = "./;echo";
package echo;
message EchoRequest {
string message = 1;
}
message EchoResponse {
string message = 1;
}
service Echo {
rpc UnaryEcho(EchoRequest) returns (EchoResponse) {}
}
$ protoc -I . --go_out=plugins=grpc:. ./echo.proto
1.3.2 服务端
package main
import (
"context"
pb "demo/pb"
"flag"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
healthgrpc "google.golang.org/grpc/health/grpc_health_v1"
"log"
"net"
"time"
)
var (
port = flag.Int("port", 50051, "the port to serve on")
sleep = flag.Duration("sleep", time.Second*5, "duration between changes in health")
// 空字符串表示系统的运行状况
system = ""
)
type echoServer struct {
pb.UnimplementedEchoServer
}
func (e *echoServer) UnaryEcho(ctx context.Context, req *pb.EchoRequest) (*pb.EchoResponse, error) {
return &pb.EchoResponse{
Message: fmt.Sprintf("hello from localhost:%d", *port),
}, nil
}
func main() {
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
healthcheck := health.NewServer()
healthgrpc.RegisterHealthServer(s, healthcheck)
pb.RegisterEchoServer(s, &echoServer{})
go func() {
// 异步检查依赖项并根据需要切换服务状态
next := healthgrpc.HealthCheckResponse_SERVING
log.Println("next: ", next)
for {
healthcheck.SetServingStatus(system, next)
if next == healthgrpc.HealthCheckResponse_SERVING {
next = healthgrpc.HealthCheckResponse_NOT_SERVING
} else {
next = healthgrpc.HealthCheckResponse_SERVING
}
time.Sleep(*sleep)
}
}()
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
1.3.3 客户端
package main
import (
"context"
pb "demo/pb"
"flag"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
_ "google.golang.org/grpc/health"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/resolver/manual"
"log"
"time"
)
var serviceConfig = `{
"loadBalancingPolicy": "round_robin",
"healthCheckConfig": {
"serviceName": ""
}
}`
func callUnaryEcho(c pb.EchoClient) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.UnaryEcho(ctx, &pb.EchoRequest{})
if err != nil {
fmt.Println("UnaryEcho: _, ", err)
} else {
fmt.Println("UnaryEcho: ", r.GetMessage())
}
}
func main() {
flag.Parse()
r := manual.NewBuilderWithScheme("whatever")
r.InitialState(resolver.State{
Addresses: []resolver.Address{
{Addr: "localhost:50051"},
{Addr: "localhost:50052"},
},
})
// whatever:///unused
address := fmt.Sprintf("%s:///unused", r.Scheme())
options := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
grpc.WithResolvers(r),
grpc.WithDefaultServiceConfig(serviceConfig),
}
conn, err := grpc.Dial(address, options...)
if err != nil {
log.Fatalf("grpc.Dial(%q): %v", address, err)
}
defer conn.Close()
echoClient := pb.NewEchoClient(conn)
for {
callUnaryEcho(echoClient)
time.Sleep(time.Second)
}
}
1.3.4 测试
这里启动两个服务端:
[root@zsx demo]# go run server/server.go -port=50051 -sleep=5s
2023/02/18 15:10:52 next: SERVING
[root@zsx demo]# go run server/server.go -port=50052 -sleep=10s
2023/02/18 15:11:04 next: SERVING
[root@zsx demo]# go run client/client.go
UnaryEcho: hello from localhost:50051
UnaryEcho: hello from localhost:50052
UnaryEcho: hello from localhost:50052
UnaryEcho: hello from localhost:50052
UnaryEcho: hello from localhost:50052
UnaryEcho: hello from localhost:50052
UnaryEcho: hello from localhost:50052
UnaryEcho: hello from localhost:50051
UnaryEcho: hello from localhost:50051
UnaryEcho: hello from localhost:50051
UnaryEcho: hello from localhost:50051
UnaryEcho: _, rpc error: code = Unavailable desc = last connection error: connection active but health check failed. status=NOT_SERVING
UnaryEcho: _, rpc error: code = Unavailable desc = last connection error: connection active but health check failed. status=NOT_SERVING
UnaryEcho: _, rpc error: code = Unavailable desc = last connection error: connection active but health check failed. status=NOT_SERVING
UnaryEcho: _, rpc error: code = Unavailable desc = last connection error: connection active but health check failed. status=NOT_SERVING
UnaryEcho: _, rpc error: code = Unavailable desc = last connection error: connection active but health check failed. status=NOT_SERVING
UnaryEcho: hello from localhost:50051
UnaryEcho: hello from localhost:50051
UnaryEcho: hello from localhost:50052
UnaryEcho: hello from localhost:50051
UnaryEcho: hello from localhost:50052
UnaryEcho: hello from localhost:50052
UnaryEcho: hello from localhost:50052
UnaryEcho: hello from localhost:50052
UnaryEcho: hello from localhost:50052
UnaryEcho: hello from localhost:50052
UnaryEcho: hello from localhost:50051
UnaryEcho: hello from localhost:50051
UnaryEcho: hello from localhost:50051
UnaryEcho: hello from localhost:50051
UnaryEcho: hello from localhost:50051
UnaryEcho: _, rpc error: code = Unavailable desc = last connection error: connection active but health check failed. status=NOT_SERVING
UnaryEcho: _, rpc error: code = Unavailable desc = last connection error: connection active but health check failed. status=NOT_SERVING
UnaryEcho: _, rpc error: code = Unavailable desc = last connection error: connection active but health check failed. status=NOT_SERVING
UnaryEcho: _, rpc error: code = Unavailable desc = last connection error: connection active but health check failed. status=NOT_SERVING
UnaryEcho: _, rpc error: code = Unavailable desc = last connection error: connection active but health check failed. status=NOT_SERVING
......
# 项目结构
$ tree demo/
demo/
├── client
│ └── client.go
├── go.mod
├── go.sum
├── pb
│ ├── echo.pb.go
│ └── echo.proto
└── server
└── server.go
3 directories, 6 files
1.4 健康检查案例2
1.4.1 proto编写和编译
syntax = "proto3";
package pb;
option go_package = "./;pb";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
$ protoc -I . --go_out=plugins=grpc:. ./helloword.proto
1.4.2 服务端
package main
import (
"context"
pb "demo/pb"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
"log"
"net"
"time"
)
const (
port = ":50051"
)
type server struct {
pb.UnimplementedGreeterServer
}
// 该函数定义必须与helloworld.pb.go 定义的SayHello一致
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
//打印客户端传入HelloRequest请求的Name参数
log.Printf("Received: %v", in.GetName())
//将name参数作为返回值,返回给客户端
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}
// main方法 函数开始执行的地方
func main() {
// 调用标准库,监听50051端口的tcp连接
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
//创建grpc服务
s := grpc.NewServer()
// 注册健康检查服务
healthcheck := health.NewServer()
grpc_health_v1.RegisterHealthServer(s, healthcheck)
// 设置服务状态
healthcheck.SetServingStatus("serviceName1", grpc_health_v1.HealthCheckResponse_SERVING)
healthcheck.SetServingStatus("serviceName2", grpc_health_v1.HealthCheckResponse_SERVING)
healthcheck.SetServingStatus("serviceName3", grpc_health_v1.HealthCheckResponse_NOT_SERVING)
go func() {
time.Sleep(20 * time.Second)
healthcheck.SetServingStatus("serviceName3", grpc_health_v1.HealthCheckResponse_SERVING)
}()
//将server对象,也就是实现SayHello方法的对象,与grpc服务绑定
pb.RegisterGreeterServer(s, &server{})
// grpc服务开始接收访问50051端口的tcp连接数据
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
1.4.3 客户端
package main
import (
"context"
"demo/pb"
"google.golang.org/grpc"
_ "google.golang.org/grpc/health"
"log"
"time"
)
const (
address = "localhost:50051"
)
var serviceConfig = `{
"loadBalancingPolicy": "round_robin",
"healthCheckConfig": {
"serviceName": "serviceName3"
}
}`
func main() {
// 访问服务端address,创建连接conn
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock(), grpc.WithDefaultServiceConfig(serviceConfig))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
// 设置客户端访问超时时间1秒
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// 客户端调用服务端 SayHello 请求,传入Name 为 "world", 返回值为服务端返回参数
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "world"})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
// 根据服务端处理逻辑,返回值也为"world"
log.Printf("Greeting: %s", r.GetMessage())
}
1.4.4 测试
[root@zsx demo]# go run server/server.go
2023/02/18 15:46:30 Received: world
[root@zsx demo]# go run client/client.go
2023/02/18 15:46:30 Greeting: Hello world
客户端一开始要等20秒之后才会发送请求。
# 项目结构
[root@zsx protoc]# tree demo/
demo/
├── client
│ └── client.go
├── go.mod
├── go.sum
├── pb
│ ├── helloword.pb.go
│ └── helloword.proto
└── server
└── server.go
3 directories, 6 files