记录线上一次故障,状态延迟,状态使用短连接,长轮训的方式获取,在每天的固定时间点,出现状态延迟,持续几分钟,然后又莫名其妙的恢复了,很是怪异,下面就来复盘下,这次问题的定位和思考。
我们可以掌握的线索有
1.固定的时间点,发生。
2.通过监控可以看到,流量并不高,但是TCP TIMEWAIT一瞬间疯涨
3.出问题的时间点,远程客户的电脑(全内网),ping网关和服务器,发现有大量延时
4.有同事通过jstat看,发现gc的次数很多,认为gc导致了接口延时。
5.接口超时的时间点,CPU不高,内存不高,I/O不高,系统负载不高,也就是未达到机器的性能瓶颈。机器配置16核64g
软件版本:
由于,这个问题牵涉的大客户,很多技术人员,投入进来一起攻破,每个人的想法不一致,导致问题更难以统一突破。
一部分人,认为是网络问题,和某为扯了半天,没有个所以然
一部分人,认为gc太频繁,full gc次数太多了,所以需要gc调优
一部分人,认为需要在测试环境,压测,测试出系统瓶颈
抛开以上的言论,我们回到线索上
1、每天固定时间出现,询问客户,这个时间段是否是业务高峰期。
回答:是
2、从监控来看,TCP-TIMEWAIT高,那我们就要先定位到系统部署方案。
系统部署方案如下
1.nginx反向代理java服务。
nginx监听443端口
java服务监听8080端口
每一次轮训,发起http请求,通过nginx,nginx请求java服务
查看8080端口的tcp端口timewait个数,发现全是timewait,现在可以确定的是nginx和tcp连接出现了大量的timewait,导致接口延时。
修改后 | 当前情况 | 说明 |
---|---|---|
net.core.netdev_max_backlog = 16384 | net.core.netdev_max_backlog = 1000 | 系统建立全链接队列长度(TCP三次握手成功后的连接放入队列) |
net.ipv4.tcp_max_syn_backlog = 8192 | net.ipv4.tcp_max_syn_backlog = 512 | 系统建立半链接队列长度(TCP三次握手过程中的连接放入队列) |
net.core.somaxconn = 4096 | net.core.somaxconn = 128 | 系统建立全链接队列(TCP三次握手成功后的连接放入队列 系统级别的 |
TCP 建立连接时要经过 3 次握手,在客户端向服务器发起连接时,对于服务器而言,一个完整的连接建立过程,服务器会经历 2 种 TCP 状态:SYN_REVD, ESTABELLISHED
对应也会维护两个队列:
一个存放 SYN 的队列(半连接队列)
一个存放已经完成连接的队列(全连接队列)
如果 backlog 大于内核参数 net.core.somaxconn,则以 net.core.somaxconn 为准,
即全连接队列长度 = min(backlog, 内核参数 net.core.somaxconn),net.core.somaxconn 默认为 128。
这个很好理解,net.core.somaxconn 定义了系统级别的全连接队列最大长度,
backlog 只是应用层传入的参数,不可能超过内核参数,所以 backlog 必须小于等于 net.core.somaxconn。
半连接队列长度由内核参数 tcp_max_syn_backlog 决定,
当使用 SYN Cookie 时(就是内核参数 net.ipv4.tcp_syncookies = 1),这个参数无效,
半连接队列的最大长度为 backlog、内核参数 net.core.somaxconn、内核参数 tcp_max_syn_backlog 的最小值。
即半连接队列长度 = min(backlog, 内核参数 net.core.somaxconn,内核参数 tcp_max_syn_backlog)。
这个公式实际上规定半连接队列长度不能超过全连接队列长度,但是tcp_syncooking默认是启用的,如果按上文的理解,那这个参数设置没有多大意义
其实,对于 Nginx/Tomcat 等这种 Web 服务器,都提供了 backlog 参数设置入口,当然它们都会有默认值,通常这个默认值都不会太大(包括内核默认的半连接队列和全连接队列长度)。如果应用并发访问非常高,只增大应用层 backlog 是没有意义的,因为可能内核参数关于连接队列设置的都很小,一定要综合应用层 backlog 和内核参数一起看,通过公式很容易调整出正确的设置
upstream node {
server 127.0.0.1:8080;
keepalive 10000; //新增
keepalive_timeout 65s; //新增
keepalive_requests 20000; //新增
}
1.长轮训 短连接改为websocket方案
评估:不确定因素太多,客户要限时解决问题,不敢上线
2.降低tcp 连接数,根据业务拆分,降低请求量。
3.控制timewait数量,保证业务高峰期不会发生太大的抖动
4.询问业务人员,请求会分配到一个线程的处理,并且没有限制线程数量,所以如果请求量很大,应该会有很多线程,并且cpu应该有很高的使用量,而现在cpu的使用量很低,不太正常。使用jstack打印线程情况
找到运维沟通,tcp的连接数设置高点。然而被告知最多只能创建65535个。这个问题我们稍后在细聊下。
控制TCP TIMEWAIT的数量,设置参数
net.ipv4.tcp_max_tw_buckets=1024
HashTable
一个数组,锁的代码逻辑,处理比较长。上面的截图中,tcp timewait的个数太多,到了65535限制,导致连接被重置,那为什么有这个结论呢
注意看上面的nginx配置
upstream node {
server 127.0.0.1:8080;
keepalive 10000; //新增
keepalive_timeout 65s; //新增
keepalive_requests 20000; //新增
}
这里指定了去连接127.0.0.1的8080端口,那么nginx去连接java服务的时候如下:
源ip | 源端口 | 目标ip | 目标端口 |
---|---|---|---|
127.0.0.1 | 20000 | 127.0.0.1 | 8080 |
127.0.0.1 | 20001 | 127.0.0.1 | 8080 |
127.0.0.1 | 20002 | 127.0.0.1 | 8080 |
所以为什么会说一个机器上最多65535个端口数。
这个端口号16位的,可以有0~65535端口数是针对单个ip来描述的,那我们在机器上可不是只有一个ip,所以理论上端口数是无上限的,配置nginx的时候这里需要注意上。
cat /proc/sys/net/ipv4/ip_local_port_range
1024 65000
每建立一个TCP连接,会分配一个文件描述符,linux 对可打开的文件描述符的数量分别作了三个方面的限制。
系统级:当前系统可打开的最大数量,通过 cat /proc/sys/fs/file-max 查看
用户级:指定用户可打开的最大数量,通过 cat /etc/security/limits.conf 查看
进程级:单个进程可打开的最大数量,通过 cat /proc/sys/fs/nr_open 查看
还记得上面说过,通过jstat看到gc很频繁,full gc次数很多,所以建议GC调优吗?
我们看关键指标
YGC 59085 Young GC次数
YGCT 318.142 Young 总时间 单位秒
FGC 407 Full GC 次数
FGCT 75.310 Full GC 总时间,单位为妙
我们看平均时间
年轻代平均gc时间
318.142/59085=0.0005
- 老年代平均gc时间
75.310/407=0.185
我们可以看到GC的次数很多,但是GC的平均时间并不高。
但是这并不是万能的
有一种情况比如Full GC发生了10次,平均值不高,但是某几次的Full GC时间为5~6秒,所以为了更确定这个问题,我们可以使用启动参数,查看每次GC的时间。
-verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:./gc.log
查看当天gc日志,Full gc时间并不高,可以排除,GC引起的问题。
所有的优化,GC调优应该是最后的手段,更多的是优化我们的代码。