php 7的新特性

介绍

我们成功的把我们的应用迁移到了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引用测试框架(当然,也是开源的),并且在整个过程中节省了上百万美元。

1.性能优化,重写了zend 引擎。像facebook (HHVM) 为了提升PHP6的效率,在php
7在wordpress测试。超过HHVM.

背景

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应用程序的代码。

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

2.PHP7 函数增加了参数和返回值类型限定。

HHVM 是 Facebook 开发的高性能 PHP 虚拟机,宣称比官方的快 9
倍,我很好奇,于是抽空简单了解了一下,并整理出这篇文章,希望能回答清楚两方面的问题:

引擎和扩展的变化

在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进入稳定产品。

3.try catch 的错误异常捕获。

  • HHVM 到底靠谱么?是否可以用到产品中?
  • 它为什么比官方的 PHP 快很多?到底是如何优化的?

更改测试基础设施

我们为我们在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极其自身扩展的一些潜在的问题(这些都可以证实)。并且不幸的是,不是所有的问题都可以在测试环境中重现出来;有一些我们只在产品的大负载时才见过。

4.重写引擎

你会怎么做?

实践出真知

很明显我们需要一种简单快速的方法在任何数量以及类型的服务器上切换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 数据中心的移动应用后端的使用。

           a.zval 直接使用栈内存

在讨论 HHVM 实现原理前,我们先设身处地想想:假设你有个 PHP
写的网站遇到了性能问题,经分析后发现很大一部分资源就耗在 PHP
上,这时你会怎么优化 PHP 性能?

响应时间分布:

            b.zend_string 存储hash值 时间复杂度 O(1)。

比如可以有以下几种方式:

图片 2

            c.hashtable桶内直接存储数据。

  • 方案 1,迁移到性能更好的语言上,如 Java、C++、Go。
  • 方案 2,通过 RPC 将功能分离出来用其它语言实现,让 PHP
    做更少的事情,比如 Twitter 就将大量业务逻辑放到了 Scala 中,前端的
    Rails 只负责展现。
  • 方案 3,写 PHP 扩展,在性能瓶颈地方换 C/C++。
  • 方案 4,优化 PHP 的性能。

RUsage (CPU 时间):

            e.int  floath bool 直接拷贝

方案 1几乎不可行,十年前 Joel 就拿 Netscape
的例子警告过,你将放弃是多年的经验积累,尤其是像
Facebook 这种业务逻辑复杂的产品,PHP 代码实在太多了,据称有 2
千万行(引用自 [PHP on the Metal with
HHVM]),修改起来的成本恐怕比写个虚拟机还大,而且对于一个上千人的团队,从头开始学习也是不可接受的。

图片 3

  5.php 7版本不会携带JIT (JUST IN TIME ):程序运行时是将php Opcode
直接转换机器指令。

方案 2是最保险的方案,可以逐步迁移,事实上 Facebook
也在朝这方面努力了,而且还开发了 Thrift 这样的 RPC 解决方案,Facebook
内部主要使用的另一个语言是 C++,从早期的 Thrift
代码就能看出来,因为其它语言的实现都很简陋,没法在生产环境下使用。

内存使用:

目前在 Facebook 中据称 PHP:C++ 已经从 9:1 增加到 7:3
了,加上有
Andrei Alexandrescu 的存在,C++ 在 Facebook
中越来越流行,但这只能解决部分问题,毕竟 C++ 开发成本比 PHP
高得多,不适合用在经常修改的地方,而且太多 RPC 的调用也会严重影响性能。

图片 4

方案
3
看起来美好,实际执行起来却很难,一般来说性能瓶颈并不会很显著,大多是不断累加的结果,加上
PHP
扩展开发成本高,这种方案一般只用在公共且变化不大的基础库上,所以这种方案解决不了多少问题。

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

可以看到,前面 3 个方案并不能很好地解决问题,所以 Facebook
其实没有选择的余地,只能去考虑 PHP 本身的优化了。

图片 5

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

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

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

Badoo 团队

更快的 PHP

既然要优化 PHP,那如何去优化呢?在我看来可以有以下几种方法:

  • 方案 1,PHP 语言层面的优化。
  • 方案 2,优化 PHP 的官方实现(也就是 Zend)。
  • 方案 3,将 PHP 编译成其它语言的
    bytecode(字节码),借助其它语言的虚拟机(如 JVM)来运行。
  • 方案 4,将 PHP 转成 C/C++,然后编译成本地代码。
  • 方案 5,开发更快的 PHP 虚拟机。

PHP 语言层面的优化是最简单可行的,Facebook 当然想到了,而且还开发了
XHProf
这样的性能分析工具,对于定位性能瓶颈是很有帮助的。

不过 XHProf 还是没能很好解决 Facebook
的问题,所以我们继续看,接下来是方案 2,简单来看,Zend
的执行过程可以分为两部分:将 PHP 编译为 opcode、执行 opcode,所以优化
Zend 可以从这两方面来考虑。
优化 opcode 是一种常见的做法,可以避免重复解析
PHP,而且还能做一些静态的编译优化,比如 Zend Optimizer
Plus,但由于
PHP 语言的动态性,这种优化方法是有局限性的,乐观估计也只能提升
20%的性能。另一种考虑是优化 opcode
架构本身,如基于寄存器的方式,但这种做法修改起来工作量太大,性能提升也不会特别明显(可能
30%?),所以投入产出比不高。

另一个方法是优化 opcode 的执行,首先简单提一下 Zend 是如何执行的,Zend
的 interpreter(也叫解释器)在读到 opcode 后,会根据不同的 opcode
调用不同函数(其实有些是
switch,不过为了描述方便我简化了),然后在这个函数中执行各种语言相关的操作(感兴趣的话可看看深入理解
PHP
内核这本书),所以
Zend 中并没有什么复杂封装和间接调用,作为一个解释器来说已经做得很好了。

想要提升 Zend
的执行性能,就需要对程序的底层执行有所解,比如函数调用其实是有开销的,所以能通过
Inline
threading
来优化掉,它的原理就像 C 语言中的 inline
关键字那样,但它是在运行时将相关的函数展开,然后依次执行(只是打个比方,实际实现不太一样),同时还避免了
CPU 流水线预测失败导致的浪费。

另外还可以像
JavaScriptCore

LuaJIT
那样使用汇编来实现 interpreter,具体细节建议看看 Mike
的解释

但这两种做法修改代价太大,甚至比重写一个还难,尤其是要保证向下兼容,后面提到
PHP 的特点时你就知道了。

开发一个高性能的虚拟机不是件简单的事情,JVM 花了 10
多年才达到现在的性能,那是否能直接利用这些高性能的虚拟机来优化 PHP
的性能呢?这就是方案 3 的思路。

其实这种方案早就有人尝试过了,比如
Quercus 和 IBM
的 P8,Quercus 几乎没见有人使用,而 P8
也已经死掉了。Facebook
也曾经调研过这种方式,甚至还出现过不靠谱的传闻
,但其实 Facebook 在 2011 年就放弃了。

因为方案 3 看起来美好,但实际效果却不理想,按照很多大牛的说法(比如
Mike),VM
总是为某个语言优化的,其它语言在上面实现会遇到很多瓶颈,比如动态的方法调用,关于这点在
Dart
的文档中有过介绍,而且据说
Quercus 的性能与 Zend+APC 比差不了太多([来自 The HipHop Compiler for
PHP]),所以没太大意义。

不过 OpenJDK 这几年也在努力,最近的
Grall
项目看起来还不错,也有语言在上面取得了显著的效果,但我还没空研究
Grall,所以这里无法判断。

接下来是方案 4,它正是 HPHPc(HHVM 的前身)的做法,原理是将 PHP 代码转成
C++,然后编译为本地文件,可以认为是一种 AOT(ahead of
time)的方式,关于其中代码转换的技术细节可以参考 The HipHop Compiler
for
PHP
这篇论文,以下是该论文中的一个截图,可以通过它来大概了解:

图片 6

Paste_Image.png

这种做法的最大优点是实现简单(相对于一个 VM
来说),而且能做很多编译优化(因为是离线的,慢点也没事),比如上面的例子就将-
1
优化掉了,但它很难支持 PHP 中的很多动态的方法,如 eval()
、create_function(),因为这就得再内嵌一个 interpreter,成本不小,所以
HPHPc 干脆就直接不支持这些语法。

除了 HPHPc,还有两个类似的项目,一个是
Roadsend,另一个是
phc ,phc
的做法是将 PHP 转成了 C 再编译,以下是它将file_get_contents($f)
转成 C 代码的例子:

tatic php_fcall_info fgc_info;
php_fcall_info_init ("file_get_contents", &fgc_info);
php_hash_find (LOCAL_ST, "f", 5863275, &fgc_info.params);
php_call_function (&fgc_info);

话说 phc
作者曾经在博客上哭诉,说他两年前就去
Facebook 演示过 phc
了,还和那里的工程师交流过,结果人家一发布就火了,而自己忙活了 4
年却默默无闻,现在前途渺茫。。。

Roadsend 也已经不维护了,对于 PHP
这样的动态语言来说,这种做法有很多的局限性,由于无法动态
include,Facebook 将所有文件都编译到了一起,上线时的文件部署居然达到了
1G,越来越不可接受了。

另外有还有一个叫 PHP
QB
的项目,由于时间关系我没有看,感觉可能是类似的东东。

所以就只剩下一条路了,那就是写一个更快的 PHP
虚拟机,将一条黑路走到底,或许你和我一样,一开始听到 Facebook
要做一个虚拟机是觉得太离谱,但如果仔细分析就会发现其实也只有这样了。

更快的虚拟机

HHVM 为什么更快?在各种新闻报道中都提到了 JIT
这个关键技术,但其实远没有那么简单,JIT
不是什么神奇的魔法棒,用它轻轻一挥就能提升性能,而且 JIT
这个操作本身也是会耗时的,对于简单的程序没准还比 interpreter
慢,最极端的例子是 LuaJIT
2
的 Interpreter 就稍微比 V8 的 JIT
快,所以并不存在绝对的事情,更多还是在细节问题的处理上,HHVM
的发展历史就是不断优化的历史,你可以从下图看到它是如何一点点超过 HPHPc
的:

图片 7

Paste_Image.png

值得一提的是在 Android 4.4 中新的虚拟机 ART 就采用的是 AOT
方案(还记得么?前面提到的 HPHPc 就是这种),结果比之前使用 JIT 的
Dalvik 快了一倍,所以说 JIT 也不一定比 AOT 快。

因此这个项目是有很大风险的,如果没有强大的内心和毅力,极有可能半途而废,Google
就曾经想用 JIT 提升 Python
的性能,但最终失败了,对于
Google 来说用到 Python 的地方其实并没什么性能问题(好吧,以前 Google
是用 Python 写过 crawl [参考 In The Plex],但那都是 1996
年的事情了)。

比起 Google,Facebook 显然有更大的动力和决心,PHP 是 Facebook
最重要的语言,我们来看看 Facebook 都投入了哪些大牛到这个项目中(不全):

  • Andrei Alexandrescu,『Modern C++ Design』和『C++ Coding
    Standards』的作者,C++ 领域无可争议的大神
  • Keith Adams,负责过 VMware 核心架构,当年 VMware 就派他一人去和
    Intel 进行技术合作,足以证明在
    VMM
    领域他有多了解了
  • Drew Paroski,在微软参与过 .NET 虚拟机开发,改进了其中的 JIT
  • Jason Evans,开发了 jemalloc,减少了 Firefox 一半的内存消耗
  • Sara Golemon,『Extending and Embedding PHP』的作者,PHP
    内核专家,这本书估计所有 PHP 高手都看过吧,或许你不知道其实她是女的

虽然没有像 Lars Bak、Mike Pall
这样在虚拟机领域的顶级专家,但如果这些大牛能齐心协力,写个虚拟机还是问题不大的,那么他们将面临什么样的挑战呢?接下来我们一一讨论。

规范是什么?

自己写 PHP 虚拟机要面临的第一个问题就是 PHP
没有语言规范,很多版本间的语法还会不兼容(甚至是小版本号,比如 5.2.1 和
5.2.3),PHP 语言规范究竟如何定义呢?来看一篇来自
IEEE
的说法:

The PHP group claim that they have the fi nal say in the speci fi cation of (the language) PHP. This groups speci fi cation is an implementation, and there is no prose speci fi cation or agreed validation suite.

所以唯一的途径就是老老实实去看 Zend 的实现,好在 HPHPc
中已经痛苦过一次了,所以 HHVM 能直接利用现成,因此这个问题并不算太大。

语言还是扩展?

实现 PHP 语言不仅仅只是实现一个虚拟机那么简单,PHP
语言本身还包括了各种扩展,这些扩展和语言是一体的,Zend
不辞辛劳地实现了各种你可能会用到的功能。如果分析过 PHP
的代码,就会发现它的 C 代码除去空行注释后居然还有 80+ 万行,而你猜其中
Zend 引擎部分有多少?只有不到 10 万行。

对于开发者来说这不是什么坏事,但对于引擎实现者来说就很悲剧了,我们可以拿
Java 来进行对比,写个 Java 的虚拟机只需实现字节码解释及一些基础的 JNI
调用,Java 绝大部分内置库都是用 Java
实现的,所以如果不考虑性能优化,单从工作量看,实现 PHP 虚拟机比 JVM
要难得多,比如就有人用 8 千行的 TypeScript 实现了一个 JVM
Doppio。

而对于这个问题,HHVM 的解决办法很简单,那就是只实现 Facebook
中用到的,而且同样可以先用 HPHPc 中之前写过的,所以问题也不大。

实现 Interpreter

接下来是 Interpreter 的实现,在解析完 PHP 后会生成 HHVM 自己设计的一种
Bytecode,存储在 ~/.hhvm.hhbc
(SQLite 文件) 中以便重用,在执行 Bytecode 时和 Zend
类似,也是将不同的字节码放到不同的函数中去实现(这种方式在虚拟机中有个专门的称呼:Subroutine
threading)

Interpreter 的主体实现在
bytecode.cpp
中,比如
VMExecutionContext::iopAdd这样的方法,最终执行会根据不同类型来区分,比如
add 操作的实现是在
tv-arith.cpp
中,下面摘抄其中的一小段

if (c2.m_type == KindOfInt64)  return o(c1.m_data.num, c2.m_data.num);
if (c2.m_type == KindOfDouble) return o(c1.m_data.num, c2.m_data.dbl);

正是因为有了 Interpreter,HHVM 在对于 PHP 语法的支持上比 HPHPc
有明显改进,理论上做到完全兼容官方 PHP,但仅这么做在性能并不会比 Zend
好多少,由于无法确定变量类型,所以需要加上类似上面的条件判断语句,但这样的代码不利于现代
CPU 的执行优化,另一个问题是数据都是 boxed 的,每次读取都需要通过类似
m_data.num 和 m_data.dbl 的方法来间接获取。

对于这样的问题,就得靠 JIT 来优化了。

实现 JIT 及优化

首先值得一提的是 PHP 的 JIT 之前并非没人尝试过:

  • 2008 年就有人用 LLVM
    实验过,结果还比原来慢了
    21 倍。。。
  • 2010 年 IBM 日本研究院基于他们的 JVM 虚拟机代码开发了 P9,性能是官方
    PHP 的 2.5 到 9.5 倍,可以看他们的论文Evaluation of a just-in-time
    compiler retrofitted for
    PHP。
  • 2011 年 Andrei Homescu 基于 RPython 开发过,还写了篇论文 HappyJIT:
    a tracing JIT compiler for
    PHP,但测试结果有好有坏,并不理想。

那么究竟什么是 JIT?如何实现一个 JIT?

在动态语言中基本上都会有个 eval 方法,可以传给它一段字符串来执行,JIT
做的就是类似的事情,只不过它要拼接不是字符串,而是不同平台下的机器码,然后进行执行,但如何用
C 来实现呢?可以参考 Eli
写的这个入门例子,以下是文中的一段代码

unsigned char code[] = {
  0x48, 0x89, 0xf8,                   // mov %rdi, %rax
  0x48, 0x83, 0xc0, 0x04,             // add $4, %rax
  0xc3                                // ret
};
memcpy(m, code, sizeof(code));

然而手工编写机器码很容易出错,所以最好的有一个辅助的库,比如的 Mozilla

Nanojit
以及 LuaJIT 的
DynASM,但
HHVM 并没有使用这些,而是自己实现了一个只支持 x64 的(另外还在尝试用
VIXL
来生成 ARM 64 位的),通过 mprotect 的方式来让代码可执行。

但为什么 JIT 代码会更快?你可以想想其实用 C++
编写的代码最终编译出来也是机器码,如果只是将同样的代码手动转成了机器码,那和
GCC 生成出来的有什么区别呢?虽然前面我们提到了一些针对 CPU
实现原理来优化的技巧,但在 JIT
中更重要的优化是根据类型来生成特定的指令,从而大幅减少指令数和条件判断,下面这张来自
TraceMonkey
的图对此进行了很直观的对比,后面我们将看到 HHVM 中的具体例子:

图片 8

Paste_Image.png

VM 首先通过 interpeter 来执行,那它会在时候使用 JIT 呢?常见的 JIT
触发条件有 2 种:

  • trace:记录循环执行次数,如果超过一定数量就对这段代码进行 JIT
  • method:记录函数执行次数,如果超过一定数量就对整个函数进行
    JIT,甚至直接 inline

关于这两种方法哪种更好在 Lambada
上有个帖子引来了各路大神的讨论,尤其是
Mike Pall(LuaJIT 作者) 、Andreas Gal(Mozilla VP) 和 Brendan
Eich(Mozilla
CTO)都发表了很多自己的观点,推荐大家围观,我这里就不献丑了。

它们之间的区别不仅仅是编译范围,还有很多细节问题,比如对局部变量的处理,在这里就不展开了

但 HHVM 并没有采用这两种方式,而是自创了一个叫
tracelet
的做法,它是根据类型来划分的,看下面这张图

图片 9

Paste_Image.png

可以看到它将一个函数划分为了 3 部分,上面 2 部分是用于处理 $k
为整数或字符串两种不同情况的,下面的部分是返回值,所以看起来它主要是根据类型的变化情况来划分
JIT 区域的,具体是如何分析和拆解 Tracelet 的细节可以查看
Translator.cpp
中的Translator::analyze
方法,我还没空看,这里就不讨论了。

当然,要实现高性能的 JIT 还需进行各种尝试和优化,比如最初 HHVM 新增的
tracelet 会放到前面,也就是将上图的 A 和 C
调换位置,后来尝试了一下放到后面,结果性能提示了
14%,因为测试发现这样更容易提前命中响应的类型

JIT 的执行过程是首先将 HHBC 转成 SSA (hhbc-translator.cpp),然后对 SSA
上做优化(比如 Copy propagation),再生成本地机器码,比如在 X64 下是由
translator-x64.cpp
实现的。

我们用一个简单的例子来看看 HHVM 最终生成的机器码是怎样的,比如下面这个
PHP 函数:

<?php
function a($b){
  echo $b + 2;
}

编译后是这个样子:

mov rcx,0x7200000
mov rdi,rbp
mov rsi,rbx
mov rdx,0x20
call 0x2651dfb <HPHP::Transl::traceCallback(HPHP::ActRec*, HPHP::TypedValue*, long, void*)>
cmp BYTE PTR [rbp-0x8],0xa
jne 0xae00306
; 前面是检查参数是否有效

mov rcx,QWORD PTR [rbp-0x10]           ; 这里将 %rcx 被赋值为 1 了
mov edi,0x2                            ; 将 %edi(也就是 %rdi 的低 32 位)赋值为 2
add rdi,rcx                            ; 加上 %rcx
call 0x2131f1b <HPHP::print_int(long)> ; 调用 print_int 函数,这时第一个参数 %rdi 的值已经是 3 了

; 后面暂不讨论
mov BYTE PTR [rbp+0x28],0x8
lea rbx,[rbp+0x20]
test BYTE PTR [r12],0xff
jne 0xae0032a
push QWORD PTR [rbp+0x8]
mov rbp,QWORD PTR [rbp+0x0]
mov rdi,rbp
mov rsi,rbx
mov rdx,QWORD PTR [rsp]
call 0x236b70e <HPHP::JIT::traceRet(HPHP::ActRec*, HPHP::TypedValue*, void*)>
ret 

而 HPHP::print_int 函数的实现是这样的:

void print_int(int64_t i) {
  char buf[256];
  snprintf(buf, 256, "%" PRId64, i);
  echo(buf);
  TRACE(1, "t-x64 output(int): %" PRId64 "n", i);
}

可以看到 HHVM 编译出来的代码直接使用了 int64_t,避免了 interpreter
中需要判断参数和间接取数据的问题,从而明显提升了性能,最终甚至做到了和 C
编译出来的代码区别不大。

需要注意:HHVM 在 server mode 下,只有超过 12 个请求就才会触发
JIT,启动过 HHVM 时可以通过加上如下参数来让它首次请求就使用 JIT:

-v Eval.JitWarmupRequests=0

所以在测试性能时需要注意,运行一两次就拿来对比是看不出效果的。

类型推导很麻烦,还是逼迫程序员写清楚吧

JIT 的关键是猜测类型,因此某个变量的类型要是老变就很难优化,于是 HHVM
的工程师开始考虑在 PHP 语法上做手脚,加上类型的支持,推出了一个新语言 –
Hack(吐槽一下这名字真不利于 SEO),它的样子如下:

<?hh
class Point2 {
  public float $x, $y;
  function __construct(float $x, float $y) {
    $this->x = $x;
    $this->y = $y;
  }
}
<?hh
class Point2 {
  public float $x, $y;
  function __construct(float $x, float $y) {
    $this->x = $x;
    $this->y = $y;
  }
}
//来自:https://raw.github.com/strangeloop/StrangeLoop2013/master/slides/sessions/Adams-TakingPHPSeriously.pdf

意到 float
关键字了么?有了静态类型可以让 HHVM 更好地优化性能,但这也意味着和 PHP
语法不兼容,只能使用 HHVM。
其实我个人认为这样做最大的优点是让代码更加易懂,减少无意的犯错,就像
Dart 中的可选类型也是这个初衷,同时还方便了 IDE 识别,据说 Facebook
还在开发一个基于 Web 的
IDE,能协同编辑代码,可以期待一下。

你会使用 HHVM 么?

总的来说,比起之前的 HPHPc,我认为 HHVM
是值得一试的,它是真正的虚拟机,能够更好地支持各种 PHP
的语法,所以改动成本不会更高,而且因为能无缝切换到官方 PHP
版本,所以可以同时启动 FPM 来随时待命,HHVM 还有
FastCGI
接口方便调用,只要做好应急备案,风险是可控的,从长远来看是很有希望的。

性能究竟能提升多少我无法确定,需要拿自己的业务代码来进行真实测试,这样才能真正清楚
HHVM
能带来多少收益,尤其是对整体性能提升到底有多少,只有拿到这个数据才能做决策。

最后整理一下可能会遇到的问题,有计划使用的可以参考:

  • 扩展问题:如果用到了 PHP 扩展,肯定是要重写的,不过 HHVM
    扩展写起来比 Zend 要简单的多,具体细节可以看 wiki
    上的例子。
  • HHVM Server
    的稳定性问题:这种多线程的架构运行一段时间可能会出现内存泄露问题,或者某个没写好的
    PHP 直接导致整个进程挂掉,所以需要注意这方面的测试和容灾措施。
  • 问题修复困难:HHVM 在出现问题时将比 Zend 难修复,尤其是 JIT
    的代码,只能期望它比较稳定了。

P.S. 其实我只了解基本的虚拟机知识,也没写过几行 PHP
代码,很多东西都是写这篇文章时临时去找资料的,由于时间仓促水平有限,必然会有不正确的地方,欢迎大家评论赐教
🙂

2014 年 1 月补充:目前 HHVM 在鄙厂的推广势头很不错,推荐大家在 2014
年尝试一下,尤其是现在兼容性测试已经达到 98.58%了,修改成本进一步减小。

2014 年 4 月补充:配图来自 @reeze,是某超大流量产品的效果。

引用

  • Andrei Alexandrescu on
    AMA
  • Keith Adams 在 HN
    上的蛛丝马迹
  • How Three Guys Rebuilt the Foundation of
    Facebook
  • PHP on the Metal with
    HHVM
  • Making HPHPi
    Faster
  • HHVM Optimization
    Tips
  • The HipHop Virtual Machine (hhvm) PHP Execution at the Speed of
    JIT
  • Julien Verlaguet, Facebook: Analyzing PHP
    statically
  • Speeding up PHP-based development with
    HHVM
  • Adding an opcode to
    HHBC

原文出处

发表评论

电子邮件地址不会被公开。 必填项已用*标注