关于LNMP的优化

线上有一个用LNMP构架的网站,有一天看到它出现了“502 Bad Gateway”错误,于是就开始思考优化问题了。
首先,明确一下HTTP各个状态码的含义:

  1. 1XX 临时消息
  2. 2XX 返回成功
  3. 3XX 重新定向
  4. 4XX 客户端错误
  5. 5XX 服务端错误

基于LNMP的网站上,当HTTP请求返回的状态码是5XX的时候,说明是服务端出了问题;但问题不一定是出在NginX,因为NginX本身十分轻量,不做太多的复杂逻辑处理,所以很少会出错;除了静态资源的请求,其他大部分请求NginX都会转给PHP-FPM来处理,所以一般问题是更多地出在PHP。比如,开篇说那个网站出现的502错误,查看NginX日志发现大量的connect() to unix:/PATH/TO/PHP-FPM-SOCK failed (11: Resource temporarily unavailable),其实原因是系统最大连接数过小。

Linux的内核优化

按照《高性能Linux服务器构建实战:运维监控、性能调优与集群应用》里的介绍,配置系统参数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# /etc/sysctl.conf

net.ipv4.tcp_max_tw_buckets = 6000
net.ipv4.ip_local_port_range = 1024 65000
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_syncookies = 1
net.core.somaxconn = 262144
net.core.netdev_max_backlog = 262144
net.ipv4.tcp_max_orphans = 262144
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_synack_retries = 1
net.ipv4.tcp_syn_retries = 1
net.ipv4.tcp_fin_timeout = 1
net.ipv4.tcp_keepalive_time = 30

个人保守起见,参数略有调整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# /etc/sysctl.conf

net.core.somaxconn = 262144 # 表示系统同时发起的最大连接数,默认128
net.core.netdev_max_backlog = 262144 # 表示当每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许发送到队列的数据包的最大数目,默认1000

net.ipv4.ip_local_port_range = 32768 60999 # 表示允许系统随机打开的端口范围,默认32768 60999
net.ipv4.tcp_max_tw_buckets = 6000 # 表示系统同时保持timewait的最大数量,默认524288
net.ipv4.tcp_tw_recycle = 0 # 开启TCP连接中timewait sockets的快速回收,默认不开启
net.ipv4.tcp_tw_reuse = 1 # 开启TCP连接中timewait sockets重新用于新的TCP连接,默认不开启
net.ipv4.tcp_max_orphans = 262144 # 表示系统中不被关联到任何一个用户文件句柄上的TCP sockets的最大数目,可以防止简单的DoS攻击,默认262144
net.ipv4.tcp_max_syn_backlog = 262144 # 表示记录尚未收到客户端确认信息的连接请求的最大数目,默认128
net.ipv4.tcp_syncookies = 1 # 开启syncookies功能,可以防止部分SYN攻击,默认开启
net.ipv4.tcp_synack_retries = 1 # 表示系统放弃连接之前发送SYN+ACK包的数量
net.ipv4.tcp_syn_retries = 1 # 表示系统放弃连接之前发送SYN包的数量
net.ipv4.tcp_fin_timeout = 3 # 表示TCP sockets保持在FIN-WAIT-2状态的时间,默认60秒
net.ipv4.tcp_keepalive_time = 30 # 表示当启用keepalive的时候,TCP发送keepalive消息的频度,默认7200秒

以上参数主要为了配合NginX、PHP-FPM和MySQL致发挥更佳效能,各个参数的数值应该根据实际主机硬件条件自行调整。将配置参数追加到/etc/sysctl.conf文件后,执行以下命令,让系统重载参数:

1
$ sudo sysctl -p

NginX的配置优化

大致配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# /etc/nginx/nginx.conf

user www-data; # 指定用户
worker_processes 8; # woker进程数,一般设置为CPU核数*线程数
worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000
01000000 10000000; # 为每个进程分配到指定的CPU,配合worker_processes使用
worker_rlimit_nofile 204800; # 每个woker能打开的最多文件描述符数目
pid /run/nginx.pid; # 进程号存放位置

events {
use epoll; # 设置用于复用客户端线程的轮询方法
worker_connections 204800; # 每个woker的最多连接数目,不要大于worker_rlimit_nofile
multi_accept on; # 启用收到一个新连接通知后接受尽可能多的连接
}

http {
keepalive_timeout 30; #
server {
listen 80 backlog=204800; #
}
}

实际上NginX的worker_connections参数会受到系统的net.core.somaxconn参数限制,而net.core.somaxconn参数也是有限制的(可以用ulimit -Hn命令查看),理论上worker_connections应该设为net.core.somaxconn除以worker_processes
在配合PHP-FPM使用的时候,如果想NginX支持1000并发请求,那么worker_connections取值不能低于2000,因为每个请求里,NginX与客户端需要一个连接,与PHP-FPM也需要一个连接。

PHP-FPM的配置优化

Unix socket VS TCP socket

PHP-FPM提供两种通信方式与NginX对接数据,一种是Unix socket,另一种是TCP socket。
Unix socket是同一操作系统上的两个或多个进程进行数据通信的编程接口,这种通信方式是发生在系统内核里而不会在网络里传播;TCP socket则是基于TCP/IP协议,进程间的通信是通过网络传输的,可以跨系统、跨主机。
两者相较之下,Unix socket更低层,执行效率更高;而TCP socket封装性更高,安全性更好。如果NginX和PHP-FPM不在同一主机上,那么只能选择TCP socket;否则,建议考虑Unix socket。

最大进程数

假设一个PHP-FPM进程占用10MB内存,且打算分配1GB内存给PHP-FPM用,那么PHP-FPM的pm.pm.max_children可以设为100。当然,实际的每个PHP-FPM进程平均所需内存要根据实际情况计算。
但PHP-FPM进程多不一定有用,有用是指PHP-FPM与NginX之间有连接,而连接数也是受到系统的net.core.somaxconn参数限制(连接数不是进程数,一个进程可以处理多个连接)。

MySQL的配置优化

最大连接数

首先,与NginX、PHP-FPM不同,MySQL是多线程方式工作。一般MySQ处理连接的方式(thread_handling)有每个连接一个线程(one-connection-per-thread)和所有连接一个线程(no-threads)。
no-threads模式大多用在调试,而正式线上环境普遍采用的是one-connection-per-thread模式。

一般情况下,一个PHP-FPM进程最多只会跟MySQL建立一个连接,MySQL的最大连接数(max_connections)不应该少于PHP-FPM进程数,否则并发的时候有的PHP-FPM会连接不上MySQL。对于越来越大的并发量,不能一味提高MySQL的最大连接数,合理的方式是,设定一个MySQL连接数界限,超出的连接请求需等待。

短连接和长连接

PHP-FPM与MySQL之间的连接有两种形式,分短连接和长连接。 MySQL在one-connection-per-thread模式下,PHP-FPM需要请求操作数据库时:如果PHP-FPM使用短连接形式,那么MySQL都会新建一个线程来支持这个连接,数据操作结束后,PHP-FPM会回收这个连接,MySQL也会回收对应的线程,这里就会有一定的时间消耗和的IO消耗;如果PHP-FPM使用长连接形式,在没有相同的长连接的情况下,PHP-FPM才会新建一个连接,数据操作结束后这个线程不会被回收,而是等待被复用,这样虽然会节省一些开销,但是在高并发的情况下,大量的长连接建立不回收,MySQL也管理同样多的线程,大量的上下文切换和资源竞争,会使得MySQL执行效率下降。

如果PHP-FPM采用动态进程管理模式,且所有进程都与MySQL长连接的话,那么空闲时,部分PHP-FPM进程被回收(与MySQL的连接也会被回收),部分PHP-FPM进程被保留下来(与MySQL的连接也会被保留下来);高并发时,保留下来的PHP-FPM进程可以复用已有的MySQL连接,其他的重新建立,这就相当于极端长连接和极端短连接的折中。

一般的LNMP网站应用使用短连接访问数据库就足够了,用完就回收,减轻系统负担;中大型的可能需要使用长连接,因为操作数据库的请求不断发起,把时间和IO花费在建立新连接和回收旧连接就很浪费,但是MySQL的线程数(连接数)必须维持在合理范围内。

线程池和连接池

要在高并发中,控制MySQL的线程数(连接数),一般的解决方案是设置线程池或者连接池。

线程池是在数据服务端建立有限数量的线程,来处理所有应用客户端的连接。MySQL企业版(收费)提供了线程池机制(thread_handling=pool-of-threads),详细配置可以参考官方文档MySQL Enterprise Thread Pool

连接池是在应用客户端与数据服务端之间,设置一个中间代理,这个中间代理会与数据服务端建立有限数量的连接,来处理所有应用客户端的数据请求。连接池可以使用开源的数据库中间件atlas来搭建,也可以使用PHP扩展swoole来自己写一个。

php-cp是Swoole组织开发的一个PHP扩展,可以本地代理MySQL、Redis连接,提供连接池,读写分离,负载均衡,慢查询日志,大数据块日志等功能,在主流php框架引入使用也十分简便,具体可以参考

其他

平时要多留意LNMP的日志信息,根据提示优化配置,比如:

  1. NginX(24: Too many open files)说明work_rlimit_nofile参数需要增大;
  2. NginX(worker_connections are not enough while connecting to upstream)说明worker_connections参数需要增大;
  3. NginX(11: Resource temporarily unavailable)一般是并发连接超过来系统的net.core.somaxconn限制;
  4. MySQL(too many connections)说明max_connections参数需要增大;
  5. MySQL(too many open files)说明open_files_limit参数需要增大,当然系统的fs.file-max也不能过小;
  6. MySQL(has gone away)是连接太久没活动被MySQL回收了,MySQL的wait_timeout也不能过小。

最后

此文的LNMP优化主要是针对高并发情况,其他方面优化有机会会继续补充。