RoadRunner:未死的PHP或Golang可以拯救



哈Ha! Badoo的工作人员正在积极提高PHP性能,因为我们使用这种语言拥有相当大的系统,而性能问题只是省钱。 十多年前,我们为此PHP-FPM创建了它,它首先是PHP的一组补丁,后来又进入了正式发行版。

近年来,PHP取得了长足的进步:垃圾收集器得到了改进,稳定性得到了提高-今天在PHP中,您可以编写恶魔和长寿命的脚本而没有任何特殊问题。 这使Spiral Scout可以走得更远:与PHP-FPM不同,RoadRunner不清除请求之间的内存,从而提高了性能(尽管这种方法使开发过程变得复杂)。 我们现在正在尝试使用此工具,但尚没有可以共享的结果。 为了让他们感到更有趣,我们发布了Spiral Scout的RoadRunner公告的译文。

本文中的方法非常接近我们:在解决问题时,我们也经常使用一堆PHP和Go,从两种语言中获得优势,而不是放弃一种而取而代之。

好好享受



在过去的十年中,我们为财富500强公司和受众不超过500 个企业创建了应用程序。 一直以来,我们的工程师主要使用PHP开发后端。 但是在两年前,不仅影响了我们产品的性能,而且极大地影响了它们的可扩展性-我们在技术堆栈中引入了Golang(Go)。

几乎立即,我们发现Go允许我们创建性能高达40倍的大型应用程序。 有了它,我们能够扩展用PHP编写的现有产品,并结合两种语言的优势对其进行改进。

我们将向您介绍Go和PHP的组合如何帮助解决实际的开发问题,以及它如何成为我们的工具,可以缓解与PHP“垂死”模型相关的部分问题。

您的日常PHP开发环境


在讨论Go如何使PHP“垂死”的模型活跃起来之前,让我们看一下您的标准PHP开发环境。

在大多数情况下,您可以结合使用nginx Web服务器和PHP-FPM服务器来启动应用程序。 第一个提供静态文件,并将特定请求重定向到PHP-FPM,而PHP-FPM本身执行PHP代码。 也许您正在使用来自Apache和mod_php的不受欢迎的捆绑软件。 但是,尽管工作原理略有不同,但原理是相同的。

考虑一下PHP-FPM如何执行应用程序代码。 当请求到达时,PHP-FPM初始化一个子PHP进程,并将请求详细信息作为其状态的一部分(_GET,_POST,_SERVER等)传递。

在执行PHP脚本期间状态无法更改,因此您只能以一种方式获取一组新的输入数据:清除进程内存并再次对其进行初始化。

这种执行模型具有许多优点。 您不必担心内存消耗,所有进程都完全隔离,并且如果其中一个进程死了,它将自动重新创建,并且不会影响其他进程。 但是,这种方法还存在尝试扩展应用程序时出现的缺点。

常规PHP环境的缺点和效率低下


如果您从事PHP的专业开发,那么您会知道选择框架从哪里开始新的项目。 它是一个用于依赖项注入,ORM,翻译和模板的库。 而且,当然,所有用户输入都可以方便地放在一个对象(Symfony / HttpFoundation或PSR-7)中。 框架很棒!

但是,一切都有代价。 在任何企业级框架中,为了处理简单的用户请求或访问数据库,您将必须下载至少几十个文件,创建大量的类并解析几种配置。 但是最糟糕的是,在完成每个任务之后,您将需要重置所有内容并重新启动:刚启动的所有代码都变得无用,并且您将不再处理其他请求。 告诉任何用其他任何语言编写的程序员,您都会看到他的困惑。

多年来,PHP工程师一直在寻求使用延迟加载,微帧,优化的库,高速缓存等经过深思熟虑的方法来解决此问题的方法。但是最后,您仍然必须重置整个应用程序,并一遍又一遍地重新开始。 (译者注:随着PHP 7.4中预加载的出现,此问题将得到部分解决)

PHP可以使用Go生存多个请求吗?


您可以编写使用寿命超过几分钟(长达数小时或数天)的PHP脚本:例如cron任务,CSV解析器,队列中断器。 他们都根据一种情况工作:提取任务,完成任务,等待下一个任务。 该代码一直在内存中,从而节省了宝贵的毫秒,因为下载框架和应用程序需要许多其他步骤。

但是开发长期存在的脚本并不是那么简单。 任何错误都会完全终止该过程,令人难以置信的是内存泄漏的诊断,并且不再可能使用F5进行调试。

随着PHP 7的发布,情况得到了改善:出现了可靠的垃圾收集器,变得更容易处理错误,并且内核扩展得到了保护,防止泄漏。 的确,工程师仍然需要仔细处理内存并记住代码中的状态问题(是否有一种语言可以忽略这些内容?)。 但是,在PHP 7中,惊喜很少。

是否可以采用一个模型来处理长期存在的PHP脚本,使其适应诸如处理HTTP请求之类的琐碎任务,从而摆脱对每个请求从头开始下载所有内容的需求?

为了解决这个问题,首先需要实现一个服务器应用程序,该应用程序能够接受HTTP请求并将它们逐个重定向到PHP工作程序,而不会每次都将其杀死。

我们知道我们可以用纯PHP(PHP-PM)或使用C扩展(Swoole)编写Web服务器。 尽管每种方法都有其自身的优势,但这两种选择都不适合我们-我想要更多。 不仅需要Web服务器,我们还希望获得一个解决方案,该解决方案可以使我们摆脱与PHP的“硬启动”相关的问题,同时可以轻松地将其适应和扩展以适应特定的应用程序。 也就是说,我们需要一个应用程序服务器。

Go可以帮忙吗? 我们知道它可以,因为这种语言将应用程序编译为单个二进制文件。 它是跨平台的; 使用其自己的,非常优雅的并发模型和一个用于HTTP的库; 最后,我们将可以使用数千种开放源代码库和集成。

两种编程语言结合起来的困难


首先,有必要确定两个或多个应用程序之间如何通信。

例如,借助出色的 Alex Palaestras 库,可以通过PHP和Go进程(类似于Apache中的mod_php)实现内存共享。 但是该库的功能限制了它解决问题的用途。

我们决定使用另一种更通用的方法:通过套接字/管道在进程之间建立交互。 在过去的几十年中,这种方法已被证明是可靠的,并且已在操作系统级别进行了优化。

首先,我们创建了一个简单的二进制协议,用于在进程之间交换数据并处理传输错误。 最简单的形式是,这种类型的协议类似于带有固定大小的数据包头 (在我们的情况下为17个字节)的netstring ,其中包含有关数据包类型,其大小和用于检查数据完整性的二进制掩码的信息。

在PHP方面,我们使用了pack函数 ,在Go方面,我们使用了编码/二进制库。

一个协议对我们来说还不够—我们增加了直接从PHP调用net / rpc Go-services的功能 。 后来,由于我们可以轻松地将Go库集成到PHP应用程序中,因此它对我们的开发大有帮助。 这项工作的结果可以在其他开源产品Goridge中看到

在几个PHP工作者之间分配任务


在实现了交互机制之后,我们开始考虑如何最好地将任务转移到PHP进程。 任务到达时,应用程序服务器必须选择一个空闲工作程序来完成任务。 如果工作程序/进程因错误或“死”而终止,我们将摆脱它并创建一个新的进程。 如果工作程序成功完成,我们将其返回到可用于完成任务的工作程序池中。



我们使用了一个缓冲通道来存储活动工人池;为了从池中删除意外的“死”工人,我们添加了一种跟踪错误和工人状态的机制。

结果,我们得到了一个工作的PHP服务器,该服务器能够处理以二进制形式提出的任何请求。

为了使我们的应用程序开始作为Web服务器工作,我必须选择一个可靠的PHP标准来呈现所有传入的HTTP请求。 在我们的案例中,我们仅 net / http请求从Go转换为PSR-7格式,使其与当今可用的大多数PHP框架兼容。

由于PSR-7被认为是不可变的(从技术上来说,有人会说它不是),因此开发人员必须编写原则上不会将请求作为全局实体处理的应用程序。 这与长期存在的PHP进程的概念非常吻合。 我们尚未命名的最终实现看起来像这样:



推出RoadRunner- 高性能PHP应用程序服务器


我们的第一个测试任务是API后端,它会周期性地导致无法预测的突发请求(比平时更频繁)。 尽管在大多数情况下有足够的Nginx功能,但我们经常遇到502错误,因为我们无法为预期的负载增加足够快地平衡系统。

为了替代此解决方案,我们在2018年初部署了第一台PHP / Go应用服务器。 并立即产生了不可思议的效果! 我们不仅完全摆脱了错误502,而且还能够将服务器数量减少三分之二,从而为工程师和产品经理节省了大量资金和药丸。

到年中,我们改进了解决方案,以MIT许可证将其发布在GitHub上,并将其命名为RoadRunner ,以强调其令人难以置信的速度和效率。

RoadRunner如何改善您的开发堆栈


使用RoadRunner可以使我们在请求进入PHP之前使用Go端的中间件net / http进行JWT验证,并在Prometheus中处理WebSocket和全局聚合状态。

借助内置的RPC,您无需编写扩展包装程序即可打开任何用于PHP的Go-libraries的API。 更重要的是,RoadRunner可以部署HTTP以外的其他新服务器。 示例包括在PHP中运行AWS Lambda处理程序,创建可靠的队列解析器,甚至将gRPC添加到我们的应用程序中。

在PHP和Go社区的帮助下,我们提高了解决方案的稳定性,在某些测试中,我们将应用程序性能提高了40倍,改进了调试工具,实现了与Symfony框架的集成,并增加了对HTTPS,HTTP / 2,插件和PSR-17的支持。

结论


有些人仍然被过时的PHP概念(一种缓慢而笨拙的语言)所吸引,仅适用于为WordPress编写插件。 这些人甚至可以说PHP有这样的局限性:当应用程序变得足够大时,您必须选择一种更“成熟”的语言并重写多年来积累的代码库。

我想回答所有这些:再想一想。 我们相信只有您自己才能为PHP设置任何限制。 您可以在一生中从一种语言切换到另一种语言,尝试找到与您的需求完美结合的方法,也可以开始将语言视为工具。 PHP之类的语言的明显缺陷实际上可能是其成功的原因。 而且,如果将其与Go之类的另一种语言结合使用,那么与仅限于使用一种语言相比,您将创建功能更强大的产品。

在使用一堆Go和PHP之后,我们可以说我们很喜欢它们。 我们不打算牺牲一方来支持另一方-相反,我们将寻求从这种双重堆栈中获得更多利益的方法。

UPD:欢迎成为RoadRunner的创建者和原始文章的合著者-Lachezis

Source: https://habr.com/ru/post/zh-CN434272/


All Articles