@xf3l1x
f3l1x.io
12.05.2023
ย
FAST & FURIOUS
SPEED
SPEED
RESEARCH
SPEED
RESEARCH
PHP is used by 77.5%ย of all the websites whose server-side programming language we know.
PHP lifecycle
Research goals
Divide & conquer
Coroutines
Eventloops
Extensions
Process managers
Built-in
Divide & conquer
Webservers
Frameworks
Process managers
Extensions
Webservers
Webservers
Apache
Webservers
Apache
<VirtualHost *:8000>
DocumentRoot /srv
<FilesMatch \.php$>
SetHandler "proxy:unix:/var/run/php-fpm.sock|fcgi://localhost/"
</FilesMatch>
<Directory /srv>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
LogLevel warn
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
Webservers
Nginx
server {
listen 8000;
index index.php;
root /srv/;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~* \.php$ {
fastcgi_pass app;
fastcgi_index index.php;
include fastcgi_params;
try_files $uri =404;
}
}
Webservers
Caddy
# Listening on
:8000
# Set the document root of the site.
root * /srv
# Set the path to the php-fpm process.
php_fastcgi unix//var/run/php-fpm.sock
# Global options
{
# Disable admin
admin off
}
Webservers
Unit
{
"listeners": {
"*:8000": { "pass": "routes" }
},
"routes": [
{
"action": {
"share": "/srv/www$uri",
"fallback": { "pass": "applications/app" }
}
}
],
"applications": {
"app": {
"type": "php",
"root": "/srv/",
"script": "index.php",
"options": {
"admin": {
"memory_limit": "256M",
}
}
}
}
}
Frameworks
Frameworks
Amphp
Loop::run(function () {
$sockets = [Socket\listen("0.0.0.0:8000")];
$server = new Server(
$sockets,
new CallableRequestHandler(static function (Request $request) {
echo "Request received {$request->getUri()}" . PHP_EOL;
return new Response(
Status::OK,
['Content-type' => 'application/json'],
json_encode([
'php' => PHP_VERSION,
'webserver' => 'Amphp',
]));
}
), new NullLogger);
echo "Listening on 0.0.0.0:8000" . PHP_EOL;
yield $server->start();
});
Frameworks
Reactphp
$http = new HttpServer(function ($request) {
echo "Request received {$request->getUri()}" . PHP_EOL;
return Response::json([
'php' => PHP_VERSION,
'webserver' => 'ReactPHP',
]);
});
$socket = new SocketServer('0.0.0.0:8000');
$http->listen($socket);
echo "Listening on {$socket->getAddress()}" . PHP_EOL;
Frameworks
Workerman
$worker = new Worker('http://0.0.0.0:8000');
$worker->count = 4;
$worker->onMessage = function ($connection, $request) {
echo "Request received {$request->uri()}" . PHP_EOL;
$connection->send(
new Response(
200,
['Content-type' => 'application/json'],
json_encode([
'php' => PHP_VERSION,
'webserver' => 'Workerman',
])
)
);
};
echo "Listening on {$worker->getSocketName()}" . PHP_EOL;
Worker::runAll();
Frameworks
PHP server
php -S 0.0.0.0:8000 /srv/index.php
PHP_CLI_SERVER_WORKERS=4 php -S 0.0.0.0:8000 /srv/index.php
Process managers
Process managers
PHP-PM
{
"host": "0.0.0.0",
"port": 8000,
"workers": 8,
"static-directory": "www",
"logging": 0,
"max-requests": 2000,
"bridge": "AppBridge",
"bootstrap": "AppBridge"
}
class AppBridge implements BridgeInterface
{
public function handle($request): ResponseInterface
{
return new Response(
200,
['Content-type' => 'application/json'],
json_encode([
'php' => PHP_VERSION,
'webserver' => 'PHP-PM',
])
);
}
}
Process managers
Roadrunner
$worker = Worker::create();
$psrFactory = new Psr17Factory();
$worker = new PSR7Worker($worker, $psrFactory, $psrFactory, $psrFactory);
while ($req = $worker->waitRequest()) {
$rsp = new Response( 200, ['Content-type' => 'application/json'],
json_encode([
'php' => PHP_VERSION,
'webserver' => 'Roadrunner',
])
);
$worker->respond($rsp);
}
version: '3'
rpc:
listen: tcp://127.0.0.1:6001
server:
command: "php /srv/index.php"
http:
address: "0.0.0.0:8000"
Extensions
Extensions
Swoole
$http = new Server('0.0.0.0', 8000);
$http->set(['hook_flags' => SWOOLE_HOOK_ALL]);
$http->on('request', function ($request, $response) {
echo "Request received {$request->server['request_uri']}" . PHP_EOL;
$response->setHeader('content-type', 'application/json');
$response->end(json_encode([
'php' => PHP_VERSION,
'webserver' => 'Swoole',
]));
});
echo "Swoole http server is started at http://0.0.0.0:8000" . PHP_EOL;
$http->start();
Extensions
Openswoole
$server = new Server("0.0.0.0", 8000);
$server->on("Start", function (Server $server) {
echo "OpenSwoole http server is started at http://0.0.0.0:8000\n";
});
$server->on("Request", function ($request, $response) {
echo "Request received {$request->server['request_uri']}" . PHP_EOL;
$response->header("Content-Type", "application/json");
$response->end(json_encode([
'php' => PHP_VERSION,
'webserver' => 'OpenSwoole',
]));
});
$server->start();
Extensions
Swow
$server = new Server();
$server->bind('0.0.0.0', 8000);
$server->listen();
echo "Swow http server is started at http://0.0.0.0:8000\n";
Coroutine::run(function () use ($server) {
while (true) {
try {
$connection = $server->acceptConnection();
Coroutine::run(function () use ($connection) {
try {
while (true) {
$request = null;
try {
$request = $connection->recvHttpRequest();
echo "Request received {$request->getUri()}" . PHP_EOL;
$response = new Response();
$response->setStatus(200);
$response->setHeader('Content-type', 'application/json');
$response->getBody()->write(json_encode([
'php' => PHP_VERSION,
'webserver' => 'Swow',
]));
$connection->sendHttpResponse($response);
} catch (ProtocolException $exception) {
$connection->error($exception->getCode(), $exception->getMessage());
}
if (!$request || !Psr7::detectShouldKeepAlive($request)) {
break;
}
}
} catch (Throwable $exception) {
echo $exception->getMessage() . PHP_EOL;
} finally {
$connection->close();
}
});
} catch (SocketException|CoroutineException $exception) {
if (in_array($exception->getCode(), [Errno::EMFILE, Errno::ENFILE, Errno::ENOMEM], true)) {
echo "Socket resources have been exhausted." . PHP_EOL;
sleep(1);
} else {
echo $exception->getMessage() . PHP_EOL;
break;
}
} catch (Throwable $exception) {
echo $exception->getMessage() . PHP_EOL;
}
}
});
waitAll();
Benchmark tools
ab
ab -k -c 1 -n 10000 http://localhost:8000
wrk
wrk -t10 -c10 -d30s http://localhost:8000
hey
hey -n 10000 -c 100 -z 1s http://localhost:8000
plow
plow -c 1000 -n 10000 http://localhost:8000
Benchmark tools
ab
ab -k -c 1 -n 10000 http://localhost:8000
wrk
wrk -t10 -c10 -d30s http://localhost:8000
hey
hey -n 10000 -c 100 -z 1s http://localhost:8000
plow
plow -c 1000 -n 10000 http://localhost:8000
Benchmark tools
{
"Percentiles": {
"P50": "944ยตs",
"P75": "1.007ms",
"P90": "1.064ms",
"P95": "1.102ms",
"P99": "4.413ms",
"P99.9": "494.124ms",
"P99.99": "494.789ms"
},
"Histograms": [
[ "932ยตs", 8050 ],
[ "1.132ms", 1820 ],
[ "3.511ms", 84 ],
[ "4.559ms", 34 ],
[ "8.31ms", 2 ],
[ "494.274ms", 4 ],
[ "494.479ms", 3 ],
[ "494.672ms", 3 ]
]
}
{
"Summary": {
"Elapsed": "1.5s",
"Count": 10000,
"Counts": {
"2xx": 10000
},
"RPS": 6661.748,
"Reads": "1.906MB/s",
"Writes": "0.368MB/s"
}
}
plow -c10 -n10000 --summary --json http://localhost:8000
Results?
Benchmarks
Apple M1 Ultra / 128 GB
App | Type | C1/R10000 | C10/R10000 | C100/R10000 | C1000/R10000 |
---|---|---|---|---|---|
apache+fpm | webserver | 1423 | 8139 | 14705 | 2937 |
nginx+fpm | webserver | 1642 | 9215 | 20944 | 15006 |
caddy+fpm | webserver | 953 | 7941 | 17318 | 15618 |
unit | webserver | 1968 | 10164 | 19593 | 19294 |
php-server | PHP built-in | 376 | 438 | 428 | 800 |
amphp | eventloop | 2213 | 10164 | 19593 | 19249 |
reactphp | eventloop | 2056 | 10190 | 16403 | 15919 |
workerman | eventloop | 2389 | 11873 | 31402 | 14314 |
php-pm | PM | 445 | 790 | 454 | 655 |
roadrunner | PM [go] | 1686 | 9437 | 29230 | 19733 |
openswoole | extension | 1711 | 10303 | 32694 | 20084 |
swoole | extension | 2193 | 11572 | 25597 | 18626 |
swow | extension | 2011 | 11053 | 23187 | 17426 |
*on my computer
Conclusion
PHP is great
ecosystem is great
serverless PHP (bref/vercel)
nativephp (self-executable) [WIP]
phpstan, codesniffer, phpunit
symfony, laravel, nette
PHP dev server [local]
Nginx + FPM [classic]
Nginx Unit [micros]
Roadrunner [win+win]
Openswoole [speed]
Let's connect
github.com/contributte/benchmarks
Time to selfie ๐ท
Thank you!
@xf3l1x
f3l1x.io