gRPC超时重传机制

本文详细解释了gRPC中的超时处理机制,包括客户端如何通过context设置超时,以及服务器端如何解析超时并在请求处理中应用。重点介绍了gRPC如何利用HTTP/2HEADERSFrame中的`grpc-timeout`字段进行超时传递和处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、前置基础

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

### gRPC 中的超时和异常处理 #### 处理超时 在 Java 和 Python 实现中,gRPC 提供了设置请求截止时间(deadline)的方法来管理超时。当客户端发起带有 deadline 的 RPC 请求时,如果服务端未能在此时间内完成响应,则该请求会被取消并抛出 `DEADLINE_EXCEEDED` 错误。 Java 客户端可以通过 `withDeadlineAfter()` 或者 `withDeadline()` 来指定一个相对或绝对的时间点作为 deadline: ```java // 设置5秒后的deadline stub.withDeadlineAfter(5, TimeUnit.SECONDS).yourRpcMethod(request); ``` Python 客户端同样支持通过参数传递 timeout 值给 RPC 方法调用来实现相同功能[^2]: ```python # 使用 with_call() 并传入timeout=5表示等待最多五秒钟 response, call = stub.YourUnaryCall(your_request_message, timeout=5) ``` #### 异常处理机制 对于可能出现的各种类型的错误状况,gRPC 设计了一套标准的状态码体系用于描述不同的失败原因。这些状态码可以在客户端和服务端之间传播,并允许开发者基于特定的状态码执行相应的逻辑操作。 以 Java 为例,在遇到取消的情况时,代码片段展示了如何检测来自上下文(Context)对象中的取消信号,并据此作出适当反应[^1]: ```java if (Context.current().isCancelled()) { responseObserver.onError( Status.CANCELLED.withDescription("Cancelled by client") .asRuntimeException()); return; } ``` 而在 Python 中,通常会在 try-except 结构内捕获由远程过程调用引发的异常实例(`_Rendezvous`),进而判断其内部属性如 code 和 details 进行进一步处理: ```python try: response = stub.YourStreamCall(your_request_iterator, metadata=(('key', 'value'),)) except grpc.RpcError as e: status_code = e.code() message = e.details() if status_code == grpc.StatusCode.DEADLINE_EXCEEDED: print(f"Request timed out: {message}") elif status_code == grpc.StatusCode.UNAVAILABLE: print(f"Service unavailable: {message}") else: raise ``` 此外,关于何时以及怎样重试失败的 RPC 请求,这取决于具体应用场景的需求。由于不同情况下可能会产生相同的 gRPC 状态码,所以应用程序应当定义自己的策略来决定哪些状态值得被重试[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

西京刀客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值