版權聲明:本文由PHP7升級項目組原創文章,轉載請註明出處:
文章原文連接:https://www.qcloud.com/community/article/74php
來源:騰雲閣 https://www.qcloud.com/communityhtml
QQ會員活動運營平臺(AMS),是QQ會員增值運營業務的重要載體之一,承擔海量活動運營的Web系統。AMS是一個主要採用PHP語言實現的活動運營平臺, CGI日請求3億左右,高峯期達到8億。然而,在以前比較長的一段時間裏,咱們都採用了比較老舊的基礎軟件版本,就是PHP5.2+Apache2.0(2008年的技術)。尤爲從去年開始,隨着AMS業務隨着QQ會員增值業務的快速增加,性能壓力日益變大。git
因而,自2015年5月,咱們就開始規劃PHP底層升級,最終的目標是升級到PHP7。那時,PHP7尚處於研發階段,而咱們討論和預研就已經開始了。github
2015年就PHP性能優化的方案,有另一個比較重要的角色,就是由Facebook開源的HHVM(HipHop Virtual Machine,HHVM是一個Facebook開源的PHP虛擬機)。HHVM使用JIT(Just In Time,即時編譯是種軟件優化技術,指在運行時纔會去編譯字節碼爲機器碼)的編譯方式以及其餘技術,讓PHP代碼的執行性能大幅提高。據傳,能夠將PHP5版本的原生PHP代碼提高5-10倍的執行性能。web
HHVM起源於Facebook公司,Facebook早起的不少代碼是使用PHP來開發的,可是,隨着業務的快速發展,PHP執行效率成爲愈來愈明顯的問題。爲了優化執行效率,Facebook在2008年就開始使用HipHop,這是一種PHP執行引擎,最初是爲了將 Fackbook的大量PHP代碼轉成 C++,以提升性能和節約資源。使用HipHop的PHP代碼在性能上有數倍的提高。後來,Facebook將HipHop平臺開源,逐漸發展爲如今的 HHVM。shell
HHVM成爲一個PHP性能優化解決方案時,PHP7還處於研發階段。曾經看過部分同窗對於HHVM的交流,性能能夠得到可觀的提高,可是服務運維和PHP語法兼容有必定成本。有一陣子,JIT成爲一個呼聲很高的東西,不少技術同窗建議PHP7也應該經過JIT來優化性能。apache
2015年7月,我參加了中國PHPCON,聽了惠新宸關於PHP7內核的技術分享。實際上,在2013年的時候,惠新宸(PHP7內核開發者)和Dmitry(另外一位PHP語言內核開發者之一)就曾經在PHP5.5的版本上作過一個JIT的嘗試(並無發佈)。PHP5.5的原來的執行流程,是將PHP代碼經過詞法和語法分析,編譯成opcode字節碼(格式和彙編有點像),而後,Zend引擎讀取這些opcode指令,逐條解析執行。api
而他們在opcode環節後引入了類型推斷(TypeInf),而後經過JIT生成ByteCodes,而後再執行。數組
因而,在benchmark(測試程序)中獲得很是好的結果,實現JIT後性能比PHP5.5提高了8倍。然而,當他們把這個優化放入到實際的項目WordPress(一個開源博客項目)中,卻幾乎看不見性能的提高。緣由在於測試項目的代碼量比較少,經過JIT產生的機器碼也不大,而真實的WordPress項目生成的機器碼太大,引發CPU緩存命中率降低(CPU Cache Miss)。緩存
總而言之,JIT並不是在每一個場景下都是點石成金的利器,而脫離業務場景的性能測試結果,並不必定具備表明性。
從官方放出Wordpress的PHP7和HHVM的性能對比能夠看出,二者基本處於同一水平。
PHP7是一個比較底層升級,比起PHP5.6的變化比較大,而就性能優化層面,大體能夠彙總以下:
就提高PHP的性能而言,能夠選擇的是2015年就可直接使用的HHVM或者是2015年末才發佈正式版的PHP7。會員AMS是一個訪問量級比較大的一個Web系統,通過四年持續的升級和優化,積累了800多個業務功能組件,還有各類PHP編寫的公共基礎庫和腳本,代碼規模也比較大。
咱們對於PHP版本對代碼的向下兼容的需求是比較高的,所以,就咱們業務場景而言,PHP7良好的語法向下兼容,正是咱們所須要的。所以,咱們選擇以PHP7爲升級的方案。
對於一個已經現網在線的大型公共Web服務來講,基礎公共軟件升級,一般是一件吃力不討好的工做,作得好,不必定被你們感知到,可是,升級出了問題,則須要承擔比較重的責任。爲了儘可能減小升級的風險,咱們必須先弄清楚咱們的升級存在挑戰和風險。
因而,咱們整理了升級挑戰和風險列表:
部分同窗可能會建議採用Nginx會是更優的選擇,的確,單純比較Nginx和Apache在高併發方面的性能,Nginx的表現更優。可是就PHP的CGI而言,Nginx+php-ftpm和Apache+mod_php二者並無很大的差距。另外一方面,咱們由於長期使用Apache,在技術熟悉和經驗方面積累更多,所以,它可能不是最佳的選擇,可是,具體到咱們業務場景,算是比較合適的一個選擇。
從一個2008年的Apache2.0直接升級到2016年的Apache2.4,這個跨度過於大,甚至使用的http.conf的配置文件都有不少的不一樣,這裏的須要更新的地方比較多,未知的風險也是存在的。因而,咱們的作法,是先嚐試將Apache2.0升級到Apach2.2,調整配置、觀察穩定性,而後再進一步嘗試到Apach2.4。所幸的是,Apache(httpd)是一個比較特別的開源社區,他們以前一直同時維護這兩個分支版本的Apache(2.2和2.4),所以,即便是Apache2.2也有比較新的版本。
因而,咱們先升級了一個PHP5.2+Apache2.2,對兼容性進行了測試和觀察,確認二者之間是能夠比較平滑升級後,咱們開始進行Apache2.4的升級方案。
PHP5.2的升級,咱們也採用相同的思路,咱們先將PHP5.2升級至PHP5.6(當時,PHP7仍是beta版本),而後再將PHP5.6升級到PHP7,以更平滑的方式,逐步解決不一樣的問題。
因而,咱們的升級計劃變爲:
Apache2.4編譯爲動態MPM的模式(支持經過httpd配置切換prefork/worker/event模式),根據現網風險等實時降級。
Prefork、Worker、Event三者粗略介紹:
開啓動態切換模式的方法,就是在編譯httpd的時候加上:--enable-mpms-shared=all
從PHP5.2升級到PHP5.6相對比較容易,咱們主要的工做以下:
從PHP5.6升級到PHP7.0的工做量就比較多,也相對比較複雜,所以,咱們制定了每個階段的升級計劃:
咱們大概在2016年4月中旬份完成了PHP7和Apache的編譯工做, 4月下旬進行現網灰度,5月初全量發佈到其中一個現網集羣。
在升級和從新編譯PHP7擴展時,若是執行結果不符合預期或者進程core掉,不少錯誤都是沒法從error日誌裏看見的,不利於分析問題。能夠採用如下幾種方法,能夠用來定位和分析大部分的問題:
var_dump/exit
gdb –p/gdb c
mod_php
(PHP變成Apache的子或塊的方式),使用gdb –p
來監控Apache的服務進程。ps aux|grep httpd
gdb -p
./apachectl -k start -X -e debug
gdb –p
來調試就更簡單一些。strace -Ttt -v -s1024 -f -p pid
(進程id)zval
結構的變化,PHP7再也不須要指針的指針,絕大部分zval**
須要修改爲zval*
。若是PHP7直接操做zval
,那麼zval*
也須要改爲zval
,Z_*P()
也要改爲Z_*()
,ZVAL_*(var, …)
須要改爲ZVAL_*(&var, …)
,必定要謹慎使用&符號,由於PHP7幾乎不要求使用zval*
,那麼不少地方的&也是要去掉的。ALLOC_ZVAL
,ALLOC_INIT_ZVAL
,MAKE_STD_ZVAL
這幾個分配內存的宏已經被移除了。大多數狀況下,zval*
應該修改成zval
,而INIT_PZVAL
宏也被移除了。/* 7.0zval結構源碼 */ /* value字段,僅佔一個size_t長度,只有指針或double或者long */ typedef union _zend_value { zend_long lval; /* long value */ double dval; /* double value */ zend_refcounted *counted; zend_string *str; zend_array *arr; zend_object *obj; zend_resource *res; zend_reference *ref; zend_ast_ref *ast; zval *zv; void *ptr; zend_class_entry *ce; zend_function *func; struct { uint32_t w1; uint32_t w2; } ww; } zend_value; struct _zval_struct { zend_value value; /* value */ union { 。。。 } u1;/* 擴充字段,主要是類型信息 */ union { … … } u2;/* 擴充字段,保存輔助信息 */ };
整型
直接切換便可:long->zend_long
/* 定義 */ typedef int64_t zend_long; /* else */ typedef int32_t zend_long;
字符串類型
PHP5.6版本中使用 char* + len的方式表示字符串,PHP7.0中作了封裝,定義了zend_string類型:
struct _zend_string { zend_refcounted_h gc; zend_ulong h; /* hash value */ size_t len; char val[1]; };
zend_string
和char*
的轉換:
zend_string *str;
char *cstr = NULL; size_t slen = 0; //... /* 從zend_string獲取char* 和 len的方法以下 */ cstr = ZSTR_VAL(str); slen = ZSTR_LEN(str); /* char* 構造zend_string的方法 */ zend_string * zstr = zend_string_init("test",sizeof("test"), 0);
擴展方法,解析參數時,使用字符串的地方,將‘s’替換成‘S’:
/* 例如 */ `zend_string` `*zstr`; if (zend_parse_parameters(ZEND_NUM_ARGS() , "S", &zstr) == FAILURE) { RETURN_LONG(-1); }
/* php7.0 zend_object 定義 */ struct _zend_object { zend_refcounted_h gc; uint32_t handle; zend_class_entry *ce; const zend_object_handlers *handlers; HashTable *properties; zval properties_table[1]; };
zendobject
是一個可變長度的結構。所以在自定義對象的結構中,zendobject
須要放在最後一項: /* 例子 */ struct clogger_object { CLogger *logger; zend_object std;// 放在後面 }; /* 使用偏移量的方式獲取對象 */ static inline clogger_object *php_clogger_object_from_obj(zend_object *obj) { return (clogger_object*)((char*)(obj) - XtOffsetOf(clogger_object, std)); } #define Z_USEROBJ_P(zv) php_clogger_object_from_obj(Z_OBJ_P((zv))) /* 釋放資源時 */ void tphp_clogger_free_storage(zend_object *object TSRMLS_DC) { clogger_object *intern = php_clogger_object_from_obj(object); if (intern->logger) { delete intern->logger; intern->logger = NULL; } zend_object_std_dtor(&intern->std); }
/*7.0中的hash表結構 */ typedef struct _Bucket { /* hash表中的一個條目 */ zval val; /* 刪除元素zval類型標記爲IS_UNDEF */ zend_ulong h; /* hash value (or numeric index) */ zend_string *key; /* string key or NULL for numerics */ } Bucket; typedef struct _zend_array HashTable; struct _zend_array { zend_refcounted_h gc; union { struct { ZEND_ENDIAN_LOHI_4( zend_uchar flags, zend_uchar nApplyCount, zend_uchar nIteratorsCount, zend_uchar reserve) } v; uint32_t flags; } u; uint32_t nTableMask; Bucket *arData; /* 保存全部數組元素 */ uint32_t nNumUsed; /* 當前用到了多少長度, */ uint32_t nNumOfElements; /* 數組中實際保存的元素的個數,一旦nNumUsed的值到達nTableSize,PHP就會嘗試調整arData數組,讓它更緊湊,具體方式就是拋棄類型爲UDENF的條目 */ uint32_t nTableSize; /* 數組被分配的內存大小爲2的冪次方(最小值爲8) */ uint32_t nInternalPointer; zend_long nNextFreeElement; dtor_func_t pDestructor; };
其中,PHP7在zend_hash.h中定義了一系列宏,用來操做數組,包括遍歷key、遍歷value、遍歷key-value等,下面是一個簡單例子:
/* 數組舉例 */ zval *arr; zend_parse_parameters(ZEND_NUM_ARGS() , "a", &arr_qos_req); if (arr) { zval *item; zend_string *key; ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(arr), key, item) { /* ... */ } } /* 獲取到item後,能夠經過下面的api獲取long、double、string值 */ zval_get_long(item) zval_get_double(item) zval_get_string(item)
PHP5.6版本中是經過zend_hash_find
查找key,而後將結果給到zval **
變量,而且查詢不到時須要本身分配內存,初始化一個item,設置默認值。
duplicate參數
PHP5.6中不少API中都須要填入一個duplicate
參數,代表一個變量是否須要複製一份,尤爲是string類
的操做,PHP7.0中取消duplicate
參數,對於string相關操做,只要有duplicate
參數,直接刪掉便可。由於PHP7.0中定義了zval_string
結構,對字符串的操做,再也不須要duplicate
值,底層直接使用zend_string_init
初始化一個zend_string
便可,而在PHP5.6中string
是存放在zval
中的,而zval
的內存須要手動分配。
涉及的API彙總以下:add_index_string
、add_index_stringl
、add_assoc_string_ex
、add_assoc_stringl_ex
、add_assoc_string
、add_assoc_stringl
、add_next_index_string
、add_next_index_stringl
、add_get_assoc_string_ex
、add_get_assoc_stringl_ex
、add_get_assoc_string
、add_get_assoc_stringl
、add_get_index_string
、add_get_index_stringl
、add_property_string_ex
、add_property_stringl_ex
、add_property_string
、add_property_stringl
、ZVAL_STRING
、ZVAL_STRINGL
、RETVAL_STRING
、RETVAL_STRINGL
、RETURN_STRING
、RETURN_STRINGL
MAKE_STD_ZVAL
PHP5.6中,zval變量是在堆上分配的,建立一個zval變量須要先聲明一個指針,而後使用MAKE_STD_ZVAL
進行分配空間。PHP7.0中,這個宏已經取消,變量在棧上分配,直接定義一個變量便可,再也不須要MAKE_STD_ZVAL
,使用到的地方,直接去掉就好。
ZEND_RSRC_DTOR_FUNC
修改參數名rsrc爲res
/* PHP5.6 */ typedef struct _zend_rsrc_list_entry { void *ptr; int type; int refcount; } zend_rsrc_list_entry; typedef void (*rsrc_dtor_func_t)(zend_rsrc_list_entry *rsrc TSRMLS_DC); #define ZEND_RSRC_DTOR_FUNC(name) void name(zend_rsrc_list_entry *rsrc TSRMLS_DC) /* PHP7.0 */ struct _zend_resource { zend_refcounted_h gc;/*7.0中對引用計數作告終構封裝*/ int handle; int type; void *ptr; }; typedef void (*rsrc_dtor_func_t)(zend_resource *res); #define ZEND_RSRC_DTOR_FUNC(name) void name(zend_resource *res)
PHP7.0中,將zend_rsrc_list_entry
結構升級爲zend_resource
,在新版本中只須要修改一下參數名稱便可。
二級指針宏,即Z_*_PP
PHP7.0中取消了全部的PP宏,大部分狀況直接使用對應的P宏便可。
zend_object_store_get_object被取消
根據官方wiki,能夠定義以下宏,用來獲取object,實際狀況看,這個宏用的仍是比較頻繁的:
static inline user_object *user_fetch_object(zend_object *obj) { return (user_object *)((char*)(obj) - XtOffsetOf(user_object, std)); } /* }}} */ #define Z_USEROBJ_P(zv) user_fetch_object(Z_OBJ_P((zv)))
zend_hash_exists、zend_hash_find
對全部須要字符串參數的函數,PHP5.6中的方式是傳遞兩個參數(char* + len)
,而PHP7.0中定義了zend_string
,所以只須要一個zend_string
變量便可。
返回值變成了zend_bool類型:
/* 例子 */ zend_string * key; key = zend_string_init("key",sizeof("key"), 0); zend_bool res_key = zend_hash_exists(itmeArr, key);
現網服務是一個很是重要而又敏感的環境,輕則影響用戶體驗,重則產生現網事故。所以,咱們4月下旬完成PHP7編譯和測試工做以後,就在AMS其中一臺機器進行了灰度上線,觀察了幾天後,而後逐步擴大灰度範圍,在5月初完成升級。
這個是咱們壓測AMS一個查詢多個活動計數器的壓測結果,以及現網CGI機器,在高峯相同TGW流量場景下的CPU負載數據:
就咱們的業務壓測和現網結果來看,和官方所說的性能提高一倍,基本一致。
AMS平臺擁有很多的CGI機器,PHP7的升級和應用給咱們帶來了性能的提高,能夠有效節省硬件資源成本。而且,經過Apache2.4的Event模式,咱們也加強了Apache在支持併發方面的能力。
咱們PHP7升級研發項目組,在過去比較長的一個時間段裏,通過持續地努力和推動,終於在2016年4月下旬現網灰度,5月初在集羣中全量升級,爲咱們的AMS活動運營平臺帶來性能上大幅度的提高。
PHP7的革新,對於PHP語言自己而言,具備非凡的意義和價值,這讓我更加確信一點,PHP會是一個愈來愈好的語言。同時,感謝PHP社區的開發者們,爲咱們業務帶來的性能提高。
騰訊增值產品部平臺開發中心——PHP7升級研發項目組:
徐漢彬、王默涵、廖聲茂、匡素文、廖增康、巫澤敏
文章來源公衆號:小時光茶社(Tech Teahouse)