威尼斯www.9778.com-威尼斯正版官方网站

Badoo 告诉你切换到 PHP7 节省了 100 万美元

日期:2020-03-13编辑作者:Web前端技术

介绍

我们成功的把我们的应用迁移到了php7上面(数百台机器的集群),而且运行的很好,据说我们是第二个把如此规模的应用切换到php7的企业,在切换的过程我们发现了一些php7字节码缓存的bug,庆幸的是这些bug现在已经被修复了,现在我们把这个激动人心的消息分享给所有的php社区:php7现在已经可以稳定的运行在商用环境上,而且比以前更加节省内存,性能也有的很大的提高。

图片 1

下面我会详细的介绍下我们是如何把应用前移动php7的,我们在这中间遇到的问题及处理情况,还有最终的结果。但首先让我们回头看看一些更常见的问题:

Web项目的瓶颈在于数据库持久化这是一个常见的误解。一个设计良好的系统应该是平衡的:当访问量增长时,由系统的各个部分分摊这些压力,同样的,当达到系统阀值时,系统的所有组件(不仅仅包括硬盘数据库,还有处理器和网络)共同分摊压力。基于这个事实,应用集群的处理能力才应该是最重要的因素。在很多项目中,这种集群由数以百计甚至数以千计的服务器组成,这是因为花时间去调整集群的处理能力更加经济实益(我们因此节省一百多万)。

PHP的Web应用,处理器的消耗跟其他动态高级语言一样多。但是PHP开发者面对着一个特别的障碍(这让他们成为其他社区恶意攻击的的受害者):缺少JIT,至少没有一个像C/C++语言那样的可编译文本的生成器。PHP社区无力在核心项目框架上去实现一个类似的解决方案更是树立了一种不良的风气:主要的开发成员开始整合他们的解决方案,所以HHVM在Facebook上诞生了,KPHP在VKontakte上诞生,还有其他类似的方案。幸运地是,在2015年,随着PHP7的正式发布,PHP要开始”Grow up”啦。虽然还是没有JIT,但很难去评定这些改变在”engine”中有多重要。现在,尽管没有JIT,PHP7可以跟HHVM相匹敌( Benchmarks from the LightSpeed blog  or PHP devs benchmarks)。新的PHP7体系架构将会让JIT的实现变得简单。

在Badoo的平台开发者已经非常关注近些年出现的每一次问题,包括HHVM试点项目,但是我们还是决定等待很有前途的PHP7的到来。现在我们启动了已经基于PHP7的Baboo!这是一个史诗般的项目,拥有300多万行的PHP代码,并且经历了60000次的测试。我们为了处理这些挑战,提出了一个新的PHP引用测试框架(当然,也是开源的),并且在整个过程中节省了上百万美元。

下面我会详细的介绍下我们是如何把应用前移动php7的,我们在这中间遇到的问题及处理情况,还有最终的结果。但首先让我们回头看看一些更常见的问题:

图片 2

HHVM的试验

在切换到PHP7之前,我们曾花了不少时间来寻找优化后端的方法。当然,第一步就是从HHVM下手。在试验了几周之后,我们获得了值得关注的结果:在给框架中的JIT热身之后,我们看到速度与CPU使用率上升了三倍。

另一方面,HHVM 被证实有一些严重的缺点:

  • 部署困难而且慢。在部署过程中,你不得不首先启动JIT-cache。当机器启动的时候,它不能负载产品流量,因为所有的事情进行的相当慢。HHVM 团队同样不推荐启动并行请求。顺便一提,大量聚类操作在启动阶段并不快速。此外,对于几百个机器构成的大集群你必须学习如何分批部署。这样体系结构和部署过程相当繁琐,而且很难估算出所需要的时间。对于我们来说,部署应该尽可能简单快捷。我们的开发者将在同一天提供两个开发版并且释出许多补丁。
  • 测试不便。我们非常依赖runkit扩展,但是它在HHVM中却不可用。稍后我们将详细介绍runkit,但是无需多言,它是一个能让你几乎随心所欲更改变量、类、方法、函数行为的扩展。这是通过一个抵达PHP核心的集成来实现的。HHVM引擎仅仅显示了略微相像的PHP外观,但是他们各自的核心十分不同。鉴 于扩展的特定功能,在HHVM上独立地实现runkit异常困难,而且我们不得不重写数万测试用例以确保HHVM和我们的代码正确的工作。这看起来似乎不 值得。公平的说,我们以后在处理所有其他选项时也会遇到同样的问题,而且我们在迁移到PHP7时仍然要重做许多事情包括摆脱runkit。但是以后会更多。
  • 兼容性。主要问题是不完全兼容PHP5.5(参考此处) ,并且不兼容现有的扩展(许多PHP5.5的)。这些所有的不兼容性导致了这个项目的明显缺点: HHVM 不是被大社区开发的,相反只是Facebook的一个分支。在这种情况下公司很容易不参考社区就修改内部规则和标准,而且大量的代码包含其中。换句话说, 他们关起门来利用自己的资源解决了问题。因此,为了解决相似的问题,一个公司需要有Facebook一样的资源不仅投入最初的实现同样要投入后续支持。这 个提议不仅有风险而且可能开销很大,所以我们决定拒绝它。
  • 潜力。尽管Facebook是一个大公司而且拥有无数顶尖程序员,我们仍然怀疑他们的HHVM开发者比整个PHP社区更强。我们猜想PHP的类似于HHVM的东西会很快出现,而前者将慢慢淡出我们的视野。

让我们耐心等待PHP7。

切换到新版本的PHP7解释器是一个重要和艰难的过程,我们准备建立一个精确的计划。这个计划包括三个阶段:

  • 修改PHP构建/部署的基础设施和为大量的扩展调整现有的code
  • 改变基础设施和测试环境
  • 修改PHP应用程序的代码。

我们稍后会给出这些这些阶段的细节。

Web项目的瓶颈在于数据库持久化这是一个常见的误解。一个设计良好的系统应该是平衡的:当访问量增长时,由系统的各个部分分摊这些压力,同样的,当达到系统阀值时,系统的所有组件(不仅仅包括硬盘数据库,还有处理器和网络)共同分摊压力。基于这个事实,应用集群的处理能力才应该是最重要的因素。在很多项目中,这种集群由数以百计甚至数以千计的服务器组成,这是因为花时间去调整集群的处理能力更加经济实益(我们因此节省一百多万)。

引用声明:本文为CSDN原创投稿文章,未经许可,禁止任何形式的转载。 作者:徐汉彬、王默涵、廖声茂、匡素文、廖增康、巫泽敏,以上为腾讯增值产品部平台开发中心——PHP7升级研发项目组核心成员。 责编:钱曙光,关注架构和算法领域,寻求报道或者投稿请发邮件qianshg@csdn.net,另有「CSDN 高级架构师群」,内有诸多知名互联网公司的大牛架构师,欢迎架构师加微信qshuguang2008申请入群,备注姓名+公司+职位。 推荐: PHP开发者的百科全书——PHP知识图谱QQ会员活动运营平台,是QQ会员增值运营业务的重要载体之一,承担海量活动运营的Web系统。AMS是一个主要采用PHP语言实现的活动运营平台, CGI日请求3亿左右,高峰期达到8亿。然而,在之前比较长的一段时间里,我们都采用了比较老旧的基础软件版本,就是PHP5.2+Apache2.0。尤其从去年开始,随着AMS业务随着QQ会员增值业务的快速增长,性能压力日益变大。于是,自2015年5月,我们就开始规划PHP底层升级,最终的目标是升级到PHP7。那时,PHP7尚处于研发阶段,而我们讨论和预研就已经开始了。一、PHP7的学习和预研1. HHVM和JIT2015年就PHP性能优化的方案,有另外一个比较重要的角色,就是由Facebook开源的HHVM。HHVM使用JIT的编译方式以及其他技术,让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年的时候,惠新宸和Dmitry就曾经在PHP5.5的版本上做过一个JIT的尝试。PHP5.5的原来的执行流程,是将PHP代码通过词法和语法分析,编译成opcode字节码,然后,Zend引擎读取这些opcode指令,逐条解析执行。而他们在opcode环节后引入了类型推断,然后通过JIT生成ByteCodes,然后再执行。于是,在benchmark中得到非常好的结果,实现JIT后性能比PHP5.5提升了8倍。然而,当他们把这个优化放入到实际的项目WordPress中,却几乎看不见性能的提升。原因在于测试项目的代码量比较少,通过JIT产生的机器码也不大,而真实的WordPress项目生成的机器码太大,引起CPU缓存命中率下降。 总而言之,JIT并非在每个场景下都是点石成金的利器,而脱离业务场景的性能测试结果,并不一定具有代表性。 从官方放出Wordpress的PHP7和HHVM的性能对比可以看出,两者基本处于同一水平。2.PHP7在性能方面的优化PHP7是一个比较底层升级,比起PHP5.6的变化比较大,而就性能优化层面,大致可以汇总如下: 将基础变量从struct变为union,节省内存空间,间接减少CPU在内存分配和管理上的开销。 部分基础变量采用内存空间连续分配的方式,降低CPU Cache Miss的发生的概率。CPU从CPU Cache获取数据和从内存获取,它们之间效率相差可以高达100倍。举一个近似的例子,系统从内存读取数据和从磁盘读取数据的效率差别很大,CPU Cache Miss类似遇到缺页中断。 通过宏定义和内联函数,让编译器提前完成部分工作。无需在程序运行时分配内存,能够实现类似函数的功能,却没有函数调用的压栈、弹栈开销,效率会比较高。 … … 更多更详细关于PHP7的介绍,有兴趣的同学可以查看:《PHP7革新与性能优化》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模式。 语法兼容性问题,从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,这个跨度过于大,甚至使用的的配置文件都有很多的不同,这里的需要更新的地方比较多,未知的风险也是存在的。于是,我们的做法,是先尝试将Apache2.0升级到Apach2.2,调整配置、观察稳定性,然后再进一步尝试到Apach2.4。所幸的是,Apache是一个比较特别的开源社区,他们之前一直同时维护这两个分支版本的Apache,因此,即使是Apache2.2也有比较新的版本。于是,我们先升级了一个PHP5.2+Apache2.2,对兼容性进行了测试和观察,确认两者之间是可以比较平滑升级后,我们开始进行Apache2.4的升级方案。PHP5.2的升级,我们也采用相同的思路,我们先将PHP5.2升级至PHP5.6,然后再将PHP5.6升级到PHP7,以更平滑的方式,逐步解决不同的问题。于是,我们的升级计划变为:Apache2.4编译为动态MPM的模式,根据现网风险等实时降级。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模式的官方介绍: 开启动态切换模式的方法,就是在编译的时候加上: –enable-mpms-shared=all从PHP5.2升级到PHP5.6相对比较容易,我们主要的工作如下: 清理了部分不再使用的老扩展 解决掉线程安全问题 将cmem等api编译到新的版本 PHP代码语法基于PHP5.6的兼容 部分扩展的同步调整。apc扩展变为zend_opcache和apcu,以前的apc是包含了编译缓存和用户内存操作的功能,在PHP比较新版本里,被分解为独立的两个扩展。从PHP5.6升级到PHP7.0的工作量就比较多,也相对比较复杂,因此,我们制定了每一个阶段的升级计划: 技术预研,PHP7升级准备。 环境编译和搭建,下载相关的编译包,搭建完整的编译环境和测试环境。 兼容升级和测试。PHP7扩展的重新编译和代码兼容性工作,AMS功能验证,性能压测。 线上灰度。打包为pkg的安装包,编写相关的安装shell安装执行代码。然后,灰度安装到现网,观察。 正式发布。扩大灰度范围,全量升级。因为从PHP5.2升级到PHP5.6的过程中,很多问题已经被我们提前解决了,所以,PHP7的升级主要难点在于tphplib扩展的编译升级。 涉及主要的工作包括: PHP5.6的扩展到PHP7.0的比较大幅度改造升级 兼容apcu的内存操作函数的改名。PHP5的时候,我们使用的apc前缀的函数不可用了,同步变为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,使用gdb –p来监控Apache的服务进程。 命令:ps aux|grep 调试指定进程: 命令:gdb -p使用c进行捕获,然后构造能够导致core的web请求:Apache通常是多进程模式,为了让问题比较容易复现,可以在里修改参数,将启动进程数修改为1个。当然还有一种更简单的方法,因为Apache本身就支持单进程调试模式的。 ./apachectl -k start -X -e debug 然后再通过gdb –p来调试就更简单一些。 通过strace命令查看Apache进程具体在做了些什么事情,根据里面的执行内容,分析和定位问题。 strace -Ttt -v -s1024 -f -p pid 备注:执行这些命令,注意权限问题,很可能需要root权限。四、PHP5.6到PHP7.0扩展升级实践记录1. 数据类型的变化zvalphp7的诞生始于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宏也被移除了。

引擎和扩展的变化

在Badoo中, 我们有积极的支持和更新的PHP分支,我们在PHP7正式版release之前我们就已经开始切换到php7了. 所以我们不得不在我们的代码树经常整合(rebase)PHP7上游的代码,以便它来更新每个候选发布版。我们每天在工作中所用的补丁和自定义的code都需要在两个版本之间进行移植。

下载和构建依赖库、扩展程序、还包括PHP 5.5和7.0的构建这些过程都是自动化的完成的。这不仅简化了我们目前的工作,也预示着未来:在版本7.1出来时, 也许这一切(解析引擎和扩展等等)都已经准备到位了;

如上所述,我们将注意力转向扩展。我们提供超过70种扩展,已经比基于我们产品改写的开源产品的半数还要多。

为了尽快能够切换到它们,我们已经决定开始同时进展两件事情。第一个是逐一重写各个关键扩展,包括blitz模板引擎,共享内存/APCu中的数据缓存,pinba数据分析采集器,以及其他内部服务的自定义扩展(总的来说,我们已经通过自己的力量完成大概20种扩展的重写了)。

第二个是积极的清理仅仅在架构中那些非关键部分使用的扩展,让整个架构更加简洁。我们已经迅速清理了11种扩展,都是那些无足轻重的!

另外,我们也同那些维护主要开放扩展的作者,一起积极地讨论PHP7的兼容性(特别感谢xdebug的开发者Derick Rethans)。

我们迟点将进入更详细的关于移植PHP7扩展的技术细节。

开发者已经对PHP7中的内部API做了大量修改,意味着我们可以修改大量的扩展代码了。

下面是几个最重要的变更:

  • zval * -> zval。在早期的版本中,zval一直为新变量来分配内存,但是现在引入了栈。
  • char * -> zend_string。PHP7的引擎使用了更先进的字符串缓存机制。理由是,当字符串与自身的长度同时存储时,新的引擎可以将普通字符串完整的转换为zend-string格式。
  • 数组API的改变。zend_string作为key来使用,同时基于双向链表的数组实现方法也被替代为普通的数组,需要强调的是,数组占用一个大的文件块,而不是很多小的空间。

所有这些都可以从根本上减少小型内存分配的数量,结果是,提高PHP引擎2%的速度。

我们能够注意到,所有这些修改都至少需要改变所有的扩展(即使不是完全重写)。虽然我们可以依赖内置扩展的作者进行必要的修改,我们也当然有责任自己修改他们,虽然工作量很大。由于内部API的修改,使得只修改一些代码段变得简单。

不幸的是,引入使代码执行速度提升的垃圾回收机制让引擎变得更加复杂并且变得更加难以定位问题。涉及到OpCache的问题。在缓存刷新期间,当可用于别的进程的已缓存的文件字节码在此时损坏,就会导致崩溃。这就是它从外部看起来的样子(zend_string):使用方法名或者常量突然崩溃并且垃圾就会出现。

鉴于我们使用了大量的内部扩展,其中许多处理都是专门针对字符串的,我们怀疑这个问题与如何使用字符串在内部扩展有关。我们写了大量的测试,并进行了大量的实验,但没有得到我们预期的结果。最后,我们从PHP引擎开发人员 Dmitri Stogov 那里寻求了帮助。
他的第一个问题是“你有没有清除缓存?”我们解释说,事实上,我们每一次都在清除缓存。在这一点上,我们意识到这个问题并不在我们这里,而是opcache。我们很快就转载了这一案例,这有助于我们在几天内回复并解决这个问题。在7.0.4版本,这个修复没有出来,就不可能使php7进入稳定产品。

PHP的Web应用,处理器的消耗跟其他动态高级语言一样多。但是PHP开发者面对着一个特别的障碍(这让他们成为其他社区恶意攻击的的受害者):缺少JIT,至少没有一个像C/C++语言那样的可编译文本的生成器。PHP社区无力在核心项目框架上去实现一个类似的解决方案更是树立了一种不良的风气:主要的开发成员开始整合他们的解决方案,所以HHVM在Facebook上诞生了,KPHP在VKontakte上诞生,还有其他类似的方案。幸运地是,在2015年,随着PHP7的正式发布,PHP要开始"Grow up"啦。虽然还是没有JIT,但很难去评定这些改变在"engine"中有多重要。现在,尽管没有JIT,PHP7可以跟HHVM相匹敌(Benchmarks from the LightSpeed blogorPHP devs benchmarks)。新的PHP7体系架构将会让JIT的实现变得简单。

/* 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;/* 扩充字段,保存辅助信息 */};

更改测试基础设施

我们为我们在Badoo上做测试感到特别骄傲。我们部署服务器的PHP代码到产品环境,每天两次,每次部署包含20-50份任务量(我们使用功能分支Git和自动化紧JIRA集成版本)。鉴于这种时间表和任务量,我们没有办法不选择自动测试。目前,我们大约有6万个单元测试,约50%的覆盖率,其运行在云上,平均2-3分钟(参见我们的文章了解更多)。除了单元测试,我们使用更高级别的自动测试,集成和系统测试,并为网页做了Selenium测试,为手机客户端做了Calabash测试。作为一个整体,这使我们能够迅速达成与结论有关的代码,每个具体版本的质量,并应用相应的解决方案。

切换到新版本的解释器是一个充满潜在问题的重大变化,所以所有测试工作都是极其重要的。为了弄清我们到底做了什么,以及我们如何设法做到这一点,让我们来看看近几年测试开发在Badoo上是如何演变的。

通常,当我们开始考虑实施产品测试(或在某些情况下,已经开始实施的话)时,在测试过程中我们会发现他们的代码“并没有达到测试阶段”。出于这个原因,在大多数情况下,开发者在写代码时要牢记,代码的可测试性是很重要的。架构师应允许用单元测试去取代调用和外部依赖对象,以便代码测试能与外部环境相隔离。当然,毫无疑问这是一个备受憎恨的要求,很多程序员认为写“可测试性”的代码是完全不可接受的。他们认为,这些限制完全不顾“优秀代码”的标准而且通常不会取得成功。你能想象到,大量不按规则编写的代码,导致测试为了等“一个更好的时机”被延迟,或者通过运行小型测试来满足并且在测试结果被推迟,或实验者为了使自己运行的小测试能够通过,只做了能够通过的那部分(也就是指测试没有产生预期的结果)。
我并不是说我们公司是一个例外,从一开始,我们的项目也未执行测试。因为依然有几行代码在生产过程中正常运作,带来效益,所以正如文献中建议的,如果只是为了运行测试重写代码将是一件愚蠢的事情。那将占用太长的时间,花费太多。

幸运的是我们有一个很棒的工具来解决“未测试代码”的大问题——runkit。当脚本在运行时,这个 PHP 扩展允许你对方法、类及函数进行增、删、改的操作。此工具还有很多其它的功能但我们这里用不到它们。从 2005 年到 2008 年这个工具由 Sara Goleman(就职于 Facebook,有趣的是他在做 HHVM 方向的工作)开发和支持了多年。从 2008 年至今则由 Dmitri Zenovich (带领 Begun 和 Mail.ru 的测试部门)进行维护。我们也对这个项目做了些许贡献。

同时,runkit 是一个非常危险的扩展,它允许你在使用它的脚本在运行的时候对常量、函数及类进行修改。就像是一个允许你在飞行中重建飞机的工具。runkit 有直达 PHP “心脏”的权力,一个小错误或缺陷就能让一切毁掉,导致 PHP 失败或者你要用很多时间来查找内存泄漏或做一些底层的调试。尽管如此,这个工具对于我们的测试还是必要的:不需要做大的重构来完成项目测试只能在程序运行的时候改变代码来实现。

但是在切换到PHP7的时候发现runkit带来了很大麻烦,因为它并不支持新的版本。我们当然也可以在新版本中添加支持,但是从长远考虑,这看起来并不是最可靠的解决途径。因此我们选择了其他方法。

最适合的方法之一就是从runkit迁移到uopz。后者也是PHP的扩展,有着(与runkit)类似的功能性,于2014年正式推出。我在Wamba的同事建议使用uopz,它将有很好的速度体验。顺便说一下uopz的维护者就是Joe Watkins(First Beat Media公司,英国)。不幸的是我们迁移到uopz的测试程序无论怎样都无法成功运行。在某些地方总会发生致命的错误,出现在段错误中。我们提交了一些报告,但很遗憾他们并没有动作(e.g. )。为了解决这种困境而重写测试程序的付出将会非常高昂,即使重写了也很容易再次暴露出问题。

鉴于我们不得不重写大量的代码,而且还要依赖于runkit和uopz这种不知道有没有问题的项目。很明显,我们有了结论:我们应该重写我们的代码,而且要尽可能独立。我们也承诺将尽一切可能来避免今后发生类似的问题,即使我们最终切换到HHVM或任何类似的产品。最终我们做出来了自己的框架。
我们的系统名为“SoftMocks”,“soft”意思是纯php实现,未使用扩展。该项目目前是一个开源的php库。 SoftMocks不跟PHP引擎绑定,它是在运行中动态重写代码,功能类似于Go语言的AOP!框架。
以下功能在我们的代码里已经测试过:

  1. override类方法
  2. 覆盖函数执行结果
  3. 更改全局常量或类常量的值
  4. 类新增方法

所有这些东西都是用runkit实现的。动态修改代码使项目临时变更有了可能性。

我们没有更多篇幅来讨论关于SoftMocks的细节,但我们计划写一篇关于这个主题的文章。 这里我们给出一些关键点:

  • 通过重写中间函数来适配原有的用户代码。因此所有的包含操作将自动被中间函数重写。
  • 在每一个用户定义的方法内都增加了是否有重写的检查。如果存在重写,相应的重写代码就会被执行。 原来直接函数调用的方式将被通过中间函数调用的方式所替换;这样内嵌函数和用户自定义函数都能被执行到。
  • 对中间函数的动态调用将覆盖代码中变量的访问权限

SoftMocks 可以和 Nikita Popov’s 的 PHP-Parser 配合: 这个库不是很快(解析速度大概比token_get_all 慢15倍),但他的接口让你绕过语法解析树,并且包含了一个方便的API 用来处理不确定的语法结构。

现在让我们回到本文主题:切换到PHP 7.0版本。  当我们通过SoftMocks把整个项切换过来后,我们依然有1000多个测试需要手动处理。你可以说这还不算太差的结果,和我们在开始时提到的60000个测试相比的话。 和runkit相比,测试速度没有下降,所以SoftMocks并没有性能问题。 为了公平起见,我们认为uopz 明显的快很多。

尽管PHP7包含了许多新功能,但是仍然存在一些与老版本兼容的问题。首要的解决办法是阅读官方的移植文档,之后我们会马上明白如果不去修改现有代码,我们将会面对的不仅仅是在生产环境中遇到致命的未知错误并且由于升级后代码的改变,我们无法在日志中查找到任何信息。这将会导致程序无法正常运行。

Badoo中有许多PHP代码仓库,其中最大的有超过2百万行代码。此外,我们还使用PHP实现了很多功能,从网站业务逻辑到手机应用后段再到集成测试和代码部署。就目前来说,我们的情况很复杂,毕竟Badoo有很长的历史,我们使用它已经快十年了,最不幸的是仍然有采用PHP4的环境在运行。在Badoo中,我们不推荐用‘just stare at it long enough’的方式来发现问题。一套所谓的’Brazilian’系统将代码部署在生产环境,你需要等待直到它发生错误,这很容易引发大面积用户在使用中遇到业务上的错误,使其不明原因。综上所诉,我们开始寻找一种方法能自动发现不兼容的地方。

最初,我们试图用IDE的,这是开发者中很受欢迎,但不幸的是,他们要么不支持PHP7的语法和特征,要么没有函数可以在代码中找到所有的明显的危险的地方,发现所有明显危险的地方。进行了一些研究(如谷歌搜索)后,我们决定尝试php7mar工具,它是用PHP实现一个静态代码分析仪。这PHP7工具使用起来非常简单,很快工程,并为您提供了一个文本文件。当然,它不是万能的; 找特别是精心隐藏的问题点。尽管如此,该实用程序帮助我们铲除约 90%的问题,大大加快和简化了准备 PHP7 的代码的过程。

对我们来说,最常遇到的和潜在危险的问题是以下内容:

  • 在func_get_arg()以及func_get_args的行为变化()。在PHP的第5版本中,这些功能中的传输的时刻返回参数值,但在七个版本发生这种情况的时刻时func_get_args()被调用。换句话说,如果函数内func_get_args前参数变量的变化()被调用,则该代码的行为可以由五个版本不同。同样的事情发生时,应用程序的业务逻辑坏了,但并没有什么在日志中。
  • 间接访问对象变量,属性和方法。并再次,危险在于,该行为可以更改“静默”。对于那些寻找更多的信息,版本间的差异进行了详细的描述在这里。

     

  • 使用保留类名。在PHP7,可以不再使用布尔,整型,浮点,字符串,空,真假类名称。,是的,我们有一个空的类。它的缺席实际上使事情变得更容易,但因为它常常导致错误。

     

  • 使用引用许多潜在的问题的foreach结构被发现了。由于我们试图早不改变迭代数组中的foreach或虽在其内部指针数,几乎所有的人都表现在版本5和7相同。

剩余的不兼容性的情况下也很少遇到了 (像 ‘e’ 修饰符在正则表达式),或他们固定的一个简单的替换 (例如,现在所有构造函数应该被命名为 __construct()。类名称不允许使用)。
但是,我们即使在开始修复代码之前,我们很担心,一些开发商做一些必要的兼容性变化,其他人会继续写不符合 PHP7 的代码。为了解决这一问题,我们把 pre-receive 钩在已更改的文件 (换句话说,确保语法匹配 PHP7) 上执行 php7-l 在每一个 git 存储库中。这并不能保证不会有任何兼容性问题,但它不会清除主机问题。在其他情况下,开发人员只是不得不变得更加专注。除此之外,我们开始在 PHP7 上运行的测试整个集并与 PHP5 的结果进行了比较。

此外,开发者不允许使用任何PHP7的新功能,例如,我们没有禁止老版本的预接收钩子 php5 -l。这允许我们让代码兼容PHP5和PHP7。为什么这个很重要?因为除了php代码的问题之外,还有PHP7极其自身扩展的一些潜在的问题(这些都可以证实)。并且不幸的是,不是所有的问题都可以在测试环境中重现出来;有一些我们只在产品的大负载时才见过。

在Badoo的平台开发者已经非常关注近些年出现的每一次问题,包括HHVM试点项目,但是我们还是决定等待很有前途的PHP7的到来。现在我们启动了已经基于PHP7的Baboo!这是一个史诗般的项目,拥有300多万行的PHP代码,并且经历了60000次的测试。我们为了处理这些挑战,提出了一个新的PHP引用测试框架(当然,也是开源的),并且在整个过程中节省了上百万美元。

整型直接切换即可:long-zend_long

实践出真知

很明显我们需要一种简单快速的方法在任何数量以及类型的服务器上切换php版本。要启用的话,所有指向CLI-interpreter的代码路径都替换成了 /local/php,相应的,是/local/php5或者/local/php7。这样的话,要在服务器上改变php版本,需要改变链接(为cli脚本操作设置原子操作是很重要的),停止php5-fpm,然后启动php7-fpm。在nginx中,我们使用不同的端口为php-fpm和启动php5-fpm,php7-fom设置两个不同的upstream,但我们不喜欢复杂的nginx配置。

在执行完以上的清单后,我们接着在预发布环境运行Selenium 测试,这个阶段暴露更多我们早期没注意到的问题。这些问题涉及到PHP代码(比如,我们不再使用过期全局变量$HTTP_RAW_POST_DATA,取而代之是 file_get_contents(“php://input”))以及扩展(这里存在各种不同类型的段错误)。
修复完早期发现的问题和重写单元测试(这个过程中我们也发现若干隐藏在解析器的BUG比如这里)后,进入到我们称为“隔离”发布阶段。这个阶段我们在一定数量的服务器上运行新版PHP。一开始我们在每个主要PHP集群(Web后台,移动APP后台,云平台)上只启动一个服务,然后在没有错误出现情况下,一点一点增加服务数量。云平台是第一个完全切换到PHP7的大集群,因为这个集群没有php-fpm需求。 fpm 集群必须等到我们找到或者Dmitri Stogov修复了OpCache问题。之后,我们也会将fpm集群切换到PHP7。

现在看下结果,简单的说,他们是非常出色的。在这里,你能看到响应时间图,包括内存消耗和我们的最大的集群(包括263服务器)的处理器的使用情况,以及在 Prague 数据中心的移动应用后端的使用。

HHVM的试验

/* 定义 */typedef int64_t zend_long;/* else */typedef int32_t zend_long;

响应时间分布:

在切换到PHP7之前,我们曾花了不少时间来寻找优化后端的方法。当然,第一步就是从HHVM下手。在试验了几周之后,我们获得了值得关注的结果:在给框架中的JIT热身之后,我们看到速度与CPU使用率上升了三倍。

字符串类型PHP5.6版本中使用char* + len的方式表示字符串,PHP7.0中做了封装,定义了zend_string类型:

图片 3

另一方面,HHVM 被证实有一些严重的缺点:

struct _zend_string { zend_refcounted_h gc; zend_ulong h; /* hash value */ size_t len; char val[1];};

RUsage (CPU 时间):

部署困难而且慢。在部署过程中,你不得不首先启动JIT-cache。当机器启动的时候,它不能负载产品流量,因为所有的事情进行的相当慢。HHVM 团队同样不推荐启动并行请求。顺便一提,大量聚类操作在启动阶段并不快速。此外,对于几百个机器构成的大集群你必须学习如何分批部署。这样体系结构和部署过程相当繁琐,而且很难估算出所需要的时间。对于我们来说,部署应该尽可能简单快捷。我们的开发者将在同一天提供两个开发版并且释出许多补丁。

zend_string和char*的转换:

图片 4

测试不便。我们非常依赖runkit扩展,但是它在HHVM中却不可用。稍后我们将详细介绍runkit,但是无需多言,它是一个能让你几乎随心所欲更改变量、类、方法、函数行为的扩展。这是通过一个抵达PHP核心的集成来实现的。HHVM引擎仅仅显示了略微相像的PHP外观,但是他们各自的核心十分不同。鉴 于扩展的特定功能,在HHVM上独立地实现runkit异常困难,而且我们不得不重写数万测试用例以确保HHVM和我们的代码正确的工作。这看起来似乎不 值得。公平的说,我们以后在处理所有其他选项时也会遇到同样的问题,而且我们在迁移到PHP7时仍然要重做许多事情包括摆脱runkit。但是以后会更多。

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);

内存使用:

兼容性。主要问题是不完全兼容PHP5.5(参考此处) ,并且不兼容现有的扩展(许多PHP5.5的)。这些所有的不兼容性导致了这个项目的明显缺点: HHVM 不是被大社区开发的,相反只是Facebook的一个分支。在这种情况下公司很容易不参考社区就修改内部规则和标准,而且大量的代码包含其中。换句话说, 他们关起门来利用自己的资源解决了问题。因此,为了解决相似的问题,一个公司需要有Facebook一样的资源不仅投入最初的实现同样要投入后续支持。这 个提议不仅有风险而且可能开销很大,所以我们决定拒绝它。

扩展方法,解析参数时,使用字符串的地方,将‘s’替换成‘S’:

图片 5

潜力。尽管Facebook是一个大公司而且拥有无数顶尖程序员,我们仍然怀疑他们的HHVM开发者比整个PHP社区更强。我们猜想PHP的类似于HHVM的东西会很快出现,而前者将慢慢淡出我们的视野。

/* 例如 */zend_string *zstr;if (zend_parse_parameters(ZEND_NUM_ARGS() , "S", zstr) == FAILURE){ RETURN_LONG(-1);}

CPU 加载 (%)-移动后台集群

让我们耐心等待PHP7。

自定义对象源代码:

图片 6

这一切到位,处理时间减少了一半,从而提高整体响应时间约40%,由于一定量的请求处理时间是花在与数据库和守护进程通信。从逻辑上讲,我们不希望这部分加快切换到php7。除此之外,由于超线程技术,集群的整体负载下降到50%以下,进一步促进了令人印象深刻的结果。广义而言,当负载增加超过50%,HT-engines,而不是作为有用的物理引擎开始工作。但这已经是另一篇文章的主题。此外,记忆的使用,这从来没有一个瓶颈,我们,减少了大约八倍以上!最后,我们节省了机器的数量。换句话说,服务器的数量可以承受更大的负载,从而降低获取和维修设备的费用。在剩余的聚类结果相似,除云上的收益是一个更温和的(大约40%个CPU),由于opcache操作的减少。

来算算我们能节省多少费用呢?大致测算一下,一个Badoo应用服务器集群大概包含600多台服务器。如果cpu使用率减半,我们可以节省大约300台服务器。考虑服务器的硬件成本和折旧,每台大约4000美元。总的算下来我们能节省大约100万美元,另加每年10万的主机托管费。而且这还没有计算对服务云性能的提升带来的价值,这个结果很令人振奋。

另外,您是否也考虑切换到PHP 7.0版本呢? 我们很希望听听您关于此问题的观点,而且非常愿意在下面的评论中回答您的疑问。

Badoo 团队

切换到新版本的PHP7解释器是一个重要和艰难的过程,我们准备建立一个精确的计划。这个计划包括三个阶段:

/* 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];};

修改PHP构建/部署的基础设施和为大量的扩展调整现有的code

zend_object是一个可变长度的结构。因此在自定义对象的结构中,zend_object需要放在最后一项:

改变基础设施和测试环境

/* 例子 */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);}

修改PHP应用程序的代码。

数组

我们稍后会给出这些这些阶段的细节。

7.0中的hash表定义如下,给出了一些注释:/* 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的幂次方 */ uint32_t nInternalPointer; zend_long nNextFreeElement; dtor_func_t pDestructor;};

引擎和扩展的变化

其中,PHP7在zend_hash.h中定义了一系列宏,用来操作数组,包括遍历key、遍历value、遍历key-value等,下面是一个简单例子:

在Badoo中, 我们有积极的支持和更新的PHP分支,我们在PHP7正式版release之前我们就已经开始切换到php7了. 所以我们不得不在我们的代码树经常整合(reBase)PHP7上游的代码,以便它来更新每个候选发布版。我们每天在工作中所用的补丁和自定义的code都需要在两个版本之间进行移植。

/* 数组举例 */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) 

下载和构建依赖库、扩展程序、还包括PHP 5.5和7.0的构建这些过程都是自动化的完成的。这不仅简化了我们目前的工作,也预示着未来:在版本7.1出来时, 也许这一切(解析引擎和扩展等等)都已经准备到位了;

PHP5.6版本中是通过zend_hash_find查找key,然后将结果给到zval **变量,并且查询不到时需要自己分配内存,初始化一个item,设置默认值。2. PHP7中的api变化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_STRINGLMAKE_STD_ZVALPHP5.6中,zval变量是在堆上分配的,创建一个zval变量需要先声明一个指针,然后使用MAKE_STD_ZVAL进行分配空间。PHP7.0中,这个宏已经取消,变量在栈上分配,直接定义一个变量即可,不再需要MAKE_STD_ZVAL,使用到的地方,直接去掉就好。ZEND_RSRC_DTOR_FUNC修改参数名rsrc为res

如上所述,我们将注意力转向扩展。我们提供超过70种扩展,已经比基于我们产品改写的开源产品的半数还要多。

/* 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)

为了尽快能够切换到它们,我们已经决定开始同时进展两件事情。第一个是逐一重写各个关键扩展,包括blitz模板引擎,共享内存/APCu中的数据缓存,pinba数据分析采集器,以及其他内部服务的自定义扩展(总的来说,我们已经通过自己的力量完成大概20种扩展的重写了)。

PHP7.0中,将zend_rsrc_list_entry结构升级为zend_resource,在新版本中只需要修改一下参数名称即可。二级指针宏,即Z_*_PPPHP7.0中取消了所有的PP宏,大部分情况直接使用对应的P宏即可。zend_object_store_get_object被取消根据官方wiki,可以定义如下宏,用来获取object,实际情况看,这个宏用的还是比较频繁的:

第二个是积极的清理仅仅在架构中那些非关键部分使用的扩展,让整个架构更加简洁。我们已经迅速清理了11种扩展,都是那些无足轻重的!

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)))

另外,我们也同那些维护主要开放扩展的作者,一起积极地讨论PHP7的兼容性(特别感谢xdebug的开发者Derick Rethans)。

zend_hash_exists、zend_hash_find对所有需要字符串参数的函数,PHP5.6中的方式是传递两个参数,而PHP7.0中定义了zend_string,因此只需要一个zend_string变量即可。返回值变成了zend_bool类型:

我们迟点将进入更详细的关于移植PHP7扩展的技术细节。

/* 例子 */zend_string * key; key = zend_string_init("key",sizeof("key"), 0);zend_bool res_key = zend_hash_exists(itmeArr, key);

开发者已经对PHP7中的内部API做了大量修改,意味着我们可以修改大量的扩展代码了。

引用1. php5 to phpng: PHP扩展开发及内核应用: PHP 7中新的Hashtable实现和性能改进: 深入理解PHP7之zval: 官方wiki: PHP手册: PHP7 使用资源包裹第三方扩展的实现及其源码解读:五、AMS平台升级PHP7的性能优化成果现网服务是一个非常重要而又敏感的环境,轻则影响用户体验,重则产生现网事故。因此,我们4月下旬完成PHP7编译和测试工作之后,就在AMS其中一台机器进行了灰度上线,观察了几天后,然后逐步扩大灰度范围,在5月初完成升级。这个是我们压测AMS一个查询多个活动计数器的压测结果,以及现网CGI机器,在高峰相同TGW流量场景下的CPU负载数据:就我们的业务压测和现网结果来看,和官方所说的性能提升一倍,基本一致。AMS平台拥有不少的CGI机器,PHP7的升级和应用给我们带来了性能的提升,可以有效节省硬件资源成本。并且,通过Apache2.4的Event模式,我们也增强了Apache在支持并发方面的能力。六、小结我们PHP7升级研发项目组,在过去比较长的一个时间段里,经过持续地努力和推进,终于在2016年4月下旬现网灰度,5月初在集群中全量升级,为我们的AMS活动运营平台带来性能上大幅度的提升。PHP7的革新,对于PHP语言本身而言,具有非凡的意义和价值,这让我更加确信一点,PHP会是一个越来越好的语言。同时,感谢PHP社区的开发者们,为我们业务带来的性能提升。

下面是几个最重要的变更:

zval * -> zval。在早期的版本中,zval一直为新变量来分配内存,但是现在引入了栈。

char * ->Zend_string。PHP7的引擎使用了更先进的字符串缓存机制。理由是,当字符串与自身的长度同时存储时,新的引擎可以将普通字符串完整的转换为zend-string格式。

数组API的改变。zend_string作为key来使用,同时基于双向链表的数组实现方法也被替代为普通的数组,需要强调的是,数组占用一个大的文件块,而不是很多小的空间。

所有这些都可以从根本上减少小型内存分配的数量,结果是,提高PHP引擎2%的速度。

我们能够注意到,所有这些修改都至少需要改变所有的扩展(即使不是完全重写)。虽然我们可以依赖内置扩展的作者进行必要的修改,我们也当然有责任自己修改他们,虽然工作量很大。由于内部API的修改,使得只修改一些代码段变得简单。

不幸的是,引入使代码执行速度提升的垃圾回收机制让引擎变得更加复杂并且变得更加难以定位问题。涉及到OpCache的问题。在缓存刷新期间,当可用于别的进程的已缓存的文件字节码在此时损坏,就会导致崩溃。这就是它从外部看起来的样子(zend_string):使用方法名或者常量突然崩溃并且垃圾就会出现。

鉴于我们使用了大量的内部扩展,其中许多处理都是专门针对字符串的,我们怀疑这个问题与如何使用字符串在内部扩展有关。我们写了大量的测试,并进行了大量的实验,但没有得到我们预期的结果。最后,我们从PHP引擎开发人员Dmitri Stogov那里寻求了帮助。

他的第一个问题是“你有没有清除缓存?”我们解释说,事实上,我们每一次都在清除缓存。在这一点上,我们意识到这个问题并不在我们这里,而是opcache。我们很快就转载了这一案例,这有助于我们在几天内回复并解决这个问题。在7.0.4版本,这个修复没有出来,就不可能使php7进入稳定产品。

更改测试基础设施

我们为我们在Badoo上做测试感到特别骄傲。我们部署服务器的PHP代码到产品环境,每天两次,每次部署包含20-50份任务量(我们使用功能分支Git和自动化紧JIRA集成版本)。鉴于这种时间表和任务量,我们没有办法不选择自动测试。目前,我们大约有6万个单元测试,约50%的覆盖率,其运行在云上,平均2-3分钟(参见我们的文章了解更多)。除了单元测试,我们使用更高级别的自动测试,集成和系统测试,并为网页做了Selenium测试,为手机客户端做了Calabash测试。作为一个整体,这使我们能够迅速达成与结论有关的代码,每个具体版本的质量,并应用相应的解决方案。

切换到新版本的解释器是一个充满潜在问题的重大变化,所以所有测试工作都是极其重要的。为了弄清我们到底做了什么,以及我们如何设法做到这一点,让我们来看看近几年测试开发在Badoo上是如何演变的。

通常,当我们开始考虑实施产品测试(或在某些情况下,已经开始实施的话)时,在测试过程中我们会发现他们的代码“并没有达到测试阶段”。出于这个原因,在大多数情况下,开发者在写代码时要牢记,代码的可测试性是很重要的。架构师应允许用单元测试去取代调用和外部依赖对象,以便代码测试能与外部环境相隔离。当然,毫无疑问这是一个备受憎恨的要求,很多程序员认为写“可测试性”的代码是完全不可接受的。他们认为,这些限制完全不顾“优秀代码”的标准而且通常不会取得成功。你能想象到,大量不按规则编写的代码,导致测试为了等“一个更好的时机”被延迟,或者通过运行小型测试来满足并且在测试结果被推迟,或实验者为了使自己运行的小测试能够通过,只做了能够通过的那部分(也就是指测试没有产生预期的结果)。

我并不是说我们公司是一个例外,从一开始,我们的项目也未执行测试。因为依然有几行代码在生产过程中正常运作,带来效益,所以正如文献中建议的,如果只是为了运行测试重写代码将是一件愚蠢的事情。那将占用太长的时间,花费太多。

幸运的是我们有一个很棒的工具来解决“未测试代码”的大问题——runkit。当脚本在运行时,这个 PHP 扩展允许你对方法、类及函数进行增、删、改的操作。此工具还有很多其它的功能但我们这里用不到它们。从 2005 年到 2008 年这个工具由 Sara Goleman(就职于 Facebook,有趣的是他在做 HHVM 方向的工作)开发和支持了多年。从 2008 年至今则由 Dmitri Zenovich (带领 Begun 和 Mail.ru 的测试部门)进行维护。我们也对这个项目做了些许贡献。

同时,runkit 是一个非常危险的扩展,它允许你在使用它的脚本在运行的时候对常量、函数及类进行修改。就像是一个允许你在飞行中重建飞机的工具。runkit 有直达 PHP “心脏”的权力,一个小错误或缺陷就能让一切毁掉,导致 PHP 失败或者你要用很多时间来查找内存泄漏或做一些底层的调试。尽管如此,这个工具对于我们的测试还是必要的:不需要做大的重构来完成项目测试只能在程序运行的时候改变代码来实现。

但是在切换到PHP7的时候发现runkit带来了很大麻烦,因为它并不支持新的版本。我们当然也可以在新版本中添加支持,但是从长远考虑,这看起来并不是最可靠的解决途径。因此我们选择了其他方法。

最适合的方法之一就是从runkit迁移到uopz。后者也是PHP的扩展,有着(与runkit)类似的功能性,于2014年正式推出。我在Wamba的同事建议使用uopz,它将有很好的速度体验。顺便说一下uopz的维护者就是Joe Watkins(First Beat Media公司,英国)。不幸的是我们迁移到uopz的测试程序无论怎样都无法成功运行。在某些地方总会发生致命的错误,出现在段错误中。我们提交了一些报告,但很遗憾他们并没有动作(e.g.https://github.com/krakjoe/uopz/issues/18)。为了解决这种困境而重写测试程序的付出将会非常高昂,即使重写了也很容易再次暴露出问题。

鉴于我们不得不重写大量的代码,而且还要依赖于runkit和uopz这种不知道有没有问题的项目。很明显,我们有了结论:我们应该重写我们的代码,而且要尽可能独立。我们也承诺将尽一切可能来避免今后发生类似的问题,即使我们最终切换到HHVM或任何类似的产品。最终我们做出来了自己的框架。

我们的系统名为“SoftMocks”,“soft”意思是纯php实现,未使用扩展。该项目目前是一个开源的php库。 SoftMocks不跟PHP引擎绑定,它是在运行中动态重写代码,功能类似于Go语言的AOP!框架。

以下功能在我们的代码里已经测试过:

override类方法

覆盖函数执行结果

更改全局常量或类常量的值

类新增方法

所有这些东西都是用runkit实现的。动态修改代码使项目临时变更有了可能性。

我们没有更多篇幅来讨论关于SoftMocks的细节,但我们计划写一篇关于这个主题的文章。 这里我们给出一些关键点:

通过重写中间函数来适配原有的用户代码。因此所有的包含操作将自动被中间函数重写。

在每一个用户定义的方法内都增加了是否有重写的检查。如果存在重写,相应的重写代码就会被执行。 原来直接函数调用的方式将被通过中间函数调用的方式所替换;这样内嵌函数和用户自定义函数都能被执行到。

对中间函数的动态调用将覆盖代码中变量的访问权限

SoftMocks 可以和 NikitaPopov's 的PHP-Parser配合: 这个库不是很快(解析速度大概比token_get_all 慢15倍),但他的接口让你绕过语法解析树,并且包含了一个方便的API 用来处理不确定的语法结构。

现在让我们回到本文主题:切换到PHP 7.0版本。  当我们通过SoftMocks把整个项切换过来后,我们依然有1000多个测试需要手动处理。你可以说这还不算太差的结果,和我们在开始时提到的60000个测试相比的话。 和runkit相比,测试速度没有下降,所以SoftMocks并没有性能问题。 为了公平起见,我们认为uopz 明显的快很多。

尽管PHP7包含了许多新功能,但是仍然存在一些与老版本兼容的问题。首要的解决办法是阅读官方的移植文档,之后我们会马上明白如果不去修改现有代码,我们将会面对的不仅仅是在生产环境中遇到致命的未知错误并且由于升级后代码的改变,我们无法在日志中查找到任何信息。这将会导致程序无法正常运行。

Badoo中有许多PHP代码仓库,其中最大的有超过2百万行代码。此外,我们还使用PHP实现了很多功能,从网站业务逻辑到手机应用后段再到集成测试和代码部署。就目前来说,我们的情况很复杂,毕竟Badoo有很长的历史,我们使用它已经快十年了,最不幸的是仍然有采用PHP4的环境在运行。在Badoo中,我们不推荐用‘just stare at it long enough’的方式来发现问题。一套所谓的'Brazilian'系统将代码部署在生产环境,你需要等待直到它发生错误,这很容易引发大面积用户在使用中遇到业务上的错误,使其不明原因。综上所诉,我们开始寻找一种方法能自动发现不兼容的地方。

最初,我们试图用IDE的,这是开发者中很受欢迎,但不幸的是,他们要么不支持PHP7的语法和特征,要么没有函数可以在代码中找到所有的明显的危险的地方,发现所有明显危险的地方。进行了一些研究(如谷歌搜索)后,我们决定尝试php7mar工具,它是用PHP实现一个静态代码分析仪。这PHP7工具使用起来非常简单,很快工程,并为您提供了一个文本文件。当然,它不是万能的; 找特别是精心隐藏的问题点。尽管如此,该实用程序帮助我们铲除约 90%的问题,大大加快和简化了准备 PHP7 的代码的过程。

对我们来说,最常遇到的和潜在危险的问题是以下内容:

在func_get_arg()以及func_get_args的行为变化()。在PHP的第5版本中,这些功能中的传输的时刻返回参数值,但在七个版本发生这种情况的时刻时func_get_args()被调用。换句话说,如果函数内func_get_args前参数变量的变化()被调用,则该代码的行为可以由五个版本不同。同样的事情发生时,应用程序的业务逻辑坏了,但并没有什么在日志中。

间接访问对象变量,属性和方法。并再次,危险在于,该行为可以更改“静默”。对于那些寻找更多的信息,版本间的差异进行了详细的描述在这里。

使用保留类名。在PHP7,可以不再使用布尔,整型,浮点,字符串,空,真假类名称。,是的,我们有一个空的类。它的缺席实际上使事情变得更容易,但因为它常常导致错误。

使用引用许多潜在的问题的foreach结构被发现了。由于我们试图早不改变迭代数组中的foreach或虽在其内部指针数,几乎所有的人都表现在版本5和7相同。

剩余的不兼容性的情况下也很少遇到了 (像 'e' 修饰符在正则表达式),或他们固定的一个简单的替换 (例如,现在所有构造函数应该被命名为 __construct()。类名称不允许使用)。

但是,我们即使在开始修复代码之前,我们很担心,一些开发商做一些必要的兼容性变化,其他人会继续写不符合 PHP7 的代码。为了解决这一问题,我们把 pre-receive 钩在已更改的文件 (换句话说,确保语法匹配 PHP7) 上执行 php7-l 在每一个 git 存储库中。这并不能保证不会有任何兼容性问题,但它不会清除主机问题。在其他情况下,开发人员只是不得不变得更加专注。除此之外,我们开始在 PHP7 上运行的测试整个集并与 PHP5 的结果进行了比较。

此外,开发者不允许使用任何PHP7的新功能,例如,我们没有禁止老版本的预接收钩子 php5 -l。这允许我们让代码兼容PHP5和PHP7。为什么这个很重要?因为除了php代码的问题之外,还有PHP7极其自身扩展的一些潜在的问题(这些都可以证实)。并且不幸的是,不是所有的问题都可以在测试环境中重现出来;有一些我们只在产品的大负载时才见过。

实践出真知

很明显我们需要一种简单快速的方法在任何数量以及类型的服务器上切换php版本。要启用的话,所有指向CLI-interpreter的代码路径都替换成了 /local/php,相应的,是/local/php5或者/local/php7。这样的话,要在服务器上改变php版本,需要改变链接(为cli脚本操作设置原子操作是很重要的),停止php5-fpm,然后启动php7-fpm。在Nginx中,我们使用不同的端口为php-fpm和启动php5-fpm,php7-fom设置两个不同的upstream,但我们不喜欢复杂的nginx配置。

在执行完以上的清单后,我们接着在预发布环境运行Selenium 测试,这个阶段暴露更多我们早期没注意到的问题。这些问题涉及到PHP代码(比如,我们不再使用过期全局变量$HTTP_RAW_POST_DATA,取而代之是 file_get_contents(“php://input”))以及扩展(这里存在各种不同类型的段错误)。

修复完早期发现的问题和重写单元测试(这个过程中我们也发现若干隐藏在解析器的BUG比如这里)后,进入到我们称为“隔离”发布阶段。这个阶段我们在一定数量的服务器上运行新版PHP。一开始我们在每个主要PHP集群(Web后台,移动APP后台,云平台)上只启动一个服务,然后在没有错误出现情况下,一点一点增加服务数量。云平台是第一个完全切换到PHP7的大集群,因为这个集群没有php-fpm需求。 fpm 集群必须等到我们找到或者Dmitri Stogov修复了OpCache问题。之后,我们也会将fpm集群切换到PHP7。

现在看下结果,简单的说,他们是非常出色的。在这里,你能看到响应时间图,包括内存消耗和我们的最大的集群(包括263服务器)的处理器的使用情况,以及在 Prague 数据中心的移动应用后端的使用。

响应时间分布:

图片 7

RUsage (CPU 时间):

图片 8

内存使用:

图片 9

CPU 加载 (%)-移动后台集群

图片 10

这一切到位,处理时间减少了一半,从而提高整体响应时间约40%,由于一定量的请求处理时间是花在与数据库和守护进程通信。从逻辑上讲,我们不希望这部分加快切换到php7。除此之外,由于超线程技术,集群的整体负载下降到50%以下,进一步促进了令人印象深刻的结果。广义而言,当负载增加超过50%,HT-engines,而不是作为有用的物理引擎开始工作。但这已经是另一篇文章的主题。此外,记忆的使用,这从来没有一个瓶颈,我们,减少了大约八倍以上!最后,我们节省了机器的数量。换句话说,服务器的数量可以承受更大的负载,从而降低获取和维修设备的费用。在剩余的聚类结果相似,除云上的收益是一个更温和的(大约40%个CPU),由于opcache操作的减少。

来算算我们能节省多少费用呢?大致测算一下,一个Badoo应用服务器集群大概包含600多台服务器。如果cpu使用率减半,我们可以节省大约300台服务器。考虑服务器的硬件成本和折旧,每台大约4000美元。总的算下来我们能节省大约100万美元,另加每年10万的主机托管费。而且这还没有计算对服务云性能的提升带来的价值,这个结果很令人振奋。

另外,您是否也考虑切换到PHP 7.0版本呢? 我很希望听听大家关于此问题的观点,而且非常愿意在下面的评论中回答您的疑问。

本文由威尼斯www.9778.com发布于Web前端技术,转载请注明出处:Badoo 告诉你切换到 PHP7 节省了 100 万美元

关键词:

PHP爬虫:百万级别知乎用户数据爬取与分析

这次抓取了110万的用户数据,数据分析结果如下: 开发前的准备 安装Linux系统(Ubuntu14.04),在VMWare虚拟机下安装一...

详细>>

PHP中的防御性编程

本文由码农网 –邱康原创翻译,转发请看清文末的转发要求,款待参加大家的付费投稿安顿!   发愤忘食27:创立性...

详细>>

如何正确配置 Nginx + PHP

对很多人而言,配置Nginx+PHP无外乎就是搜索一篇教程,然后拷贝粘贴。听上去似乎也没什么问题,可惜实际上网络上...

详细>>

PHP威尼斯www.9778.com 面试题10个值得深思问题

Q7 经过下面的运算 $x的值应该是多少? $x = 3 + "15%" + "$25" 答案是18,PHP是会根据上下文实现类型的自动转换 上面的代...

详细>>