/ php

php-performance

相关文章:

1 压力测试

压力测试的两个注意点:

  1. 不用在同一台机器上测试。
  2. 不用对线上机器测试。

测试条件:

  • 主机 4 核 3.2GHz。
  • PHP-FPM 的最大子线程为 100。

简单推演:

  • 单个请求 200ms 可以完成的话,则 1 个进程的 QPS 为 5;
  • 100 个进程的 QPS 就是 500 (100 * 5),这样的处理能力还是不错的。
  • 但是如果请求内要调用外网Http接口,像QQ、微博登录,耗时会很长,一个请求需要10s。那一个进程1秒只能处理0.1个请求,100个进程只能达到10qps,这样的处理能力就太差了。
  • 都是最大理论值
  • PS,但是实际测试 QPS 只能达到 35 左右。

ab

测试模式1:指定并发线程和总请求数: ab -n 全部请求数 -c 并发线程 测试url
测试模式2:指定并发线程和时间:ab -t 持续秒数 -c 并发线程 测试url

Server Software:        web服务器软件及版本
Server Hostname:        表示请求的URL中的主机部分名称
Server Port:            被测试的Web服务器的监听端口
 
Document Path:          请求的页面路径
Document Length:        页面大小
 
Concurrency Level:      并发数
Time taken for tests:   测试总共花费的时间
Complete requests:      完成的请求数
Failed requests:        失败的请求数,这里的失败是指请求的连接服务器、发送数据、接收数据等环节发生异常,以及无响应后超时的情况。对于超时时间的设置可以用ab的-t参数。如果接受到的http响应数据的头信息中含有2xx以外的状态码,则会在测试结果显示另一个名为“Non-2xx responses”的统计项,用于统计这部分请求数,这些请求并不算是失败的请求。
Write errors:           写入错误
Total transferred:      总共传输字节数,包含http的头信息等。使用ab的-v参数即可查看详细的http头信息。
HTML transferred:       html字节数,实际的页面传递字节数。也就是减去了Total transferred中http响应数据中头信息的长度。
Requests per second:    每秒处理的请求数,服务器的吞吐量,等于:Complete requests / Time taken for tests
Time per request:       用户平均请求等待时间
Time per request:       服务器平均处理时间
Transfer rate:          平均传输速率(每秒收到的速率)。可以很好的说明服务器在处理能力达到限制时,其出口带宽的需求量。

平均响应时间的 2/5 原则:

  • 2 秒内给用户响应是“非常有吸引力”的用户体验;
  • 在 5 秒内响应是“比较不错”的用户体验;

性能指标

  • CPU ,占用率。
  • 内存,占用率,换页数等。
  • I/O ,读写请求数、读写量等。
  • 带宽,进站出站带宽占用率。

并发线程=并发用户

ApacheBench(ab)

ab -n 1000 -c 50 http://example.com/

这里我们使用 50 个并发线程,执行 1000 个请求:

  • -n 指定请求总数 1000
  • -c 并发线程(用户)50,这个值等同于 PHP-FPM 的 pm.max_children
  • 注意结尾的斜线,如果以斜线结尾,ab 只会执行测试

关注指标:

  • Time taken for tests,测试耗时。
  • Failed request,失败的请求数。
  • Requests per second,每秒处理请求数。

并发线程 = 并发用户

Q:如何设定并发线程?
A:指定为 PHP-FPM 的 pm.max_children

Q:主要考察指标是什么?
A:在 Failed requests 为 0 的前提下 Requests per second 越大越好。

Q:提升Requests per second的手段有哪些?
A:缓存字节码(OPcache)、缓存变量(APCu)、会话存储使用 memcached 或 redis。

请求预热:

ab -n 100 -c 100 http://example.com/

完成后紧接着进行正式压测:

ab -n 10000 -c 100 http://example.com/

预热的 QPS 为 55,正式压测的 QPS 能达到 230。

PHP-FPM 进程池最多 100 个进程时

5个并发,请求 10 次:

100 个并发,请求 2000 次

➜  ~ ab -c 100 -n 2000 http://192.168.1.88/

Completed 200 requests
    Completed 400 requests
Completed 600 requests
apr_pollset_poll: The timeout specified has expired (70007)
Total of 684 requests completed

这时候,mysql 扛不住了,报错:Too many connections。这个时候 PHP 还没有达到瓶颈,可以引入 DB 缓存,Redis。

PHP-FPM 进程池最多 80 个进程时

100 个并发,请求 2000 次

➜  ~ ab -c 100 -n 2000 http://192.168.1.88/

Document Length:        52776 bytes

Concurrency Level:      100
Time taken for tests:   813.098 seconds
Complete requests:      2000
Failed requests:        219
   (Connect: 0, Receive: 0, Length: 219, Exceptions: 0)
Non-2xx responses:      219
Requests per second:    2.46 [#/sec] (mean)

这时候 mysql 没有挂掉。

siege

sudo yum -y install siege

使用siege -h 或者siege --help查看帮助信息,这几个是我们常用的:

  • -c 并发数量
  • -r 重复的次数
  • -t 测试时间

开始测试:

100 个并发用户,测试 30 秒 ( 示例 ):siege -c 100 -t 30s http://127.0.0.1/info.php
255 个并发用户,测试 5 分钟 ( 示例 ):siege -c 255 -t 5 http://127.0.0.1/info.php

siege -c 100 -t 30s http://127.0.0.1/info.php

Transactions:		         100 hits
Availability:		      100.00 %
Elapsed time:		       29.62 secs
Response time:		        3.87 secs
Transaction rate:	        3.38 trans/sec
Concurrency:		       13.08
Successful transactions:         150
Failed transactions:	           0
Longest transaction:	        4.33
Shortest transaction:	        2.10

2 系统测试

2.1 INI 设置

php.ini 文件

PHP 解释器在 php.ini 文件中的配置调优。

  • PHP-FPM 的配置文件:
  • PHP 命令行的配置文件:/etc/php.ini

附属的扩展配置文件/etc/php.d/

  • apcu.ini
  • bcmath.ini
  • opcache.ini
  • pdo.ini
  • pdo_mysql.ini
  • mbstring.ini
  • curl.ini

检查 php.ini 的安全性

安装 PHP Iniscan 工具,扫描 php.ini 文件,检查是否使用了安全方面的最佳实践。

composer global require psecio/iniscan

If you add ~/.config/composer/vendor/bin to your $PATH environment variable, you can use these tools directly.

$HOME/.config/composer/vendor/bin:$PATH

然后执行命令:

iniscan scan --path=/etc/php.ini

或者只显示需要改善的建议:

iniscan scan --path=/etc/php.ini --fail-only

session.name = XID - Name of the session (used as cookie name).
session.cookie_httponly = 1 - Setting session cookies to 'http only' makes them only readable by the browser
session.cookie_secure = 1 - Cookie secure specifies whether cookies should only be sent over secure connections.
allow_url_fopen = Off - Whether to allow the treatment of URLs (like http:// or ftp://) as files.注意,这里的 Composer install 需要启用,所以需要为 allow_url_fopen = On

内存分配

运行 PHP 时我最关心的是每个 PHP 进程要使用多少内存。

定义 PHP 能使用的内存大小。

php.ini 文件中的 memory_limit 设置用于设定一个 PHP 脚本可以使用的系统内存最大值,这有助于防止编写的脚本占用服务器上所有可用内存,防止其他服务无法正常运行。

memory_limit = 20M - 默认为 128M:这个值的设置由可用的系统内存决定。

决定给 PHP 分配多少内存,以及能负担得起多少个 PHP-FPM 进程时,我会问自己以下几个问题:

  • 一共能给 PHP 多少内存?
    • 2GB内存,如果这台机器同时运行 nginx、mysql 或 memcache,那么给 PHP 分 512MB 就足够了。
  • 单个 PHP 进程平均消耗多少内存?
    • 以单个 PHP 进程消耗 20MB 内存。
  • 能负担得起多少个 PHP-FPM 进程?
    • 假设我给 PHP 分配了 512MB 内存,每个 PHP 进程平均消耗 20MB 内存。我拿内存总量除以每个 PHP 进程消耗的内存量,从而确定我能负担得起 20 个 PHP-FPM 进程。这个是个估值,应该通过压力测试验证。

对大多数中小型PHP应用来说,每个PHP进程要使用 15-20MB 内存(具体用量可能有差异)。假设我们使用的设备为这个 PHP-FPM 进程池分配了 512MB 可用内存,那么可以设置进程池的pm.max_children = 25。即 (512MB总内存)/(每个进程使用了20MB)= 25.6 个进程

APCu 设置

对于每一个请求,PHP 进行代码解析,编译成操作码(或令牌),然后这些令牌随即传递给 Zend 引擎以便执行。操作码缓存(opcode cache) 将会在首次运行后保存操作码,让 Zend 引擎在随后的请求中继续使用它们。

而 APC 本来拥有:

  • 操作码缓存(opcode cache)。被 opcache 取代。
  • 用户变量缓存,可以存储k/v对,类似memcache。被 APCu 取代。

PHP5.5 以后,opcache 将代替 APC 做为 PHP 加速的功能,也就是代替其系统缓存的位置。并将用户缓存功能独立出来,开启新的组件,这个组件名称叫做 APCu。

版本情况:

  • APC - 最新版本为 3.1.13,更新日期为 2012年。
  • APCu - 最新版本为 5.1.11,更新日期为 2018年。

我们需要安装 apc (Alternative PHP Cache):

pecl install apc

然后,编辑 php.ini 添加:

extension=apc.so
apc.enable_cli = 1
apc.shm_size=32M 
apc.enable_cli=1

接着重新启动 PHP(也就是 PHP-FPM),然后通过压力测试来对比效果。

缓存对比:

  • 分布式:redis/memcache:如果要做分布式存储可以使用,否则不推荐,因为redis/memcache需要tcp通信,即便是本地也需要unix domain socket通信,其效率远不如其于共享内存的 APCu。
  • 集中式:APCu 这个选项可以认为是集中式应用程序环境中 (例如:单一服务器,没有独立的负载均衡器等)最快的缓存方案。

Zend OPcache 设置

字节码缓存能存储预先编译好的 PHP 字节码。这意味着,请求 PHP 脚本时,PHP 解释器不用每次都读取、解析和编译 PHP 代码。PHP 解释器会从内存中读取之前编译好的字节码,然后立即执行。这样能节省很多时间,极大地提升应用的性能。

确定分配多少内存后,我会配置 PHP5.5.0+ 中内置的 Zend OPcache 扩展。这个扩展用于缓存操作码。

/etc/php.d/opcache.ini

opcache.validate_timestamps=0
opcache.revalidate_freq=0
opcache.memory_consumption=128
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=4000
  • opcache.memory_consumption=128 - 为操作码缓存分配的内存量(MB),默认值为 128。
    • 分配的内存量应该够保存应用中所有 PHP 脚本编译得到的操作码。如果是小型 PHP 应用,脚本数较少,可以设置为较低的值,例如 16MB;如果是大型 PHP 应用,有很多脚本,那就使用较大的值,例如 128MB。
  • opcache.interned_strings_buffer=16 - 用来存储驻留字符串的内存量(MB),默认值为 8。
  • opcache.max_accelerated_files=4000 - 操作码缓存中最多能存储多少个脚本,默认值 4000.
    • 这个值一定要比 PHP 应用中的文件数要大。
  • opcache.validate_timestamps=1 - 是否检查 PHP 脚本的内容变化。
    • 检查的时间间隔由 opcache.revalidate_freq 设置决定。
    • 生产环境中设置为 0,不检查 PHP 脚本变化,必须自己手动重启 PHP-FPM 服务清除缓存的操作码。
  • opcache.revalidate_freq=0 - 检查 PHP 脚本的内容变化的频率。
    • 开发环境中设置为 0,每次请求时都重新验证 PHP 脚本变化。如果有变化,PHP 会重新编译并缓存。
    • 生产环境中因为永远不会启用检查,所以这个设置对生产环境没有任何意义。
sudo systemctl restart php-fpm.service

关于:

Cache hits 	576 
Cache misses 	251 

实测:调优过 opcache.ini 配置后,Requests per second 能提升 3 倍!

开发环境要关闭 opcache

修改配置 sudo vi /etc/php.d/opcache.ini 禁用 opcahce :

; Determines if Zend OPCache is enabled
opcache.enable=0

重启 php-fpm:

sudo systemctl restart php-fpm.service 

重新 debug 就很简单了。

文件上传

file_uploads = 1
upload_max_filesize = 10M
max_file_uploads = 3

默认情况下,PHP 允许在单次请求中上传 20 个文件,这里设置为 3 个。

upload_max_filesize 这个值不要太大,否则 Web 服务器会抱怨 HTTP 请求的 body 太大,或者请求超时。

如果需要上传非常大的文件,Web 服务器的配置也要做相应调整,除了在 php.ini 文件中设置之外,还需要调整 nginx 虚拟主机配置中的 client_max_body_size 设置。

最长执行时间

max_execution_time = 5,默认为 30秒,推荐改为 5秒。

还可以在 PHP 脚本中调用 set_time_limit() 函数覆盖这个设置。

你可能会问,如果 PHP 脚本需要运行更长的时间怎么办?

答案是,PHP 脚本不能长时间运行。PHP 运行的时间越长,用户等待相应的时间就越长。如果有长时间运行的任务(例如,调整图像尺寸或者生成报告),需要在单独的职程中运行。

建议:我会使用 PHP 中的 exec() 函数调用 bash 的 at 命令。这个命令的作用是派生单独的非阻塞进程,不耽误当前的 PHP 进程。使用 PHP 中的 exec() 函数时,要使用 escapeshellarg() 函数转义 shell 参数。

假设我们要生成报告,并把结果制作成 PDF 文件。这个任务可能要花 10 分钟才能完成,而我们肯定不想让 PHP 请求等待 10分钟。我们应该单独编写一个 PHP 文件,假设将其命名为 create-report.php,让这个文件运行 10分钟,最后生成报告。

<?php
exec('echo "create-report.php" | at now');
echo '报告生成中……';

create-report.php 脚本在单独的后台进程中运行,运行完毕后可以更新数据库,或者通过email 发送给用户。可以看出,我们完全没有理由让长时间运行的任务拖延 PHP 主脚本,影响用户体验。

建议:如果发现自己派生了很多后台进程,或许最后使用专门的作业队列(Job Queue)。

优化 session

缓冲输出

如果在较少的块中发送更多的数据,而不是在较多的数据块中发送较少的数据,那么网络的效率会更高。也就是说,在较少的片段中把内容传递给访问者的浏览器,能减少 HTTP 请求总数。

因此,我们要让 PHP 缓冲输出。默认情况下,PHP 已经启用了输出缓冲功能(不过没在命令行启用)。PHP 缓冲 4096 字节的输出之后才会把其中的内容发送给 Web 服务器。下面是推荐的 php.ini 配置:

output_buffering = 4096
implicit_flush = Off

真实路径缓存

PHP 会缓存应用使用的文件路径,这样每次 require 或 include 文件时就无需不断搜索文件路径了。这个缓存叫做真实路径缓存(realpath cache)。如果运行的是大型 PHP 文件(例如 Drupal 和 Composer组件等),使用了大量文件,增加 PHP 真实路径缓存的大小能得到更好的性能。

默认大小为 16K。这个缓存所需的准确大小不容确定,不过可以使用一个小技巧:

  • 首先,设为特别大的值,例如 256k
  • 然后再一个 PHP 脚本的末尾加上 print_r(realpath_cache_size()); 输出真实路径缓存的真正大小为 41803,即这里用了 42k,但是因为这个值不是固定的,会有波动,所有真是的值设为 64k
  • 最后,将 realpath_cache_size 的值设置为打印输出显示的实际大小 64k
realpath_cache_size = 64k

3 数据库

4 文件系统

5 程序概要分析