首页vns威尼斯城官网登入 › 让PHP代码的执行性能大幅提升vns威尼斯城官网登入:,我将详细介绍如何使用PHP

让PHP代码的执行性能大幅提升vns威尼斯城官网登入:,我将详细介绍如何使用PHP

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

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

让我们耐心等待PHP7。

capistrano的工作方式

capistrano会在远程服务器中保存之前部署的应用,而且每次部署的版本放在各自的目录中。capistrano还会创建一个current/目录,通过符号连接指向当前部署的应用所在的目录。当部署应用时,capistrano会首先从git仓库获取最新代码,然后把代码放到realeases/的一个新子目录,然后把current/符号连接指向新目录。

  • Deployer
  • Magallanes
  • Rocketeer

从长远来看,测试省钱,省精力

  • 开发前:把测试工具当做项目开发的重要依赖
  • 开发中,每个功能都要编写并运行测试
  • 开发后,编写新测试,确保修补缺陷的方式是正确的

单元测试和功能测试

  • 单元测试:使用PHPUnit或者PHPSpec
  • 测试驱动开发: 编写之前写测试,再开发,然后再写测试,再开发
  • 行为驱动开发: 编写故事,描述应用的表现。
    • SpecBDD:也是单元测试,使用人类能读懂的语言。比如PHPUnit风格测试命名为testRenderTemplate(),
      等价SpecBDD命名为itRendersTheTemplate(),
      而且会使用诸如$this->shouldReturn()、$this->shouldbe()之类易于理解的辅助方法。
    • StoryBDD:类似,但是更多关注整体行为。StoryBDD用于测试产品经理的需求(要生成报告,并且电子邮件发送给用户),SpecBDD测试开发的需求(这个类方法只能接收一个数组)

术语:PHPUnit测试在一起组成测试用例(test
case),测试用例在一起组成测试组件(test
suite),PHPUnit会使用测试运行程序(test runner)运行测试组件。

一个测试用例是一个PHP类(且以Test结尾,文件名以Test.php结尾),扩展自PHPUnit_Framework_TestCase类。测试用例中有一些以test开头的公开方法,一个方法是一个测试,在方法中我们断言会发生什么事。断言可能通过也可能失败,目标是使断言都通过。

测试运行程序默认使用命令行运行程序,调用phpunit命令。

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

现存的混合状态

毫不奇怪,迁移到 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
的互操作性。幸运的是,我们在推出之前或过程中不会再遇到任何其他的互操作性问题。

在上线过程中,一个非常有用的处理工具就是我们简单到爆的主机转换法。尽管使转换到
HHVM
的过程尽可能简单是一件再明显不过的事,但它任然值得我们在所有场合拿出来炫耀一下。我们决定通过
puppet 将 HHVM 部署在一个 host-by-host 基础上,转换过程是通过 puppet
中可以由主机名调整的一个标志控制的。主机转换到 HHVM
和回滚的操作仅仅需要调整标志的条件,不需要从负载平衡器上移除主机,也不需要做其它任何事。完整的回滚操作可以五分钟内完成,多主机一次性快速迁移和回滚是必须的。

上线过程中我们所有网络应用的登录和监控系统是至关重要的,但是有一种形式的监控被证明特别有用;监控
HHVM 错误日志以得到新的错误。我们维护了一个内部数据库,它包含有我们从
Apache 错误日志中观测到的所有独特的 PHP 错误。当上线 HHVM
时,这个系统告诉我们所有发生在 HHVM
上的新错误,通过对错误进行分类,并使用 PHP
错误数据库判断其是否是一个之前存在的错误。这使我们更多地了解到 HHVM
是否导致了任何新的倒退。

介绍

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

vns威尼斯城官网登入 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引用测试框架(当然,也是开源的),并且在整个过程中节省了上百万美元。

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

使用SSH密钥对认证方式登录远程设备时,远程设备随机创建一个消息,使用公钥加密,然后把密文发给本地设备,本地设备收到密文后使用私钥解密,然后把解密后的消息发给远程服务器。远程服务器验证解密后的消息之后,再赋予你访问服务器的权限。

vns威尼斯城官网登入 2

修正部署

HHVM运行在最佳性能时有两点与众不同的,这两个不同点迫使我们重新思考了一下我们PHP应用的部署方式。第一个不同点是HHVM需要对新编写代码“热身”以后才能达到最佳性能。由于HHVM是一个即时编译器(JIT),因此需要对新编写的代码运行几次后,才能收集到足够的信息,进而对这些代码进行转换,最终达到有效装配。实际上,在对当前请求服务之前,这样的转换是发生在curl请求的几个重大节点上的。第二个不同点是规律性地重新启动HHVM以达到最佳性能。这使得升级HHVM就容易多了,同时也是一个保障HHVM和其连接库出现内存泄漏的好方法。要满足上面两点需要对Apache通常的PHP部署稍作调整。

以前我们是通过Apache
web服务器单个实例为整个站点提供服务的,同时我们通过转换指向当前代码库的符号链接来实现多个代码库之间循环访问的。现在,我们使用三个HHVM实例为各种请求提供不间断的服务。通常情况下,其中一个HHVM实例为当前应用的所有请求提供服务,另外两个实例做为备用,为以前的应用和以前的以前的应用提供服务(见下图)。每个HHVM
web服务器都指向一个绝对路径,因此当我们需要部署新版本的时候,我们只要停止指向旧版本的web服务器,启动指向新版本的服务器,并使用多个curl请求对代码库进行热身,再重定向请求就可以了。这种部署方式满足了上段提到的两个不同点,这样可以很容易地实现回滚和前期测试。要回滚到以前版本,我们可以把请求重定向到提供以前版本代码服务的HHVM实例上(注意这个HHVM实例已经在运行)。要对新部署的应用进行前期测试,我们只要把一部分请求路由到新的HHVM实例上,测试一直持续到我们确信新的部署稳定时为止。这种部署已证明非常强大:在生产环境下可以处理大容量的请求。

vns威尼斯城官网登入 3

常见的HHVM服务器

vns威尼斯城官网登入 4

部署代码后的HHVM服务器

引擎和扩展的变化

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

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

类型

静态语言,通常都要编译,编译器提供类型检查和错误报告,通常更稳定。缺点是要先编译,反馈回路长。

动态语言,通常解释执行,在运行时发现错误。迭代速度快,反馈及时。缺点是,没有类型检查,缺少内在的准确性。

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

设计方面的差异

在进行移植期间,我们发现两个运行时环境几个行为方面不一致的地方,这些不一致使得基本的设计有所不同。这可能是需要解决的最棘手的差异化问题。例如,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
的内存消耗始终保持稳定。

vns威尼斯城官网登入 5

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

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

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

Badoo 团队

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

一般选择nginx。因为apache为每个PHP请求派生一个子进程,比较耗资源。

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

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

更改测试基础设施

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

因为使用密码登陆有漏洞,尽量使用SSH密钥对认证。SSH密钥对登录流程如下:

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

获得同等的功能

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

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

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

实践出真知

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

override类方法

可选:

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

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

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

vns威尼斯城官网登入 6

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

安装

PHPUnit用于测试,用composer安装;XDebug用于生成覆盖度信息,是PHP扩展,用包管理器安装。

紧接着,书中有一个具体的测试用例,用的时候找更详细的blog看看吧

原生提供钩子,可以集成github仓库,每次提交后,都能自动测试,并在多个版本中测试。

指的是分析应用的性能。

什么时候使用?

当遇到性能瓶颈时再使用。

  • 在开发环境:XDebug。它的结果人类读不懂,需要KCacheGrind或者WinCacheGrind形象化展示。
  • 在生产环境:XHprof,使用XHGUI展示结果。

为节省资源,配置成触发执行,具体配置略

接下来讨论的是PHP的未来。

传统的PHP解释器是Zend Engine, HHVM(Hip Hop Virtual
Machiane)由Facebook开发,目的是提高性能,HHVM先把PHP代码转换成字节码,然后缓存字节码,然后使用JIT编译器(Just
in
Time)转换并优化成x86_64机器码。这样传统的解释型语言就有了一些编译型语言的速度。JIT提供了很多底层性能优化。

HHVM和Zend Engine是等价的。相当于PHP+PHP-FPM。

HHVM配置也是用php.ini文件

推荐使用Supervisord监控,HHVM挂掉后立即重启。

他们把自己看做PHP的方言,为PHP引入了新的数据类型和结构,以及静态类型。

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

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 的部署进行修补;确保在这一混合环境下一切可以非常良好的运行。

RUsage (CPU 时间):

更改测试基础设施

 // 生成私钥、公钥文件 ~/.ssh/id_rsa 和 ~/.ssh/id_rsa.pub ssh-keygen // 复制公钥到服务器 scp ~/.ssh/id_rsa.pub deploy@100.10.3.1: // 末尾要加':',会复制到家目录 // deploy用户登录服务器 // 确认有~/.ssh目录,没有则创建目录和文件,文件存储允许登录的公钥 touch ~/.ssh/authorized_keys cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys // 修改几个目录和文件的权限,只让deploy用户访问 chown -R deploy:deploy ~/.ssh chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys
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;};

Box中的PHP

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

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

vns威尼斯城官网登入 7

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

系列笔记:Modern PHP 笔记:语言特性Modern PHP 笔记:良好实践Modern PHP
笔记:部署测试和调优

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

大棒末端的胡萝卜:HHVM 的好处

vns威尼斯城官网登入 8

大部分基础设施迁移到 HHVM 期间的服务器端延迟。迁移大约是在 10:50am 到
1:00pm 之间进行的。

与我们前期的测评一致,HHVM
大幅度地缩减了服务器端延迟。上面的图片显示了我们将大部分生产基础设施迁移到
HHVM 那天的迁移期间服务器端延迟。从图中可以直观地看出,HHVM
明显地减小了服务器端延迟。HHVM 平均可以将服务器端的延迟降低为原来的
2/5,这里的服务器端延迟是请求进入我们的基础设施到响应离开之间经过的时间。更令人印象深刻的是,这个数字包括等待后端服务响应的时间,本质上,所有的请求都对许多不同的服务进行了调用。没有
HHVM,很明显我们不可能有其他方式可以如此大幅度地消减延迟。更重要的是,HHVM
对延迟的影响可以从用户的反应中轻松得到。

vns威尼斯城官网登入 9

大部分基础设施迁移到 HHVM 期间一个数据中心的 CPU 使用率。

HHVM 也可以大幅度提高效率。上面的图片是我们大部分服务器迁移到 HHVM
期间的平均前端 CPU 使用率。可以看出,CPU 的使用率约为原来的
1/2。这免费地将我们的前端容量扩大了一倍,因为 CPU
使用率是我们前端机器的主要限制因素。在我们的领域内,这将显著节约服务器的开支、电力消耗和对数据中心容量的需求。

HHVM 在速度和高效性之外还提供许多令人惊奇的功能,包括:

  • 运行 Hack 代码的能力。Hack
    提供很多我们想要的功能,包括一个有表现力的类型系统,一个类型检查器和对异步执行的支持。我们认真地评估了在我们的
    PHP 代码库中大规模使用 Hack 的可行性。
  • 精致的性能分析工具。HHVM 实现了许多性能分析机制,包括那些可以在
    XDebug 扩展中找到的,和一个被称为 Xenon 基于时间的取样分析器。HHVM
    还与 jemalloc 的内存分析工具完美整合以提供详细进程规模的度量和分析。
  • 更大地提升速度,效率和代码质量的可能性。HHVM
    在这一领域提供的主要便利就是“仓库授权”模式,在这里,你可以将 PHP
    预编译成中间代码并使其替代运行,仓库授权模式禁止使用较多的 PHP
    动态特性,例如将字符串作为 PHP
    代码执行。我们的经验是,使用它可以提高 15% 的速度和效率。
  • 一个非常活跃的社区。HHVM 在争取与默认 PHP
    解释器相同地位的道路上快速发展,并进行性能提升和扩展移植。HHVM
    正处于一个快速的版本迭代过程中,社区对问题和和并请求的响应非常迅速。

总而言之,将 PHP 运行时迁移到 HHVM
并不是一个简单的过程,但是是件非常值得的事。必我们须要小心进行,提前做大量的测试并且要时刻保持警惕,完全有可能安全且零停机地完成这个过程。需要特别关注的是部署架构,转换方法,对所有运行时的不兼容进行修复和监控。HHVM
在加速受 CPU 限制的 PH
P应用时具有非常巨大的潜力,而且它还具有许多其他优点。我们对于 HHVM
现在驱动 Box 感到非常兴奋,而且将为进一步挖掘 HHVM 的潜力,使 Box
尽可能地快速可靠地工作。

vns威尼斯城官网登入 10

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

目标:安装Web服务器,以便接收HTTP请求;设置并管理一组PHP进程,处理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和JIT
2015年就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.
数据类型的变化zval
php7的诞生始于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宏也被移除了。

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

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

然后作者展示了一个登陆VPS的例子:需要ssh登陆、创建非根用户管理web服务器,不要使用root

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

响应时间分布:

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

hack两者兼有

hack基本兼容PHP

查了下最新的情况,PHP7性能卓越,Hack优势不明显了,前景不明。

以上就是这本书的精华,不过瘾的话强烈推荐购买。欢迎交流

zend_string和char*的转换:

内存使用:

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

禁用密码,禁止根用户登录

这样最安全

 // /etc/ssh/sshd_config PassWordAuthentication 设置为no PermitRootLogin 设置为no

PHP FastCGI Process Manager(PHP
FastCGI进程管理器)。它会创建一个主进程,控制何时以及如何把HTTP请求转发给一个或多个子进程处理。PHP-FPM还需控制何时创建、销毁PHP子进程。

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

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

从PHP转向Hack

<?php改成<?hh

/* 数组举例 */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-FPM进程池都以指定的操作系统用户和用户组的身份运行,每个PHP用户一个非根用户,这样查看和管理都很方便。

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

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

全局配置

CentOS主配置文件:/etc/php-fpm.conf建议修改默认配置:

 emergency_restart_threshold = 10 // 指定时间内,失效的PHP-FPM子进程超过这个值,让PHP-FPM主进程优雅重启 emergency_resttart_interval = 1m // 指定时间跨度
/* 例如 */zend_string *zstr;if (zend_parse_parameters(ZEND_NUM_ARGS() , "S", zstr) == FAILURE){ RETURN_LONG(-1);}

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

适合在一台电脑中登录远程,多台不适合。

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

转载本站文章请注明出处:vns威尼斯城官网登入 http://www.tiec-ccpittj.com/?p=4614

上一篇:

下一篇:

相关文章