《架构师》2016年12月
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人
 推荐文章|Article

亿级用户PC主站的PHP7升级实践

作者 侯青龙

背景

新浪微博在2016年Q2季度公布月活跃用户(MAU)较上年同期增长33%,至2.82亿;日活跃用户(DAU)较上年同期增长36%,至1.26亿,总注册用户达8亿多。PC主站作为重要的流量入口,承载部分用户访问和流量落地,其中我们提供的部分服务(如:头条文章)承担全网所有流量。

随着业务的增长,系统压力也在不断的增加。峰值时,服务器Hits达10W+,CPU使用率也达到了80%,远超报警阈值。另外,当前机房的机架已趋于饱和,遇到突发事件,只能对非核心业务进行降低,挪用这些业务的服务器来进行临时扩容,这种方案只能算是一种临时方案,不能满足长久的业务增长需求。再加上一年一度的三节(圣诞、元旦、春节),系统需预留一定的冗余来应对,所以当前系统面临的问题非常严峻,解决系统压力的问题也迫在眉急。

面对当前的问题,我们内部也给出两套解决方案同步进行。

· 方案一:申请新机房,资源统一配置,实现弹性扩容。

· 方案二:对系统进行优化,对性能做进一步提升。

针对方案一,通过搭建与新机房之间的专线与之打通,高峰时,运用内部自研的混合云DCP平台,对所有资源进行调度管理,实现了真正意义上的弹性扩容。目前该方案已经在部分业务灰度运行,随时能对重点业务进行小流量测试。

针对方案二,系统层面,之前做过多次大范围的优化,比如:

· 将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_next_index_stringl(parray, value, value_len);
        //PHP7 add_next_index_string(parray, value); //PHP7

(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()替代。

PHP5:

        $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小时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一定会是一门越来越好的语言。

作者简介

侯青龙,微博主站研发负责人。2010年加入新浪微博,先后参与过微博主站V2版至V6版的研发,主导过主站V6版以及多机房消息同步系统等重大项目的架构设计工作。致力于提升产品研发效率以及优化系统性能。