九阳真经之Nginx(前世今生)

249 阅读8分钟

Nginx调优与背后原理


1.Linux网络IO流程

下面三种IO多路复用的对比,可以看到epoll模型的性能比select/poll要高很多。

系统调用 select poll epoll
操作方式 遍历 遍历 遍历
底层实现 数组bitmap 链表 哈希表
查找就绪fd时间复杂度 O(n) O(n) O(1)
最大支持文件描述符数 一般有最大值限制 无上限 65535
工作模式 LT LT 支持ET高效模式
fd拷贝 每次调用select都需要把fd集合从用户态拷贝到内核态 每次调用poll都需要把fd集合从用户态拷贝到内核态 采用回调方式检测就绪时间,时间复杂度:O(1)

2.TCP/IP协议简介

2.1 TCP三次握手四次断开简介

简而言之TCP/IP是指协议簇,也就是一组协议,而非单指TCP协议或者IP协议。我们今天要介绍的这组协议中的TCP协议,主要会介绍跟Nginx调优相关的一些技术。首先我们要介绍下著名的TCP协议的三次握手与四次挥手。如下图:

三次握手
三次握手分为以下三步:

  • client发送syn(x)到server
  • server返回给client ack(x+1)+syn(y)
  • client确认ack(y+1)给server

在咱们之前建立的nginx服务器上,我们也可以通过抓包来查看这三次握手和四次挥手的过程。分别如下图所示的两个黑色选中部分,第一部分三个报文是建立tcp的三次握手。握手成功后开始建立htpp请求,与下面黑色部分之间则是数据报文的传递,客户端请求、服务端进行响应。后面的三行则是进程tcp的四次挥手。(但是为什么只有三个报文,是因为我们请求的数据服务端已经完成响应,没有额外需要发送的数据,所以客户端发送FIN标志位之后,服务端就直接回复ACK并且调用close()方法,也向客户端发送了FIN标志,客户端接收到之后发送ACK确认信息,等待两个MSL周期之后关闭连接)

11:52:50.426805 IP 192.168.198.157.37076 > 192.168.198.150.80: Flags [S], seq 2098832266, win 29200, options [mss 1460,sackOK,TS val 1451447984 ecr 0,nop,wscale 7], length 0
11:52:50.426847 IP 192.168.198.150.80 > 192.168.198.157.37076: Flags [S.], seq 1618028255, ack 2098832267, win 28960, options [mss 1460,sackOK,TS val 2047609024 ecr 1451447984,nop,wscale 7], length 0
11:52:50.427342 IP 192.168.198.157.37076 > 192.168.198.150.80: Flags [.], ack 1618028256, win 229, options [nop,nop,TS val 1451447985 ecr 2047609024], length 0
11:52:50.427400 IP 192.168.198.157.37076 > 192.168.198.150.80: Flags [P.], seq 2098832267:2098832357, ack 1618028256, win 229, options [nop,nop,TS val 1451447985 ecr 2047609024], length 90: HTTP: HEAD /index.html HTTP/1.1
11:52:50.427410 IP 192.168.198.150.80 > 192.168.198.157.37076: Flags [.], ack 2098832357, win 227, options [nop,nop,TS val 2047609025 ecr 1451447985], length 0
11:52:50.427719 IP 192.168.198.150.80 > 192.168.198.157.37076: Flags [P.], seq 1618028256:1618028559, ack 2098832357, win 227, options [nop,nop,TS val 2047609025 ecr 1451447985], length 303: HTTP: HTTP/1.1 200 OK
11:52:50.428008 IP 192.168.198.157.37076 > 192.168.198.150.80: Flags [.], ack 1618028559, win 237, options [nop,nop,TS val 1451447986 ecr 2047609025], length 0
11:52:50.448329 IP 192.168.198.157.37076 > 192.168.198.150.80: Flags [F.], seq 2098832357, ack 1618028559, win 237, options [nop,nop,TS val 1451447986 ecr 2047609025], length 0
11:52:50.452578 IP 192.168.198.150.80 > 192.168.198.157.37076: Flags [F.], seq 1618028559, ack 2098832358, win 227, options [nop,nop,TS val 2047609050 ecr 1451447986], length 0
11:52:50.452921 IP 192.168.198.157.37076 > 192.168.198.150.80: Flags [.], ack 1618028560, win 237, options [nop,nop,TS val 1451448008 ecr 2047609050], length 0

四次挥手
四次挥手分为以下四步

  • 客户端发送一个FIN,用来关闭客户到服务器的数据传送
  • 服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号
  • 服务器关闭客户端的连接,发送一个FIN给客户端
  • 客户端发回ACK报文确认,并将确认序号设置为收到序号加1

我们的优化部门会涉及到这三次握手和四次挥手,当然这跟系统级的优化也有关系。我们先来看下从网卡接收到数据包到交由Linux内核协议簇处理,再到建立TCP三次握手的简单过程。

驱动程序将内存中的数据包转换成内核网络模块能识别的格式。数据包会被先存入到CPU的input_pkt_queue队列中(通过调整系统参数net.core.netdev_max_backlog来调整队列大小),然后内核将Client首先发送的SYN连接信息放到syn队列中(该队列由内核参数net.ipv4.tcp_max_syn_backlok决定)。这个时候队列中都处于半连接状态,同时返回一个syn+ack包给客户端,这里涉及到一个调优参数net.ipv4.tcp_synack_retries:即服务端向客户端返回syn+ack的尝试测试,在centos7上默认是5次(ls+2s+4s+8s+16s=31s.即会尝试5次,大概31s时间。)之后client再次发送ack包给服务端,内核会把连接从syn队列中取出,再把这个连接放到accept队列中。此时已经是establishd状态。最好应用服务器调用accept()系统从accept队列中获取已经建立成功的连接套接字。这里我们就涉及到以下内核调优参数,其中包含三个队列:

net.core.netdev_max_backlog: 接收自网卡,但未被内核协议栈处理的报文队列长度

net.ipv4.tcp_max_syn_backlog: SYN_RCVD状态(即半连接)队列长度

backlog: 全连接队列也就是我们图中标注的accept队列。该队列大小由系统参数和应用参数共同决定。即:**全连接队列的大小取决于:min(backlog,somaxconn),其中backlog是由应用程序传入,somaxconn是一个os级别的系统参数,通过设置net.core.somaxconn来调整。**在Nginx中,backlog参数在listen参数后面指定,在Centos上,默认为511

2.2 由三次握手引发的一些问题

如果是syn队列满了,不开启syncookies(net.ipv4.tcp_syncookies决定)的时候,服务端会丢弃新来的SYN包,而client在多次重发SYN(由参数net.ipv4.tcp_syn_retries决定,默认为6次。大概63s时间)包得不到响应而返回connection timeout错误

3. Linux内核优化

首先,你需要修改/etc/sysctl.conf来更改内核参数

net.ipv4.tcp_synack_retries = 2
net.core.netdev_max_backlog = 250000
net.ipv4.tcp_max_syn_backlog = 10240
net.core.somaxconn = 10240
net.ipv4.tcp_syncookies = 1 # 0表示禁用,1表示启用。可以预防少量的syn攻击
net.ipv4.tcp_syn_retries = 2
net.ipv4.ip_local_port_range = 1024 65000
net.ipv4.tcp_abort_on_overflow = 1
net.ipv4.tcp_rmem = 16384 1048576 12582912
net.ipv4.tcp_wmem = 16384 1048576 12582912
net.ipv4.tcp_mem = 1541646 2055528 3083292
net.ipv4.tcp_moderate_rcvbuf = 1
net.ipv4.tcp_orphan_retries = 3
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_max_tw_buckets = 5000
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 0
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_retries1 = 2
net.ipv4.tcp_retries2 = 3
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 2
net.ipv4.tcp_keepalive_probes = 3
fs.file-max = 500000000
fs.nr_open = 10000000

4. Nginx调优

5.1 主配置段优化

【设置进程静态优先级】

我们从宏观层面上一颗CPU好像可以同时执行很多进程,但其实从微观意义上来看,这些进程其实是串行执行的,也就是CPU把进程的运行时间分成一个个的时间片,然后CPU在通过系统调度某个进程,然后最多执行该进程时间片的时间。所以,如果进程时间片较长,那么它的执行优先级就更改,在Linux中我们可以通过调整NICE即静态优先级-20~19的大小来实现。数值越小,优先级越高。因此我们可以将nginx优先级设置为-20,设置参数如下:

worker_priority -20; # 默认为0

【设置worker进程数】

设置worker进程数与CPU核心数相同,更好地利用CPU的多级缓存

worker_processes auto; # 默认为1

【CPU绑定提供缓存命中率】

为了适应CPU的高速取值,现在的CPU一般都会有三级缓存,其中一级L1缓存速度最好。依次减速。其中L1和L2为单插槽上单核CPU独享,L3则是单插槽上多核CPU共享。

worker_cpu_affinity auto;