最近在使用 Go 语言编写一些应用的时候,遇到了一个比较棘手的坑。情况大概就是当应用在运行几个小时之后,API 访问就会出错了。打开控制条调试器查看一下返回的内容,提示的是 "too many open file"。相信很多开发网络程序的同学应该对这个错误都很熟悉了,一个比较正常的想法就是看一下应用的 FD 上限是多少,然后使用了多少了,不知道其他同学是不是这样,反正我是这样的:

$ ulimit -n
1024
$ lsof -p 2211 | wc -l
1032

我擦,这里似乎数量比我期望中的多啊,而且为啥 limit 是 1024 而实际是 1032?OK,先说这个问题的原因,在我百般调试无果之后我只能求助于搜索引擎了。感谢一家国内不存在的公司,让我发现了解决这个问题的方法,原来罪魁祸首居然是 Go 语言自带的 Http Client,这篇说明的文章是:Go HTTP: Too Many Open Files。首先,我的应用里面是有 HTTP 的 Client 的,用于调用其他系统的 REST 接口,但是没想到问题居然出自随用随取的 HTTP。

根据文章的解决,Go 语言的 HTTP 默认使用的是长连接,也就是说当请求完成之后,TCP 连接还会继续保留,直到一段时间之后,这篇文章没有介绍这个时间是多久,但是经过查看其它文档和软件的说明,这个时间大概是 3 分钟左右。这其实也就说,如果我们不特别得对 Go 语言的 HTTP Client 进行设置连接参数的话,一个 HTTP 的 TCP 连接可能会在完成通讯之后 3 分钟内才关闭。这也就是说如果期间你不重用连接,而是每个请求都是独立的连接的话,只要你的连接请求数稍微频繁一些,果断会崩掉。

这么算一下,总共是 1024,3 分钟就是 180 秒,1024 / 180 ≈ 6 qps,这个应该是很容易达到的条件。OK,那么是如何解决的呢,我们来看下代码,原始的代码可能类似于这样:

这段代码就是说出现 Too Many Open Files 问题的代码,所以要解决这个问题,我们需要在第 7 行后面再加一行设置:

这里的第 8 行就是添加的内容,加上这个内容之后就会在数据传输完成之后关闭连接,释放 TCP 的连接了。但是,这里要强调的是: 一定要确定你使用的是短连接,而不是长连接时加这个参数

题外话

OK,这个问题是解决了,但是在定位这个问题的过程中我发现了一些有趣的事情,顺便和大家分享一下。

1. 为什么 ulimit -nlsof -p pid 的数值不一样

这个我现在解答不了,我觉得一个可能性是有些 FD 是不计数的?

2. lsof 怎么用?

在查看进程占用 FD 的时候,我使用过几种方式,我现在记得的有这三种:

这三种方式的输出是不一样的。后面我发现其实第一种是错误的,因为过滤中有一些影响因素,导致结果和下面两个相差非常非常多,那么下面两个哪个是正确的呢?我现在也给不出答案,不过我认为应该第三个是比较正确的。看问题1,我发现第三条命令出来的结果是 1024,而第二条出来的命令是 1032,这两个数值相差了 8,留个坑吧,下次来填。

3. Go 语言应用如何定位 FD/Goroutine 泄露

其实 Go 语言提供了很多有意思的工具,最近我发现有一个 Prometheus 的 Exporter 可以用于展示应用内开了多少个 Goroutine,以及是哪里开出来的,值得大家去尝试一波:prometheus/client_golang

Reference

  1. golang使用pprof检查goroutine泄露
  2. Go HTTP: Too Many Open Files