Observabilité de Shared*
Chaque instance OxPHP\Shared\* est une entrée de registre que le runtime suit déjà pour son refcount et sa capacité. Ce suivi est exposé aux opérateurs sous forme d'introspection JSON sous /__ox_shared/* et de métriques Prometheus sous oxphp_shared_*. Cette page en est à la fois la référence et le guide de terrain.
Activation
L'observabilité s'appuie sur le serveur interne. Définissez INTERNAL_ADDR pour le démarrer :
INTERNAL_ADDR=127.0.0.1:9090Les endpoints JSON et /metrics sont alors accessibles à cette adresse. Aucune configuration supplémentaire n'est nécessaire.
Vous pouvez désactiver l'un ou l'autre indépendamment :
| Variable d'env | Défaut | Effet |
|---|---|---|
SHARED_INTROSPECTION_ENABLED |
true |
Active/désactive l'API JSON /__ox_shared/*. |
SHARED_INTROSPECTION_PREVIEW_ENABLED |
true |
Active/désactive /preview (les aperçus de forme de valeur peuvent divulguer des données). |
SHARED_METRICS_ENABLED |
true |
Active/désactive les métriques Prometheus oxphp_shared_*. |
Désactivez l'introspection dans les déploiements à locataires hostiles ; les métriques sont uniquement agrégées et peuvent rester activées sans risque.
Endpoints d'introspection
Toutes les réponses sont Content-Type: application/json; charset=utf-8. Les paramètres de requête utilisent l'encodage URL standard.
GET /__ox_shared/summary
Instantané de haut niveau : comptes agrégés par type, mémoire, débit d'opérations et saturation par rapport aux plafonds configurés.
{
"total_entries": 127,
"total_bytes": 2_481_664,
"by_type": {
"Counter": { "count": 48, "bytes": 3_072, "ops": 1_402_391 },
"Map": { "count": 12, "bytes": 1_638_400, "ops": 48_201 },
"Pool": { "count": 4, "bytes": 16_384, "ops": 67_014 }
},
"limits": { "max_entries": 100_000, "max_bytes": 1073741824, "soft_ratio": 0.7 },
"saturation": { "entries": 0.00127, "bytes": 0.00231 },
"diagnostics": {
"lock_diagnostics_level": "warn",
"cycle_detect_depth": 16,
"poison_strict": false
}
}Utilisez summary dans les tableaux de bord et les alertes cron. Un seul scrape vous donne l'état de santé par type et la marge de capacité disponible.
GET /__ox_shared/entries?limit=N
Liste les entrées en direct (limitées à limit, 100 par défaut, 500 au maximum). Une ligne par entrée :
{
"items": [
{ "id": 42, "type": "Map", "refcount": 2, "ops": 1820, "mem_bytes": 204_800, "age_sec": 612 },
{ "id": 43, "type": "Counter", "refcount": 3, "ops": 48_014, "mem_bytes": 64, "age_sec": 612 }
],
"next_cursor": null,
"total_matching": 127
}refcount est le compte de rétention externe : le nombre de wrappers PHP et d'entrées Shared imbriquées qui maintiennent celle-ci. Lorsque vous vous attendez à ce qu'une entrée soit récupérable par le GC mais qu'elle ne l'est pas, c'est le champ à vérifier.
GET /__ox_shared/entry?id=N
Détail spécifique au type pour une entrée :
{
"id": 42,
"type": "Map",
"refcount": 2,
"ops": 1820,
"mem_bytes": 204_800,
"age_sec": 612,
"type_specific": {
"key_count": 1_240,
"max_entries": 50_000,
"saturation": 0.0248,
"sample_keys": ["tenant:acme", "tenant:beta", "..."]
}
}type_specific varie selon le type : Pool expose { size, in_use, idle, waiting, idle_by_thread, max_size }, Channel expose { capacity, pending, closed, senders_blocked, receivers_blocked }, Counter expose { value }, et ainsi de suite.
GET /__ox_shared/preview?id=N
Aperçu de la forme des valeurs scalaires et des petits tableaux. Les valeurs de type chaîne sont tronquées à SHARED_PREVIEW_STRING_LIMIT (256 octets par défaut) ; les tableaux affichent les SHARED_PREVIEW_ARRAY_LIMIT premières entrées (20 par défaut). Conditionné par SHARED_INTROSPECTION_PREVIEW_ENABLED.
{ "id": 42, "type": "Counter", "preview": "1420" }Utilisez preview pendant le développement ; désactivez-le en production lorsque les valeurs peuvent contenir des données utilisateur.
GET /__ox_shared/types
Énumère le catalogue de types v1, utile pour l'outillage généré qui a besoin de la correspondance tag → classe :
{
"types": [
{ "tag": 10, "name": "Counter", "php_class": "OxPHP\\Shared\\Counter" },
{ "tag": 11, "name": "Flag", "php_class": "OxPHP\\Shared\\Flag" },
{ "tag": 12, "name": "Once", "php_class": "OxPHP\\Shared\\Once" },
{ "tag": 20, "name": "Map", "php_class": "OxPHP\\Shared\\Map" },
{ "tag": 30, "name": "Mutex", "php_class": "OxPHP\\Shared\\Mutex" },
{ "tag": 31, "name": "Channel", "php_class": "OxPHP\\Shared\\Channel" },
{ "tag": 50, "name": "Pool", "php_class": "OxPHP\\Shared\\Pool" }
]
}GET /__ox_shared/graph?id=N[&depth=D][&edges=E]
Parcours en largeur (BFS) des références Shareable sortantes à partir de id=N. Retourne les nœuds et les arêtes du sous-graphe accessible. Valeurs par défaut : depth=16, edges=500. Lorsque le budget du walker est atteint, la réponse indique truncated: true.
{
"root": 42,
"nodes": [
{ "id": 42, "type": "Map", "refcount": 2, "mem_bytes": 204_800 },
{ "id": 51, "type": "Counter", "refcount": 1, "mem_bytes": 64 }
],
"edges": [
{ "from": 42, "to": 51, "key": "hits" }
],
"truncated": false
}Utilisez graph après une CycleException pour voir le chemin accessible qu'a emprunté le walker, ou pour diagnostiquer « pourquoi ce Counter n'est-il pas récupéré par le GC » : le graphe montre chaque parent qui maintient une rétention sur lui.
Métriques Prometheus
Toutes les métriques sont exposées sur GET /metrics aux côtés des métriques principales du serveur.
À l'échelle du registre
| Métrique | Type | Labels | Description |
|---|---|---|---|
oxphp_shared_objects_total |
gauge | type |
Nombre d'entrées en direct par type. |
oxphp_shared_operations_total |
counter | type |
Nombre cumulé d'opérations distribuées à chaque type. |
oxphp_shared_bytes |
gauge | type |
Octets approximatifs par type (±30 % par rapport à mallinfo). |
oxphp_shared_total_bytes |
gauge | — | Somme sur l'ensemble des types. |
oxphp_shared_capacity_saturation |
gauge | kind |
entries et bytes en fractions de leurs plafonds. |
oxphp_shared_deadlock_detected_total |
counter | — | Cycles d'attente inter-threads détectés. |
Channel
| Métrique | Type | Labels |
|---|---|---|
oxphp_shared_channel_count |
gauge | channel_id |
oxphp_shared_channel_pending (déprécié) |
gauge | channel_id |
oxphp_shared_channel_senders_blocked |
gauge | channel_id |
oxphp_shared_channel_receivers_blocked |
gauge | channel_id |
oxphp_shared_channel_items_sent_total |
counter | channel_id |
oxphp_shared_channel_items_dropped_total |
counter | channel_id |
oxphp_shared_channel_pending est l'ancienne orthographe de
oxphp_shared_channel_count ; les deux séries portent la même valeur pendant
la fenêtre de dépréciation et divergeront lorsque l'alias sera supprimé dans
une version future. Branchez les nouveaux tableaux de bord sur _count.
Map
| Métrique | Type | Labels |
|---|---|---|
oxphp_shared_map_entries |
gauge | map_id |
oxphp_shared_map_max_entries |
gauge | map_id |
oxphp_shared_map_saturation |
gauge | map_id |
Pool
| Métrique | Type | Labels |
|---|---|---|
oxphp_shared_pool_count |
gauge | pool_id |
oxphp_shared_pool_size (déprécié) |
gauge | pool_id |
oxphp_shared_pool_in_use |
gauge | pool_id |
oxphp_shared_pool_idle |
gauge | pool_id |
oxphp_shared_pool_waiting |
gauge | pool_id |
oxphp_shared_pool_acquire_total |
counter | pool_id |
oxphp_shared_pool_evicted_total |
counter | pool_id, reason |
oxphp_shared_pool_wait_seconds |
histogram | pool_id |
oxphp_shared_pool_size est l'ancienne orthographe de
oxphp_shared_pool_count ; les deux séries portent la même valeur pendant
la fenêtre de dépréciation et divergeront lorsque l'alias sera supprimé dans
une version future. Branchez les nouveaux tableaux de bord sur _count.
Labels de oxphp_shared_pool_evicted_total : reason=idle_timeout | evict | shutdown. idle_timeout est une éviction automatique d'un slot inactif, evict est un appel explicite à Pool::evict(), et shutdown correspond au démontage à la sortie du processus.
Counter / Flag / Once / Mutex
Les compteurs, flags, onces et mutex individuels n'embarquent pas de séries de métriques propres : cela ferait gonfler la cardinalité des labels. Utilisez plutôt le compteur oxphp_shared_operations_total{type=...} à l'échelle du registre et le JSON /__ox_shared/entry?id=… pour l'inspection par instance.
Suivi comme travail ultérieur ; aujourd'hui, la visibilité passe par /__ox_shared/entry.
Guides de diagnostic
Le pool est saturé (429 avec échec des tentatives)
Symptômes : les appelants HTTP subissent des délais d'expiration, oxphp_shared_pool_waiting grimpe, oxphp_shared_pool_count reste bloqué à maxSize.
Vérifiez :
curl -s http://localhost:9090/__ox_shared/entry?id=<pool_id> | jq .type_specificRegardez idle_by_thread. S'il vaut {} ou s'il est fortement déséquilibré (le worker 0 a 8 slots inactifs, le worker 3 en a 0), l'acquisition entre en contention pour des threads qui se trouvent occupés ailleurs. En v1, l'affinité par thread ne se rééquilibre pas. Soit augmentez maxSize, soit réduisez le point chaud d'acquisition par thread.
Si idle_by_thread est équilibré mais que tout est en in_use, augmentez maxSize.
Saturation mémoire
Vérifiez oxphp_shared_total_bytes et oxphp_shared_capacity_saturation{kind="bytes"}. Si l'un des deux est élevé :
curl /__ox_shared/entries?limit=500et triez parmem_bytespour trouver les principaux contributeurs.curl /__ox_shared/entry?id=<N>sur chacune pour vérifier la forme. Pour Map, regardezkey_countpar rapport àmax_entries.- Cause la plus fréquente : un
Shared\Mapnon borné indexé par une entrée utilisateur. Le remède est un plafondmaxEntrieset une politique de rétention.
Un wrapper refuse d'être récupéré par le GC
refcount dans /__ox_shared/entries vous indique le nombre de rétentions en cours. S'il reste supérieur à 1 après que le wrapper PHP est sorti de portée, c'est qu'une autre entrée Shared le maintient en vie.
curl -s http://localhost:9090/__ox_shared/graph?id=<N> | jq .nodesParcourez le graphe à rebours. Tout nœud qui atteint l'entrée bloquée détient une rétention. Supprimez la référence ($map->remove($key), fermez le channel, supprimez l'entrée Mutex) et le refcount diminue.
Une CycleException s'est déclenchée en production
Le message de l'exception inclut le chemin accessible qu'a exploré le détecteur de cycles. Faites correspondre ces IDs aux types via /__ox_shared/entries, et interrogez /__ox_shared/graph?id=<root> pour obtenir la forme complète :
# Exception message: "cycle would form: #42 → #51 → #42"
curl -s http://localhost:9090/__ox_shared/graph?id=42 | jqLe résultat visualise la chaîne pour vous permettre de voir où la référence arrière involontaire a été introduite.
Le détecteur d'interblocage s'est déclenché
oxphp_shared_deadlock_detected_total s'incrémente. Consultez les logs du serveur. Le détecteur émet un enregistrement de log par cycle, avec les IDs des mutex impliqués et les threads propriétaires. Pour récupérer :
curl /__ox_shared/entry?id=<mutex_id>sur chacun et confirmezpoisoned=false. S'il est empoisonné, le détecteur a déjà interrompu le cycle.- Si le cycle est un véritable bug de réentrance, refactorisez pour utiliser des mutex distincts par portée de verrou.
- Passez
SHARED_LOCK_DIAGNOSTICS=stricten préproduction pour transformer une future réentrance en échec immédiat plutôt qu'en cycle détecté.
Harnais de soak longue durée
tests/soak/pool_soak.sh est un harnais manuel (hors CI) permettant de vérifier la stabilité de Shared\Pool sur des heures ou des jours de charge continue. Il :
- Démarre l'image de dev avec la mise à l'échelle dynamique des workers (
PHP_WORKERS=4:40par défaut) et unidleTimeoutde pool court, afin que le planificateur d'éviction se déclenche en continu. - Charge
tests/soak/workload.phpcomme bootstrap du worker, qui construit 10 pools ×maxSize=8et sert des acquire/release à chaque requête. - Génère du trafic avec
wrkpendantSOAK_DURATION_MINminutes (1440 par défaut = 24h). - Scrape
/metricset le RSS du conteneur toutes les 60 s danstests/soak/out/<timestamp>/metrics.csv. - Écrit
verify.txtà la fin avec un statut réussite/échec pour les cinq critères de sortie de release (dérive du RSS dans une plage de ±5 %, zéro panique de handle obsolète, zéro entrée fuitée à l'arrêt, évictions par idle-timeout en hausse régulière, zéro déclenchement du détecteur d'interblocage).
Prérequis sur l'hôte : docker, wrk, curl, awk.
Invocations typiques :
# 24h full soak before a release
tests/soak/pool_soak.sh
# 1h smoke for validating the harness itself
SOAK_DURATION_MIN=60 tests/soak/pool_soak.sh
# Heavier concurrency
SOAK_CONCURRENCY=400 SOAK_THREADS=8 tests/soak/pool_soak.shLes artefacts atterrissent dans tests/soak/out/<timestamp>/ :
metrics.csv— une ligne par minute (ts unix, RSS, nombre d'entrées par type, compteurs d'éviction par pool, nombre d'interblocages, ops).server.log— stdout/stderr du conteneur, y compris toute trace de handle obsolète ou de panique.wrk.out/wrk.err— sortie brute du générateur de charge.metrics.final— le dernier scrape de/metricspris juste avant le démontage du conteneur. Sert à confirmer que les nombres d'entrées et d'octets sont revenus au niveau de référence (aucune entrée Shared fuitée) une fois la charge arrêtée.verify.txt— rapport réussite/échec pour les cinq critères de sortie.
Ne branchez pas ceci dans la CI. Une exécution de 24h n'est pas bon marché et sa raison d'être est la confiance avant release, pas la validation continue.
Cadence de scrape
Les endpoints du registre parcourent l'état en direct sous des verrous en lecture ; le scraping est donc peu coûteux, mais pas gratuit. Cadences recommandées :
/metrics— toutes les 15 s (valeur par défaut typique de Prometheus). Uniquement agrégé ; le surcoût est négligeable./__ox_shared/summary— toutes les 60 s pour les tableaux de bord. Légèrement plus lourd que/metrics./__ox_shared/entries— à la demande uniquement. Itère sur tous les shards ; ne le scrapez pas à chaque tick./__ox_shared/entry//preview//graph— à la demande lors des investigations.
Voir aussi
- État partagé — modèle mental et vue d'ensemble des primitives.
- Métriques Prometheus — métriques principales du serveur sous le même endpoint
/metrics. - Serveur interne — comment les endpoints
/__ox_shared/*se branchent surINTERNAL_ADDR. - Migrer vers un stockage externe — lorsque la saturation est structurelle et non ajustable.