GRPC 超时机制
超时介绍
一般而言,在微服务架构下,客户端通过设置合理的调用超时时间在系统性能、服务运维层面取一个折中。超时时间设置太短,在服务端正常突发的压力下,可能获取不到正常的结果;超时时间设置太长,极端情况下(网络延迟),则可能处于一直等待状态. gRPC 中设置超时时间有两种概念:
- timeout 针对单个rpc调用 客户端设置等待超时时间
- deadline 针对微服务调用链路 在最开始调用的地方设置
在不考虑网络延迟的情况下,整个微服务调用链路的deadline截止时间 应该是所有客户端 超时时间之和
如上图,客户端调用商品服务需要3S, 商品服务调用库存服务设置5S超时,那么整个链路的调用截止时间就是8S.
Example 代码
客户端-核心代码
- 设置超时时间
//设置5秒超时
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
- 设置Deadline时间
//设置5秒调用截止
clientDeadline := time.Now().Add(time.Duration(1000) * time.Millisecond * 5)
ctx, cancel := context.WithDeadline(context.Background(), clientDeadline)
服务端-核心代码
针对客户端设置的超时请求(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())
}
代码测试
- 服务端完整代码
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)
}
}
- 客户端完整代码
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())
}
- 测试结果
先启动服务端,后启动客户端,客户端输出结果
2022/11/06 17:02:26 could not greet: rpc error: code = DeadlineExceeded desc = context deadline exceeded
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 定义
Code | Number | Description |
OK | 0 | 没有错误 成功返回 |
CANCELLED | 1 | 客户端取消调用 |
UNKNOWN | 2 | 未知异常 |
INVALID_ARGUMENT | 3 | 非法参数 |
DEADLINE_EXCEEDED | 4 | 调用超时时间异常 |
NOT_FOUND | 5 | 实体未找到;某些访问拒绝的场景也可以使用该异常 |
ALREADY_EXISTS | 6 | 客户端创建的实体已存在 |
PERMISSION_DENIED | 7 | 非法访问 |
RESOURCE_EXHAUSTED | 8 | 资源耗尽 |
FAILED_PRECONDITION | 9 | 操作被拒绝 |
ABORTED | 10 | 操作终止 |
OUT_OF_RANGE | 11 | 超出合法范围 |
UNIMPLEMENTED | 12 | 操作不支持 |
INTERNAL | 13 | 内部错误 |
UNAVAILABLE | 14 | 服务不可用 |
DATA_LOSS | 15 | 数据丢失 |
UNAUTHENTICATED | 16 | 未认证 |
如上图,在gRPC Response 返回的信息中 已经没有 熟知的HTTP status 200的状态了,取而代之的是grpc-status:0 表示正常的返回。个人猜测应该是HTTP STATUS 状态码在某些特定的场景下 不能满足gRPC的要求,如INVALID_ARGUMENT、DEADLINE_EXCEEDED。