版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | http://vearne.cc

1. 前言

说到Golang中应用最广泛的web框架,恐怕非gin-gonic/gin莫属了。在服务中,如果它依赖的后端服务出现异常,我们希望错误能够快速的暴露给调用方,而部署无限期的等待。我们需要一个timeout middleware, 来完成这个目标。
对于gin框架,timeout middleware可参考的资料,比较多见的是参考资料1,但是它的实现,对业务代码造成了入侵,不是很友好,这里给出笔者的实现,供大家参考

2. 实现

直接上代码
main.go

package main

import (
    "bytes"
    "github.com/gin-gonic/gin"
    "github.com/vearne/golib/buffpool"
    "log"
    "net/http"
    "time"
)


type SimplebodyWriter struct {
    gin.ResponseWriter
    body *bytes.Buffer
}

func (w SimplebodyWriter) Write(b []byte) (int, error) {
    return w.body.Write(b)
}


func Timeout(t time.Duration) gin.HandlerFunc {
    return func(c *gin.Context) {
        // sync.Pool
        buffer := buffpool.GetBuff()

        blw := &SimplebodyWriter{body: buffer, ResponseWriter: c.Writer}
        c.Writer = blw

        finish := make(chan struct{})
        go func() { // 子协程只会将返回数据写入到内存buff中
            c.Next()
            finish <- struct{}{}
        }()

        select {
        case <-time.After(t):
            c.Writer.WriteHeader(http.StatusGatewayTimeout)
            c.Abort()
            // 1. 主协程超时退出。此时,子协程可能仍在运行
            // 如果超时的话,buffer无法主动清除,只能等待GC回收
        case <-finish:
            // 2. 返回结果只会在主协程中被写入
            blw.ResponseWriter.Write(buffer.Bytes())
            buffpool.PutBuff(buffer)
        }
    }
}

func short(c *gin.Context) {
    time.Sleep(1 * time.Second)
    c.JSON(http.StatusOK, gin.H{"hello":"world"})
}

func long(c *gin.Context) {
    time.Sleep(5 * time.Second)
    c.JSON(http.StatusOK, gin.H{"hello":"world"})
}

func main() {
    // create new gin without any middleware
    engine := gin.New()

    // add timeout middleware with 2 second duration
    engine.Use(Timeout(time.Second * 2))

    // create a handler that will last 1 seconds
    engine.GET("/short", short)

    // create a route that will last 5 seconds
    engine.GET("/long", long)

    // run the server
    log.Fatal(engine.Run(":8080"))
}

简单解释一下:

2.1 子协程

具体的处理逻辑在子协程中完成

go func() { // 子协程只会将返回数据写入到内存buff中
    c.Next()
    finish <- struct{}{}
}()

但子协程只是将返回结果写入内存buff中

2.2 主协程

主协程阻塞
1)或者等待deadline到达
2)或者接收到子协程的通知(通过channel)

情况1
如果dealline到达,返回给调用方HTTP Code 504

╰─$ curl -v http://localhost:8080/long
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /long HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.46.0
> Accept: */*
>
< HTTP/1.1 504 Gateway Timeout
< Date: Thu, 16 May 2019 02:48:03 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact

但此时子协程仍在运行。(Golang中没有提供主动结束某个协程的API,所以它将继续执行,直到主动退出)

子协程的所有的所有输出,都只暂存在内存buff中。主协程退出后,这些数据也就没有机会被返回给调用方

注意
在gin的后台,仍然能观察到警告

[WARNING] Headers were already written. Wanted to override status code 504 with 200

但是没有关系,没有任何数据被发送给调用方

情况2
主协程收到子协程的消息, 将header头和内存buff中的数据,写入到流中,发送给调用方

    blw.ResponseWriter.Write(buffer.Bytes())

3. 总结

本实现对业务代码没有任何入侵,无需修改原来的代码

警告
子协程的退出,需要被考虑,否则在高并发情况下,会有大量的协程在后台运行。要么最终达到最大协程限制,或者耗尽内存而被操作系统杀死。

4. 参考资料

  1. montanaflynn的实现

5. 后记

重要
此程序有bug,请阅读
GIN的TIMEOUT MIDDLEWARE实现(续2)


如果我的文章对你有帮助,你可以给我打赏以促使我拿出更多的时间和精力来分享我的经验和思考总结。

微信支付码

发表评论

电子邮件地址不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据