首页 存档 技术 查看内容

日请求亿级的 QQ 会员 AMS 平台 PHP7 升级实践

2018-3-30 13:00 |来自: 互联网 337 0

摘要: QQ会员活动运营平台(AMS),是QQ会员增值运营业务的重要载体之一,承担海量活动运营的 Web 系统。AMS是一个主要采用 PHP 语言实现的活动运营平台, CGI 日请求3亿左右,高峰期达到 8 亿。然而,在之前比较长的一段 ...


QQ会员活动运营平台(AMS),是QQ会员增值运营业务的重要载体之一,承担海量活动运营的 Web 系统。AMS是一个主要采用 PHP 语言实现的活动运营平台, CGI 日请求3亿左右,高峰期达到 8 亿。然而,在之前比较长的一段时间里,我们都采用了比较老旧的基础软件版本,就是 PHP 5.2 Apache2.0 (2008 年的技术)。尤其从去年开始,随着 AMS 业务随着 QQ 会员增值业务的快速增长,性能压力日益变大。


于是,自 2015 年 5 月,我们就开始规划 PHP 底层升级,最终的目标是升级到 PHP7 。那时, PHP7 尚处于研发阶段,而我们讨论和预研就已经开始了。



PHP7 的学习和预研


1. HHVM 和 JIT


2015 年就 PHP 性能优化的方案,有另外一个比较重要的角色,就是由 Facebook 开源的 HHVM (HipHop Virtual Machine, HHVM 是一个 Facebook 开源的 PHP 虚拟机)。 HHVM 使用 JIT ( Just In Time ,即时编译是种软件优化技术,指在运行时才会去编译字节码为机器码)的编译方式以及其他技术,让 PHP 代码的执行性能大幅提升。据传,可以将 PHP5 版本的原生 PHP 代码提升 5-10 倍的执行性能。


HHVM 起源于 Facebook 公司, Facebook 早起的很多代码是使用 PHP 来开发的,但是,随着业务的快速发展, PHP 执行效率成为越来越明显的问题。为了优化执行效率, Facebook 在 2008 年就开始使用 HipHop ,这是一种 PHP 执行引擎,最初是为了将 Fackbook 的大量 PHP 代码转成 C ,以提高性能和节约资源。使用 HipHop 的 PHP 代码在性能上有数倍的提升。后来, Facebook 将 HipHop 平台开源,逐渐发展为现在的 HHVM 。


HHVM 成为一个 PHP 性能优化解决方案时, PHP7 还处于研发阶段。曾经看过部分同学对于 HHVM 的交流,性能可以获得可观的提升,但是服务运维和 PHP 语法兼容有一定成本。有一阵子, JIT 成为一个呼声很高的东西,很多技术同学建议 PHP7 也应该通过 JIT 来优化性能。


2015 年 7 月,我参加了中国 PHPCON ,听了惠新宸关于 PHP7 内核的技术分享。实际上,在 2013 年的时候,惠新宸( PHP7 内核开发者)和 Dmitry (另一位PHP语言内核开发者之一)就曾经在 PHP5.5 的版本上做过一个 JIT 的尝试(并没有发布)。 PHP5.5 的原来的执行流程,是将 PHP 代码通过词法和语法分析,编译成 opcode 字节码(格式和汇编有点像),然后, Zend 引擎读取这些 opcode 指令,逐条解析执行。



而他们在 opcode 环节后引入了类型推断( TypeInf ),然后通过JIT生成 ByteCodes ,然后再执行。



于是,在 benchmark (测试程序)中得到非常好的结果,实现 JIT 后性能比 PHP5.5 提升了8倍。然而,当他们把这个优化放入到实际的项目 WordPress (一个开源博客项目)中,却几乎看不见性能的提升。原因在于测试项目的代码量比较少,通过 JIT 产生的机器码也不大,而真实的 WordPress 项目生成的机器码太大,引起CPU缓存命中率下降(CPU Cache Miss)。


总而言之, JIT 并非在每个场景下都是点石成金的利器,而脱离业务场景的性能测试结果,并不一定具有代表性。


从官方放出 Wordpress 的 PHP7 和 HHVM 的性能对比可以看出,两者基本处于同一水平。




2. PHP7 在性能方面的优化


PHP7 是一个比较底层升级,比起 PHP5.6 的变化比较大,而就性能优化层面,大致可以汇总如下:


  • 将基础变量从 struct (结构体)变为 union (联合体),节省内存空间,间接减少CPU在内存分配和管理上的开销。

  • 部分基础变量( zend_array 、 zend_string 等)采用内存空间连续分配的方式,降低 CPU Cache Miss 的发生的概率。 CPU 从 CPU Cache 获取数据和从内存获取,它们之间效率相差可以高达 100 倍。举一个近似的例子,系统从内存读取数据和从磁盘读取数据的效率差别很大, CPU Cache Miss 类似遇到缺页中断。


  • 通过宏定义和内联函数( inline ),让编译器提前完成部分工作。无需在程序运行时分配内存,能够实现类似函数的功能,却没有函数调用的压栈、弹栈开销,效率会比较高。
    ... ...


3. AMS 平台技术选型的背景


就提升 PHP 的性能而言,可以选择的是 2015 年就可直接使用的 HHVM 或者是 2015 年底才发布正式版的 PHP7。会员 AMS 是一个访问量级比较大的一个 Web 系统,经过四年持续的升级和优化,积累了 800 多个业务功能组件,还有各种 PHP 编写的公共基础库和脚本,代码规模也比较大。


我们对于 PHP 版本对代码的向下兼容的需求是比较高的,因此,就我们业务场景而言, PHP7 良好的语法向下兼容,正是我们所需要的。因此,我们选择以 PHP7 为升级的方案。



PHP7 升级面临的风险和挑战


对于一个已经现网在线的大型公共 Web 服务来说,基础公共软件升级,通常是一件吃力不讨好的工作,做得好,不一定被大家感知到,但是,升级出了问题,则需要承担比较重的责任。为了尽量减少升级的风险,我们必须先弄清楚我们的升级存在挑战和风险。


于是,我们整理了升级挑战和风险列表:


  • Apache2.0 和 PHP5.2 这两个 2008-2009 年的基础软件版本比较古老,升级到 Apache2.4 和 PHP7 ,版本升级跨度比较大,时间跨度相差 7-8 年,因此,兼容性问题挑战比较高。实际上,我们公司的现网 PHP 服务,很多都停留在 PHP5.2 和 PHP5.3 的版本,版本偏低。

  • AMS 大量使用自研 tphplib 扩展, tphplib 很早在公司内部就没有人维护了,这个扩展之前只有 PHP5.3 和 PHP5.2 的编译 so 版本,并且,部分扩展没有支持线程安全。支持线程安全,是因为我们以前的 Apache 使用了 prefork 模式,而我们希望能够使用 Apache2.4 的 Event 模式( 2014 年中,在 prefork 和 worker 之后,推出的多进程线程管理模式,对于支持高并发,有更良好的表现)。


  • 语法兼容性问题,从 PHP5.2 到 PHP7 的跨度过大,即使 PHP 官方号称在向下兼容方面做到 99 %,但是,我们的代码规模比较大,它仍然是一个未知的风险。


  • 新软件面临的风险,将 Apache 和 PHP 这种基础软件升级到最新的版本,而这些版本的部分功能可能存在未知的风险和缺陷。


部分同学可能会建议采用 Nginx 会是更优的选择,的确,单纯比较 Nginx 和 Apache 在高并发方面的性能, Nginx 的表现更优。但是就 PHP 的 CGI 而言, Nginx php-ftpm 和 Apache mod_php 两者并没有很大的差距。另一方面,我们因为长期使用 Apache ,在技术熟悉和经验方面积累更多,因此,它可能不是最佳的选择,但是,具体到我们业务场景,算是比较合适的一个选择。



版本升级实施过程


1. 高跨度版本升级方式


从一个 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 三者粗略介绍:


  • prefork ,多进程模式, 1 个进程服务于 1 个用户请求,成本比较高。但是,稳定性最高,不需要支持线程安全。


  • worker ,多进程多线程模式, 1 个进程含有多个 worker 线程,1 个 worker 线程服务于1个用户请求,因为线程更轻量,成本比较低。但是,在 KeepAlive 场景下, worker 资源会被 client 占据,无法响应其他请求(空等待)。


  • event ,多进程多线程模式, 1 个进程也含有多个 worker 线程, 1 个 worker 线程服务于 1 个用户请求。但是,它解决了 KeepAlive 场景下的 worker 线程被占据问题,它通过专门的线程来管理这些 KeepAlive 连接,然后再分配“工作”给具体处理的 worker ,工作 worker 不会因为 KeepAlive 而导致空等待。


    关于 Event 模式的官方介绍:
    http://httpd.apache.org/docs/2.4/mod/event.html

    (部分同学可能会有 event 模式不支持 https 的印象,那个说法其实是 2 年多以前的国内部分技术博客的说法,目前的版本是支持的,详情可以浏览官方介绍)


开启动态切换模式的方法,就是在编译httpd的时候加上:
--enable-mpms-shared=all


从 PHP5.2 升级到 PHP5.6 相对比较容易,我们主要的工作如下:


  • 清理了部分不再使用的老扩展

  • 解决掉线程安全问题

  • 将 cmem 等 api 编译到新的版本

  • PHP 代码语法基于 PHP5.6 的兼容(实际上变化不大)

  • 部分扩展的同步调整。apc 扩展变为 zend_opcache 和 apcu ,以前的 apc 是包含了编译缓存和用户内存操作的功能,在 PHP 比较新版本里,被分解为独立的两个扩展。


从 PHP5.6 升级到 PHP7.0 的工作量就比较多,也相对比较复杂,因此,我们制定了每一个阶段的升级计划:


  • 技术预研, PHP7 升级准备。

  • 环境编译和搭建,下载相关的编译包,搭建完整的编译环境和测试环境。(编译环境还是需要比较多的依赖 so )

  • 兼容升级和测试。 PHP7 扩展的重新编译和代码兼容性工作, AMS 功能验证,性能压测。

  • 线上灰度。打包为 pkg 的安装包,编写相关的安装 shell 安装执行代码(包括软链接、解决一些 so 依赖)。然后,灰度安装到现网,观察。

  • 正式发布。扩大灰度范围,全量升级。



因为从 PHP5.2 升级到 PHP5.6 的过程中,很多问题已经被我们提前解决了,所以, PHP7 的升级主要难点在于 tphplib 扩展的编译升级。


涉及主要的工作包括:

  • PHP5.6 的扩展到 PHP7.0 的比较大幅度改造升级(工作量比较大的地方)

  • 兼容 apcu 的内存操作函数的改名。 PHP5 的时候,我们使用的 apc 前缀的函数不可用了,同步变为 apcu 前缀的函数(需要 apcu 扩展)。


  • 语法兼容升级。实际上工作量不算大,从 PHP5.6 升级到 PHP7 变化并不多。


我们大概在 2016 年 4 月中旬份完成了 PHP7 和 Apache 的编译工作, 4 月下旬进行现网灰度,5 月初全量发布到其中一个现网集群。


2. 升级过程中的错误调试方法


在升级和重新编译 PHP7 扩展时,如果执行结果不符合预期或者进程 core 掉,很多错误都是无法从 error 日志里看见的,不利于分析问题。可以采用以下几种方法,可以用来定位和分析大部分的问题:


  • var_dump/exit

    从PHP代码层逐步输出信息和执行exit,可以逐步定位到异常执行的PHP函数位置,然后再根据PHP函数名,反查扩展内的实现函数,找到问题。这种方法比较简单,但是效率不高。

  • gdb p/gdb c
    这种方法主要用于分析进程 core 的场景,我们采用的编译方式,是将mod_php( PHP 变成 Apache 的子或块的方式),使用gdb p来监控 Apache 的服务进程。
    命令:
    ps aux|grep httpd

    gdb 调试指定进程:
    命令:
    gdb -p

    使用 c 进行捕获,然后构造能够导致 core 的 web 请求:

    Apache 通常是多进程模式,为了让问题比较容易复现,可以在 http.con 里修改参数,将启动进程数修改为 1 个(下图中的多个参数都需要调整,以达到只启动单进程单线程的目的)。

    当然还有一种更简单的方法,因为Apache本身就支持单进程调试模式的。
    ./apachectl -k start -X -e debug
    然后再通过gdb p来调试就更简单一些。

  • 通过 strace 命令查看 Apache 进程具体在做了些什么事情,根据里面的执行内容,分析和定位问题。
    strace -Ttt -v -s1024 -f -p pid(进程 id)

    备注:执行这些命令,注意权限问题,很可能需要 root 权限。



PHP5.6 到 PHP7.0 扩展升级实践记录


1. 数据类型的变化


  • zval

    php7 的诞生始于 zval 结构的变化, PHP7 不再需要指针的指针,绝大部分 zval** 需要修改成 zval*。如果 PHP7 直接操作 zval,那么 zval*也需要改成 zvalZ_*P()也要改成 Z_*()ZVAL_*(var, …)需要改成 ZVAL_*(

声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系 [邮箱地址] 删除

路过

雷人

握手

鲜花

鸡蛋

相关分类

返回顶部