本文来自作者 蒋岩 在 GitChat 上分享 「网站性能优化从入门到粗通(PHP 篇)」,「阅读原文」查看交流实录。
「文末高能」
编辑 | 哈比
题外话:本次 Chat 的主题是入门到粗通,所以内容对于老司机来说会比较 Low,请多包涵。
进入正题。
笔者第一次接触网站性能优化是在 2009 年实习时期,当时所在的公司有一个 PHP+Mysql 开发的 B2C 平台需要做用户体验优化。
因为用户量有限,网站整体架构比较简单,所以我选择的测试工具是 Google
的 PageSpeed Insights 以及 Yahoo 的 Yslow。
Google 官方的 PageSpeed Tools 针对 Firefox 以及 Google Chrome 都有扩展插件,这里以在线版做一点简单的演示。PageSpeed Tools 在线版网址:https://developers.google.com/speed/pagespeed/。
大家可以访问 这个链接地址 来查看 PageSpeed Tools 给出的评估结果和建议、对于如何优化它有相应的介绍。
工具所给出的修缮细节这里不做详细介绍,感兴趣的童鞋可以使用该工具给自己的网站做一个测试,并结合 PageSpeed Tools 提供的思路和解决方案来做一些优化。
接着可以使用 http://www.webpagetest.org/ 这个在线工具查看每一个元素加载时间具体耗时。
点击图片即可看到清晰的报告:
这里可以看到整个页面从解析 dns 到完全加载所消耗的详细时间,针对耗时严重的节点因地制宜地处理。
接下来一些没有技术含量的优化套路(个人觉得适合运维的童鞋)。
所谓工欲善其事、必先利其器。功夫再好,也怕菜刀。通过几个常用的 linux 命令来检测自己机器硬件是否牛逼。
1)查看 CPU
grep “model name” /proc/cpuinfo | cut -f2 -d:
2)查看内存大小及使用情况
3)查看服务器负载情况
命令详解请参考 http://www.jb51.net/article/40807.htm。
4)咨询服务商机器带宽
如果这几个数据排查来来结果不理想,请砸钱狠狠升级硬件,把砍柴刀升级到屠龙刀、只要代码层面不是太作死,一般都可以在性能有很大的提升。
非常建议直接选购阿里等大厂优化过软环境参数的高配服务器(如果您有这个想法和条件,后面就不用继继续看下去了,毕竟我的知识深度在大厂的大牛面前不值一提),另外如果图片、JS、CSS、等静态资源等资源,建议选用稳定性及效果比较好的 CDN 加速节点服务。
如果在意 “节点数量” 可选择安全宝。
如果在意 “节点稳定性及 SEO 优化” 可选择百度云加速。
如果在意 “是否免费” 可选择 360 卫士。
如果在意 “功能均衡” 可选择加速乐。
1)升级 PHP
如鸟哥惠新宸大大多次提到,PHP7 的性能在多方面都是 PHP5 的 2 倍以上、如果你的代码兼容 PHP5.6,那么放心大胆的把 PHP 环境升级到 7 吧。关于从 5 到 7 性能提升的报告,请参考 鸟哥的 PPT。
如果你对性能的要求非常的极致、那么请使用 PHP7 的强类型模式、一定可以让你获得非常不错的体验。
2)升级 Nginx 版本
Nginx 从 1.7.11 开始实现了线程池机制,大部分场景中可以避免使用阻塞,整体性能有了数倍提升。
如果你还在使用低版本的 Nginx 并且想在并发效率上做一个质的提升,新版本 Nginx 在等到你的召唤。
请参考官网的介绍:
https://www.nginx.com/blog/thread-pools-boost-performance-9x/
3)选择 Mysql 版本
这里建议使用 Mysql 5.6 版本(相对更稳定,更快)。Mysql 5.7 这个版本因为增加了些新特性,普通的软件工程师未必能 Hold 住,当然,如果您公司有大牛 DBA 支持,则建议使用 Mysql5.7 版本。
这里强烈建议选择兼容 PHP7 的框架,生产环境配合 PHP7+ 和 Nginx1.7+ 可以在整体的执行效率上好很多,当然如果您习惯类似 CodeIgniter 3 这类不完全兼容 PHP7 的但是轻量级的框架,那么您可以自己修改代码让其兼容 PHP7。
使用框架开发很大可能的会带来一个问题——资源浪费,因为不是每一个框架加载的模块都是我们需要的,那么我们最好移除或禁用相应模块。
比方 Magento1.* 这个庞大的产品,默认开启了 N 多个模块,很多是我们不需要的,笔者在数年的 Mangento 开发和运维中都是依靠禁用相应模块这个方法来给产品加速。
对于这种庞然大物,我也会给它加上一个 Redis 缓存中间件(推荐使用最新稳定版的偶数版本,相对稳定。
关于 redis 的性能测试报告,这里不多介绍,继续请看 推荐的链接。Mysql 数据库这块我会做一个主从复制,读写分离策略。
注:默认安装的 PHP 是使用的是一个通用配置,可以正常运行项目,但是不一定是最适合我们的配置,因此最好根据自己的实际情况来调整参数配置,首先建议使用 PHP Iniscan 工具扫描 php.ini,工具会给出一个安全配置方面的建议。
这里谈一下常见的几个配置选项,memory_limit
这个用于设定单个 PHP 进程可以使用的系统内存最大值,从系统可用性上来讲建议越大越好。
笔者曾经遇到过一个项目是 PHP 操作 Redis Set 集合,Set 集合中有 500 万 + 的数据,当时测试下来脚本无法执行。后来通过一个办法解决了,就是修改 php.ini 中的一个参数找到memory_limit
,设置到 2G 以上。问题解决了。
当然如果您的项目中每页页面使用的内存不大,建议改成小一些,这样可以承载更多的并发处理。
可以使用 top 命令或者在 PHP 脚本中调用 memory_get_peak_usage()
函数多次测试自己项目脚本,来不断修缮以得到一个比较精确的值。
配置 PHP 的 Zend OPcache 扩展,内容较多可以参考 http://laravelacademy.org/post/4396.html,就不多过多介绍了。
php.ini 文件中的 max_execution_time
用于设置单个 PHP 进程在终止之前最长可运行时间。如果您的脚本都是需要较长时间才能执行的,那么设置的大一些,不然建议设置为 5-10 秒之间。
Session 会话放在 Redis 或者 Memcached 中,这么做不仅可以减少磁盘的 IO 操作频率,还可以方便业务服务器伸缩。如果想把会话数据保存在 Memcached 中,需要做如下配置:
session.save_handler = 'memcached' session.save_path = '127.0.0.1:11211'
真实路径缓存
PHP 会缓存应用使用的文件路径,这样每次包含或导入文件时就无需不断搜索包含路径了,这个缓存叫真实路径缓存(realpath cache),如果运行的是大型的 PHP 文件(如 Composer 组件),使用了大量文件,增加 PHP 真实路径缓存的大小能得到更好的性能。
接下来讨论 GZIP 压缩的问题。(gzip 原本是文件压缩格式,扩展名为 .gz,HTTP 协议用它来改进 WEB 应用程序性能,加快 HTTP 请求返回内容的下载速度,降低网络带宽占用。)
http://tool.chinaz.com/Gzips/
使用该工具可以查看我们的网站是否开起来 Gzip 压缩功能。
该工具既可以测试 gzip 是否已开启,也可以测试页面加载的时间。针对 php 常用的环境 apache 和 nginx 我们分别来看看如何开启 gzip 压缩。
1)首先检查我们的服务器是否安装了 mod_so.c
apachectl -l
发现 mod_so.c
,ok 可以动态加模块,不用重新编译。
Apache ☞修改 httpd.conf。编辑 httpd.conf,配置mod_deflate
。对下类型文件的开启mod_deflate
。
这里需要注意的是LoadModule deflate_module
需要放在LoadModule php5_module
之后。
DeflateCompressionLevel 为压缩级别 1-9 ,数字越高说明压缩比越大,同时更占用 CPU 资源。默认建议从 4 开始设置根据实际情况调整。
2)添加到 httpd.conf 的文件尾,去掉注释
3)参考内容
这段代码在配置文件里面添加的时候要去掉注释,不然 apache 启动的时候会报错。
4)重启 apache,让配置生效
或将此内容放到网站根目录下的 .htaccess 文件中:
Nginx 下的 Gzip 开启方法:打开 nginx.conf,找到如下一段,进行修改。
gzip on; gzip_min_length 1k; gzip_buffers 4 16k; gzip_http_version 1.0; gzip_comp_level 4; gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; gzip_vary off; gzip_disable "MSIE [1-6]\.";
注解一下:
第 1 行:开启 Gzip。
第 2 行:不压缩临界值,大于 1K 的才压缩,一般不用改。
第 3 行:buffer,缓冲大小,不用改。
第 4 行:用了反向代理的话,末端通信是 HTTP/1.0,有需求的应该也不用看我这科普文了;有这句的话注释了就行了,默认是 HTTP/1.1。
第 5 行:压缩级别,1-10,数字越大压缩的越好,时间也越长,建议从 4 开始测试(根据实际项目调试)。
第 6 行:进行压缩的文件类型,缺啥补啥就行了,JavaScript 有两种写法,最好都写上吧,总有人抱怨 js 文件没有压缩,其实多写一种格式就行了。
第 7 行:跟 Squid 等缓存服务有关,on 的话会在 Header 里增加 “Vary: Accept-Encoding”,我不需要这玩意,自己对照情况看着办吧。
第 8 行:IE6 对 Gzip 不怎么友好,不给它 Gzip 了。
保存后重启 nginx,然后用 http://tool.chinaz.com/Gzips/ 这个工具测试一下是否生效了。
从图片上可以看出,没压缩和压缩之间是差了很多的。
同时可以考虑禁用 Apache 和 Nginx 内置的我们用不到的模块,这样可以释放不少的系统资源出来。通过 apachectl -M 命令列举当前服务器加载的模块。
可以到 httpd.conf 文件中把不需要的模块哪一行前面加个#号把它干掉。
通常情况下,nginx 默认的配置都不是最适合当前服务器硬件条件的,当我们提高了硬件环境后,nginx 的软配置也要做相应的修改,Nginx 提供 2 个参数来适配,分别是nginx worker_processes
和worker_connections
。
worker_processes
是表示工作进程的数量,但是这个值应该怎么设置呢?理论上来说按照 cpu 核心数的倍数来设置是没错的,但是到底是几倍呢?有的人说是有多少个核心就设置多少个进程,还有种说法是设成 cpu 核心数的两倍。这里建议设置成核心数即可。
同时不要忘了设置worker_cpu_affinity
,这个配置用于将 worker process 与指定 cpu 核绑定,降低由于多 CPU 核切换造成的寄存器等现场重建带来的性能损耗。这个配置的修改请参考:
https://nginx.org/en/docs/ngx_core_module.html#worker_cpu_affinity
接着是worker_connections
的设置,安装好 nginx 之后,其初始最大的并发数为 1024,如果你的网站访问量已经远远超过这个并发数,那你就要修改worker_connecions
这个值 ,这个值越大,并发数也有就大。不过必须根据实际情况,不能让你的 CPU 负载太高。
如果你修改提高了配置文件中的worker_connections
值,重启 nginx 后在日志里发现一个 warn 警告提示,大概的意思就是: 20000 并发连接已经超过了打开文件的资源限制:1024!
在这种情况下,我们就要修改配置文件,添加一行来解除这个限制,这就好像是 apache 中的 ServerLimit。
打开配置文件在 “event” 这行上面添加这一行:
worker_rlimit_nofile xxxxx; ####Specifies the value for maximum file descriptors that can be opened by this process.
注意:设置了这个后,你修改worker_connections
值时,是不能超过worker_rlimit_nofile
的这个值,不然又会有前面的那个 warn 提示。保存配置文件,退出重启 nginx。
如果 nginx 中worker_connections
值设置是 1024,worker_processes
值设置是 4,按反向代理模式下最大连接数的理论计算公式:最大连接数 = worker_processes * worker_connections/4
建议生产环境中worker_connections
建议值设置为 10240,运行一段时间后和优化前做一下对比,慢慢找到一个适合自己服务器的值。
如果你的网站都是静态资源,可以试试如下配置,并发可以做的更大。Nginx 指令中的优化(配置文件):
worker_processes 8;
nginx 进程数,建议按照 cpu 数目来指定,一般为它的倍数。
worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000;
为每个进程分配 cpu,上例中将 8 个进程分配到 8 个 cpu,当然可以写多个,或者将一个进程分配到多个 cpu。
worker_rlimit_nofile 102400;
这个指令是指当一个 nginx 进程打开的最多文件描述符数目,理论值应该是最多打开文件数(ulimit -n)与 nginx 进程数相除,但是 nginx 分配请求并不是那么均匀,所以最好与 ulimit -n 的值保持一致。
use epoll;
使用 epoll 的 I/O 模型,这个不用说了吧。
worker_connections 102400;
每个进程允许的最多连接数,理论上每台 nginx 服务器的最大连接数为:
worker_processes*worker_connections。 keepalive_timeout 60;
keepalive 超时时间。
client_header_buffer_size 4k;
客户端请求头部的缓冲区大小,这个可以根据你的系统分页大小来设置,一般一个请求的头部大小不会超过 1k,不过由于一般系统分页都要大于 1k,所以这里设置为分页大小。
分页大小可以用命令 getconf PAGESIZE 取得。
open_file_cache max=102400 inactive=20s;
这个将为打开文件指定缓存,默认是没有启用的,max 指定缓存数量,建议和打开文件数一致,inactive 是指经过多长时间文件没被请求后删除缓存。
open_file_cache_valid 30s;
这个是指多长时间检查一次缓存的有效信息。
open_file_cache_min_uses 1;
open_file_cache
指令中的 inactive 参数时间内文件的最少使用次数,如果超过这个数字,文件描述符一直是在缓存中打开的,如上例,如果有一个文件在 inactive 时间内一次没被使用,它将被移除。内核参数的优化:
net.ipv4.tcp_max_tw_buckets = 6000
timewait 的数量,默认是 180000。
net.ipv4.ip_local_port_range = 1024 65000
允许系统打开的端口范围。
net.ipv4.tcp_tw_recycle = 1
启用 timewait 快速回收。
net.ipv4.tcp_tw_reuse = 1
开启重用。允许将 TIME-WAIT sockets 重新用于新的 TCP 连接。
net.ipv4.tcp_syncookies = 1
开启 SYN Cookies,当出现 SYN 等待队列溢出时,启用 cookies 来处理。
net.core.somaxconn = 262144
web 应用中 listen 函数的 backlog 默认会给我们内核参数的 net.core.somaxconn 限制到 128,而 nginx 定义的NGX_LISTEN_BACKLOG
默认为 511,所以有必要调整这个值。
net.core.netdev_max_backlog = 262144
每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目。
net.ipv4.tcp_max_orphans = 262144
系统中最多有多少个 TCP 套接字不被关联到任何一个用户文件句柄上。如果超过这个数字,孤儿连接将即刻被复位并打印出警告信息。
这个限制仅仅是为了防止简单的 DoS 攻击,不能过分依靠它或者人为地减小这个值,更应该增加这个值(如果增加了内存之后)。
net.ipv4.tcp_max_syn_backlog = 262144
记录的那些尚未收到客户端确认信息的连接请求的最大值。对于有 128M 内存的系统而言,缺省值是 1024,小内存的系统则是 128。
net.ipv4.tcp_timestamps = 0
时间戳可以避免序列号的卷绕。一个 1Gbps 的链路肯定会遇到以前用过的序列号。时间戳能够让内核接受这种 “异常” 的数据包。这里需要将其关掉。
net.ipv4.tcp_synack_retries = 1
为了打开对端的连接,内核需要发送一个 SYN 并附带一个回应前面一个 SYN 的 ACK。也就是所谓三次握手中的第二次握手。这个设置决定了内核放弃连接之前发送 SYN+ACK 包的数量。
net.ipv4.tcp_syn_retries = 1
在内核放弃建立连接之前发送 SYN 包的数量。
net.ipv4.tcp_fin_timeout = 1
如果套接字由本端要求关闭,这个参数决定了它保持在 FIN-WAIT-2 状态的时间。对端可以出错并永远不关闭连接,甚至意外当机。缺省值是 60 秒。2.2 内核的通常值是 180 秒,你可以按这个设置。
但要记住的是,即使你的机器是一个轻载的 WEB 服务器,也有因为大量的死套接字而内存溢出的风险,FIN- WAIT-2 的危险性比 FIN-WAIT-1 要小,因为它最多只能吃掉 1.5K 内存,但是它们的生存期长些。
net.ipv4.tcp_keepalive_time = 30
当 keepalive 起用的时候,TCP 发送 keepalive 消息的频度。缺省是 2 小时。
假如我们的网站是博客,新闻等静态信息,我们可以把页面全部静态化、这样网站就变成了类似静态 html 页面。代码层面可以参考一下这里:
http://www.jb51.net/article/59693.htm
这里再推荐一个工具 Varnish,可以做全页面动态缓存,也可以实现负载均衡,减轻服务器压力。本次 Chat 里就不做深入探讨,感兴趣的可以参考:
http://www.drupal001.com/2011/12/varnish-drupal-basic/
也可以利用缓存模块(如 Memcache)或者模板系统(如 Smarty)进行缓存处理。我们可以缓存数据库结果和提取页面结果的方式来提升网站性能。也可以网站部署环境安装 APC 或者 Xcache 缓存能够有效提升网站运行性能和内存占用。
XCache 是一个开源的 opcode 缓存器 / 优化器 , 这意味着他能够提高您服务器上 的 PHP 性能 . 他通过把编译 PHP 后的数据缓冲到共享内存从而避免重复的编译 过程 , 能够直接使用缓冲区已编译的代码从而提高速度 . 通常能够提高您的页面生 成速率 2 到 5 倍 , 降低服务器负载。
Alternative PHP Cache (APC) 是一种对 PHP 有效的开放源高速缓冲储存器工具,它能够缓存 opcode 的 php 中间码。
这里主要从 css,js 这两个方面来下手,客户访问我们的网站,是需要将我们服务器上的各种资源全部加载到他电脑本地的,因此我们服务器里面的内容减轻一些,传输给客户的内容也就少一些。
js 和 css 中的空格,换行,注释,空行等也都是需要占用空间的,因此我们建议使用 Minify 这个 PHP 编写的库来操作。
为节省篇幅,操作的方法和介绍请参考 https://www.cnblogs.com/simaosu/p/3735784.html。
是 Nginx+Php-fpm,于是通过 find / -name nginx.conf,命令找到 Nginx.conf 这个文件。
修改 Nginx.conf 的配置如图:
重启 Nginx 后了。即不存在这个脚本超时的问题了。
因为 php 主要是与 Mysql 做交互用的多的就是优化索引、分表分库这些思想,可以多参考沈剑老师的一些文章。这里也推荐一个 Mysql 优化的文章。
另外,笔者曾经遇到过一个问题,就是 B2C 商城在商品数据较多时候遇到的加载问题、网站前后台加载的都非常的慢、最终排查下来是 mysql 的配置中有个 buffer 太低,但是需要从 Mysql 中读取出来的数据非常大。
My.cnf 中read_buffer_size = 2M
修改为read_buffer_size = 512M
(MySQL 读入缓冲区大小)、query_cache_limit = 2M
修改为 200M(指定单个查询能够使用的缓冲区大小)后解决该问题。
鉴于此我对 My.cnf 这一块做了一些优化,整体的提升了网站访问的性能。back_log = 600
,MySQL 能有的连接数量。max_connections = 1000
,MySQL 的最大连接数。
如果您有兴致读完了以上的内容并且觉得很无聊,那么笔者这里在分享一个实用的小想法吧,我们可以把业务模块,Redis 缓存模块,Mysql 数据库,图片存储,静态资源等分别放在不同的服务器上(同机房)。
这样不同的业务由不同的服务器处理,可以增加网站系统的整体性能。
Aapche 下的操作方法:
修改 httpd.conf 中 # KeepAlive: Whether or not to allow persistent connections (more than one request per connection). Set to "Off" to deactivate. Keep-Alive on # MaxKeepAliveRequests: The maximum number of requests to allow during a persistent connection. Set to 0 to allow an unlimited amount. We recommend you leave this number high, for maximum performance. MaxKeepAliveRequests 0 # KeepAliveTimeout: Number of seconds to wait for the next request from the same client on the same connection.
KeepAliveTimeout 15 # 客户端发送 HTTP 请求成功之后,Apache 将不会立刻断开 socket,而是一直监听客户端这一请求,持续时间为 15 秒,如果超过这一时间,Apache 就立即断开 socket。
注意:Apache 的版本为 2.0 以上。
默认情况下,nginx 已经自动开启了对 client 连接的 keep alive 支持(同时 client 发送的 HTTP 请求要求 keep alive)。一般场景可以直接使用,但是对于一些比较特殊的场景,还是有必要调整个别参数(keepalive_timeout
和keepalive_requests
)。
第一个参数:设置 keep-alive 客户端连接在服务器端保持开启的超时值(默认 75s);值为 0 会禁用 keep-alive 客户端连接;第二个参数:可选、在响应的 header 域中设置一个值 “Keep-Alive: timeout=time”;通常可以不用设置。
1)keepalive_timeout 默认 75s,一般情况下也够用,对于一些请求比较大的内部服务器通讯的场景,适当加大为 120s 或者 300s;
2)keepalive_requests
指令用于设置一个 keep-alive 连接上可以服务的请求的最大数量,当最大请求数量达到时,连接被关闭。默认是 100。
这个参数的真实含义,是指一个 keep alive 建立之后,nginx 就会为这个连接设置一个计数器,记录这个 keep alive 的长连接上已经接收并处理的客户端请求的数量。
如果达到这个参数设置的最大值时,则 nginx 会强行关闭这个长连接,逼迫客户端不得不重新建立新的长连接。
大多数情况下当 QPS(每秒请求数)不是很高时,默认值 100 凑合够用。但是,对于一些 QPS 比较高(比如超过 10000QPS,甚至达到 30000,50000 甚至更高) 的场景,默认的 100 就显得太低。
简单计算一下,QPS=10000 时,客户端每秒发送 10000 个请求 (通常建立有多个长连接),每个连接只能最多跑 100 次请求,意味着平均每秒钟就会有 100 个长连接因此被 nginx 关闭。
同样意味着为了保持 QPS,客户端不得不每秒中重新新建 100 个连接。因此,就会发现有大量的TIME_WAIT
的 socket 连接(即使此时 keep alive 已经在 client 和 nginx 之间生效)。
因此对于 QPS 较高的场景,非常有必要加大这个参数,以避免出现大量连接被生成再抛弃的情况,减少TIME_WAIT
。
更细致的内容我们可以在 chat 交流的时候,好好的交流,谢谢!
近期热文
《Web 安全:前端攻击 XSS 深入解析》
《300万粉丝,全国最大的线上抽奖平台,深度解析》
《高可用、高性能? 接口设计的 16 个原则》
《【钓鱼】与【反钓鱼】的技术剖析》
《快速了解 Java 9 平台模块系统》
「阅读原文」看交流实录,你想知道的都在这里