• Grpc 高级特性之 重试机制 &状态码定义


    GRPC 超时机制

    超时介绍

    一般而言,在微服务架构下,客户端通过设置合理的调用超时时间在系统性能、服务运维层面取一个折中。超时时间设置太短,在服务端正常突发的压力下,可能获取不到正常的结果;超时时间设置太长,极端情况下(网络延迟),则可能处于一直等待状态. gRPC 中设置超时时间有两种概念:

    • timeout 针对单个rpc调用 客户端设置等待超时时间

    • deadline 针对微服务调用链路 在最开始调用的地方设置

      在不考虑网络延迟的情况下,整个微服务调用链路的deadline截止时间 应该是所有客户端 超时时间之和

    在这里插入图片描述

    如上图,客户端调用商品服务需要3S, 商品服务调用库存服务设置5S超时,那么整个链路的调用截止时间就是8S.

    Example 代码

    客户端-核心代码

    • 设置超时时间
    //设置5秒超时
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
    defer cancel()
    
    • 1
    • 2
    • 3
    • 设置Deadline时间
    //设置5秒调用截止
    clientDeadline := time.Now().Add(time.Duration(1000) * time.Millisecond * 5)
    ctx, cancel := context.WithDeadline(context.Background(), clientDeadline)
    
    • 1
    • 2
    • 3

    服务端-核心代码

    针对客户端设置的超时请求(timeout、deadline),服务端都需要进行超时检测,并进行对应的处理代码如下

    time.Sleep(time.Second * 10)
    log.Printf("Received: %v", in.GetName())
    // 网上说这种方式判断是否取消,但是测试不成功 还希望熟悉的大神 指导下
    //if ctx.Err() == context.Canceled {
    if ctx.Err() != nil {
      log.Printf("request canceled %v", in.GetName())
      return nil, status.Errorf(codes.Internal, "Unexpected error from context packet: %v", ctx.Err())
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    代码测试

    • 服务端完整代码

      package main
      
      import (
      	"context"
      	"flag"
      	"fmt"
      	"google.golang.org/grpc"
      	"google.golang.org/grpc/codes"
      	pb "google.golang.org/grpc/examples/helloworld/helloworld"
      	"google.golang.org/grpc/status"
      	"log"
      	"net"
      	"time"
      )
      
      var (
      	port = flag.Int("port", 50051, "The server port")
      )
      
      // server is used to implement helloworld.GreeterServer.
      type server struct {
      	pb.UnimplementedGreeterServer
      }
      
      // SayHello implements helloworld.GreeterServer
      func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
      	time.Sleep(time.Second * 10)
      	log.Printf("Received: %v", in.GetName())
      	// 判断context上下文是否超时 状态
      	// 网上说这种方式判断是否取消,但是测试不成功 还希望熟悉的大神 指导下
      	//if ctx.Err() == context.Canceled {
      	if ctx.Err() != nil {
      		log.Printf("request canceled %v", in.GetName())
      		return nil, status.Errorf(codes.Internal, "Unexpected error from context packet: %v", ctx.Err())
      	}
      	return &pb.HelloReply{Message: "Hello " + in.GetName()}, 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()
      	pb.RegisterGreeterServer(s, &server{})
      	log.Printf("server listening at %v", lis.Addr())
      	if err := s.Serve(lis); err != nil {
      		log.Fatalf("failed to serve: %v", err)
      	}
      }
      
      • 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
    • 客户端完整代码

      package main
      
      import (
      	"context"
      	"flag"
      	"log"
      	"time"
      
      	"google.golang.org/grpc"
      	"google.golang.org/grpc/credentials/insecure"
      	pb "google.golang.org/grpc/examples/helloworld/helloworld"
      )
      
      const (
      	defaultName = "world"
      )
      
      var (
      	addr = flag.String("addr", "localhost:50051", "the address to connect to")
      	name = flag.String("name", defaultName, "Name to greet")
      )
      
      func main() {
      	flag.Parse()
      	// Set up a connection to the server.
      	conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
      	if err != nil {
      		log.Fatalf("did not connect: %v", err)
      	}
      	defer conn.Close()
      	c := pb.NewGreeterClient(conn)
      
      	// Contact the server and print out its response.
      	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
      
      	//clientDeadline := time.Now().Add(time.Duration(1000) * time.Millisecond * 5)
      	//ctx, cancel := context.WithDeadline(context.Background(), clientDeadline)
      
      	defer cancel()
      	r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
      	if err != nil {
      		log.Fatalf("could not greet: %v", err)
      	}
      	log.Printf("Greeting: %s", r.GetMessage())
      }
      
      • 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
      • 测试结果

        先启动服务端,后启动客户端,客户端输出结果

        2022/11/06 17:02:26 could not greet: rpc error: code = DeadlineExceeded desc = context deadline exceeded
        
        • 1

    HTTP2 超时协议

    到目前为止我们已经完成了grpc 超时功能的开发,包含客户端设置、服务端检测。现在来通过抓包工具看一下客户端设置的超时数据是如何传递给服务端的。

    抓包工具使用Wireshark,至于如何使用请参考另一篇blog,Grpc Quick Start 之协议分析

    在这里插入图片描述

    如上图,客户端设置的超时时间为5秒,grpc协议将超时时间放置在HTTP Header 请求头里面

    grpc-timeout: 4995884u

    其中u表示时间单位为纳秒,4995884u 约等于 5秒。然后服务端接收到该请求后,就可以根据这个时间计算出是否超时,由header 超时设置,接下来衍生一下grpc在HTTP2 Header里面自定义了多少消息头 ,以及它们的应用场景。

    grpc headers

    该部分介绍通过HTTP2通讯协议来实现gRPC的实现。如元数据定义、header新增,通过Request、Response两部分来介绍。

    Http Request Header

    gRPC在Request 请求头自定义了以下元数据已满足通讯的要求

    • Method → 指定请求类型

    • Scheme → 请求模式 (“http” / “https”)

    • Path → 请求URI “:path” “/” Service-Name “/” {method name}

    • Service-Name → IDL特定服务名称

    • Authority → “认证信息” 一般为请求host + 端口

    • TE → 通常用于检测不兼容的代码,值为: “te” “trailers”

    • Timeout → grpc 超时header

    • TimeoutValue →grpc 超时数值 一般为正整数,最多8个数字表示

    • TimeoutUnit → 超时单位,支持小时/分钟/秒/毫秒/微妙/纳秒,用一个字符表示

      • Hour → “H”

      • Minute → “M”

      • Second → “S”

      • Millisecond → “m”

      • Microsecond → “u” 默认使用微妙表示超时时间

      • Nanosecond → “n”

    • Content-Type → “content-type” “application/grpc” 请求类型

    • Content-Coding → “identity” / “gzip” / “deflate” / “snappy” / {custom} 数据压缩方式

    • Message-Encoding → “grpc-encoding” 消息编码方式

    • Message-Accept-Encoding → “grpc-accept-encoding” Content-Coding *(“,” Content-Coding)

    • User-Agent → “user-agent” 用户代理

    • Message-Type → “grpc-message-type” 消息类型

    • Custom-Metadata → Binary-Header / ASCII-Header 自定义元数据

    • Binary-Header → {Header-Name “-bin” } 表示二进制传输

    • ASCII-Header → Header-Name ASCII-Value 表示ASCII码传输

    • Header-Name → 1*( %x30-39 / %x61-7A / “_” / “-” / “.”) ; 0-9 a-z _ - .

    • ASCII-Value → 1*( %x20-%x7E ) ; 空格、可打印的ASCII码

    Http Response Header

    • Response → (Response-Headers *Length-Prefixed-Message Trailers) / Trailers-Only
    • Response-Headers → HTTP-Status [Message-Encoding] [Message-Accept-Encoding] Content-Type *Custom-Metadata
    • Trailers-Only → HTTP-Status Content-Type Trailers
    • Trailers → Status [Status-Message] *Custom-Metadata
    • HTTP-Status → “:status 200”
    • Status → “grpc-status”
    • Status-Message → “grpc-message” Percent-Encoded
    • Percent-Encoded → 1*(Percent-Byte-Unencoded / Percent-Byte-Encoded)
    • Percent-Byte-Unencoded → 1*( %x20-%x24 / %x26-%x7E ) ; space and VCHAR, except %
    • Percent-Byte-Encoded → “%” 2HEXDIGIT ; 0-9 A-F

    grpc-status 定义

    CodeNumberDescription
    OK0没有错误 成功返回
    CANCELLED1客户端取消调用
    UNKNOWN2未知异常
    INVALID_ARGUMENT3非法参数
    DEADLINE_EXCEEDED4调用超时时间异常
    NOT_FOUND5实体未找到;某些访问拒绝的场景也可以使用该异常
    ALREADY_EXISTS6客户端创建的实体已存在
    PERMISSION_DENIED7非法访问
    RESOURCE_EXHAUSTED8资源耗尽
    FAILED_PRECONDITION9操作被拒绝
    ABORTED10操作终止
    OUT_OF_RANGE11超出合法范围
    UNIMPLEMENTED12操作不支持
    INTERNAL13内部错误
    UNAVAILABLE14服务不可用
    DATA_LOSS15数据丢失
    UNAUTHENTICATED16未认证

    在这里插入图片描述

    如上图,在gRPC Response 返回的信息中 已经没有 熟知的HTTP status 200的状态了,取而代之的是grpc-status:0 表示正常的返回。个人猜测应该是HTTP STATUS 状态码在某些特定的场景下 不能满足gRPC的要求,如INVALID_ARGUMENT、DEADLINE_EXCEEDED。

  • 相关阅读:
    优化elementUI的Message消息提示连续触发,满屏显示问题
    SpringBoot项目中实现MySQL读写分离
    MySQL8修改密码
    PowerDotNet平台化软件架构设计与实现系列(17):PCRM个人用户管理平台
    商城体系之产商品系统
    使用ensp搭建路由拓扑,并使用ospf协议实现网络互通实操
    Java中的排序接口Comparable和比较器Comparator详解
    面向对象重写理解 求值策略 -共享对象调用 面向对象原则
    pdf如何让多张图片在一页
    jupyter notebook 魔术命令介绍和简单使用
  • 原文地址:https://blog.csdn.net/u013433591/article/details/127721619