gin的timeout middleware实现
版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | 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. 参考资料
5. 后记
重要
此程序有bug,请阅读
GIN的TIMEOUT MIDDLEWARE实现(续2)
当超时次数多,
go func 不会释放, 会出现goroutine leak.
看萌叔的这篇文章 GIN的TIMEOUT MIDDLEWARE实现(续2)