gRPC之grpc健康检查

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 服务端

服务器控制其服务状态,它们通过检查相关系统来做到这一点,然后相应地更新自己的状态。健康服务器可以返回

四种状态之一:UNKNOWNSERVINGNOT_SERVINGSERVICE_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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值