线上有一个用LNMP构架的网站,有一天看到它出现了“502 Bad Gateway”错误,于是就开始思考优化问题了。
首先,明确一下HTTP各个状态码的含义:
- 1XX 临时消息
- 2XX 返回成功
- 3XX 重新定向
- 4XX 客户端错误
- 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 | # /etc/sysctl.conf |
个人保守起见,参数略有调整:
1 | # /etc/sysctl.conf |
以上参数主要为了配合NginX、PHP-FPM和MySQL致发挥更佳效能,各个参数的数值应该根据实际主机硬件条件自行调整。将配置参数追加到/etc/sysctl.conf
文件后,执行以下命令,让系统重载参数:
1 | $ sudo sysctl -p |
NginX的配置优化
大致配置如下:
1 | # /etc/nginx/nginx.conf |
实际上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的日志信息,根据提示优化配置,比如:
- NginX(24: Too many open files)说明
work_rlimit_nofile
参数需要增大; - NginX(worker_connections are not enough while connecting to upstream)说明
worker_connections
参数需要增大; - NginX(11: Resource temporarily unavailable)一般是并发连接超过来系统的
net.core.somaxconn
限制; - MySQL(too many connections)说明
max_connections
参数需要增大; - MySQL(too many open files)说明
open_files_limit
参数需要增大,当然系统的fs.file-max
也不能过小; - MySQL(has gone away)是连接太久没活动被MySQL回收了,MySQL的
wait_timeout
也不能过小。
最后
此文的LNMP优化主要是针对高并发情况,其他方面优化有机会会继续补充。