Go语言的gRPC教程-超时控制
前言
一个合理的超时时间是非常必要的,它能提高用户体验,提高服务器的整体性能,是服务治理的常见手段之一
一、为什么要设置超时?
用户体验:很多RPC都是由用户侧发起,如果请求不设置超时时间或者超时时间不合理,会导致用户一直处于白屏或者请求中的状态,影响用户的体验
资源利用:一个RPC会占用两端(服务端与客户端)端口、cpu、内存等一系列的资源,不合理的超时时间会导致RPC占用的资源迟迟不能被释放,因而影响服务器稳定性
综上,一个合理的超时时间是非常必要的。在一些要求更高的服务中,我们还需要针对DNS解析、连接建立,读、写等设置更精细的超时时间。除了设置静态的超时时间,根据当前系统状态、服务链路等设置自适应的动态超时时间也是服务治理中一个常见的方案。
二、客户端的超时
连接超时
还记得我们怎么在客户端创建连接?
conn, err := grpc.NewClient("localhost:8090",grpc.WithTransportCredentials(insecure.NewCredentials()),grpc.WithUnaryInterceptor(clientUnaryInterceptor),)
if err != nil {panic(err)
}
如果目标地址127.0.0.1:8090
无法建立连接,grpc.Dial()
会返回错误么?答案是不会的,grpc.NewClient() 立即返回一个 *grpc.ClientConn,不等待连接建立。连接是异步后台建立的,如果连接没有创建成功会在下面的RPC调用中报错。
2.1 服务调用的超时
和上面连接超时的配置类似。无论是普通RPC
还是流式RPC
,服务调用的第一个参数均是context.Context
所以可以使用context.Context
来控制服务调用的超时时间,然后使用status
来判断是否是超时报错,关于status
可以回顾之前讲过的错误处理
// 服务调用的超时, 3秒内调用完成, 否则超时, 并返回超时错误ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)defer cancel()// 2. 调用方法resp, err := client.Search(ctx, &helloworld.SearchRequest{Request: "Golang",Keywords: []string{"hello", "world"},})if err != nil {st, ok := status.FromError(err)if !ok {log.Println(err)return}switch st.Code() {case codes.DeadlineExceeded:fmt.Println("服务调用超时:", st.String())default:log.Printf("Unhandled error : %s ", st.String())}return}
2.2 拦截器中的超时
普通RPC
还是流式RPC
拦截器函数签名第一个参数也是context.Context
,我们也可以在拦截器中修改超时时间。错误处理也是和服务调用是一样的
需要注意的是context.WithTimeout(context.Background(), 100*time.Second)
func clientUnaryInterceptor(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {start := time.Now()// 拦截器中的超时, 2秒内调用完成, 否则超时, 并返回超时错误var cancel context.CancelFuncctx, cancel = context.WithTimeout(ctx, 3*time.Second)defer cancel()err := invoker(ctx, method, req, reply, cc, opts...)if err != nil {st, ok := status.FromError(err)if ok && st.Code() == codes.DeadlineExceeded {panic(err)}fmt.Println("调用失败:", err)}fmt.Printf("client interceptor 耗时: %d\n, 客户端操作系统: %s\n, method: %s", time.Since(start), cos, method)return err
}
三、服务端的超时
3.1 连接超时
服务端也可以控制连接创建的超时时间,如果没有在设定的时间内建立连接,服务端就会主动断连,避免浪费服务端的端口、内存等资源
s := grpc.NewServer(grpc.ConnectionTimeout(3*time.Second),
)
3.2 服务实现中的超时
服务实现函数的第一个参数也是context.Context
,所以我们可以在一些耗时操作前对context.Context
进行判断:如果已经超时了,就没必要继续往下执行了。此时客户端也会收到上文提到过的超时error
。
func (server *HelloWorldServer) Search(ctx context.Context, r *helloworld.SearchRequest) (*helloworld.SearchResponse, error) {// 超时判断,ctx.Done() 表示上下文已取消, 超时, 或者手动取消select {case <-ctx.Done():return nil, status.Errorf(codes.Canceled, "Client cancelled, abandoning.")default:}return &helloworld.SearchResponse{Response: fmt.Sprintf("hello %s", r.GetRequest()),}, status.New(codes.OK, "success").Err()
}
很多库都支持类似的操作,我们要做的就是把context.Context
透传下去,当context.Context
超时时就会提前结束操作了
db, err := gorm.Open()
if err != nil {panic("failed to connect database")
}db.WithContext(ctx).Save(&users)
3.3 拦截器中的超时
在服务端的拦截器里也可以修改超时时间
// serverUnaryInterceptor 服务端一元拦截器
func serverUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {start := time.Now()// 拦截器中超时控制var cancel context.CancelFuncctx, cancel = context.WithTimeout(ctx, 5*time.Second)defer cancel()log.Printf("Method: %s, req: %s, resp: %s, latency: %s\n",info.FullMethod, req, resp, time.Now().Sub(start))return resp, err
}
四、超时传递
一个正常的请求会涉及到多个服务的调用。从源头开始一个服务端不仅为上游服务提供服务,也作为下游的客户端
如上的链路,如果当请求到达某一服务时,对于服务A来说已经超时了,那么就没有必要继续把请求传递下去了。这样可以最大限度的避免后续服务的资源浪费,提高系统的整体性能。
grpc-go
实现了这一特性,我们要做的就是不断的把context.Context
传下去
// 服务A
func main(){ctx, cancel = context.WithTimeout(context.Background(), 3*time.Second)defer cancel()client.ServiceB(ctx)
}
// 服务B
func ServiceB(ctx context.Context){client.ServiceC(ctx)
}
// 服务C
func ServiceC(ctx context.Context){client.ServiceD(ctx)
}
在每一次的context.Context
透传中, timeout都会减去在本进程中耗时,导致这个 timeout 传递到下一个 gRPC 服务端时变短,当在某一个进程中已经超时,请求不会再继续传递,这样即实现了所谓的 超时传递
关于超时传递的实现可以参考下面的参考资料中的链接
五、总结
通过使用context.Context
,我们可以精细化的控制gRPC中服务端、客户端两端的建连,调用,以及在拦截器中的超时时间。同时gRPC还提供了超时传递的能力,让超时的请求不继续在链路中往下传递,提高链路整体的性能。
参考
[1]示例代码 : gitee
[2] https://segmentfault.com/a/1190000043583545