伴随业务的增长,系统压力也在不断增加,再加上机房机架趋于饱和,无法更加有效应对各种突发事件。在这样的情况下,PC 主站升级为 PHP 7,有哪些技术细节可以分享?
背景
新浪微博在 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_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 ({1} 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 一定会是一门越来越好的语言。
作者简介
侯青龙,微博主站研发负责人。2010 年加入新浪微博,先后参与过微博主站 V2 版至 V6 版的研发,主导过主站 V6 版以及多机房消息同步系统等重大项目的架构设计工作。致力于提升产品研发效率以及优化系统性能。
感谢韩婷对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论