Référence de configuration

OxPHP se configure entièrement au moyen de variables d'environnement. Il n'y a aucun fichier de configuration à gérer, et chaque paramètre possède une valeur par défaut, si bien qu'un déploiement sans configuration fonctionne d'emblée.

Valeurs booléennes

Les variables marquées comme booléennes acceptent un ensemble canonique fixe, insensible à la casse et débarrassé des espaces superflus :

  • vraies : on, true, 1, yes
  • fausses : off, false, 0, no

Toute valeur non vide en dehors de cet ensemble — des fautes de frappe comme ture — échoue immédiatement au démarrage avec une erreur nommant la variable. On repère ainsi une mauvaise configuration avant l'arrivée du trafic, au lieu de basculer silencieusement un indicateur dans le mauvais sens.

Une variable non définie ou une affectation vide (FOO=) retombe sur la valeur par défaut documentée. Une valeur vide est traitée comme non définie à dessein : une substitution Docker Compose / Kubernetes du type FOO=${FOO} produit FOO= lorsque la variable de l'hôte est absente, et cela ne doit pas empêcher le serveur de démarrer.

Serveur

Variable Valeur par défaut Description
LISTEN_ADDR 0.0.0.0:80 Adresse et port du serveur HTTP principal
DOCUMENT_ROOT /var/www/html/public Répertoire racine pour servir les fichiers et les scripts PHP
ENTRY_FILE (non défini) Script d'entrée canonique unique. Non défini = mappage direct des fichiers. *.php = contrôleur frontal. Non-.php = repli statique (SPA). Avec WORKER_MODE_ENABLED=true = script d'amorçage du worker. Résolu par rapport à DOCUMENT_ROOT (chemins relatifs et .. autorisés ; chemins absolus utilisés tels quels). Voir Routage
WORKER_MODE_ENABLED false Active le mode worker persistant. Nécessite que ENTRY_FILE pointe vers un script .php. Booléen — voir Valeurs booléennes
MAX_CONNECTIONS 10000 Nombre maximal de connexions TCP simultanées. Sert aussi de plafond au QUEUE_MAX_WAITING par défaut (sa moitié), si bien qu'une valeur malformée est une erreur au démarrage plutôt qu'un repli silencieux. L'abaisser ne déplace pas la valeur par défaut de QUEUE_CAPACITY, dimensionnée d'après le seul nombre de workers — gardez-le au-dessus de PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING, voir Garder de la marge pour la boucle d'acceptation
TOKIO_WORKERS CPU / 2 (min. 1) Threads d'E/S asynchrones. 1 = mono-thread, N > 1 = nombre de threads fixe, non défini = automatique (CPU / 2, min. 1)

Workers PHP

Variable Valeur par défaut Description
EXECUTOR sapi Backend d'exécution PHP. sapi pour l'exécution PHP, stub pour le benchmarking sans PHP
PHP_WORKERS CPU / 2 (min. 1) Taille du pool de workers. N = pool fixe, MIN:MAX = mise à l'échelle dynamique, 0 = automatique
PHP_WORKERS_IDLE_SECONDS 30 Nombre de secondes pendant lesquelles un worker dynamique reste inactif avant d'être retiré (mode dynamique uniquement). Un worker est retiré à un moment où il n'a rien en cours, si bien que rien de ce qu'il sert n'est interrompu — et un worker qui détient une requête qui ne se termine jamais, comme un flux ouvert, n'atteint jamais un tel moment et n'est pas retiré du tout
QUEUE_CAPACITY Workers initiaux × 128 Nombre maximal de requêtes en attente dans la file PHP. Les requêtes arrivant sur une file pleine attendent une place (voir QUEUE_WAIT_TIMEOUT_MS). Pour les pools dynamiques (MIN:MAX), le nombre de workers initiaux = le minimum. 0 = automatique
QUEUE_WAIT_TIMEOUT_MS 1000 Durée pendant laquelle une requête peut attendre un worker PHP avant d'être rejetée avec un 529. Une seule échéance, estampillée à l'arrivée, couvrant les deux attentes qu'une requête peut affronter : pour une place dans la file, puis dans la file pour un worker. Une requête qu'un worker atteint après son échéance est refusée à la prise en charge plutôt qu'exécutée, si bien que le budget borne l'attente entière et pas seulement sa moitié d'admission — il ne borne pas la durée d'exécution du gestionnaire ensuite. Au plus QUEUE_MAX_WAITING requêtes attendent à la fois ; au-delà, les requêtes sont rejetées immédiatement. 0 = rejeter immédiatement dès que la file est pleine. Abaissez-le, ou mettez 0, quand l'application rappelle ce même serveur en HTTP (l'appel intérieur ne peut pas être admis tant que l'appel extérieur ne libère pas son worker, l'attente est donc dépensée pour rien), ou quand un répartiteur de charge en amont a un délai plus court que lui. Un client qui ferme la connexion en pleine attente rend sa place immédiatement, en HTTP/1.1 comme en HTTP/2, si bien qu'un répartiteur qui expire et ferme ne remplit pas l'ensemble d'attente de tentatives qu'il a déjà abandonnées. Ce que le serveur ne peut pas voir, c'est un client qui cesse d'attendre sans fermer — il garde sa place jusqu'à son admission ou l'épuisement du budget, et si un worker se libère d'abord, son script s'exécute pour personne — garder le budget en dessous du délai de tout ce qui se trouve devant reste donc une bonne idée
QUEUE_MAX_WAITING Workers initiaux × 128, plafonné à MAX_CONNECTIONS / 2 Nombre maximal de requêtes stationnées en attente d'une place dans la file au même moment. Au-delà, les requêtes sont rejetées immédiatement au lieu d'attendre. Chaque requête en attente détient une connexion et un corps de requête entièrement mis en tampon aussi longtemps qu'elle attend, c'est donc une borne sur les ressources détenues, pas sur les attentes qui aboutiront. Le plafond MAX_CONNECTIONS / 2 de la valeur par défaut ne borne que cette partie de l'arriéré ; les requêtes en file et en cours d'exécution détiennent une connexion de la même manière, si bien que la marge que le serveur garde réellement pour accepter et refuser découle de PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING face à MAX_CONNECTIONS — voir plus bas. Dimensionnez-le d'après la latence de votre propre gestionnaire — plus bas également. 0 = automatique, jamais en dessous de 1
QUEUE_MAX_WAITING_BYTES 67108864 (64 Mio) Nombre maximal d'octets de corps de requête que les requêtes stationnées peuvent détenir à elles toutes. Une requête dont le corps ferait dépasser ce total est rejetée immédiatement avec un 529 au lieu d'attendre ; une requête sans corps n'est jamais rejetée pour cela. QUEUE_MAX_WAITING borne le même ensemble en requêtes, ce qui ne dit rien de leur taille — les corps sont mis en tampon en entier avant qu'une requête n'atteigne la file, si bien qu'avec un simple décompte pour les arrêter, l'ensemble stationné peut détenir QUEUE_MAX_WAITING × 10 Mio. Augmentez-le pour une application riche en uploads qui doit absorber les rafales de gros corps plutôt que les délester ; abaissez-le sur un conteneur à mémoire plafonnée. 0 = automatique

Dimensionner l'ensemble en attente

Le nombre de requêtes qui peuvent utilement attendre découle de la vitesse à laquelle le pool les écoule. Avec W workers et un gestionnaire qui prend T millisecondes, le pool admet W / T requêtes par milliseconde, donc un budget de B millisecondes peut laisser entrer environ W × B / T requêtes en attente. Tout ce qui dépasse attend le budget entier et est refusé de toute façon, en détenant une connexion et un corps de requête en tampon pendant tout ce temps.

La valeur par défaut ne peut pas calculer cela — le débit de service n'est pas connu au démarrage — elle est donc délibérément généreuse. Cela convient aux gestionnaires rapides, où le pool résorbe un arriéré profond bien avant la fin du budget, et c'est beaucoup trop grand pour les lents : avec 8 workers, le budget par défaut de 1 s et un gestionnaire à 200 ms, seules 40 requêtes environ peuvent être admises à temps alors que la valeur par défaut en stationne jusqu'à 1024.

bash
QUEUE_MAX_WAITING=40

Le régler près de W × B / T transforme le surplus en un 529 immédiat plutôt qu'en un 529 une seconde plus tard. Raccourcir QUEUE_WAIT_TIMEOUT_MS obtient le même résultat par l'autre terme. Les deux s'échangent l'un contre l'autre, et sur un gestionnaire lent, le budget plus court est en général le meilleur levier, car il borne aussi le temps qu'un client attend le refus.

L'ensemble est borné une seconde fois, en octets. Chaque requête en attente garde son corps en mémoire aussi longtemps qu'elle attend, si bien qu'un plafond compté en requêtes laisse au trafic la mémoire qu'elles détiennent : les mêmes 1024 requêtes en attente ne coûtent rien sur des GET sans corps, et des gigaoctets sur des uploads. QUEUE_MAX_WAITING_BYTES borne cette somme directement — au-delà, une requête portant un corps est refusée aussitôt au lieu d'être stationnée, tandis que celles sans corps continuent d'attendre normalement. Une application riche en uploads qui doit absorber les rafales plutôt que les délester veut une valeur plus grande ; un conteneur à limite mémoire stricte en veut une plus petite. Les refus dus à l'un ou l'autre plafond sont comptés séparément dans oxphp_admission_refused_total (waiting_full pour le décompte, waiting_bytes pour la mémoire), si bien que la métrique nomme le levier à actionner.

Garder de la marge pour la boucle d'acceptation

Une requête détient sa connexion — et l'un des permis MAX_CONNECTIONS — du moment où elle arrive jusqu'à sa réponse, quoi qu'elle fasse entre-temps. Cela couvre trois populations distinctes, car une place dans la file est libérée dès qu'un worker prend la requête en charge, avant que le script ne s'exécute :

  • en cours d'exécution dans un worker : au moins PHP_WORKERS, et davantage en mode worker, où un thread multiplexe des fibers ;
  • en file, jusqu'à QUEUE_CAPACITY ;
  • stationnées à l'admission, jusqu'à QUEUE_MAX_WAITING.

Seule la troisième a un plafond dérivé de MAX_CONNECTIONS. Gardez PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING en dessous de MAX_CONNECTIONS. Au-delà, le mode de défaillance change pour le pire : la boucle d'acceptation prend un permis de connexion avant de commencer à servir une connexion, si bien qu'une fois que le chemin PHP les détient tous, elle se met en pause, et un client qui arrive alors n'obtient aucune réponse au lieu d'un 529 — ce qu'un répartiteur de charge ne peut pas distinguer d'une instance morte, tandis que la sonde de santé sur INTERNAL_ADDR reste au vert parce qu'elle ne passe par aucune des deux limites.

Le serveur le vérifie au démarrage et avertit quand la somme atteint le budget, en nommant chaque valeur ; oxphp config --check rapporte la même chose, sans changer son verdict ni son code de sortie. Considérez la disparition de l'avertissement comme nécessaire plutôt que suffisante : les connexions keep-alive inactives, les requêtes de fichiers statiques et les poignées de main en cours détiennent elles aussi des permis, et aucune ne peut être comptée au démarrage.

Abaisser MAX_CONNECTIONS est la manière habituelle de déclencher l'avertissement, car la valeur par défaut de QUEUE_CAPACITY est dimensionnée d'après le nombre de workers et ne le suit pas vers le bas — 7 workers et MAX_CONNECTIONS=1000 donnent 7 + 896 + 500 face à un budget de 1000. Notez quel levier actionner : relever MAX_CONNECTIONS seul ne le fait pas disparaître tant que QUEUE_MAX_WAITING reste à sa valeur par défaut, car cette valeur vaut la moitié de MAX_CONNECTIONS et monte avec lui — à 7 workers, la somme se stabilise à 1799 et la condition ne se dissipe qu'à partir de MAX_CONNECTIONS=1800. Abaissez QUEUE_CAPACITY, ou définissez QUEUE_MAX_WAITING explicitement puis relevez le budget.

Pour un pool dynamique (MIN:MAX), le terme en cours d'exécution est le minimum, le même nombre qui dimensionne les valeurs par défaut de la file, si bien qu'un pool monté à son maximum détient plus que ce que la somme annonce.

La comparaison compte des connexions alors que le budget se dépense en requêtes, si bien qu'un déploiement à dominante HTTP/2 peut légitimement se situer au-dessus : une connexion transporte jusqu'à H2_MAX_CONCURRENT_STREAMS requêtes, le même arriéré est donc détenu par une fraction des connexions. L'avertissement y est attendu. Un grand pool avec le budget d'origine l'atteint aussi — un pool auto-dimensionné de 39 workers donne 39 + 4992 + 4992 face à 10 000 — et celui-là mérite d'être traité plutôt qu'ignoré.

Workers statiques ou dynamiques

Définissez PHP_WORKERS sur un seul nombre pour un pool fixe :

bash
PHP_WORKERS=8 # Fixed 8 workers PHP_WORKERS=0 # Auto-detect: CPU / 2 (min 1)

Définissez PHP_WORKERS sur MIN:MAX pour une mise à l'échelle automatique :

bash
PHP_WORKERS=2:16 # Scale between 2 and 16 workers PHP_WORKERS=4:0 # 4 minimum, auto-detect maximum (CPU × 2) PHP_WORKERS=0:16 # auto-detect minimum (CPU / 4, min 1), 16 maximum

En mode dynamique, OxPHP augmente le nombre de workers lorsqu'ils sont tous occupés et le réduit lorsque des workers sont restés inactifs plus longtemps que PHP_WORKERS_IDLE_SECONDS.

Mode worker

Variable Valeur par défaut Description
WORKER_MAX_MEMORY_MIB 0 Mémoire maximale en MiB par worker avant recyclage. 0 = illimité

Définissez WORKER_MODE_ENABLED=true et faites pointer ENTRY_FILE vers votre script d'amorçage de worker (par exemple ENTRY_FILE=worker.php ou ENTRY_FILE=../worker.php). Les processus PHP restent alors en vie d'une requête à l'autre, en gardant en mémoire l'état d'amorçage (autoloaders, connexions à la base de données). Les workers sont recyclés automatiquement lorsqu'ils dépassent WORKER_MAX_MEMORY_MIB, ou à la demande lorsque l'application appelle Worker::scheduleExit(). Le réglage WORKER_MAX_REQUESTS des versions antérieures est déprécié et ignoré — ne définissez ni l'un ni l'autre, ou migrez vers Worker::scheduleExit().

Déprécié : INDEX_FILE et WORKER_FILE

Les variables héritées INDEX_FILE et WORKER_FILE sont toujours analysées à des fins de rétrocompatibilité. Lorsqu'elles sont définies, elles émettent une ligne de journal WARN au démarrage et se traduisent dans le nouveau modèle :

Héritée Équivalent actuel
INDEX_FILE=index.php ENTRY_FILE=index.php
INDEX_FILE=index.html ENTRY_FILE=index.html
WORKER_FILE=/path/worker.php WORKER_MODE_ENABLED=true ENTRY_FILE=/path/worker.php

Si l'ancienne et la nouvelle sont toutes deux définies, ENTRY_FILE / WORKER_MODE_ENABLED l'emportent. Migrez à votre convenance ; les formes dépréciées seront supprimées dans une version future.

SAPI / PHP

Variable Valeur par défaut Description
SUPERGLOBALS_ENABLED true Renseigne les superglobales PHP ($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER, php://input) avant l'exécution du script. Définissez sur une valeur fausse pour ignorer ce remplissage — les données de la requête ne sont alors accessibles que via l'API objet (oxphp_http_request()). Utile pour les applications qui consomment directement l'API objet et veulent éviter le coût de construction des superglobales à chaque requête

Délais d'expiration

Variable Valeur par défaut Description
HEADER_TIMEOUT_SECONDS 5 Nombre maximal de secondes pour recevoir les en-têtes HTTP après la connexion (protection contre Slowloris)
DRAIN_TIMEOUT_SECONDS 25 Nombre maximal de secondes d'attente des connexions en cours lors d'un arrêt gracieux

Le temps d'exécution PHP est borné par la directive ini max_execution_time de PHP (et set_time_limit() à l'exécution), et non par une variable d'environnement OxPHP.

Limitation de débit

Variable Valeur par défaut Description
RATE_LIMIT 0 (désactivé) Nombre maximal de requêtes par IP et par fenêtre de temps. 0 désactive la limitation de débit
RATE_WINDOW_SECONDS 60 Durée de la fenêtre de limitation de débit en secondes

Sécurité

Variable Valeur par défaut Description
FRAME_OPTIONS SAMEORIGIN Protection contre le clickjacking. SAMEORIGIN n'autorise le framing que par des pages de la même origine, DENY bloque tout affichage en cadre (framing), off le désactive (à utiliser lorsque vous gérez le framing via votre propre CSP). Toute autre valeur retombe sur la valeur par défaut SAMEORIGIN avec un avertissement au démarrage. Définit à la fois X-Frame-Options et Content-Security-Policy: frame-ancestors sur chaque réponse. Voir Protection contre le clickjacking ci-dessous pour les valeurs d'en-tête émises, la manière dont les en-têtes serveur s'effacent devant ceux définis par l'application, et des conseils pour choisir une valeur
TRUSTED_PROXIES (non défini) Réseaux de proxys inverses de confiance (CIDR séparés par des virgules ou private). Lorsqu'elle est définie, OxPHP extrait la véritable IP du client à partir des en-têtes Forwarded (RFC 7239) ou X-Forwarded-For en appliquant l'algorithme rightmost-non-trusted (le nœud non fiable le plus à droite). Traite également X-Forwarded-Proto et X-Forwarded-Host pour $_SERVER['HTTPS'], REQUEST_SCHEME, SERVER_NAME et SERVER_PORT. Non défini = fonctionnalité désactivée
PHP_DENY_PATHS (non défini) Motifs glob séparés par des virgules dont les fichiers .php ne doivent jamais s'exécuter via un URI direct (par exemple /uploads/**,/cache/**,/admin/legacy.php). Les motifs peuvent cibler des répertoires entiers ou des fichiers isolés. S'applique dans les modes de mappage direct — Traditionnel et SPA ; ignoré avec un avertissement au démarrage dans les modes Framework et Worker, qui n'exécutent jamais directement des fichiers .php arbitraires. Couvre aussi les scripts atteints via la résolution d'index de répertoire (/uploads/uploads/index.php). Pour les URI .php directs, la correspondance a lieu avant toute E/S disque, si bien que les chemins refusés produisent la même réponse que le fichier existe ou non (pas d'oracle d'existence). L'ancien nom PHP_DENY_DIRS est accepté comme alias déprécié et émet un WARN au démarrage. Voir Liste de refus d'exécution PHP
PHP_DENY_FALLBACK 404 Ce qui est renvoyé en cas de correspondance avec PHP_DENY_PATHS. Soit un statut HTTP 400599 (associé à ERROR_PAGES_DIR pour un HTML personnalisé), soit un chemin d'URI préfixé par / vers un script PHP de repli situé dans DOCUMENT_ROOT. Le script de repli reçoit OXPHP_DENIED_PATH et OXPHP_DENIED_PATTERN dans $_SERVER. Validé au démarrage : le script doit exister, se canonicaliser à l'intérieur de DOCUMENT_ROOT, et ne doit pas lui-même correspondre à PHP_DENY_PATHS (prévention des boucles)
SYMLINK_ALLOW_PATHS (non défini) Liste, séparée par des virgules, de chemins absolus sous lesquels les liens symboliques sont autorisés à sortir de DOCUMENT_ROOT. Chaque entrée doit déjà exister sur le disque ; les chemins relatifs et les chemins manquants interrompent le démarrage. Non défini = aucune sortie par lien symbolique autorisée. Voir Liste d'autorisation de liens symboliques

La valeur spéciale private se développe en tous les réseaux privés RFC-1918, l'adresse de bouclage (loopback) et les adresses link-local (IPv4 et IPv6) : 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16, ::1/128, fc00::/7, fe80::/10.

Protection contre le clickjacking

Le clickjacking est une attaque où une page hostile intègre votre site dans une <iframe> invisible et pousse l'utilisateur à cliquer sur quelque chose qu'il ne voit pas : un bouton « oui, supprimer mon compte », un achat en un clic, une invite « autoriser » OAuth. La défense consiste à dire au navigateur qui, le cas échéant, a le droit d'encadrer vos pages. FRAME_OPTIONS contrôle cela.

Deux en-têtes gouvernent le framing — l'historique X-Frame-Options (compris par tous les navigateurs) et le moderne Content-Security-Policy: frame-ancestors (qui le supplante là où les deux sont présents) — OxPHP émet donc les deux, et la politique tient sur les navigateurs anciens comme récents. Une seule valeur de FRAME_OPTIONS correspond à une paire assortie :

FRAME_OPTIONS X-Frame-Options Content-Security-Policy Qui peut encadrer vos pages
SAMEORIGIN (par défaut) SAMEORIGIN frame-ancestors 'self' Uniquement les pages de la même origine
DENY DENY frame-ancestors 'none' Personne, pas même vos propres pages
off (non envoyé) (non envoyé) N'importe qui — aucune restriction de framing côté serveur

Choisir une valeur. SAMEORIGIN est la valeur par défaut : elle bloque le framing inter-origines sur lequel le clickjacking repose réellement, tout en laissant vos propres pages s'intégrer les unes dans les autres — ce que beaucoup d'applications font légitimement (aperçus d'admin, widgets de tableau de bord, composants de paiement hébergés sur la même origine). Choisissez DENY quand rien sur votre site n'est jamais censé être encadré, pas même par lui-même, pour la posture la plus stricte. Choisissez off uniquement quand vous gérez le framing vous-même via une Content-Security-Policy complète que votre application définit — voir ci-dessous.

Encadrer depuis des origines externes. Aucune valeur de X-Frame-Options ne peut nommer une origine autorisée précise (ALLOW-FROM a été retiré du standard). Pour permettre à un tiers nommé d'encadrer vos pages, définissez FRAME_OPTIONS=off et faites émettre à votre application sa propre Content-Security-Policy avec une liste frame-ancestors explicite, par exemple header("Content-Security-Policy: frame-ancestors 'self' https://partner.example.com");.

Les en-têtes applicatifs l'emportent. Les en-têtes serveur sont des replis, appliqués uniquement quand la réponse ne porte pas déjà un tel en-tête : une application qui définit son propre X-Frame-Options ou sa propre Content-Security-Policy via header() en PHP les conserve intacts. Les deux en-têtes de framing sont traités comme une seule politique, si bien que le serveur ne contredit jamais l'application :

  • Si votre application définit X-Frame-Options, OxPHP omet son repli frame-ancestors (une CSP serveur écraserait le choix de votre application dans les navigateurs modernes).
  • Si votre application définit une Content-Security-Policy contenant une directive frame-ancestors, OxPHP omet son repli X-Frame-Options (un X-Frame-Options serveur plus strict sur-bloquerait dans les navigateurs anciens qui ignorent la CSP).

La même priorité s'applique à X-Content-Type-Options, qu'OxPHP définit à nosniff sur chaque réponse : une valeur définie par l'application est conservée telle quelle. Notez que nosniff est la seule valeur qui fasse quelque chose — une application qui la remplace par autre chose désactive silencieusement la protection contre le MIME-sniffing.

TLS

Variable Valeur par défaut Description
TLS_CERT (non défini) Chemin vers le certificat TLS encodé en PEM. TLS_CERT et TLS_KEY doivent tous deux être définis pour activer TLS
TLS_KEY (non défini) Chemin vers la clé privée TLS encodée en PEM
TLS_MIN_VERSION 1.2 Version minimale acceptée du protocole TLS : 1.2 ou 1.3. Validée au démarrage (et par oxphp config --check) même lorsque TLS n'est pas activé — toute autre valeur, y compris des octets non-UTF-8, constitue une erreur de démarrage fatale. Une valeur vide est traitée comme non définie

HTTP/2

Variable Valeur par défaut Description
H2_MAX_CONCURRENT_STREAMS PHP_WORKERS_MAX × 4 (min. 32) Nombre maximal de flux ouverts simultanément par connexion HTTP/2
H2_MAX_PENDING_RESET 20 Nombre maximal de trames RST_STREAM en file d'attente avant fermeture d'une connexion (protection contre Rapid Reset)
H2_MAX_HEADER_LIST_BYTES 65536 Nombre maximal total d'octets d'en-tête décodés par requête
H2_KEEPALIVE_INTERVAL_SECS 20 Nombre de secondes entre les trames PING HTTP/2 ; 0 désactive
H2_KEEPALIVE_TIMEOUT_SECS 10 Nombre de secondes d'attente d'une réponse PING avant de fermer la connexion

Fichiers statiques

Variable Valeur par défaut Description
STATIC_MAX_AGE 30d Cache-Control: max-age pour les fichiers statiques. Accepte : 30s, 5m, 2h, 30d, 1w, 1y, un nombre de secondes brut (3600), ou off pour désactiver l'en-tête. Remplace STATIC_CACHE_TTL, déprécié.
STATIC_REVALIDATE off Booléen — voir Valeurs booléennes. Définissez sur une valeur vraie pour activer la revalidation par mtime du cache de contenu en mémoire : la date de modification du fichier est revérifiée au plus une fois toutes les 3 secondes par fichier (pas par requête) et les entrées périmées sont évincées automatiquement, si bien que les changements deviennent visibles en moins de 3 secondes. Remplace STATIC_CACHE, déprécié (où off avait le sens inverse).
COMPRESSION_LEVEL 4 Qualité de compression Brotli (0–11). 0 désactive la compression

Journalisation

Variable Valeur par défaut Description
LOG_LEVEL info Verbosité des journaux : trace, debug, info, warn, error
ACCESS_LOG (non défini) Journal d'accès par requête : all = chaque requête, error = 4xx/5xx uniquement, non défini = désactivé
Note

ACCESS_LOG accepte all ou error. Laissez la variable non définie pour désactiver totalement la journalisation des accès.

Observabilité

Variable Valeur par défaut Description
INTERNAL_ADDR (non défini) Adresse du serveur interne (/health, /metrics, /config). Le serveur interne n'est pas démarré lorsque cette variable n'est pas définie. Une valeur ne comportant qu'un port (:9090 ou 9090) se lie à 127.0.0.1 ; liez explicitement 0.0.0.0:9090 pour l'exposer hors de l'hôte
INTERNAL_ALLOW_IPS (non défini) Liste d'autorisation CIDR/IP, séparée par des virgules, pour le serveur interne. Un pair extérieur à la liste reçoit 403 sur /metrics, /config et les chemins de plugins ; les sondes de santé (/health, /healthz, /readyz, /startupz et leurs formes longues) restent accessibles. Non défini/vide = tout est autorisé. Le bouclage (loopback) n'est pas implicite — listez 127.0.0.1/32 pour conserver l'accès depuis localhost. Une liste mal formée interrompt le démarrage
ERROR_PAGES_DIR (non défini) Répertoire contenant les pages d'erreur personnalisées nommées {status}.html (par exemple 404.html, 503.html)
MAX_QUERY_BODY 524288 Taille maximale du corps de requête, en octets, pour les endpoints de requête internes (512 KiB)
TRACE_CONTEXT false Booléen — voir Valeurs booléennes. Lorsque la valeur est vraie, active la propagation du W3C Trace Context : lit les en-têtes traceparent/tracestate et les transmet à PHP via $_SERVER

OpenTelemetry

Variable Valeur par défaut Description
OTEL_ENABLED false Active l'export de spans OpenTelemetry. Définit automatiquement TRACE_CONTEXT=true. Booléen — voir Valeurs booléennes
OTEL_EXPORTER_OTLP_PROTOCOL grpc Protocole d'export : grpc ou http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT http://localhost:4317 (gRPC) ou http://localhost:4318 (HTTP) Endpoint du collecteur OTLP
OTEL_EXPORTER_OTLP_TIMEOUT 10000 Délai d'expiration de l'export en millisecondes
OTEL_EXPORTER_OTLP_HEADERS (non défini) En-têtes d'authentification : key=value,key2=value2
OTEL_SERVICE_NAME oxphp Nom du service dans les spans exportés
OTEL_SERVICE_VERSION (non défini) Attribut de version du service
OTEL_RESOURCE_ATTRIBUTES (non défini) Attributs de ressource supplémentaires : env=prod,region=us-east-1
OTEL_TRACES_SAMPLER parentbased_traceidratio Stratégie d'échantillonnage : always_on, always_off, traceidratio, parentbased_always_on, parentbased_always_off, parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG 1.0 Taux d'échantillonnage (0.0–1.0) pour les échantillonneurs basés sur un ratio
Note

Les valeurs OTEL_TRACES_SAMPLER_ARG invalides ou hors plage sont ramenées à [0.0, 1.0] et journalisées au niveau warn. Les valeurs OTEL_TRACES_SAMPLER inconnues retombent sur parentbased_traceidratio et sont journalisées.

APM

Variable Valeur par défaut Description
OTEL_APM_ENABLED false Active l'APM : instrumentation automatique, capture des erreurs et SDK de traçage PHP. Nécessite OTEL_ENABLED=true. Booléen — voir Valeurs booléennes
OTEL_APM_SLOW_QUERY_MS 100 Seuil de requête lente en millisecondes. Les requêtes de base de données qui le dépassent reçoivent un attribut de span oxphp.db.slow=true
OTEL_APM_DB_CAPTURE_PARAMS_ENABLED false Enregistre les paramètres liés (bind) dans l'attribut de span db.params. À désactiver en production si les paramètres peuvent contenir des données sensibles. Booléen — voir Valeurs booléennes
OTEL_APM_STACKTRACE_MAX_BYTES 8192 Taille maximale, en octets, de l'attribut exception.stacktrace. Au-delà du plafond, la trace d'appels est tronquée par la fin avec un marqueur …(truncated). 0 désactive la troncature
OTEL_APM_MESSAGE_MAX_BYTES 4096 Taille maximale, en octets, de l'attribut exception.message (la valeur par défaut correspond à la limite de valeur par attribut de New Relic). Au-delà du plafond, le message est tronqué par la fin avec un marqueur …(truncated). 0 désactive la troncature

Lorsque l'APM est activé, OxPHP intercepte automatiquement 34 fonctions PHP internes (PDO, mysqli, cURL, Redis, Memcached, E/S sur fichiers) pour créer des spans enfants. Les fonctions PHP oxphp_apm_*() sont enregistrées que l'APM soit activé ou non — lorsqu'il est désactivé, ce sont des no-ops sans effet.

Workers asynchrones

Variable Valeur par défaut Description
ASYNC_WORKERS 0 (désactivé) Nombre de threads de workers asynchrones dédiés. Lorsque la valeur est 0, les fonctions asynchrones (oxphp_async, etc.) sont enregistrées mais lèvent OxPHP\Async\AsyncException à l'appel. Définissez une valeur positive pour activer l'exécution de tâches en arrière-plan
ASYNC_QUEUE_CAPACITY ASYNC_WORKERS × 64 Nombre maximal de tâches en attente dans la file asynchrone. 0 = automatique (workers × 64)
ASYNC_MAX_FIBERS 256 Plafond, par worker, du nombre de Fibers de tâches asynchrones concurrentes. La limite globale du processus (en attente + en cours d'exécution) est ASYNC_MAX_FIBERS × ASYNC_WORKERS ; un envoi au-delà est rejeté immédiatement avec OxPHP\Async\AsyncException, si bien qu'une composition en fan-out ne peut pas provoquer d'interblocage

Le pool de workers asynchrones traite les tâches d'arrière-plan de type « fire-and-forget » envoyées depuis PHP. Il est distinct du pool de workers PHP et n'est pas nécessaire au traitement standard des requêtes.

Une valeur mal formée dans l'une de ces trois variables (par exemple ASYNC_WORKERS=8x) constitue une erreur de démarrage — retomber sur une valeur par défaut désactiverait ou mal configurerait silencieusement le pool. Une valeur exactement vide est traitée comme non définie.

Hooks d'exécution

Variable Valeur par défaut Description
RUNTIME_HOOKS (désactivé) Remplacement opt-in de fonctions natives PHP bloquantes par des implémentations qui suspendent la Fiber. 1/true/all active toutes les catégories de hooks ; une liste séparée par des virgules active des catégories précises (par exemple RUNTIME_HOOKS=sleep,streams)
Catégorie Ce qu'elle intercepte
sleep Les fonctions natives sleep() et usleep() suspendent la Fiber courante exactement comme oxphp_sleep()/oxphp_usleep()
streams Deux attentes suspendent la Fiber courante au lieu d'épingler le thread worker : une lecture bloquante sur un flux socket tcp://, et stream_select(). La lecture couvre les clients qui bloquent sur un seul socket — fsockopen(), stream_socket_client(), les wrappers de flux HTTP, mysqlnd (PDO_MySQL, mysqli), phpredis ; stream_select() couvre les boucles qui attendent sur plusieurs sockets à la fois. Aucune modification de code dans les deux cas. Les clients qui attendent d'une autre manière ne sont pas concernés (voir ci-dessous)

Les hooks prennent effet dans les Fibers de requête du mode worker et dans les Fibers de tâches asynchrones. Hors d'une Fiber (contexte de requête traditionnel/framework/SPA, CLI), le comportement natif d'origine est préservé, y compris les erreurs de validation d'arguments. Avec les hooks sleep activés, du code tiers appelant sleep() cesse d'épingler le thread worker — aucune modification de code requise. Annuler une tâche asynchrone pendant un sleep intercepté la déroule avec OxPHP\Async\AsyncException, et un sleep() intercepté renvoie toujours 0 (la valeur de retour d'interruption par signal de la fonction native ne survient pas).

Ce que couvre le hook streams

Ce que le hook streams rend coopératif, c'est une lecture bloquante sur un flux socket PHP et une attente dans stream_select(). Avant de compter dessus, vérifiez que votre client attend de l'une de ces deux manières. Plusieurs clients courants ne le font pas, et pour eux rien ne change :

  • ext/curl. curl_exec() et curl_multi_* parlent aux sockets eux-mêmes, sous les flux PHP, si bien que le hook ne les voit jamais. Cela couvre tous les clients HTTP bâtis sur curl — Guzzle avec son handler par défaut en fait partie. Un client curl peut être pointé vers le handler à wrappers de flux, qui, lui, passe par les flux PHP.
  • socket_select(). C'est ext/sockets, une API différente sur des descripteurs bruts, et elle n'est pas interceptée. stream_select() l'est. Il en va de même pour l'attente dans stream_socket_accept(), qui n'est pas interceptée non plus.
  • Les flux issus de socket_export_stream(). Ils portent une table d'opérations différente, privée à PHP, qui ne peut pas être modifiée depuis une extension.
  • Les flux unix://, udp:// et udg://. Même raison : leurs tables d'opérations sont privées à PHP.
  • ssl:// et tls:// une fois le chiffrement actif. Avant que stream_socket_enable_crypto() ne réussisse, les lectures d'un flux SSL sont déléguées à la lecture du socket en clair et suspendent donc bien ; à partir de la poignée de main, elles ne suspendent plus.
  • L'établissement de connexion et la résolution DNS. Un client qui garde sa connexion ouverte en bénéficie à chaque requête ; l'établissement de la connexion, non.
  • MySQL atteint via localhost. Le client MySQL lit localhost comme « utiliser le socket unix », qui n'est pas un flux tcp:// — écrivez plutôt 127.0.0.1 dans le DSN. Cela vaut pour PDO_MySQL comme pour mysqli, et c'est facile à manquer parce que tout continue de fonctionner, simplement sans le bénéfice.
  • Les écritures. Seules les lectures attendent. La disponibilité en lecture reste acquise tant qu'une Fiber possède le descripteur et que rien d'autre ne le vide, alors que la place dans le tampon d'envoi est accordée et retirée par le pair : une Fiber réveillée sur la disponibilité en écriture peut trouver la fenêtre refermée au moment où l'écriture s'exécute, après quoi PHP bloque de toute façon le thread pour son délai complet. Une écriture qui remplit le tampon du socket se comporte donc exactement comme sans le hook. En pratique cela coûte peu : c'est attendre une réponse qui prend du temps, pas remettre une requête au noyau. (Les écritures sont tout de même examinées, pour une seule chose : savoir quel échange de quelle Fiber la connexion transporte en ce moment — voir la note sur les connexions partagées plus bas. Sur une connexion qu'aucune autre Fiber n'utilise, c'est-à-dire toutes les connexions dans le cas ordinaire, l'écriture suit le chemin natif intact.)

Dans ce périmètre, le hook conserve le contrat natif : les délais de socket (stream_set_timeout(), default_socket_timeout) s'appliquent inchangés, une lecture expirée rapporte toujours timed_out via stream_get_meta_data(), et l'identité du flux est intacte, si bien que socket_import_stream() et consorts continuent de fonctionner. Une limite sur le délai : l'échéance n'est examinée qu'une fois par tick de l'ordonnanceur, elle ne se déclenche donc pas avant le tick suivant — au mieux 100 µs en mode worker et 50 µs dans le pool asynchrone, et davantage sous charge, puisqu'un tick dure aussi longtemps que ce que le worker exécute. Le pool asynchrone fait des pauses plus longues que cela quand il ne se passe rien, mais jamais au-delà d'une échéance qu'il détient : une échéance de lecture ou d'écriture raccourcit la pause pour tomber sur elle-même, si bien qu'attendre plus longtemps ne retarde pas le délai. Avec default_socket_timeout à 60 secondes, ce n'est pas quelque chose que la plupart des déploiements peuvent observer.

stream_select() est intercepté en attendant sur les descripteurs que ses trois tableaux nomment, puis en remettant l'appel à PHP avec le délai mis à zéro, si bien que PHP décide encore de tout ce que vous pouvez observer : le nombre renvoyé, la réécriture des tableaux réduits aux flux prêts, les avertissements et les erreurs d'argument. L'attente est sautée — et l'appel s'exécute exactement comme sans le hook — dès que les tableaux contiennent quelque chose que le hook ne remplacera pas : un flux en lecture avec des données déjà en tampon (auquel stream_select() répond depuis le tampon, sans regarder un descripteur), un flux sans descripteur du tout, un élément qui n'est pas un flux vivant (un flux fermé laissé dans le tableau, par exemple — PHP y répond par une erreur, pas par une attente), un descripteur dont le noyau ne surveillera jamais la disponibilité (un fichier ordinaire est le cas usuel, et il compte comme prêt dès qu'on le demande), ou un descripteur égal ou supérieur à FD_SETSIZE, que le select() de PHP refuse d'emblée. Ce dernier cas est un vrai plafond plutôt qu'une formalité : un worker occupé peut détenir plus de 1024 descripteurs ouverts, et un stream_select() qui en nomme un échoue de la même façon avec ou sans le hook.

Connexions partagées entre Fibers concurrentes

Une connexion partagée entre Fibers concurrentes est sûre pour les clients qu'OxPHP protège, et n'y gagne rien. C'est la forme normale d'une application en mode worker plutôt qu'un cas limite : WordPress, Laravel et Symfony ouvrent leurs clients de base de données et de cache une fois à l'amorçage du worker et confient les mêmes à chaque requête, et à moins de réécrire leur couche d'accès aux données, on ne peut pas leur demander une connexion par Fiber.

Un protocole client est une suite d'échanges — écrire une commande, lire la réponse — sans que rien sur la connexion ne marque où l'un se termine. Une Fiber stationnée sur une lecture est stationnée au milieu d'un échange, et la commande d'une seconde Fiber qui y atterrit casse le protocole. Les deux clients échouent différemment sur ce point, et aucun ne le fait bien : mysqlnd suit l'état de sa connexion et refuse la commande avant d'envoyer quoi que ce soit, tandis que phpredis n'a pas un tel contrôle et les deux Fibers lisent les réponses l'une de l'autre — les données d'une requête renvoyées à une autre sans qu'aucune erreur soit levée.

Une Fiber revendique donc une connexion avant de s'en servir, aux deux niveaux où cela doit se faire : les opérations de socket, qui gardent les octets en ordre, et les points d'entrée client de PDO et mysqli, puisque le refus de mysqlnd survient avant toute E/S et que rien au niveau du socket ne peut être atteint à temps pour l'empêcher. Une autre Fiber qui atteint la même connexion attend qu'elle soit rendue au niveau client, où rien d'autre que l'identité de la connexion n'est détenu ; au niveau du socket, elle n'attend pas mais est refusée, comme l'est un délai de socket, parce qu'une Fiber suspendue dans une opération sur le flux de quelqu'un d'autre détiendrait un pointeur que son propriétaire peut libérer. phpredis est protégé au niveau client aussi, méthode par méthode, exactement pour cette raison. Ce que le niveau client revendique est la connexion elle-même et non l'objet PHP qui la détient, si bien qu'une connexion persistante atteinte via plusieurs objets PDO compte pour une seule, et qu'une connexion ouverte avec PDO::connect() — qui renvoie la sous-classe propre au pilote plutôt qu'un PDO — est couverte comme n'importe quelle autre.

Lisez donc le gain du hook comme appartenant aux connexions qu'une Fiber ouvre pour elle-même : une tâche asynchrone faisant ses propres appels HTTP ou base de données, une requête ouvrant son propre client. Ce qu'une connexion partagée obtient, c'est le thread worker rendu pendant qu'elle attend, pour que d'autres requêtes exécutent le travail qui n'est pas sur cette connexion ; ses propres échanges s'exécutent l'un après l'autre, exactement comme avec le hook désactivé. Quatre limites méritent d'être connues :

  1. Une Fiber qui a interrogé une fois garde la connexion jusqu'à la fin de sa requête, parce que la fin de la requête est le premier moment certainement postérieur à la fin d'un échange. Elle la garde aussi pendant qu'elle est stationnée sur autre chose, si bien qu'une requête qui a interrogé puis attend un travail à elle qui a besoin de la même connexion s'attend elle-même ; les deux se séparent sur la borne ci-dessous au lieu d'avancer.
  2. L'attente est toujours bornée : par le plus petit de max_execution_time et default_socket_timeout. Ne définissez ni l'un ni l'autre et cette borne est de 30 secondes, puisqu'une SAPI serveur prend les valeurs par défaut du moteur — 30 pour le premier et 60 pour le second ; elle vaut les 60 de default_socket_timeout uniquement là où max_execution_time est à 0. max_execution_time est lu tel que la requête l'a en ce moment, puisque set_time_limit() est la manière dont une requête déclare combien de temps elle peut s'exécuter ; default_socket_timeout est lu tel qu'au démarrage du processus, parce que c'est l'échéance par défaut d'une opération de socket, et une requête qui le resserre pour un appel à elle — chose courante pour une bibliothèque, et qu'elle laisse souvent derrière elle — ne doit pas raccourcir cette borne pour les requêtes qui la suivent sur le même worker. Passé la borne, l'appel retombe sur le comportement non protégé et en donne la raison dans le log du serveur : pour PDO et mysqli, il est remis au client, dont le propre refus d'une commande émise au milieu d'un échange est l'erreur que l'application gère déjà, tandis que pour phpredis, qui n'a pas un tel refus et lirait à la place la réponse de quelqu'un d'autre, il lève une RedisException et n'envoie rien. Un conflit au niveau du socket n'a pas de borne propre parce qu'il n'attend jamais : l'opération échoue aussitôt comme le fait un délai expiré, si bien que stream_get_meta_data() rapporte timed_out. Deux Fibers détenant chacune ce que l'autre attend se séparent donc sur cette borne au lieu de s'attendre l'une l'autre pour de bon.
  3. Certains cas sont délibérément laissés sans couverture. Un objet statement ou résultat conservé d'une requête à l'autre, qu'aucun appel revendiqué ne précède : PDOStatement::execute() sur un statement préparé dans une requête antérieure se comporte comme sans revendication du tout. Construire un second handle sur une connexion persistante pendant qu'une autre Fiber y est au milieu d'un échange : PDO vérifie qu'une connexion du pool est vivante avant de la remettre, cette vérification échoue au milieu d'un échange, et PDO répond en abandonnant la connexion. Un protocole écrit à la main sur un socket brut — écrire une commande, se suspendre sur autre chose, lire la réponse plus tard — puisque le niveau socket ne refuse que pendant que le détenteur est stationné sur la réponse elle-même, et qu'entre ces deux points une autre Fiber s'empare de la connexion ; les trois clients ci-dessus sont couverts ici par leur revendication au niveau client, dont un protocole écrit à la main n'a pas d'équivalent. Et tout ce qui atteint une connexion complètement hors d'une Fiber, comme un destructeur exécuté par le collecteur de cycles du moteur entre les requêtes, qu'aucune revendication ne peut voir.
  4. Fermer une connexion partagée pendant qu'une autre requête est stationnée à la lire met fin à cette requête, avec un 500 et une ligne de log nommant ce qui s'est passé. La revendication tient deux échanges à l'écart l'un de l'autre ; elle ne peut pas garder une connexion en vie, et la réponse de la requête stationnée se trouve sur une connexion qui n'existe plus — la réponse honnête est donc d'y mettre fin plutôt que de rendre ce que la mémoire libérée contient désormais. Les appels capables de cela dépendent du client, et un seul d'entre eux attend : mysqli::close() et mysqli_close() sont des appels revendiqués, ils attendent donc le détenteur et ferment une fois cette attente épuisée, tandis que Redis::close() est revendiqué aussi mais lève une RedisException au lieu de fermer. Tout le reste ferme aussitôt — fclose() sur un flux brut, et, surtout, PDO, qui n'a pas de méthode close() du tout : une connexion est libérée en abandonnant la dernière référence au handle (unset($pdo), le réassigner, ou le laisser sortir de portée), et ce chemin s'exécute dans le démontage d'objets du moteur lui-même, où aucune revendication n'est consultée. L'assistant de reconnexion qui réassigne un handle PDO partagé met donc fin aux requêtes stationnées sur l'ancienne connexion, sans l'attente bornée qu'obtient son équivalent mysqli. La requête est prévenue dès que la fermeture a lieu plutôt qu'à sa propre échéance de lecture, qui pour mysqlnd serait mysqlnd.net_read_timeout — un jour entier par défaut. Les applications qui ferment une connexion partagée pour forcer une reconnexion (le $wpdb->check_connection() de WordPress est le cas usuel) doivent s'attendre à ce que les requêtes qui y sont stationnées à ce moment-là échouent plutôt qu'elles ne renvoient de mauvaises données.

Une dernière limite : faire tourner le hook sous un ordonnanceur de Fibers en espace utilisateur (AMPHP, Revolt) retombe sur des E/S bloquantes. Une Fiber démarrée par un tel ordonnanceur s'exécute sur son propre contexte, que l'ordonnanceur d'OxPHP ne peut pas reprendre, si bien que le hook le détecte et prend le chemin natif plutôt que de corrompre l'un ou l'autre ordonnanceur. Il s'agit là d'une Fiber qu'un ordonnanceur en espace utilisateur démarre à l'intérieur d'une requête. La Fiber propre de la requête est pilotée par OxPHP, et en mode worker c'est une vraie FiberFiber::getCurrent() dans une requête la renvoie — si bien que les bibliothèques qui n'ont besoin que de distinguer les requêtes concurrentes fonctionnent sans aucun repli. C'est le même fait qui explique pourquoi Revolt refuse d'exécuter sa boucle d'événements depuis l'intérieur d'une requête en mode worker.

Activation et coût

1, true et all activent toutes les catégories, streams comprise. Un déploiement qui définit déjà RUNTIME_HOOKS=1 pour les hooks sleep se met donc à intercepter les sockets à la mise à niveau sans la moindre modification de sa part ; listez les catégories explicitement (RUNTIME_HOOKS=sleep) si ce n'est pas ce que vous voulez. Activer streams modifie aussi en mémoire, au démarrage, une entrée de la table d'opérations de flux de PHP ; la page est remise comme elle a été trouvée, mais sur une plateforme où sa protection d'origine ne peut pas être déterminée, elle reste inscriptible — une petite perte de durcissement, signalée dans le log du serveur.

Le coût, mesuré : environ 3–5 µs par aller-retour de socket sur un worker sans rien d'autre de stationné, environ 5–6 µs avec 64 Fibers stationnées sur des descripteurs, et environ 7–11 µs avec 200. La disponibilité est résolue via un ensemble que le noyau conserve entre les attentes, si bien que ce que le chiffre suit est le nombre de descripteurs devenus prêts, pas le nombre en attente. Un worker inactif attend sur ces descripteurs au lieu de dormir un intervalle fixe et de remarquer la disponibilité à son tick suivant, ce qui compte énormément : dormir à l'aveugle coûtait environ 2 ms par aller-retour à la place.

Un stream_select() large porte un surcoût que le cas étroit n'a pas, parce que chaque descripteur nommé par un appel est enregistré avant l'attente et retiré après. Mesuré en retirant l'attente elle-même — chaque descripteur déjà lisible, si bien que l'appel revient aussitôt et qu'il ne reste que le surcoût — cela donne environ 6 µs contre 5 µs sans hook à un descripteur, 56 µs contre 9 µs à 64, et 130–150 µs contre 16–21 µs à 200 : environ 0,65 µs par descripteur.

Lisez cela comme un coût fixe par appel, pas comme un ralentissement du même travail. Un stream_select() qui attend réellement — ce pour quoi une boucle de select est écrite — l'éclipse : une milliseconde d'attente ramène même le chiffre à 200 descripteurs à environ un dixième de l'appel, et ce qu'il achète est le thread worker, que l'appel non intercepté détient pendant toute l'attente. Deux formes font exception, et pour elles la catégorie est mieux laissée désactivée : un appel sur de nombreux descripteurs qui n'attend presque jamais, parce qu'il n'y a pas de temps de thread à récupérer ; et une requête qui est la boucle d'événements, où le thread n'a de toute façon rien d'autre à exécuter. Les clients qui attendent sur une seule connexion — mysqlnd, phpredis, les wrappers de flux HTTP — se situent en haut du tableau, où le surcoût est de l'ordre de la microseconde.

RUNTIME_HOOKS et oxphp_sleep()

Le hook ne remplace pas la primitive maison — ils couvrent des cas différents :

  • Choisissez oxphp_sleep() / oxphp_usleep() dans le code que vous écrivez. Ils suspendent la Fiber en mode worker inconditionnellement, sans indicateur d'environnement, et oxphp_sleep() accepte des secondes fractionnaires (oxphp_sleep(0.25)) — une précision que le sleep() natif (secondes entières) ne peut pas exprimer.
  • Activez RUNTIME_HOOKS=sleep pour le code que vous ne pouvez pas modifier — un framework ou une bibliothèque tierce qui appelle directement les sleep()/usleep() natifs. Il est désactivé par défaut, ne s'applique qu'à l'intérieur d'une Fiber et conserve le contrat natif (un sleep() intercepté prend toujours un int et renvoie 0).

Compter sur RUNTIME_HOOKS pour vos propres gestionnaires attache leur coopérativité à un réglage de déploiement plutôt qu'au code ; préférez-y le oxphp_sleep() explicite.

État partagé

Primitives de concurrence intra-processus (OxPHP\Shared\Counter, Map, Channel, Mutex, Once, Pool, Atomic, Flag, Registry). Voir État partagé pour un tour d'horizon de l'API.

Variable Valeur par défaut Description
SHARED_ENABLED true Booléen — voir Valeurs booléennes. Interrupteur principal de l'ensemble du sous-système OxPHP\Shared\*
SHARED_MAX_ENTRIES 100000 Plafond global sur l'ensemble des entrées Shared combinées. Une insertion au-delà échoue avec CapacityException
SHARED_MAX_BYTES 1073741824 (1 GiB) Plafond global sur la mémoire estimée pour l'ensemble des entrées Shared
SHARED_SOFT_LIMIT_RATIO 0.7 Commence à délester le travail de plus faible priorité lorsque l'utilisation franchit cette fraction de SHARED_MAX_BYTES / SHARED_MAX_ENTRIES
SHARED_METRICS_ENABLED true Booléen. Active/désactive l'exposition Prometheus oxphp_shared_*
SHARED_INTROSPECTION_ENABLED true Booléen. Active/désactive l'API d'introspection /__ox_shared/* sur le serveur interne
SHARED_INTROSPECTION_PREVIEW_ENABLED true Booléen. Active/désactive les aperçus de valeurs dans les réponses d'introspection (désactivez-les lorsque les aperçus pourraient divulguer des données sensibles)
SHARED_CYCLE_DETECT_DEPTH 16 Profondeur du parcours BFS lors de la vérification des cycles. À augmenter pour des graphes profonds légitimes
SHARED_CYCLE_DETECT_EDGES 10000 Nombre d'arêtes parcourues lors de la vérification des cycles. À augmenter pour des graphes denses légitimes
SHARED_MAX_VALUE_SIZE 1048576 (1 MiB) Plafond de taille par valeur. L'insertion d'une valeur plus grande échoue immédiatement
SHARED_MAX_CHANNEL_BYTES 67108864 (64 MiB) Plafond de charge utile totale par canal
SHARED_POISON_STRICT false Booléen. Lorsque la valeur est vraie, un panic à l'intérieur d'une closure Mutex/Once empoisonne la primitive de façon permanente au lieu d'une récupération au mieux
SHARED_LOCK_DIAGNOSTICS off Diagnostics de contention de verrous : off, count ou trace
SHARED_LOCK_POLL_INTERVAL_MS 100 Intervalle d'échantillonnage utilisé par l'échantillonneur de diagnostics de verrous
SHARED_PREVIEW_STRING_LIMIT 256 Troncature par chaîne dans les aperçus /__ox_shared/preview, en octets (coupée à une frontière de caractère)
SHARED_PREVIEW_ARRAY_LIMIT 20 Nombre d'entrées échantillonnées dans les aperçus /entry?id=…

Profilage

Profileur par échantillonnage qui émet des traces xhprof / speedscope. Voir Profilage pour les formats de sortie et l'intégration avec les visualiseurs.

Variable Valeur par défaut Description
PROFILER_ENABLED false Booléen — voir Valeurs booléennes. Interrupteur principal. Toutes les autres variables PROFILER_* sont tout de même analysées au démarrage, de sorte que les fautes de frappe apparaissent immédiatement
PROFILER_SAMPLE_RATE 0.0 Probabilité (0.0–1.0) qu'une requête soit échantillonnée. Les valeurs hors plage sont ramenées dans les bornes
PROFILER_INTERNAL false Booléen. Lorsque la valeur est vraie, les requêtes vers le serveur interne (/health, /metrics, endpoints de plugins) sont elles aussi éligibles à l'échantillonnage
PROFILER_AUTH_TOKEN (non défini) Jeton bearer facultatif. Lorsqu'il est défini, les fonctions PHP oxphp_profiler_* exigent que les requêtes portent ce jeton pour activer le profilage à la demande
PROFILER_MAX_SPANS 50000 Plafond de spans de profil par requête. Les profils dépassant ce plafond sont tronqués
PROFILER_MAX_DEPTH 256 Profondeur maximale de la pile d'appels capturée par échantillon. Plafonnée en dur à 65535
PROFILER_OUTPUT_DIR /tmp/oxphp-profiles Répertoire des fichiers de profil sur disque
PROFILER_OUTPUT_FORMATS xhprof,speedscope Liste, séparée par des virgules, des formats de sortie à écrire sur disque
PROFILER_DISK_MAX_PER_SEC 10 Limite de débit sur les fichiers de profil écrits sur disque par seconde
PROFILER_RETENTION_COUNT 100 Nombre maximal de fichiers de profil conservés dans PROFILER_OUTPUT_DIR. Les fichiers plus anciens sont supprimés
PROFILER_EXPORT_URL (non défini) Endpoint distant vers lequel envoyer (POST) les profils. Lorsqu'il est défini, les écritures sur disque ont toujours lieu, sauf si PROFILER_OUTPUT_FORMATS est vide
PROFILER_EXPORT_FORMAT xhprof Format de transmission (wire) pour les envois vers PROFILER_EXPORT_URL
PROFILER_EXPORT_AUTH_TOKEN (non défini) Jeton bearer facultatif envoyé avec chaque requête d'export
PROFILER_EXPORT_XHGUI (détection automatique) Booléen. Force l'encapsulation de la charge utile d'export au format compatible XHGui. Non défini = détection automatique lorsque le chemin de PROFILER_EXPORT_URL se termine par /run/import (les indices d'hôte/de requête ne sont pas pris en compte)
PROFILER_EXPORT_BUGGREGATOR (détection automatique) Booléen. Force l'enveloppe Buggregator. Non défini = détection automatique lorsque le chemin de PROFILER_EXPORT_URL se termine par /api/profiler/store. L'enveloppe émet toujours du xhprof, si bien que PROFILER_EXPORT_FORMAT est ignoré dans ce cas (une valeur non-xhprof émet un avertissement, non fatal). Mutuellement exclusif avec PROFILER_EXPORT_XHGUI — activer les deux est une erreur de démarrage
PROFILER_EXPORT_APP_NAME (non défini) app_name Buggregator pour le regroupement par projet
PROFILER_EXPORT_TAGS (non défini) tags Buggregator sous la forme key=value,key2=value2 ; un jeton mal formé, une clé vide ou une clé en double est une erreur de démarrage

Exemples de configurations

Développement

bash
LISTEN_ADDR=127.0.0.1:8080 DOCUMENT_ROOT=./public LOG_LEVEL=debug ACCESS_LOG=all PHP_WORKERS=1 INTERNAL_ADDR=127.0.0.1:9090

Production (Framework)

bash
LISTEN_ADDR=0.0.0.0:80 DOCUMENT_ROOT=/var/www/html/public ENTRY_FILE=index.php PHP_WORKERS=8 QUEUE_CAPACITY=1024 LOG_LEVEL=warn ACCESS_LOG=error MAX_CONNECTIONS=10000 INTERNAL_ADDR=127.0.0.1:9090 RATE_LIMIT=100 RATE_WINDOW_SECONDS=60 TRUSTED_PROXIES=private HEADER_TIMEOUT_SECONDS=5 DRAIN_TIMEOUT_SECONDS=25 COMPRESSION_LEVEL=4 STATIC_MAX_AGE=30d

Production (mode worker)

bash
LISTEN_ADDR=0.0.0.0:80 DOCUMENT_ROOT=/var/www/html/public WORKER_MODE_ENABLED=true ENTRY_FILE=../worker.php PHP_WORKERS=8 WORKER_MAX_MEMORY_MIB=128 QUEUE_CAPACITY=1024 LOG_LEVEL=warn ACCESS_LOG=error INTERNAL_ADDR=127.0.0.1:9090

TLS

bash
LISTEN_ADDR=0.0.0.0:443 TLS_CERT=/etc/ssl/oxphp/cert.pem TLS_KEY=/etc/ssl/oxphp/key.pem DOCUMENT_ROOT=/var/www/html/public ENTRY_FILE=index.php

Inspecter la configuration active

Lorsque le serveur interne est en cours d'exécution, interrogez l'endpoint /config pour voir la configuration résolue :

bash
curl -s http://localhost:9090/config | jq .
json
{ "listen_addr": "0.0.0.0:80", "document_root": "/var/www/html/public", "entry_file": "/var/www/html/public/index.php", "log_level": "warn", "executor_type": "sapi", "php_workers": "8", "tokio_workers": 4, "queue_capacity": 1024, "queue_wait_timeout_ms": 1000, "queue_max_waiting": 1024, "queue_max_waiting_bytes": 67108864, "max_connections": 10000, "drain_timeout_seconds": 30, "header_timeout_seconds": 5, "rate_limit": 100, "rate_window_seconds": 60, "tls_enabled": true, "compression_level": 4, "access_log": "all", "max_query_body": 524288, "worker_mode_enabled": false, "worker_max_memory_mib": 0, "static_max_age": 2592000, "static_revalidate": false, "async_workers": 0, "async_queue_capacity": 0, "async_max_fibers": 256, "async_in_flight_cap": 0, "trace_context": true, "superglobals_enabled": true, "trusted_proxies": false, "plugins": { "otel": { "enabled": true, "protocol": "grpc", "service_name": "oxphp" }, "apm": { "enabled": true, "slow_query_ms": 100, "db_capture_params": false, "hooks_registered": 34 } } }
Note

La réponse /config servie expurge quelques clés que porte la représentation interne Config : les chemins du certificat et de la clé TLS ne sont jamais émis (tls_enabled indique si TLS est actif), et internal_addr et error_pages_dir sont retirés — topologie de déploiement et chemins de système de fichiers qui aident un attaquant et ne sont pas nécessaires aux collecteurs de métriques.

Voir aussi

Une erreur ? Signalez-la →