grpc链接池(6)超时的设置和传递
我们经常看到下面的日志: rpc error: code = DeadlineExceeded desc = context deadline exceeded
我们需要思考两个问题:1,这个错误码来源是哪里?2,超时是如何设置和生效的?
首先我们看下第一个问题:我们可以发现这段错误文案是golang源码里的错误文案:src/context/context.go var DeadlineExceeded error = deadlineExceededError{} func (deadlineExceededError) Error() string { return "context deadline exceeded" }
什么时候会返回这个错误呢?同样是golang源码的context包里 func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { if cur, ok := parent.Deadline(); ok && cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) } dur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded) // deadline has already passed return c, func() { c.cancel(false, Canceled) } } if c.err == nil { c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) } }func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) }
了解了上面的背景后,我们就可以排查grpc-go的client在何时使用了 WithTimeout
google.golang.org/grpc@v1.50.1/clientconn.go type ClientConn struct { dopts dialOptions }func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) { if cc.dopts.timeout > 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, cc.dopts.timeout) defer cancel() } }
可以看到,在发起连接的时候会有,当server超过超时时间没有响应的时候就会报上面的错误。
第二个地方就是我们发送请求的时候,我先会获取一个连接 func invoke(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error { cs, err := newClientStream(ctx, unaryStreamDesc, cc, method, opts...)func newClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, opts ...CallOption) (_ ClientStream, err error) { var newStream = func(ctx context.Context, done func()) (iresolver.ClientStream, error) { return newClientStreamWithParams(ctx, desc, cc, method, mc, onCommit, done, opts...) } rpcConfig, err := cc.safeConfigSelector.SelectConfig(rpcInfo)func newClientStreamWithParams(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, mc serviceconfig.MethodConfig, onCommit, doneFunc func(), opts ...CallOption) (_ iresolver.ClientStream, err error) { if mc.Timeout != nil && *mc.Timeout >= 0 { ctx, cancel = context.WithTimeout(ctx, *mc.Timeout) } else { ctx, cancel = context.WithCancel(ctx) }
可以看到,如果方法配置了超时,在超时时间完成之前,没有响应,也会报错。
还有没有其它地方可以配置超时呢?答案是肯定的,Interceptor里我们也可以定义超时。下面就是我们常用的两种设置的超时的方法,分别是连接维度和请求方法维度。 clientConn, err := grpc.Dial(serverAddress, grpc.WithTimeout(5 * time.Second), grpc.WithInsecure()) if err != nil { log.Println("Dial failed!") return err }c := pb.NewGreeterClient(conn) c.SayHello(context.Background(), &pb.HelloRequest{Name: "world"}, WithForcedTimeout(time.Duration(10)*time.Second))
那么上述设置是如何生效的?如何传递到服务端的呢?先看下
grpc.WithTimeout 源码位于google.golang.org/grpc@v1.50.1/dialoptions.go func WithTimeout(d time.Duration) DialOption { return newFuncDialOption(func(o *dialOptions) { o.timeout = d }) }
它修改了dialOptions的timeout type dialOptions struct { timeout time.Duration }type DialOption interface { apply(*dialOptions) }
而dialOptions是ClientConn的一个属性 type ClientConn struct { dopts dialOptions }
我们发起连接的时候用的就是这个属性上的timeout func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) { if cc.dopts.timeout > 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, cc.dopts.timeout) defer cancel() } }
Interceptor 是如何让超时生效的呢,逻辑更简单,我们看下它的定义,在发起真正调用之前先调用Interceptor ,这个时候设置超时时间: func TimeoutInterceptor(t time.Duration) grpc.UnaryClientInterceptor { return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { timeout := t if v, ok := getForcedTimeout(opts); ok { timeout = v } if timeout <= 0 { return invoker(ctx, method, req, reply, cc, opts...) } ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() return invoker(ctx, method, req, reply, cc, opts...) } }func getForcedTimeout(callOptions []grpc.CallOption) (time.Duration, bool) { for _, opt := range callOptions { if co, ok := opt.(TimeoutCallOption); ok { return co.forcedTimeout, true } } return 0, false }
而超时时间是我们发起调用的时候通过option传递下来的 type TimeoutCallOption struct { grpc.EmptyCallOption forcedTimeout time.Duration } func WithForcedTimeout(forceTimeout time.Duration) TimeoutCallOption { return TimeoutCallOption{forcedTimeout: forceTimeout} }
弄清楚客户端的超时时间是如何设置和生效的以后,服务端怎么保证,客户端超时以后,马上终止当前任务呢?回答这个问题之前,我们看下超时是如何传递的。首先,给出答案:grpc协议将超时时间放置在HTTP Header 请求头里面。客户端设置的超时时间为5秒,http2的header如下 grpc-timeout: 4995884u
其中u表示时间单位为纳秒,4995884u 约等于 5秒。然后服务端接收到该请求后,就可以根据这个时间计算出是否超时,由header 超时设置。
那么header是何时由client设置的,以及何时由服务端解析的呢?
google.golang.org/grpc@v1.50.1/internal/transport/http2_client.go
发起客户端请求的时候会调用 func (t *http2Client) NewStream(ctx context.Context, callHdr *CallHdr) (*Stream, error) { headerFields, err := t.createHeaderFields(ctx, callHdr)
内部我们可以看到,它从context里面取出超时截止时间,然后写入header "grpc-timeout" 里面 func (t *http2Client) createHeaderFields(ctx context.Context, callHdr *CallHdr) ([]hpack.HeaderField, error) { if dl, ok := ctx.Deadline(); ok { // Send out timeout regardless its value. The server can detect timeout context by itself. // TODO(mmukhi): Perhaps this field should be updated when actually writing out to the wire. timeout := time.Until(dl) headerFields = append(headerFields, hpack.HeaderField{Name: "grpc-timeout", Value: grpcutil.EncodeDuration(timeout)}) }
解析的过程:
google.golang.org/grpc@v1.50.1/internal/transport/handler_server.go func NewServerHandlerTransport(w http.ResponseWriter, r *http.Request, stats []stats.Handler) (ServerTransport, error) { if v := r.Header.Get("grpc-timeout"); v != "" { to, err := decodeTimeout(v) if err != nil { return nil, status.Errorf(codes.Internal, "malformed time-out: %v", err) } st.timeoutSet = true st.timeout = to } if timeoutSet { s.ctx, s.cancel = context.WithTimeout(t.ctx, timeout) } else { s.ctx, s.cancel = context.WithCancel(t.ctx) }
可以看到,首先从header里面取出超时时间,然后设置context.WithTimeout func (ht *serverHandlerTransport) HandleStreams(startStream func(*Stream), traceCtx func(context.Context, string) context.Context) { if ht.timeoutSet { ctx, cancel = context.WithTimeout(ctx, ht.timeout) } else { ctx, cancel = context.WithCancel(ctx) }
google.golang.org/grpc@v1.50.1/internal/transport/http2_server.go func (t *http2Server) operateHeaders(frame *http2.MetaHeadersFrame, handle func(*Stream), traceCtx func(context.Context, string) context.Context) (fatal bool) { case "grpc-timeout": timeoutSet = true var err error if timeout, err = decodeTimeout(hf.Value); err != nil { headerError = true }
上海海港外援奥斯卡未来大概率留在中国新华社南京1月7日电(记者王恒志)中超上海海港队队长奥斯卡7日表示,自己目前会专注帮助球队拿下中国足协杯冠军。至于未来,他大概率会和家人继续留在中国。2022年1月4日,奥斯卡(左
汤加去年火山喷发6小时内引发近40万次闪电,全球一半闪电集中在周围楚天都市报极目新闻记者李力力胡秀文2022年1月汤加海底火山喷发,不仅引发了海啸,还向大气喷出大量气体与水蒸气。据美国有线电视新闻网1月8日报道,现在研究人员还发现,在汤加火山喷发
国门洞开,越南计划这样来欢迎中国客人,你想到了吗?越南旅游业需要考虑研究并出台通过边境口岸欢迎中国游客的具体机制近期,在没有机制的情况下,将成立团体和俱乐部,就如何接待客人通过协商达成一致。这是广宁省旅游局局长范玉翠先生在1月9日
这才是翅根最好吃做法,不焯水不油炸,鲜嫩多汁,比红烧肉还好吃冬日生活打卡季大家好,我是不二,我的小棉袄吵着要吃可乐吃根可乐吃根,好多小朋友都喜欢吃,今天就来分享一个不一样的做法,用我这个做法做出的翅根是相当的嫩,点个赞,接着往下看。可乐翅根
喜迎春节,上门送这3款礼酒,酒质高也拿得出手,重点是倍有面子一年走到了尽头,春节的脚步越来越近,团圆夜马上就要到来,相信大家都做好了充足的过年的准备工作吧,今天我们就来聊一聊过年。喜迎春节,上门送这3款礼酒,酒质高也拿得出手,重点是倍有面子
武都万象洞冬日游客络绎不绝新甘肃每日甘肃网通讯员后斌玉摄影报道冬日,笔者走进陇南市武都区万象洞景区,在景区看到,游客们每到一处都随手拿起相机和手机照相留念,他们都被万象洞雄奇秀丽的景色所深深吸引,生怕错过这
隆冬闲游永乐镇周末闲暇,偶遇入冬后难得的好天气,天高云淡,艳阳高照,夫人提议去周边小镇转转。说实话,西安和咸阳周边几乎所有稍有名气的小镇都去过了,想想找个就近的,重游永乐镇就成了首选。永乐镇古称
昭通绥江首个半山酒店开业云南网讯(记者谢毅通讯员罗洪邓雾军)1月1日,昭通市绥江县千年渔村半山酒店开门迎客,这是该县首个建成投用的半山酒店。据悉,千年渔村半山酒店于2022年2月启动建设,总投资4000万
观音山上观山水最接近大奖佳对欣赏风景这边独好观音山征联已截稿数日,高境下对屏上出现寥寥无几。只有数幅看似沾边却不沾门的半吊子联在征联大戏前闹台,挺热闹的,好玩儿!我属闹台之流中的C角儿,愿给友们献上新年快乐。之所以不见高对,
江苏南京无想山风景区冬景如画来源人民网江苏南京无想山风景区冬景如画江苏南京无想山风景区冬景如画2江苏南京无想山风景区冬景如画3江苏南京无想山风景区冬景如画4江苏南京无想山风景区冬景如画5江苏南京无想山风景区冬
非热门景区,今年冬天新晋的6处耍雪地,槽点和亮点并存成都的冬天怎么能没有雪呢?虽然成都市区难得下雪,但成都人对于雪的热爱因子,每到冬季就会被激活。元旦的时候,已经有不少家长带娃出去耍了雪,感受玩雪的乐趣。今年玩雪,有没有小众不远景美