介绍使用opcache来提升php的性能
前言
php作为脚本语言,在脚本执行结束后不会保留任何状态,所以每次执行都需要重新解析脚本,增加了内存与cpu的开销。通过使用opcache可以避免每次都对脚本文件进行解析,从而提高性能。
php运行周期
php5
关闭opcache
开启opcache
步骤说明:
- Scanning(Lexing):词法分析,将php代码转换为语言片段(tokens)
- Parsing:语法分析,将tokens转换成简单而有意义的表达式(op arrays)
- Compilation:将表达式编译成opcodes
- Execution:顺次执行opcodes,每次一条,从而实现PHP脚本的功能
php7
相对于php5的解析过程,php7在parsing阶段将不直接生成op arrays而是生成ast(抽象语法树),在Compilation时先从ast生成op arrays再编译为Opcodes。让解释器与编译器进行了解耦,增加了内存的使用,但降低了执行时间。
关闭opcache
开启opcache
步骤说明:
- Scanning(Lexing):词法分析,将PHP代码转换为语言片段(tokens)
- Parsing:语法分析,将tokens转换成抽象语法树(ast)
- AST:抽象语法树,解耦parsing与compilation
- Compilation:从ast生成op arrays,再编译成Opcodes
- Execution:顺次执行Opcodes,每次一条,从而实现PHP脚本的功能
opcache原理
OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码的好处就是 省去了每次加载和解析 PHP 脚本的开销。
opcache使用
开启opcache
在php5.5.0及之后的版本,会自带opcache扩展,位置一般在extension_dir
所指向的文件夹。php默认是不开启opcache的,如果需要使用可通过如下方式开启:
#php.ini
zend_extension=opcache.so
opcache.enable=1
推荐配置
#OPcache的共享内存大小,以兆字节(m)为单位
opcache.memory_consumption=128
#用来存储临时字符串的内存大小,以兆字节为单位
opcache.interned_strings_buffer=8
#OPcache哈希表中可存储的脚本文件数量上限,真实的取值是在质数集合 { 223, 463, 983, 1979, 3907, 7963, 16229, 32531, 65407, 130987 } 中找到的第一个大于等于设置值的质数
opcache.max_accelerated_files=4000
#检查脚本时间戳是否有更新的周期,以秒为单位。 设置为 0 会导致针对每个请求, OPcache 都会检查脚本更新。
#如果opcache.validate_timestamps 配置指令设置为禁用,那么此设置项将会被忽略
opcache.revalidate_freq=60
#如果启用,则会使用快速停止续发事件。
#所谓快速停止续发事件是指依赖 Zend 引擎的内存管理模块 一次释放全部请求变量的内存,而不是依次释放每一个已分配的内存块
opcache.fast_shutdown=1
#仅针对 CLI 版本的 PHP 启用操作码缓存。 通常被用来测试和调试。
opcache.enable_cli=1
也可以修改如下2个配置。在生产环境中使用下面配置之前,必须经过严格测试。因为上述配置存在一个已知问题,它会引发一些框架和应用的异常, 尤其是在存在文档使用了备注注解的时候。
#如果禁用,脚本文件中的注释内容将不会被包含到操作码缓存文件, 这样可以有效减小优化后的文件体积。
#禁用此配置指令可能会导致一些依赖注释或注解的 应用或框架无法正常工作, 比如: Doctrine, Zend Framework 2 以及 PHPUnit
opcache.save_comments=0
#如果启用,则在调用函数 file_exists(), is_file() 以及 is_readable() 的时候, 都会检查操作码缓存,无论文件是否已经被缓存。
#如果应用中包含检查 PHP 脚本存在性和可读性的功能,这样可以提升性能。 但是如果禁用了 opcache.validate_timestamps 选项, 可能存在返回过时数据的风险
opcache.enable_file_override=1
strace跟踪分析
通过strace的跟踪日志来看opcache开启前后的php执行信息
开启opcache
第一次执行,需要打开关闭执行的文件并对文件进行分配回收内存等一系列操作
11:09:17.993927 chdir("/www/htdocs/Interview/Web/singlepage") = 0 <0.000716>
11:09:17.995499 setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={30, 0}}, NULL) = 0 <0.000076>
11:09:17.996063 fcntl(25, F_SETLK, {type=F_RDLCK, whence=SEEK_SET, start=1, len=1}) = 0 <0.000047>
11:09:17.996979 lstat("/www/htdocs/Interview/Web/singlepage/test.php", {st_mode=S_IFREG|0777, st_size=42875, ...}) = 0 <0.000727>
11:09:17.999070 lstat("/www/htdocs/Interview/Web/singlepage", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0 <0.000533>
11:09:17.999880 lstat("/www/htdocs/Interview/Web", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0 <0.000202>
11:09:18.000208 lstat("/www/htdocs/Interview", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0 <0.000150>
11:09:18.000481 lstat("/www/htdocs", {st_mode=S_IFLNK|0777, st_size=16, ...}) = 0 <0.000010>
11:09:18.000615 readlink("/www/htdocs", "/vagrant/htdocs/", 4096) = 16 <0.000011>
11:09:18.000746 lstat("/vagrant/htdocs", {st_mode=S_IFDIR|0777, st_size=12288, ...}) = 0 <0.000110>
11:09:18.000988 lstat("/vagrant", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0 <0.000064>
#打开test.php文件
11:09:18.001210 open("/vagrant/htdocs/Interview/Web/singlepage/test.php", O_RDONLY) = 21 <0.000523>
#根据文件描述符获取文件状态
11:09:18.001871 fstat(21, {st_mode=S_IFREG|0777, st_size=42875, ...}) = 0 <0.000069>
11:09:18.002075 fstat(21, {st_mode=S_IFREG|0777, st_size=42875, ...}) = 0 <0.000061>
11:09:18.002268 fstat(21, {st_mode=S_IFREG|0777, st_size=42875, ...}) = 0 <0.000057>
#建立内存映射
11:09:18.002440 mmap(NULL, 42875, PROT_READ, MAP_SHARED, 21, 0) = 0x7f9cf614a000 <0.000014>
#根据文件路径获取文件状态
11:09:18.002580 stat("/vagrant/htdocs/Interview/Web/singlepage/test.php", {st_mode=S_IFREG|0777, st_size=42875, ...}) = 0 <0.000417>
11:09:18.003577 brk(0x10fd000) = 0x10fd000 <0.000011>
11:09:18.005148 brk(0x113d000) = 0x113d000 <0.000011>
11:09:18.006018 fcntl(25, F_SETLKW, {type=F_WRLCK, whence=SEEK_SET, start=0, len=1}) = 0 <0.000013>
11:09:18.006535 fcntl(25, F_SETLK, {type=F_UNLCK, whence=SEEK_SET, start=0, len=1}) = 0 <0.000012>
#解除内存映射
11:09:18.006782 munmap(0x7f9cf614a000, 42875) = 0 <0.000019>
#关闭文件
11:09:18.006925 close(21) = 0 <0.000158>
11:09:18.007428 uname({sys="Linux", node="vagrant.localhost", ...}) = 0 <0.000009>
11:09:18.010017 brk(0x1160000) = 0x1160000 <0.000012>
11:09:18.010200 brk(0x11a0000) = 0x11a0000 <0.000009>
11:09:18.012429 chdir("/") = 0 <0.000014>
第二次及以后的执行,因为opcache中已有缓存,不需要再解析文件,所以减少了很多操作
11:09:52.324217 chdir("/www/htdocs/Interview/Web/singlepage") = 0 <0.000319>
11:09:52.324707 setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={30, 0}}, NULL) = 0 <0.000011>
11:09:52.324878 fcntl(25, F_SETLK, {type=F_RDLCK, whence=SEEK_SET, start=1, len=1}) = 0 <0.000018>
11:09:52.325063 stat("/vagrant/htdocs/Interview/Web/singlepage/test.php", {st_mode=S_IFREG|0777, st_size=42875, ...}) = 0 <0.000608>
11:09:52.325957 uname({sys="Linux", node="vagrant.localhost", ...}) = 0 <0.000012>
11:09:52.332211 chdir("/") = 0 <0.000120>
关闭opcache
每次执行,需要打开关闭执行的文件并对文件进行分配回收内存等一系列操作
#第一次
3570 14:24:34.125873 chdir("/www/htdocs/Interview/Web/singlepage") = 0 <0.000450>
3570 14:24:34.126573 setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={30, 0}}, NULL) = 0 <0.000014>
3570 14:24:34.126930 lstat("/www/htdocs/Interview/Web/singlepage/test.php", {st_mode=S_IFREG|0777, st_size=42875, ...}) = 0 <0.000874>
3570 14:24:34.128097 lstat("/www/htdocs/Interview/Web/singlepage", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0 <0.000644>
3570 14:24:34.128981 lstat("/www/htdocs/Interview/Web", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0 <0.000397>
3570 14:24:34.129558 lstat("/www/htdocs/Interview", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0 <0.000337>
3570 14:24:34.130117 lstat("/www/htdocs", {st_mode=S_IFLNK|0777, st_size=16, ...}) = 0 <0.000010>
3570 14:24:34.130271 readlink("/www/htdocs", "/vagrant/htdocs/", 4096) = 16 <0.000010>
3570 14:24:34.130402 lstat("/vagrant/htdocs", {st_mode=S_IFDIR|0777, st_size=12288, ...}) = 0 <0.000146>
3570 14:24:34.130737 lstat("/vagrant", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0 <0.000096>
3570 14:24:34.131021 open("/vagrant/htdocs/Interview/Web/singlepage/test.php", O_RDONLY) = 21 <0.020507>
3570 14:24:34.151795 fstat(21, {st_mode=S_IFREG|0777, st_size=42875, ...}) = 0 <0.000059>
3570 14:24:34.152002 fstat(21, {st_mode=S_IFREG|0777, st_size=42875, ...}) = 0 <0.000056>
3570 14:24:34.152182 fstat(21, {st_mode=S_IFREG|0777, st_size=42875, ...}) = 0 <0.000064>
3570 14:24:34.152362 mmap(NULL, 42875, PROT_READ, MAP_SHARED, 21, 0) = 0x7f9cf614a000 <0.000013>
3570 14:24:34.154280 munmap(0x7f9cf614a000, 42875) = 0 <0.000019>
3570 14:24:34.154437 close(21) = 0 <0.000116>
3570 14:24:34.154721 uname({sys="Linux", node="vagrant.localhost", ...}) = 0 <0.000008>
3570 14:24:34.158334 chdir("/") = 0 <0.000000>
#第二次及以后
3570 14:24:34.842757 chdir("/www/htdocs/Interview/Web/singlepage") = 0 <0.000752>
3570 14:24:34.843855 setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={30, 0}}, NULL) = 0 <0.000021>
3570 14:24:34.844160 open("/vagrant/htdocs/Interview/Web/singlepage/test.php", O_RDONLY) = 21 <0.001091>
3570 14:24:34.845657 fstat(21, {st_mode=S_IFREG|0777, st_size=42875, ...}) = 0 <0.000242>
3570 14:24:34.846246 fstat(21, {st_mode=S_IFREG|0777, st_size=42875, ...}) = 0 <0.000195>
3570 14:24:34.846808 fstat(21, {st_mode=S_IFREG|0777, st_size=42875, ...}) = 0 <0.000166>
3570 14:24:34.847286 mmap(NULL, 42875, PROT_READ, MAP_SHARED, 21, 0) = 0x7f9cf614a000 <0.000027>
3570 14:24:34.850917 munmap(0x7f9cf614a000, 42875) = 0 <0.000044>
3570 14:24:34.851078 close(21) = 0 <0.000159>
3570 14:24:34.851457 uname({sys="Linux", node="vagrant.localhost", ...}) = 0 <0.000011>
3570 14:24:34.856732 chdir("/") = 0 <0.000025>
opcache缓存更新
当修改了业务代码更新到线上环境后,如果不更新缓存那么实际执行的代码还是旧的,可通过如下方式进行更新。
全部更新
1.重启apache或php-fpm
重启web服务可清除掉所有缓存的opcode信息,之后处理web请求时会重新更新缓存,是最简单也是最保险的做法
2.在代码中执行opcache_reset
在不方便重启web服务的情况下,可在项目文件中嵌入一个操作opcache的脚本,在需要的时候进行调用
局部更新
1.自动清理
借助如下2个配置,让opcache定期检查脚本是否有变化,从而自动更新缓存
opcache.validate_timestamps=1
opcache.revalidate_freq=60
2.手动清理
如果知道更新的文件列表,可以使用opcache_invalidate
来局部更新,避免影响在此服务器上的所有业务。