记一次nginx与linux内核性能调优的过程

因为监管对接的需要,用俩段nginx,一个装在centos7.6上,一个装在windowServer2008R2上来做转发。流量图如下:

因业务量陡然增大,转发服务出现很多502错误,同时nginx出现大量error:

nginx 10054 An existing connection was forcibly closed by the remote host

同时在阿里云上能看到流量和TCP连接数都有一个陡然的上升,出现大量TIME_WAIT,所以在老大带领下一起进行了排查。

排查过程:

一、按照百度/google工程师的做法,首先找nginx报这个1054这个error的解决方法,

当使用nginx作为反向代理时,为了支持长连接,需要做到两点:
从client到nginx的连接是长连接
从nginx到server的连接是长连接

保持和client的长连接:

http {

    keepalive_timeout 120s 120s;

    keepalive_requests 1000;

  }

keepalive_timeout

#语法
keepalive_timeout timeout [header_timeout];

第一个参数:设置keep-alive客户端(浏览器)连接在服务器端(nginx端)保持开启的超时值(默认75s);值为0会禁用keep-alive客户端连接;
第二个参数:可选、在响应的header域中设置一个值“Keep-Alive: timeout=time”;通常可以不用设置;
这个keepalive_timeout针对的是浏览器和nginx建立的一个tcp通道,没有数据传输时最长等待该时候后就关闭.
nginx和upstream中的keepalive_timeout则受到tomcat连接器的控制,tomcat中也有一个类似的keepalive_timeout参数

keepalive_requests
keepalive_requests指令用于设置一个keep-alive连接上可以服务的请求的最大数量,当最大请求数量达到时,连接被关闭。默认是100。

这个参数的真实含义,是指一个keep alive建立之后,nginx就会为这个连接设置一个计数器,记录这个keep alive的长连接上已经接收并处理的客户端请求的数量。如果达到这个参数设置的最大值时,则nginx会强行关闭这个长连接,逼迫客户端不得不重新建立新的长连接。

保持和server的长连接
为了让nginx和后端server(nginx称为upstream)之间保持长连接,典型设置如下:(默认nginx访问后端都是用的短连接(HTTP1.0),一个请求来了,Nginx 新开一个端口和后端建立连接,后端执行完毕后主动关闭该链接)

location / {
    proxy_http_version 1.1;
    proxy_set_header Connection keep-alive;
    proxy_pass http://httpurl;
}

HTTP协议中对长连接的支持是从1.1版本之后才有的,因此最好通过proxy_http_version指令设置为”1.1”;

从client过来的http header,因为即使是client和nginx之间是短连接,nginx和upstream之间也是可以开启长连接的。

keepalive
此处keepalive的含义不是开启、关闭长连接的开关;也不是用来设置超时的timeout;更不是设置长连接池最大连接数。官方解释:
    1.The connections parameter sets the maximum number of idle keepalive connections to upstream servers connections(设置到upstream服务器的空闲keepalive连接的最大数量)
    2.When this number is exceeded, the least recently used connections are closed. (当这个数量被突破时,最近使用最少的连接将被关闭)
    3.It should be particularly noted that the keepalive directive does not limit the total number of connections to upstream servers that an nginx worker process can open.(特别提醒:keepalive指令不会限制一个nginx worker进程到upstream服务器连接的总数量)

总结:
keepalive 这个参数一定要小心设置,尤其对于QPS比较高的场景,推荐先做一下估算,根据QPS和平均响应时间大体能计算出需要的长连接的数量。比如前面10000 QPS和100毫秒响应时间就可以推算出需要的长连接数量大概是1000. 然后将keepalive设置为这个长连接数量的10%到30%。比较懒的同学,可以直接设置为keepalive=1000之类的,一般都OK的了。

3、综上,出现大量TIME_WAIT的情况
  1)导致 nginx端出现大量TIME_WAIT的情况有两种:

  • keepalive_requests设置比较小,高并发下超过此值后nginx会强制关闭和客户端保持的keepalive长连接;(主动关闭连接后导致nginx出现TIME_WAIT)
  • keepalive设置的比较小(空闲数太小),导致高并发下nginx会频繁出现连接数震荡(超过该值会关闭连接),不停的关闭、开启和后端server保持的keepalive长连接;

  2)导致后端server端出现大量TIME_WAIT的情况:
nginx没有打开和后端的长连接,即:没有设置proxy_http_version 1.1;和proxy_set_header Connection “”;从而导致后端server每次关闭连接,高并发下就会出现server端出现大量TIME_WAIT

参考: https://www.cnblogs.com/grimm/p/13038577.html

nginx for windows: http://nginx-win.ecsds.eu/

nginx for windows documentation:http://nginx-win.ecsds.eu/download/documentation-pdf/nginx%20for%20Windows%20-%20documentation%201.8.pdf

所有文档下载:http://nginx-win.ecsds.eu/download/documentation-pdf/

二、在系统层面查找出现大量TIME_WAIT的原因

1.Windows系统的ECS实例Time_Wait连接

问题原因

默认Windows Server 2008版本以后,动态端口的数量为16384个(从49152起始,到65536结束),如果服务器对外有大量连接,而根据TCP默认的Time Wait Delay时间为4分钟,这会导致大量连接在断开后处于Time_Wait状态,无法快速释放给其它连接使用,会导致端口耗尽。

解决方案
1).通过管理终端登录Windows系统的ECS实例中,在CMD界面执行如下命令,查看当前动态端口配置。
   netsh interface ipv4 show tcpstats
   netsh int ipv4 show dynamicport tcp
2).执行如下命令,增大动态端口数量,无需重启即可生效。
  netsh int ipv4 set dynamicport tcp start=15000 num=50000
3).如果以上配置不能完全解决,可以通过修改注册表降低Time Wait时间,最低为30秒。打开注册表,定位到HKLMSYSTEMCurrentControlSetServicesTcpipParameters,新增键值TcpTimedWaitDelay,类型REG_DWORD , 设置为十进制30。

参考: https://help.aliyun.com/document_detail/40701.html

2.Linux系统的 Time_Wait连接

TCP连接 的建立与终止 (1)三次握手,四次挥手

 1 三次握手,建立连接协议:

(1)请求端(client) 发送一个 SYN端 指明 1)客户打算连接的服务器的端口 2)初始序号 ISN
(2)接收端(server) 发回包含服务器的初始序号的 SYN报文段(含 ACK) 进行确认,同时 将确认序号(ACK字段 置为1时 有效)设置为 客户的初始序号ISN +1 以对客户的 SYN报文段 进行确认。
(3)客户 必须发送 **将确认序号 设置为 服务器的 ISN+1 **的 SYN报文段 进行确认(ACK)。

(1)client ---Hello, can I speak to you? & client-ISN:J---> server

(2)client <---ACK: Yes, I receive your request! & server-ISN:K ACK:J+1--- server

(3)client ---ACK: Okey, thank you for your permission! Let's start! & ACK:K+1 ---> server

2.四次挥手,连接终止协议

连接终止 需要 四次挥手,而这是由 TCP的半关闭 导致的。TCP连接 是双全工的:通信允许两个方向进行传输。
由于 TCP连接 是 双全工 的,因此 每个方向都必须 单独的进行关闭。

第一次挥手:(1)首先进行发送的一方(一般是 client,但是 也可以是 server) 发送第一个 FIN 将执行 主动关闭,发送端的应用层关闭之后,才发送的 第一个FIN。 此时,发送端不能够再发送数据了,但是仍然可以接收数据。(附:这对 利用半关闭的应用 来说是可能的,但是很少的TCP应用程序这样做。)

第二次挥手:(2)接收到 第一个 FIN 的另外一方(一般是 server),返回一个 FIN的ACK 给发送端,确认序号 为 收到的序号+1。和 SYN 一样,FIN 也将占用一个序号。

(3)接着 接收端-server 向应用程序交送EOF(end of file),代表从 发送端 接收数据的结束。

第三次挥手:(4)接收端-server 关闭它的进程(关闭应用层),并发送 FIN 给 发送端-client。

第四次挥手:(5)发送端-client 接收到这个 FIN,并发送一个 FIN的ACK 给 接收端-server,确认序号 设置为 收到的序号+1。

注意:

确认序号 只有在 ACK字段为1 的情况下才有效,也就是说,发送请求的 是 序号,返回的ACK 是 确认序号(值为 序号+1)。

(1)client ---FIN: Hey,man. I finished my application, I will not send any data to you! Bye! & 序号 M ---> server

(2)client <---ACK: OKey, I will not receive your data. & 确认序号 M+1 --- server

(3)server ---EOF---> server-application

(4)client <---FIN: Goodbye, I will not send any data to you, too! & 序号 N --- server

(5)client ---ACK: Okey, bye! & 确认序号 N+1 ---> server

 

总的过程:

 

CLOSED: 表示初始状态。

LISTEN: 表示服务器端的某个SOCKET处于监听状态,可以接受连接了。

SYN_RCVD: 这个状态表示接受到了SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂,基本上用netstat你是很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次TCP握手过程中最后一个ACK报文不予发送。因此这种状态时,当收到客户端的ACK报文后,它会进入到ESTABLISHED状态。

SYN_SENT: 这个状态与SYN_RCVD遥想呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,因此也随即它会进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。

ESTABLISHED:这个容易理解了,表示连接已经建立了。

FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。

FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你,稍后再关闭连接。

TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。

注:MSL(最大分段生存期)指明TCP报文在Internet上最长生存时间,每个具体的TCP实现都必须选择一个确定的MSL值.RFC 1122建议是2分钟,但BSD传统实现采用了30秒.TIME_WAIT 状态最大保持时间是2 * MSL,也就是1-4分钟.

CLOSING: 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。

CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。

LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。

最后有2个问题的回答,我自己分析后的结论(不一定保证100%正确)

1、 为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?

这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

2、 为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?

这是因为:虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文,并保证于此。

 

查看当前系统下所有连接状态的数:

# netstat -ano|awk -F " " '{print $6}'|sort |uniq -c |sort -r -n
   2074 TIME_WAIT
     52 CONNECTED
     40 FIN_WAIT1
     21 ESTABLISHED
     16 STREAM
      3 off
      2 LISTEN
      1 SEQPACKET

如发现系统存在大量TIME_WAIT状态的连接,通过调整内核参数解决:

编辑文件/etc/sysctl.conf,加入以下内容:

net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_fin_timeout = 30 表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间。
net.ipv4.tcp_keepalive_time = 1200 表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为20分钟。
net.ipv4.ip_local_port_range = 1024 65000 表示用于向外连接的端口范围。缺省情况下很小:32768到61000,改为1024到65000。
net.ipv4.tcp_max_syn_backlog = 8192 表示SYN队列的长度,默认为1024,加大队列长度为8192,可以容纳更多等待连接的网络连接数。
net.ipv4.tcp_max_tw_buckets = 5000 表示系统同时保持TIME_WAIT套接字的最大数量,如果超过这个数字,TIME_WAIT套接字将立刻被清除并打印警告信息。
默 认为180000,改为5000。对于Apache、Nginx等服务器,上几行的参数可以很好地减少TIME_WAIT套接字数量,但是对于Squid,效果却不大。此项参数可以控制TIME_WAIT套接字的最大数量,避免Squid服务器被大量的TIME_WAIT套接字拖死。

参考:

https://www.cnblogs.com/qq952693358/p/5759129.html

https://www.jianshu.com/p/bd8fad4cf478

三、阿里云 Linux 内核优化实战

1、sysctl.conf优化

Linux系统内核参数的配置文件为 /etc/sysctl.conf 和 /etc/sysctl.d/ 目录。其读取顺序为:

/etc/sysctl.d/ 下面的文件按照字母排序;然后读取 /etc/sysctl.conf 。

一般所有对系统的修改参数放在 /etc/sysctl.d/ 目录,系统原有的 sysctl.conf 文件保存一些更加基础的配置。

sysctl.conf 可优化参数如下:

fs.file-max = 2097152
 
# 减少交换内存使用,默认60,建议10-30
vm.swappiness = 30
 
# 脏数据的比例和处理,根据场景不同设置,
# 参考 https://lonesysadmin.net/2013/12/22/better-linux-disk-caching-performance-vm-dirty_ratio/
# 如果是数据库服务器,希望数据能够尽快安全写入,可降低内存缓存比例
# vm.dirty_background_ratio = 5
# vm.dirty_ratio = 10
 
# 如果是业务服务器,对数据安全写入无要求,可加大内存缓存比例
# vm.dirty_background_ratio = 50
# vm.dirty_ratio = 80
 
# 设置为1,内核允许分配所有的物理内存,Redis常用
vm.overcommit_memory = 1
 
# 系统拥有的内存数,ElasticSearch启动必备
vm.max_map_count = 262144
 
# 设置为1
net.ipv4.tcp_no_metrics_save = 1
 
# 禁用 sysrq 功能
kernel.sysrq = 0
 
# 控制 core 文件的文件名中是否添加 pid 作为扩展
kernel.core_uses_pid = 1
 
# 设置为1,防止 SYNC FLOOD 攻击
net.ipv4.tcp_syncookies = 1
 
# 消息队列的最大消息大小,默认8k,建议64kb
kernel.msgmax = 65536
# 消息队列存放消息的总字节数
kernel.msgmnb = 163840
 
# TIME_WAIT socket的最大数目,不宜太大或者太小,nginx反向代理必备
net.ipv4.tcp_max_tw_buckets = 50000
# 打开 SACK 选项,设置为1
net.ipv4.tcp_sack = 1
# 激活窗口扩充因子,支持64kb以上数据传输
net.ipv4.tcp_window_scaling = 1
 
# TCP 缓冲区内存,连接数达到非常高时候需要配置好
net.ipv4.tcp_mem = 786432 2097152 3145728  
net.ipv4.tcp_rmem = 4096 4096 16777216
net.ipv4.tcp_wmem = 4096 4096 16777216
 
# socket缓冲区默认值和最大值
net.core.wmem_default = 8388608
net.core.rmem_default = 8388608
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
 
# ACCEPT等待队列长度,适当,太大了堆积也无用 
net.core.netdev_max_backlog = 65535
 
# 允许最大并发连接数,重要
net.core.somaxconn = 65535
 
# 不属于任何进程的socket数目,不宜太大,防止攻击
net.ipv4.tcp_max_orphans = 65535
 
# SYNC等待队列长度,适当,太大了排队也没用
net.ipv4.tcp_max_syn_backlog = 65535
 
# 禁用timestamp,重要,高并发下设置为0
net.ipv4.tcp_timestamps = 0
 
# 发送 SYNC+ACK 的重试次数,不宜太大,5以内
net.ipv4.tcp_synack_retries = 1
# 发送SYNC的重试次数,不宜太大,5以内
net.ipv4.tcp_syn_retries = 1
 
# 允许回收TCP连接,重要,必须为1
net.ipv4.tcp_tw_recycle = 1
 
# 允许重用TCP连接,重要,必须为1
net.ipv4.tcp_tw_reuse = 1
 
# 服务端主动关闭后,客户端释放连接的超时,重要,<30
net.ipv4.tcp_fin_timeout = 5
 
# 允许TCP保持的空闲keepalive时长,不需要太长
net.ipv4.tcp_keepalive_time = 30
 
# 系统作为TCP客户端连接自动使用的端口(start,end),可发起并发连接数为end-start
net.ipv4.ip_local_port_range = 10240 65535

运行 sysctl -p 重载配置。

【tips】常用指令:sysctl -a |grep <参数名> 可查询当前参数值

# sysctl -a|grep net.ipv4.tcp_tw_reuse
net.ipv4.tcp_tw_reuse = 1

2.ulimits 优化

设置打开文件的最大数量(文件描述符),按需修改最大数值。

编辑 /etc/security/limits.conf ,添加或替换下面几行代码到文件结尾:

root hard nofile 65535
* soft nofile 65535
* hard nofile 65535
* soft nproc 65535
* hard nproc 65535

断开 SSH 并重新 SSH 连接服务器,输入下面的命令检查是否生效:

ulimit -a

对于进程来说,重启就能使其生效。

参考:https://developer.aliyun.com/article/718976

阿里云-Linux实例常用内核网络参数介绍与常见问题处理

https://help.aliyun.com/knowledge_detail/41334.html

原文地址:https://www.cnblogs.com/sfnz/p/14693190.html