目录
  • 错误处理
    • 常规用法
    • 进阶用法
    • 原理
  • 多路复用
    • 元数据
      • 负载均衡
        • 压缩数据

          错误处理

          gRPC 一般不在 message 中定义错误。毕竟每个 gRPC 服务本身就带一个 error 的返回值,这是用来传输错误的专用通道。gRPC 中所有的错误返回都应该是 nil 或者 由 status.Status 产生的一个error。这样error可以直接被调用方Client识别。

          常规用法

          当遇到一个go错误的时候,直接返回是无法被下游client识别的。

          恰当的做法是
          调用 status.New 方法,并传入一个适当的错误码,生成一个 status.Status 对象
          调用该 status.Err 方法生成一个能被调用方识别的error,然后返回
          st := status.New(codes.NotFound, “some description”)
          err := st.Err()
          传入的错误码是 codes.Code 类型。

          此外还有更便捷的办法:使用 status.Error。它避免了手动转换的操作。

          err := status.Error(codes.NotFound, "some description")
          

          进阶用法

          上面的错误有个问题,就是 code.Code 定义的错误码只有固定的几种,无法详尽地表达业务中遇到的错误场景。

          gRPC 提供了在错误中补充信息的机制:status.WithDetails 方法

          Client 通过将 error 重新转换位 status.Status ,就可以通过 status.Details 方法直接获取其中的内容。

          status.Detials 返回的是个slice, 是interface{}的slice,然而go已经自动做了类型转换,可以通过断言直接使用。

          服务端示例

          • 生成一个 status.Status 对象
          • 填充错误的补充信息
          // 生成一个 status.Status 
          st := status.New(codes.ResourceExhausted, "Request limit exceeded.")
          // 填充错误的补充信息 WithDetails
          ds, err := st.WithDetails(
              &epb.QuotaFailure{
                  Violations: []*epb.QuotaFailure_Violation{{
                      Subject:     fmt.Sprintf("name:%s", in.Name),
                      Description: "Limit one greeting per person",
                  }},
              },
          )
          if err != nil {
              return nil, st.Err()
          }
          return nil, ds.Err()
          

          客户端的示例

          • 调用RPC错误后,解析错误信息
          • 通过断言直接获取错误详情
          r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "world"})
          // 调用 RPC 如果遇到错误就对错误处理
          if err != nil {
              // 转换错误
              s := status.Convert(err)
              // 解析错误信息
              for _, d := range s.Details() {
                  // 通过断言直接使用
                  switch info := d.(type) {
                      case *epb.QuotaFailure:
                      log.Printf("Quota failure: %s", info)
                      default:
                      log.Printf("Unexpected type: %s", info)
                  }
              }
          }
          

          原理

          这个错误是如何传递给调用方Client的呢?

          是放到 metadata中的,而metadata是放到HTTP的header中的。

          metadata是key:value格式的数据。错误的传递中,key是个固定值:grpc-status-details-bin。

          而value,是被proto编码过的,是二进制安全的。

          目前大多数语言都实现了这个机制。

          多路复用

          同一台服务器上的多个RPC服务的多路复用,比如同时保存一个订单的存根、一个欢迎的存根因为多个RPC服务运行在一个服务端上,所以客户端的多个存根之间是可以共享gRPC连接的
          服务端代码

          func main() {
          	lis, err := net.Listen("tcp", port)
          	if err != nil {
          		log.Fatalf("failed to listen: %v", err)
          	}
          	grpcServer := grpc.NewServer() 
          
          	// 注册进订单服务
          	ordermgt_pb.RegisterOrderManagementServer(grpcServer, &orderMgtServer{}) 
          	// 注册进欢迎服务
          	hello_pb.RegisterGreeterServer(grpcServer, &helloServer{}) 
          }
          

          客户端代码

          func main() {
          	conn, err := grpc.Dial(address, grpc.WithInsecure())
          	if err != nil {
          		log.Fatalf("did not connect: %v", err)
          	}
          	defer conn.Close()
          
          	// 订单服务建立实例连接
          	orderManagementClient := pb.NewOrderManagementClient(conn)
          	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
          	defer cancel()
          
          	order1 := pb.Order{Id: "101", Items:[]string{"iPhone XS", "Mac Book Pro"}, Destination:"San Jose, CA", Price:2300.00}
          	res, addErr := orderManagementClient.AddOrder(ctx, &order1)
            
          	// 欢迎服务建立实例连接
          	helloClient := hwpb.NewGreeterClient(conn)
          	hwcCtx, hwcCancel := context.WithTimeout(context.Background(), time.Second)
          	defer hwcCancel()
            
          	helloResponse, err := helloClient.SayHello(hwcCtx, &hwpb.HelloRequest{Name: "gRPC Up and Running!"})
          	fmt.Println("Greeting: ", helloResponse.Message)
          }
          

          元数据

          在多个微服务的调用当中,信息交换常常是使用方法之间的参数传递的方式,但是在有些场景下,一些信息可能和 RPC 方法的业务参数没有直接的关联,所以不能作为参数的一部分,在 gRPC 中,可以使用元数据来存储这类信息。

          元数据创建

          // 方法1
          md := metadata.Pairs(
          		"1", "v1",
              "1", "v2",	// 方法1会把相同的键的字段合并,[ ]string{"v1","v2"}
          		"2", "v3",
          	)
          // 方法2
          md := metadata.New(map[string]string{"1":"v1","2":"v2"})
          

          客户端收发

          在context中设置的元数据会转换成线路层的gRPC头信息和 trailer

          客户端发送这些头信息,收件方会以头信息的形式接收他们

          	// 创建元数据
          	md := metadata.Pairs(
          		"timestamp", time.Now().Format(time.StampNano),
          		"kn", "vn",
          	)
          	// 创建新元数据的上下文,这种方法会替换掉已有的上下文
          	mdCtx := metadata.NewOutgoingContext(context.Background(), md)
          	// 这种方法是将元数据附加到已有的上下文
          	ctxA := metadata.AppendToOutgoingContext(mdCtx, "k1", "v1", "k1", "v2", "k2", "v3")
          
          	// 定义头信息和 trailer,可以用来接收元数据
          	var header, trailer metadata.MD
          
          	order1 := pb.Order{Id: "101", Items: []string{"iPhone XS", "Mac Book Pro"}, Destination: "San Jose, CA", Price: 2300.00}
          	res, _ := client.AddOrder(ctxA, &order1, grpc.Header(&header), grpc.Trailer(&trailer))
          
          	log.Print("AddOrder Response -> ", res.Value)
          	// 获取头信息
          	head, err := res.Header()
          	// 获取trailer
          	trail, err := res.Trailer()
          

          服务端收发

          // 从上下文中获取元数据列表
          md, metadataAvailable := metadata.FromIncomingContext(ctx)
          	if !metadataAvailable {
          		return nil, status.Errorf(codes.DataLoss, "UnaryEcho: failed to get metadata")
          	}
          // 操作元数据逻辑
          	if t, ok := md["timestamp"]; ok {
          		fmt.Printf("timestamp from metadata:\n")
          		for i, e := range t {
          			fmt.Printf("====> Metadata %d. %s\n", i, e)
          		}
          	}
          
          // 创建元数据
          header := metadata.New(map[string]string{"location": "San Jose", "timestamp": time.Now().Format(time.StampNano)})
          // 发送头信息
          grpc.SendHeader(ctx, header)
          trailer := metadata.Pairs("status","ok")
          // 设置trailer
          grpc.SetTrailer(ctx,trailer)
          

          负载均衡

          负载均衡器代理

          也就是说后端的结构对gRPC客户端是不透明的,客户端只需要知道均衡器的断点就可以了,比如NGINX代理、Envoy代理

          客户端负载均衡

          func main(){
            roundrobinConn, err := grpc.Dial(
          		address,
          		grpc.WithBalancerName("round_robin"), 	// 指定负载均衡的算法
              // 默认是"pick_first",也就是从服务器列表中第一个服务端开始尝试发送请求,成功则后续所有RPC都发往这个服务器
              // "round_robin"轮询调度算法,连接所有地址,每次向后端发送一个RPC
          		grpc.WithInsecure(),
          	)
          	if err != nil {
          		log.Fatalf("did not connect: %v", err)
          	}
          	defer roundrobinConn.Close()
          	// 起10个RPC调度任务
          	makeRPCs(roundrobinConn, 10)
          }
          
          func makeRPCs(cc *grpc.ClientConn, n int) {
          	hwc := ecpb.NewEchoClient(cc)
          	for i := 0; i < n; i++ {
          		callUnary(hwc)
          	}
          }
          
          func callUnary(c ecpb.EchoClient) {
          	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
          	defer cancel()
            
          }
          

          go grpc高级用法

          压缩数据

          在服务端会对已注册的压缩器自动解码,响应时自动编码
          始终从客户端获取指定的压缩方法,如果没被注册就会返回Unimplemented

          func main() {
          	conn, err := grpc.Dial(address, grpc.WithInsecure())
          	defer conn.Close()
          	client := pb.NewOrderManagementClient(conn)
          	ctx, cancel := context.WithTimeout(context.Background(), time.Second * 5)
          	defer cancel()
          
          	order1 := pb.Order{Id: "101", Items:[]string{"iPhone XS", "Mac Book Pro"}, Destination:"San Jose, CA", Price:2300.00}
            // 通过 grpc.UseCompressor(gzip.Name) 就可以轻松压缩数据
          	res, _ := client.AddOrder(ctx, &order1, grpc.UseCompressor(gzip.Name))
          }

          声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。