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