Golang未关闭HTTP连接导致端口用尽

接上篇,面试被问处理过哪些故障的备选。此问题是由于 Golang 未关闭 HTTP 连接,产生大量 Close-Wait 状态用尽端口。

问题背景

故障程序是一个 Telegraf 插件,名为 url_monitor,修改自 http_response 插件,用于监控 URL 的状态码,响应内容及响应时间,本站之前也发过相关文章,见 基于Telegraf和InfluxDB的url监控方案

故障发生时,首先是收到大量 URL 报警,检查后发现是误报,立即查看 Telegraf 日志,发现大量 cannot assign requested address 日志。通过 Zabbix 查看 TCP 状态监控,发现 Close-Wait 状态持续增长,经过近一个月时间超过 6 万,导致端口用尽。

故障处理

首先立即暂停报警发送程序,同时通过微信群通知研发同事,忽略误报。之后重启 Telegraf ,释放被占用的端口。

接下来是分析代码,调用 resp.Body.Close() ,确保不堆积 Close-Wait 状态,见 commit/d230b

	// 当请求失败,resp为nil时,直接defer会导致panic,因此需要先判断
	if resp != nil {
        // 保证关闭连接. 不关闭连接将导致close-wait累积,最终占满端口。监控将报错:cannot assign requested address
		defer resp.Body.Close()
	}

原理分析

TCP四次挥手

  1. 客户端(主动端)发送 FIN 包通知服务端(被动端)断开连接,并进入 FIN-WAIT-1 状态
  2. 服务端(被动端)响应 ACK ,并进入 CLOSE-WAIT 状态
  3. 服务端(被动端)处理完成之后,发送 FIN+ACK ,并进入 LAST-ACK 状态
  4. 客户端(主动端)响应 ACK ,进入 TIME-WAIT 状态,然后等待 2MSL 之后关闭连接,服务端(被动端)收到 ACK 后关闭连接

示意图如下

TCP四次挥手
TCP四次挥手

HTTP请求,谁先关闭连接?

HTTP 1.0协议

  1. 如果响应头中有 content-length 头,则客户端在接收 body 时,可以依照这个长度来接收数据,接收完后,就表示这个请求完成了。
    –>服务端和客户端在明确自己数据处理完成后,都可以主动断开连接
  2. 如果没有 content-length 头,则客户端会一直接收数据,直到服务端主动断开连接,才表示 body 接收完了。
    –>客户端必须等待服务端断开连接后,才能断开自己的连接。

HTTP 1.1协议

  1. 如果响应头中的 Transfer-encodingchunked 传输,则表示 body 是流式输出,body 会被分成多个块,每块的开始会标识出当前块的长度,此时,body 不需要通过长度来指定。
    –>服务端和客户端在明确自己数据处理完成后,都可以主动断开连接
  2. 如果响应头中的 Transfer-encoding 为非 chunked 传输,但有 content-length,则按照 content-length 来接收数据。
    –>服务端和客户端在明确自己数据处理完成后,都可以主动断开连接
  3. 如果响应头中的 Transfer-encoding 为非 chunked 传输,但没有 content-length,则客户端接收数据,直到服务端主动断开连接。
    –>客户端必须等待服务端断开连接后,才能断开自己的连接。

resp.Body.Close()

If Response.Body won't be closed with Close() method than a resources associated with a fd won't be freed. This is a resource leak.

Closing Response.Body

From response source:

It is the caller's responsibility to close Body.

So there is no finalizers bound to the object and it must be closed explicitly.

Error handling and deferred cleanups

On error, any Response can be ignored. A non-nil Response with a non-nil error only occurs when CheckRedirect fails, and even then the returned Response.Body is already closed.

resp, err := http.Get("http://example.com/")
if err != nil {
    // Handle error if error is non-nil
}
defer resp.Body.Close() // Close body only if response non-nil

参考连接

1. https://blog.csdn.net/weixin_39366864/article/details/104552012
2. https://stackoverflow.com/a/41512208

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注