Fork me on GitHub

版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | https://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)


请我喝瓶饮料

微信支付码

2 对 “gin的timeout middleware实现”的想法;

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

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