我们来看一段日常代码。
1package main
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "io/ioutil"
8 "net"
9 "net/http"
10 "time"
11)
12
13var tr *http.Transport
14
15func init() {
16 tr = &http.Transport{
17 MaxIdleConns: 100,
18 Dial: func(netw, addr string) (net.Conn, error) {
19 conn, err := net.DialTimeout(netw, addr, time.Second*2) //设置建立连接超时
20 if err != nil {
21 return nil, err
22 }
23 err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //设置发送接受数据超时
24 if err != nil {
25 return nil, err
26 }
27 return conn, nil
28 },
29 }
30}
31
32func main() {
33 for {
34 _, err := Get("http://www.baidu.com/")
35 if err != nil {
36 fmt.Println(err)
37 break
38 }
39 }
40}
41
42
43func Get(url string) ([]byte, error) {
44 m := make(map[string]interface{})
45 data, err := json.Marshal(m)
46 if err != nil {
47 return nil, err
48 }
49 body := bytes.NewReader(data)
50 req, _ := http.NewRequest("Get", url, body)
51 req.Header.Add("content-type", "application/json")
52
53 client := &http.Client{
54 Transport: tr,
55 }
56 res, err := client.Do(req)
57 if res != nil {
58 defer res.Body.Close()
59 }
60 if err != nil {
61 return nil, err
62 }
63 resBody, err := ioutil.ReadAll(res.Body)
64 if err != nil {
65 return nil, err
66 }
67 return resBody, nil
68}
做的事情,比较简单,就是循环去请求 http://www.baidu.com/
, 然后等待响应。
看上去貌似没啥问题吧。
代码跑起来,也确实能正常收发消息。
但是这段代码跑一段时间,就会出现 i/o timeout
的报错。
这其实是最近排查了的一个问题,发现这个坑可能比较容易踩上,我这边对代码做了简化。
实际生产中发生的现象是,golang
服务在发起http
调用时,虽然http.Transport
设置了3s
超时,会偶发出现i/o timeout
的报错。
但是查看下游服务的时候,发现下游服务其实 100ms
就已经返回了。
五层网络协议对应的消息体变化分析
就很奇怪了,明明服务端显示处理耗时才100ms
,且客户端超时设的是3s
, 怎么就出现超时报错 i/o timeout
呢?
这里推测有两个可能。
3s-100ms
可能是耗在了整个流程里的各个层上。比如网络不好的情况下,传输层TCP使劲丢包重传之类的原因。100ms
左右。客户端处理逻辑问题导致超时。一般遇到问题,大部分情况下都不会是底层网络的问题,大胆怀疑是自己的问题就对了,不死心就抓个包看下。
抓包结果
分析下,从刚开始三次握手(画了红框的地方)。
到最后出现超时报错 i/o timeout
(画了蓝框的地方)。
从time
那一列从7
到10
,确实间隔3s
。而且看右下角的蓝框,是51169
端口发到80
端口的一次Reset
连接。
80
端口是服务端的端口。换句话说就是客户端3s
超时主动断开链接的。
但是再仔细看下第一行三次握手到最后客户端超时主动断开连接的中间,其实有非常多次HTTP请求。
回去看代码设置超时的方式。
1 tr = &http.Transport{
2 MaxIdleConns: 100,
3 Dial: func(netw, addr string) (net.Conn, error) {
4 conn, err := net.DialTimeout(netw, addr, time.Second*2) //设置建立连接超时
5 if err != nil {
6 return nil, err
7 }
8 err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //设置发送接受数据超时
9 if err != nil {
10 return nil, err
11 }
12 return conn, nil
13 },
14 }
也就是说,这里的3s
超时,其实是在建立连接之后开始算的,而不是单次调用开始算的超时。
看注释里写的是
SetDeadline sets the read and write deadlines associated with the connection.
大家知道HTTP
是应用层协议,传输层用的是TCP
协议。
HTTP协议从1.0
以前,默认用的是短连接
,每次发起请求都会建立TCP连接。收发数据。然后断开连接。
TCP连接每次都是三次握手。每次断开都要四次挥手。
其实没必要每次都建立新连接,建立的连接不断开就好了,每次发送数据都复用就好了。
于是乎,HTTP协议从1.1
之后就默认使用长连接
。具体相关信息可以看之前的 [这篇文章] 。
那么golang标准库
里也兼容这种实现。
通过建立一个连接池,针对每个域名
建立一个TCP长连接,比如http://baidu.com
和http://golang.com
就是两个不同的域名。
第一次访问http://baidu.com
域名的时候会建立一个连接,用完之后放到空闲连接池里,下次再要访问http://baidu.com
的时候会重新从连接池里把这个连接捞出来复用
。
复用长连接
插个题外话:这也解释了之前[这篇文章] 里最后的疑问,为什么要强调是同一个域名:一个域名会建立一个连接,一个连接对应一个读goroutine和一个写goroutine。正因为是同一个域名,所以最后才会泄漏
3
个goroutine,如果不同域名的话,那就会泄漏1+2*N
个协程,N
就是域名数。
假设第一次请求要100ms
,每次请求完http://baidu.com
后都放入连接池中,下次继续复用,重复29
次,耗时2900ms
。
第30
次请求的时候,连接从建立开始到服务返回前就已经用了3000ms
,刚好到设置的3s超时阈值,那么此时客户端就会报超时 i/o timeout
。
虽然这时候服务端其实才花了100ms
,但耐不住前面29次
加起来的耗时已经很长。
也就是说只要通过 http.Transport
设置了 err = conn.SetDeadline(time.Now().Add(time.Second * 3))
,并且你用了长连接,哪怕服务端处理再快,客户端设置的超时再长,总有一刻,你的程序会报超时错误。
原本预期是给每次调用设置一个超时,而不是给整个连接设置超时。
另外,上面出现问题的原因是给长连接设置了超时,且长连接会复用。
基于这两点,改一下代码。
1package main
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "io/ioutil"
8 "net/http"
9 "time"
10)
11
12var tr *http.Transport
13
14func init() {
15 tr = &http.Transport{
16 MaxIdleConns: 100,
17 // 下面的代码被干掉了
18 //Dial: func(netw, addr string) (net.Conn, error) {
19 // conn, err := net.DialTimeout(netw, addr, time.Second*2) //设置建立连接超时
20 // if err != nil {
21 // return nil, err
22 // }
23 // err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //设置发送接受数据超时
24 // if err != nil {
25 // return nil, err
26 // }
27 // return conn, nil
28 //},
29 }
30}
31
32
33func Get(url string) ([]byte, error) {
34 m := make(map[string]interface{})
35 data, err := json.Marshal(m)
36 if err != nil {
37 return nil, err
38 }
39 body := bytes.NewReader(data)
40 req, _ := http.NewRequest("Get", url, body)
41 req.Header.Add("content-type", "application/json")
42
43 client := &http.Client{
44 Transport: tr,
45 Timeout: 3*time.Second, // 超时加在这里,是每次调用的超时
46 }
47 res, err := client.Do(req)
48 if res != nil {
49 defer res.Body.Close()
50 }
51 if err != nil {
52 return nil, err
53 }
54 resBody, err := ioutil.ReadAll(res.Body)
55 if err != nil {
56 return nil, err
57 }
58 return resBody, nil
59}
60
61func main() {
62 for {
63 _, err := Get("http://www.baidu.com/")
64 if err != nil {
65 fmt.Println(err)
66 break
67 }
68 }
69}
看注释会发现,改动的点有两个
http.Transport
里的建立连接时的一些超时设置干掉了。http.Client
,此时加入超时设置,这里的超时就可以理解为单次请求的超时了。同样可以看下注释Timeout specifies a time limit for requests made by this Client.
到这里,代码就改好了,实际生产中问题也就解决了。
实例代码里,如果拿去跑的话,其实还会下面的错
1Get http://www.baidu.com/: EOF
这个是因为调用得太猛了,http://www.baidu.com
那边主动断开的连接,可以理解为一个限流措施,目的是为了保护服务器,毕竟每个人都像这么搞,服务器是会炸的。。。
解决方案很简单,每次HTTP调用中间加个sleep
间隔时间就好。
到这里,其实问题已经解决了,下面会在源码层面分析出现问题的原因。对读源码不感兴趣的朋友们可以不用接着往下看,直接拉到文章底部右下角,做点正能量的事情(点两下)支持一下。(疯狂暗示,拜托拜托,这对我真的很重要!)
用的go版本是1.12.7。
从发起一个网络请求开始跟。
1res, err := client.Do(req)
2func (c *Client) Do(req *Request) (*Response, error) {
3 return c.do(req)
4}
5
6func (c *Client) do(req *Request) {
7 // ...
8 if resp, didTimeout, err = c.send(req, deadline); err != nil {
9 // ...
10 }
11 // ...
12}
13func send(ireq *Request, rt RoundTripper, deadline time.Time) {
14 // ...
15 resp, err = rt.RoundTrip(req)
16 // ...
17}
18
19// 从这里进入 RoundTrip 逻辑
20/src/net/http/roundtrip.go: 16
21func (t *Transport) RoundTrip(req *Request) (*Response, error) {
22 return t.roundTrip(req)
23}
24
25func (t *Transport) roundTrip(req *Request) (*Response, error) {
26 // 尝试去获取一个空闲连接,用于发起 http 连接
27 pconn, err := t.getConn(treq, cm)
28 // ...
29}
30
31// 重点关注这个函数,返回是一个长连接
32func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (*persistConn, error) {
33 // 省略了大量逻辑,只关注下面两点
34 // 有空闲连接就返回
35 pc := <-t.getIdleConnCh(cm)
36
37 // 没有创建连接
38 pc, err := t.dialConn(ctx, cm)
39
40}
这里上面很多代码,其实只是为了展示这部分代码是怎么跟踪下来的,方便大家去看源码的时候去跟一下。
最后一个上面的代码里有个 getConn
方法。在发起网络请求的时候,会先取一个网络连接,取连接有两个来源。
如果有空闲连接,就拿空闲连接
1/src/net/http/tansport.go:810
2func (t *Transport) getIdleConnCh(cm connectMethod) chan *persistConn {
3 // 返回放空闲连接的chan
4 ch, ok := t.idleConnCh[key]
5 // ...
6 return ch
7}
没有空闲连接,就创建长连接。
1/src/net/http/tansport.go:1357
2func (t *Transport) dialConn() {
3 //...
4 conn, err := t.dial(ctx, "tcp", cm.addr())
5 // ...
6 go pconn.readLoop()
7 go pconn.writeLoop()
8 // ...
9}
当第一次发起一个http请求时,这时候肯定没有空闲连接,会建立一个新连接。同时会创建一个读goroutine和一个写goroutine。
读写协程
注意上面代码里的t.dial(ctx, "tcp", cm.addr())
,如果像文章开头那样设置了 http.Transport
的
1Dial: func(netw, addr string) (net.Conn, error) {
2 conn, err := net.DialTimeout(netw, addr, time.Second*2) //设置建立连接超时
3 if err != nil {
4 return nil, err
5 }
6 err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //设置发送接受数据超时
7 if err != nil {
8 return nil, err
9 }
10 return conn, nil
11},
那么这里就会在下面的dial
里被执行到
1func (t *Transport) dial(ctx context.Context, network, addr string) (net.Conn, error) {
2 // ...
3 c, err := t.Dial(network, addr)
4 // ...
5}
这里面调用的设置超时,会执行到
1/src/net/net.go
2func (c *conn) SetDeadline(t time.Time) error {
3 //...
4 c.fd.SetDeadline(t)
5 //...
6}
7
8//...
9
10func setDeadlineImpl(fd *FD, t time.Time, mode int) error {
11 // ...
12 runtime_pollSetDeadline(fd.pd.runtimeCtx, d, mode)
13 return nil
14}
15
16
17//go:linkname poll_runtime_pollSetDeadline internal/poll.runtime_pollSetDeadline
18func poll_runtime_pollSetDeadline(pd *pollDesc, d int64, mode int) {
19 // ...
20 // 设置一个定时器事件
21 rtf = netpollDeadline
22 // 并将事件注册到定时器里
23 modtimer(&pd.rt, pd.rd, 0, rtf, pd, pd.rseq)
24}
上面的源码,简单来说就是,当第一次调用请求的,会建立个连接,这时候还会注册一个定时器事件,假设时间设了3s
,那么这个事件会在3s
后发生,然后执行注册事件的逻辑。而这个注册事件就是netpollDeadline
。 注意这个netpollDeadline
,待会会提到。
读写协程定时器事件
设置了超时事件,且超时事件是3s后之后,发生。再次期间正常收发数据。一切如常。
直到3s
过后,这时候看读goroutine
,会等待网络数据返回。
1/src/net/http/tansport.go:1642
2func (pc *persistConn) readLoop() {
3 //...
4 for alive {
5 _, err := pc.br.Peek(1) // 阻塞读取服务端返回的数据
6 //...
7}
然后就是一直跟代码。
1src/bufio/bufio.go: 129
2func (b *Reader) Peek(n int) ([]byte, error) {
3 // ...
4 b.fill()
5 // ...
6}
7
8func (b *Reader) fill() {
9 // ...
10 n, err := b.rd.Read(b.buf[b.w:])
11 // ...
12}
13
14/src/net/http/transport.go: 1517
15func (pc *persistConn) Read(p []byte) (n int, err error) {
16 // ...
17 n, err = pc.conn.Read(p)
18 // ...
19}
20
21// /src/net/net.go: 173
22func (c *conn) Read(b []byte) (int, error) {
23 // ...
24 n, err := c.fd.Read(b)
25 // ...
26}
27
28func (fd *netFD) Read(p []byte) (n int, err error) {
29 n, err = fd.pfd.Read(p)
30 // ...
31}
32
33/src/internal/poll/fd_unix.go:
34func (fd *FD) Read(p []byte) (int, error) {
35 //...
36 if err = fd.pd.waitRead(fd.isFile); err == nil {
37 continue
38 }
39 // ...
40}
41
42func (pd *pollDesc) waitRead(isFile bool) error {
43 return pd.wait('r', isFile)
44}
45
46func (pd *pollDesc) wait(mode int, isFile bool) error {
47 // ...
48 res := runtime_pollWait(pd.runtimeCtx, mode)
49 return convertErr(res, isFile)
50}
直到跟到 runtime_pollWait,这个可以简单认为是等待服务端数据返回。
1//go:linkname poll_runtime_pollWait internal/poll.runtime_pollWait
2func poll_runtime_pollWait(pd *pollDesc, mode int) int {
3
4 // 1.如果网络正常返回数据就跳出
5 for !netpollblock(pd, int32(mode), false) {
6 // 2.如果有出错情况也跳出
7 err = netpollcheckerr(pd, int32(mode))
8 if err != 0 {
9 return err
10 }
11 }
12 return 0
13}
整条链路跟下来,就是会一直等待数据,等待的结果只有两个
这里面的报错,又有那么两种
1func netpollcheckerr(pd *pollDesc, mode int32) int {
2 if pd.closing {
3 return 1 // errClosing
4 }
5 if (mode == 'r' && pd.rd < 0) || (mode == 'w' && pd.wd < 0) {
6 return 2 // errTimeout
7 }
8 return 0
9}
其中提到的超时
,就是指这里面返回的数字2
,会通过下面的函数,转化为 ErrTimeout
, 而 ErrTimeout.Error()
其实就是 i/o timeout
。
1func convertErr(res int, isFile bool) error {
2 switch res {
3 case 0:
4 return nil
5 case 1:
6 return errClosing(isFile)
7 case 2:
8 return ErrTimeout // ErrTimeout.Error() 就是 "i/o timeout"
9 }
10 println("unreachable: ", res)
11 panic("unreachable")
12}
那么问题来了。上面返回的超时错误,也就是返回2的时候的条件是怎么满足的?
1 if (mode == 'r' && pd.rd < 0) || (mode == 'w' && pd.wd < 0) {
2 return 2 // errTimeout
3 }
还记得刚刚提到的 netpollDeadline
吗?
这里面放了定时器3s
到点时执行的逻辑。
1func timerproc(tb *timersBucket) {
2 // 计时器到设定时间点了,触发之前注册函数
3 f(arg, seq) // 之前注册的是 netpollDeadline
4}
5
6func netpollDeadline(arg interface{}, seq uintptr) {
7 netpolldeadlineimpl(arg.(*pollDesc), seq, true, true)
8}
9
10/src/runtime/netpoll.go: 428
11func netpolldeadlineimpl(pd *pollDesc, seq uintptr, read, write bool) {
12 //...
13 if read {
14 pd.rd = -1
15 rg = netpollunblock(pd, 'r', false)
16 }
17 //...
18}
这里会设置pd.rd=-1
,是指 poller descriptor.read deadline
,含义网络轮询器文件描述符的读超时时间, 我们知道在linux里万物皆文件,这里的文件其实是指这次网络通讯中使用到的socket。
这时候再回去看发生超时的条件就是if (mode == 'r' && pd.rd < 0)
。
至此。我们的代码里就收到了 io timeout
的报错。
http.Transport
中设置超时,那是连接的超时,不是请求的超时。否则可能会出现莫名 io timeout
报错。client
里设置。本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/UBA17eN3n51d0UU1zlbqNQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。