线上502,居然是这个原因!

现象#

article-go 微服务有请求 502,之前稀稀拉拉有少量的 502(有同学排查过,未果),但是运维同学最近升级了 ingress,502 变多了。

image-20211013173634404

信息收集#

大量的 TW#

登录 article-go 容器,使用 lsof -p 1 查看进程的 fd 信息,发现大量的 TIME_WAIT。查询了下目标地址,都是 ingress pod 的地址。

image-20211013173654236

image-20211013173713876

首先 TIME_WAIT 就非常可疑,先看下 TCP 的四次挥手过程:

image-20211013173731270

发现只有客户端的连接状态才可能是 TIME_WAIT,我们 article_go 明明是服务端,怎么会有 TIME_WAIT 状态呢(这个是(我)一个认知错误,后面会提到)?

抓包#

随便找了一次接口请求,发现是正常的,先 3 次握手建立连接,然后 request、response 都很正常。

image-20211013173749578

想到 http 1.1 里面服务端 response 的时候是可以加一个 connection: closeheader 头来主动断开链接的,看下请求 header 信息:

image-20211013173802626

发现正常,服务端没有设置 header 头主动断开链接。

那是咋回事?脱发中。。。

查疑解惑#

第一个疑惑#

为什么服务端会有 TIME_WAIT 状态?

这其实是我的一个认知错误(也是下面这个图有歧义的地方,不知道你们有没有,如果有,请点赞👍)。

image-20211013173820965

先说结论:

  • 一个链接,并不是只有客户端可以主动断开链接,服务端也可以
  • 主动断开链接的一方,主动发起断开链接请求(FIN),就是对应上图左边这块(最上面写的是客户端,就是这个地方有歧义,其实服务端也可以)

本地实验下:

客户端在建立连接之后,等待 5 秒之后关闭连接,服务端在接受到请求之后,立即调用 close 方法关闭链接。以此来模拟服务端主动关闭链接的场景。

image-20211013173837507

抓包看下:

在 5 秒内,server 发布 fin,并且已经得到了 client 的 ACK

  • 服务端进入 FIN_WAIT_2

  • 客户端因为还没有 CLOSE,状态是 CLOSE_WAIT

image-20211013173852356

image-20211013173906151

5 秒后,client close,服务端状态变成 TIME_WAIT,客户端完全释放

image-20211013173917672

image-20211013173928399

小结:

服务端主动断开链接确实会有 TIME_WAIT 状态,和线上现象一致。所以问题排查方向就改成为什么服务端会断开链接?

第二个疑惑#

为什么服务端要主动断开链接?

这个问题问得好,看代码没有可疑的地方,一时间不知道如何排查了(此处掉了 2 根头发)。

但是有一个现象可疑是 —— 只有 article-go 服务会 502,api-go 服务不会。

我本地分别启动了 api-go 服务和 article-go 服务,分别请求并抓包:

  • Article-go response 之后有一次 TCP Keep-Alive,然后就 close 链接了

image-20211013173946654

  • Api-go response 之后有很多次 TCP Keep-Alive

image-20211013173959601

发现不同,就对比下代码(因为是服务端主动断开链接,所以重点关注的是 http server 启动的配置):

Article_go 的:

s := &http.Server{
   Addr:              addr,
   Handler:           engine,
   ReadTimeout:       2 * time.Second,
   MaxHeaderBytes:    1 << 20,
}

fmt.Println("Server listen on", addr)
err := s.ListenAndServe()

Api-go 的:

s := &http.Server{
   Addr:        ":" + port,
   Handler:     router,
   ReadTimeout: timeout * time.Second,
   // WriteTimeout:   timeout * time.Second,
   MaxHeaderBytes: 1 << 20,
}

// graceful restart
err := gracehttp.Serve(s)

唯一的区别是 ReadTimeout 参数设置的不太一样,api-go 设置的是 10s,article-go 是 2s。三板斧第四招(😊),改下 ReadTimeout 参数试试,结果行了,请求之后会有多次 TCP Keep-Alive 了。

而且我发现(书读少了),ReadTimeout 设置成几秒,TCP Keep-Alive 就会有几次(每间隔 1 秒,一次 TCP Keep-Alive)。

网上找了一个图,描述了 ReadTimeout 的计算范围。但是不太可能啊,这个过程不太可能会超过 2s。

image-20211013174020559

然后无意间看到这段段代码,原来 go http server 在没有设置 IdleTimeout 的情况下,会使用 ReadTimeout 作为 IdleTimeout

// net/conn/server.go
func (s *Server) idleTimeout() time.Duration {
   if s.IdleTimeout != 0 {
      return s.IdleTimeout
   }
   return s.ReadTimeout
}

终于,我,停。止. 了。脱. 发。。。

总结#

问题总结

  • 502 现象是因为服务端主动关闭了链接,(正常情况下应该由客户端主动断开链接)
  • 服务端主动断开链接,是因为设置了 ReadTimeout,而 net 包把这个参数当成了 IdleTimeout
  • 一个链接在 IdleTimeout 时间内,就会一直 Keep-Alive,超过这个时间,就会主动断开链接。另外一端如果没有没有及时释放,就会有问题。

改进代码

  • 单独设置 IdleTimeout,ingress 设置的时间是 300s,服务端设置 IdleTimeout 超过 300s 就行了。
  • 个人建议可以不用特别指定 ReadTimeout、WriteTimeout 和 IdleTimeout 的这些参数,默认值就是不限,然后让 ingress 来完全掌握链接的建立和关闭。安全问题,也是由 ingress 来控制

这次问题发现了自己很多认知错误

  • 认知问题一:502 现象多数是客户端和服务端设置的超时时间不一致导致的

认知不准确,超时时间是指什么?read 超时?write 超时? 其实链接的问题,主要是 idle 链接的问题。再直白点,就是 idle 链接的超时时间设置(对应 nginx 就是 keepalive_timeout 参数,对应 go http 就是 idleTimeout 参数)。此外服务端的 WriteTimeout 也会有影响。

  • 认知问题二:链接断开一般都是客户端发起断开链接请求

一般情况下是这样,但是服务端也是可以主动断开链接的,重新学了下 TCP4 次挥手过程,有了深刻认识。

  • 认知问题三:Http 想要长链接需要再 header 里面加个 connection: keep-alive

认知不准确,再 http 1.0 里面是需要显示的增加这个 header 头,但是在 http 1.1 里面这个 header 是默认值,可以不用加。

另外 Connection: close 这个 header 头在 http 1.0 里面是默认值(默认是短链接),在 http 1.1 里面需要手动增加该 header 头(通常用于服务端发布过程中,老服务响应时让客户端断开链接,然后和新服务建立连接)。

developer.mozilla.org/zh-CN/docs/W...

如果再有 502 的问题,应该如何快速定位?

  • 客户端和服务端的 idleTimeout 参数的设置是否匹配,原则是:客户端的 idletimeout 要比服务端设置的小。
  • 直接看服务端的 writeTimeout 设置是否合理。

事后发现了一篇文章总结的非常到位,推荐所有人都看下。

本作品采用《CC 协议》,转载必须注明作者和本文链接
您的点赞、评论和关注,是我创作的不懈动力。 学无止境,让我们一起加油,在技术的胡同里越走越深!
本帖由系统于 3年前 自动加精