高并发情况下服务器调优

TCP内核参数调优

调整全连接队列长度

TCP 建立连接时要经过 3 次握手,在客户端向服务器发起连接时,
对于服务器而言,一个完整的连接建立过程,服务器会经历 2 种 TCP 状态:SYN_REVD, ESTABELLISHED。对应也会维护两个队列:

  1. 一个存放SYN的队列(半连接队列,也成SYN队列)
  2. 一个存放已经完成连接的队列(全连接队列, 也称Accept队列)

当一个连接的状态是 SYN RECEIVED 时,它会被放在 SYN 队列中。
当它的状态变为 ESTABLISHED 时,它会被转移到另一个队列。应用程序只从已完成的连接的队列中获取请求。

tcp_sync_queue

全连接队列长度如何计算?

对于Linux系统上的服务程序(比如nginx、php-fpm等应用程序),都会使用同一个底层系统调用API来监听端口。listen中的backlog参数,设置了应用程序全连接队列长度。应用程序的全队列长度,影响了可以接受的并发请求的最大值。

1
2
#include <sys/socket.h>
int listen(int socket, int backlog);

应用程序的全队列长度除依赖于程序本身listen中的backlog,还依赖系统内核中net.core.somaxconn参数,即应用程序的全队列长度取backlog和net.core.somaxconn中最小值

对高并发访问的应用,如果只增加应用层的backlog是不够,还要注意系统内核中队列设置,linux系统默认值都是不会太大。比如Ubuntu 16中net.core.somaxconn默认值是128。

我们可以通过下面方式,查看系统内核参数大小:

1
cat /proc/sys/net/core/somaxconn

通过以下方式调整内核参数大小

1
2
3
4
5
sudo sysctl -w net.core.somaxconn=32768

// 或者

sudo echo 32768 > /proc/sys/net/core/somaxconn

注意:上面方式更改之后,会立即生效,但服务器重启之后会恢复到默认值。我们可以通过以下方式,永久性更改:

1
2
echo 'net.core.somaxconn=32768' >> /etc/sysctl.conf
sysctl -p // 让改动立即生效

半连接队列长度如何计算?

半连接队列长度由内核参数 tcp_max_syn_backlog 决定,
当使用 SYN Cookie 时(即内核参数 net.ipv4.tcp_syncookies = 1时候),半连接对垒长度不能大于长连接长度

半连接队列长度 = min(应用层的backlog, 内核参数net.core.somaxconn,内核参数tcp_max_syn_backlog)

查看队列溢出

高并发访问情况,若应用程序处理不过来,就会造成SYN/Accept队列溢出,我们可以通过以下方式查看溢出情况:

  • 查看SYN队列溢出
1
2
netstat -s | grep LISTEN
102324 SYNs to LISTEN sockets dropped
  • 查看ACCEPT队列溢出
1
2
netstat -s | grep TCPBacklogDrop
TCPBacklogDrop: 2334

查看应用的全连接队列最大长度

应用的backlog调整之后,我们可以通过ss命令查看调整结果:

1
2
3
4
ss -ln
Recv-Q Send-Q Local Address:Port Peer Address:Port
0 511 *:80 *:*
0 128 127.0.0.1:9000 *:*

在LISTEN状态,其中 Send-Q 即为Accept queue的最大值,Recv-Q 则表示全连接队列中等待被服务器accept()。从上面我们可以看出Nginx(80端口)的全连接队列长度是511,PHP-FPM(9000端口)全连接队列长度是128

注意: 调整net.core.somaxconn参数之后,一定要重启应用。比如Nginx, PHP-FPM默认的backlog都是511,若此前服务器net.core.somaxconn参数是128。则Nginx,FPM的全连接队列长度就是128。此时调整net.core.somaxconn`参数值1024,对内核来说是立即生效的。但对Nginx,FPM主进程早已经启动,最大队列长度还是128。所以需要我们手动启动应用主进程

1
2
sudo sysytemctl restart nginx
sudo systemclt restart php7.1-fpm-service

调整本地端口范围,防止端口资源耗尽

默认情况本地端口范围不足3万,对于高并发服务器,有时候是不够的,之前我们项目中峰值qps达到3.5k时候出现Cannot assign requested address。查看项目日志分析原因是大量连接Redis时候,此时项目服务器作为客户端,端口资源大量处理TIME_WAIT状态,导致端口资源使用完了。解决这个问题一个手段就是调整端口范围。

查看端口范围:

1
2
cat /proc/sys/net/ipv4/ip_local_port_range
32768 60999

调整端口范围:

1
echo 'net.ipv4.ip_local_port_range = 1024    65000' > /proc/sys/net/ipv4/ip_local_port_range

开启tcp_syncookies,增强抗SYN Flood的能力

TCP连接建立时,客户端通过发送SYN报文发起向处于监听状态的服务器发起连接,服务器为该连接分配一定的资源,并发送SYN+ACK报文。对服务器来说,此时该连接的状态称为半连接(Half-Open),而当其之后收到客户端回复的ACK报文后,连接才算建立完成。在这个过程中,如果服务器一直没有收到ACK报文(比如在链路中丢失了),服务器会在超时后重传SYN+ACK。

syn_flood_attack

但是如果有坏人故意大量不断发送伪造的SYN报文,那么服务器就会分配大量注定无用的资源,并且从backlog的意义 中可知,服务器能保存的半连接的数量是有限的!所以当服务器受到大量攻击报文时,它就不能再接收正常的连接了。换句话说,它的服务不再可用了!这就是SYN Flood攻击的原理,它是一种典型的DDoS攻击。

SYN Cookie技术可以让服务器在SYN队列溢出时候,在收到客户端的SYN报文时,不再分配资源保存客户端信息,而是将这些信息保存在SYN+ACK的初始序号和时间戳中。对正常的连接,这些信息会随着ACK报文被带回来。SYN Cookie是通过参数tcp_syncookies开启。

1
2
3
4
5
cat /proc/sys/net/ipv4/tcp_syncookies // 查看是否开启tcp_syncookies
echo 1 > /proc/sys/net/ipv4/tcp_syncookies // 开启tcp_syncookies功能,重启后失效

echo 'net.ipv4.tcp_syncookies=1' >> /etc/sysctl.conf
sysctl -p // 让改动立即生效

其他内核参数优化

  • net.ipv4.tcp_tw_reuse = 1

    默认值是0,表示关闭。开启之后允许将TIME_WAIT socket重新用于新的TCP连接。开启后,有助于解决Cannot assign requested address等time_wait过多造成的问题。

  • net.ipv4.tcp_tw_recycle = 1

    默认值是0,表示关闭。表示开启TCP连接中TIME_WAIT socket的快速回收。

  • net.ipv4.tcp_fin_timeout = 30

    如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间

  • net.ipv4.tcp_keepalive_time = 1200

    默认值是2小时,表示当Keepalived启用时候,TCP发送Keepalived消息的平度,改为20分钟

  • net.ipv4.tcp_max_syn_backlog = 8192

    默认值是1024,表示SYN队列的长度,增大队列长队值值8192,可以容纳更多等待连接的网络连接数。

  • net.ipv4.tcp_synack_retries = 1

    默认值5,表示重发5次,每次等待30~40秒,即半连接默认时间大约为180秒。客户端connect()返回不代表TCP连接建立成功,有可能此时accept queue 已满,系统会直接丢弃后续ACK请求;客户端误以为连接已建立,开始调用等待至超时;服务器则等待ACK超时,会重传SYN+ACK 给客户端,重传次数为net.ipv4.tcp_synack_retries

  • net.core.netdev_max_backlog = 4096
    每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目。

  • net.ipv4.tcp_max_tw_buckets = 5000

    表示系统同时保持TIME_WAIT套接字的最大数量,如果超过这个数字,TIME_WAIT套接字将立刻被清除并打印警告信息。默认为180000,改为 5000。对于Apache、Nginx等服务器,上几行的参数可以很好地减少TIME_WAIT套接字数量,但是对于Squid,效果却不大。此项参数可以控制TIME_WAIT套接字的最大数量,避免Squid服务器被大量的TIME_WAIT套接字拖死

Nginx配置调优

工作进程

NGINX可以运行多个工作进程,每个工作进程都能够处理大量的同时连接。我们可以控制工作进程的数量以及它们处理最大连接数:

  • worker_processes

    work_processes设置Nginx工作进程数,默认是1。最佳配置是有几个cpu,就配置几个工作进程,可以通过设置auto自动侦探cpu个数来实现。

    1
    worker_processes auto;
  • worker_connections

    worker_connections设置每个工作进程可以同时处理的最大连接数。默认值为512。对于高并发情况下,这个有点小。

    1
    worker_connections 2048;

    注意同时连接的实际数量不能超过打开文件最大数量的当前限制,即worker_rlimit_nofile指令的限制。

    若worker_connections设置过小此时nginx错误日志会报下面错误:

    1
    2019/12/31 09:01:22 [alert] 6215#6215: *60878 768 worker_connections are not enough while connecting to upstream, client: 192.168.33.1, server: , request: "GET /api/v2/cats?page=1&size=99&lang=zh HTTP/1.0", upstream: "fastcgi://unix:/var/run/php/php7.1-fpm.sock:", host: "192.168.33.10:9969"
  • worker_rlimit_nofile

    用于限制工作进程最大打开文件数(RLIMIT_NOFILE)。此指令可以在不重启主进程生效。

    1
    worker_rlimit_nofile 65535;

    在调整work_connetions设置之后,如果未调整此指令值,高并发情况下,Nginx错误日志里面会报下面错误:

    1
    2019/12/31 09:52:30 [alert] 7122#7122: *21193 socket() failed (24: Too many open files) while connecting to upstream, client: 192.168.33.10, server: , request: "GET /api/v2/cats?page=1&size=99&lang=zh HTTP/1.0", upstream: "fastcgi://unix:/var/run/php/php7.1-fpm.sock:", host: "192.168.33.10:9969"

backlog设置

Nginx默认监听的backlog是512,如果队列满了,客户端再进行请求时候,会收到连接拒绝(Connection refused)。在高并发访问情况下,我们可以调整至8192。

1
listen  80 backlog=8192;

PHP-FPM配置调优

  • 调整backlog配置

fpm默认backlog大小是511,对于高并发应用,这个值有点低。我们可以根据FPM的能支持的QPS来调整中,即backlog = QPS。

1
listen.backlog = 1024

若发现nginx报告如下错误:

1
connect() to unix:/var/run/php/php7.1-fpm.sock failed (11: Resource temporarily unavailable) while connecting to upstream, client: 192.168.33.1, server: , request: "GET /api/

往往说明fpm backlog设置太小。

注意:

fpm的backlog并不是越大越好。fpm的backlog太大,会导致大量请求在fpm这边堆集,处理不过来,最终nginx会等待超时,报错504 gateway timeout错误,同时等fpm处理完准备write 数据给nginx时,发现TCP连接断开了,报Broken pipe错误。

fpm的backlog太小的话,会导致nginx代理过来的请求,根本进入不了php-fpm的全队列,报502 Bad Gateway错误。

调整fpm的backlog时候,注意调整系统内核的TCP全队列长度是否大于backlog。

对于内核参数,Nginx,FPM等调优过程中,需要不断测试调优,先使用系统默认参数,逐个参数调整,然后进行ab压测, 观察压测的qps,异常等信息,结合nginx日志,fpm日志来定位参数调整后情况

1
ab -n 10000 -c 500 "http://192.168.33.10:9969/api/v2/cats?page=1&size=99&lang=zh"

参考来源