背景前端
新浪微博在2016年Q2季度公布月活跃用户(MAU)较上年同期增加33%,至2.82亿;日活跃用户(DAU)较上年同期增加36%,至1.26亿,总注册用户达8亿多。PC主站做为重要的流量入口,承载部分用户访问和流量落地,其中咱们提供的部分服务(如:头条文章)承担全网全部流量。linux
随着业务的增加,系统压力也在不断的增长。峰值时,服务器Hits达10W+,CPU使用率也达到了80%,远超报警阈值。另外,当前机房的机架已趋于饱和,遇到突发事件,只能对非核心业务进行下降,挪用这些业务的服务器来进行临时扩容,这种方案只能算是一种临时方案,不能知足长久的业务增加需求。再加上一年一度的三节(圣诞、元旦、春节),系统需预留必定的冗余来应对,因此当前系统面临的问题很是严峻,解决系统压力的问题也迫在眉急。服务器
面对当前的问题,咱们内部也给出两套解决方案同步进行。架构
- 方案一:申请新机房,资源统一配置,实现弹性扩容。
- 方案二:对系统进行优化,对性能作进一步提高。
针对方案一,经过搭建与新机房之间的专线与之打通,高峰时,运用内部自研的混合云DCP平台,对全部资源进行调度管理,实现了真正意义上的弹性扩容。目前该方案已经在部分业务灰度运行,随时能对重点业务进行小流量测试。框架
针对方案二,系统层面,以前作过屡次大范围的优化,好比:ide
- 将Apache升级至Nginx
- 应用框架升级至Yaf
- CPU计算密集型的逻辑扩展化
- 弃用smarty
- 并行化调用
优化效果很是明显,若是再从系统层面进行优化,性能可提高的空间很是有限。好在业界传出了两大福音,分别为HHVM和PHP7。函数
方案选型性能
在PHP7还未正式发布时,咱们也研究过HHVM(HipHop Virtual Machine),关于HHVM更多细节,这里就再也不赘述,可参考官方说明。下面对它提高性能的方式进行一个简单的介绍。单元测试
默认状况下,Zend引擎先将PHP源码编译为opcode,而后Zend解析引擎逐条执行。这里的opcode码,能够理解成C语言级的函数。而HHVM提高性能方式为替代Zend引擎将PHP代码转换成中间字节码(HHVM本身的中间字节码,一般称为中间语言),而后在运行时经过即时(JIT)编译器将这些字节码转换成x64的机器码,相似于Java的JVM。测试
HHVM为了达到最佳优化效果,须要将PHP的变量类型固定下来,而不是让编译器去猜想。Facebook的工程师们就定义一种Hack写法,进而来达到编译器优化的目的,写法相似以下:
<?hh class point { public float $x, $y; function __construct(float $x, float $y) { $this->x = $x; $this->y = $y; } }
经过前期的调研,若是使用HHVM解析器来优化现有业务代码,为了达到最佳的性能提高,必须对代码进行大量修改。另外,服务部署也比较复杂,有必定的维护成本,综合评估后,该方案咱们也就再也不考虑。
固然,PHP7的开发进展咱们也一直在关注,经过官方测试数据以及内部本身测试,性能提高很是明显。
使人兴奋的是,在去年年末(2015年12月04日),官方终于正式发布了PHP7,而且对原生的代码几乎能够作到彻底兼容,性能方面与PHP5比较能提高达一倍左右,和HHVM相比已是不相上下。
不管从优化成本、风险控制,仍是从性能提高上来看,选择PHP7无疑是咱们的最佳方案。
系统现状以及升级风险
微博PC主站从2009年8月13日发布初版开始,前后经历了6个大的版本,系统架构也随着需求的变化进行过屡次重大调整。截止目前,系统部分架构以下。
从系统结构层面来看,系统分应用业务层、应用服务层,系统所依赖基础数据由平台服务层提供。
从服务部署层面来看,业务主要部署在三大服务集群,分别为Home池、Page池以及应用服务池。
为了提高系统性能,咱们自研了一些PHP扩展,因为PHP5和PHP7底层差异太大,大部分Zend API接口都进行了调整,全部扩展都须要修改。
因此,将PHP5环境升级至PHP7过程当中,主要面临以下风险:
- 使用了自研的PHP扩展,目前这些扩展只有PHP5版本,将这些扩展升级至PHP7,风险较大。
- PHP5与PHP7语法在某种程度上,多少仍是存在一些兼容性的问题。因为涉及主站代码量庞大,业务逻辑分支复杂,不少测试范围仅仅经过人工测试是很难触达的,也将面临不少未知的风险。
- 软件新版本的发布,都会面临着一些未知的风险和版本缺陷。这些问题,是否能快速获得解决。
- 涉及服务池和项目较多,基础组件的升级对业务范围影响较大,升级期间出现的问题、定位会比较复杂。
对微博这种数亿用户级别的系统的基础组件进行升级,影响范围将很是之大,一旦某个环节考虑不周全,颇有可能会出现比较严重的责任事故。
PHP7升级实践
1. 扩展升级
一些经常使用的扩展,在发布PHP7时,社区已经作了相应升级,如:Memcached、PHPRedis等。另外,微博使用的Yaf、Yar系列扩展,因为鸟哥(laruence)的支持,很早就全面支持了PHP7。对于这部分扩展,须要详细的测试以及现网灰度来进行保障。
PHP7中,不少经常使用的API接口都作了改变,例如HashTable API等。对于自研的PHP扩展,须要作升级,好比咱们有个核心扩展,升级涉及到代码量达1500行左右。
新升级的扩展,刚开始也面临着各式各样的问题,咱们主要经过官方给出的建议以及测试流程来保证其稳定可靠。
官方建议
- 在PHP7下编译你的扩展,编译错误与警告会告诉你绝大部分须要修改的地方。
- 在DEBUG模式下编译与调试你的扩展,在run-time你能够经过断言捕捉一些错误。你还能够看到内存泄露的状况。
测试流程
- 首先经过扩展所提供的单元测试来保证扩展功能的正确性。
- 其次经过大量的压力测试来验证其稳定性。
- 而后再经过业务代码的自动化测试来保证业务功能的可用性。
- 最后再经过现网流量灰度来确保最终的稳定可靠。
总体升级过程当中,涉及到的修改比较多,如下只简单列举出一些参数变动的函数。
(1)addassocstringl参数4个改成了3个。
//PHP5 add_assoc_stringl(parray, key, value, value_len); //PHP7 add_assoc_stringl(parray, key, value);
(2)addnextindex_stringl 参数从3个改成了2个。
//PHP5 add_assoc_stringl(parray, key, value, value_len); //PHP7 add_assoc_stringl(parray, key, value);
(3)RETURN_STRINGL 参数从3个改成了2个。
//PHP5 RETURN_STRINGL(value, length,dup); //PHP7 RETURN_STRINGL(value, length);
(4)变量声明从堆上分配,改成栈上分配。
//PHP5 zval* sarray_l; ALLOC_INIT_ZVAL(sarray_l); array_init(sarray_l); //PHP7 zval sarray_l; array_init(&sarray_l);
(5)zendhashgetcurrentkey_ex参数从6个改成4个。
//PHP5 ZEND_API int ZEND_FASTCALL zend_hash_get_current_key_ex ( HashTable* ht, char** str_index, uint* str_length, ulong* num_index, zend_bool duplicate, HashPosition* pos); //PHP7 ZEND_API int ZEND_FASTCALL zend_hash_get_current_key_ex( const HashTable *ht, zend_string **str_index, zend_ulong *num_index, HashPosition *pos);
更详细的说明,可参考官方PHP7扩展迁移文档:https://wiki.PHP.net/PHPng-upgrading。
2. PHP代码升级
总体来说,PHP7向前的兼容性正如官方所描述那样,能作到99%向前兼容,不须要作太多修改,但在总体迁移过程当中,仍是须要作一些兼容处理。
另外,在灰度期间,代码将同时运行于PHP5.4和PHP7环境,现网灰度前,咱们首先对全部代码进行了兼容性修改,以便同一套代码能同时兼容两套环境,而后再按计划对相关服务进行现网灰度。
同时,对于PHP7的新特性,升级期间,也强调不容许被使用,不然代码与低版本环境的兼容性会存在问题。
接下来简单介绍下升级PHP7代码过程当中,须要注意的地方。
(1)不少致命错误以及可恢复的致命错误,都被转换为异常来处理,这些异常继承自Error类,此类实现了 Throwable 接口。对未定义的函数进行调用,PHP5和PHP7环境下,都会出现致命错误。
undefine_function();
错误提示:
PHP Fatal error: Call to undefined function undefine_function() in /tmp/test.PHP on line 4
在PHP7环境下,这些致命的错误被转换为异常来处理,能够经过异常来进行捕获。
try { undefine_function(); } catch (Throwable $e) { echo $e; }
提示:
Error: Call to undefined function undefine_function() in /tmp/test.PHP:5 Stack trace: #0 {main}
(2)被0除,PHP 7 以前,被0除会致使一条 E_WARNING 并返回 false 。一个数字运算返回一个布尔值是没有意义的,PHP 7 会返回以下的 float 值之一。
- +INF
- -INF
- NAN
以下:
var_dump(42/0); // float(INF) + E_WARNING var_dump(-42/0); // float(-INF) + E_WARNING var_dump(0/0); // float(NAN) + E_WARNING
当使用取模运算符( % )的时候,PHP7会抛出一个 DivisionByZeroError 异常,PHP7以前,则抛出的是警告。
echo 42 % 0;
PHP5输出:
PHP Warning: Division by zero in /tmp/test.PHP on line 4
PHP7输出:
PHP Fatal error: Uncaught DivisionByZeroError: Modulo by zero in /tmp/test.PHP:4 Stack trace: # 0 {main} thrown in /tmp/test.PHP on line 4
PHP7环境下,能够捕获该异常:
try { echo 42 % 0; } catch (DivisionByZeroError $e) { echo $e->getMessage(); }
输出:
Modulo by zero
(3)pregreplace() 函数再也不支持 "\e" (PREGREPLACEEVAL). 使用 pregreplace_callback() 替代。
$content = preg_replace("/#([^#]+)#/ies", "strip_tags('#\\1#')", $content);
PHP7:
$content = preg_replace_callback("/#([^#]+)#/is", "self::strip_str_tags", $content); public static function strip_str_tags($matches){ return "#".strip_tags($matches[1]).'#'; }
(4)以静态方式调用非静态方法。
class foo { function bar() { echo 'I am not static!'; } } foo::bar();
以上代码PHP7会输出:
PHP Deprecated: Non-static method foo::bar() should not be called statically in /tmp/test.PHP on line 10 I am not static!
(5)E_STRICT 警告级别变动。
原有的 ESTRICT 警告都被迁移到其余级别。 ESTRICT 常量会被保留,因此调用 errorreporting(EALL|E_STRICT) 不会引起错误。
关于代码兼容PHP7,基本上是对代码的规范要求更严谨。之前写的不规范的地方,解析引擎只是输出NOTICE或者WARNING进行提示,不影响对代码上下文的执行,而到了PHP7,颇有可能会直接抛出异常,中断上下文的执行。
如:对0取模运行时,PHP7以前,解析引擎只抛出警告进行提示,但到了PHP7则会抛出一个DivisionByZeroError异常,会中断整个流程的执行。
对于警告级别的变动,在升级灰度期间,必定要关注相关NOTICE或WARNING报错。PHP7以前的一个NOTICE或者WARNING到了PHP7,一些报警级变成致命错误或者抛出异常,一旦没有对相关代码进行优化处理,逻辑被触发,业务系统很容易由于抛出的异常没处理而致使系统挂掉。
以上只列举了PHP7部分新特性,也是咱们在迁移代码时重点关注的一些点,更多细节可参考官方文档http://PHP.net/manual/zh/migration70.PHP。
3. 研发流程变动
一个需求的开发到上线,首先咱们会经过统一的开发环境来完成功能开发,其次通过内网测试、仿真测试,这两个环境测试经过后基本保证了数据逻辑与功能方面没有问题。而后合并至主干分支,并将代码部署至预发环境,再通过一轮简单回归,确保合并代码没有问题。最后将代码发布至生产环境。
为了确保新编写的代码能在两套环境(未灰度的PHP5.4环境以及灰度中的PHP7环境)中正常运行,代码在上线前,也须要在两套环境中分别进行测试,以达到彻底兼容。
因此,在灰度期间,对每一个环节的运行环境除了现有的PHP5.4环境外,咱们还分别提供了一套PHP7环境,每一个阶段的测试中,两套环境都须要进行验证。
4. 灰度方案
以前有过简单的介绍,系统部署在三大服务池,分别为Home池、Page池以及应用服务池。
在准备好安装包后,先是在每一个服务池分别部署了一台前端机来灰度。运行一段时间后,期间经过错误日志发现了很多问题,也有用户投诉过来的问题,在问题都基本解决的状况下,逐渐将各服务池的机器池增长至多台。
通过前期的灰度测试,主要的问题获得基本解决。接下是对应用服务池进行灰度,陆续又发现了很多问题。先后大概经历了一个月左右,完成了应用服务池的升级。而后再分别对Home池以及Page池进行灰度,通过漫长灰度,最终完成了PC主站全网PHP7的升级。
虽然不少问题基本上在测试或者灰度期间获得了解决,但依然有些问题是全量上线后一段时间才暴露出来,业务流程太多,不少逻辑须要必定条件才能被触发。为此BUG都要第一时间同步给PHP7升级项目组,对于升级PHP引发的问题,要求必须第一时间解决。
5. 优化方案
(1)启用Zend Opcache,启用Opcache很是简单, 在PHP.ini配置文件中加入:
zend_extension=opcache.so opcache.enable=1 opcache.enable_cli=1"
(2)使用GCC4.8以上的编译器来编译安装包,只有GCC4.8以上编译出的PHP才会开启Global Register for opline and execute_data支持。
(3)开启HugePage支持,首先在系统中开启HugePages, 而后开启Opcache的hugecodepages。
关于HugePage
操做系统默认的内存是以4KB分页的,而虚拟地址和内存地址须要转换, 而这个转换要查表,CPU为了加速这个查表过程会内建TLB(Translation Lookaside Buffer)。 显然,若是虚拟页越小,表里的条目数也就越多,而TLB大小是有限的,条目数越多TLB的Cache Miss也就会越高, 因此若是咱们能启用大内存页就能间接下降这个TLB Cache Miss。
PHP7与HugePage
PHP7开启HugePage支持后,会把自身的text段, 以及内存分配中的huge都采用大内存页来保存, 减小TLB miss, 从而提升性能。相关实现可参考Opcache实现中的accel_move_code_to_huge_pages()函数。
# 开启方法
以CentOS 6.5为例, 经过命令:
sudo sysctl vm.nr_hugepages=128
分配128个预留的大页内存。
$ cat /proc/meminfo | grep Huge AnonHugePages: 444416 kB HugePages_Total: 128 HugePages_Free: 128 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB
而后在PHP.ini中加入
opcache.huge_code_pages=1
6. 关于负载太高,系统CPU使用占比太高的问题
当咱们升级完第一个服务池时,感受整个升级过程仍是比较顺利,当灰度Page池,低峰时一切正常,但到了流量高峰,系统CPU占用很是高,如图:
系统CPU的使用远超用户程序CPU的使用,正常状况下,系统CPU与用户程序CPU占比应该在1/3左右。但咱们的实际状况则是,系统CPU是用户CPU的2~3倍,很不正常。
对比了一下两个服务池的流量,发现Page池的流量正常比Home池高很多,在升级Home池时,没发现该问题,主要缘由是流量没有达到必定级别,因此未触发该问题。当单机流量超过必定阈值,系统CPU的使用会出现一个直线的上升,此时系统性能会严重降低。
这个问题其实困扰了咱们有一段时间,经过各类搜索资料,均未发现任何升级PHP7会引发系统CPU太高的线索。但咱们发现了另一个比较重要的线索,不少软件官方文档里很是明确的提出了能够经过关闭Transparent HugePages(透明大页)来解决系统负载太高的问题。后来咱们也尝试对其进行了关闭,通过几天的观察,该问题获得解决,如图:
什么是Transparent HugePages(透明大页)
简单的讲,对于内存占用较大的程序,能够经过开启HugePage来提高系统性能。但这里会有个要求,就是在编写程序时,代码里须要显示的对HugePage进行支持。
而红帽企业版Linux为了减小程序开发的复杂性,并对HugePage进行支持,部署了Transparent HugePages。Transparent HugePages是一个使管理Huge Pages自动化的抽象层,实现方案为操做系统后台有一个叫作khugepaged的进程,它会一直扫描全部进程占用的内存,在可能的状况下会把4kPage交换为Huge Pages。
为何Transparent HugePages(透明大页)对系统的性能会产生影响
在khugepaged进行扫描进程占用内存,并将4kPage交换为Huge Pages的这个过程当中,对于操做的内存的各类分配活动都须要各类内存锁,直接影响程序的内存访问性能。而且,这个过程对于应用是透明的,在应用层面不可控制,对于专门为4k page优化的程序来讲,可能会形成随机的性能降低现象。
怎么关闭Transparent HugePages(透明大页)
(1)查看是否启用透明大页。
[root@venus153 ~]# cat /sys/kernel/mm/transparent_hugepage/enabled [always] madvise never
使用命令查看时,若是输出结果为[always]表示透明大页启用了,[never]表示透明大页禁用。
(2)关闭透明大页。
echo never > /sys/kernel/mm/transparent_hugepage/enabled echo never > /sys/kernel/mm/transparent_hugepage/defrag
(3)启用透明大页。
echo always > /sys/kernel/mm/transparent_hugepage/enabled echo always > /sys/kernel/mm/transparent_hugepage/defrag
(4)设置开机关闭。
修改/etc/rc.local文件,添加以下行:
if test -f /sys/kernel/mm/redhat_transparent_hugepage/enabled; then echo never > /sys/kernel/mm/transparent_hugepage/enabled echo never > /sys/kernel/mm/transparent_hugepage/defrag fi
升级效果
因为主站的业务比较复杂,项目较多,涉及服务池达多个,每一个服务池所承担业务与流量也不同,因此咱们在对不一样的服务池进行灰度升级,遇到的问题也不尽相同,致使总体升级先后达半年之久。庆幸的是,遇到的问题,最终都被解决掉了。最让人兴奋的是升级效果很是好,基本与官方一致,也为公司节省了很多成本。
如下简单地给你们展现下此次PHP7升级的成果。
(1)PHP5与PHP7环境下,分别对咱们的某个核心接口进行压测(压测数据由QA团队提供),相关数据以下:
一样接口,分别在两个不现的环境中进行测试,平均TPS从95提高到220,提高达130%。
(2)升级先后,单机CPU使用率对好比下。
升级先后,1小时流量状况变化:
升级先后,1小时CPU使用率变化:
升级先后,在流量变化不大的状况下,CPU使用率从45%降至25%,CPU使用率下降44.44%。
(3)某服务集群升级先后,同一时间段1小时CPU使用对好比下。
PHP5环境下,集群近1小时CPU使用变化:
PHP7环境下,集群近1小时CPU使用变化:
升级先后,CPU变化对比:
升级先后,同一时段,集群CPU平均使用率从51.6%下降至22.9%,使用率下降56.88%。
以上只简单从三个维度列举了一些数据。为了让升级效果更加客观,咱们实际的评估维度更多,如内存使用、接口响应时间占比等。最终综合得出的结论为,经过本次升级,PC主站总体性能提高在48.82%,效果很是好。团队今年的职能KPI就算是提早完成了。
总结
总体升级从准备到最终PC主站全网升级完成,时间跨度达半年之久,不管是扩展编写、准备安装脚本、PHP代码升级仍是全网灰度,期间一直会出现各式各样的问题。最终在团队的共同努力下,这些问题都完全获得了解决。
一直以来,对社区的付出深怀敬畏之心,也是由于他们对PHP语言性能极限的追求,才能让你们的业务坐享数倍性能的提高。同时,也让咱们更加相信,PHP必定会是一门愈来愈好的语言。
免费提供最新Linux技术教程书籍,为开源技术爱好者努力作得更多更好:http://www.linuxprobe.com/