Skip to content

Latest commit

 

History

History
114 lines (86 loc) · 5.01 KB

File metadata and controls

114 lines (86 loc) · 5.01 KB

Stoke

Read in English

Un serveur d'application PHP. libphp est lié dans un binaire Rust, avec un SAPI maison et hyper au-dessus. Pas de FastCGI, pas de reverse proxy, pas de gestionnaire de processus.

Preuve de concept. C'est testé comme du logiciel de production. 31 cas de conformité comparés octet à octet à php-fpm, 21 injections de fautes, 6 vérifications de streaming, 13 tests unitaires, un test d'endurance de 10 minutes. Tout passe. Ça ne suffit pas à en faire du logiciel de production. Un seul contributeur, aucun historique de déploiement, aucun audit de sécurité, pas de HTTP/3. Pour quelque chose de réel, prends FrankenPHP ou php-fpm.

Résultats

Symfony 7.2, prod, cache chaud, 6 workers, 64 connexions, médiane de 3 runs.

stack route rps p50 p99 mémoire
stoke worker / 59 386 0,96 ms 4,30 ms 81 Mo
frankenphp worker / 24 574 2,31 ms 7,59 ms 196 Mo
stoke worker /json 100 Ko 8 971 6,47 ms 18,31 ms 83 Mo
frankenphp worker /json 100 Ko 7 278 8,50 ms 16,31 ms 199 Mo
stoke classic / 4 857 13,29 ms 28,82 ms 50 Mo
php-fpm + nginx / 4 046 15,53 ms 25,93 ms 40 Mo

CPU par requête : stoke 105 µs, FrankenPHP 252 µs, php-fpm 1 952 µs. La p99 sur les grosses réponses change de camp d'un run à l'autre, donc considère ces deux-là comme à égalité.

Mesuré dans une VM Docker Desktop sur Apple M4. Les chiffres ne valent qu'en relatif. Toutes les stacks ont tourné dans le même conteneur, sur la même application, dans la même session.

Pourquoi c'est rapide

PHP tourne sur le thread auquel hyper remet la requête. Pas de canal, pas de réveil. Le moteur n'est pas thread-safe, donc le serveur duplique le processus au lieu d'utiliser des threads.

L'app est bootée avant le fork. Les workers partagent son tas en copie sur écriture : un worker coûte 38 Mo, seize en coûtent 44.

$_SERVER ne change presque pas d'une requête à l'autre, donc il est copié depuis une table de hachage persistante au lieu d'être réenregistré entrée par entrée.

Utilisation

docker compose -f docker/compose.yml up -d dev
docker compose -f docker/compose.yml exec dev cargo build --release

STOKE_ROOT=/app/public stoke                                      # classic
STOKE_ROOT=/app/public STOKE_WORKER_SCRIPT=/app/worker.php stoke   # worker

Un script worker renvoie un callable :

<?php
$kernel = new Kernel('prod', false);
$kernel->boot();

return function () use ($kernel) {
    $request = Request::createFromGlobals();
    $response = $kernel->handle($request);
    $response->send();
    $kernel->terminate($request, $response);
};

Les scripts worker écrits pour FrankenPHP tournent sans modification. frankenphp_handle_request($handler) est accepté et récupère le callable.

Renvoie un tableau à la place pour avoir un hook de démarrage par worker. Il tourne une fois dans chaque processus dupliqué. C'est là qu'on rouvre ce que le bootstrap d'avant le fork a laissé partagé :

return [
    'on_worker_start' => fn () => $pool->reconnect(),
    'handler' => fn () => $kernel->handle(Request::createFromGlobals())->send(),
];

Les réglages viennent de l'environnement. STOKE_CONFIG désigne un fichier qui utilise les mêmes noms sans le préfixe (workers = 4). L'environnement l'emporte sur le fichier.

variable défaut
STOKE_LISTEN 0.0.0.0:8080
STOKE_ROOT /app/public racine documentaire
STOKE_WORKERS nombre de cœurs processus workers
STOKE_WORKER_SCRIPT active le mode worker
STOKE_FRONT_CONTROLLER script de repli en mode classic
STOKE_TLS_CERT / STOKE_TLS_KEY active TLS, ALPN négocie HTTP/2
STOKE_STREAM 0 envoie la sortie au fil de l'eau. Nécessaire pour SSE et long polling, coûte environ 10 % de débit
STOKE_STATIC 1 sert les fichiers statiques
STOKE_MAX_BODY 8 Mio taille maximale du corps de requête
STOKE_LOOP_MAX 0 recycle un worker après N requêtes
STOKE_PREFORK_BOOT 1 boote l'app avant le fork, pour que les workers partagent sa mémoire
STOKE_INI directives php.ini supplémentaires, séparées par des virgules
STOKE_CONFIG chemin d'un fichier de réglages

HTTP/1.1 et HTTP/2 partagent le même port, avec ou sans TLS.

Les suites de tests se lancent avec sh tests/run-all.sh dans le conteneur de dev.

Périmètre

HTTP/3 et les certificats automatiques ne sont pas prévus. C'est le boulot du proxy qu'on met devant. Pas la peine de refaire Caddy.

Deux trucs à savoir. Avec STOKE_STREAM=1, un client qui arrête de lire bloque son worker jusqu'à max_execution_time. Tous les serveurs sans tampon font pareil. Avec STOKE_PREFORK_BOOT=1, les workers partagent ce que le bootstrap a ouvert. Le hook on_worker_start sert à le rouvrir, sinon mets le drapeau à 0.