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 相关文章推荐
win10下go mod配置方式
Apr 25 Golang
goland 清除所有的默认设置操作
Apr 28 Golang
goland 恢复已更改文件的操作
Apr 28 Golang
golang 生成对应的数据表struct定义操作
Apr 28 Golang
解决Golang time.Parse和time.Format的时区问题
Apr 29 Golang
golang 实现时间戳和时间的转化
May 07 Golang
golang中的struct操作
Nov 11 Golang
golang为什么要统一错误处理
Apr 03 Golang
Go语言 详解net的tcp服务
Apr 14 Golang
golang定时器
Apr 14 Golang
Golang 实现 WebSockets 之创建 WebSockets
Apr 24 Golang
在ubuntu下安装go开发环境的全过程
Aug 05 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支持断点续传的源码
2010/05/16 PHP
php中定义网站根目录的常用方法
2010/08/08 PHP
PHP实现的比较完善的购物车类
2014/12/02 PHP
PHP实现 APP端微信支付功能
2018/06/22 PHP
CSS JavaScript 实现菜单功能 改进版
2008/12/09 Javascript
jQuery Tips 为AJAX回调函数传递额外参数的方法
2010/12/28 Javascript
EXTJS记事本 当CompositeField遇上RowEditor
2011/07/31 Javascript
javascript的动态加载、缓存、更新以及复用(一)
2014/06/09 Javascript
如何用javascript计算文本框还能输入多少个字符
2015/07/29 Javascript
AngularJS Phonecat实例讲解
2016/11/21 Javascript
vue axios 二次封装的示例代码
2017/12/08 Javascript
详解keep-alive + vuex 让缓存的页面灵活起来
2019/04/19 Javascript
[01:46]2018完美盛典章节片——坚守
2018/12/17 DOTA
[00:52]DOTA2国际邀请赛
2020/02/21 DOTA
python简单实现基数排序算法
2015/05/16 Python
基于python 处理中文路径的终极解决方法
2018/04/12 Python
python opencv实现旋转矩形框裁减功能
2018/07/25 Python
对python 中re.sub,replace(),strip()的区别详解
2019/07/22 Python
在python中用url_for构造URL的方法
2019/07/25 Python
Python 切分数组实例解析
2019/11/07 Python
python/Matplotlib绘制复变函数图像教程
2019/11/21 Python
Django基于客户端下载文件实现方法
2020/04/21 Python
台湾SHOPRO购物行家:亚洲首创影视.3C.家电.优质购物平台
2018/05/07 全球购物
烹饪大赛策划方案
2014/05/26 职场文书
企业环保标语
2014/06/10 职场文书
小学数学课题方案
2014/06/15 职场文书
学校综治宣传月活动总结
2014/07/02 职场文书
小学安全汇报材料
2014/08/14 职场文书
高等学院职业生涯规划书范文
2014/09/16 职场文书
个人四风问题整改措施思想汇报
2014/10/04 职场文书
公务员党的群众路线教育实践活动学习心得体会
2014/10/30 职场文书
2015年纪委工作总结
2015/05/13 职场文书
2016年三八节红领巾广播稿
2015/12/17 职场文书
听课评课活动心得体会
2016/01/15 职场文书
多台电脑共享文件怎么设置?多台电脑共享文件操作教程
2022/04/08 数码科技
Java的Object类的九种方法
2022/04/13 Java/Android