Référence de configuration
OxPHP se configure au moyen de variables d'environnement, plus un fichier facultatif : .oxphpdeny, les règles de refus de chemins lues depuis le sommet de DOCUMENT_ROOT. 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). Voir Dimensionner le runtime asynchrone |
Dimensionner le runtime asynchrone
TOKIO_WORKERS vaut par défaut la moitié des cœurs disponibles, et la moitié qu'il laisse n'est pas inoccupée : elle revient au pool de workers PHP. Les deux ensembles de threads se disputent les mêmes cœurs, si bien qu'on ne peut pas élargir le runtime sans prendre des cycles à l'application.
Le porter au nombre total de cœurs est une perte partout où le pool est déjà dimensionné près de ce nombre. Mesuré sur une machine à 4 cœurs avec un pool de 4 workers servant des réponses compressées, passer de 2 threads de runtime à 4 a coûté 13 % du débit sur un corps JSON de 1,6 Ko et 26 % sur un corps de 5 Ko, tout en consommant davantage de CPU, parce que les threads étaient en sursouscription.
Augmentez-le là où l'équilibre est différent : un pool petit par rapport à la machine, ou un travail limité par les E/S plutôt que par PHP (upstreams proxifiés, fichiers statiques, réponses en streaming), où c'est le runtime qui travaille et le pool qui attend surtout. Chaque fois que vous le modifiez, surveillez le pool autant que le débit : oxphp_busy_workers rapporté à oxphp_workers_current vous dit de quel côté les cœurs sont partis. Un débit qui monte pendant que le pool s'occupe davantage est un vrai gain ; un débit qui monte pendant que le pool se vide signifie en général que le travail s'est déplacé, pas qu'il s'en est fait davantage.
TOKIO_WORKERS et PHP_WORKERS valent tous deux par défaut la moitié des cœurs disponibles, si bien que leur somme tombe sur le nombre de cœurs. Modifiez l'un et la somme bouge : c'est pourquoi l'un comme l'autre mérite d'être mesuré plutôt que deviné.
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 maximale pendant laquelle une requête peut attendre un worker PHP avant d'être rejetée avec un 529. C'est un plafond plutôt qu'une valeur fixe. 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. En surcharge, le serveur raccourcit de lui-même le budget sous ce plafond et le rallonge une fois la file résorbée ; oxphp_admission_wait_budget_us publie la valeur en vigueur, oxphp_admission_wait_budget_ceiling_us celle qui a été configurée (voir Le budget d'attente adaptatif). L'échéance est appliquée par les deux bouts : la requête reçoit un 529 quand elle est dépassée, et un worker qui atteint une requête au-delà refuse de l'exécuter, si bien qu'une requête en file que personne ne prend en charge n'attend pas indéfiniment. Elle 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 ; cela supprime l'attente d'une place dans la file, mais pas l'attente à l'intérieur de la file. Abaissez-le quand l'application rappelle ce même serveur en HTTP (l'appel intérieur a besoin d'un worker que l'appel extérieur détient, 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 ; celui qui ferme alors qu'il est déjà dans la file garde sa place jusqu'à ce qu'un worker l'atteigne ou que le budget s'épuise, et reçoit alors un 499 au lieu d'être compté comme un refus pour surcharge. Ce que le serveur ne peut pas voir, c'est un client qui cesse d'attendre sans fermer. Un tel client 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.
Le débit de service n'est pas connu au démarrage, si bien que la valeur par défaut ne peut pas calculer cela et est 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, un gestionnaire à 200 ms et le plafond par défaut de 1 s (le budget en vigueur tant que le pool ne déleste pas), seules 40 requêtes environ peuvent être admises à temps alors que la valeur par défaut en stationne jusqu'à 1024.
QUEUE_MAX_WAITING=40Le 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. B désigne ici le budget en vigueur, qui en surcharge est celui auquel le serveur l'a raccourci plutôt que le plafond configuré. Lisez oxphp_admission_wait_budget_us avant de dimensionner l'ensemble en attente d'après un nombre tiré d'un fichier de configuration.
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.
Le budget d'attente adaptatif
QUEUE_WAIT_TIMEOUT_MS est un plafond. Tant que le travail que l'attente admet s'achève pour des clients déjà partis, le serveur raccourcit le budget de lui-même, et fortement, ce qu'il vaut mieux savoir avant de choisir un plafond :
- À la baisse : le budget est divisé par deux chaque fois qu'un quart ou plus de ce que le pool a démarré est allé à des clients déjà partis, mesuré sur une fenêtre d'au moins huit requêtes démarrées plutôt que sur un intervalle fixe.
- Plancher : un soixante-quatrième du plafond (15,6 ms par défaut) ou 10 ms, selon la plus grande valeur, sans jamais dépasser le plafond lui-même.
- À la hausse : il remonte par huitièmes du plafond, et seulement tant que la file est observée vide et que moins d'un quart de ce que le pool a démarré est allé à des clients partis. Le filet ordinaire d'onglets fermés et de délais client expirés ne le maintient pas bas, et un serveur dont la file ne se vide jamais reste sur le budget raccourci jusqu'à ce que la charge retombe.
Un opérateur qui a défini 1000 doit s'attendre à ce qu'environ 15 soit en vigueur pendant un épisode ; écrivez donc les alertes sur l'attente d'après la paire oxphp_admission_wait_budget_us / oxphp_admission_wait_budget_ceiling_us plutôt que d'après le nombre configuré.
Le raccourcissement est provoqué par les clients qui ferment. Une population qui cesse d'attendre sans fermer ne produit aucun signal et laisse le plafond en vigueur.
Une application qui rappelle ce même serveur en HTTP gaspille ses attentes quelle que soit leur durée : l'appel intérieur a besoin d'un worker que l'appel extérieur détient. oxphp_admission_wait_wasted_total compte ces attentes, mais seulement tant que le budget est à son plafond. Là où le budget a baissé, le compteur se tait, si bien qu'un 0 à cet endroit ne montre pas à lui seul que le motif est absent. Que le budget baisse ou non n'est pas non plus une propriété de l'auto-appel : un auto-appelant dont les propres appelants attendent généreusement (un cron interne, un sidecar au délai long) garde son plafond.
QUEUE_WAIT_TIMEOUT_MS=0 ne règle pas cela. Il supprime l'attente d'une place dans la file, pas l'attente à l'intérieur de la file, si bien qu'à moins que QUEUE_CAPACITY soit assez petit pour que la file soit pleine, l'appel intérieur est admis dans une file que rien ne consomme et ne reçoit jamais de réponse.
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 blocage est journalisé. Une acceptation qui doit attendre un permis journalise un WARN nommant le budget, et un INFO indiquant combien de temps la boucle est restée en pause est écrit dès que le permis arrive. Ces rapports sont limités en débit pour qu'un serveur qui oscille autour du plafond ne puisse pas transformer son propre journal en panne : un blocage qui commence quelques secondes après un blocage déjà rapporté est d'abord retenu, puis rapporté à la fermeture de cette fenêtre s'il dure encore ; un blocage qui se termine dans la fenêtre ne laisse aucune ligne. Les métriques les voient tous : oxphp_accept_stalled (une jauge, lisible pendant toute la durée du blocage parce que le listener interne ne passe pas par MAX_CONNECTIONS) et oxphp_accept_stalls_total (un compteur) ; voir la référence des métriques. Les clients qui arrivent n'obtiennent toujours aucune réponse plutôt qu'une erreur, mais le journal et les métriques enregistrent le blocage.
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 :
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 :
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 maximumEn 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.
Comment le nombre de CPU est détecté
Chaque valeur par défaut de cette page exprimée en CPU (TOKIO_WORKERS, et PHP_WORKERS non défini, à 0, ou avec 0 de part ou d'autre de MIN:MAX) compte les CPU sur lesquels le processus est autorisé à s'exécuter : son masque d'affinité (taskset, docker run --cpuset-cpus), abaissé au quota CPU du conteneur lorsqu'il y en a un (docker run --cpus ; cpu.max en cgroup v2, cpu.cfs_quota_us en v1). Un quota est arrondi à l'inférieur et compte pour au moins 1, si bien que --cpus=1.5 compte pour 1.
Deux types de limite ne sont pas comptés, et un conteneur bridé par l'une d'elles seulement est dimensionné d'après tous les CPU qu'il voit :
- Un poids CPU :
docker run --cpu-shares,cpu.weighten cgroup v2,cpu.sharesen v1. Il décide comment le temps CPU disputé est partagé avec les voisins. Ce n'est pas un plafond, et aucun nombre de CPU n'en découle. - Une limite appliquée hors du conteneur. Une plateforme peut vendre une part d'une machine et appliquer la limite là où le conteneur ne peut pas la voir. Sur DigitalOcean App Platform, par exemple, une instance vendue comme 1 vCPU a été observée en train de tourner dans un bac à sable gVisor sans quota (
cpu.cfs_quota_usvaut-1) et avec 8 CPU visibles, si bien que les valeurs par défaut aboutissaient à 4 workers PHP, 4 threads de runtime et une file de 512.
Sur une telle offre, définissez explicitement PHP_WORKERS et TOKIO_WORKERS selon ce que l'offre fournit réellement. En mode worker, confrontez aussi PHP_WORKERS × WORKER_MAX_MEMORY_MIB à la mémoire de l'offre. Ce seuil est un déclencheur de recyclage que le worker vérifie dans sa boucle, pas un plafond sur ce que son tas PHP peut allouer : le pool peut donc détenir autant, et davantage, avant qu'un worker ne soit recyclé pour cette raison, avec OPcache et le serveur lui-même en plus ; avec la valeur par défaut 0, il n'y a aucun seuil. Le même bac à sable peut aussi masquer la limite de mémoire : le conteneur App Platform ci-dessus n'en voyait aucune.
Au démarrage, le serveur journalise le nombre, ce qui l'a fixé, et les PHP_WORKERS et TOKIO_WORKERS qui en résultent. Voici ce que cela donne sous docker run --cpus=1 sur une machine à 14 CPU :
{"timestamp":"2026-10-05T18:55:39.801254Z","level":"INFO","fields":{"message":"CPU count for default sizing","cpus":1,"cpu_source":"cgroup quota","host_cpus":14,"php_workers":"1 (auto)","tokio_workers":"1 (auto)"}}cpu_sourcevauthostquand ni un masque ni un quota n'ont abaissé le nombre,affinityoucgroup quotaquand l'un d'eux l'a fait, etunknownquand le serveur n'a pas pu trancher parce que le masque ou le nombre de CPU en ligne n'a pas pu être lu. S'il n'a pas pu compter les CPU du tout,cpusvaut 4.host_cpusest le nombre de CPU que le système déclare en ligne (/sys/devices/system/cpu/onlinesous Linux), qu'un masque ou un quota n'abaisse pas. Il est omis quand il ne peut pas être lu. Un runtime qui réécrit ce fichier pour n'y lister que les CPU du conteneur fait lire un masque restreint commehost.(auto)marque une valeur laissée entièrement à la valeur par défaut : la variable n'est pas définie ou est vide, ou, pourTOKIO_WORKERS, n'est pas un nombre.
Un cpu_source à host sur une offre qui vous a vendu moins de CPU que cpus, c'est le cas décrit plus haut.
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 workers 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 reçoivent trois requêtes consécutives qui se sont mal terminées, et à la demande lorsque l'application appelle Worker::scheduleExit() ; voir Recyclage pour ce qui compte dans ce troisième cas. 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 |
Décrit la requête à PHP pour qu'il construise $_SERVER et $_GET avant l'exécution du script. Définissez sur une valeur fausse pour s'épargner ce travail dans une application qui ne lit ni l'une ni l'autre. Cela ne concerne que ces deux-là : $_POST, $_FILES et $_COOKIE sont construits à partir du corps de la requête et de l'en-tête Cookie, qui sont remis à PHP dans tous les cas, et php://input est un flux plutôt qu'une superglobale. Voir SUPERGLOBALS_ENABLED pour le tableau complet |
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. Une requête prise dans un appel bloquant à l'échéance peut retenir le processus au-delà (voir 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. Cette borne n'atteint pas une tâche asynchrone : un thread du pool ASYNC_WORKERS ne détient aucune échéance d'exécution, si bien que ni la directive ni set_time_limit() ne limitent ce qu'exécute une tâche envoyée avec oxphp_async(). Bornez plutôt une telle tâche avec l'argument de délai d'oxphp_async_await() ; voir Promesses asynchrones.
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 : un mélange, séparé par des virgules, de CIDR, d'IP nues et des mots-clés private et cloudflare (par exemple private,cloudflare). 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) ; avec cloudflare, une requête dont la chaîne de transfert montre qu'elle est arrivée depuis un nœud de bordure Cloudflare la tire plutôt de CF-Connecting-IP. 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 ou avec une règle de refus .oxphpdeny. Soit un statut HTTP 400–599 (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). Une règle .oxphpdeny peut couvrir le script ; en mode worker, les règles de refus .oxphpdeny n'acceptent qu'un code de statut. Voir Règles de refus de chemins |
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 repliframe-ancestors(une CSP serveur écraserait le choix de votre application dans les navigateurs modernes). - Si votre application définit une
Content-Security-Policycontenant une directiveframe-ancestors, OxPHP omet son repliX-Frame-Options(unX-Frame-Optionsserveur 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
| Variable | Valeur par défaut | Description |
|---|---|---|
COMPRESSION_ENCODINGS |
br,zstd,gzip |
Codages de contenu proposés par le serveur, séparés par des virgules : br (ou brotli), zstd, gzip. Un nom inconnu est une erreur au démarrage. Définissez la valeur entière à off (ou none) pour désactiver complètement la compression. Ce n'est pas un élément de liste, si bien que br,off est une erreur plutôt qu'un interrupteur. Une affectation vide rétablit la valeur par défaut. L'ordre écrit ici est ignoré : les fichiers statiques en cache préfèrent Brotli, dont le coût n'est payé qu'une fois, et tout le reste préfère Zstandard. Voir Compression |
COMPRESSION_BROTLI_LEVEL |
5 |
Qualité Brotli (0 à 11). Remplace COMPRESSION_LEVEL, déprécié |
COMPRESSION_ZSTD_LEVEL |
6 |
Niveau de compression Zstandard (0 à 19) |
COMPRESSION_GZIP_LEVEL |
6 |
Niveau de compression Gzip (0 à 9) |
COMPRESSION_LEVEL |
(non défini) | Ancien nom, déprécié, de COMPRESSION_BROTLI_LEVEL ; émet un WARN au démarrage. COMPRESSION_LEVEL=0 garde son sens d'origine et désactive tous les codages, pas seulement Brotli. Un COMPRESSION_BROTLI_LEVEL explicite l'emporte sur une valeur non nulle, mais pas sur 0, qui désactive la compression quoi que soit défini par ailleurs |
Un codage est proposé quand COMPRESSION_ENCODINGS le liste et que son niveau n'est pas 0 ; l'une ou l'autre condition seule suffit à le retirer. Un niveau hors de sa plage, ou qui n'est pas un nombre, interrompt le démarrage avec une erreur nommant la variable. Les valeurs ne sont pas ramenées dans les bornes.
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é |
ACCESS_LOG accepte all ou error. Laissez la variable non définie pour désactiver totalement la journalisation des accès.
Un client qui ferme sa connexion avant que le serveur ait répondu est journalisé au niveau debug, pas error. C'est l'événement le plus courant que voit un listener public (un fetch annulé, un onglet fermé, un proxy dont le délai de lecture a expiré) et ce n'est pas une défaillance du serveur ; au LOG_LEVEL par défaut, il est donc invisible. Comptez-les avec oxphp_request_cancelled_total{reason="client_abort"}, ou passez à LOG_LEVEL=debug pour voir les connexions individuelles. Les délais que le serveur applique lui-même (HEADER_TIMEOUT_SECONDS) restent au niveau warn, et toutes les autres défaillances de connexion restent au niveau error.
Les deux sont indépendants. LOG_LEVEL règle la verbosité des diagnostics propres au serveur, si bien que LOG_LEVEL=warn avec ACCESS_LOG=error rapporte les avertissements et les erreurs et journalise chaque requête 4xx/5xx. Faire taire le journal d'accès seul demande une directive RUST_LOG nommant sa cible : RUST_LOG=warn,access_log=off, où warn tient lieu du LOG_LEVEL que vous utilisiez. RUST_LOG, lorsqu'il est défini et analysable, remplace LOG_LEVEL au lieu de s'y ajouter. Voir 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) 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. Avec http/protobuf, une URL de base : les spans sont envoyés à /v1/traces sous celle-ci |
OTEL_EXPORTER_OTLP_TIMEOUT |
10000 |
Délai d'expiration de l'export en millisecondes. En gRPC, il borne aussi la connexion au collecteur, poignée de main TLS comprise : gardez-le donc au-dessus du temps que cela prend ; une tentative qui doit d'abord se connecter peut durer jusqu'à deux fois ce délai. En gRPC sous Linux, il fixe aussi au bout de combien de temps une connexion qui a cessé d'accepter des données est fermée — voir Erreurs d'export de spans dans le journal |
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 |
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é, les fonctions de span sont des no-ops qui renvoient des valeurs sentinelles. oxphp_apm_trace() fait exception : elle exécute toujours son callback et renvoie la valeur de celui-ci, si bien que désactiver l'APM ne change jamais ce que calcule votre application.
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(), y compris dans les contextes où ces deux-là ne peuvent pas suspendre non plus et attendent sur le thread worker à la place (voir Où les hooks sleep ne suspendent pas) |
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) |
net |
Les attentes situées sous les flux PHP, là où le hook de lecture streams ne peut pas les voir, suspendent la Fiber courante au lieu d'épingler le thread worker : l'établissement de connexion via stream_socket_client(), fsockopen() et tous les clients bâtis sur les flux PHP (mysqlnd, donc PDO_MySQL et mysqli, et phpredis parmi eux), la poignée de main TLS, les lectures sur les flux ssl:///tls:// une fois le chiffrement actif, les écritures qui doivent attendre de la place dans le tampon d'envoi, et les lectures de flux sur les sockets unix://, udp:// et udg://, sur les flux stream_socket_pair(), et sur les flux que socket_export_stream() crée à partir de sockets qui ne sont pas TCP. Nécessite que streams soit listé à côté, et ne fonctionne que sous Linux (voir Quand net reste désactivé). Non couverts : les attentes dans libcurl, libpq (pgsql, pdo_pgsql) et libmemcached, qui appellent poll() depuis leurs propres bibliothèques ; la résolution DNS ; stream_socket_recvfrom() ; les écritures sur un flux en clair (non TLS) avec un délai infini (default_socket_timeout=-1) ; et les établissements de connexion persistants (pfsockopen(), STREAM_CLIENT_PERSISTENT, et les modes persistants des clients bâtis sur eux) |
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 partout où la Fiber peut être suspendue (ce qui n'est pas partout ; voir ci-dessous), sans aucune modification de code. Annuler une tâche asynchrone pendant un sleep intercepté la déroule avec OxPHP\Async\AsyncException, et un sleep() intercepté qui suspend renvoie 0 ; la valeur de retour d'interruption par signal de la fonction native ne survient que sur le chemin bloquant ci-dessous.
Vérifier que le réglage a été pris en compte
Le serveur rapporte les catégories qu'il a installées, sous forme d'ensemble analysé, à deux endroits. Sur le serveur interne :
$ curl -s localhost:9090/config | jq .runtime_hooks
[
"sleep",
"streams",
"net"
]Et depuis PHP, pour un déploiement dont le serveur interne n'est pas joignable ou pas activé :
<?php
echo json_encode(oxphp_server_info()['runtime_hooks']); // ["sleep","streams","net"]Les deux lisent la même valeur et rapportent l'ensemble plutôt que la chaîne : RUNTIME_HOOKS=all, RUNTIME_HOOKS=1 et RUNTIME_HOOKS=sleep, streams, net reviennent tous sous la forme ["sleep","streams","net"]. Une liste vide signifie qu'aucun hook n'est installé. C'est la réponse pour la variable non définie, pour RUNTIME_HOOKS=0, pour une valeur ne nommant que des catégories que ce build ne connaît pas, et pour un nom de variable mal orthographié. Une valeur qui mélange les deux est différente : RUNTIME_HOOKS=sleep,bogus se lit ["sleep"], si bien qu'une liste non vide ne prouve pas à elle seule que chaque nom a été compris. Chaque nom non reconnu obtient sa propre ligne dans le journal du serveur au démarrage, et cette ligne est le seul endroit où ils sont nommés. Vérifiez après tout changement du réglage : une variable d'environnement non reconnue n'est une erreur nulle part, si bien qu'une faute de frappe ne se manifeste sinon que par de la latence.
La liste indique quelles fonctions natives ont vu leurs handlers remplacés, ce qui se produit au démarrage quel que soit le mode de routage. Là où il n'y a aucune Fiber, les remplacements délèguent aux handlers qu'ils ont remplacés, si bien qu'un serveur en mode traditionnel rapporte ["sleep","streams","net"] tout en se comportant nativement de bout en bout. Un processus CLI php rapporte ["sleep","streams"] : le moteur fait partie de cet exécutable, si bien que net ne peut pas distinguer les attentes de PHP de celles du programme lui-même et reste désactivé. Lisez la liste comme « ce processus est préparé pour les hooks », et le mode de routage comme l'indication que quelque chose peut ou non s'en servir.
Au sein de streams, c'est plus étroit encore : la partie lecture de socket peut échouer à s'installer à elle seule, sur une plateforme où la table d'opérations de flux ne peut pas être rendue inscriptible, tandis que stream_select() et les points d'entrée de base de données restent interceptés. La catégorie est rapportée dans les deux cas ; le seul signal est une ligne du journal du serveur indiquant que les lectures de socket restent bloquantes. Cette ligne, comme celle des catégories non reconnues et les lignes qu'écrit net (ci-dessous), suit la journalisation des erreurs de PHP : avec error_log non défini (la valeur par défaut), elle atteint le journal propre du serveur en tant que warn, et LOG_LEVEL=error la masque.
Quand net reste désactivé
net n'est rapporté que lorsqu'il est réellement en place. Listé sans streams, il reste désactivé, avec une ligne du journal du serveur qui le dit. Sur une plateforme qui n'est pas Linux, là où le code propre du serveur ne peut pas être distingué de celui de PHP au démarrage, ou là où le poll() de PHP n'est pas lié à celui que le serveur intercepte, il reste désactivé avec une ligne indiquant que les établissements de connexion et les attentes TLS restent bloquants. Un net absent de la liste alors que vous l'avez demandé est donc la réponse, et non une lacune du rapport.
La seule chose que la liste ne vous dit pas, c'est où vit OpenSSL. Les attentes qu'effectue OpenSSL (la poignée de main TLS, y compris celle qui termine l'établissement d'une connexion ssl://, ainsi que les lectures et écritures sur un flux TLS une fois le chiffrement actif) ne sont stationnées que lorsqu'OpenSSL est compilé dans PHP, comme c'est le cas dans les images officielles. Avec OpenSSL chargé comme extension partagée distincte (extension=openssl), ces attentes ne sont pas celles de PHP pour autant que le serveur puisse en juger : elles gardent donc le thread, tandis que les établissements de connexion, unix:// et les autres lectures suspendent toujours, et que net est toujours rapporté.
Où les hooks sleep ne suspendent pas
Là où un changement de Fiber est refusé, les sleep()/usleep() interceptés remettent l'appel à la fonction native qu'ils ont remplacée, qui attend sur le thread worker et renvoie ce qu'elle renvoie. Un sleep() interrompu par un signal à cet endroit rend donc les secondes qu'il n'a pas dormies, comme il le fait hors d'une Fiber. (La vérification de l'argument est celle du hook dans les deux cas : elle rejette toute valeur supérieure à 4294967295, que la fonction native non interceptée accepte et tronque.) Le changement de contexte est bloqué partout où quitter le frame courant serait dangereux, et la liste ci-dessous n'est pas exhaustive :
- dans un handler
declare(ticks)et dans la distribution des signaux de pcntl (ces deux cas relèvent de PHP lui-même) ; - pendant la construction de l'entrée d'une requête ;
- dans un
FILTER_CALLBACKatteint depuisfilter_input_array()avec un tableau de définitions par champ (voir la note sous$_GETdans Superglobales) ; - dans les parcours de démontage qui exécutent les destructeurs après qu'une requête s'est mal terminée (après une erreur fatale, ou après une écriture vers une requête déjà annulée), si bien qu'un
__destruct()qui dort pour temporiser le fait en détenant le thread.
La fin ordinaire d'une requête n'en fait pas partie : les destructeurs et les fonctions d'arrêt s'y exécutent avec le changement de contexte autorisé. oxphp_sleep() et oxphp_usleep() prennent eux aussi leur chemin bloquant dans chacun de ces endroits, si bien que s'en servir ne permet pas de les contourner.
Une Fiber démarrée par un ordonnanceur en espace utilisateur (AMPHP, Revolt, ou un simple new Fiber) est un cas différent : le changement de contexte n'y est pas bloqué, et l'ordonnanceur suspend et reprend ses propres Fibers normalement, mais OxPHP ne peut pas reprendre un contexte qui ne lui appartient pas, si bien que le hook s'efface pour la même raison que le hook streams. Il s'agit là d'une Fiber démarrée à l'intérieur d'une requête ; une requête en mode worker est elle-même une Fiber pilotée par OxPHP, si bien que les hooks s'y appliquent normalement. Le code à l'intérieur d'une Fiber que l'application a démarrée elle-même dort sur le thread worker, et cette part de la charge doit être dimensionnée comme si le hook était désactivé.
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, ou de l'une de celles qu'ajoute la catégorie net (ci-dessous). Plusieurs clients courants ne le font pas, et pour eux rien ne change :
-
ext/curl.
curl_exec()etcurl_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 dansstream_socket_accept(), qui n'est pas interceptée non plus. -
Les flux
unix://,udp://etudg://, les paires de sockets, les flux issus desocket_export_stream()sur des sockets qui ne sont pas TCP, etssl:///tls://une fois le chiffrement actif. Leurs tables d'opérations sont privées à PHP ou à OpenSSL, si bien que le hookstreamsne les atteint pas. La catégorienet, si : quand elle est listée, une lecture de flux (fread(),fgets()et consorts) sur l'un d'eux suspend la Fiber comme sur un fluxtcp://. Sansnet, ils détiennent le thread comme auparavant (avant questream_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 dans les deux cas).stream_socket_recvfrom()n'est pas une lecture de flux : il appellerecvfrom()directement, sans attente propre à PHP à suspendre, si bien que sur un socketudp://ouudg://il détient le thread jusqu'à l'arrivée d'un datagramme, même avecnet.socket_export_stream()crée un flux du transport qu'utilise le socket : un socket UDP, unix ou unix en datagrammes devient un fluxudp://,unix://ouudg://, un socket TCP un fluxtcp://ordinaire (questreamsseul couvre déjà), et un socket de tout autre type le flux générique qu'a une paire de sockets. -
L'établissement de connexion et la résolution DNS. Avec
net, un établissement de connexion via les transports de flux de PHP suspend la Fiber :stream_socket_client(),fsockopen()et les clients bâtis sur eux.ftp_connect()etsocket_connect()se connectent par eux-mêmes, hors de la couche de flux, et ne sont pas couverts. La résolution DNS ne suspend pas non plus : un nom d'hôte est résolu sur le thread worker avant que l'établissement de connexion ne commence, si bien qu'un résolveur lent le détient. Un client doté de sa propre bibliothèque réseau (pdo_pgsqletpgsqlvia libpq, l'extensionmemcachedvia libmemcached) se connecte et attend à l'intérieur de cette bibliothèque, qu'aucune des deux catégories ne voit. Un client qui garde sa connexion ouverte en bénéficie à chaque requête quoi qu'il en soit.Un établissement de connexion persistante fait exception, et cela couvre plus que
pfsockopen()etSTREAM_CLIENT_PERSISTENT: PDO_MySQL avecPDO::ATTR_PERSISTENT, mysqli avec le préfixe d'hôtep:et phpredis avecpconnect()ouvrent eux aussi leurs flux comme persistants, et leurs établissements de connexion gardent le thread pendant l'attente, comme avant l'existence denet. La raison en est la liste des persistants. Un flux persistant y est inscrit sous sa clé avant d'être connecté, si bien qu'une seconde Fiber demandant la même clé pendant que la première attend trouverait un flux encore sans descripteur, le prendrait pour une connexion morte et le fermerait sous la Fiber encore en train de se connecter. Certains clients donnent à chaque connexion une clé qui lui est propre (c'est le cas de mysqlnd), et rien ne peut retrouver ces flux de cette façon, mais le serveur décide selon que le flux est persistant ou non, pas selon qui pourrait le retrouver : ils détiennent donc eux aussi le thread. Le coût porte sur l'établissement de la connexion, qu'une connexion persistante ne paie qu'une fois, à sa première ouverture, et non sur les lectures et écritures qui s'y font. -
MySQL atteint via
localhost. Le client MySQL litlocalhostcomme « utiliser le socket unix », qui n'est pas un fluxtcp://, si bien que le hookstreamsne le voit pas. Écrivez plutôt127.0.0.1dans le DSN, ou listeznet, qui couvre les fluxunix://. 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. Avec
streamsseul, 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.)Avec
netlisté, une écriture qui doit attendre de la place suspend elle aussi la Fiber, et le descripteur est revérifié après chaque réveil, si bien qu'une fenêtre refermée entre-temps coûte une suspension de plus plutôt que le thread. Un flux en clair dont le délai est infini (default_socket_timeout=-1) fait exception : PHP y envoie avec unsend()bloquant et n'attend jamais de lui-même, il n'y a donc rien à suspendre et l'écriture détient le thread comme sans le hook. Un flux TLS au délai infini est différent : la propre boucle d'OpenSSL attend danspoll()sans limite, et cette attente est suspendue.
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 :
-
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.
-
L'attente est toujours bornée : par le plus petit de
max_execution_timeetdefault_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 dedefault_socket_timeoutuniquement là oùmax_execution_timeest à 0.max_execution_timeest lu tel que la requête l'a en ce moment, puisqueset_time_limit()est la manière dont une requête déclare combien de temps elle peut s'exécuter ;default_socket_timeoutest 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 uneRedisExceptionet n'envoie rien.Un appel PDO fait exception, ici comme pour le code qui s'exécute hors de toute Fiber (ci-dessous) :
PDO::rollBack()sur une connexion persistante qu'une autre Fiber détient, s'il y trouve une transaction ouverte, n'envoie rien et renvoiefalse, parce que cette transaction est celle du détenteur et qu'unROLLBACKenvoyé entre ses instructions y mettrait fin. L'erreur qu'il signale, de la manière que dicte le mode d'erreur, est une erreur que la connexion a déjà enregistrée, qui peut être celle du détenteur. Le démontage d'objets propre à PDO est lui aussi réservé au détenteur. Tant qu'une requête ou une tâche détient une connexion persistante, abandonner un handle sur celle-ci n'annule la transaction qui y est ouverte et n'exécute le nettoyage de fin de requête du pilote que si le détenteur a été le dernier à utiliser ce handle (il l'a construit, ou a depuis appelé à travers lui l'une des méthodes PDO qu'OxPHP protège), où que le handle soit détruit, y compris par le collecteur de cycles de PHP s'exécutant dans une autre requête. Tout autre handle saute les deux. Sur une connexion que personne ne détient, chaque handle effectue les deux, comme auparavant.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()rapportetimed_out. Cela peut coûter plus que l'appel : un client est libre d'interpréter une opération refusée comme quelque chose de plus grave qu'une commande échouée, et mysqlnd le fait des deux côtés d'un échange. Une écriture qui n'envoie rien marque la connexion comme perdue et la ferme, une lecture qui revient vide la marque comme perdue sur place, et chaque commande ultérieure sur elle reçoit une réponse tirée de cet état sans toucher au réseau. La perte porte sur la connexion entière plutôt que sur l'appel isolé, aussi longtemps que quelque chose détient le handle. C'est la revendication au niveau client décrite plus haut qui empêche les commandes d'un client protégé d'atteindre ce refus ; un protocole écrit à la main sur un socket brut doit traiter une opération refusée, de l'un ou l'autre type, comme il traite une connexion fermée. 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.Le code qui s'exécute hors de toute Fiber ne peut pas du tout attendre une revendication, et seul le niveau socket s'applique à lui. En mode worker avec
RUNTIME_HOOKS=streams, il s'agit surtout d'un destructeur que la boucle du worker exécute entre les requêtes quand elle collecte les cycles, pendant que d'autres requêtes sont stationnées. Une opération de socket qu'il effectue et qui devrait attendre, sur une connexion dont le détenteur y est stationné en attente de sa réponse, échoue aussitôt comme le fait un délai expiré et n'envoie rien ; à tout autre moment, elle se poursuit. Le niveau client n'y est pas appliqué, parce que sa revendication dure jusqu'à la fin de la requête du détenteur : il refuserait à un tel code une connexion que le détenteur n'utilise pas à ce moment-là, ce qui est le cas la plupart du temps. Ce que coûte un refus dépend du client, comme ci-dessus. phpredis fait échouer l'appel et le détenteur garde sa réponse. mysqlnd soit refuse la commande d'après son propre état, soit, lorsque l'échange du détenteur a laissé cet état prêt, voit son écriture refusée et ferme la connexion, et la requête du détenteur se termine comme décrit au point 4. Mieux vaut donc ne pas utiliser de client partagé du tout dans un destructeur. -
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, et cela coûte plus que l'appel : atteignant le socket pendant qu'une Fiber quelconque est stationnée sur la connexion, il y est refusé, et pour mysqlnd un refus fait perdre la connexion, comme ci-dessus, aussi bien sur la lecture qui récupère la ligne suivante d'un résultat non mis en tampon que sur l'écriture qui envoie la commande. 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.Les connexions persistantes sont couvertes ; voir Connexions PDO persistantes ci-dessous.
-
Fermer une connexion partagée pendant qu'une autre requête est stationnée à la lire met fin à cette requête, avec un
500et 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()etmysqli_close()sont des appels revendiqués, ils attendent donc le détenteur et ferment une fois cette attente épuisée, tandis queRedis::close()est revendiqué aussi mais lève uneRedisExceptionau lieu de fermer. Tout le reste ferme aussitôt —fclose()sur un flux brut, et, surtout, PDO, qui n'a pas de méthodeclose()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 avant que la connexion ne soit fermé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 équivalentmysqli. 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 seraitmysqlnd.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 streams 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 Fiber (Fiber::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.
Connexions PDO persistantes
PDO::ATTR_PERSISTENT garde la connexion du pilote dans un pool que tout le worker partage, et PDO atteint ce pool en deux étapes qui ne supportent pas d'être entrelacées : il cherche une connexion dans le pool avant de se connecter, et enregistre ensuite ce qu'il a construit. Deux constructeurs qui se chevauchent manquent tous deux le pool, et le second enregistrement remplace la première entrée, fermant cette connexion sous la requête qui y lit sa réponse, quelle qu'elle soit. Le chevauchement est le cas ordinaire, puisque static $pdo ??= new PDO(...) est atteint par chaque requête qui arrive avant la fin de la première connexion.
Les constructeurs qui demandent une connexion persistante vers une même source de données s'exécutent donc un par un par worker, et le second trouve ce que le premier a enregistré. La source de données est celle que PDO résout (un alias issu de pdo.dsn.* dans php.ini compte comme le DSN qu'il nomme), et les constructeurs vers des sources différentes ne s'attendent pas, en règle générale, les uns les autres : un constructeur bloqué à se connecter à un serveur qui accepte la connexion et ne répond jamais ne retient donc pas les constructeurs vers un serveur sain. Deux cas attendent davantage plutôt que moins :
- Les DSN identiques jusqu'à un deux-points. Un DSN n'est comparé que jusqu'au premier deux-points qui suit son préfixe de pilote (un hôte IPv6 est une façon d'en avoir un), parce que PDO joint DSN, utilisateur et mot de passe dans sa clé de pool avec des deux-points et n'échappe rien : un DSN prolongé par
:…peut donc nommer la même entrée de pool que celui qui ne l'est pas. Les sources identiques jusqu'à ce deux-points partagent une même attente, y compris une source bloquée : deux hôtes IPv6 dans le même premier groupe de chiffres s'attendent l'un l'autre comme le faisaient autrefois tous les constructeurs d'un worker. - Les DSN
uri:et les DSN qui ne sont pas des chaînes. Ils ne peuvent pas être résolus sans refaire ce que PDO en fait, si bien qu'un tel constructeur attend qu'aucun constructeur persistant vers quelque source de données que ce soit ne s'exécute sur son worker, et tous attendent pendant qu'il s'exécute. Il ne détient rien pendant qu'il attend, il ne retient donc pas les autres, mais tant qu'un constructeur vers un serveur qui ne répond jamais s'exécute, il ne peut pas trouver ce moment, et quand son attente est épuisée il poursuit sans sérialisation. Le cas du serveur bloqué ci-dessus atteint donc toujours les DSNuri:et objets, et un tel DSN qui nomme le serveur bloqué lui-même retient tous les constructeurs persistants de son worker aussi longtemps que dure son établissement de connexion, exactement comme auparavant.
Cette attente est bornée comme celles ci-dessus, mais ce qu'elle coûte une fois épuisée n'est pas une erreur que le client lève : un constructeur dont le budget s'épuise poursuit sans sérialisation et le signale dans le journal du serveur, et deux constructeurs qui se chevauchent ainsi remplacent l'entrée de pool l'un de l'autre comme auparavant. Cela met fin à la requête qui lit sa réponse sur la connexion remplacée et, parce qu'un handle du pool est libéré quel que soit le nombre d'objets PDO qui pointent encore vers lui, peut mettre fin au processus worker lorsqu'un de ces objets est détruit plus tard. À l'intérieur de la propre Fiber d'un ordonnanceur en espace utilisateur, ce budget est épuisé aussitôt : un tel contexte ne peut pas être suspendu, si bien qu'un constructeur qui en trouve un autre en cours abandonne immédiatement.
L'autre étape est la vérification de vivacité que PDO effectue avant de remettre une connexion du pool : un ping sur la connexion elle-même. Envoyé pendant que le détenteur est stationné en attente de sa réponse, il serait refusé, PDO considérerait la connexion comme morte et en ouvrirait une seconde, et ce que l'application voulait partager ne le serait plus. Une connexion qu'une autre Fiber détient est donc déclarée vivante sans le ping, mais seulement pour un constructeur qui la laisserait exactement en l'état (là où un pilote ne fournit aucune vérification de vivacité, comme pdo_sqlite, la même réponse remplace celle de PDO). PDO écrit le mode d'erreur, l'indicateur d'autocommit et le reste de ses options sur un handle du pool qu'il remet, et sur une connexion en cours d'utilisation ces écritures atterriraient chez le détenteur. La connexion n'est donc partagée qu'avec un constructeur qui n'y écrirait rien de nouveau :
- chaque option qu'il passe est une option que PDO stocke sur le handle lui-même (
PDO::ATTR_ERRMODE,PDO::ATTR_CASE,PDO::ATTR_ORACLE_NULLS,PDO::ATTR_DEFAULT_FETCH_MODE; l'entréePDO::ATTR_PERSISTENTqui nomme le pool ne compte pas), et - chacune porte la valeur que la connexion a déjà.
Une option dont le pilote doit être informé est refusée quelle que soit sa valeur, PDO::ATTR_AUTOCOMMIT et PDO::ATTR_STRINGIFY_FETCHES compris, sur tous les pilotes, tiers inclus. Un constructeur qui ne remplit pas l'une ou l'autre condition apprend que la connexion est morte et en obtient une à lui, et le journal du serveur le signale au plus une fois par seconde. Pour une clé de pool dont les options portent PDO::ATTR_EMULATE_PREPARES ou PDO::ATTR_STRINGIFY_FETCHES, comme celles de Laravel, cela signifie : partagée entre requêtes, et entre Fibers qui construisent avant d'interroger, mais un constructeur qui arrive pendant qu'une autre requête détient la connexion ouvre la sienne. Une revendication court de la première requête SQL du détenteur jusqu'à la fin de sa requête.
Une connexion que rien n'utilise est vérifiée comme auparavant, y compris celle dont le détenteur a abandonné son dernier handle, si bien qu'une connexion réellement morte est toujours remplacée. Cette vérification est une commande comme une autre et elle est revendiquée comme telle : une requête SQL arrivant par un client protégé pendant qu'elle est en cours l'attend, dans la borne habituelle. Les cas non couverts ci-dessus restent non couverts ici aussi.
Activation et coût
1, true et all activent toutes les catégories, streams et net comprises. 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, ou RUNTIME_HOOKS=sleep,streams pour conserver ce qu'activait la 0.12.0) 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. Activer net modifie le même type d'entrées dans les tables d'opérations des autres transports de socket et d'OpenSSL, aux mêmes conditions, et fait répondre la bibliothèque passerelle à poll() pour les appels que fait le code propre de PHP ; tout autre appelant, le serveur lui-même compris, est transmis directement à la libc.
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 sans indicateur d'environnement à définir, etoxphp_sleep()accepte des secondes fractionnaires (oxphp_sleep(0.25)), une précision que lesleep()natif (secondes entières) ne peut pas exprimer. Ils ne sont pas exemptés des endroits où une Fiber ne peut pas suspendre : là, ils détiennent le thread worker exactement comme les fonctions natives interceptées. - Activez
RUNTIME_HOOKS=sleeppour le code que vous ne pouvez pas modifier, comme un framework ou une bibliothèque tierce qui appelle directement lessleep()/usleep()natifs. Il est désactivé par défaut, ne fait rien hors d'une Fiber et conserve le contrat de retour natif : unsleep()intercepté prend toujours unint, renvoie0quand il suspend et ce que renvoie la fonction native quand il se replie sur elle. La vérification de l'argument est la sienne, comme indiqué plus haut.
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\*. Défini à false, aucune de ses classes n'est enregistrée : new OxPHP\Shared\Counter() lève Error: Class "OxPHP\Shared\Counter" not found, et /__ox_shared/* ainsi que les métriques oxphp_shared_* sont absents. Protégez avec class_exists('OxPHP\Shared\Counter') le code qui doit fonctionner dans les deux cas. Ne protégez pas avec interface_exists('OxPHP\Shared\Shareable') : cela reste vrai, car l'extension enregistre cette interface dans tous les cas |
SHARED_MAX_ENTRIES |
100000 |
Plafond global sur le nombre d'objets Shared vivants (chaque Counter, Map, Channel, … compte pour un). Créer un objet au-delà échoue avec CapacityException. Les clés à l'intérieur d'une Map n'y comptent pas ; bornez une Map avec maxEntries |
SHARED_MAX_BYTES |
1073741824 (1 GiB) |
Plafond global sur la mémoire estimée pour l'ensemble des objets Shared, vérifié uniquement à la création d'un objet (CapacityException). Les objets qui existent déjà peuvent continuer à grossir au-delà, si bien que le total peut dépasser le plafond |
SHARED_SOFT_LIMIT_RATIO |
— | Déprécié et ignoré ; le définir journalise un WARN au démarrage. Il n'a jamais eu d'effet : rien n'est délesté à l'approche de SHARED_MAX_BYTES / SHARED_MAX_ENTRIES, qui ne font que refuser la création d'un nouvel objet Shared une fois atteints. Pour être prévenu à l'avance, alertez sur oxphp_shared_capacity_saturation, publié tant que SHARED_METRICS_ENABLED est actif (voir Observabilité de Shared*) |
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 |
— | Déprécié et ignoré ; le définir journalise un WARN au démarrage. Il n'a jamais eu d'effet, et il n'existe aucune politique globale d'empoisonnement qu'il puisse régler : une exception levée à l'intérieur d'une closure Shared\Mutex n'empoisonne jamais le mutex, et le fait qu'une factory Shared\Once en échec désactive définitivement la cellule se choisit par instance ; passez Once\FailureMode::Poison au constructeur (voir Shared\Once) |
SHARED_LOCK_DIAGNOSTICS |
warn (strict dans un build de débogage du binaire) |
Détecteur d'interblocages inter-threads pour Shared\Mutex : off, warn (journalise et compte les cycles) ou strict (les rompt en plus avec DeadlockException). En minuscules uniquement ; toute autre valeur donne silencieusement la valeur par défaut. Voir Détecteur d'interblocage |
SHARED_LOCK_POLL_INTERVAL_MS |
100 |
Fréquence d'analyse du détecteur d'interblocages, en millisecondes. Inutilisé quand SHARED_LOCK_DIAGNOSTICS=off |
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) | Secret que doivent porter les déclencheurs explicites (l'en-tête X-OxPHP-Profile, le cookie OXPROF, le paramètre de requête __oxprof), et jeton bearer des routes /__profiler/* du serveur interne. Non défini ou vide = aucun jeton requis : toute valeur de déclencheur non vide passe, et les routes /__profiler/* répondent sans bearer. L'échantillonnage PROFILER_SAMPLE_RATE et les fonctions PHP OxPHP\Profile\* ne le consultent jamais : avec PROFILER_ENABLED=true, un script qui appelle OxPHP\Profile\start() profile sa propre requête sans qu'aucun jeton n'intervienne |
PROFILER_MAX_SPANS |
50000 |
Plafond de spans de profil par requête. Les profils dépassant ce plafond sont tronqués |
PROFILER_MAX_DEPTH |
0 (pas de plafond) |
Profondeur d'imbrication maximale des appels enregistrés ; un appel plus profond et, sauf en cas d'auto-récursion au-delà de 32 frames, tout ce qu'il appelle sont absents du profil. Leur temps, lui, ne l'est pas : il apparaît comme temps exclusif de l'appel situé à la limite. Les appels écartés comptent tout de même dans PROFILER_MAX_SPANS. Ramené à 32, la profondeur que suit l'observateur du moteur ; avec PROFILER_ENABLED=true, une valeur plus grande est signalée comme ramenée au démarrage |
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
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:9090Production (Framework)
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_BROTLI_LEVEL=5
COMPRESSION_ZSTD_LEVEL=6
COMPRESSION_GZIP_LEVEL=6
STATIC_MAX_AGE=30dProduction (mode worker)
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:9090TLS
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.phpInspecter la configuration active
Lorsque le serveur interne est en cours d'exécution, interrogez l'endpoint /config pour voir la configuration résolue :
curl -s http://localhost:9090/config | jq .{
"access_log": "all",
"async_in_flight_cap": 0,
"async_max_fibers": 256,
"async_queue_capacity": 0,
"async_workers": 0,
"brotli_level": 5,
"deny_file": {"loaded": false},
"document_root": "/var/www/html/public",
"drain_timeout_seconds": 25,
"entry_file": "/var/www/html/public/index.php",
"executor_type": "sapi",
"gzip_level": 6,
"header_timeout_seconds": 5,
"listen_addr": "0.0.0.0:443",
"log_level": "warn",
"max_connections": 10000,
"max_query_body": 524288,
"php_workers": "8",
"plugins": {
"apm": {
"db_capture_params": false,
"enabled": true,
"hooks_registered": 34,
"slow_query_ms": 100
},
"otel": {
"enabled": true,
"protocol": "grpc",
"service_name": "oxphp"
}
},
"queue_capacity": 1024,
"queue_max_waiting": 1024,
"queue_max_waiting_bytes": 67108864,
"queue_wait_timeout_ms": 1000,
"rate_limit": 100,
"rate_window_seconds": 60,
"runtime_hooks": [],
"static_max_age": 2592000,
"static_revalidate": false,
"superglobals_enabled": true,
"tls_enabled": true,
"tls_min_version": "1.2",
"tokio_workers": 4,
"trace_context": true,
"trusted_proxies": false,
"worker_max_memory_mib": 0,
"worker_mode_enabled": false,
"zstd_level": 6
}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.
Prompts pour votre agent
Ces prompts supposent un agent connecté au serveur MCP de la documentation OxPHP — il lira lui-même les pages dont il a besoin.
Règle OxPHP pour mon projet. Lis la page `operations/configuration` via le serveur MCP de la documentation OxPHP.
Regarde ce que fait tourner ce dépôt — le framework, le nombre de workers qu'il peut se permettre, la forme des requêtes qu'il sert — et donne-moi un bloc de variables d'environnement de production avec une justification pour chaque valeur que tu retiens.Passe en revue ma configuration OxPHP. Lis la page `operations/configuration` via le serveur MCP de la documentation OxPHP.
Compare les variables d'environnement que ce dépôt définit aux valeurs par défaut, et dis-moi lesquelles de mes surcharges ne servent à rien, lesquelles sont risquées, et quelles valeurs par défaut j'aurais dû surcharger sans le faire.Voir aussi
- Routage — modes de routage et comportement de
ENTRY_FILE - Contrôles de santé — endpoints du serveur interne
- Métriques — référence des métriques compatibles Prometheus
- Arrêt gracieux — comment
DRAIN_TIMEOUT_SECONDSinflue sur l'arrêt - TLS — configuration TLS et exigences relatives aux certificats
- Limitation de débit — détails de la limitation de débit par IP
- Mode worker — architecture des workers PHP persistants
- Compression — choix du codec et niveaux par codage
- Fichiers statiques — mise en cache et service de fichiers
- Traçage distribué & APM — export OTel, auto-instrumentation et SDK de traçage PHP