在过去的十年中,我们一直在为 财富 500 强公司 以及用户人数不超过 500 人的企业开发应用程序。 一直以来,我们的工程师主要使用 PHP 来开发后端。 但是两年前,出现了一些问题不仅严重影响了我们的产品性能,还影响了它们的可扩展性 —— 因此我们将 Golang (Go) 引入了我们的技术栈。
几乎同时,我们发现 Go 不仅允许我们创建更大的应用程序,并且能够将性能提高多达 40 倍。 有了它,我们能够扩展使用 PHP 编写的现有产品,并通过结合两种语言的优势来改进它们。
【资料图】
我们将通过大量的 Go 和 PHP 经验告诉你,如何用它解决实际的开发问题,以及我们如何把它变成一个工具,来消除与 PHP 死亡模型 相关的一些问题。
常规 PHP 开发环境#
在讲述 Go 如何改善 PHP 死亡模型前,先了解一下常规 PHP 开发环境。
通常,应用运行于 nginx 和 PHP-FPM 上。nginx 处理静态请求,而动态请求则被重定向给 PHP-FPM,并由其执行 PHP 代码。也许你用的是 Apache 和 mod_php,但是他们原理相同,运行起来只有细微的差别。
看看 PHP-FPM 是如何执行代码的。当收到请求,PHP-FPM 初始化 PHP 子进程,并将请求的详细信息转发给它,作为其状态的一部分(_GET, _POST, _SERVER 等)。
在 PHP 脚本执行期间,状态将无法更改,因此只能通过一种方式获取一组新的输入数据:清除进程内存并再次初始化它。
这种性能模型有许多优点。你不需要太担心内存消耗,所有进程都是完全隔离的,如果其中一个进程「死亡」,它将自动重新创建,并且不会影响其他进程。但是,当你尝试扩展应用程序时,这种方式会有缺点产生。
典型 PHP 环境的缺点和低效性
如果你从事 PHP 的专业开发,那么你就知道从哪儿开始创建一个新项目 —— 选择框架。它是一个用于依赖注入、ORM、转化和模板方法的库。当然,所有用户输入的数据都可以方便地放在一个对象中(Symfony / HttpFoundation 或者 PSR-7)。这些框架很棒!
但一切都有它的代价。在任何企业框架中,为了处理一个简单的用户请求或访问数据库,您必须加载至少几十个文件,创建许多类,并解析多个配置。但最糟糕的是,在每个任务完成后,您需要重置所有内容并重新启动:您刚刚启动的所有代码都将变得无用,在它的帮助下,您将无法处理另一个请求。把这件事告诉任何用其他语言编写的程序员 —— 你会看到他脸上的困惑。
多年来,PHP 工程师一直在寻找解决此问题的方法,他们使用了延迟加载技术、微帧、优化库、缓存等。但最终,您仍然必须放弃整个应用程序,重新开始 *(译者注:随着 PHP7.4 中预加载的出现,这个问题将得到部分解决)
一个 PHP 进程能处理多个请求吗?
您可以编写持续时间超过几分钟的 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 的 go-php 库,可以实现 PHP 和 Go 进程 (如 Apache 中的 mod_php) 之间的内存共享。但是这个库的功能限制了我们使用它解决问题。
我们决定使用另一种更常见的方法:通过使用 sockets /pipelines 来构建进程之间的交互。 这种方法在过去十年中已经证明了其可靠性,并且在操作系统级别得到了很好的优化。
首先,我们创建了一个简单的二进制协议,用于在进程之间交换数据和处理传输错误。在其最简单的形式中, 这种类型的协议类似于 一个具有固定大小的 packet 头 (在我们的示例中为 17 个字节) 的 netstring ,其中包含的信息有 packet 的类型,其大小和二进制掩码的信息,用来检查数据的完整性。
在 PHP 端,我们使用了 pack 函数 ,在 Go 端,使用了 编码 / 二进制 库。
有一个协议对我们来说有点过时,我们添加了直接 从 PHP 调用 net /rpc Go 服务 的功能。 这个功能在后面的开发中对我们有很大的帮助,因为我们可以轻松地将 Go 库集成到 PHP 应用程序中。这项工作的结果可以在我们的另一个开源产品 Goridge 中看到。
在多个 PHP Worker 之间分配任务
在交互机制实现之后,我们开始思考如何更好地将任务转移到 PHP 进程中。当任务到达时,应用服务器必须选择一个空闲的 worker 来执行它。 如果 worker 进程因错误而终止或「死亡」,我们将清除它并创建一个新的。 如果 worker 进程成功执行,我们会将它返回到可用于执行任务的 worker 池中。
为了存储活跃的 worker 进程池,我们使用了一个 缓冲通道 , 为了从池中清除意外「死亡」的工作进程,我们添加了一种跟踪错误和 worker 进程状态的机制。
最终,我们得到了一个可以运行的 PHP 服务器,它能够处理任何以二进制形式呈现的请求。
为了让我们的应用程序作为 web 服务器开始工作,我们必须选择一个可靠的 PHP 标准来处理任何传入的 HTTP 请求。在我们的例子中,我们只需将简单的 net /http 请求从 Go 转换 为 PSR-7 格式,这样它就可以与目前大多数可用的 PHP 框架兼容。
由于 PSR-7 被认为是不可变的(有人会说在技术上不是),开发人员必须编写那些在原则上不将请求视为全局实体的应用程序。这完全符合 PHP 常驻进程的概念。我们的最终实现(尚未收到名称)如下所示:
RoadRunner - 高- 性能 PHP 应用服务器
我们的第一个测试任务是一个 API 后端,在该后端上,会周期性地出现不可预测的突发请求(比平时更频繁)。虽然在大多数情况下 nginx capabilities 是足够的,但是我们经常因为无法在预期的负载增加下快速平衡系统而遇到 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 处理器,创建强大的队列 选择器, 甚至将 gRPC 添加到我们的应用程序中。
同时使用 PHP 和 Go ,对解决方案有了稳定的提升,在一些测试中将应用程序性能提高了 40 倍,改进了调试工具,实现了与 Symfony 框架的集成,并添加了对 HTTPS、HTTP/2、插件和 PSR-17 的支持。
结论
有些人仍然被过时的 PHP 概念所束缚,认为 PHP 是一种缓慢、繁琐的语言,只适合在 WordPress 下编写插件。这些人甚至还说 PHP 有这样一个限制:当应用程序变得足够大时,你必须选择一种更「成熟」的语言,并重写多年积累的代码库。
对于这些问题,我的回答是:再想一想。我们相信只是你自己为 PHP 设置了一些限制。你可以用一生的时间从一种语言迁移到另一种语言,试图找到与你的需求完美结合的语言,或者你可以将语言视为工具。像 PHP 这样的语言,它的假想缺陷可能是其成功的真正原因。如果你将它与另一种语言(如 Go)相结合,那么你将创造出比只使用一种语言更强大的产品。
在交替使用过 Go 和 PHP 之后,我们可以说我们很喜欢它们。我们不打算牺牲其中一个来换取另一个,相反,我们会想办法从这个双重架构中获得更多收益。
推荐学习:《PHP视频教程》
以上就是为速度而生:PHP 与Golang 的合体 —— RoadRunner的详细内容,更多请关注php中文网其它相关文章!