有时候,我们会听说关于一些公司采用 Facebook 的开源项目的事情。Box 团队近期给我们发送了他们是如何使用 HHVM 的故事,是一个很好的文章。所以我们把他贴在这里, 我们感谢他们以这种方式发给我们.。我们也会寻求反馈意见.。你们可以在Facebook Engineering 主页 或者在 GitHub联系到我们。 By Joe Marrama,软件工程师,Box团队 减少延迟和增加我们的基础设施的能力一直是 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 个用于解决功能不等同问题和增强功能的补丁。 设计方面的差异在进行移植期间,我们发现两个运行时环境几个行为方面不一致的地方,这些不一致使得基本的设计有所不同。这可能是需要解决的最棘手的差异化问题。例如,HHVM 的多路处理模型(MPM)与以前我们曾经使用过的 Apache prefork 多路处理模型完全不同。HHVM 给每个请求提供服务的是一个工作者线程,而 Apache prefork 则给每个请求提供服务的是一个工作者进程。这对我们来说就有一点挑战。一旦发现问题所在,我们首先要做的就是进行相对简单的漏洞修复-我们曾经使用过 PHP 的进程 ID 来区分日志文件和其他临时文件。由于 HHVM 使用的是单一进程,因此当前的进程 ID 是不能用来区分并发的多个请求了。发现到这个漏洞后,我们对代码库中可能收到新多路处理模块影响的所有功能进行了一次全面的核查,例如,设置创建文件的模式的掩码和切换目录。 |
|
一个特别令人意想不到的行为上的差异是通过运行 PHP 的"memory_get_usage"函数使自身显露出来的,这个函数是用来汇报分配给当前请求的内存数量的。对于某种请求流,我们将根据这个函数汇报的数值定期地刷新内存中的缓冲数据。这个差异影响到 HHVM 的内存预分配库 jemalloc。jemalloc 是一个基于 Slab 的分配器,这种分配器分配的是各种大型的内存块,较小的内存分配则由这些内存块来分配。HHVM的“memory_get_usage”返回的是分配给某个请求的所有 slab 的总的大小,而不是实际上这个请求正在使用的slab内存的数量。当我们继续以同样的方式运行HHVM的“memory_get_usage”时,特定的请求流就会出现行为错乱,就会在本地缓冲数据区乱冲乱撞,这必然使得后台系统负载大大地增加。很幸运,可以完全修补这个问题:通过更改 HHVM 汇报的内存机制使得其汇报的是该请求实际上使用的本地内存数量(即通过设置参数 "$real_usage" 参数为 true 实现)。 由 HHVM 的多路处理模块(MPM)的差异还引起了另一个更加严重的问题:内存泄漏!在 Apache prefork 的多路处理模块(MPM)里,缓慢的内存泄漏不会太引起人们的关心,因为工作者进程在服务完一定量的请求后会被回收。而在 HHVM 里,这种待遇不再有了。我们在多天非标准负载的 HHVM 上碰到了非常难以处理的内存泄漏。经过大量对 jemalloc 的仔细设置,我们追踪到问题是由第三方库引起的,并马上对其打上补丁。打上这个补丁之后,在许多天服务数百万个请求的情况下,HHVM 的内存消耗始终保持稳定。 |
|
现存的混合状态
毫不奇怪,迁移到 HHVM 过程中最危险的部分是将其推出应用。没有足够的测试以确保 HHVM 在生产环境中可以完美处理所有请求并与后续系统完美配合。HHVM 在预生产环境中的重度测试中表现良好,但是我们任然对此表示怀疑。此外,我们不能提供一个独立的只读生产环境供 HHVM 测试,而且我们不能容许有任何的停机时间。对我们来说,应用 HHVM 唯一可行的方式是具有长期,充分观察的实验过程的可控方式。这需要使 HHVM 运行在与 Apache 和默认 PHP 解释器相同的环境中。只有在这种情况下最终存在,才可以使我们成功放出 HHVM,而不会使 HHVM 对用户有负面影响。 我们的 PHP 代码库与大量的不同后端系统交互。幸运的是,绝大多数交互的发生是通过 curl 对内部 REST APIs 的请求,curl 是经过非常充分测试和稳定的 HHVM curl 扩展。我们使用 PDO 扩展来与我们的 MySQL 数据库服务器交互,同样表现出了与默认 PHP 解释器相同的功能行为。可能有潜在麻烦的后端系统是 Memcached,为了确保两个运行时中完整的互操作性,两个运行时都必须具有功能相等的 Memcached 扩展而且都必须是相同的序列化对象。如果任何一个运行时的序列化对象有不同行为,在另一个运行时中从 Memcached 中回复对象序列化在不同格式时就会轻易造成严重破坏。我们在混合环境中进行了众多的实验以确保两个运行时都不会毒化 Memcached 或任何其他后端存储。所有事情都表现的很好,除了一个小问题:ArrayObjects。在标准的 PHP 解释器中,实现 ArrayObject 的扩展定义了一个自定义的序列格式,然而 HHVM 中只使用标准的对象序列。我们需要在我们的应用中禁用 ArrayObjects 缓存以确保 Memcached 的互操作性。幸运的是,我们在推出之前或过程中不会再遇到任何其他的互操作性问题。 |
|






