Swoole 驱动的 Laravel 应用优化原理及注意事项

上篇教程学院君给大家简单介绍了 Swoole 底层组件和运行原理,今天我们结合 Laravel 框架来介绍基于 Swoole 驱动的 Laravel 应用开发与传统的基于 PHP-FPM 的 Laravel 应用开发有哪些区别,在开发过程中要注意些什么。

Swoole vs. PHP-FPM

我们先来看看传统的基于 PHP-FPM 的 Laravel 应用启动和请求处理流程:

PHP 底层处理逻辑

如上图所示,PHP-FPM 位于 SAPI 层,PHP 底层在接收到来自 Nginx 转发过来的 PHP 请求时,会将其交给某个空闲的 PHP-FPM 进程来处理,PHP-FPM 进程会在启动阶段设置 HTTP 环境变量,然后通过 PHP 核心代码初始化所有已经启用的 PHP 模块(即扩展),并对此次请求上下文进行初始化,完成,这些操作后再调用 Zend 引擎来编译并执行业务逻辑代码(进入 Laravel 项目,从入口文件开始执行),具体的代码执行流程如下:

PHP Zend 引擎编译执行流程

声明:图片来源于 laravel-swoole wiki

Zend 引擎会检查 OpCode 缓存,如果代码片段已经缓存,则从缓存中读取并执行,否则还要编译成 OpCode 并缓存后才能执行。

代码执行完成后,会将处理结果打印或着发送 HTTP 响应给客户端,然后 PHP 底层代码会执行请求关闭及模块关闭函数进行后续清理工作,最后再回到 SAPI 层,调用 PHP-FPM 对应的关闭函数,从而完成此次请求的所有流程。

这个过程周而复始,每次用户有新请求过来都会从头执行一遍,所有的环境初始化、模块初始化、请求初始化以及 Laravel 应用的启动过程,乃至后续请求关闭、模块关闭、PHP-FPM 关闭,如果 Redis、MySQL 之类的网络请求没有连接池,那么每次新请求过来,所有的连接操作也要重新建立,所以传统模式下的 PHP 应用性能表现一直为人所诟病,尽管 Nginx + PHP-FPM 模式已经大大优于基于 Apache 运行的 PHP 应用了。

那我们能不能优化这个请求处理流程呢?比如把环境初始化、模块初始化、请求初始化、Laravel 应用的启动过程只执行一次,然后后面过来的请求复用上一次初始化的 PHP 环境?此外,对于 Redis、MySQL 这些耗时的网络连接以连接池的方式管理起来?事实上,基于 Swoole 就可以完成这些优化,并且我们还可以基于其提供的协程功能实现并发编程,使得在 PHP 中也可以轻松实现异步并发编程,不过关于 PHP 动态语言执行时的性能优化(边解释边执行)这一点需要 PHP 底层开发组去优化,毕竟动态语言有利有弊,不可能又要性能,又要编码灵活性。

但是 Laravel 官方并没有实现对 Swoole 的兼容和集成,所以我们需要自己实现在 Laravel 中集成 Swoole 进行编码工作,从而充分利用 Swoole 的异步编程、并发编程特性提升 Laravel 的性能,但是如果想在 Laravel 中充分集成 Swoole 并不是一件轻松的工作,要考虑和测试的东西很多,好在现在已经有了可选的扩展包,业内比较有名的是 laravelslaravel-swoole,基于它们提供的功能,我们可以轻松在 Laravel 中基于 Swoole 实现高性能编程。

为什么基于 Swoole 驱动的 Laravel 应用性能更好?

下面我们来看看基于 Swoole 驱动的 Laravel 应用从哪些方面对传统的 PHP Web 请求处理流程进行了优化。

laravels 扩展包为例,它为我们提供了一个内置的基于 Swoole 的 HTTP 服务器,通过 php bin/laravels start 命令启动,Nginx 会将 PHP 请求都发到这个服务器进行处理,与 PHP-FPM 不同的是,这个 Swoole 服务器启动后,会开启多个 Worker 进程,在每个 Worker 进程中,Laravel 应用启动及之前的环境初始化工作只执行一次,请求结束后,Laravel 应用实例不会回收,后续发给该 Worker 进程处理的请求会复用之前已经启动的 Laravel 应用实例,再结合 MySQL、Redis 长连接,从而极大提高了 Laravel 应用的性能。

在 Laravel 中使用 Swoole 的注意事项

单例模式

如上所述,Laravel 应用实例位于 Swoole 的 Worker 进程中,并且常驻内存,这种模式提升了应用性能的同时,也引入了新的复杂性,因为 Laravel 底层的核心是一个 Application IoC 容器,所有服务都绑定在这个容器里,然后在应用的时候从里面解析,包括通过 singleton 方法以单例模式绑定的服务。

这在传统的每次请求重新初始化新的 Application 容器的开发模式中当然没有什么问题,但是现在问题来了,单例模式绑定的服务在整个应用生命周期内解析时返回的都是同一个对象实例,现在这个生命周期延生到整个 Worker 进程的生命周期,只要 Worker 进程还在,那么多个请求使用的可能都是同一个单例服务,这对不同请求可以复用单例的场景来说是好事,比如数据库连接,但是对另一些场景,不同请求不能复用同一个实例,比如用户认证,则是灾难了,所以在这种场景下,需要在一次请求完成后手动注销这些单例服务(或者在下次实例化先判断单例是否存在,如果存在将其销毁)。

还是以 laravels 扩展包为例,它为我们提供的针对这种场景的解决方案是在每次请求处理完成后调用清理器对这些单例服务进行请求,你可以通过在 laravels 配置文件中注释 cleaners 配置项来启用这些清理器:

'cleaners'                 => [
    Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class, // If you use the session/authentication in your project, please uncomment this line
    Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class,    // If you use the authentication/passport in your project, please uncomment this line
    Hhxsv5\LaravelS\Illuminate\Cleaners\JWTCleaner::class,     // If you use the package "tymon/jwt-auth" in your project, please uncomment this line
    // ...
],

上面三个都是用户认证相关的清理器,除此之外,该扩展包还提供了针对 Request 和 Cookie 的清理器,可以去源码中查看,如果你想要自定义清理器,也可以仿照这些自带的清理器实现来编写实现了 Hhxsv5\LaravelS\Illuminate\Cleaners\CleanerInterface 接口的清理器类并将其配置到 cleaners 配置项。

除了清理类之外,还可以像上面介绍的那样,在中间件或者服务提供者中处理新请求时销毁已存在的单例服务(laravels 配置文件中包含一个 register_providers 配置项,用于在每次请求处理时重新初始化服务绑定设置)。

同理,通过 static 定义的静态变量也要在必要的时候进行清理,通过 global 定义的全局变量则要慎用,因为它会在同一个 Worker 进程处理的多个请求中复用。

禁用函数

exit/die 相关

由于 Swoole 中禁用 exit/die 函数,所以在 Laravel 中也不能使用它们,以及与之相关的 dd 函数。

请求相关

不要使用 $_GET/$_POST/$_FILES/$_COOKIE/$_REQUEST/$_SESSION/$GLOBALS/$_ENV 之类的超全局变量,统一通过 Illuminate\Http\Request 对象获取请求数据。

另外,Swoole 限制 GET 请求头长度不能超过 2KB,POST 请求数据长度也会通过 package_max_length 配置进行限制,默认是 2M。

响应相关

统一通过 Illuminate\Http\Response 返回响应,不要使用 header()/setcookie()/http_response_code() 之类的函数,以免引起异常问题。

flush 相关

swoole_http_response 不支持 flush 函数,所以不要使用与之相关的 flush/ob_flush/ob_end_flush/ob_implicit_flush 等函数。

上一篇: Swoole 的底层架构及运行原理

下一篇: 基于 Laravel + Swoole + Vue 组件实现支持高并发的实时弹幕功能(上)