golang timeoutHandler解析及kubernetes中的变种

Golang里的http request timeout比较简单,但是稍不留心就容易出现错误,最近在kubernetes生产环境中出现了的一个问题让我有机会好好捋一捋golang中关于timeout中的所有相关的东西。

Basic

golang中timeout有关的设置, 资料已经比较多, 其中必须阅读的就是The complete guide to Go net/http timeouts,里面详述了关于http中各个timeou字段及其影响, 写的很详细, 本文就不在重复造轮子了。 所以我们在生产环境中的代码绝对不能傻傻的使用http.Get("www.baidu.com")了, 很容易造成client hang死, 默认的http client的timeout值为0, 也就是没有超时。具体的血泪教训可以参见Don’t use Go’s default HTTP client (in production)。对于http package中default的设置最后还是仔细review一遍再使用。

Advanced golang http.TimeoutHandler

了解了基本的使用方式后,笔者带领大家解析一下其中的,TimeoutHandler顾名思义是一个handler wrapper, 用来限制ServeHttp的最大时间,也就是除去读写请求外真正执行服务器逻辑的时间(如果仔细分析的话其实各个结构体中关于timeout的设置中并没有办法来设置这部分超时时间),如果运行时间超过了设定的时间, 将返回一个"503 Service Unavailable" 和一个指定的message。
我们来一起探究一下他的实现, 首先是函数定义:

// TimeoutHandler returns a Handler that runs h with the given time limit. // // The new Handler calls h.ServeHTTP to handle each request, but if a // call runs for longer than its time limit, the handler responds with // a 503 Service Unavailable error and the given message in its body. // (If msg is empty, a suitable default message will be sent.) // After such a timeout, writes by h to its ResponseWriter will return // ErrHandlerTimeout. // // TimeoutHandler buffers all Handler writes to memory and does not // support the Hijacker or Flusher interfaces. func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler { return &timeoutHandler{ handler: h, body: msg, dt: dt, } }

可以看到典型的handler wrapper的函数signature, 接收一个handler并返回一个hander, 返回的timeout handler中ServeHttp方法如下:

func (h *timeoutHandler) ServeHTTP(w ResponseWriter, r *Request) { ctx := h.testContext if ctx == nil { var cancelCtx context.CancelFunc ctx, cancelCtx = context.WithTimeout(r.Context(), h.dt) defer cancelCtx() } r = r.WithContext(ctx) done := make(chan struct{}) tw := &timeoutWriter{ w: w, h: make(Header), } panicChan := make(chan interface{}, 1) go func() { defer func() { if p := recover(); p != nil { panicChan <- p } }() h.handler.ServeHTTP(tw, r) close(done) }() select { case p := <-panicChan: panic(p) case <-done: tw.mu.Lock() defer tw.mu.Unlock() dst := w.Header() for k, vv := range tw.h { dst[k] = vv } if !tw.wroteHeader { tw.code = StatusOK } w.WriteHeader(tw.code) w.Write(tw.wbuf.Bytes()) case <-ctx.Done(): tw.mu.Lock() defer tw.mu.Unlock() w.WriteHeader(StatusServiceUnavailable) io.WriteString(w, h.errorBody()) tw.timedOut = true } }

整体流程为:

首先初始化context的timeout

初始化一个timeoutWriter, 该timeoutWriter实现了http.ResponseWriter接口, 内部结构体中有一个bytes.Buffer, 所有的Write方法都是写入到该buffer中。

异步goroutine调用serveHttp方法, timeoutWriter作为serveHttp的参数, 所以此时写入的数据并没有发送给用户, 而是缓存到了timeoutWriter的buffer中

最后select监听各个channel:

如果子groutine panic,则捕获该panic并在主grouinte中panic进行propagate

如果请求正常完成则开始写入header并将buffer中的内容写给真正的http writer

如果请求超时则返回用户503

为什么需要先写入buffer, 然后在写给真正的writer呐? 因为我们无法严格意义上的cancel掉一个请求。如果我们已经往一个http writer中写了部分数据(例如已经写了hedaer), 而此时因为某些逻辑处理较慢, 并且发现已经过了timeout阈值, 想要cancel该请求。此时已经没有办法真正意义上取消了,可能对端已经读取了部分数据了。一个典型的场景是HTTP/1.1中的分块传输, 我们先写入header, 然后依次写入各个chunk, 如果后面的chunk还没写已经超时了, 那此时就陷入了两难的情况。
此时就需要使用golang内置的TimeoutHandler了,它提供了两个优势:

首先是提供了一个buffer, 等到所有的数据写入完成, 如果此时没有超时再统一发送给对端。 并且timeoutWriter在每次Write的时候都会判断此时是否超时, 如果超时就马上返回错误。

给用户返回一个友好的503提示

实现上述两点的代价就是需要维护一个buffer来缓存所有的数据。有些情况下是这个buffer会导致一定的问题,设想一下对于一个高吞吐的server, 每个请求都维护一个buffer势必是不可接受的, 以kubernete为例, 每次list pods时可能有好几M的数据, 如果每个请求都写缓存势必会占用过多内存, 那kubernetes是如何实现timeout的呐?

kubernetes timeout Handler

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpdzdf.html