Swoole & Laravel Octane

實戰工作坊

Albert Chen

@LaravelVueConf Taiwan 2022

Information

source code

slides

SSID: LaravelVueConfTaiwan 2022

Password: lovelaravelvue

installation

Slido

What is Swoole?

  • Since 2012

  • Open source based on Apache-2.0 License

  • 17.6k stars on Dec. 2022

  • More than 130 contributors

  • Adopted by many enterprises in China

  • Laravel Octane was released in 2021

What is Swoole?

  • Not a new language, but an extension for PHP

  • Provides many new features for traditional PHP

  • New lifecycle in Swoole

  • Server patterns, TCP, UDP, Coroutine, Async I/O, Process Management, etc.

  • It's still under active development

  • Stable for production environment

  • High performance

Swoole Versions

  • v4.4.x: LTS version

  • v4.8.x: Stable version with new features

  • Followed PHP's official supported schedule

Branch Initial Release Active Support Until Security Support Until
7.4 28 Nov 2019 28 Nov 2021 28 Nov 2022
8.0 26 Nov 2020 26 Nov 2022 26 Nov 2023
8.1 25 Nov 2021 25 Nov 2023 25 Nov 2024
(https://www.php.net/supported-versions.php)

Installations

  • Supported on Linux、FreeBSD、MacOS

  • Windows needs CygWin or WSL

  • Requeirements for compliation:

    • GCC >= 4.8

    • 4.8: >= PHP 7.2

    • 5.0: >= PHP 8

Installations

  • Compile manually

mkdir -p ~/build && \
cd ~/build && \
rm -rf ./swoole-src && \
curl -o ./tmp/swoole.tar.gz https://github.com/swoole/swoole-src/archive/master.tar.gz -L && \
tar zxvf ./tmp/swoole.tar.gz && \
mv swoole-src* swoole-src && \
cd swoole-src && \
phpize && \
./configure \
--enable-openssl \
--enable-http2 && \
make && sudo make install

Installations

  • Via pecl

pecl install swoole
pecl install -D 'enable-openssl="yes" enable-http2="yes" swoole
extension=swoole.so
  • Append extension to php.ini

Installations

  • Via docker

docker run --rm phpswoole/swoole "php --ri swoole"
swoole

Swoole => enabled
Author => Swoole Team <team@swoole.com>
Version => 4.8.10
Built => May 19 2022 04:26:27
coroutine => enabled with boost asm context
kqueue => enabled
rwlock => enabled
pcre => enabled
zlib => 1.2.11
brotli => E16777225/D16777225
async_redis => enabled

Directive => Local Value => Master Value
swoole.enable_coroutine => On => On
swoole.enable_library => On => On
swoole.enable_preemptive_scheduler => Off => Off
swoole.display_errors => On => On
swoole.use_shortname => On => On
swoole.unixsock_buffer_size => 262144 => 262144

Installations

  • swoole-cli

    • a standalone binary (like NodeJS)

    • integrated with PHP and swoole

    • if other extensions are needed, you need to compile it manually

    • php.ini is not applied by default (with -d or -c)

wget https://wenda-1252906962.file.myqcloud.com/dist/swoole-cli-v5.0.1-macos-x64.tar.xz
tar zxvf swoole-cli-v5.0.1-macos-x64.tar.xz
mv swoole-cli /usr/local/bin/composer
swoole-cli --ri swoole
(https://mp.weixin.qq.com/s/y1tvbEzNuSbSc3mID0Zv5A)

Stateless PHP

  • How do we serve PHP today?

    • ​PHP-FPM

    • mod_php for Apache

  • Both patterns are all stateless

Stateless PHP

  • Pros

    • Easy scaling

    • Simple, less risk causing memory leaks

  • Cons

    • States can't be shared between requests

    • States must rely on external storages

    • Resources can't be reused efficiently

    • Connection cost is expensive (like database)

    • Not good for high performance

PHP FPM

  • FPM (FastCGI Process Manager)

PHP FPM

PHP Architecture

  • SAPI: Adapters like PHP-Cli, PHP-Fpm or embeded

  • ZendVM: Composed with Compiler and Executor, like JVM in Java

  • Extension: Including PHP Extension and Zend Extension

PHP Lifecycle

PHP Lifecycle

Swoole Server

Swoole Server

  • Process Mode

    • ​Master process to manage connections

    • Connection won't be disconnected if worker process failed

  • Base Mode

    • Better performance

    • Other connections will be effected if worker process failed

Swoole Server

  • Communication Between Processes

    • Master <==> Worker

    • Worker <==> Worker

    • Worker <==> Task Worker

  • Server Protocols

    • ​TCP

      • HTTP

      • Websocket

    • UDP

    • UnixSocket

Swoole Server

  • Callbacks for HTTP Server

    • ​onStart

    • onManagerStart

    • onWorkerStart

    • onRequest

    • onBeforeShutdown (SISGTERM 15)

HTTP Server

HTTP Server

  • Swoole

<?php

$server = new Swoole\HTTP\Server('0.0.0.0', 9501);

$server->on('request', function ($request, $response) {
    $response->end('Hello Swoole!');
});

echo "Server is starting...\n";
$server->start();

HTTP Server

package main

import (
    "fmt"
    "io"
    "net/http"
)

func main() {
    fmt.Println("Server is starting...")
    http.HandleFunc("/", handle)
    http.ListenAndServe("0.0.0.0:9501", nil)
}

func handle(rw http.ResponseWriter, r *http.Request) {
    rw.Header().Set("Content-Type", "text/html")
    io.WriteString(rw, "Hello Go!")
}
  • Go

var http = require('http');

http.createServer(function (request, response) {

response.writeHead(200, {'Content-Type': 'text/plain'});
    response.end('Hello NodeJS!');
}).listen(9501);

console.log('Server is starting...');
  • NodeJS

HTTP Server

  • Watch Processes

pstree | grep http.php

pstree | grep php
 |   |   \-+= 70392 Albert php http.php
 |   |     \-+- 70393 Albert php http.php
 |   |       |--- 70394 Albert php http.php
 |   |       |--- 70395 Albert php http.php
 |   |       |--- 70396 Albert php http.php
 |   |       |--- 70397 Albert php http.php
 |   |       |--- 70398 Albert php http.php
 |   |       |--- 70399 Albert php http.php
 |   |       |--- 70400 Albert php http.php
 |   |       \--- 70401 Albert php http.php

Master

Manager

Workers

HTTP Server

  • Watch Threads

ps M 70392

Albert 70392 s006    0.0 S    31T   0:00.02   0:00.06 php http.php
       70392         0.0 S    31T   0:00.00   0:00.00
       70392         0.0 S    31T   0:00.00   0:00.00
       70392         0.0 S    31T   0:00.00   0:00.00
       70392         0.0 S    31T   0:00.00   0:00.00

Master

Threads

HTTP Server

TCP Server

  • Callbacks for TCP Server

    • ​onConnect

    • onReceive

    • onClose

Swoole Server

  • TCP Server

<?php

$server = new Swoole\Server('0.0.0.0', 9501, SWOOLE_PROCESS, SWOOLE_SOCK_TCP);

$server->on('connect', function ($server, $fd){
    echo "Client {$fd} connected.\n";
});

$server->on('receive', function ($server, $fd, $reactorId, $data) {
    if (trim($data) === 'exit') {
        $server->close($fd);
        return;
    }
    $server->send($fd, "Swoole Response: {$data}");
});

$server->on('close', function ($server, $fd) {
    echo "Client {$fd} closed.\n";
});

echo "Server is starting...\n";
$server->start();
  • server->getClientInfo()
array(10) {
  ["server_port"]=>
  int(9501)
  ["server_fd"]=>
  int(4)
  ["socket_fd"]=>
  int(18)
  ["socket_type"]=>
  int(1)
  ["remote_port"]=>
  int(58485)
  ["remote_ip"]=>
  string(9) "127.0.0.1"
  ["reactor_id"]=>
  int(2)
  ["connect_time"]=>
  int(1567943830)
  ["last_time"]=>
  int(1567943830)
  ["close_errno"]=>
  int(0)
}

TCP Server

  • TCP Handshake

TCP Server

$server->set([
    'heartbeat_check_interval' => 5,
    'heartbeat_idle_time' => 10
]);
  • Heartbeat

    • Disconnect idle connections

    • Server and clients need to implement their ping-pong policy

TCP Server

  • Exercise

    • Implement a chatroom server based on TCP server

    • Support multi-users chat on the same channel

Swoole\Server->getClientList(int $start_fd = 0, int $pageSize = 10): bool|array

TCP Server

Websocket Server

  • Websocket Protocol

Websocket Server

  • Request

GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13
  • Response

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Location: ws://example.com/

Websocket Server

  • Websocket Server

<?php

$server = new Swoole\WebSocket\Server('0.0.0.0', 9501);

$server->on('open', function (Swoole\Websocket\Server $server, Swoole\Http\Request $request) {
    echo "handshake succeeded with fd {$request->fd}\n";
});

$server->on('message', function (Swoole\Websocket\Server $server, Swoole\Websocket\Frame $frame) {
    echo "received from {$frame->fd}:{$frame->data}, opcode:{$frame->opcode}, fin:{$frame->finish}\n";
    $server->push($frame->fd, $frame->data);
});

$server->on('close', function (Swoole\Websocket\Server $server, $fd) {
    echo "client {$fd} closed\n";
});

echo "Server is starting...\n";
$server->start();

Websocket Server

  • Online Websocket Client

    • https://websocketking.com

    • http://coolaf.com/tool/chattest

Task Worker

  • Task Wokers

    • Handle jobs from workers

    • Workers are dispatched through unix socket, no I/O consuming

    • Simple message queue

Task Worker

  • Task Woker

<?php

$server->set([
    'worker_num' => 2,
    'task_worker_num' => 4
]);

$server->on('task', function (Swoole\Server $server, $taskId, $fromId, $data) {
    // ...
});

$server->on('finish', function (Swoole\Server $server, $taskId, $data) {
    // ...
});

Process Communication

  • What's the count?

<?php

$server = new Swoole\HTTP\Server('0.0.0.0', 9501);

$count = 0;
$server->on('request', function ($request, $response) use (&$count) {
    $response->end($count++);
});

echo "Server is starting...\n";
$server->start();
  • Each worker has its own memory space

Process Communication

  • Use Swoole Table for Sharing Data

Process Communication

  • Atomic

Process Communication

<?php

$server = new Swoole\Http\Server('127.0.0.1', 9501);

$atomic = new Swoole\Atomic(1);

$server->on('request', function ($request, $response) use ($atomic) {
    $response->end($atomic->add(1));
});

echo "Server is starting...\n";
$server->start();
  • Swoole Table

    • Based on shared memory (hash table)

    • Data can be shared within processes

    • Row lock instead of global lock while concurrent access to the same row

    • Extremely high performance

    • Table size can't be extended once allocated

Process Communication

  • Swoole Table

    • ​Initialize table

    • Configure column types

    • Create table

Process Communication

$table = new \Swoole\Table(1024, 0.2);

$table->column('id', \Swoole\Table::TYPE_INT, 8);
$table->column('name', \Swoole\Table::TYPE_STRING, 32);

$table->create();
  • Swoole Table

Process Communication

$table->set('a', ['id' => 1, 'name' => 'foo']);
$table->set('b', ['id' => 2, 'name' => 'bar']);

$table->get('a', 'name');

$table->exist('a');

foreach ($table as $key => $value) {
    $table->del($key);
}
Swoole\Timer::tick(1000, function() {
    echo "hello\n";
});
  • Exercise

Process Communication

interface CacheInterface
{
    public function __construct(array $options = []);

    public function getCount(): int;

    public function put(string $key, string $value, int $ttl = 60): void;

    public function get(string $key): ?string;

    public function forget(string $key): void;

    public function flush(): void;

    public function recycle(): void;
}
  • Inter-Process Lock

    • Lock shared in processes

Process Communication

$lock = new Swoole\Lock(SWOOLE_MUTEX);

// blocking
$lock->lock();

// non-blocking
$lock->trylock();

$lock->unlock();
  • Simplified Matrix of Linux I/O Models

I/O Models in Linux

I/O Models in Linux

Blocking I/O in PHP

  • PHP is originally created as glue layer to call C functions
  • The I/O model is blocking by default
  • Almost all client-side libraries involving I/O are blocking
  • Multiple process for handling blocking I/O
  • I/O bound concurrency depends on process number
  • Cost for context switch in processes is expensive

Performance Problem

  • Resource can't be reused

  • I/O requests are blocking

  • 80% time cost on blocking I/O

Execute PHP Code

Initialize PHP Code

Database Request

HTTP API Request

Response

Include PHP Files

Concurrency Problem

  • 100 requests at the same time need 100 processes

  • 100 connections will be established as well

  • Scale to increase concurrency

Concurrency Models

Coroutine

  • Coroutine

    • Has ability to yield and resume
    • Not controlled by system kernel. It's controlled by process instead.
    • No context switching between processes
    • Can be considered as lightweight threads

According to Donald Knuth, the term coroutine was coined by Melvin Conway in 1958, after he applied it to construction of an assembly program.The first published explanation of the coroutine appeared later, in 1963. - wiki

Coroutine

  • Coroutine in Swoole

Coroutine

  • Coroutine in Swoole

run(function() {
    go(function () {
        Coroutine::sleep(2);
        echo "Hello world!\n";
    });

    go(function () {
        Coroutine::sleep(1);
        echo "Swoole is awesome!\n";
    });

    echo "PHP is the best!\n";
});

echo "Coroutine to the moon~\n";

Coroutine

  • Coroutine in Swoole

Runtime::enableCoroutine(false);

run(function() {
    go(function () {
        sleep(2);
        echo "Hello world!\n";
    });

    go(function () {
        sleep(1);
        echo "Swoole is awesome!\n";
    });

    echo "PHP is the best!\n";
});

echo "Coroutine to the moon~\n";

Coroutine

  • Variable Pollution

    • ​Coroutines in Swoole are implemented as single thread (1:N Model)
    • Variables in coroutines are shared, not isolated
run(function() {
    $variable = null;
    $cid = go(function () {
        global $variable;
        $variable = 'a';
        Coroutine::yield();
        echo $variable . "\n";
    });
    go(function () use ($cid) {
        global $variable;
        $variable = 'b';
        Coroutine::resume($cid);
        echo $variable . "\n";
    });
});

Coroutine

  • Variable Pollution

    • ​Variables need to be managed by coroutine context
run(function() {
    $cid = go(function () {
        $context = Coroutine::getContext();
        $context['a'] = 'b';
        Coroutine::yield();
        var_dump($context);
    });

    go(function () use ($cid) {
        $context = Coroutine::getContext();
        $context['a'] = 'c';
        Coroutine::resume($cid);
        var_dump($context);
    });
});

Coroutine

  • CSP Model

    • Decoupled by channel

    • Pub/Sub Pattern

    • Shared memory and lock are implemented in channel

    • Only one active consumer can access data

    • Processes are anonymous

Coroutine

  • Channel
run(function() {
    $channel = new Swoole\Coroutine\Channel(1);

    go(function () use ($channel) {
        for ($i = 1; $i <= 5; $i++) {
            echo "push {$i}\n";
            $channel->push($i);
            Swoole\Coroutine::sleep(1);
        }
    });

    go(function () use ($channel) {
        while ($result = $channel->pop(1.2)) {
            if (! $result) {
                break;
            }
            echo "pop {$result}\n";
        }
    });
});

Coroutine

  • Deadlock
run(function() {
    $amount = 100;
    $lock = new Swoole\Lock;
    echo "remaining amount: {$amount}\n";
    go(function () use (&$amount, $lock) {
        $lock->lock();
        Coroutine::sleep(1);
        $amount -= 10;
        $lock->unlock();
        echo "remaining amount: {$amount}\n";
    });
    go(function () use (&$amount, $lock) {
        $lock->lock();
        Coroutine::sleep(1);
        $amount -= 20;
        $lock->unlock();
        echo "remaining amount: {$amount}\n";
    });
});

Coroutine

  • Starving Problem
<?php

use function Swoole\Coroutine\run;

run(function() {
    go(function () {
        $a = 0;
        while (true) {
            $a++;
        }
    });

    go(function () {
        echo "hello world!\n";
    });
});

Coroutine

  • Preemptive Scheduler
Coroutine::set([
    'enable_preemptive_scheduler' => true
]);

run(function() {
    go(function () {
        $a = 0;
        while (true) {
            $a++;
        }
    });

    go(function () {
        echo "hello world!\n";
    });
});

Laravel Octane

  • Published in April 2021

  • Maintained by official Laravel team

  • Supports Swoole and Roadrunner

  • Requires Laravel 8 and PHP 8

  • Becoming more friendly in long-lived app

  • Hot reload support

  • Brings additional features

What is RoadRunner?

  • Built with Golang

  • A replacement for web server and PHP-FPM

  • Works on both Linux and Windows

  • HTTPS and HTTP/2 support (including HTTP/2 Push, H2C)

  • No external PHP dependencies

What is RoadRunner?

Laravel Octane

  • Lifecycle in Laravel

Laravel Octane

  • Installation

composer require laravel/octane
php artisan octane:install
  • Up and Run

PHP_BINARY=/usr/local/bin/swoole-cli php artisan octane:start

Laravel Octane

  • Watching For File Changes

npm install --save-dev chokidar

php artisan octane:start --watch

Laravel Octane

  • Supported Commands

php artisan octane:start --workers=4 --task-workers=6

php artisan octane:start --max-requests=250

php artisan octane:reload

php artisan octane:stop

php artisan octane:status

Laravel Octane

  • Singleton Binding

    • Avoid inject container or warmed services to your services
$this->app->singleton(Service::class, function ($app) {
    return new Service($app);
});
$this->app->bind(Service::class, function ($app) {
    return new Service($app);
});
 
$this->app->singleton(Service::class, function () {
    return new Service(fn () => Container::getInstance());
});

Laravel Octane

  • Listeners for Hooks

    • ​You can customize your resetting logics in the lifecycle
'listeners' => [
    RequestReceived::class => [
        ...Octane::prepareApplicationForNextOperation(),
        ...Octane::prepareApplicationForNextRequest(),
        //
    ],

    RequestHandled::class => [
        //
    ],

    RequestTerminated::class => [
        // FlushUploadedFiles::class,
    ],

    ......
 ]

Laravel Octane

  • Warmed Services

    • ​Services in warm config will be kept once initialized
public static function defaultServicesToWarm(): array
{
    return [
        'auth','cache','cache.store','config','cookie',
        'db','db.factory','db.transactions','encrypter',
        'files','hash','log','router','routes','session','session.store',
        'translator','url','view',
    ];
}

Laravel Octane

  • Global States Pollution

    • ​Global states will be shared in different requests.
    • Use global variables carefully unless you know what you’re doing in long-lived PHP.
    • You need to reset these states in the beginning of request if you don’t want to share them.
class Counter
{
	public static $count = 0;
}

Counter::$count++;

Laravel Octane

  • Memory Leaks

    • ​Global variables will cause memory leaks and states pollutions
    • `max_execution_time` in config can prevent memory leaks
use App\Service;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
 
public function index(Request $request)
{
    Service::$data[] = Str::random(10);
 
    // ...
}

Laravel Octane

  • Ticks

    • Based on Swoole Timer
    • Simple periodic jobs without cronjobs setup
Octane::tick('simple-ticker', fn () => ray('Ticking...'))
    ->seconds(10)
    ->immediate();

Laravel Octane

  • Cache and Table

    • Based on Swoole Table
    • Used for sharing data within proccesses
Cache::store('octane')->interval('random', function () {
    return Str::random(10);
}, seconds: 5);

Octane::table('example')->set('uuid', [
    'name' => 'Nuno Maduro',
    'votes' => 1000,
]);
 
Octane::table('example')->get('uuid');

Laravel Octane

  • Concurrent Tasks

Laravel Octane

  • Concurrent Tasks

    • Based on task workers in Swoole
    • Doesn't support coroutines yet
use App\User;
use App\Server;
use Laravel\Octane\Facades\Octane;
 
[$users, $servers] = Octane::concurrently([
    fn () => User::all(),
    fn () => Server::all(),
]);

Laravel Octane

  • Concurrent Tasks in Coroutine (Not supported yet)

Discussion

Swoole Workshop 2022

By Albert Chen

Swoole Workshop 2022

  • 426