LaravelS - 基于 Swoole 加速 Laravel/Lumen - 带你飞 🚀

LaravelS Logo

中文文档 | English Docs

🚀 LaravelS 是 Laravel/Lumen 和 Swoole 之间开箱即用的适配器

Latest Version PHP Version Swoole Version Total Downloads Build Status Code Intelligence Status License


QQ交流

  • 698480528
  • 62075835

文档目录

特性

Benchmark

要求

依赖 说明
PHP >= 5.5.9 推荐PHP7+
Swoole >= 1.7.19 从2.0.12开始不再支持PHP5 推荐4.5.0+
Laravel/Lumen >= 5.1 推荐8.0+

安装

1.通过Composer安装(packagist)。有可能找不到3.0版本,解决方案移步#81

composer require "hhxsv5/laravel-s:~3.7.0" -vvv
# 确保你的composer.lock文件是在版本控制中

2.注册Service Provider(以下两步二选一)。

  • Laravel: 修改文件config/app.phpLaravel 5.5+支持包自动发现,你应该跳过这步

      'providers' => [
          //...
          Hhxsv5\LaravelS\Illuminate\LaravelSServiceProvider::class,
      ],
  • Lumen: 修改文件bootstrap/app.php

      $app->register(Hhxsv5\LaravelS\Illuminate\LaravelSServiceProvider::class);

3.发布配置和二进制文件。

每次升级LaravelS后,需重新publish;点击Release去了解各个版本的变更记录。

php artisan laravels publish
# 配置文件:config/laravels.php
# 二进制文件:bin/laravels bin/fswatch bin/inotify

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

5.性能调优

  • 调整内核参数

  • Worker数量:LaravelS 使用 Swoole 的同步IO模式,worker_num设置的越大并发性能越好,但会造成更多的内存占用和进程切换开销。如果1个请求耗时100ms,为了提供1000QPS的并发能力,至少需要配置100个Worker进程,计算方法:worker_num = 1000QPS/(1s/1ms) = 100,故需进行增量压测计算出最佳的worker_num

  • Task Worker数量

运行

在运行之前,请先仔细阅读:注意事项(这非常重要)。

  • 操作命令:php bin/laravels {start|stop|restart|reload|info|help}
命令 说明
start 启动LaravelS,展示已启动的进程列表 “ps -ef|grep laravels
stop 停止LaravelS,并触发自定义进程的onStop方法
restart 重启LaravelS:先平滑Stop,然后再Start;在Start完成之前,服务是不可用的
reload 平滑重启所有Task/Worker/Timer进程(这些进程内包含了你的业务代码),并触发自定义进程的onReload方法,不会重启Master/Manger进程;修改config/laravels.php后,你只有调用restart来完成重启
info 显示组件的版本信息
help 显示帮助信息
  • 启动选项,针对startrestart命令。
选项 说明
-d|–daemonize 以守护进程的方式运行,此选项将覆盖laravels.phpswoole.daemonize设置
-e|–env 指定运行的环境,如--env=testing将会优先使用配置文件.env.testing,这个特性要求Laravel 5.2+
-i|–ignore 忽略检查Master进程的PID文件
-x|–x-version 记录当前工程的版本号(分支),保存在$_ENV/$_SERVER中,访问方式:$_ENV['X_VERSION'] $_SERVER['X_VERSION'] $request->server->get('X_VERSION')
  • 运行时文件:start时会自动执行php artisan laravels config并生成这些文件,开发者一般不需要关注它们,建议将它们加到.gitignore中。
文件 说明
storage/laravels.conf LaravelS的运行时配置文件
storage/laravels.pid Master进程的PID文件
storage/laravels-timer-process.pid 定时器Timer进程的PID文件
storage/laravels-custom-processes.pid 所有自定义进程的PID文件

部署

建议通过Supervisord监管主进程,前提是不能加-d选项并且设置swoole.daemonizefalse

[program:laravel-s-test]
directory=/var/www/laravel-s-test
command=/usr/local/bin/php bin/laravels start -i
numprocs=1
autostart=true
autorestart=true
startretries=3
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/supervisor/%(program_name)s.log

与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 swoole {
    # 通过 IP:Port 连接
    server 127.0.0.1:5200 weight=5 max_fails=3 fail_timeout=30s;
    # 通过 UnixSocket Stream 连接,小诀窍:将socket文件放在/dev/shm目录下,可获得更好的性能
    #server unix:/yourpath/laravel-s-test/storage/laravels.sock weight=5 max_fails=3 fail_timeout=30s;
    #server 192.168.1.1:5200 weight=3 max_fails=3 fail_timeout=30s;
    #server 192.168.1.2:5200 backup;
    keepalive 16;
}
server {
    listen 80;
    # 别忘了绑Host
    server_name laravels.com;
    root /yourpath/laravel-s-test/public;
    access_log /yourpath/log/nginx/$server_name.access.log  main;
    autoindex off;
    index index.html index.htm;
    # Nginx处理静态资源(建议开启gzip),LaravelS处理动态资源。
    location / {
        try_files $uri @laravels;
    }
    # 当请求PHP文件时直接响应404,防止暴露public/*.php
    #location ~* \.php$ {
    #    return 404;
    #}
    location @laravels {
        # proxy_connect_timeout 60s;
        # proxy_send_timeout 60s;
        # proxy_read_timeout 120s;
        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;
        # “swoole”是指upstream
        proxy_pass http://swoole;
    }
}

与Apache配合使用

LoadModule proxy_module /yourpath/modules/mod_proxy.so
LoadModule proxy_balancer_module /yourpath/modules/mod_proxy_balancer.so
LoadModule lbmethod_byrequests_module /yourpath/modules/mod_lbmethod_byrequests.so
LoadModule proxy_http_module /yourpath/modules/mod_proxy_http.so
LoadModule slotmem_shm_module /yourpath/modules/mod_slotmem_shm.so
LoadModule rewrite_module /yourpath/modules/mod_rewrite.so
LoadModule remoteip_module /yourpath/modules/mod_remoteip.so
LoadModule deflate_module /yourpath/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>
    # 别忘了绑Host
    ServerName www.laravels.com
    ServerAdmin hhxsv5@sina.com

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

    RemoteIPHeader X-Forwarded-For

    ProxyRequests Off
    ProxyPreserveHost On
    <Proxy balancer://laravels>  
        BalancerMember http://192.168.1.1:5200 loadfactor=7
        #BalancerMember http://192.168.1.2:5200 loadfactor=3
        #BalancerMember http://192.168.1.3:5200 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。start时会自动实例化,不需要手动创建实例。

namespace App\Services;
use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface;
use Swoole\Http\Request;
use Swoole\Http\Response;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
/**
 * @see https://wiki.swoole.com/#/start/start_ws_server
 */
class WebSocketService implements WebSocketHandlerInterface
{
    // 声明没有参数的构造函数
    public function __construct()
    {
    }
    // public function onHandShake(Request $request, Response $response)
    // {
           // 自定义握手:https://wiki.swoole.com/#/websocket_server?id=onhandshake
           // 成功握手之后会自动触发onOpen事件
    // }
    public function onOpen(Server $server, Request $request)
    {
        // 在触发onOpen事件之前,建立WebSocket的HTTP请求已经经过了Laravel的路由,
        // 所以Laravel的Request、Auth等信息是可读的,Session是可读写的,但仅限在onOpen事件中。
        // \Log::info('New WebSocket connection', [$request->fd, request()->all(), session()->getId(), session('xxx'), session(['yyy' => time()])]);
        // 此处抛出的异常会被上层捕获并记录到Swoole日志,开发者需要手动try/catch
        $server->push($request->fd, 'Welcome to LaravelS');
    }
    public function onMessage(Server $server, Frame $frame)
    {
        // \Log::info('Received message', [$frame->fd, $frame->data, $frame->opcode, $frame->finish]);
        // 此处抛出的异常会被上层捕获并记录到Swoole日志,开发者需要手动try/catch
        $server->push($frame->fd, date('Y-m-d H:i:s'));
    }
    public function onClose(Server $server, $fd, $reactorId)
    {
        // 此处抛出的异常会被上层捕获并记录到Swoole日志,开发者需要手动try/catch
    }
}

2.更改配置config/laravels.php

// ...
'websocket'      => [
    'enable'  => true, // 注意:设置enable为true
    'handler' => \App\Services\WebSocketService::class,
],
'swoole'         => [
    //...
    // dispatch_mode只能设置为2、4、5,https://wiki.swoole.com/#/server/setting?id=dispatch_mode
    'dispatch_mode' => 2,
    //...
],
// ...

3.使用SwooleTable绑定FD与UserId,可选的,Swoole Table示例。也可以用其他全局存储服务,例如Redis/Memcached/MySQL,但需要注意多个Swoole Server实例时FD可能冲突。

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

参考 WebSocket代理

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}
upstream swoole {
    # 通过 IP:Port 连接
    server 127.0.0.1:5200 weight=5 max_fails=3 fail_timeout=30s;
    # 通过 UnixSocket Stream 连接,小诀窍:将socket文件放在/dev/shm目录下,可获得更好的性能
    #server unix:/yourpath/laravel-s-test/storage/laravels.sock weight=5 max_fails=3 fail_timeout=30s;
    #server 192.168.1.1:5200 weight=3 max_fails=3 fail_timeout=30s;
    #server 192.168.1.2:5200 backup;
    keepalive 16;
}
server {
    listen 80;
    # 别忘了绑Host
    server_name laravels.com;
    root /yourpath/laravel-s-test/public;
    access_log /yourpath/log/nginx/$server_name.access.log  main;
    autoindex off;
    index index.html index.htm;
    # Nginx处理静态资源(建议开启gzip),LaravelS处理动态资源。
    location / {
        try_files $uri @laravels;
    }
    # 当请求PHP文件时直接响应404,防止暴露public/*.php
    #location ~* \.php$ {
    #    return 404;
    #}
    # Http和WebSocket共存,Nginx通过location区分
    # !!! WebSocket连接时路径为/ws
    # Javascript: var ws = new WebSocket("ws://laravels.com/ws");
    location =/ws {
        # proxy_connect_timeout 60s;
        # proxy_send_timeout 60s;
        # proxy_read_timeout:如果60秒内被代理的服务器没有响应数据给Nginx,那么Nginx会关闭当前连接;同时,Swoole的心跳设置也会影响连接的关闭
        # proxy_read_timeout 60s;
        proxy_http_version 1.1;
        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_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_pass http://swoole;
    }
    location @laravels {
        # proxy_connect_timeout 60s;
        # proxy_send_timeout 60s;
        # proxy_read_timeout 60s;
        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;
    }
}

5.心跳配置

  • Swoole的心跳配置

      // config/laravels.php
      'swoole' => [
          //...
          // 表示每60秒遍历一次,一个连接如果600秒内未向服务器发送任何数据,此连接将被强制关闭
          'heartbeat_idle_time'      => 600,
          'heartbeat_check_interval' => 60,
          //...
      ],
  • Nginx读取代理服务器超时的配置

      # 如果60秒内被代理的服务器没有响应数据给Nginx,那么Nginx会关闭当前连接
      proxy_read_timeout 60s;

6.在控制器中推送数据

namespace App\Http\Controllers;
class TestController extends Controller
{
    public function push()
    {
        $fd = 1; // Find fd by userId from a map [userId=>fd].
        /**@var \Swoole\WebSocket\Server $swoole */
        $swoole = app('swoole');
        $success = $swoole->push($fd, 'Push data to fd#1 in Controller');
        var_dump($success);
    }
}

监听事件

系统事件

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

  • 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
      });

自定义的异步事件

此特性依赖SwooleAsyncTask,必须先设置config/laravels.phpswoole.task_worker_num。异步事件的处理能力受Task进程数影响,需合理设置task_worker_num

1.创建事件类。

use Hhxsv5\LaravelS\Swoole\Task\Event;
class TestEvent extends Event
{
    protected $listeners = [
        // 监听器列表
        TestListener1::class,
        // TestListener2::class,
    ];
    private $data;
    public function __construct($data)
    {
        $this->data = $data;
    }
    public function getData()
    {
        return $this->data;
    }
}

2.创建监听器类。

use Hhxsv5\LaravelS\Swoole\Task\Task;
use Hhxsv5\LaravelS\Swoole\Task\Listener;
class TestListener1 extends Listener
{
    /**
     * @var TestEvent
     */
    protected $event;

    public function handle()
    {
        \Log::info(__CLASS__ . ':handle start', [$this->event->getData()]);
        sleep(2);// 模拟一些慢速的事件处理
        // 监听器中也可以投递Task,但不支持Task的finish()回调。
        // 注意:config/laravels.php中修改配置task_ipc_mode为1或2,参考 https://wiki.swoole.com/#/server/setting?id=task_ipc_mode
        $ret = Task::deliver(new TestTask('task data'));
        var_dump($ret);
        // 此处抛出的异常会被上层捕获并记录到Swoole日志,开发者需要手动try/catch
    }
}

3.触发事件。

// 实例化TestEvent并通过fire触发,此操作是异步的,触发后立即返回,由Task进程继续处理监听器中的handle逻辑
use Hhxsv5\LaravelS\Swoole\Task\Event;
$event = new TestEvent('event data');
// $event->delay(10); // 延迟10秒触发
// $event->setTries(3); // 出现异常时,累计尝试3次
$success = Event::fire($event);
var_dump($success);// 判断是否触发成功

异步的任务队列

此特性依赖SwooleAsyncTask,必须先设置config/laravels.phpswoole.task_worker_num。异步任务的处理能力受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);// 模拟一些慢速的事件处理
        // 此处抛出的异常会被上层捕获并记录到Swoole日志,开发者需要手动try/catch
        $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秒投递任务
// $task->setTries(3); // 出现异常时,累计尝试3次
$ret = Task::deliver($task);
var_dump($ret);// 判断是否投递成功

毫秒级定时任务

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

1.创建定时任务类。

namespace App\Jobs\Timer;
use App\Tasks\TestTask;
use Swoole\Coroutine;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Hhxsv5\LaravelS\Swoole\Timer\CronJob;
class TestCronJob extends CronJob
{
    protected $i = 0;
    // !!! 定时任务的`interval`和`isImmediate`有两种配置方式(二选一):一是重载对应的方法,二是注册定时任务时传入参数。
    // --- 重载对应的方法来返回配置:开始
    public function interval()
    {
        return 1000;// 每1秒运行一次
    }
    public function isImmediate()
    {
        return false;// 是否立即执行第一次,false则等待间隔时间后执行第一次
    }
    // --- 重载对应的方法来返回配置:结束
    public function run()
    {
        \Log::info(__METHOD__, ['start', $this->i, microtime(true)]);
        // do something
        // sleep(1); // Swoole < 2.1
        Coroutine::sleep(1); // Swoole>=2.1 run()方法已自动创建了协程。
        $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(); // 终止此定时任务,但restart/reload后会再次运行
            // CronJob中也可以投递Task,但不支持Task的finish()回调。
            // 注意:修改config/laravels.php,配置task_ipc_mode为1或2,参考 https://wiki.swoole.com/#/server/setting?id=task_ipc_mode
            $ret = Task::deliver(new TestTask('task data'));
            var_dump($ret);
        }
        // 此处抛出的异常会被上层捕获并记录到Swoole日志,开发者需要手动try/catch
    }
}

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, [1000, true]], // 注册时传入参数
            \App\Jobs\Timer\TestCronJob::class, // 重载对应的方法来返回参数
        ],
        'max_wait_time' => 5, // Reload时最大等待时间
        // 打开全局定时器开关:当多实例部署时,确保只有一个实例运行定时任务,此功能依赖 Redis,具体请看 https://learnku.com/docs/laravel/7.x/redis
        'global_lock'     => false,
        'global_lock_key' => config('app.name', 'Laravel'),
    ],
    // ...
];

3.注意在构建服务器集群时,会启动多个定时器,要确保只启动一个定期器,避免重复执行定时任务。

4.LaravelS v3.4.0开始支持热重启[Reload]定时器进程,LaravelS 在收到SIGUSR1信号后会等待max_wait_time(默认5)秒再结束进程,然后Manager进程会重新拉起定时器进程。

5.如果你仅需要用到分钟级的定时任务,建议启用Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob来替代Linux Crontab,这样就可以沿用Laravel任务调度的编码习惯,配置Kernel即可。

// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
    // runInBackground()方法会新启子进程执行任务,这是异步的,不会影响其他任务的执行时机
    $schedule->command(TestCommand::class)->runInBackground()->everyMinute();
}

修改代码后自动Reload

  • 基于inotify,仅支持Linux。

    1.安装inotify扩展。

    2.开启配置项

    3.注意:inotify只有在Linux内修改文件才能收到文件变更事件,建议使用最新版Docker,Vagrant解决方案

  • 基于fswatch,支持OS X、Linux、Windows。

    1.安装fswatch

    2.在项目根目录下运行命令。

      # 监听当前目录
      ./bin/fswatch
      # 监听app目录
      ./bin/fswatch ./app
  • 基于inotifywait,仅支持Linux。

    1.安装inotify-tools

    2.在项目根目录下运行命令。

      # 监听当前目录
      ./bin/inotify
      # 监听app目录
      ./bin/inotify ./app
  • 当以上方法都不行时,终极解决方案:配置max_request=1,worker_num=1,这样Worker进程处理完一个请求就会重启,这种方法的性能非常差,故仅限在开发环境使用

在你的项目中使用SwooleServer实例

/**
 * 如果启用WebSocket server,$swoole是`Swoole\WebSocket\Server`的实例,否则是是`Swoole\Http\Server`的实例
 * @var \Swoole\WebSocket\Server|\Swoole\Http\Server $swoole
 */
$swoole = app('swoole');
var_dump($swoole->stats());
$swoole->push($fd, 'Push WebSocket message');

使用SwooleTable

1.定义Table,支持定义多个Table。

Swoole启动之前会创建定义的所有Table。

// 在"config/laravels.php"配置
[
    // ...
    'swoole_tables'  => [
        // 场景:WebSocket中UserId与FD绑定
        'ws' => [// Key为Table名称,使用时会自动添加Table后缀,避免重名。这里定义名为wsTable的Table
            'size'   => 102400,//Table的最大行数
            'column' => [// Table的列定义
                ['name' => 'value', 'type' => \Swoole\Table::TYPE_INT, 'size' => 8],
            ],
        ],
        //...继续定义其他Table
    ],
    // ...
];

2.访问Table:所有的Table实例均绑定在SwooleServer上,通过app('swoole')->xxxTable访问。

namespace App\Services;
use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
class WebSocketService implements WebSocketHandlerInterface
{
    /**@var \Swoole\Table $wsTable */
    private $wsTable;
    public function __construct()
    {
        $this->wsTable = app('swoole')->wsTable;
    }
    // 场景:WebSocket中UserId与FD绑定
    public function onOpen(Server $server, Request $request)
    {
        // var_dump(app('swoole') === $server);// 同一实例
        /**
         * 获取当前登录的用户
         * 此特性要求建立WebSocket连接的路径要经过Authenticate之类的中间件。
         * 例如:
         * 浏览器端:var ws = new WebSocket("ws://127.0.0.1:5200/ws");
         * 那么Laravel中/ws路由就需要加上类似Authenticate的中间件。
         * Route::get('/ws', function () {
         *     // 响应状态码200的任意内容
         *     return 'websocket';
         * })->middleware(['auth']);
         */
        // $user = Auth::user();
        // $userId = $user ? $user->id : 0; // 0 表示未登录的访客用户
        $userId = mt_rand(1000, 10000);
        // if (!$userId) {
        //     // 未登录用户直接断开连接
        //     $server->disconnect($request->fd);
        //     return;
        // }
        $this->wsTable->set('uid:' . $userId, ['value' => $request->fd]);// 绑定uid到fd的映射
        $this->wsTable->set('fd:' . $request->fd, ['value' => $userId]);// 绑定fd到uid的映射
        $server->push($request->fd, "Welcome to LaravelS #{$request->fd}");
    }
    public function onMessage(Server $server, Frame $frame)
    {
        // 广播
        foreach ($this->wsTable as $key => $row) {
            if (strpos($key, 'uid:') === 0 && $server->isEstablished($row['value'])) {
                $content = sprintf('Broadcast: new message "%s" from #%d', $frame->data, $frame->fd);
                $server->push($row['value'], $content);
            }
        }
    }
    public function onClose(Server $server, $fd, $reactorId)
    {
        $uid = $this->wsTable->get('fd:' . $fd);
        if ($uid !== false) {
            $this->wsTable->del('uid:' . $uid['value']); // 解绑uid映射
        }
        $this->wsTable->del('fd:' . $fd);// 解绑fd映射
        $server->push($fd, "Goodbye #{$fd}");
    }
}

多端口混合协议

更多的信息,请参考Swoole增加监听的端口多端口混合协议

为了使我们的主服务器能支持除HTTPWebSocket外的更多协议,我们引入了Swoole多端口混合协议特性,在LaravelS中称为Socket。现在,可以很方便地在Laravel上构建TCP/UDP应用。

  1. 创建Socket处理类,继承Hhxsv5\LaravelS\Swoole\Socket\{TcpSocket|UdpSocket|Http|WebSocket}

     namespace App\Sockets;
     use Hhxsv5\LaravelS\Swoole\Socket\TcpSocket;
     use Swoole\Server;
     class TestTcpSocket extends TcpSocket
     {
         public function onConnect(Server $server, $fd, $reactorId)
         {
             \Log::info('New TCP connection', [$fd]);
             $server->send($fd, 'Welcome to LaravelS.');
         }
         public function onReceive(Server $server, $fd, $reactorId, $data)
         {
             \Log::info('Received data', [$fd, $data]);
             $server->send($fd, 'LaravelS: ' . $data);
             if ($data === "quit\r\n") {
                 $server->send($fd, 'LaravelS: bye' . PHP_EOL);
                 $server->close($fd);
             }
         }
         public function onClose(Server $server, $fd, $reactorId)
         {
             \Log::info('Close TCP connection', [$fd]);
             $server->send($fd, 'Goodbye');
         }
     }

    这些连接和主服务器上的HTTP/WebSocket连接共享Worker进程,因此可以在这些事件回调中使用LaravelS提供的异步任务投递SwooleTable、Laravel提供的组件如DBEloquent等。同时,如果需要使用该协议端口的Swoole\Server\Port对象,只需要像如下代码一样访问Socket类的成员swoolePort即可。

     public function onReceive(Server $server, $fd, $reactorId, $data)
     {
         $port = $this->swoolePort; // 获得`Swoole\Server\Port`对象
     }
     namespace App\Http\Controllers;
     class TestController extends Controller
     {
         public function test()
         {
             /**@var \Swoole\Http\Server|\Swoole\WebSocket\Server $swoole */
             $swoole = app('swoole');
             // $swoole->ports:遍历所有Port对象,https://wiki.swoole.com/#/server/properties?id=ports
             $port = $swoole->ports[1]; // 获得`Swoole\Server\Port`对象,$port[0]是主服务器的端口
             foreach ($port->connections as $fd) { // 遍历所有连接
                 // $swoole->send($fd, 'Send tcp message');
                 // if($swoole->isEstablished($fd)) {
                 //     $swoole->push($fd, 'Send websocket message');
                 // }
             }
         }
     }
  2. 注册套接字。

     // 修改文件 config/laravels.php
     // ...
     'sockets' => [
         [
             'host'     => '127.0.0.1',
             'port'     => 5291,
             'type'     => SWOOLE_SOCK_TCP,// 支持的嵌套字类型:https://wiki.swoole.com/#/consts?id=socket-%e7%b1%bb%e5%9e%8b
             'settings' => [// Swoole可用的配置项:https://wiki.swoole.com/#/server/port?id=%e5%8f%af%e9%80%89%e5%8f%82%e6%95%b0
                 'open_eof_check' => true,
                 'package_eof'    => "\r\n",
             ],
             'handler'  => \App\Sockets\TestTcpSocket::class,
             'enable'   => true, // 是否启用,默认为true
         ],
     ],

    关于心跳配置,只能设置在主服务器上,不能配置在套接字上,但套接字会继承主服务器的心跳配置。

    对于TCP协议,dispatch_mode选项设为1/3时,底层会屏蔽onConnect/onClose事件,原因是这两种模式下无法保证onConnect/onClose/onReceive的顺序。如果需要用到这两个事件,请将dispatch_mode改为2/4/5参考

     'swoole' => [
         //...
         'dispatch_mode' => 2,
         //...
     ];
  3. 测试。

  • TCP:telnet 127.0.0.1 5291

  • UDP:Linux下 echo "Hello LaravelS" > /dev/udp/127.0.0.1/5292

  1. 其他协议的注册示例。

    • UDP

      'sockets' => [
        [
            'host'     => '0.0.0.0',
            'port'     => 5292,
            'type'     => SWOOLE_SOCK_UDP,
            'settings' => [
                'open_eof_check' => true,
                'package_eof'    => "\r\n",
            ],
            'handler'  => \App\Sockets\TestUdpSocket::class,
        ],
      ],
    • Http

      'sockets' => [
        [
            'host'     => '0.0.0.0',
            'port'     => 5293,
            'type'     => SWOOLE_SOCK_TCP,
            'settings' => [
                'open_http_protocol' => true,
            ],
            'handler'  => \App\Sockets\TestHttp::class,
        ],
      ],
    • WebSocket:主服务器必须开启WebSocket,即需要将websocket.enable置为true

      'sockets' => [
        [
            'host'     => '0.0.0.0',
            'port'     => 5294,
            'type'     => SWOOLE_SOCK_TCP,
            'settings' => [
                'open_http_protocol'      => true,
                'open_websocket_protocol' => true,
            ],
            'handler'  => \App\Sockets\TestWebSocket::class,
        ],
      ],

协程

Swoole原始文档

  • 警告:协程下代码执行顺序是乱序的,请求级的数据应该以协程ID隔离,但Laravel/Lumen中存在很多单例、静态属性,不同请求间的数据会相互影响,这是不安全的。比如数据库连接就是单例,同一个数据库连接共享同一个PDO资源,这在同步阻塞模式下是没问题的,但在异步协程下是不行的,每次查询需要创建不同的连接,维护不同的IO状态,这就需要用到连接池。

  • 不要使用协程,仅自定义进程中可使用协程。

自定义进程

支持开发者创建一些特殊的工作进程,用于监控、上报或者其他特殊的任务,参考addProcess

  1. 创建Proccess类,实现CustomProcessInterface接口。

     namespace App\Processes;
     use App\Tasks\TestTask;
     use Hhxsv5\LaravelS\Swoole\Process\CustomProcessInterface;
     use Hhxsv5\LaravelS\Swoole\Task\Task;
     use Swoole\Coroutine;
     use Swoole\Http\Server;
     use Swoole\Process;
     class TestProcess implements CustomProcessInterface
     {
         /**
          * @var bool 退出标记,用于Reload更新
          */
         private static $quit = false;
    
         public static function callback(Server $swoole, Process $process)
         {
             // 进程运行的代码,不能退出,一旦退出Manager进程会自动再次创建该进程。
             while (!self::$quit) {
                 \Log::info('Test process: running');
                 // sleep(1); // Swoole < 2.1
                 Coroutine::sleep(1); // Swoole>=2.1 已自动为callback()方法创建了协程并启用了协程Runtime。
                 // 自定义进程中也可以投递Task,但不支持Task的finish()回调。
                 // 注意:修改config/laravels.php,配置task_ipc_mode为1或2,参考 https://wiki.swoole.com/#/server/setting?id=task_ipc_mode
                 $ret = Task::deliver(new TestTask('task data'));
                 var_dump($ret);
                 // 上层会捕获callback中抛出的异常,并记录到Swoole日志,然后此进程会退出,3秒后Manager进程会重新创建进程,所以需要开发者自行try/catch捕获异常,避免频繁创建进程。
                 // throw new \Exception('an exception');
             }
         }
         // 要求:LaravelS >= v3.4.0 并且 callback() 必须是异步非阻塞程序。
         public static function onReload(Server $swoole, Process $process)
         {
             // Stop the process...
             // Then end process
             \Log::info('Test process: reloading');
             self::$quit = true;
             // $process->exit(0); // 强制退出进程
         }
         // 要求:LaravelS >= v3.7.4 并且 callback() 必须是异步非阻塞程序。
         public static function onStop(Server $swoole, Process $process)
         {
             // Stop the process...
             // Then end process
             \Log::info('Test process: stopping');
             self::$quit = true;
             // $process->exit(0); // 强制退出进程
         }
     }
  2. 注册TestProcess。

     // 修改文件 config/laravels.php
     // ...
     'processes' => [
         'test' => [ // Key为进程名
             'class'    => \App\Processes\TestProcess::class,
             'redirect' => false, // 是否重定向输入输出
             'pipe'     => 0,     // 管道类型:0不创建管道,1创建SOCK_STREAM类型管道,2创建SOCK_DGRAM类型管道
             'enable'   => true,  // 是否启用,默认true
             //'num'    => 3,  // 创建多个进程实例,默认为1
             //'queue'    => [ // 启用消息队列作为进程间通信,配置空数组表示使用默认参数
             //    'msg_key'  => 0,    // 消息队列的KEY,默认会使用ftok(__FILE__, 1)
             //    'mode'     => 2,    // 通信模式,默认为2,表示争抢模式
             //    'capacity' => 8192, // 单个消息长度,长度受限于操作系统内核参数的限制,默认为8192,最大不超过65536
             //],
             //'restart_interval' => 5, // 进程异常退出后需等待多少秒再重启,默认5秒
         ],
     ],
  3. 注意:callback()方法不能退出,如果退出,Manager进程将会重新创建进程。

  4. 示例:向自定义进程中写数据。

     // config/laravels.php
     'processes' => [
         'test' => [
             'class'    => \App\Processes\TestProcess::class,
             'redirect' => false,
             'pipe'     => 1,
         ],
     ],
     // app/Processes/TestProcess.php
     public static function callback(Server $swoole, Process $process)
     {
         while ($data = $process->read()) {
             \Log::info('TestProcess: read data', [$data]);
             $process->write('TestProcess: ' . $data);
         }
     }
     // app/Http/Controllers/TestController.php
     public function testProcessWrite()
     {
         /**@var \Swoole\Process $process */
         $process = app('swoole')->customProcesses['test'];
         $process->write('TestController: write data' . time());
         var_dump($process->read());
     }

常用组件

Apollo

启动LaravelS时会获取Apollo配置并写入到.env文件,同时会启动自定义进程apollo用于监听配置变更,当配置发生变更时自动reload

  1. 启用Apollo组件:启动参数加上--enable-apollo以及Apollo的配置参数。

     php bin/laravels start --enable-apollo --apollo-server=http://127.0.0.1:8080 --apollo-app-id=LARAVEL-S-TEST
  2. 配置热更新(可选的)。

     // 修改文件 config/laravels.php
     'processes' => Hhxsv5\LaravelS\Components\Apollo\Process::getDefinition(),
     // 当存在其他自定义进程配置时
     'processes' => [
         'test' => [
             'class'    => \App\Processes\TestProcess::class,
             'redirect' => false,
             'pipe'     => 1,
         ],
         // ...
     ] + Hhxsv5\LaravelS\Components\Apollo\Process::getDefinition(),
  3. 可用的参数列表。

参数名 描述 默认值 示例
apollo-server Apollo服务器URL - –apollo-server=127.0.0.1:8080
apollo-app-id Apollo应用ID - –apollo-app-id=LARAVEL-S-TEST
apollo-namespaces APP所属的命名空间,可指定多个 application –apollo-namespaces=application –apollo-namespaces=env
apollo-cluster APP所属的集群 default –apollo-cluster=default
apollo-client-ip 当前实例的IP,还可用于灰度发布 本机内网IP –apollo-client-ip=10.2.1.83
apollo-pull-timeout 拉取配置时的超时时间(秒) 5 –apollo-pull-timeout=5
apollo-backup-old-env 更新配置文件.env时是否备份老的配置文件 false –apollo-backup-old-env

Prometheus

支持Prometheus监控与告警,Grafana可视化查看监控指标。请参考Docker Compose完成Prometheus与Grafana的环境搭建。

  1. 依赖APCu >= 5.0.0扩展,请先安装它 pecl install apcu

  2. 拷贝配置文件prometheus.php到你的工程config目录。视情况修改配置。

     # 项目根目录下执行命令
     cp vendor/hhxsv5/laravel-s/config/prometheus.php config/

    如果是Lumen工程,还需要在bootstrap/app.php中手动加载配置$app->configure('prometheus');

  3. 配置全局中间件:Hhxsv5\LaravelS\Components\Prometheus\RequestMiddleware::class。为了尽可能精确地统计请求耗时,RequestMiddleware必须作为第一个全局中间件,需要放在其他中间件的前面。

  4. 注册 ServiceProvider:Hhxsv5\LaravelS\Components\Prometheus\ServiceProvider::class

  5. config/laravels.php中配置 CollectorProcess 进程,用于定时采集 Swoole Worker/Task/Timer 进程的指标。

     'processes' => Hhxsv5\LaravelS\Components\Prometheus\CollectorProcess::getDefinition(),
  6. 创建路由,输出监控指标数据。

     use Hhxsv5\LaravelS\Components\Prometheus\Exporter;
    
     Route::get('/actuator/prometheus', function () {
         $result = app(Exporter::class)->render();
         return response($result, 200, ['Content-Type' => Exporter::REDNER_MIME_TYPE]);
     });
  7. 完成Prometheus的配置,启动Prometheus。

     global:
       scrape_interval: 5s
       scrape_timeout: 5s
       evaluation_interval: 30s
     scrape_configs:
     - job_name: laravel-s-test
       honor_timestamps: true
       metrics_path: /actuator/prometheus
       scheme: http
       follow_redirects: true
       static_configs:
       - targets:
         - 127.0.0.1:5200 # The ip and port of the monitored service
     # Dynamically discovered using one of the supported service-discovery mechanisms
     # https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config
     # - job_name: laravels-eureka
     #   honor_timestamps: true
     #   scrape_interval: 5s
     #   metrics_path: /actuator/prometheus
     #   scheme: http
     #   follow_redirects: true
       # eureka_sd_configs:
       # - server: http://127.0.0.1:8080/eureka
       #   follow_redirects: true
       #   refresh_interval: 5s
  8. 启动Grafana,然后导入panel json

Grafana Dashboard

其他特性

配置Swoole事件

支持的事件列表:

事件 需实现的接口 发生时机
ServerStart Hhxsv5\LaravelS\Swoole\Events\ServerStartInterface 发生在Master进程启动时,此事件中不应处理复杂的业务逻辑,只能做一些初始化的简单工作
ServerStop Hhxsv5\LaravelS\Swoole\Events\ServerStopInterface 发生在Server正常退出时,此事件中不能使用异步或协程相关的API
WorkerStart Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface 发生在Worker/Task进程启动完成后
WorkerStop Hhxsv5\LaravelS\Swoole\Events\WorkerStopInterface 发生在Worker/Task进程正常退出后
WorkerError Hhxsv5\LaravelS\Swoole\Events\WorkerErrorInterface 发生在Worker/Task进程发生异常或致命错误时

1.创建事件处理类,实现相应的接口。

namespace App\Events;
use Hhxsv5\LaravelS\Swoole\Events\ServerStartInterface;
use Swoole\Atomic;
use Swoole\Http\Server;
class ServerStartEvent implements ServerStartInterface
{
    public function __construct()
    {
    }
    public function handle(Server $server)
    {
        // 初始化一个全局计数器(跨进程的可用)
        $server->atomicCount = new Atomic(2233);

        // 控制器中调用:app('swoole')->atomicCount->get();
    }
}
namespace App\Events;
use Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface;
use Swoole\Http\Server;
class WorkerStartEvent implements WorkerStartInterface
{
    public function __construct()
    {
    }
    public function handle(Server $server, $workerId)
    {
        // 初始化一个数据库连接池对象
        // DatabaseConnectionPool::init();
    }
}

2.配置。

// 修改文件 config/laravels.php
'event_handlers' => [
    'ServerStart' => [\App\Events\ServerStartEvent::class], // 按数组顺序触发事件
    'WorkerStart' => [\App\Events\WorkerStartEvent::class],
],

Serverless

阿里云函数计算

函数计算官方文档

1.修改bootstrap/app.php,设置storage目录。因为项目目录只读,/tmp目录才可读写。

$app->useStoragePath(env('APP_STORAGE_PATH', '/tmp/storage'));

2.创建Shell脚本laravels_bootstrap,并赋予可执行权限

#!/usr/bin/env bash
set +e

# 创建storage相关目录
mkdir -p /tmp/storage/app/public
mkdir -p /tmp/storage/framework/cache
mkdir -p /tmp/storage/framework/sessions
mkdir -p /tmp/storage/framework/testing
mkdir -p /tmp/storage/framework/views
mkdir -p /tmp/storage/logs

# 设置环境变量APP_STORAGE_PATH,请确保与.env的APP_STORAGE_PATH一样
export APP_STORAGE_PATH=/tmp/storage

# Start LaravelS
php bin/laravels start

3.配置template.xml

ROSTemplateFormatVersion: '2015-09-01'
Transform: 'Aliyun::Serverless-2018-04-03'
Resources:
  laravel-s-demo:
    Type: 'Aliyun::Serverless::Service'
    Properties:
      Description: 'LaravelS Demo for Serverless'
    fc-laravel-s:
      Type: 'Aliyun::Serverless::Function'
      Properties:
        Handler: laravels.handler
        Runtime: custom
        MemorySize: 512
        Timeout: 30
        CodeUri: ./
        InstanceConcurrency: 10
        EnvironmentVariables:
          BOOTSTRAP_FILE: laravels_bootstrap

注意事项

单例问题

  • 传统FPM下,单例模式的对象的生命周期仅在每次请求中,请求开始=>实例化单例=>请求结束后=>单例对象资源回收。

  • Swoole Server下,所有单例对象会常驻于内存,这个时候单例对象的生命周期与FPM不同,请求开始=>实例化单例=>请求结束=>单例对象依旧保留,需要开发者自己维护单例的状态。

  • 常见的解决方案:

    1. 写一个XxxCleaner清理器类来清理单例对象状态,此类需实现接口Hhxsv5\LaravelS\Illuminate\Cleaners\CleanerInterface,然后注册到laravels.phpcleaners中。

    2. 用一个中间件重置单例对象的状态。

    3. 如果是以ServiceProvider注册的单例对象,可添加该ServiceProviderlaravels.phpregister_providers中,这样每次请求会重新注册该ServiceProvider,重新实例化单例对象,参考

清理器

设置清理器

常见问题

常见问题:一揽子的已知问题和解决方案。

调试方式

  • 记录日志;如想要在控制台输出,可使用stderr,Log::channel(‘stderr’)->debug(‘debug message’)。

  • Laravel Dump Server(Laravel 5.7已默认集成)。

读取请求

应通过Illuminate\Http\Request对象来读取请求信息,$_ENV是可读取的,$_SERVER是部分可读的,不能使用$_GET、$_POST、$_FILES、$_COOKIE、$_REQUEST、$_SESSION、$GLOBALS。

public function form(\Illuminate\Http\Request $request)
{
    $name = $request->input('name');
    $all = $request->all();
    $sessionId = $request->cookie('sessionId');
    $photo = $request->file('photo');
    // 调用getContent()来获取原始的POST body,而不能用file_get_contents('php://input')
    $rawContent = $request->getContent();
    //...
}

输出响应

推荐通过返回Illuminate\Http\Response对象来响应请求,兼容echo、vardump()、print_r(),不能使用函数 dd()、exit()、die()、header()、setcookie()、http_response_code()。

public function json()
{
    return response()->json(['time' => time()])->header('header1', 'value1')->withCookie('c1', 'v1');
}

持久连接

单例的连接将被常驻内存,建议开启持久连接,获得更好的性能。

  1. 数据库连接,连接断开后会自动重连
// config/database.php
'connections' => [
    'my_conn' => [
        'driver'    => 'mysql',
        'host'      => env('DB_MY_CONN_HOST', 'localhost'),
        'port'      => env('DB_MY_CONN_PORT', 3306),
        'database'  => env('DB_MY_CONN_DATABASE', 'forge'),
        'username'  => env('DB_MY_CONN_USERNAME', 'forge'),
        'password'  => env('DB_MY_CONN_PASSWORD', ''),
        'charset'   => 'utf8mb4',
        'collation' => 'utf8mb4_unicode_ci',
        'prefix'    => '',
        'strict'    => false,
        'options'   => [
            // 开启持久连接
            \PDO::ATTR_PERSISTENT => true,
        ],
    ],
],
  1. Redis连接,连接断开后不会立即自动重连,会抛出一个关于连接断开的异常,下次会自动重连。需确保每次操作Redis前正确的SELECT DB
// config/database.php
'redis' => [
    'client' => env('REDIS_CLIENT', 'phpredis'), // 推荐使用phpredis,以获得更好的性能
    'default' => [
        'host'       => env('REDIS_HOST', 'localhost'),
        'password'   => env('REDIS_PASSWORD', null),
        'port'       => env('REDIS_PORT', 6379),
        'database'   => 0,
        'persistent' => true, // 开启持久连接
    ],
],

关于内存泄露

  • 避免使用全局变量,如一定要,请手动清理或重置。

  • 无限追加元素到全局变量、静态变量、单例,将导致内存溢出。

      class Test
      {
          public static $array = [];
          public static $string = '';
      }
    
      // 某控制器
      public function test(Request $req)
      {
          // 内存溢出
          Test::$array[] = $req->input('param1');
          Test::$string .= $req->input('param2');
      }
  • 内存泄露的检测方法

    1. 修改config/laravels.phpworker_num=1, max_request=1000000,测试完成后记得改回去;

    2. 增加路由/debug-memory-leak,不设置任何路由中间件,用于观察Worker进程的内存变化情况;

      Route::get('/debug-memory-leak', function () {
       global $previous;
       $current = memory_get_usage();
       $stats = [
           'prev_mem' => $previous,
           'curr_mem' => $current,
           'diff_mem' => $current - $previous,
       ];
       $previous = $current;
       return $stats;
      });
    3. 启动LaravelS,请求/debug-memory-leak,直到diff_mem小于或等于零;如果diff_mem一直大于零,说明全局中间件Laravel框架可能存在内存泄露;

    4. 完成步骤3后,交替请求业务路由与/debug-memory-leak(建议使用ab/wrk对业务路由进行大量的请求),刚开始出现的内存增涨是正常现象。业务路由经过大量请求后,如果diff_mem一直大于零,并且curr_mem持续增大,则大概率存在内存泄露;如果curr_mem始终在一定范围内变化,没有持续变大,则大概率不存在内存泄露。

    5. 如果始终没法解决,max_request是最后兜底的方案。

Linux内核参数调整

Linux内核参数调整

压力测试

压力测试

用户与案例

  • 特别赞助商 果酱社区:果酱社区是一个专业的会员制社区,这里提供高质量的技术资料,
    专注程序员成长,创业孵化,提供优秀且稳定的开源产品。

    果酱社区
  • Pool-X:可以交易锁定资产的新一代POS矿池。

  • KuCoin:全球化第一的交易所。

  • 医联:WEB站、M站、APP、小程序的账户体系服务。

    医联
  • ITOK在线客服平台:用户IT工单的处理跟踪及在线实时沟通。

    ITOK在线客服平台
  • 盟呱呱

    盟呱呱
  • WookTeam:WookTeam是一款轻量级的在线团队协作工具,提供各类文档工具、在线思维导图、在线流程图、项目管理、任务分发,知识库管理等工具。

  • 微信公众号-广州塔:活动、商城

    广州塔
  • 企鹅游戏盒子、明星新势力、以及小程序广告服务

    企鹅游戏盒子
  • 小程序-修机匠手机上门维修服务:手机维修服务,提供上门服务,支持在线维修。

    修机匠手机上门维修服务
  • 亿健APP

Github: LaravelS 欢迎 Star + Fork

本作品采用《CC 协议》,转载必须注明作者和本文链接
hhxsv5
本帖由 Summer 于 6年前 加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 76

昨天Swoole发布2.1,提供了全新的短名 API,完整支持了协程(Coroutine)+通道(Channel)特性,为 PHP 语言带来了全新的编程模式。???Swoole 2.1
LaravelS后续会增加对2.1+的自动协程支持,提高并发。

6年前 评论

@lazyou 加速Laravel跟Apache没关系,主要依赖于两点

  1. 常驻内存后每个请求减少了一大堆的初始化开销:加载PHP文件、初始化Laravel框架、初始化数据库连接等资源。即比如完成一个请求需要走10步(1到10),现在只需走4步(7-10)。
  2. Swoole由C/C++编写,内置的HTTP Server性能高于Nginx+FPM
6年前 评论

laravel/passport 不同access_token认证的request->user()返回的都是一样的用户为啥?

5年前 评论

@Nixus 容器的启动命令必须是前台阻塞运行的,所以不能加-d-d表示以daemon守护进程化运行。

4年前 评论
水底沉星 4年前

@腊梅绽放 支持,正式的环境还是慎用,会和很多插件有冲突,自己玩无所谓!

4年前 评论

@GhostCoder 写绝对路径。

$file = $request->file('file1');
$file->move(storage_path());
5年前 评论
阿麦

@好好先森V5 嗯 感谢思路

5年前 评论
阿麦

@好好先森V5 嗯 感谢思路

5年前 评论

@好好先森V5

卡在了nginx配置文件 ;
能不能给一个通用的实例(我是在homestead中用的)

5年前 评论

@好好先森V5

卡在了nginx配置文件 ;
能不能给一个通用的实例(我是在homestead中用的)

5年前 评论
阿麦

@好好先森V5 嗯,就是用了反向代理图片上传到 /storage/ 里去了而不是 /public/storage/ 里,因为我代码里写的是上传到 storage/ 下因为 nginx 设置了权限目录为 public 所以不用代理的时候是好的,估计用了代理权限目录就到根目录了?

5年前 评论

@babyCatSwimming 加QQ群 698480528 细聊

5年前 评论

大佬,为什么获取不到微信支付回调的数据啊. $request->all()没有数据, file_get_content('php://input')也没有数据

5年前 评论

Swoole\Server::task(): task method can't be executed without task worker 参照项目:异步的任务队列 报错如上所示:排查过程执行过

  • php artisan cache:clear
  • 设置过: task_worker_num

都没起作用,知道配置修改后要重启;目前没看到效果

当前配置如图所示:请教大佬

file

4个月前 评论

@xxxao 获取原始php请求body '$request->getContent()',不能读取php://input

5年前 评论

@GhostCoder 没有遇到,具体什么错误信息

5年前 评论

可以,可以,学习了

5年前 评论

项目中使用实例 报Class swoole does not exist 是怎么回事 我用的是websocket 已经启动了 测试连接也是成功的

5年前 评论
5年前 评论
kingThegirl 4年前

好东西,稳定运行半年以上。没一点问题

4年前 评论

在使用docker跑laravels的时候,总是执行完后docker容器就退出了,比如:

 docker run -it -d -v ~/html:/var/www/html d536bc773362 sh -c "php /var/www/html/PPOS/bin/laravels start -d"

执行docker logs 容器名,得到下图的信息:
file
laravels是正常执行了,但是好像是没有长驻后台的,随着sh的退出,也退出了……
之后又尝试了

docker run -it -d -v ~/html:/var/www/html d536bc773362 php /var/www/html/PPOS/bin/laravels start

也是一样的情况,docker logs输出的日志也是一样的:
file
不知道该怎样实现laravels进程长驻,如果通过tail -f /dev/null来实现的话,那就没办法知laravels的状态,也就没办法通过supervisor来监控和启动laravels所在的容器了,这样可能会是什么问题?要怎样解决?

4年前 评论
zxdstyle 4年前

不知道laravel6.0 支持这个扩展吗

4年前 评论

多个项目nginx怎么配置?端口可以不同项目设置不同的端口吗?然后nginx配置upstream swoole不同端口吗?

3年前 评论
hhxsv5 (楼主) 1年前

你好 @好好先森V5 !

  1. 问一下laravelS长时间不访问然后一刷新就特别慢是那种情况?
  2. 还有一个是像这种路由 127.0.0.1:8001/static/home/live2d_a... (?前面是index.php给省略了) 就直接404或者直接返回PHP代码,将php代码理解为了静态文件应该怎么做!我百度了没有结果请大神解答一下
3年前 评论
hhxsv5 (楼主) 1年前

@好好先森V5 请问:刷新页面断开ws重连一开始响应会变慢,过段时间才正常是啥原因啊

2年前 评论
hhxsv5 (楼主) 1年前
游离不2

laravel 和 lumen 在 laravel-s 下性能差距大吗?@

1年前 评论
hhxsv5 (楼主) 1年前

mark, click zan !

1年前 评论

apache 呢? 还有原理是啥?

6年前 评论

@就好比 apache和swoole的worker数量调成一样测试。swoole的backlog、reactor_num 可以调优下。

6年前 评论

api路由里面使用auth
Route::group(['middleware'=>['auth:api']], function(){

6年前 评论

api认证有问题
"message": "Auth guard driver [api] is not defined.",

6年前 评论

@等车的猪 如果编码习惯好的话,几乎是无缝切换。需要注意全局变量、静态变量、单例。

6年前 评论

大佬 支持无缝切换laravel吗,需要对程序做哪些修改吗

6年前 评论

@z88089 是Mac的cpu占用,不是容器的

6年前 评论

@就好比 worker数不是越多越好,参考worker_num,与apache对比测试,swoole应设置成一样的worker数。 swoole和apache运行时sapi不同,php.ini 也可能不同,确保都开启了opcache。
其他注意事项:Swoole压力测试

6年前 评论

@好好先森V5 :joy: 还是不行,我把work_num 开到100 还是么有本机的apache并发高= = 姿势不太对吧 可能

6年前 评论

@甄亦贾 发一个能重现问题的精简包到hhxsv5@sina.com

6年前 评论

是我姿势不对么,为什么我加上swoole 只有单个请求速度变快了 但是并发还是没有apache的高

6年前 评论

建个群吧,一起学习

6年前 评论

老铁顶一个

6年前 评论

@yueshang 文末加了简单的压测。

6年前 评论

大佬 使用 扩展之后 能提升多少呢

6年前 评论
阿麦

大佬你有遇到过 配置了 nginx 反向代理导致 图片上传功能失败的情况么

5年前 评论

@好好先森V5 就是启用 laravel passport
然后config/auth.php
'api' => [
'driver' => 'passport',
'provider' => 'users',
],

然后routes/api,php
Route::group(['middleware'=>['auth:api']], function(){
Route::get('/', 'InfoController@index')->name('user.info');
});

6年前 评论

@好好先森V5
我追踪到源码 得到的路由组是web不是api

6年前 评论

@甄亦贾 谢谢支持,已修复。

6年前 评论

目前看过的,最好用的laravel 使用swoole 方法,简单,不需要修改文件, 效果明显大增 ab测试 qps从2.6变成126

6年前 评论
Aaron

刚才我也在开发服务器,做了个测试, qps 显著提升,很棒。但还是不敢部署在生成环境上,期待之后的表现。大赞

6年前 评论

交流QQ群:698480528

6年前 评论
绝缘体菜狗

哪个大哥如果在线上环境用,结果咋样,回来回复一下啊,谢谢。

6年前 评论

@一穷二白 我司Passport服务和App接口层已上线使用。
新项目按照文档来写,一般没有啥坑
老项目得多测试,注意全局变量、静态变量的使用。

6年前 评论
黑将军

我在homestead上使用laravelS测试,可以快10倍左右,不过涉及到ob_start ob_flush之类的操作就会有问题了 :joy:

6年前 评论

@黑将军 因为底层考虑到要兼容var_dump(), echo, print_r()等等输出函数,就使用了ob缓存。如果你在业务中使用了这个可能会冲突,提个issue。

6年前 评论

新人请教,现在还用不到websocket这些,单纯的做WEB应用,开发还是laravel那套吗?性能是否也一样可以提升?

5年前 评论

开发效率 + 性能 赞

5年前 评论

@大师兄 常驻内存后,性能都会有较大的提升。

5年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!