Laravel应用使用Swoole
Swoole官方:Swoole - PHP 协程框架
为什么要使用Swoole
目前我感知到的(非网络上的官方话语)
常驻内存,避免重复加载带来的性能损耗
支持协程异步,提高对IO密集场景的处理能力
方便更高效的开发、处理Http、Websocket、TCP、UDP等应用(echo-server有bug?swoole能作为替换方案)
常驻内存
「简单描述」
传统处理过程:

在传统的PHP框架处理每一个请求之前,它会做一遍加载框架文件、配置的操作,这个过程成为消耗性能的一大原因。
Swoole:「一次加载重复使用,与Http请求无关的全局对象构造一次即可」。
协程
使用协程时,读写文件、请求接口等场景,会自动挂起协程,把CPU让给其他协程执行任务,这样提升了单线程CPU资源利用,从而提升性能。
总结
PHP 与 Swoole 本身定位不同,没有比较性,Swoole在解决一些PHP覆盖不到的问题和比较薄弱的地方(协程、异步、通信)。
如何使用
Laravel-S 是 Swoole与Laravel之间的适配器,如果项目编码习惯优秀的话,几乎能实现无缝切换。
引入到当前项目
$ composer require "hhxsv5/laravel-s:~3.7.0" -vvv
由于中国网络原因,如果你觉得引入很慢的话,你可以尝试开启命令行代理或尝试以下方案
- 使用阿里云
composer镜像(全局配置):
$ composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
- 多线程
composer
$ composer global require hirak/prestissimo
相比刚出箱的composer,composer install/update 速度几乎快了10倍。
注册服务
在 config/app.php 中,增加以下代码到 providers 数组中。
Hhxsv5\LaravelS\Illuminate\LaravelSServiceProvider::class,
发布配置文件
php artisan laravels publish
config目录下多出了laravels.php,为其配置文件。
本地运行测试
php bin/laravels start
运行结果
[2020-08-01 21:02:38] [INFO] The max time of waiting to forcibly stop is 60s.
[2020-08-01 21:02:38] [INFO] Waiting Swoole[PID=32070] to stop. [1]
[2020-08-01 21:02:39] [INFO] Swoole [PID=32070] is stopped.
_ _ _____
| | | |/ ____|
| | __ _ _ __ __ ___ _____| | (___
| | / _` | '__/ _` \ \ / / _ \ |\___ \
| |___| (_| | | | (_| |\ V / __/ |____) |
|______\__,_|_| \__,_| \_/ \___|_|_____/
Speed up your Laravel/Lumen
>>> Components
+--------------------------+---------+
| Component | Version |
+--------------------------+---------+
| PHP | 7.4.4 |
| Swoole | 4.4.17 |
| LaravelS | 3.7.6 |
| Laravel Framework [prod] | 7.19.1 |
+--------------------------+---------+
>>> Protocols
+-----------+--------+-------------------+----------------+
| Protocol | Status | Handler | Listen At |
+-----------+--------+-------------------+----------------+
| Main HTTP | On | Laravel Framework | 127.0.0.1:5200 |
+-----------+--------+-------------------+----------------+
>>> Feedback: https://github.com/hhxsv5/laravel-s
[2020-08-01 21:02:40] [TRACE] Swoole is running in daemon mode, see "ps -ef|grep laravels".
运行:ps -ef|grep laravels 效验运行状态,再访问 127.0.0.1:5200。
运行成功后,会多出以下文件,将他们添加至 .gitignore中。
| 文件 | 说明 |
|---|---|
| storage/laravels.json | LaravelS的运行时配置文件 |
| storage/laravels.pid | Master进程的PID文件 |
| storage/laravels-timer-process.pid | 定时器Timer进程的PID文件 |
| storage/laravels-custom-processes.pid | 所有自定义进程的PID文件 |
如需想使用 daemonize 模式,修改 config/laravels.php -> swoole -> daemonize 即可,修改完成后 php bin/laravels restart 会以后台运行方式启动。
与Nginx一起使用
配置文件:
upstream swoole {
server 127.0.0.1:5200 weight=5 max_fails=3 fail_timeout=30s;
keepalive 16;
}
server {
listen 80;
server_name 你的域名;
root 你的项目地址/public;
access_log off;
autoindex off;
index index.html index.htm;
location / {
try_files $uri @laravels;
}
location @laravels {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_pass http://swoole;
}
}
注意事项
在swoole中禁用
exit/dit相关函数,所以在 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()之类的函数,以免引起异常问题。swoole_http_response不支持flush函数,所以不要使用与之相关的flush/ob_flush/ob_end_flush/ob_implicit_flush等函数。static的全局变量需要手动销毁。
不要将元素无限追加到静态、全局变量中,可能会导致内存泄露。
单例模式
由于应用启动后,Laravel应用实例位于 Swoole 的 Worker 进程中,并「常驻内存」,Laravel的所有服务都绑定在 Application Ioc 容器中,用的时候从里面取(解析)。
单例模式绑定的服务在应用内解析返回的是同一个对象实例,在传统模式下每次请求会初始化新的Application 容器,但在 Swoole 下不同,Application容器的生命周期将会与 Worker进程的生命周期相同,意味着多个请求返回的是同一个单例实例。
这对大部分场景下是优点,比如数据库连接,但对有些场景则会导致「应用逻辑崩盘」,比如:用户认证。
举例:
$user = User::find(1);
Auth::login($user);
在此之后,只要Worker进程还在,那么:
if(Auth::check()){
return Auth::user();
}
它返回第一次login的用户(ID:1),但之后的所有请求拿到的都是该用户,就算你的id不是1。
对待这些需要在每次请求结束后清理的单例服务,在 config/laravels.php -> cleaners 中配置来启用清理器。
举例:
'cleaners' => [
Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class,
Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class\
],
上面三个都是用户认证相关的清理器,除此之外,该扩展包还提供了针对 Request 和 Cookie 的清理器,可以去源码中查看,如果你想要自定义清理器,也可以仿照这些自带的清理器实现来编写实现了 Hhxsv5\LaravelS\Illuminate\Cleaners\CleanerInterface 接口的清理器类并将其配置到 cleaners 配置项。
除了清理类之外,还可以像上面介绍的那样,在中间件或者服务提供者中处理新请求时销毁已存在的单例服务(laravels 配置文件中包含一个 register_providers 配置项,用于在每次请求处理时重新初始化服务绑定设置)。
同理,通过 static 定义的静态变量也要在必要的时候进行清理,通过 global 定义的全局变量则要慎用,因为它会在同一个 Worker 进程处理的多个请求中复用。
我的项目中的配置:
<?php
/**
* @see https://github.com/hhxsv5/laravel-s/blob/master/Settings-CN.md Chinese
* @see https://github.com/hhxsv5/laravel-s/blob/master/Settings.md English
*/
return [
'listen_ip' => env('LARAVELS_LISTEN_IP', '127.0.0.1'),
'listen_port' => env('LARAVELS_LISTEN_PORT', 5200),
'socket_type' => defined('SWOOLE_SOCK_TCP') ? SWOOLE_SOCK_TCP : 1,
'enable_coroutine_runtime' => true,
'server' => env('LARAVELS_SERVER', 'LaravelS'),
'handle_static' => env('LARAVELS_HANDLE_STATIC', false),
'laravel_base_path' => env('LARAVEL_BASE_PATH', base_path()),
'inotify_reload' => [
'enable' => env('LARAVELS_INOTIFY_RELOAD', false),
'watch_path' => base_path(),
'file_types' => ['.php'],
'excluded_dirs' => [],
'log' => true,
],
'event_handlers' => [],
'websocket' => [
'enable' => false,
//'handler' => XxxWebSocketHandler::class,
],
'sockets' => [],
'processes' => [
//[
// 'class' => \App\Processes\TestProcess::class,
// 'redirect' => false, // Whether redirect stdin/stdout, true or false
// 'pipe' => 0 // The type of pipeline, 0: no pipeline 1: SOCK_STREAM 2: SOCK_DGRAM
// 'enable' => true // Whether to enable, default true
//],
],
'timer' => [
'enable' => env('LARAVELS_TIMER', false),
'jobs' => [
// Enable LaravelScheduleJob to run `php artisan schedule:run` every 1 minute, replace Linux Crontab
//\Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob::class,
// Two ways to configure parameters:
// [\App\Jobs\XxxCronJob::class, [1000, true]], // Pass in parameters when registering
// \App\Jobs\XxxCronJob::class, // Override the corresponding method to return the configuration
],
'max_wait_time' => 5,
],
'swoole_tables' => [],
'register_providers' => [
\Illuminate\Auth\AuthServiceProvider::class,
\Illuminate\Session\SessionServiceProvider::class,
\Illuminate\Pagination\PaginationServiceProvider::class,
],
'cleaners' => [
\Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class,
\Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class,
],
'destroy_controllers' => [
'enable' => false,
'excluded_list' => [
//\App\Http\Controllers\TestController::class,
],
],
'swoole' => [
'daemonize' => env('LARAVELS_DAEMONIZE', false),
'dispatch_mode' => 2,
'reactor_num' => env('LARAVELS_REACTOR_NUM', function_exists('swoole_cpu_num') ? swoole_cpu_num() * 2 : 4),
'worker_num' => env('LARAVELS_WORKER_NUM', function_exists('swoole_cpu_num') ? swoole_cpu_num() * 2 : 8),
//'task_worker_num' => env('LARAVELS_TASK_WORKER_NUM', function_exists('swoole_cpu_num') ? swoole_cpu_num() * 2 : 8),
'task_ipc_mode' => 1,
'task_max_request' => env('LARAVELS_TASK_MAX_REQUEST', 8000),
'task_tmpdir' => @is_writable('/dev/shm/') ? '/dev/shm' : '/tmp',
'max_request' => env('LARAVELS_MAX_REQUEST', 8000),
'open_tcp_nodelay' => true,
'pid_file' => storage_path('laravels.pid'),
'log_file' => storage_path(sprintf('logs/swoole-%s.log', date('Y-m'))),
'log_level' => 4,
'document_root' => base_path('public'),
'buffer_output_size' => 2 * 1024 * 1024,
'socket_buffer_size' => 128 * 1024 * 1024,
'package_max_length' => 4 * 1024 * 1024,
'reload_async' => true,
'max_wait_time' => 60,
'enable_reuse_port' => true,
'enable_coroutine' => true,
'http_compression' => false,
// Slow log
// 'request_slowlog_timeout' => 2,
// 'request_slowlog_file' => storage_path(sprintf('logs/slow-%s.log', date('Y-m'))),
// 'trace_event_worker' => true,
/**
* More settings of Swoole
* @see https://wiki.swoole.com/#/server/setting Chinese
* @see https://www.swoole.co.uk/docs/modules/swoole-server/configuration English
*/
],
];
效果
简单测试一下使用前和使用后的对比。
GraphQL 接口内容:
{
user(id:1){
id
name
}
不使用Swoole:

响应时间:597 ms
使用Swoole:

响应时间:25 ms
对比还是比较明显的。
❝从单机测试参数来看 swoole 可能确实要好很多,但是如果真的实际运用到一个架构体系中 http 的表现能力不一定有 php-fpm 好,因为这不是 swoole 的强项。
从公司当前技术体系来看,Swoole比较适合。
❞
感谢以下博客、文档对我的帮助:
https://github.com/hhxsv5/laravel-s
https://learnku.com/php/t/10939/use-swoole-to-speed-up-your-laravel-application