Laravel应用使用Swoole

2,248 阅读8分钟

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

由于中国网络原因,如果你觉得引入很慢的话,你可以尝试开启命令行代理或尝试以下方案

  1. 使用阿里云composer镜像(全局配置):
$ composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
  1. 多线程composer
$ composer global require hirak/prestissimo

相比刚出箱的composercomposer 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应用实例位于 SwooleWorker 进程中,并常驻内存,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

Swoole4 文档

https://learnku.com/php/t/10939/use-swoole-to-speed-up-your-laravel-application