一个较为完善的服务,必须要考虑到业务处理超时导致的出现雪崩现象的可能。为此,开发了一个简单实用的超时中间件,项目地址:https://github.com/cyj19/gin-timeout
1. 思路
在网上搜索没有发现现成的可用代码,但是超时控制明显是一个常见的问题,不可能只有我遇到。后来换了思路,搜索Golang如何进行超时控制处理,发现标准库net/http下的server.go中有一个TimeoutHandler,模仿该Handler实现超时中间件。
核心思路:把业务处理的响应结果存放到缓冲区而不是直接写入响应流,在中间件判断是否超时来选择把哪些内容写入响应流
2. 实现
有了思路,效仿之前的访问日志中间件,重写ResponseWriter涉及的方法,把结果存储到缓冲区。需要特别注意的是要把业务处理返回的响应头也是存储到缓冲区,否则会出现超时返回和正常业务返回同时写响应头的问题
writer.go
// 这部分可以参照net/http server.go中的timeoutWriter来写,稍微修改一下就行
// 在不入侵业务代码的情况下,我们是无法阻止业务结果写入响应的,因此需要自定义Writer,
// 把响应头和响应内容都只存放到缓冲区,不直接写入响应流
type timeoutWriter struct {
gin.ResponseWriter
h http.Header // response header
wbuf *bytes.Buffer // response content
mu sync.Mutex
timedOut bool
wroteHeader bool
code int // response code
}
func (tw *timeoutWriter) Header() http.Header {
return tw.h
}
func checkWriteHeaderCode(code int) {
if code < 100 || code > 999 {
panic(fmt.Sprintf("invalid WriteHeader code %v", code))
}
}
func (tw *timeoutWriter) writeHeaderLocked(code int) {
checkWriteHeaderCode(code)
switch {
case tw.timedOut:
return
case tw.wroteHeader:
return
default:
tw.wroteHeader = true
tw.code = code
return
}
}
func (tw *timeoutWriter) WriteHeader(code int) {
tw.mu.Lock()
defer tw.mu.Unlock()
tw.writeHeaderLocked(code)
}
func (tw *timeoutWriter) Write(p []byte) (int, error) {
tw.mu.Lock()
defer tw.mu.Unlock()
if tw.timedOut {
return 0, nil
}
if !tw.wroteHeader {
tw.WriteHeader(http.StatusOK)
}
// normal response content is written to wbuf
return tw.wbuf.Write(p)
}
option.go
// Option timeout configuration
type Option struct {
Timeout *time.Duration // The timeout time is generally configured in the configuration file. In order to facilitate hot update, use the pointer
Code int // response code
Msg string // response message
}
timeout.go
// 这部分可以参考net/http server.go的TimeHandler来写
func ContextTimeout(opt Option) gin.HandlerFunc {
return func(c *gin.Context) {
var tw = &timeoutWriter{
wbuf: bytes.NewBufferString(""),
ResponseWriter: c.Writer,
h: make(http.Header),
}
c.Writer = tw
ctx, cancel := context.WithTimeout(c.Request.Context(), *opt.Timeout)
defer cancel()
// 通知业务处理结束
done := make(chan struct{})
// 通知发生恐慌
panicChan := make(chan interface{}, 1)
go func() {
// 恐慌恢复
defer func() {
if p := recover(); p != nil {
panicChan <- p
}
}()
c.Next()
close(done)
}()
select {
case p := <-panicChan:
panic(p)
case <-ctx.Done():
tw.mu.Lock()
defer tw.mu.Unlock()
tw.ResponseWriter.WriteHeader(opt.Code)
_, _ = tw.ResponseWriter.Write([]byte(opt.Msg))
tw.timedOut = true
c.Abort()
case <-done:
tw.mu.Lock()
defer tw.mu.Unlock()
dst := tw.ResponseWriter.Header()
// add the header of timeoutWriter to the original header
for k, vv := range tw.h {
dst[k] = vv
}
if !tw.wroteHeader {
tw.code = http.StatusOK
}
tw.ResponseWriter.WriteHeader(tw.code)
_, _ = tw.ResponseWriter.Write(tw.wbuf.Bytes())
}
}
}
3. 总结
习惯了遇到问题就想找现成的代码,其实超时中间件和之前的访问日志中间件的实现原理都是大同小异的。多思考,多思考,多思考,不要吝啬这些时间。