上一篇文章说到还是产生 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
", i)
if _, err := l.Accept(); err != nil {
log.Println("accept error:", err)
break
}
i++
log.Printf("%d: accept a new connection
", 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