Box 迁移到 HHVM 实践

有时候,我们会听说关于一些公司采用 Facebook 的开源项目的事情。Box
团队近期给我们发送了他们是如何使用 HHVM
的故事,是一个很好的文章。所以我们把他贴在这里,
我们感谢他们以这种方式发给我们.。我们也会寻求反馈意见.。你们可以在Facebook
Engineering 主页
或者在 GitHub联系到我们。

By Joe Marrama, class=”wp_keywordlink”>软件工程师,Box团队

HHVM

HHVM是什么?

HHVM(HipHop
VM)是Fackbook推出用于在执行PHP代码的虚拟机,是一个PHP的JIT编译器,具有产生快速代码和及时编译的优点。

HHVM能干什么?

HHVM脚本主要应用服务器端脚本和命令行脚本两大领域,专注于服务器端脚本,如收集表单数据、生成动态页面、发送接受COOKIE等。

HHVM为什么比ZendEngine快?

HHVM是Facebook开发的高性能PHP虚拟机,宣称比官方Zend快9倍。

PHP使用的Zend虚拟机(VM),首先会先将PHP代码编译成二进制指令opcode,然后逐条执行,每条opcode指令都对应一个C函数。对于PHP的用户函数、运行时局部变量、常量会存在一个Hashtable中。

执行一次C函数的开销

  • 参数的入栈出栈
  • CPU寄存器状态保存

例如:在PHP中执行1000w次累加

<?php
$sum = 0;
// 发生1000w次C函数调用
for($i=0; $i<10000000; $i++){
  $sum += $i;
}

若编译为机器码情况是什么样的呢?

主频2.0GHZ的CPU每秒执行20亿次指令,函数调用则1秒只能运行1000W次。

因此,编译为机器码执行语言如C、C++、Golang…,或拥有JIT的语言如Java、NodeJS、LuaJIT、HHVM…,单从指令执行角度上看至少比PHP快几十倍。

对于字符串处理、JSON编码解码、iconv编码解码、数组操作等,
PHP比C++、Java慢吗?

在PHP中此类操作都是C扩展函数完成的,性能与编译型语言一致。

PHP到底比编译型语言慢的原因在哪里呢?

PHP代码中用户函数、类、对象操作等。

运算密集型 vs IO密集型

运算密集型程序指的是需大量执行内存复制操作、循环、运行指令等,瓶颈在CPU上,提升性能的解决方案就是提升CPU硬件配置、改进算法、提升语言/工具的执行性能。对于此类程序,PHP性能问题很明显,执行相同的逻辑,比C/C++慢几十倍甚至百倍,这是不可接受的。

IO密集型程序瓶颈在IO等待,例如HTTP请求执行100ms后返回,其中90ms查询数据库,8ms读写文件,
那么无论C/C++还是PHP,请求响应时间总是100ms左右,语言性能优化只有2ms的空间。

如何优化PHP呢

  • PHP语言层面优化
  • 优化PHP官方实现ZendEngine
  • 将PHP编译为其他语言字节码(bytecode),借助于其他语言虚拟机来运行。
  • 将PHP转成C/C++,编译成本地代码。
  • 开发更快的PHP虚拟机

Zend的执行过程可分为两个环节

  • 将PHP编译为opcode
  • 执行opcode

优化opcode可编码重复解析PHP与静态编译优化,由于PHP的动态性,这种优化方式是有局限,乐观估计可提升20%的性能。

优化opcode架构本身,工作量大投入产生比不高。

优化opcode执行,Zend解释器interpreter在读到opcode后会根据不同opcode调用不同函数(switch),在函数中执行语言相关的操作。优化空间不大。

优化Zend执行性能,对于函数调用的开销,通过inline
threading来优化,其原理如C中的inline关键字。

更快的虚拟机

HHVM 为什么更快,原因是JIT。

JIT操作本身是耗时的,对于简单程序或许比interpreter慢。HHVM的发展就是不断优化、优化、在优化。

图片 1

HHVM是如何超过HPHPc

什么是JIT,如何实现一个JIT?

动态语言中基本都会有一个eval(),作用是传入一段字符串来执行。JIT做着类似的事,不过它要拼接的不是字符串,而是不同平台下的机器码,然后执行。在JIT中更重要的优化是根据类型来生成特定的指令,从而减少指令数量和条件判断。

类型推导

JIT的关键是猜测类型,变量的类型要是老是变就很难优化。HHVM工程师考虑在PHP语法上做手脚,加上类型的支持,推出Hack。

<?hh
class Point
{
  // 使用静态类型可让HHVM更好的优化性能,不过这也意味着和PHP语法不兼容。
  public float $x,$y;
  public function __construct(float $x, float $y)
  {
    $this->x = $x;
    $this->y = $y;
  }
}

HHVM提升PHP执行性能

HHVM生成和执行PHP的在中间字节码,执行时通过JIT(Just In
Time即时编译,软件优化技术,指在运行时才会去编译字节码为机器码)转换为机器码执行。JIT将大量重复执行的字节码在运行时编译为机器码,达到提高执行效率的目的。通常触发JIT的条件是代码或函数被多次重复调用。

什么是字节码?

图片 2

字节码

ZendEngine做法是先编译为opcode,逐条执行,每条指令对应的是C语言级别的函数。

HHVM服务器最开始的少数请求会比其余的慢,因为它必须在执行PHP和Hack代码之前将它们编译成机器码,这个效果是非常明显的,所以你不应当立即把一个新设置的HHVM服务器应用到生产环境中。你应该先发送一些人工模拟的请求到这个HHVM服务器上,对它进行热身。
事实上,服务器启动的时候,并不会编译任何代码。初始的请求就是在HHVM的字节码解释器下运行的。原理就是:对于一个web服务器来说,最初的几个请求是不寻常的。在这个期间,开始了初始化,还对缓存进行填充等等。对这些代码路径的编译对整体性能的表现是非常糟糕的,因为一旦对服务器进行了预热,这些过程是不会被经常调用的。HHVM还利用这些请求,来收集一些代码所用到的数据类型分析的工作。所以它可以稍后更加有效地进行编译。你可以使用选项
hhvm.jit_profile_interp_requests 来调整这个门槛。
对于发送预热请求,颗通过命令行或其它类似的地方,简单地使用 curl
这个命令功能。为了得到最好的结果:
使用你希望在产品中看到的,能够代表最常见的请求的混合集合。例如,如果你期待所有对这个产品的请求中的40%都是到达
index.php 的,那么你的 40% 的预热请求都 应该是到 index.php 的请求。
避免并行发送多个预热请求,若你真的并行发送了多个请求,那么并不会出现什么问题。单对于JIT编译器来说,若没有同时工作在多个请求上的话,它往往能够生成更好的代码。
最终,你最好有个进程脚本用于服务器热身,这样的话,颗在命令行里仅仅执行一个命令就可以做到热身了。但是在最初期的时候,你还需要一些人工的参与,要实际计算出用于热身的请求数量是非常微妙的,
这主要取决于你的程序本身。

减少延迟和增加我们的基础设施的能力一直是 Box
最优先考虑的问题。我们努力以最有效的方式提供最好的用户体验,并且以前我们的
PHP
还选择不与这些目标一致。我很高兴地说,对于这两个目标我们最近取得了非常显著的进步,成功的部署了
HHVM(HipHop虚拟机)作为我们 PHP
代码的独家引擎服务;在这篇文章的其余部分,我将详细介绍如何使用PHP,如何使用HHVM,我们所面临的挑战是HHVM迁移,和提供卓越的性能。

Box中的PHP

在 Box 里,PHP
是开发栈的核心部分。尽管我们在大量的后台服务中使用了许多语言,然而每个后台服务都要求我们首先和
PHP web 应用进行交互。从 Box 诞生那一天开始,我们产品的核心功能都是使用
PHP 来实现的。

减缓由超过150个活跃贡献者编写的和超过75万行代码组成的还在不断增长的
PHP代
码所带来的延迟是最大的挑战。我们不能对我们提供服务的大部分页面进行缓冲处理,因为用户通常希望
Box
上的各种各样的动作都是原子性的。随着我们产品的不断成长、演变,这本身就会增大延迟。我们曾经一直投入巨大的努力来减少延迟,然而似乎一直没有找到好的办法。我们重构了旧的、效率低下的代码;把一些自身可独立做为组件的提取出来做为
PHP
扩展;这样就会大量地缓冲请求时可共享的保持不变的状态,然而,所做的一切都只是微微地减少了延迟,所取的效果很容易由新的功能来替代性的实现。然而自去年我们花时间对
HHVM 进行全面评估开始,这一切就有所改变。

HipHop 虚拟机(HHVM)

HHVM 是由 Facebook 牵头开发的开源 PHP 解释器。它诞生初期是做为 PHP 到
C++ 的编译器,是对 Facebook 的 PHP
代码库进行大量的裁剪基础上产生的,不过,最近几年它已经成长为一个即时(JIT)编译器。简言之,即时编译器就是以统一的方式对经常需要执行的
PHP 代码块进行编译并装载。演进为即时编译器也使得 HHVM 获得了与通常 PHP
解释器几乎相同的功能,同时 HHVM 现在还支持更多 PHP
语言的动态机制。例如,旧的 PHP 到 C++ 的编译器就无法运行 PHP
的”eval”语句(”eval”是把字符串当做 PHP 代码来执行,而 C++
不支持这样的功能),而新版本的 HHVM 就可以。由于 HHVM
已经成长为即时编译器,因此它已经逐渐大量替换了标准的 PHP 解释器。

一年多以前,我们就留意到 HHVM 团队把大量的精力都集中在获得与普通的 PHP
解释器同样的功能上。过去,我们曾经对 HHVM 进行评估,不过证明要让 HHVM
正确地运行我们开发的 web
应用非常困难。然而,这次我们重新迎接这一挑战,对 HHVM
进行全面评估,确定它在延迟方面的效果。把 HHVM 合并到我们的开发栈里和让
HHVM
运行我们部分代码是一项非常重要的任务,不过潜在回报很快证明我们的努力是值得的。我们最初的试验显示:HHVM
运行一个核心端点要比默认的 PHP 解释器快四倍多。

这标志着为期一年的把我们的产品安全地移植并运行在 HHVM
上的奋斗开始了。在移植过程中,我们在开发栈的许多地方都遇到各种各样的挑战。我们遇到大部分重大困难其他人在运行时也会遇到。在接下来的一部分,我将详细说明运行
HHVM 时通常遇到四个难点:解决存在在 HHVM
和默认的解释器之间的意想不到的不兼容性;回避二者之间可预料的不兼容性;对
PHP 的部署进行修补;确保在这一混合环境下一切可以非常良好的运行。

获得同等的功能

PHP
是一个非常庞大的语言。仅它的核心运行环境就包含大量的函数,配置设置和大量的类,这些都是十多年的社团贡献累积而来的。这甚至还不包括大量的
PHP 扩展,所有这些扩展也必须移植到 HHVM 上。让 HHVM 具有与默认的
PHP解释器几乎完全相同的功能本身就是惊人的重大壮举。我们试图说明这两个运行时环境行为上的差异。

我们发现大量的运行时差异是在 PHP
中几乎不经常使用的地方。其中一些差异是极易发现的错误,通过单元测试就可发现错误。而一些差异则是隐藏很深的漏洞,这些漏洞可引起非常严重的后果。HHVM
每天都会接近 PHP
解释器所提供的功能,然而移植这么庞大的代码库必然会使更多的行为上的差异浮出水面。自动测试是一种最安全的保障措施,它可以排除那些影响到用户功能的运行时差异。要让
HHVM 通过我们的 PHPUnit
测试套件是要花大力气的,即要进行许多修补才能获得同等功能。手工测试则是另一种必不可少的保障措施,尤其可以找到外部服务和
HHVM 之间交互时出现的错误。在 HHVM
使用在生产环境前,我们通过自动测试和手工测试混合的方法发现了大量功能存在差异的地方。

对 HHVM 运行时环境差异的修补过程非常有趣。HHVM
社团非常活跃,在很短的时间内就可以在
GitHub
或者 HHVM 的 IRC
聊天室获得帮助。HHVM
的代码库充分利用到了现代C++的各种构件,同时理解和给代码库做出贡献也相对容易多了。在发布
HHVM 之前,我们贡献了大约 20 个用于解决功能不等同问题和增强功能的补丁。

发表评论

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