Swoole

重新定義 PHP

@ PHP 也有 Day # 33

By Albert Chen

About Me

{
  "data": {
    "human": {
      "name": "Albert Chen",
      "occupation": "Software Engineer",
      "company": "Unisharp Techonology",
      "website": "http://albert-chen.com",
      "interests": [
        "traveling",
        "programming",
        "cooking"
      ],
      "age": 25,
      "relationship_status": "single"
    }
  }
}

Programmer Hierarchy

What is Swoole?

Swoole is...

a C extension for      

Open Source

Based on Apache 2.0 Liscense 

Swoole is a

Multi-process & Multi-thread

framework

Author

姓名:韓天峰 (Rango)

 

經歷:阿里巴巴集團

           車輪互聯 · 總架構師

 

簡介:資深 PHP 程序員

            Swoole 開源項目創始人

            PHP官方擴展開發组成員

 

+

=

Naming of Swoole

Features

  • Written in C, extremely high performance

  • Event driven, non-blocking I/O

  • Supports TCP /UDP / UnixSock

  • Supports Async / Sync / Coroutine

  • Supports IPv4 / IPv6 network

  • High CPU affinity
  • Multi-process, multi-thread structure

Use Cases

  • TCP / UDP server and client

  • Task worker

  • Database connection pool

  • Millisecond timer

  • Async I/O client

  • Async Http / WebSocket client

  • Async Redis / MySQL client

Use Cases

PHP is awesome

But can it be even faster?

PHP 7

PHP Lifecycle

PHP Execution

High Concurrency

Matters

PHP vs Node JS

PHP

Node JS

Sync vs. Async

Async Solutions

Wokerman

Kraken PHP

Let's get started

with Swoole!

Prerequisites

  1. Operation system: Linux, FreeBSD or MacOS

  2. Linux kernel version >= 2.3.32

  3. PHP version >= 5.3.10

  4. GCC version >= 4.4 or Clang

  5. Cmake version >= 2.4 (Cmake is required to compile libswoole as C/C++ library)

Installation

Linux

pecl install swoole

MacOS X

brew install php71-swoole

Manually Compile

#  enter the directory of swoole source code
cd swoole   
#  prepare the build environment for a PHP extension
phpize
#  add configuration paramaters as needed
./configure
#  a successful result of make is swoole/module/swoole.so
make
#  install the swoole into the PHP extensions directory
sudo make install

Enable Swoole

# check the php.ini file location
php -i | grep php.ini
# add the extension=swoole.so to the end of php.ini
sudo echo "extension=swoole.so" >> php.ini
# check if the swoole extension has been enabled
php -m | grep swoole
# check swoole version and modules
php --ri swoole

After install the swoole extension to the PHP extensions directory, you will need to adjust php.ini and add an extension=swoole.so line before you can use the extension.

Hello World

<?php

$http = new Swoole\Http\Server("127.0.0.1", 9501);

$http->on("start", function ($server) {
    echo "Swoole http server is started at http://127.0.0.1:9501\n";
});

$http->on("request", function ($request, $response) {
    $response->header("Content-Type", "text/plain");
    $response->end("Hello World\n");
});

$http->start();

vs.

Swoole vs. NodeJS

Benchmark

<?php

$http = new Swoole\Http\Server("127.0.0.1", 9501);

$http->on("start", function ($server) {
    echo "Swoole http server is started at http://127.0.0.1:9501\n";
});

$http->on("request", function ($request, $response) {
    $response->header("Content-Type", "text/html");
    $response->end("hello world");
});

$http->start();

Benchmark

var http = require('http');

var app = function (req, res) {
  res.writeHead(200, {
    'Content-Type': 'text/html'
  });
  res.end('hello world');
};

var server = http.createServer(app);

server.listen(9502, function() {
  console.log("Server running at http://127.0.0.1:9502");
});

Benchmark

wrk -t4 -c400 -d10s http://127.0.0.1:9501
Running 10s test @ http://127.0.0.1:9501
  4 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    17.16ms   26.41ms 199.98ms   86.94%
    Req/Sec    12.53k     1.59k   17.51k    72.75%
  499976 requests in 10.03s, 78.20MB read
  Socket errors: connect 0, read 235, write 0, timeout 0
Requests/sec:  49853.41
Transfer/sec:      7.80MB
Running 10s test @ http://127.0.0.1:9502
  4 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    30.62ms    5.28ms 144.94ms   92.94%
    Req/Sec     3.14k   645.97     4.37k    85.50%
  125051 requests in 10.03s, 19.20MB read
  Socket errors: connect 0, read 164, write 19, timeout 0
Requests/sec:  12462.35
Transfer/sec:      1.91MB

What happend behind the scenes?

Work Flow

Swoole Server Model

Reactor Model

Requests arrive main reactor

Main reactor binds request to reactor

Reactor dispatches socket to worker

Worker handles the main logic

Callback to correspondent reactor

Reactor returns result to client

Sequence Diagram

Swoole Basics

HTTP Server

HTTP Server

<?php

$http = new Swoole\Http\Server("127.0.0.1", 9501);

$http->set([
    'daemonize' => true,
    'reactor_num' => 4,
    'worker_num' => 4,
    'task_worker_num' => 4,
    'max_request' => 10000,
    'task_max_request' => 10000,
    'open_cpu_affinity' => true,
    'log_file' => __DIR__ . '/swoole.log',
    'ssl_cert_file' => $key_dir . '/ssl.crt',
    'ssl_key_file' => $key_dir . '/ssl.key'
]);

HTTP Server

<?php

$http->on("start", function ($server) {
    
    echo "Swoole http server is started at http://127.0.0.1:9501\n";
    // don't define any global variables here
});
In the callback function registered for start, it only allows to log record and modify the name of process.

The event start and workerstart happen concurrently in different processes and have no precedence.

HTTP Server

<?php

$http->on("workerStart", function (Server $server, int $worker_id) {
    echo "worker start id: {$worker_id}\n";
    echo 'taskworker: ' . json_encode($server->taskworker) . "\n";
    // not allowed in osx
    // swoole_set_process_name('process name');
});

The event workerstart happens when the worker process or task worker process starts.

HTTP Server

<?php

$http->on("connect", function (Server $server, int $fd, int $from_id) {
    // echo json_encode($server) . "\n";
});

This event connect happens when the new connection comes. And the worker process will call the callback function registered for the event connect.

In the mode UDP, there is only receive event and there is no connect and close event.

  • $fd the id number of client

  • $from_id the id number of reactor thread

HTTP Server

<?php

$http->on("request", function ($request, $response) {
    echo json_encode($request);
    // forget php magic functions
    // echo json_encode($_GET);
    $response->header("Content-Type", "text/html");
    $response->end('Hello World!');
});

You can get the information of request here (GET, POST, etc.) and handle your response

HTTP Server

<?php

$http->set([
    'task_worker_num' => 4
]);

Send data to the task worker processes.To call this method, it needs to set the configuration of task_worker_num and the callback function of onTask and onFinish.

This method is non-blocking and return immediately after sending the task data to the pool of task worker.

HTTP Server

<?php

$http->on("task", function (Server $server, $task_id, $from_id, $data) {
    // task worker received data
    echo "task received:" . json_encode($data) . "\n";
    return $data;
});

$http->on("finish", function (Server $server, $task_id, $data) {
    // task worker callback
    echo "task finished:" . json_encode($data) . "\n";
});

global $globalServer;
$data = [
    'code' => 'ok',
    'error' => false,
    'payload' => 'Hello World'
];
$globalServer->task($data);

Swoole Basics

Websocket Server

Websocket Server

Websocket Server

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

Websocket Server

The swoole_websocket_server class inherits from the class swoole_server.

If the swoole_websocket_server has been setted the callback function of event request, it can be used as Http server.

If the swoole_websocket_server hasn't been setted the callback function of event request and received http request, it would repond http 400 error.

Websocket Server

<?php

use Swoole\Websocket\Server;

$server = new Server("127.0.0.1", 9503);

$server->on('open', function (Server $server, $request) {
    // echo json_encode($request) . "\n";
    echo "server: handshake success with fd{$request->fd}\n";
});

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

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

$server->start();

Websocket Server

<?php

$server->on('handshake', function (Request $request, Response $response) {
    // you can choose to implement your handshake method
    $secWebSocketKey = $request->header['sec-websocket-key'];
    $patten = '#^[+/0-9A-Za-z]{21}[AQgw]==$#';
    if (0 === preg_match($patten, $secWebSocketKey) || 16 !== strlen(base64_decode($secWebSocketKey))) {
        $response->end();
        return false;
    }
    echo $request->header['sec-websocket-key'];
    $key = base64_encode(sha1(
        $request->header['sec-websocket-key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
        true
    ));

    $headers = [
        'Upgrade' => 'websocket',
        'Connection' => 'Upgrade',
        'Sec-WebSocket-Accept' => $key,
        'Sec-WebSocket-Version' => '13',
    ];

    if (isset($request->header['sec-websocket-protocol'])) {
        $headers['Sec-WebSocket-Protocol'] = $request->header['sec-websocket-protocol'];
    }

    foreach ($headers as $key => $val) {
        $response->header($key, $val);
    }

    $response->status(101);
    $response->end();
    echo "connected!" . PHP_EOL;
    return true;
});

Websocket Server

<?php

public function broadcast($server, $data)
{
    $connections = $server->connections;
    $sender = $data['sender'] ?? 0;

    foreach($connections as $fd) {
        if ($sender !== (integer) $fd) {
            $server->push($fd, $message, $opcode);
        }
    }
}

There's no socket.io library for swoole yet, that means you need to handle every detail of websocket on your own.

Swoole Basics

Async IO

Async File IO

<?php

use Swoole\Async;

// you can configure your async settings here.
Async::set([
    'aio_mode' => SWOOLE_AIO_LINUX,
    'thread_num' => 4,
]);

Async::readfile(__DIR__ . "/file.txt", function($filename, $content) {
     echo "$filename: $content";
});

Read files in the async way. When the operation of reading content from file finished, the callback registered is callback automatically.

The max length of file is 4M.

Async File IO

<?php

use Swoole\Async;

// If the aio mode setted by swoole_async_set is SWOOLE_AIO_BASE,
// the method can't support append content to the end of file and
// must set the value of $fileContent to integer multiples of 4096.
Async::writeFile(__DIR__ . "/file.log", "test\n", function ($filename) {
    echo "wirte ok.\n";
}, $append = false);

Write file in the async way. When the operation of writing content to file finished, the callback registered is callback automatically.

Async File IO

<?php

use Swoole\Async;

Async::read(__DIR__ . "/file.txt", function ($filename, $content) {
    echo "file: $filename\ncontent-length: " . strlen($content) . "\nContent: $content\n";
    if (empty($content)) {
        echo "file is end.\n";
        return false;
    } else {
        return true;
    }
}, 8192);

Read the file content stream in the async way. When the operation of reading content from file finished, the callback registered is callback automatically.

Async File IO

<?php

use Swoole\Async;

for ($i = 0; $i < 10; $i++) {
    Async::write(__DIR__ . "/file.log", "A\n", -1, function ($file, $writen) {
        echo "write $file [$writen]\n";
        //return true: write contine. return false: close the file.
        return true;
    });
}

Write file stream in the async way. When the operation of writing content to file finished, the callback registered is callback automatically.

Async IO

  • Async MySQL Client

  • Async Redis Client

  • Async HTTP Client

  • Async Websocket Client

  • Async HTTP 2.0 Client

Swoole Basics

Coroutine

Coroutine

Coroutine

<?php

$a = sum(1, 2);
$b = sum($a, 3);
$c = sum($b, 4);
var_dump(array($a, $b, $c));
<?php

async_sum(1, 2, function($a) {
    async_sum($a, 3, function($b) use ($a) {
        async_sum($b, 4, function($c) use ($a, $b) {
            var_dump(array($a, $b, $c));
        });
    });
});

Coroutine

Coroutine

Coroutine

Coroutine

<?php

use Swoole\Coroutine\MySQL;

$mysql = new Swoole\Coroutine\MySQL();
$mysql->connect([
    'host' => '127.0.0.1',
    'user' => 'user',
    'password' => 'pass',
    'database' => 'test',
]);
$result = $mysql->query('select sleep(1)');

// your business logic

Coroutine

<?php

use Swoole\Coroutine\MySQL;
use Swoole\Coroutine\Redis;

$redis = new Redis;
$redis->connect('127.0.0.1', 6379);
$redis->setDefer();
$redis->get('key');

$mysql = new MySQL;
$mysql->connect(['host' => '127.0.0.1', 'user' => 'user', 'password' => 'pass', 'database' => 'test']);
$mysql->setDefer();
$mysql->query('select sleep(1)');

$redis_res = $redis->recv();
$mysql_res = $mysql->recv();

// your business logic

Coroutine

Redis sent packet

Redis received packet

MySQL sent packet

MySQL received packet

Redis sent packet

Redis received packet

MySQL sent packet

MySQL received packet

Communication in

Different Processes and Threads

Communication Issue

<?php

use Swoole\Http\Server;

$http = new Server("127.0.0.1", 9501);

$http->on("start", function ($server) {
    echo "Swoole http server is started at http://127.0.0.1:9501\n";
});

$i = 1;
$http->on("request", function ($request, $response) {
    global $i;
    $response->header("Content-Type", "text/html");
    $response->end($i);
    $i++;
});

$http->start();

Process Model

Communication

  • I/O (File, DB)

  • Pipe

  • Message Queue

  • Memory Table

  • Channel

Swoole Table

Swoole table is a high performance memory management module, implemented based on shared memory and spin lock.

  • High performance, the single thread read/write speed is more than 2 millions per second.
  • Can be used by multiple threads or processes.

In order to keep the data synchronization, you have to use Swoole\Lock if the memory is modified and accessed by multiple threads or processes.

Swoole Table

<?php

use Swoole\Table;

// create table
// columns size
$table = new Table(1024);
// 1, 2, 4, 8
$table->column('id', swoole_table::TYPE_INT, 4);
$table->column('name', swoole_table::TYPE_STRING, 64);
$table->column('num', swoole_table::TYPE_FLOAT);
$table->create();

// set column
$table->set('tianfenghan@qq.com', array('id' => 145, 'name' => 'rango', 'num' => 3.1415));
$table->set('350749960@qq.com', array('id' => 358, 'name' => "Rango1234", 'num' => 3.1415));

// get column
$ret1 = $table->get('350749960@qq.com');

// delete column
$table->del('350749960@qq.com');

Notices

  • You need to allocate memory size for Swoole Table beforehand.
  • Swoole Table can't be expanded once the memory is allocated.
  • Column key can not be greater than 64 bytes.
  • Swoole Table is based on the structure of hash table, there's collision possibility.
  • You can cutomize your pool size(20 % as default). 
  • Insufficient memory will cause an error while allocating swoole table.

Database

Connection Pooling

Async IO

Connection Pooling

Connection Pooling

<?php

$count = 0;
$pool = new SplQueue();
$server = new Swoole\Http\Server('127.0.0.1', 9501, SWOOLE_BASE);

$server->on('Request', function($request, $response) use(&$count, $pool) {
    if (count($pool) == 0) {
        $redis = new Swoole\Coroutine\Redis();
        $res = $redis->connect('127.0.0.1', 6379);
        if ($res == false) {
            $response->end("redis connect fail!");
            return;
        }
        $pool->push($redis);
        $count++;
    }
    $redis = $pool->pop();
    $ret = $redis->set('key', 'value');
    $response->end("swoole response is ok, count = $count, result=" . var_export($ret, true));
    $pool->push($redis);
});
$server->start();

Connection Pooling

swoole/php-cp

  • A powerful local connection pool proxy server
  • Implement mysql/redis database connection pool
  • Provide max/min pool size setting
  • Support connection waiting queue
  • Support async mysql/redis query
pool_server start
pool_server stop
pool_server restart
pool_server status

Connection Pooling

<?php

// redis
$obj = new redisProxy();
$rs = $obj->connect("192.168.20.130");
$obj->select(5);
$obj->set("test", '1111');
var_dump($obj->get("test"));
$obj->release();
// pdo
$obj1 = new pdoProxy('mysql:host=192.168.20.131;dbname=db1', "admin", "admin");
$rs = $obj1->query("show tables");
var_dump($rs->fetchAll());
$obj1->release();
// aync pdo
$obj2 = new asyncPdoProxy('mysql:host=192.168.1.19;dbname=mz_db', "public_user", "1qa2ws3ed");
$stmt = $obj2->prepare("select * from mz_account where user_id=:user_id");
$stmt->bindParam(':user_id', "311");
$stmt->execute(function($stmt, $ret) {
    $data = $stmt->fetchAll();
    var_dump($data);
    $stmt->release();
});

Speed up your

Laravel App

with Swoole

Laravel with Swoole

  • scil/LaravelFly
  • garveen/laravoole
  • chongyi/swoole-laravel-framework
  • huang-yi/swoole-laravel

Current related projects on github

  1. Purely run laravel with swoole server
  2. Restore laravel snapshot every request

Implementations

Laravel with Swoole

Test with Lumen 5.1 and

huang-yi/swoole-laravel

public function onRequest(Request $request, Response $response)
{
    $illuminateRequest = IlluminateRequest::swooleCapture($request);
    $illuminateResponse = $this->runApplication($illuminateRequest);
    $swooleResponse = ResponseFactory::createFromIlluminate(
        $response,
        $illuminateResponse
    );
    $swooleResponse->send();
}

Laravel with Swoole

wrk -t4 -c400 -d10s http://lumen51.app:9999
Running 10s test @ http://lumen51.app:9999
  4 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   692.27ms  147.64ms   1.51s    85.77%
    Req/Sec    44.84     28.67   160.00     72.78%
  1679 requests in 10.05s, 2.52MB read
  Socket errors: connect 0, read 300, write 0, timeout 21
  Non-2xx or 3xx responses: 41
Requests/sec:    167.04
Transfer/sec:    256.68KB
Running 10s test @ http://localhost:1215
  4 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    22.38ms    5.88ms 116.88ms   77.94%
    Req/Sec     4.18k   446.00     5.10k    74.25%
  167130 requests in 10.06s, 247.37MB read
  Socket errors: connect 0, read 226, write 0, timeout 0
Requests/sec:  16616.24
Transfer/sec:     24.59MB

Notices

  • Most of those packages are not maintained
  • Blocked IO breaks everything
  • It's easier for stateless usage
  • You need to reset some objects status by yourself
  • It's not suitable for production usage yet

Swoole Frameworks

  • Swoole Framework
  • shenzhe/zphp
  • keaixiaou/zapi
  • hprose/hprose-php
  • bixuehujin/blink
  • swoft-cloud/swoft
  • tmtbe/SwooleDistributed

Q&A

Swoole 台灣