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使用單核

/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模式,在使用vpn或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即可完成,很簡潔。

master

worker

結論

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,  "\1\1\0\1\0\10\0\0" , 8)         = 8 <0.000018>
 
read(4,  "\0\1\0\0\0\0\0\0" , 8)          = 8 <0.000017>
 
read(4,  "\1\4\0\1\2\276\2\0" , 8)        = 8 <0.000017>
 
read(4,  "\0177SCRIPT_FILENAME/home/shenguanpu/devspace/test_yaf/src/public/index.php\f\0QUERY_STRING\16\3REQUEST_METHODGET\f\0CONTENT_TYPE\16\0CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\v\rREQUEST_URI/Main/example\f\27DOCUMENT_URI/index.php/Main/example\r-DOCUMENT_ROOT/home/shenguanpu/devspace/test_yaf/src/public\17\10SERVER_PROTOCOLHTTP/1.1\21\7GATEWAY_INTERFACECGI/1.1\17\fSERVER_SOFTWAREnginx/1.12.1\v\nREMOTE_ADDR10.20.1.19\v\5REMOTE_PORT35085\v\nSERVER_ADDR10.20.1.13\v\2SERVER_PORT80\v\34SERVER_NAMEshenguanpu.test_yaf.panda.tv\21\0HTTP_X_REQUEST_ID\17\3REDIRECT_STATUS200\t\rPATH_INFO/Main/example\17:PATH_TRANSLATED/home/shenguanpu/devspace/test_yaf/src/public/Main/example\t\34HTTP_HOSTshenguanpu.test_yaf.panda.tv\17\vHTTP_USER_AGENTcurl/7.44.0\v\3HTTP_ACCEPT*/*\0\0" , 704) = 704 <0.000017>
 
read(4,  "\1\4\0\1\0\0\0\0" , 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,  "\1\6\0\1\0/\1\0Content-type: text/html; charset=UTF-8\r\n\r\ntest1\0\1\3\0\1\0\10\0\0\0\0\0\0\0\0\0\0" , 72) = 72 <0.000086>
 
shutdown(4, SHUT_WR)                    = 0 <0.000023>
 
recvfrom(4,  "\1\5\0\1\0\0\0\0" , 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\0\0\0b\0\0\0\np\3\0GET /Main/example?test=1 HTTP/1.1\r\nHost: 10.20.1.13:9601\r\nUser-Agent: curl/7.44.0\r\nAccept: */*\r\n\r\n" , 8192) = 110 <0.000029>
 
sendto(4,  "\3\0\0\0\252\0\0\0\0\377\0\0HTTP/1.1 200 OK\r\nServer: swoole-http-server\r\nContent-Type: text/html\r\nConnection: keep-alive\r\nDate: Fri, 26 Jan 2018 08:17:44 GMT\r\nContent-Length: 17\r\n\r\nthis is a swoole!" , 182, 0, NULL, 0) = 182 <0.000029>

 

 

參考文獻:

php內核剖析

Linux系統調用列表

strace跟蹤進程

fastcgi-protocol-specification

 
我的博客即將搬運同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM