RoadRunner:PHP 不是为了死而生的,还是 Golang 来拯救

RoadRunner:PHP 不是为了死而生的,还是 Golang 来拯救

嘿哈布尔! 我们在 Badoo 很活跃 致力于 PHP 性能,因为我们用这种语言有一个相当大的系统,并且性能问题是一个省钱的问题。 十多年前,我们为此创建了 PHP-FPM,最初是 PHP 的一组补丁,后来进入了正式发行版。

近年来,PHP 取得了长足的进步:垃圾收集器得到了改进,稳定性水平也得到了提高 - 今天您可以在 PHP 中编写守护进程和长期脚本而不会出现任何问题。 这使得 Spiral Scout 能够走得更远:RoadRunner 与 PHP-FPM 不同,不会在请求之间清理内存,这会带来额外的性能增益(尽管这种方法使开发过程变得复杂)。 我们目前正在试验这个工具,但还没有任何结果可以分享。 为了让等待他们变得更加有趣, 我们发布了 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 Worker,而不必每次都杀死它。

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

Go 可以帮忙解决这个问题吗? 我们知道它可以,因为该语言将应用程序编译成单个二进制文件; 它是跨平台的; 使用自己的、非常优雅的并行处理模型(并发)和一个用于处理 HTTP 的库; 最后,我们将可以使用数千个开源库和集成。

结合两种编程语言的困难

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

例如,使用 优秀的图书馆 Alex Palaestras,可以在 PHP 和 Go 进程之间共享内存(类似于 Apache 中的 mod_php)。 但这个库的一些特性限制了它解决我们问题的用途。

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

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

在 PHP 端我们使用 打包功能,而在 Go 方面,图书馆 编码/二进制.

在我们看来,一个协议是不够的 - 我们添加了调用的能力 net/rpc 直接从 PHP 获取服务。 后来,这对我们的开发帮助很大,因为我们可以轻松地将Go库集成到PHP应用程序中。 例如,可以在我们的其他开源产品中看到这项工作的结果 戈里奇.

将任务分配给多个 PHP Worker

实现交互机制后,我们开始思考如何将任务转移到PHP进程中最高效。 当任务到达时,应用服务器必须选择一个空闲的worker来执行它。 如果一个工作进程/进程因错误退出或“死亡”,我们会删除它并创建一个新的来替换它。 如果工作人员/进程已成功完成,我们会将其返回到可用于执行任务的工作人员池中。

RoadRunner:PHP 不是为了死而生的,还是 Golang 来拯救

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

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

为了让我们的应用程序开始作为 Web 服务器工作,我们必须选择可靠的 PHP 标准来表示任何传入的 HTTP 请求。 在我们的例子中,我们只是 转换 来自 Go to format 的 net/http 请求 PSR-7因此它与当今大多数可用的 PHP 框架兼容。

由于 PSR-7 被认为是不可变的(有些人会说技术上并非如此),因此开发人员编写的应用程序原则上不得将请求视为全局实体。 这非常符合长寿命 PHP 进程的概念。 我们的最终实现(尚未命名)如下所示:

RoadRunner:PHP 不是为了死而生的,还是 Golang 来拯救

介绍 RoadRunner - 高性能PHP应用服务器

我们的第一个测试任务是 API 后端,它会定期出现不可预测的突发情况(比平常频繁得多)。 尽管 nginx 在大多数情况下已经足够,但我们经常遇到 502 错误,因为我们无法足够快地平衡系统以满足预期的负载增加。

为了取代此解决方案,我们于 2018 年初部署了第一个 PHP/Go 应用程序服务器。 并立即得到了令人难以置信的效果! 我们不仅彻底摆脱了 502 错误,而且还能够将服务器数量减少三分之二,为工程师和产品经理节省了大量资金和头痛药。

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

RoadRunner 如何改进您的开发堆栈

应用 ROADRUNNER 允许我们在 Go 端使用中间件 net/http 在请求到达 PHP 之前执行 JWT 验证,以及在 Prometheus 中全局处理 WebSocket 和聚合状态。

由于内置的​​ RPC,您可以为 PHP 打开任何 Go 库的 API,而无需编写扩展包装器。 更重要的是,使用 RoadRunner,您可以部署新的非 HTTP 服务器。 示例包括在 PHP 中运行处理程序 AWS Lambda,创建可靠的队列断路器,甚至添加 远程过程调用 到我们的应用程序。

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

结论

有些人仍然陷入 PHP 过时的观念中,认为 PHP 是一种缓慢、笨拙的语言,只适合为 WordPress 编写插件。 这些人甚至可能会说PHP有这样一个局限性:当应用程序变得足够大时,你必须选择一种更“成熟”的语言并重写多年积累的代码库。

对于这一切,我想回答:再想一想。 我们相信只有您对 PHP 设置了任何限制。 您可以花一生的时间从一种语言过渡到另一种语言,试图找到最适合您需求的语言,或者您可以开始将语言视为工具。 像 PHP 这样的语言的所谓缺陷实际上可能是其成功的原因。 如果你将它与另一种语言(如 Go)结合起来,那么你将创建比仅限于使用任何一种语言更强大的产品。

使用过很多 Go 和 PHP 后,我们可以说我们喜欢它们。 我们不打算牺牲其中一个 - 相反,我们将寻找方法从这个双堆栈中获得更多价值。

UPD:我们欢迎 RoadRunner 的创建者和原文章的合著者 - 拉克西斯

来源: habr.com

添加评论