上一篇文章说到还是产生 accpet open too many files的错误。
这个一般是通过修改ulimit就可以了,但是修改这个参数有一个误区,就是生效的时机。虽然你登录终端看ulimit -a看到open files连接数是够大了,但是对于app运行的环境并不一定是这个数目。具体可以通过cat /proc/{app自己的pid}/limits来确认真实的limits。
对于当时情况是:ulimit 修改后没有对用户进程产生作用。经过不停排除发现进程是由supervisor来启动,先找到supervisor,修改superv里面的fd值,发现还是不能生效。发现supervisord又是由 systemctrl启动,在systemtrl的配置文件里面修改了最大文件数,最后才生效。
通过修改好limit open fils这一项,后续再也没有出现open too many files的错误了。
当时在看出现open too many files的时候,发现netstat -apnt 查看,发现有很多close_wait。这个是如何产生的?
首先我们理解accept: open too many files中的open too many files是句柄数目的限制。那么accept是什么?
tcp连接中,分为两种连接,半连接和全连接。全连接的也就是 ESTABLISHED 建立的连接,也就是accept操作建立的连接。accpet队列就是全连接队列。这个队列的大小由两个参数的最小值决定,一个是系统级别,另一个是独立应用级别的。
-
全连接队列的大小:min(backlog, /proc/sys/net/core/somaxconn),意思是取backlog 与 somaxconn 两值的最小值,
net.core.somaxconn
定义了系统级别的全连接队列最大长度,而 backlog 只是应用层传入的参数,所以 backlog 值尽量小于net.core.somaxconn
; -
net.core.somaxconn
(内核态参数,系统中每一个端口最大的监听队列的长度); -
net.core.netdev_max_backlog
(每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目); -
ServerSocket(int port, int backlog) 代码中的backlog参数;
-
文件句柄;
-
net.ipv4.tcp_abort_on_overflow
= 0,此值为 0 表示握手到第三步时全连接队列满时则扔掉 client 发过来的 ACK,此值为 1 则说明握手到第三步时全连接队列满时则返回 reset 给客户端。
连接的数量有一个地方需要澄清,客户端进入了连接状态,但是服务端不一定进入了连接状态,根据状态转移图:有一个步骤的时间差,就是收到客户端的ack,后服务端才认为自己正式建立了连接。所以连接数,在客户端与服务端之间有一个时间差。
实际中,发现服务端建立连接,但是由于服务端各种原因,自己可以让服务睡眠时间久点。accept客户端的连接请求慢点,导致最终accept队列满了,一般阈值是(128+1)个连接。然后客户端超时,请求关闭还没有进入到全连接队列的连接,SYN_RECV 状态持续一段时间后会消失。服务端对于客户端的关闭请求回复了ack,进入了close_wait, 但是服务端的连接还在被accept阻塞,所以会一直处于close_wait的状态中不释放。 最终要把close_wait释放的手段,一种程序内部调用close关闭连接(需要改代码),一种是被kill掉。通过tcpdump抓包,发现这种是服务端主动发起RST报文,然后关闭了连接。 所以线上出现了close_wait问题,那么很有可能就是程序被阻塞,或者是某种情况没有close掉连接 注意,该代码没有给连接主动close掉。
package main import ( "log" "net" "time" ) func main() { l, err := net.Listen("tcp", ":8889") if err != nil { log.Println("error listen:", err) return } defer l.Close() log.Println("listen ok") var i int for { time.Sleep(time.Second * 100000) log.Printf("%d: accept a new connection\n", i) if _, err := l.Accept(); err != nil { log.Println("accept error:", err) break } i++ log.Printf("%d: accept a new connection\n", i) } }
netstat -antp 查看
cp 0 0 192.168.0.105:8889 192.168.0.100:40088 SYN_RECV - tcp 0 0 192.168.0.105:8889 192.168.0.100:40100 SYN_RECV - tcp 0 0 192.168.0.105:8889 192.168.0.100:40102 SYN_RECV - tcp 0 0 192.168.0.105:8889 192.168.0.100:40090 SYN_RECV - tcp 0 0 192.168.0.105:8889 192.168.0.100:40098 SYN_RECV - tcp 0 0 192.168.0.105:8889 192.168.0.100:40092 SYN_RECV - tcp 0 0 192.168.0.105:8889 192.168.0.100:40096 SYN_RECV - tcp 0 0 192.168.0.105:8889 192.168.0.100:40094 SYN_RECV - tcp 0 0 192.168.0.105:8889 192.168.0.100:40086 SYN_RECV - tcp6 129 0 :::8889 :::* LISTEN 37482/server tcp6 0 0 192.168.0.105:8889 192.168.0.100:40034 ESTABLISHED - tcp6 0 0 192.168.0.105:8889 192.168.0.100:40014 ESTABLISHED - tcp6 0 0 192.168.0.105:8889 192.168.0.100:39844 ESTABLISHED - tcp6 0 0 192.168.0.105:8889 192.168.0.100:40002 ESTABLISHED - tcp6 0 0 192.168.0.105:8889 192.168.0.100:40040 ESTABLISHED - tcp6 0 0 192.168.0.105:8889 192.168.0.100:39942 ESTABLISHED - tcp6 0 0 192.168.0.105:8889 192.168.0.100:40022 ESTABLISHED - tcp6 0 0 192.168.0.105:8889 192.168.0.100:39992 ESTABLISHED - ...... [jet@192 ~]$ sudo netstat -lpnt | grep 8889 tcp6 129 0 :::8889 :::* LISTEN 37482/server 客户端: [jet@192 ~]$ sudo netstat -antp | grep client | wc -l 129 服务端: jet@192 ~]$ sudo sysctl -a | grep conn net.core.somaxconn = 128 kill 掉客户端请求后,就发现服务端的状态进入了close_wait,因为kill掉客户端后,客户端会发出关闭连接请求, SYN_RECV 状态请求在半连接队列里面,后面会消失: [jet@192 ~]$ sudo netstat -antp | grep client [jet@192 ~]$ 服务端 [jet@192 ~]$ sudo netstat -anpt | grep 8889 | grep CLOSE_WAIT | wc -l 129