版权声明:本文由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)