记一次http超时引发的事故

前言

我们使用的是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的超时,具体的做法要通过contexthttp.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.ClientTimeout字段,就可以实现客户端的超时控制

http.client超时是超时的高层实现,包含了从DialResponse 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本身的超时机制,实现httprequest的超时处理

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

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

参考

【[译]Go net/http 超时机制完全手册】https://colobu.com/2016/07/01/the-complete-guide-to-golang-net-http-timeouts/

【Go 语言 HTTP 请求超时入门】https://studygolang.com/articles/14405

【使用 timeout、deadline 和 context 取消参数使 Go net/http 服务更灵活】https://jishuin.proginn.com/p/763bfbd2fb6a

记go中一次http超时引发的事故的更多相关文章

  1. httpClient中的三种超时设置小结

    httpClient中的三种超时设置小结   本文章给大家介绍一下关于Java中httpClient中的三种超时设置小结,希望此教程能给各位朋友带来帮助. ConnectTimeoutExceptio ...

  2. ie、firefox、chrome中关于style="display:block" 引发的页面布局错乱的解决办法

    ie.firefox.chrome中关于style="display:block" 引发的页面布局错乱的解决办法: table中tr 添加style="display:b ...

  3. 记Oracle中regexp_substr的一次调优(速度提高95.5%)

    项目中需要做一个船舶代理费的功能,针对代理的船进行收费,那么该功能的第一步便是选择进行代理费用信息的录入,在进行船舶选择的时候,发现加载相关船舶信息十分的慢,其主要在sql语句的执行,因为测试的时候数 ...

  4. golang中mysql建立连接超时时间timeout 测试

    本文测试连接mysql的超时时间. 这里的"连接"是建立连接的意思. 连接mysql的超时时间是通过参数timeout设置的. 1.建立连接超时测试 下面例子中,设置连接超时时间为 ...

  5. 【故障公告】再次遭遇SQL语句执行超时引发网站首页访问故障

    非常抱歉,昨天 18:40~19:10 再次遭遇上次遇到的 SQL 语句执行超时引发的网站首页访问故障,由此您带来麻烦,请您谅解. 上次故障详见故障公告,上次排查下来以为是 SQL Server 参数 ...

  6. linux自动化交互脚本expect详解set timeout 5是 意思是在expect语句中,5s后超时,不再作出选择。

    linux自动化交互脚本expect详解  更新时间:2020年10月21日 10:13:20   作者:lendsomething     这篇文章主要介绍了linux自动化交互脚本expect的相 ...

  7. 记ByteCTF中的Node题

    记ByteCTF中的Node题 我总觉得字节是跟Node过不去了,初赛和决赛都整了个Node题目,当然PHP.Java都是必不可少的,只是我觉得Node类型的比较少见,所以感觉挺新鲜的. Nothin ...

  8. 一次单片机 SFR 页引发的“事故”

    一次单片机 SFR 页引发的"事故" 现象 需要使用单片机的 ADC 功能,在对 ADC 初始化后,根据内部分的 IVREN 计算出 VDD 的电压值 . 在读取时一直显示 ADC ...

  9. 记一次ZOOKEEPER集群超时问题分析

    CDH安装的ZK,三个节点,基本都是默认配置,一直用得正常,今天出现问题,客户端连接超时6倍时长,默认最大会话超时时间是一分钟.原因分析:1.首先要确认网络正确.确认时钟同步.2.查看现有的配置,基本 ...

随机推荐

  1. Redis——急速安装并设置自启(CentOS)

    现状 对于开发人员来说,部署服务器环境并不是一个高频操作.所以就导致绝大部分开发人员不会花太多时间去学习记忆,而是直接百度(有一些同学可能连链接都懒得收藏).所以到了部署环境的时候就头疼,甚至是抗拒. ...

  2. hdu3715 二分+2sat+建图

    题意:       给你一个递归公式,每多一层就多一个限制,问你最多能递归多少层. 思路:      先分析每一层的限制 x[a[i]] + x[b[i]] != c[i],这里面x[] = 0,1, ...

  3. LA3602DNA序列

    题意:      给你一个一些DNA序列(只有ACGT)然后让你构造一个序列,使得所有的序列到他的Hamming距离最小,所有的序列包括构造的序列长度都是N,Hamming表示两个序列的不同字符位置个 ...

  4. ZOJ3715 竞选班长求最小花费

    题意:       有n个小朋友竞选班长,一号想当班长,每个人都必须选择一个人当班长,并且不可以选择自己,并且每个人都有一个权值ai,这个权值就是如果1想让这个人改变主意选择自己当班长就得给他ai个糖 ...

  5. POJ3160强连通+spfa最长路(不错)

    题意:       给你一个有向图,每个点上有一个权值,可正可负,然后给你一些链接关系,让你找到一个起点,从起点开始走,走过的边可以在走,但是拿过权值的点就不能再拿了,问最多能拿到多少权值? 思路: ...

  6. [转帖]大家分析分析C++ X64X86通用驱动读写API源码教程

    //#include  <windows.h>//#include <algorithm>  //#include <string.h>//#include < ...

  7. jquery里面.length和.size()有什么区别

    区别: 1.针对标签对象元素,比如数html页面有多少个段落元素<p></p>,那么此时的$("p").size()==$("p").l ...

  8. Jedis基础详解

    Jedis 使用Java来操作Redis 什么是Jedis 是Redis官方推荐的Java操作Redis中间件, 如果你要使用Java操作Redis, 那么就该对jedis熟悉 测试 导入对应的依赖 ...

  9. Lombok Requires Annotation Processing Annotation processing seems to be disabled for the project "HelloWorld". For  plugin to function correctly, please enable it under "Settings > Build > Compiler >

    更多精彩详见微信公众号  在网上查找说是插件的问题,但是我安装类插件父级项目没有开启注解处理Annotation Processor,子项目都有开启,如图,顶级项目是demo,下面的都是子项目,把第一 ...

  10. layui中select的change事件、动态追加option

    说明:layui中用jquery 中的选择器例如$('#id').change(function(){})发现不起作用 layui操作:lay-felter标识操作哪个select html部分: & ...