"too many open files"意味着当前进程用完了所有的文件描述符。本文简要描述如何处理这种问题。
一切皆文件
在Linux系统中,一切皆文件。普通文件、目录、设备以及socket都是当作文件来对待。Linux在所有这些类型的文件系统上面封装了一层虚拟文件系统(VFS),从而为上层应用提供了统一的编程接口。这里推荐一篇讲得比较好的入门级别文章:
https://ops.tips/blog/what-is-slash-proc/
下面这个图片也是来自上面这篇文章。vfs_read会将上层的read请求翻译成相应的底层文件系统的请求。
现在的应用程序一般都是分布式运行,相互通过网络通信。所以一般遇到“too many open files”问题都是与socket相关。本文也主要是围绕socket来描述。
文件描述符泄漏or设计缺陷?
一旦发生"too many open files",一般有两种可能,要么是代码有BUG,导致socket泄漏,要么是有设计缺陷。
如果遇到突发流量,导致socket激增,但是当流量减弱或消失后,socket数量能逐渐回归到正常。那就说明没有socket泄漏。这时往往是设计问题,由于没有相应的熔断限流措施,以及没有设置合理的最大文件描述符数。
如果当流量减弱后,socket数量还是居高不下,那就说明有泄漏,就需要排查代码问题。
定位工具
一般来说,定位这类问题,有三种常用的方法或者工具。
/proc//fd
第一种方式是直接查看"/proc//fd"这个目录中的文件。其中是要查看的进程的ID,例如下面就是进程29716打开的所有文件,
用下列命令就可以快速统计进程打开的文件描述符的总数(注意:要刨去第一行:)),
ls -lrt /proc/29716/fd | wc -l
如果是进程在代码中查看自己占用的文件描述符时,则可以直接访问/proc/self/fd这个目录,因为/proc/self其实就是指向/proc/的一个链接。Golang代码如下,
fds, err := ioutil.ReadDir("/proc/self/fd")
lsof
使用"lsof -p "也可以查询某个进程占用的所有文件描述符。例如下面就是查询进程29716打开的所有文件描述符。lsof显示的信息比/proc//fd更全一点,而且条目稍多,因为它将进程所在的目录以及加载的动态库也算在内。但是最终系统报"too many open files"时,是以/proc//fd中所包含的条目为准。
netstat
netstat可以很方便的查看本机的网络连接情况。例如下面的截图就是执行如下命令的输出。如果要查看某个进程的连接信息,可以根据pid过滤。
netstat -nap | grep tcp
TCP状态机
分析socket问题,一定要会看TCP状态机,要清楚各个状态分别代表什么含义,以及相互之间如何转化。下图来自google搜索。
首先,一定要明白TCP是双工的,当一方关闭连接,只表示它没有数据要发送了,但是它还可以接受数据。只有双方都关闭了连接,才算最终关闭了连接。
在上面众多的状态中,CLOSE_WAIT这个状态需要重点关注。其含义是对方已经关闭了TCP连接,但自己这方还没有关闭连接。如果不是故意这么设计或实现,往往意味着代码中有BUG,没有及时关闭连接。如果由于处于CLOSE_WAIT状态的socket过多,导致“too many open files”,那八成就是代码中在需要调用close关闭连接的地方遗漏了。
另外,TIME_WAIT是一种正常的状态。当双方都关闭了连接,主动关闭的一方最后会进入TIME_WAIT状态,等待2MSL的时间之后,就会自动变成CLOSED状态。其目的是确保对方能收到最后一个ACK包,以及等待网络中延迟的各种包消失。
如果看到很多SYNC_WAIT,那说明对方网络不通,和对方的连接无法建立。
getrlimit & setrlimit
如果需要,可以设置更大的文件描述符数量。可以通过ulimit命令,或者系统调用setrlimit来设置。ulimit是一个shell命令,它设置shell以及通过shell启动的进程的资源限制,它最后也是调用setrlimit。这里推荐一篇很简单实用的文章:
https://www.robustperception.io/dealing-with-too-many-open-files
获取或设置当前进程的资源限制,可以使用系统调用getrlimit和setrlimit,
#include #include int getrlimit(int resource, struct rlimit *rlim);int setrlimit(int resource, const struct rlimit *rlim);
Golang中对应的系统调用定义在syscall包中,不过从go1.4之后,应用使用golang.org/x/sys中的包。
func Getrlimit(resource int, rlim *Rlimit) (err error)func Setrlimit(resource int, rlim *Rlimit) (err error)
这里一定要分清soft limit和hard limit。首先, soft limit肯定小于或等于hard limit。对于某个进程来说,起作用的是soft limit。如果不是特权用户,只能调整soft limit,最大可以调成与hard limit一样大。对于特权用户,可以同时修改soft limti和hard limit,但同样soft limit不能超过hard limit。例如Golang中,Getrlimit返回的结构体中,Cur对应的是soft limit,而Max则是hard limit。
type Rlimit struct { Cur uint64 Max uint64}
一般来说,在linux系统中,soft limit和hard limit默认分别是1024和4096。
etcd的做法
etcd有一个goroutine来监控文件描述符的使用情况,代码如下,
// https://github.com/etcd-io/etcd/blob/master/etcdserver/metrics.go#L201func monitorFileDescriptor(lg *zap.Logger, done ticker := time.NewTicker(10 * time.Minute) defer ticker.Stop() for { used, err := runtime.FDUsage() if err != nil { lg.Warn("failed to get file descriptor usage", zap.Error(err)) return } fdUsed.Set(float64(used)) limit, err := runtime.FDLimit() if err != nil { lg.Warn("failed to get file descriptor limit", zap.Error(err)) return } fdLimit.Set(float64(limit)) if used >= limit/5*4 { lg.Warn("80% of file descriptors are used", zap.Uint64("used", used), zap.Uint64("limit", limit)) } select { case C: case done: return } }}
从上面的代码中,不难看出,通过两个metrics (fdUsed和fdLimit) 与普罗米修斯(prometheus)做了集成。
上面代码中的,FDLimit和FDUsage的实现如下。其中FDUsage就是通过目录"/proc/self/fd"来统计当前进程打开的文件描述符总数的。而FDLimit是利用了系统调用Getrlimit。
func FDLimit() (uint64, error) { var rlimit syscall.Rlimit if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlimit); err != nil { return 0, err } return rlimit.Cur, nil}func FDUsage() (uint64, error) { return countFiles("/proc/self/fd")}
正确使用Golang中Transport
在Golang中,客户端通过http访问服务器时,会使用到http.Transport。这里要注意一点,Transport维护了一个连接池。所以在实际使用中,不要为每一个HTTP请求创建一个新的Transport。正确的做法是只创建一个http.Client和http.Transport,以后每个http请求都使用同一个client。具体参考下面这个issue,
https://github.com/golang/go/issues/24719
--END--
相关文章
Prometheus + Grafana构建云时代的monitoring解决方案