文章目录
一、前置基础
1. gRPC
gRPC 是在 HTTP/2 之上实现的 RPC 框架,HTTP/2 是第 7 层(应用层)协议,它运行在 TCP(第 4 层 - 传输层)协议之上,相比于传统的 REST/JSON 机制有诸多的优点:
- 基于 HTTP/2 之上的二进制协议(Protobuf 序列化机制);
- 一个连接上可以多路复用,并发处理多个请求和响应;
- 多种语言的类库实现;
- 服务定义文件和自动代码生成(.proto 文件和 Protobuf 编译工具)。
一个完整的 RPC 调用流程示例如下:
2. gRPC Deadlines/Timeouts
context.WithDeadlines: 意指截止时间,当客户端未设置 Deadlines 时,将采用默认的 DEADLINE_EXCEEDED(这个时间非常大)。
context.WithTimeout:很常见的另外一个方法,是便捷操作。实际上是对于 WithDeadline 的封装。
一般来说,当您没有设置截止日期时,如果客户端产生了请求阻塞等待,就会造成大量正在进行的请求都会被保留,并且所有请求都有可能达到最大值才超时。这使得服务耗尽资源的风险。
因此,一般推荐使用context.WithTimeout,设置客户端请求超时时间。
指定截止日期(Deadlines)或超时(Timeouts)是语言相关的:某些语言API支持timeouts (时间持续时间),某些语言API支持截止日期(一个特定的时间点),有的有、有的没有默认的deadline。
在服务器端,服务器可以查询以查看特定的RPC是否不再需要。 在服务器开始工作之前,检查是否仍然等待它是非常重要的。 尤其在做比较费资源的处理之前。
由于 Client 已经设置了截止时间。Server 最好再做昂贵操作之前去检查它。
当客户端请求已达到截止时间,服务器是否要继续执行? 取决于是否在服务器中缓存响应, 这将使未来的请求更快,因为结果已经可用。 不过也要根据具体业务场景和具体情况,自行判断。
客户端设置超时时间
go:
clientDeadline := time.Now().Add(time.Duration(*deadlineMs) * time.Millisecond)
ctx, cancel := context.WithDeadline(ctx, clientDeadline)
java:
response = blockingStub.withDeadlineAfter(deadlineMs, TimeUnit.MILLISECONDS).sayHello(request);
服务器端检查超时时间
当请求超时后,服务端应该停止正在进行的操作,避免资源浪费。事实上,我并不能很好控制服务端停止工作而避免资源浪费,只能尽量减少资源浪费。一般地,在耗时操作或写库前进行超时检测,发现超时就停止工作。
go:
if ctx.Err() == context.Canceled {
return status.New(codes.Canceled, "Client cancelled, abandoning.")
}
java:
if (Context.current().isCancelled()) {
responseObserver.onError(Status.CANCELLED.withDescription("Cancelled by client").asRuntimeException());
return;
}
二、gRPC超时传递基本原理
gRPC 基于 HTTP2,HTTP2 传输的最小单位是 Frame(帧)。HTTP2 的帧包含很多类型:“DATA Frame”、“HEADERS Frame”、“PRIORITY Frame”、“RST_STREAM Frame”、“CONTINUATON Frame”等。一个 HTTP2 请求/响应可以被拆成多个帧并行发送,每一帧都有一个 StreamID 来标记属于哪个 Stream。服务端收到 Frame 后,根据 StreamID 组装出原始请求数据。
对于 gRPC 而言,Data Frame 用来存放请求的 response payload;Headers Frame 可用来存放一些需要进行跨进程传递的数据 ,比如“grpc-status(RPC 请求状态码)”、“:path(RPC 完整路径)”等。
gRPC 是通过 HTTP2 HEADERS Frame 中的 “grpc-timeout”字段来实现跨进程传递超时时间。
1. 客户端设置 timeout源码分析
用户定义好 protobuf 并通过 protoc 生成桩代码(xxx_service.pb.go)后,每一个在 protobuf 中定义的 RPC方法,用户定义好 protobuf 并通过 protoc 生成桩代码后,桩代码中已经包含了 gRPCCient 接口的实现,每一个在 protobuf 中定义的 RPC,客户端发起 gRPC 请求时,最终会调用 invoke() 方法,
func invoke(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error {
cs, err := newClientStream(ctx, unaryStreamDesc, cc, method, opts...)
if err != nil {
return err
}
if err := cs.SendMsg(req); err != nil {
return err
}
return cs.RecvMsg(reply)
}
继续跟代码,调用关键字 invoke -> newClientStream ->newClientStreamWithParams->csAttempt
type csAttempt struct {
cs *clientStream
t transport.ClientTransport // 客户端 Transport
s *transport.Stream // 真正处理RPC 的 Stream
...
}
“transport.ClientTransport”是一个接口,gRPC 内部“internal/transport.http2Client”实现了此接口。
“http2Client.NewStream()”中
func (t *http2Client) NewStream(ctx context.Context, callHdr *CallHdr) (_ *Stream, err error) {
ctx = peer.NewContext(ctx, t.getPeer())
headerFields, err := t.createHeaderFields(ctx, callHdr)
...
继续跟 createHeaderFields
func (t *http2Client) createHeaderFields(ctx context.Context, callHdr *CallHdr) ([]hpack.HeaderField, error) {
...
// 如果透传过来的 ctx 被设置了 timeout/deadline,则在 HTTP2 headers frame 中添加 grpc-timeout 字段,
if dl, ok := ctx.Deadline(); ok {
timeout := time.Until(dl)
headerFields = append(headerFields, hpack.HeaderField{Name: "grpc-timeout", Value: encodeTimeout(timeout)})
}
...
return headerFields, nil
}
可以看到客户端发起请求时,如果设置了带 timeout 的ctx,则会导致底层 HTTP2 HEADERS Frame 中追加“grpc-timeout”字段。
该问题可以直接 跟踪grpc go源码,全局使用 关键字 grpc-timeout 跟踪。
源码版本:grpc 官方v1.39.0
2. 服务器端解析timeout源码分析
//实现gRPC Server
s := grpc.NewServer()
//注册HeartBeatServer为客户端提供服务
pb.RegisterHeartBeatServiceServer(s, HeartBeatServer)
log.Info("Listen on:" + Address)
s.Serve(listen)
服务端通过“Serve”方法启动 grpc Server,监听来自客户端连接。
跟踪 Serve–> handleRawConn–>serveStreams–>HandleStreams
// http2Server.HandleStreams 会调用传入的 handle 处理 HTTP2 Stream
func (t *http2Server) HandleStreams(handle func(*Stream), traceCtx func(context.Context, string) context.Context) {
defer close(t.readerDone)
for {
t.controlBuf.throttle()
frame, err := t.framer.fr.ReadFrame()
...
switch frame := frame.(type) {
// 如果是 Headers 帧,则调用 operateHeaders 方法处理 Headers
case *http2.MetaHeadersFrame:
if t.operateHeaders(frame, handle, traceCtx) {
t.Close()
break
}
// 如果是 Data 帧,则调用 handleData 方法处理
case *http2.DataFrame:
t.handleData(frame)
...
}
}
}
我们继续跟 operateHeaders
func (t *http2Server) operateHeaders(frame *http2.MetaHeadersFrame, handle func(*Stream), traceCtx func(context.Context, string) context.Context) (fatal bool) {
streamID := frame.Header().StreamID
...
for _, hf := range frame.Fields {
switch hf.Name {
case "content-type":
contentSubtype, validContentType := grpcutil.ContentSubtype(hf.Value)
if !validContentType {
break
}
mdata[hf.Name] = append(mdata[hf.Name], hf.Value)
s.contentSubtype = contentSubtype
isGRPC = true
case "grpc-encoding":
s.recvCompress = hf.Value
case ":method":
httpMethod = hf.Value
case ":path":
s.method = hf.Value
//从“grpc-timeout” 中解析出上游传递过来的 timeout
case "grpc-timeout":
timeoutSet = true
var err error
if timeout, err = decodeTimeout(hf.Value); err != nil {
headerError = true
}
...
// 如果 state.data.timeoutSet 为 true,则构造一个新的带 timeout 的 ctx 覆盖原 s.ctx
// s.ctx 最终会透传到用户实现的 gRPC Handler 中,参与业务逻辑处理
if timeoutSet {
s.ctx, s.cancel = context.WithTimeout(t.ctx, timeout)
} else {
s.ctx, s.cancel = context.WithCancel(t.ctx)
}
...
“decodeHeader”会遍历 frame 中所有 Fields,并调用“processHeaderField”对 HTTP2 HEADERS 帧中的特定的 Field 进行处理。
客户端通过 HTTP2 HEADERS Frame 中的 “grpc-timeout”字段来实现跨进程传递超时时间,服务还是通过解析 HTTP2 HEADERS Frame 中的 “grpc-timeout”字段来创建timeout的ctx覆盖本地的ctx 。
3. 总结
- 客户端客户端发起 RPC 调用时传入了带 timeout 的 ctx。
- gRPC 基于 HTTP2 协议。传播 gRPC 附加信息时,是基于 HEADERS 帧进行传播和设置,将 timeout 值写入到 “grpc-timeout” HEADERS Frame 中。
- 服务端接收 RPC 请求时,gRPC 框架底层解析 HTTP2 HEADERS 帧,读取 “grpc-timeout”值,并覆盖透传到实际处理 RPC 请求的业务 gPRC Handle 中。
三、参考
gRPC官网
参考URL: https://www.grpc.io/docs/what-is-grpc/core-concepts/
官网deadlines
参考URL: https://grpc.io/blog/deadlines/
从实践到原理,带你参透 gRPC
参考URL: https://segmentfault.com/a/1190000019608421
grpc 超时传递原理
参考URL: https://blog.youkuaiyun.com/u014229282/article/details/109294837