go语言中http超时引发的事故解决


Posted in Golang onJune 02, 2021

前言

我们使用的是golang标准库的http client,对于一些http请求,我们在处理的时候,会考虑加上超时时间,防止http请求一直在请求,导致业务长时间阻塞等待。

最近同事写了一个超时的组件,这几天访问量上来了,网络也出现了波动,造成了接口在报错超时的情况下,还是出现了请求结果的成功。

分析下具体的代码实现

type request struct {
 method string
 url    string
 value  string
 ps     *params
}

type params struct {
 timeout     int //超时时间
 retry       int //重试次数
 headers     map[string]string
 contentType string
}

func (req *request) Do(result interface{}) ([]byte, error) {
 res, err := asyncCall(doRequest, req)
 if err != nil {
  return nil, err
 }

 if result == nil {
  return res, nil
 }

 switch req.ps.contentType {
 case "application/xml":
  if err := xml.Unmarshal(res, result); err != nil {
   return nil, err
  }
 default:
  if err := json.Unmarshal(res, result); err != nil {
   return nil, err
  }
 }

 return res, nil
}
type timeout struct {
 data []byte
 err  error
}


func doRequest(request *request) ([]byte, error) {
 var (
  req    *http.Request
  errReq error
 )
 if request.value != "null" {
  buf := strings.NewReader(request.value)
  req, errReq = http.NewRequest(request.method, request.url, buf)
  if errReq != nil {
   return nil, errReq
  }
 } else {
  req, errReq = http.NewRequest(request.method, request.url, nil)
  if errReq != nil {
   return nil, errReq
  }
 }
 // 这里的client没有设置超时时间
 // 所以当下面检测到一次超时的时候,会重新又发起一次请求
 // 但是老的请求其实没有被关闭,一直在执行
 client := http.Client{}
 res, err := client.Do(req)
 ...
}

// 重试调用请求
// 当超时的时候发起一次新的请求
func asyncCall(f func(request *request) ([]byte, error), req *request) ([]byte, error) {
 p := req.ps
 ctx := context.Background()
 done := make(chan *timeout, 1)

 for i := 0; i < p.retry; i++ {
  go func(ctx context.Context) {
   // 发送HTTP请求
   res, err := f(req)
   done <- &timeout{
    data: res,
    err:  err,
   }
  }(ctx)
  // 错误主要在这里
  // 如果超时重试为3,第一次超时了,马上又发起了一次新的请求,但是这里错误使用了超时的退出
  // 具体看上面
  select {
  case res := <-done:
   return res.data, res.err
  case <-time.After(time.Duration(p.timeout) * time.Millisecond):
  }
 }
 return nil, ecode.TimeoutErr
}

错误的原因

1、超时重试,之后过了一段时间没有拿到结果就认为是超时了,但是http请求没有被关闭;

2、错误使用了http的超时,具体的做法要通过context或http.client去实现,见下文;

修改之后的代码

func doRequest(request *request) ([]byte, error) {
 var (
  req    *http.Request
  errReq error
 )
 if request.value != "null" {
  buf := strings.NewReader(request.value)
  req, errReq = http.NewRequest(request.method, request.url, buf)
  if errReq != nil {
   return nil, errReq
  }
 } else {
  req, errReq = http.NewRequest(request.method, request.url, nil)
  if errReq != nil {
   return nil, errReq
  }
 }

 // 这里通过http.Client设置超时时间
 client := http.Client{
  Timeout: time.Duration(request.ps.timeout) * time.Millisecond,
 }
 res, err := client.Do(req)
 ...
}

func asyncCall(f func(request *request) ([]byte, error), req *request) ([]byte, error) {
 p := req.ps
 // 重试的时候只有上一个http请求真的超时了,之后才会发起一次新的请求
 for i := 0; i < p.retry; i++ {
  // 发送HTTP请求
  res, err := f(req)
  // 判断超时
  if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
   continue
  }

  return res, err

 }
 return nil, ecode.TimeoutErr
}

服务设置超时

http.Server有两个设置超时的方法:

ReadTimeout
ReadTimeout的时间计算是从连接被接受(accept)到request body完全被读取(如果你不读取body,那么时间截止到读完header为止)

WriteTimeout
WriteTimeout的时间计算正常是从request header的读取结束开始,到response write结束为止 (也就是ServeHTTP方法的生命周期)

srv := &http.Server{  
    ReadTimeout: 5 * time.Second,
    WriteTimeout: 10 * time.Second,
}

 
srv.ListenAndServe()

net/http包还提供了TimeoutHandler返回了一个在给定的时间限制内运行的handler

func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler

第一个参数是Handler,第二个参数是time.Duration(超时时间),第三个参数是string类型,当到达超时时间后返回的信息

func handler(w http.ResponseWriter, r *http.Request) {
 time.Sleep(3 * time.Second)
 fmt.Println("测试超时")

 w.Write([]byte("hello world"))
}

func server() {
 srv := http.Server{
  Addr:         ":8081",
  WriteTimeout: 1 * time.Second,
  Handler:      http.TimeoutHandler(http.HandlerFunc(handler), 5*time.Second, "Timeout!\n"),
 }
 if err := srv.ListenAndServe(); err != nil {
  os.Exit(1)
 }
}

客户端设置超时

http.client
最简单的我们通过http.Client的Timeout字段,就可以实现客户端的超时控制

http.client超时是超时的高层实现,包含了从Dial到Response Body的整个请求流程。http.client的实现提供了一个结构体类型可以接受一个额外的time.Duration类型的Timeout属性。这个参数定义了从请求开始到响应消息体被完全接收的时间限制。

func httpClientTimeout() {
 c := &http.Client{
  Timeout: 3 * time.Second,
 }

 resp, err := c.Get("http://127.0.0.1:8081/test")
 fmt.Println(resp)
 fmt.Println(err)
}

context
net/http中的request实现了context,所以我们可以借助于context本身的超时机制,实现http中request的超时处理

func contextTimeout() {
 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
 defer cancel()

 req, err := http.NewRequest("GET", "http://127.0.0.1:8081/test", nil)
 if err != nil {
  log.Fatal(err)
 }

 resp, err := http.DefaultClient.Do(req.WithContext(ctx))
 fmt.Println(resp)
 fmt.Println(err)
}

使用context的优点就是,当父context被取消时,子context就会层层退出。

http.Transport
通过Transport还可以进行一些更小维度的超时设置

  • net.Dialer.Timeout 限制建立TCP连接的时间
  • http.Transport.TLSHandshakeTimeout 限制 TLS握手的时间
  • http.Transport.ResponseHeaderTimeout 限制读取response header的时间
  • http.Transport.ExpectContinueTimeout 限制client在发送包含 Expect: 100-continue的header到收到继续发送body的response之间的时间等待。注意在1.6中设置这个值会禁用HTTP/2(DefaultTransport自1.6.2起是个特例)
func transportTimeout() {
 transport := &http.Transport{
  DialContext:           (&net.Dialer{}).DialContext,
  ResponseHeaderTimeout: 3 * time.Second,
 }

 c := http.Client{Transport: transport}

 resp, err := c.Get("http://127.0.0.1:8081/test")
 fmt.Println(resp)
 fmt.Println(err)
}

问题
如果在客户端在超时的临界点,触发了超时机制,这时候服务端刚好也接收到了,http的请求

这种服务端还是可以拿到请求的数据,所以对于超时时间的设置我们需要根据实际情况进行权衡,同时我们要考虑接口的幂等性。

总结

1、所有的超时实现都是基于Deadline,Deadline是一个时间的绝对值,一旦设置他们永久生效,不管此时连接是否被使用和怎么用,所以需要每手动设置,所以如果想使用SetDeadline建立超时机制,需要每次在Read/Write操作之前调用它。

2、使用context进行超时控制的好处就是,当父context超时的时候,子context就会层层退出。

参考

【[译]Go net/http 超时机制完全手册】
【Go 语言 HTTP 请求超时入门】
【使用 timeout、deadline 和 context 取消参数使 Go net/http 服务更灵活】

到此这篇关于go语言中http超时引发的事故解决的文章就介绍到这了,更多相关go语言 http超时内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Golang 相关文章推荐
Go语言切片前或中间插入项与内置copy()函数详解
Apr 27 Golang
解决Goland 同一个package中函数互相调用的问题
May 06 Golang
golang 实现时间戳和时间的转化
May 07 Golang
Go遍历struct,map,slice的实现
Jun 13 Golang
go开发alertmanger实现钉钉报警
Jul 16 Golang
如何解决goland,idea全局搜索快捷键失效问题
Apr 03 Golang
golang的文件创建及读写操作
Apr 14 Golang
GO语言异常处理分析 err接口及defer延迟
Apr 14 Golang
Golang日志包的使用
Apr 20 Golang
Golang gRPC HTTP协议转换示例
Jun 16 Golang
Golang二维数组的使用方式
May 28 #Golang
Golang标准库syscall详解(什么是系统调用)
May 25 #Golang
go 实现简易端口扫描的示例
May 22 #Golang
go xorm框架的使用
May 22 #Golang
Golang实现AES对称加密的过程详解
May 20 #Golang
go语言基础 seek光标位置os包的使用
May 09 #Golang
Golang 实现获取当前函数名称和文件行号等操作
May 08 #Golang
You might like
有关PHP性能优化的介绍
2013/06/20 PHP
PHP Class&amp;Object -- PHP 自排序二叉树的深入解析
2013/06/25 PHP
php使用GD实现颜色渐变实例
2015/06/02 PHP
PHP 序列化和反序列化函数实例详解
2020/07/18 PHP
use jscript Create a SQL Server database
2007/06/16 Javascript
javascript 面向对象全新理练之数据的封装
2009/12/03 Javascript
jquery 页面全选框实践代码
2010/04/02 Javascript
jQuery实战之品牌展示列表效果
2011/04/10 Javascript
php实例分享之实现显示网站运行时间
2014/05/20 Javascript
JQuery分屏指示器图片轮换效果实例
2015/05/21 Javascript
原生js配合cookie制作保存路径的拖拽
2015/12/29 Javascript
jQuery焦点图轮播插件KinSlideshow用法分析
2016/06/08 Javascript
Angular多选、全选、批量选择操作实例代码
2017/03/10 Javascript
js案例之鼠标跟随jquery版(实例讲解)
2017/07/21 jQuery
浅谈vue+webpack项目调试方法步骤
2017/09/11 Javascript
Javascript中从学习bind到实现bind的过程
2018/01/05 Javascript
bootstrap fileinput插件实现预览上传照片功能
2018/01/23 Javascript
js实现上下左右键盘控制div移动
2020/01/16 Javascript
全面解析js中的原型,原型对象,原型链
2021/01/25 Javascript
Python编程pygame模块实现移动的小车示例代码
2018/01/03 Python
详解Python3 pandas.merge用法
2019/09/05 Python
Pandas数据离散化原理及实例解析
2019/11/16 Python
html5 利用canvas实现超级玛丽简单动画
2013/09/06 HTML / CSS
瑞贝卡·明可弗包包官网:Rebecca Minkoff
2016/07/21 全球购物
圣彼得堡鲜花配送:Semicvetic
2020/09/15 全球购物
养殖项目策划书范文
2014/01/13 职场文书
岗位职责的构建方法
2014/02/01 职场文书
新闻编辑专业毕业自荐书范文
2014/02/05 职场文书
文秘班元旦晚会活动策划方案
2014/08/28 职场文书
2014年国庆晚会主持词
2014/09/19 职场文书
2014年创先争优工作总结
2014/12/11 职场文书
太空授课观后感
2015/06/17 职场文书
安全守法证明
2015/06/23 职场文书
导游词之岳阳楼
2019/09/25 职场文书
Java实现字符串转为驼峰格式的方法详解
2022/07/07 Java/Android
CSS中理解层叠性及权重如何分配
2022/12/24 HTML / CSS