在上一篇文章中,我们探讨过设置超时时间过小,没有错误,服务端返回“empty reply”的错误(参见go http包超时时间设置过小会发生什么),本次我们将会探讨go http包的超时问题,如何避免因超时导致资源耗尽而引发崩溃,最后我们还会讨论下go 并发设计之context。
先来看一段代码,在代码中我们设置WriteTimeout为1s,但请求处理需要3s,
package main
import (
"fmt"
"io"
"net/http"
"time"
)
func slowHandler(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, "begin slowHandler\n")
sleepSeconds := 3
//模拟慢处理请求 耗时 3s
for i := 0; i < sleepSeconds; i++ {
fmt.Println("i:", i, ", now:", time.Now())
time.Sleep(1 * time.Second)
}
io.WriteString(w, "end slowHandler\n")
fmt.Println("finish slow execution!")
}
func main() {
srv := http.Server{
Addr: ":8888",
WriteTimeout: 1 * time.Second,//设置超时为1s
Handler: http.HandlerFunc(slowHandler),
}
if err := srv.ListenAndServe(); err != nil {
fmt.Printf("Server failed: %s\n", err)
}
}
运行代码后IDE输出
curl请求返回“Empty reply"
从上面的例子中我们可以发现设置1s超时,
1s后我们写不了响应
了,但是handler请求还在处理,并没有停止,而且即使写不了响应后,我们还需要等待3s(慢请求执行3s)才能拿到响应结果,那我们需要如何优化,才能立即拿到超时结果,且能终止慢请求的处理呢?一、基本原理介绍1、基本概念 在net/http包中,通过创建一个http.Server结构体,该结构体包括4个超时参数,如下图所示,这4个参数很好理解,这里就不再解释。
我们之前讲过处理请求时,每次都会调用readRequest,从下图我们可以看出我们设置的超时会被用做设置c.rwc的dealline参数,而c.rwc是什么呢?
它是*net.TCPConn类型,这体现了我们的http请求实际上是基于tcp的,意味着我们设置的超时参数表现为tcp连接的deadline,而不是http超时。
简单理解deadline机制就是
设置一个绝对值,超过该值后,特定的行为都会被限制,比如一开始的例子中,因无法写入导致无法获取超时后的响应。那我们如何处理超时呢?2、处理超时 我们还是先贴代码
package main
import (
"fmt"
"io"
"net/http"
"time"
)
func slowHandler(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, "begin slowHandler\n")
sleepSeconds := 10
//模拟慢处理请求 耗时 3s
for i := 0; i < sleepSeconds; i++ {
fmt.Println("i:", i, ", now:", time.Now())
time.Sleep(1 * time.Second)
}
io.WriteString(w, "end slowHandler\n")
fmt.Println("finish slow execution!")
}
func main() {
srv := http.Server{
Addr: ":8888",
//该值需要大于超时时间,不然无法写入响应
WriteTimeout: 5 * time.Second,
// http.HandlerFunc(slowHandler),使用TimeoutHandler进行封装
//方法在go中是一等公民,设置1s超时
Handler: http.TimeoutHandler(http.HandlerFunc(slowHandler), 1*time.Second, "request Timeout!\n"),
}
if err := srv.ListenAndServe(); err != nil {
fmt.Printf("Server failed: %s\n", err)
}
}
代码运行后发现超时输出了"request Timeout"
但是请求处理并未结束,虽然超时了,但是仍然占着资源。那我们怎么才能在超时后,请求结束,释放资源呢?这就依赖于我们的上下文context了。
二、go context设计原理 在介绍之前我们先来了解下go上下文的设计原理,本小节篇幅较长,大家可选择跳过。1、基本介绍 go中的上下文可用来同步信号,设置截止日期,传递请求相关值的结构体,context是go语言中独有的设计,
context.Context接口中定义了如下4个方法。
- Deadline,返回context.Context被取消的截止时间,即工作截止时间
- Done,返回一个struct的chan,这个chan会在工作完成,或者上下文取消后关闭;多次调用返回的值相同
- Err,返回context.Context结束的原因,只有在Done返回的chan被关闭时才会返回非空值
- Value, 从context.Context中获取对应键的值,多次调用返回的结果相同
- 当 parent.Done() == nil,也就是 parent 不会触发取消信号时,方法会直接返回
- 当取消信号非context.cancelCtx、context.timerCtx、context.valueCtx时,判断 parent 是否已经触发了取消信号,如果取消了会调用child.cancel来取消child,如果没取消,会将child加入parent的等待列表
- 当取消信号来自其它上下文(开发者自定义的类型),会新开一个goroutine,同时监听parent.Done,child.Done,在parent取消时,也会取消child
package main
import (
"context"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"time"
)
func slowCall(ctx context.Context) string {
s := rand.Intn(5)
for i := 0; i
select {
case
log.Printf("Slow call done after %d seconds, reason %s\n", s, ctx.Err().Error())
return "done"
case
log.Printf("Slow call finished after %d seconds.\n", s)
default:
log.Println("select default case")
}
fmt.Println("i:", i, ", now:", time.Now())
time.Sleep(1 * time.Second)
}
return "end"
}
func slowHandler(w http.ResponseWriter, r *http.Request) {
res := slowCall(r.Context())
io.WriteString(w, "response:"+res+"\n")
}
func main() {
srv := http.Server{
Addr: ":8888",
//该值需要大于慢请求处理时间,不然无法写入响应
WriteTimeout: 5 * time.Second,
Handler: http.TimeoutHandler(http.HandlerFunc(slowHandler), 1*time.Second, "request Timeout!\n"),
}
if err := srv.ListenAndServe(); err != nil {
fmt.Printf("Server failed: %s\n", err)
}
}
for中的循环仅执行了一次就退出了
四、关于写HTTP服务的建议1、要使用截止时间,理解各项超时参数的意义,确保做了完整测试,达到符合预期的控制2、对于超时取消别忘记使用上下文