嘿哈布尔! 我们在 Badoo 很活跃
近年来,PHP 取得了长足的进步:垃圾收集器得到了改进,稳定性水平也得到了提高 - 今天您可以在 PHP 中编写守护进程和长期脚本而不会出现任何问题。 这使得 Spiral Scout 能够走得更远:RoadRunner 与 PHP-FPM 不同,不会在请求之间清理内存,这会带来额外的性能增益(尽管这种方法使开发过程变得复杂)。 我们目前正在试验这个工具,但还没有任何结果可以分享。 为了让等待他们变得更加有趣, 我们发布了 Spiral Scout 的 RoadRunner 公告的翻译。
文章中的方法与我们很接近:在解决问题时,我们也最常使用一堆 PHP 和 Go,获得两种语言的好处,而不是放弃一种语言而选择另一种语言。
在过去十年中,我们为列表中的公司创建了应用程序
几乎立刻,我们发现 Go 允许我们构建更大的应用程序,性能提升高达 40 倍。 有了它,我们能够扩展用 PHP 编写的现有产品,通过结合两种语言的优点来改进它们。
我们将告诉您 Go 和 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 与 Go 能否承受多个请求?
可以编写运行时间超过几分钟(长达数小时或数天)的 PHP 脚本:例如,cron 任务、CSV 解析器、队列断路器。 它们都根据相同的场景工作:检索任务,执行它,然后等待下一个任务。 代码始终驻留在内存中,节省了宝贵的毫秒时间,因为加载框架和应用程序需要许多额外的步骤。
但开发长期存在的脚本并不容易。 任何错误都会完全终止进程,诊断内存泄漏令人恼火,并且不再可能进行 F5 调试。
随着 PHP 7 的发布,这种情况得到了改善:出现了可靠的垃圾收集器,处理错误变得更加容易,并且内核扩展现在是防泄漏的。 确实,工程师仍然需要小心内存并注意代码中的状态问题(是否有一种语言可以忽略这些事情?)。 尽管如此,PHP 7 给我们带来的惊喜还是较少。
是否有可能采用使用长期 PHP 脚本的模型,使其适应更琐碎的任务,例如处理 HTTP 请求,从而摆脱每个请求从头开始加载所有内容的需要?
为了解决这个问题,我们首先需要实现一个服务器应用程序,它可以接受 HTTP 请求并将它们一一重定向到 PHP Worker,而不必每次都杀死它。
我们知道我们可以用纯 PHP (PHP-PM) 或使用 C 扩展 (Swoole) 编写 Web 服务器。 尽管每种方法都有自己的优点,但这两种选择都不适合我们——我们想要更多的东西。 我们需要的不仅仅是一个 Web 服务器 - 我们希望得到一个解决方案,可以使我们摆脱与 PHP“硬启动”相关的问题,同时可以轻松地针对特定应用程序进行调整和扩展。 也就是说,我们需要一个应用程序服务器。
Go 可以帮忙解决这个问题吗? 我们知道它可以,因为该语言将应用程序编译成单个二进制文件; 它是跨平台的; 使用自己的、非常优雅的并行处理模型(并发)和一个用于处理 HTTP 的库; 最后,我们将可以使用数千个开源库和集成。
结合两种编程语言的困难
首先,有必要确定两个或多个应用程序如何相互通信。
例如,使用
我们决定使用一种不同的、更常见的方法:通过套接字/管道在进程之间构建交互。 这种方法在过去几十年中已被证明是可靠的,并且在操作系统级别得到了很好的优化。
首先,我们创建了一个简单的二进制协议,用于在进程之间交换数据并处理传输错误。 从最简单的形式来看,这种类型的协议类似于
在 PHP 端我们使用
在我们看来,一个协议是不够的 - 我们添加了调用的能力
将任务分配给多个 PHP Worker
实现交互机制后,我们开始思考如何将任务转移到PHP进程中最高效。 当任务到达时,应用服务器必须选择一个空闲的worker来执行它。 如果一个工作进程/进程因错误退出或“死亡”,我们会删除它并创建一个新的来替换它。 如果工作人员/进程已成功完成,我们会将其返回到可用于执行任务的工作人员池中。
为了存储活跃工人池,我们使用
结果,我们得到了一个能够处理任何以二进制形式提出的请求的可用 PHP 服务器。
为了让我们的应用程序开始作为 Web 服务器工作,我们必须选择可靠的 PHP 标准来表示任何传入的 HTTP 请求。 在我们的例子中,我们只是
由于 PSR-7 被认为是不可变的(有些人会说技术上并非如此),因此开发人员编写的应用程序原则上不得将请求视为全局实体。 这非常符合长寿命 PHP 进程的概念。 我们的最终实现(尚未命名)如下所示:
介绍 RoadRunner - 高性能PHP应用服务器
我们的第一个测试任务是 API 后端,它会定期出现不可预测的突发情况(比平常频繁得多)。 尽管 nginx 在大多数情况下已经足够,但我们经常遇到 502 错误,因为我们无法足够快地平衡系统以满足预期的负载增加。
为了取代此解决方案,我们于 2018 年初部署了第一个 PHP/Go 应用程序服务器。 并立即得到了令人难以置信的效果! 我们不仅彻底摆脱了 502 错误,而且还能够将服务器数量减少三分之二,为工程师和产品经理节省了大量资金和头痛药。
到了年中,我们改进了我们的解决方案,在 MIT 许可下将其发布在 GitHub 上,并将其命名为
RoadRunner 如何改进您的开发堆栈
应用
由于内置的 RPC,您可以为 PHP 打开任何 Go 库的 API,而无需编写扩展包装器。 更重要的是,使用 RoadRunner,您可以部署新的非 HTTP 服务器。 示例包括在 PHP 中运行处理程序
在 PHP 和 Go 社区的帮助下,我们提高了解决方案的稳定性,在某些测试中将应用程序性能提高了 40 倍,改进了调试工具,实现了与 Symfony 框架的集成,并增加了对 HTTPS、HTTP/2、插件和 PSR-17。
结论
有些人仍然陷入 PHP 过时的观念中,认为 PHP 是一种缓慢、笨拙的语言,只适合为 WordPress 编写插件。 这些人甚至可能会说PHP有这样一个局限性:当应用程序变得足够大时,你必须选择一种更“成熟”的语言并重写多年积累的代码库。
对于这一切,我想回答:再想一想。 我们相信只有您对 PHP 设置了任何限制。 您可以花一生的时间从一种语言过渡到另一种语言,试图找到最适合您需求的语言,或者您可以开始将语言视为工具。 像 PHP 这样的语言的所谓缺陷实际上可能是其成功的原因。 如果你将它与另一种语言(如 Go)结合起来,那么你将创建比仅限于使用任何一种语言更强大的产品。
使用过很多 Go 和 PHP 后,我们可以说我们喜欢它们。 我们不打算牺牲其中一个 - 相反,我们将寻找方法从这个双堆栈中获得更多价值。