LaravelS – 通过Swoole加速 Laravel/Lumen

Php

浏览数:941

2018-4-2

LaravelS – 站在巨人的肩膀上

? 通过Swoole加速Laravel/Lumen,其中的S代表Swoole,速度,高性能。

Github LaravelS QQ交流群:698480528

特性

要求

依赖 说明
PHP >= 5.5.9
Swoole >= 1.7.19 推荐最新的稳定版 从2.0.12开始不再支持PHP5
Laravel/Lumen >= 5.1
Gzip[可选的] zlib,用于压缩HTTP响应,检查本机libz是否可用 ldconfig -p|grep libz
Inotify[可选的] inotify,用于修改代码后自动Reload Worker进程,检查本机inotify是否可用 php –ri inotify

安装

1.通过Composer安装(packagist)。

# 在你的Laravel/Lumen项目的根目录下执行
composer require "hhxsv5/laravel-s:~1.0" -vvv
# 确保你的composer.lock文件是在版本控制中

2.添加Service Provider。

  • Laravel: 修改文件config/app.php
'providers' => [
    //...
    Hhxsv5\LaravelS\Illuminate\LaravelSServiceProvider::class,
],
  • Lumen: 修改文件bootstrap/app.php
$app->register(Hhxsv5\LaravelS\Illuminate\LaravelSServiceProvider::class);

3.发布配置文件。

每次升级LaravelS后,建议重新发布一次配置文件

php artisan laravels publish

使用Lumen时的特别说明: 你不需要手动加载配置laravels.php,LaravelS底层已自动加载。

// 不必手动加载,但加载了也不会有问题
$app->configure('laravels');

4.修改配置config/laravels.php:监听的IP、端口等,请参考配置项

运行

php artisan laravels {start|stop|restart|reload|publish}

命令 说明
start 启动LaravelS,展示已启动的进程列表 ps -ef|grep laravels
stop 停止LaravelS
restart 重启LaravelS
reload 平滑重启所有worker进程,这些worker进程内包含你的业务代码和框架(Laravel/Lumen)代码,不会重启master/manger进程
publish 发布配置文件到你的项目中config/laravels.php

与Nginx配合使用(推荐)

gzip on;
gzip_min_length 1024;
gzip_comp_level 2;
gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml application/x-httpd-php image/jpeg image/gif image/png font/ttf font/otf image/svg+xml;
gzip_vary on;
gzip_disable "msie6";

upstream laravels {
    server 192.168.0.1:5200 weight=5 max_fails=3 fail_timeout=30s;
    #server 192.168.0.2:5200 weight=3 max_fails=3 fail_timeout=30s;
    #server 192.168.0.3:5200 backup;
}
server {
    listen 80;
    server_name laravels.com;
    root /xxxpath/laravel-s-test/public;
    access_log /yyypath/log/nginx/$server_name.access.log  main;
    autoindex off;
    index index.html index.htm;
    
    # Nginx处理静态资源,LaravelS处理动态资源。
    location / {
        try_files $uri @laravels;
    }
    location @laravels {
        proxy_http_version 1.1;
        # proxy_connect_timeout 60s;
        # proxy_send_timeout 60s;
        # proxy_read_timeout 120s;
        proxy_set_header Connection "keep-alive";
        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 $host;
        proxy_pass http://laravels;
    }
}

与Apache配合使用

LoadModule proxy_module /yyypath/modules/mod_deflate.so
<IfModule deflate_module>
    SetOutputFilter DEFLATE
    DeflateCompressionLevel 2
    AddOutputFilterByType DEFLATE text/html text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml application/x-httpd-php image/jpeg image/gif image/png font/ttf font/otf image/svg+xml
</IfModule>

<VirtualHost *:80>
    ServerName www.laravels.com
    ServerAdmin hhxsv5@sina.com

    DocumentRoot /xxxpath/laravel-s-test/public;
    DirectoryIndex index.html index.htm
    <Directory "/">
        AllowOverride None
        Require all granted
    </Directory>

    LoadModule proxy_module /yyypath/modules/mod_proxy.so
    LoadModule proxy_module /yyypath/modules/mod_proxy_balancer.so
    LoadModule proxy_module /yyypath/modules/mod_lbmethod_byrequests.so.so
    LoadModule proxy_module /yyypath/modules/mod_proxy_http.so.so
    LoadModule proxy_module /yyypath/modules/mod_slotmem_shm.so
    LoadModule proxy_module /yyypath/modules/mod_rewrite.so

    ProxyRequests Off
    ProxyPreserveHost On
    <Proxy balancer://laravels>  
        BalancerMember http://192.168.1.1:8011 loadfactor=7
        #BalancerMember http://192.168.1.2:8011 loadfactor=3
        #BalancerMember http://192.168.1.3:8011 loadfactor=1 status=+H
        ProxySet lbmethod=byrequests
    </Proxy>
    #ProxyPass / balancer://laravels/
    #ProxyPassReverse / balancer://laravels/

    # Apache处理静态资源,LaravelS处理动态资源。
    RewriteEngine On
    RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-d
    RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-f
    RewriteRule ^/(.*)$ balancer://laravels/%{REQUEST_URI} [P,L]

    ErrorLog ${APACHE_LOG_DIR}/www.laravels.com.error.log
    CustomLog ${APACHE_LOG_DIR}/www.laravels.com.access.log combined
</VirtualHost>

启用WebSocket服务器

WebSocket服务器监听的IP和端口与Http服务器相同。

1.创建WebSocket Handler类,并实现接口WebsocketHandlerInterface

namespace App\Services;
use Hhxsv5\LaravelS\Swoole\WebsocketHandlerInterface;
/**
 * @see https://wiki.swoole.com/wiki/page/400.html
 */
class WebsocketService implements WebsocketHandlerInterface
{
    public function onOpen(\swoole_websocket_server $server, \swoole_http_request $request)
    {
        \Log::info('New Websocket connection', [$request->fd]);
        $server->push($request->fd, 'Welcome to LaravelS');
        // throw new \Exception('an exception'); //上层会自动忽略handle时抛出的异常
    }
    public function onMessage(\swoole_websocket_server $server, \swoole_websocket_frame $frame)
    {
        \Log::info('Received message', [$frame->fd, $frame->data, $frame->opcode, $frame->finish]);
        $server->push($frame->fd, date('Y-m-d H:i:s'));
    }
    public function onClose(\swoole_websocket_server $server, $fd, $reactorId)
    {
    }
}

2.更改配置config/laravels.php

// ...
'websocket'      => [
    'enable'  => true,
    'handler' => \App\Services\WebsocketService::class,
],
'swoole'         => [
    //...
    // dispatch_mode只能设置为2、4、5,https://wiki.swoole.com/wiki/page/277.html
    'dispatch_mode' => 2,
    //...
],
// ...

3.与Nginx配合使用(推荐)

参考 WebSocket代理

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

upstream laravels-ws {
    server 192.168.0.1:5200 weight=5 max_fails=3 fail_timeout=30s;
    #server 192.168.0.2:5200 weight=3 max_fails=3 fail_timeout=30s;
    #server 192.168.0.3:5200 backup;
}

server {
    listen 80;
    server_name laravels-ws.com;
    root /xxxpath/laravel-s-test/public;
    access_log /yyypath/log/nginx/$server_name.access.log  main;
    autoindex off;
    index index.html index.htm;
    
    location / {
        proxy_http_version 1.1;
        # proxy_connect_timeout 60s;
        # proxy_send_timeout 60s;
        # proxy_read_timeout:如果60秒内客户端没有发数据到服务端,那么Nginx会关闭连接;同时,Swoole的心跳设置也会影响连接的关闭
        # proxy_read_timeout 60s;
        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 $host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_pass http://laravels-ws;
    }
}

监听事件

系统事件

通常,你可以在这些事件中重置或销毁一些全局或静态的变量,也可以修改当前的请求和响应。

  • laravels.received_requestswoole_http_request转成Illuminate\Http\Request后,在Laravel内核处理请求前。
// 修改`app/Providers/EventServiceProvider.php`, 添加下面监听代码到boot方法中
// 如果变量$events不存在,你也可以通过Facade调用\Event::listen()。
$events->listen('laravels.received_request', function (\Illuminate\Http\Request $req, $app) {
    $req->query->set('get_key', 'hhxsv5');// 修改querystring
    $req->request->set('post_key', 'hhxsv5'); // 修改post body
});
  • laravels.generated_response 在Laravel内核处理完请求后,将Illuminate\Http\Response转成swoole_http_response之前(下一步将响应给客户端)。
// 修改`app/Providers/EventServiceProvider.php`, 添加下面监听代码到boot方法中
// 如果变量$events不存在,你也可以通过Facade调用\Event::listen()。
$events->listen('laravels.generated_response', function (\Illuminate\Http\Request $req, \Symfony\Component\HttpFoundation\Response $rsp, $app) {
    $rsp->headers->set('header-key', 'hhxsv5');// 修改header
});

自定义的异步事件

事件监听的处理能力受Task进程数影响,需合理设置task_worker_num

1.创建事件类。

use Hhxsv5\LaravelS\Swoole\Task\Event;
class TestEvent extends Event
{
    private $data;
    public function __construct($data)
    {
        $this->data = $data;
    }
    public function getData()
    {
        return $this->data;
    }
}

2.创建监听器类。

use Hhxsv5\LaravelS\Swoole\Task\Event;
use Hhxsv5\LaravelS\Swoole\Task\Listener;
class TestListener1 extends Listener
{
    public function handle(Event $event)
    {
        \Log::info(__CLASS__ . ':handle start', [$event->getData()]);
        sleep(2);// 模拟一些慢速的事件处理
        // throw new \Exception('an exception'); //上层会自动忽略handle时抛出的异常
    }
}

3.绑定事件与监听器。

// 在"config/laravels.php"中绑定事件与监听器,一个事件可以有多个监听器,多个监听器按顺序执行
[
    // ...
    'events' => [
        \App\Tasks\TestEvent::class => [
            \App\Tasks\TestListener1::class,
            //\App\Tasks\TestListener2::class,
        ],
    ],
    // ...
];

4.触发事件。

// 实例化TestEvent并通过fire触发,此操作是异步的,触发后立即返回,由Task进程继续处理监听器中的handle逻辑
use Hhxsv5\LaravelS\Swoole\Task\Event;
$success = Event::fire(new TestEvent('event data'));
var_dump($success);//判断是否触发成功

异步的任务队列

异步任务的处理能力受Task进程数影响,需合理设置task_worker_num

1.创建任务类。

use Hhxsv5\LaravelS\Swoole\Task\Task;
class TestTask extends Task
{
    private $data;
    private $result;
    public function __construct($data)
    {
        $this->data = $data;
    }
    // 处理任务的逻辑,运行在Task进程中,不能投递任务
    public function handle()
    {
        \Log::info(__CLASS__ . ':handle start', [$this->data]);
        sleep(2);// 模拟一些慢速的事件处理
        // throw new \Exception('an exception'); //上层会自动忽略handle时抛出的异常
        $this->result = 'the result of ' . $this->data;
    }
    // 可选的,完成事件,任务处理完后的逻辑,运行在Worker进程中,可以投递任务
    public function finish()
    {
        \Log::info(__CLASS__ . ':finish start', [$this->result]);
        Task::deliver(new TestTask2('task2')); // 投递其他任务
    }
}

2.投递任务。

// 实例化TestTask并通过deliver投递,此操作是异步的,投递后立即返回,由Task进程继续处理TestTask中的handle逻辑
use Hhxsv5\LaravelS\Swoole\Task\Task;
$task = new TestTask('task data');
// $task->delay(3);// 延迟3秒投放任务
$ret = Task::deliver($task);
var_dump($ret);//判断是否投递成功

毫秒级定时任务

基于Swoole的毫秒定时器,封装的定时任务,取代LinuxCrontab

1.创建定时任务类。

namespace App\Jobs\Timer;
use Hhxsv5\LaravelS\Swoole\Timer\CronJob;
class TestCronJob extends CronJob
{
    protected $i = 0;
    public function __construct()
    {
    }
    public function interval()
    {
        return 1000;// 每1秒运行一次
    }
    public function run()
    {
        \Log::info(__METHOD__, ['start', $this->i, microtime(true)]);
        // do something
        $this->i++;
        \Log::info(__METHOD__, ['end', $this->i, microtime(true)]);

        if ($this->i >= 10) { // 运行10次后不再执行
            \Log::info(__METHOD__, ['stop', $this->i, microtime(true)]);
            $this->stop(); // stop this cron job
        }
    }
}

2.绑定定时任务类。

// 在"config/laravels.php"绑定定时任务类
[
    // ...
    'timer'          => [
        'enable' => true, //启用Timer
        'jobs'   => [ //绑定的定时任务类列表
            // 启用LaravelScheduleJob来执行`php artisan schedule:run`,每分钟一次,替代Linux Crontab
            //\Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob::class,
            \App\Jobs\Timer\TestCronJob::class,
        ],
    ],
    // ...
];

在你的项目中使用swoole_server实例

/**
 * 如果启用websocket server,$swoole是`swoole_websocket_server`的实例,否则是是`\swoole_http_server`的实例
 * @var \swoole_http_server|\swoole_websocket_server $swoole
 */
$swoole = app('swoole');
var_dump($swoole->stats());// 单例

注意事项

  • 推荐通过Illuminate\Http\Request对象来获取请求信息,兼容$_SERVER、$_ENV、$_GET、$_POST、$_FILES、$_COOKIE、$_REQUEST,不能使用$_SESSION。
public function form(\Illuminate\Http\Request $request)
{
    $name = $request->input('name');
    $all = $request->all();
    $sessionId = $request->cookie('sessionId');
    $photo = $request->file('photo');
    $rawContent = $request->getContent();
    //...
}
  • 推荐通过返回Illuminate\Http\Response对象来响应请求,兼容echo、vardump()、print_r(),不能使用函数像exit()、die()、header()、setcookie()、http_response_code()。
public function json()
{
    return response()->json(['time' => time()])->header('header1', 'value1')->withCookie('c1', 'v1');
}
  • 你声明的全局、静态变量必须手动清理或重置。
  • 无限追加元素到静态或全局变量中,将导致内存爆满。
// 某类
class Test
{
    public static $array = [];
    public static $string = '';
}

// 某控制器
public function test(Request $req)
{
    // 内存爆满
    Test::$array[] = $req->input('param1');
    Test::$string .= $req->input('param2');
}

已知的兼容性问题

待办事项

  1. 针对MySQL/Redis的连接池。
  2. 包装MySQL/Redis/Http的协程客户端。

如果对你有帮助,Star Me LaravelS QQ交流群:698480528

原文地址:https://segmentfault.com/a/1190000013358289