golang 标准输出文件和行定位_定位"too many open files"问题

"too many open files"意味着当前进程用完了所有的文件描述符。本文简要描述如何处理这种问题。

一切皆文件

在Linux系统中,一切皆文件。普通文件、目录、设备以及socket都是当作文件来对待。Linux在所有这些类型的文件系统上面封装了一层虚拟文件系统(VFS),从而为上层应用提供了统一的编程接口。这里推荐一篇讲得比较好的入门级别文章:

https://ops.tips/blog/what-is-slash-proc/

下面这个图片也是来自上面这篇文章。vfs_read会将上层的read请求翻译成相应的底层文件系统的请求。

eddcc317766fa29e587286adda72cb8c.png

现在的应用程序一般都是分布式运行,相互通过网络通信。所以一般遇到“too many open files”问题都是与socket相关。本文也主要是围绕socket来描述。

文件描述符泄漏or设计缺陷?

一旦发生"too many open files",一般有两种可能,要么是代码有BUG,导致socket泄漏,要么是有设计缺陷。

如果遇到突发流量,导致socket激增,但是当流量减弱或消失后,socket数量能逐渐回归到正常。那就说明没有socket泄漏。这时往往是设计问题,由于没有相应的熔断限流措施,以及没有设置合理的最大文件描述符数。

如果当流量减弱后,socket数量还是居高不下,那就说明有泄漏,就需要排查代码问题。

定位工具

一般来说,定位这类问题,有三种常用的方法或者工具。

/proc//fd

第一种方式是直接查看"/proc//fd"这个目录中的文件。其中是要查看的进程的ID,例如下面就是进程29716打开的所有文件,

d1cbae2ec86b85ff770a5fea020927bd.png

用下列命令就可以快速统计进程打开的文件描述符的总数(注意:要刨去第一行:)),

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中所包含的条目为准。

5b691795a9f5b5bf548443b7a778eb7b.png

netstat

netstat可以很方便的查看本机的网络连接情况。例如下面的截图就是执行如下命令的输出。如果要查看某个进程的连接信息,可以根据pid过滤。

netstat -nap | grep tcp

13d7e4087ac8d815a8bdddd48c040723.png

TCP状态机
分析socket问题,一定要会看TCP状态机,要清楚各个状态分别代表什么含义,以及相互之间如何转化。下图来自google搜索。

45093f0a8ecdf4d751f858068a5ba4d2.png

首先,一定要明白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解决方案

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值