在使用swoole等异步PHP运行时的注意事项

284 阅读8分钟

异步和非阻塞运行时在许多编程语言中都是很常见的,还有那些长期停留在内存中的Web应用,能够调度多个HTTP请求而不必每次都完全启动。在传统上,PHP应用程序并不是这样的。

然而,许多项目开始被采用,它们将这种长寿命的方法带到了PHP世界。

这样做的问题是,PHP开发者并不习惯这样做,他们倾向于继续按照他们一直以来的做法行事。另外,库可能做了一些假设,使得它们在这种情况下使用时不能像预期那样工作。

其中一些项目是swooleReactPHPAmphp,仅举几例。

在这篇文章中,我将专注于第一个项目,并解释如何处理我在一个现有的PHP项目上使用了一年半之后发现的一些痛点。

介绍一下Swoole

与其他类似的项目相比,swoole的主要区别在于它是作为一个原生的PHP扩展而编写的,需要用pecl

这显然有点麻烦,但好处是,由于它是用C++编写的,所以对内存分配有更好的处理,这对实现它试图做的事情非常重要。

swoole所做的是运行一个主进程,该进程对PHP应用程序进行一次引导,然后将其保留在内存中,这样它就可以继续分派请求,而不必每次都加载资源。这使得它非常快。

那些HTTP请求是由一组被称为 "Web工作者 "的子进程处理的。Swoole将主进程内存中的内容分叉给每个工作者,所以每个工作者在内存中都有一份应用程序的 "副本"。

每个工作者都是独立的,但是每个工作者一次只能处理一个请求,所以如果你期望有大量的并发流量,你必须增加工作者的数量。

除了网络工作者之外,swoole还有另一组被称为 "任务工作者 "的子进程,它们可以用来在运行时委托长任务,以防止网络工作者被阻塞。

这里你可以看到swoole的架构图。

How Swoole works

更多信息可以在Swoole的网站上找到。

要解决的问题

现在我们知道了swoole是如何工作的,让我们来看看我过去所面临的一些挑战,如果你习惯于从 "阻塞 "和 "短暂 "的角度考虑问题,这些挑战通常并不可怕。

数据库连接保持开放

你发现的第一件事是,由于应用程序保持在内存中,任何由工作者(无论是Web还是任务工作者)打开的数据库连接都将保持开放,并在随后的执行或应用程序提供的HTTP请求中重复使用。

这在理论上是好事,但也有一个副作用,这在开发过程中并不明显:打开的连接最终会过期

由于大多数流行的PHP数据库抽象层都来自于PHP只是一个每次请求都能启动的技术时代,他们从来没有太在意过期的数据库连接,因为它们会在下次请求时再次打开。

然而,当使用swoole和类似的技术时,你必须实现一些机制来处理重新连接或在发生这种情况时优雅地恢复。

我写了一篇文章,解释了我是如何在使用swoole和doctrine服务的基于中间件的应用中解决这个问题的:如何在使用swoole服务的表达式应用中正确处理doctrine实体管理器

显然,swoole(还有其他的)提供了一些与数据库合作的工具(比如这个MySQL客户端),我相信这些工具不会有这个问题(虽然,我没有亲自尝试过),但它们比其他被广泛采用的库更有局限性。

内存缓存的行为不符合预期

使用本地内存缓存,比如APCu,来提高应用程序在生产中的性能,是一种常见的做法。

通常情况下,APCu比其他选项更快,比如redis或memcached,所以当你只有一个应用实例在运行时(意味着你没有在负载平衡器后面有一个集群),它是一个选择。

然而,即使你只在一台服务器上使用swoole运行你的应用程序,由于前面解释过的web worker系统,你实际上在内存中有好几份你的应用程序,当使用APCu时,它们中的每一个都有一个独立而不共享的缓存。

这可能会导致难以调试的不一致,所以你需要考虑到这一点。

另外,由于正在加载的信息无论如何都是保存在内存中的,所以使用APCu或其他内存缓存,你不会注意到如此大的改进。

请注意,当使用分布式/集中式缓存解决方案时,这个问题不会发生,比如redis或memcached,因为在这种情况下,缓存技术是由所有worker共享的。

不需要优化包括/要求的文件

许多项目都有复杂的配置系统,这些配置系统被分散到多个文件中。虽然这使它更容易维护,但当所有这些文件需要在每个请求中加载时,它也会对性能产生影响。

正因为如此,有一些像laminas-config-aggregator这样的库,可以动态地从许多来源加载配置,如果满足某些条件,它们可以将结果合并到一个更大的文件中,可以在以后的请求中使用。

在swoole中,由于所有的文件在第一次加载时都会被保存在内存中,所以不需要进行这种优化。

类的自动加载只发生一次

这一点与上一点非常相关。

Composer提供了很多优化选项,这样类的自动加载在生产中会更快。虽然你通常还是想使用其中的一些选项,以便在第一次加载类的时候仍然有合理的速度,但任何后续的点击都会使用已经在内存中的类。

你可能不想在swoole中使用作曲家的标志之一是--apcu-autoloader ,这是因为上面提到的两个原因。

服务应该是真正的无状态

这是你无论如何都应该做的事情,但是当一个服务实例在HTTP请求之间被保留在内存中时,这就特别重要了。

有时,我们很容易在一个服务上有一些小的内部状态,我们把一些东西作为一个方法调用的副作用来保存,最终在另一个公共方法被调用时使用它。

当整个应用在每个请求中都被引导时,这似乎是无害的,但当应用被swoole服务时,它可能会产生非常危险的副作用,因为这个实例将持续到另一个请求被同一个web worker派发。

试想一下,你在一个服务上保存了一些用户数据,而它最终在下一个请求中被提供给另一个用户。那就不妙了。

正因为如此,要确保你的服务是真正的无状态,如果因为任何原因无法做到这一点,至少要记得在完成请求之前有一些机制来刷新数据。

开发环境的热重载并不那么好

我们通常认为理所当然的一件事是,我们可以修改一个PHP文件,只要提出一个新的请求,新的代码就会被执行。然而,当代码被保存在内存中时,情况就不那么简单了。

当你在开发一个用swoole提供服务的项目时,你将不得不每次都重新启动服务器以使你的修改得到应用。

Swoole有一个机制可以进行轻度的服务器重载,这样会更快。你可以在它的网站上找到相关的文档。

这个机制的问题是,你需要实现在文件被修改时调用这个机制,或者使用一些库来为你做这件事。

另外,我注意到,即使调用这个机制并看到服务器被重新加载,也不一定有预期的效果,你最终还是要做一个硬重新加载。这是我不完全理解的东西,我仍然需要进一步调查。

结论

当新技术出现时,总是需要一些时间来适应它们,但我强烈建议你尝试一下swoole或其他非阻塞运行时,因为它能使你的应用程序变得超级快。

只要记住以上所有的内容就可以了;-)

也就是说,在这里你可以找到一些库,当你试图用swoole为现有的项目提供服务时,这些库会让你的生活更轻松,而不必与之耦合,能够继续使用你最喜欢的框架。