PHP码农在Golang压力下的生存之道-PHP性能优化实践
随着国内Golang的火爆,phper的生存压力越来越大,在一次内部技术讨论中,gopher甚至提出,要什么php,写php的全部开掉,唉,码农何苦为难码农。
本文试图寻找一种有效实践,减少php web程序和golang之间的性能差距,摆脱php在公司往后只能写后台的悲惨命运。
做优化的思路
1、了解php语言特性
2、了解php的执行过程
3、压测分析性能
语言特性
PHP被称为脚本语言或解释型语言,它没有被直接编译为机器指令,而是编译为一种中间代码的形式,无法直接在CPU上执行。 所以PHP的执行需要在进程级虚拟机上(见Virtual machine中的Process virtual machines,下文简称虚拟机)。
PHP语言,包括其他的解释型语言,其实是一个跨平台的被设计用来执行抽象指令的程序。PHP主要用于解决WEB开发相关的问题。
诸如Java, Python, C#, Ruby, Pascal, Lua, Perl, Javascript等编程语言所编写的程序,都需要在虚拟机上执行。虚拟机可以通过JIT编译技术将一部分虚拟机指令编译为机器指令以提高性能。PHP未来有可能加入JIT支持。
使用解释型语言的优点:
- 代码编写简单,能够快速开发
- 自动的内存管理
- 抽象的数据类型,程序可移植性高
缺点:
- 无法直接地进行内存管理和使用进程资源
- 比编译为机器指令的语言速度慢:通常需要更多的CPU周期来完成相同的任务(JIT试图缩小差距,但永远不能完全消除)
- 抽象了太多东西,以至于当程序出问题时,许多程序员难以解释其根本原因
PHP的生命周期
Zend虚拟机分为两大部分:
- 编译:将PHP代码转换为虚拟机指令(OPCode)
- 执行:执行生成的虚拟机指令
zend执行过程
1 2 3 4 |
词法分析(zend_language_scanner),将PHP代码转换为语言片段(Tokens) 语法分析(zend_language_parser)将Tokens转换成简单而有意义的表达式 编译(compiler),将表达式编译成Opocdes,返回zend_op_array指针 Zend Engine(zend_vm_execute),顺次执行Opcodes,每次一条, 根据传入的zend_op_array指针,执行opcode并将结果返回输出 |
---|
解释型语言性能问题也就是因为每次执行脚本,上述过程都会重复执行。因此,也就出现了APC, xcache, eAccelerator等缓存,不过现在官方主推的是opcache
什么是opcode缓存
当解释器完成对脚本代码的分析后,便将它们生成可以直接运行的中间代码,也称为操作码(Operate Code,opcode)。Opcode cache的目地是避免重复编译,减少CPU和内存开销。如果动态内容的性能瓶颈不在于CPU和内存,而在于I/O操作,比如数据库查询带来的磁盘I/O 开销,那么opcode cache的性能提升是非常有限的。也就是opcode cache能带来CPU和内存开销的降低
APC, xcache, eAccelerator,opcache 使用共享内存进行存储,并且可以直接从中执行文件,而不用在执行前“反序列化”代码
PHP-FPM的生命周期
模块初始化(master)
请求初始化 (worker)
执行脚本(worker)
请求关闭(worker)
模块关闭(master关闭)
由以上我们可以看到 php的优化思路:1、使用opcache去掉php生命周期的词法分析、语法分析、opcode生成环节 2、提升zend虚拟机性能 3、减少worker每次请求初始化的消耗
我们作为web开发者还能做什么优化呢?
1、使用轻量级框架
2、引入协程,解决多进程的调度消耗问题,解决IO阻塞问题
性能实验
几种框架比较压测
首先使用php内置web server做个测试
四核16G内存虚拟机,golang使用4个核,php使用单核
<!-- p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 14.0px Menlo; background-color: #cff5d6} span.s1 {font-variant-ligatures: no-common-ligatures} -->
/usr/local/php-7.0.11/bin/php -S 10.20.1.12:8000 router.php -c php.ini
php.ini:
[opcache]
zend_extension = opcache.so
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=400000
opcache.revalidate_freq=600
opcache.validate_timestamps=0
opcache.fast_shutdown=1
opcache.enable_cli=1
opcache.enable=1
[vld]
extension=vld.so
router.php
<?php
echo '{"errno":0,"errmsg":"success","data":"e"}';
siege -c 100 -r 10000 "http://10.20.1.12:8000/" -b
压测结果:
Transactions: 1000000 hits
Availability: 100.00 %
Elapsed time: 77.15 secs
Data transferred: 39.10 MB
Response time: 0.01 secs
Transaction rate: 12961.76 trans/sec
Throughput: 0.51 MB/sec
Concurrency: 97.56
Successful transactions: 1000000
Failed transactions: 0
Longest transaction: 0.24
Shortest transaction: 0.00
可以认为 php的虚拟机执行效率是可以的 ,使用golang的原生http模块echo helloworld 在24000 trans/sec 。
php的cpu利用率在100%,golang的利用率在 200% (设置了 runtime.GOMAXPROCS(4) 并没达到400%)
使用php-fpm方式挂载到nginx中去访问,直接请求index.php 并echo结果 trans: 7300 trans/sec
使用yaf controller方式 ,trans:5000 trans/sec , 损失了 32%的性能 ,略微尴尬
zan framework 3570 trans/sec
swoole!几年前测试,性能很不怎么样,如今,php7+swoole 25000 trans/sec 跟golang毫不逊色啊 !
dev压测
dev02启动一个qps 2w+的curl接口
dev03 4核16G机器,分别跑yaf 、golang、es(EasySwoole,之后换成yaf+swoole,性能差不多)、lua ,执行空接口、访问11的redis、访问dev02的curl接口
yaf 开启opcode,使用线上dynamic php-fpm配置,
es worker数设置为40(测试4核 40最佳)
案例 |
并发 |
请求数 |
失败数 |
QPS |
性能指数(golang为基准) |
---|---|---|---|---|---|
yaf 空接口 |
200 |
100w |
0 |
7013.11 trans/sec |
24.5% |
go 空接口 |
200 |
100w |
0 |
28645.09 trans/sec |
100% |
es空接口 |
200 |
100w |
0 |
27285.13 trans/sec |
95% |
yaf curl |
200 |
100w |
0 |
3475.33 trans/sec |
26.3% |
go curl |
200 |
100w |
0 |
13227.51 trans/sec |
100% |
es curl |
200 |
100w |
0 |
11178.18 trans/sec |
84.5% |
lua curl |
200 |
100w |
0 |
12528.19 trans/sec |
94.7% |
yaf redis read |
200 |
100w |
0 |
5389.09 trans/sec |
32.6% |
go redis read |
200 |
100w |
0 |
16550.81 trans/sec |
100% |
es redis read |
200 |
100w |
0 |
13917.88 trans/sec |
84% |
线上压测
当CPU提升到8核?
eris3v 压测 eris6v 的 yaf接口(access_log off ,减小写日志影响)
1、空接口 siege -c 200 -r 4000 "10.110.18.72:8360/main/example" -b -q Transaction rate: 20356.23 trans/sec 2、curl一次( lib httprequest写log) siege -c 200 -r 4000 "10.110.18.72:8360/main/curl" -b -q Transaction rate: 7560.01 trans/sec 3、curl一次( lib httprequest不写log) siege -c 200 -r 4000 "10.110.18.72:8360/main/curl" -b -q Transaction rate: 13807.39 trans/sec 4、读一次redis( zscore) siege -c 200 -r 4000 "10.110.18.72:8360/member/in?rid=30510982&groupid=10000" -b -q Transaction rate: 11677.13 trans/sec
5、读两次redis, 把测试3的逻辑在controller中执行两次 siege -c 200 -r 4000 "10.110.18.72:8360/member/in?rid=30510982&groupid=10000" -b -q Transaction rate: 8463.79 trans/sec
线上环境压测发现,8核16G机器下,yaf+php-fpm的性能有大幅提升,空接口可以跑满8个核,如果不经过nginx日志,性能和swoole、golang仅有10%~20%左右差距,swoole受限于master调度,无法跑满8个核,只有一个核负载100%,其他空闲较大(多开master?使用层面暂时无法解决)。siege 不开启 -q quiet模式,在使用V**或wifi情况下,有可能因为压测机到本机的同步output速度,影响压测结果,建议关闭。
性能分析
实验
问题简单化一下,我们测试一下在dev环境只有一个worker 只能利用单核情况下 原生php-fpm、php-fpm+yaf路由、 swoole+yaf的空跑接口性能差异(需要开启opcache)。
1、新建yaf项目
2、 使用 https://github.com/LinkedDestiny/swoole-yaf新建swoole+yaf项目,使用yaf作为路由
其中 yaf项目 可更改 src/public/index.php 只echo "hello world" ,不启动yaf 作为测试1 ,启动yaf 执行MainController中的exampleAction作为测试2,swoole+yaf项目作为测试3
siege -c 300 -r 3000 "10.20.1.13/Main/example" -b -q
90w请求 |
备注 |
||
---|---|---|---|
测试1: php-fpm |
5990 trans/sec |
||
测试2: yaf |
2687 trans/sec |
||
测试3: swoole+yaf |
18382 trans/sec |
过nginx代理则变为8980 trans/sec,日志是性能杀手 golang也是一样,性能损失50% |
分析
分别执行一次请求,使用strace 分别跟踪master和worker执行,
sudo strace -p 5450 -s 10000 -T ,具体调用操作见附录
1) php-fpm
worker执行了24次系统调用 ,master没有操作,只是监控worker状态及重启
worker工作周期:
accept收到请求
1、fcgi_accept_request() 解析请求 fcgi_read_request() -> safe_read() ,调用了5次系统调用read() 才完成了fastcgi协议的解析
然后进入获取请求信息阶段,将请求的method、query string、request uri等信息保存worker进程的fpm_scoreboard_proc_s结构中
2、php_request_startup() 请求初始化
3、php_execute_script() 进入FPM_REQUEST_EXECUTING阶段,完成php脚本编译,执行操作 ,这个阶段虽然有opcache(已经对文件执行了open操作)仍然会做 getcwd chdir stat等系统操作去查找文件,然后执行 zend_execute_scripts ( zend_execute(op_array, retval); ) , write 出结果
4、php_request_shutdown() shutdown recvfrom 从主进程接收两次响应包, close req文件描述符 ,这又是四次系统调用
2)yaf
执行了38次系统调用, 24次是和fpm相同的 ,会额外stat open一次 app.ini文件,stat Bootstrap.php、 include文件和controller文件,并做内存页映射操作
3) swoole+yaf
只执行了5次系统调用,发挥了常驻进程的优势, 其他系统调用在初始化时即完成,之后的请求只需要master accept 和epoll出请求, worker read ,在用户态处理后 sendto master即可完成,很简洁。
结论
swoole+yaf因为是常驻进程,初始化只需要一次,在系统调用层面消耗非常少,单worker进程性能就非常强悍,但在多核多进程模型下,yaf和php-fpm又能依托多核硬件,追平性能差异,所以在机器预算有限情况下,比如1~4核,使用swoole+yaf ,相比yaf能大幅提升性能。在大部分web高性能接口场景,使用yaf或swoole就能够满足性能要求,且开发效率很高,并不必须要用golang。对于需要多线程、异步、长连接或者中间件、可靠分布式存储服务的场景还是选择golang比较靠谱,用swoole也有学习成本,不如只是用它最稳定成熟的地方。
附录
php-fpm系统调用:
times({tms_utime=0, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 1031139243 <0.000072> poll([{fd=4, events=POLLIN}], 1, 5000) = 1 ([{fd=4, revents=POLLIN}]) <0.000021> read(4, "11 1 10 ", 8) = 8 <0.000018> read(4, " 1 ", 8) = 8 <0.000017> read(4, "14 122762 ", 8) = 8 <0.000017> read(4, " 177SCRIPT_FILENAME/home/shenguanpu/devspace/test_yaf/src/public/index.phpf QUERY_STRING163REQUEST_METHODGETf CONTENT_TYPE16 CONTENT_LENGTHvnSCRIPT_NAME/index.phpvrREQUEST_URI/Main/examplef27DOCUMENT_URI/index.php/Main/exampler-DOCUMENT_ROOT/home/shenguanpu/devspace/test_yaf/src/public1710SERVER_PROTOCOLHTTP/1.1217GATEWAY_INTERFACECGI/1.117fSERVER_SOFTWAREnginx/1.12.1vnREMOTE_ADDR10.20.1.19v5REMOTE_PORT35085vnSERVER_ADDR10.20.1.13v2SERVER_PORT80v34SERVER_NAMEshenguanpu.test_yaf.panda.tv21 HTTP_X_REQUEST_ID173REDIRECT_STATUS200trPATH_INFO/Main/example17:PATH_TRANSLATED/home/shenguanpu/devspace/test_yaf/src/public/Main/examplet34HTTP_HOSTshenguanpu.test_yaf.panda.tv17vHTTP_USER_AGENTcurl/7.44.0v3HTTP_ACCEPT*/* ", 704) = 704 <0.000017> read(4, "14 1 ", 8) = 8 <0.000034> setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={30, 0}}, NULL) = 0 <0.000037> rt_sigaction(SIGPROF, {0x798430, [PROF], SA_RESTORER|SA_RESTART, 0x3490c326a0}, {0x798430, [PROF], SA_RESTORER|SA_RESTART, 0x3490c326a0}, 8) = 0 <0.000027> rt_sigprocmask(SIG_UNBLOCK, [PROF], NULL, 8) = 0 <0.000027> getcwd("/home/shenguanpu/devspace/test_yaf", 4095) = 35 <0.000022> chdir("/home/shenguanpu/devspace/test_yaf/src/public") = 0 <0.000049> fcntl(3, F_SETLK, {type=F_RDLCK, whence=SEEK_SET, start=1, len=1}) = 0 <0.000030> stat("/home/shenguanpu/devspace/test_yaf/src/public/index.php", {st_mode=S_IFREG|0775, st_size=221, ...}) = 0 <0.000025> chdir("/home/shenguanpu/devspace/test_yaf") = 0 <0.000030> times({tms_utime=0, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 1031139243 <0.000017> setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={0, 0}}, NULL) = 0 <0.000017> fcntl(3, F_SETLK, {type=F_UNLCK, whence=SEEK_SET, start=0, len=0}) = 0 <0.000021> write(4, "16 1 /1 Content-type: text/html; charset=UTF-8rnrntest1 13 1 10 ", 72) = 72 <0.000086> shutdown(4, SHUT_WR) = 0 <0.000023> recvfrom(4, "15 1 ", 8, 0, NULL, NULL) = 8 <0.000027> recvfrom(4, "", 8, 0, NULL, NULL) = 0 <0.000024> close(4) = 0 <0.000069> setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={0, 0}}, NULL) = 0 <0.000024> |
---|
yaf比php-fpm多了14次系统调用
stat("/home/shenguanpu/devspace/test_yaf/src/public/index.php", {st_mode=S_IFREG|0775, st_size=207, ...}) = 0 <0.000019> (yaf内操作开始) stat("/home/shenguanpu/devspace/test_yaf/src/conf/app.ini", {st_mode=S_IFREG|0775, st_size=364, ...}) = 0 <0.000030> open("/home/shenguanpu/devspace/test_yaf/src/conf/app.ini", O_RDONLY) = 5 <0.000023> ioctl(5, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0x7ffd3bf1f2d0) = -1 ENOTTY (Inappropriate ioctl for device) <0.000016> fstat(5, {st_mode=S_IFREG|0775, st_size=364, ...}) = 0 <0.000015> mmap(NULL, 396, PROT_READ, MAP_PRIVATE, 5, 0) = 0x7f7d06803000 <0.000022> fstat(5, {st_mode=S_IFREG|0775, st_size=364, ...}) = 0 <0.000015> mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f7d06802000 <0.000017> lseek(5, 0, SEEK_CUR) = 0 <0.000016> munmap(0x7f7d06803000, 396) = 0 <0.000022> close(5) = 0 <0.000017> munmap(0x7f7d06802000, 4096) = 0 <0.000014> stat("/home/shenguanpu/devspace/test_yaf/src/Bootstrap.php", {st_mode=S_IFREG|0775, st_size=2392, ...}) = 0 <0.000020> stat("/home/shenguanpu/devspace/test_yaf/src/library/XLogKit.php", {st_mode=S_IFREG|0664, st_size=1933, ...}) = 0 <0.000024> stat("/home/shenguanpu/devspace/test_yaf/src/controllers/Main.php", {st_mode=S_IFREG|0664, st_size=962, ...}) = 0 <0.000021> (往下回到fpm) chdir("/home/shenguanpu/devspace/test_yaf") = 0 <0.000022> |
---|
swoole+yaf
1、master accept4(3, {sa_family=AF_INET, sin_port=htons(22468), sin_addr=inet_addr("10.20.1.19")}, [16], SOCK_CLOEXEC|SOCK_NONBLOCK) = 9 <0.000024> epoll_ctl(8, EPOLL_CTL_ADD, 9, {EPOLLOUT, {u32=9, u64=9}}) = 0 <0.000019> accept4(3, 0x7ffdf04ad430, [16], SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN (Resource temporarily unavailable) <0.000019> 2、worker read(4, "3 b np3 GET /Main/example?test=1 HTTP/1.1rnHost: 10.20.1.13:9601rnUser-Agent: curl/7.44.0rnAccept: */*rnrn", 8192) = 110 <0.000029> sendto(4, "3 252 377 HTTP/1.1 200 OKrnServer: swoole-http-serverrnContent-Type: text/htmlrnConnection: keep-alivernDate: Fri, 26 Jan 2018 08:17:44 GMTrnContent-Length: 17rnrnthis is a swoole!", 182, 0, NULL, 0) = 182 <0.000029> |
---|
参考文献:
fastcgi-protocol-specification
我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan
- php概述
- php教程
- php环境搭建
- PHP书写格式
- php变量
- php常量
- PHP注释
- php数组
- php字符串 string
- PHP整型 integer
- PHP浮点型 float
- php布尔型
- php数据类型之数组
- php数据类型之对象
- php数据类型之null
- php数据类型之间的转换
- php运算符
- php表达式
- PHP循环控制
- PHP流程控制
- php函数
- php全局变量
- PHP魔术变量
- php命名空间
- php 日期
- PHP包含文件
- php文件
- PHP 文件上传
- php Cookies
- php Sessions
- php email
- php安全email
- php错误处理
- PHP异常处理
- php过滤器
- PHP 高级过滤器
- php json
- php 表单
- PHP MySQL 简介
- PHP 连接 MySQL
- php创建数据库
- php 创建表
- php mysq 插入数据
- PHP MySQL 插入多条数据
- PHP MySQL 预处理语句
- php mysql 读取数据
- php mysql where
- PHP MySQL Order By
- PHP MySQL Update
- PHP MySQL Delete
- php ODBC