Contrôles de santé

OxPHP exécute un serveur HTTP interne sur un port distinct pour la surveillance de la santé, la collecte de métriques et l'inspection de la configuration. Il reste isolé du trafic applicatif, si bien que la surveillance n'entre jamais en concurrence avec les requêtes des utilisateurs.

Configuration

Définissez INTERNAL_ADDR pour activer le serveur interne :

bash
INTERNAL_ADDR=127.0.0.1:9090

Lorsque INTERNAL_ADDR n'est pas défini, le serveur interne ne démarre pas et aucun endpoint de santé n'est disponible.

Note

Un INTERNAL_ADDR réduit à un port (:9090) se lie à 127.0.0.1 ; utilisez un 0.0.0.0:9090 explicite pour exposer le serveur interne en dehors de l'hôte. Lorsqu'il est joignable en dehors de l'hôte, restreignez l'accès avec INTERNAL_ALLOW_IPS (une liste d'autorisation CIDR/IP — /metrics, /config et les chemins de plugins renvoient 403 aux pairs qui n'en font pas partie, tandis que les sondes de santé restent joignables) ; le loopback n'est pas implicite, indiquez donc 127.0.0.1/32 pour conserver l'accès depuis localhost. Le serveur émet un avertissement au démarrage si l'écouteur est exposé sans liste d'autorisation.

Sondes Kubernetes

OxPHP fournit des endpoints dédiés pour chaque type de sonde Kubernetes. Chaque endpoint est également disponible sous un alias court (/healthz, /readyz, /startupz).

Endpoint Alias Vérifie 200 503
/health/liveness /healthz Rien (vivant s'il répond) Toujours Jamais
/health/readiness /readyz Pas d'arrêt en cours, threads de workers actifs, pool non bloqué, aucun plugin en échec Prêt Pas prêt
/health/startup /startupz Threads de workers actifs Prêt Pas prêt

Liveness renvoie toujours 200 OK. Si le processus peut répondre à la requête HTTP, c'est qu'il est vivant. Aucune vérification de l'exécuteur ni des plugins n'est effectuée — cela empêche Kubernetes de redémarrer les pods à cause de problèmes transitoires du pool de workers.

Readiness renvoie 503 Service Unavailable lorsque :

  • Le serveur est en cours d'arrêt (arrêt gracieux en cours)
  • Tous les threads de workers PHP se sont terminés (le pool n'a plus personne à qui confier une requête)
  • Le pool est bloqué : des requêtes attendent des workers inactifs qui n'avancent sur rien. Cet état n'est détecté qu'en mode worker (voir Le signal de pool bloqué)
  • Un plugin signale une défaillance

Pendant l'arrêt gracieux, la sonde de readiness renvoie immédiatement 503, ce qui pousse Kubernetes à retirer le pod des endpoints du Service avant que le drainage ne se termine.

Startup renvoie 503 Service Unavailable tant qu'aucun thread de worker ne tourne. Utilisez cette sonde pour empêcher des arrêts prématurés dus à la sonde de liveness durant une initialisation lente. Elle rend compte des threads, qui existent dès la création du pool. Elle ne rend pas compte du bootstrap de l'application qui s'exécute à l'intérieur.

Le signal de pool bloqué

Un pool peut cesser de prendre du travail dans sa file alors que chacun de ses threads est vivant : les requêtes s'accumulent, aucun worker n'est occupé, et rien de ce qui est en file n'est jamais exécuté. Chaque requête reçoit tout de même une réponse une fois son propre budget d'attente épuisé (un 529, ou un 499 lorsque le client a cessé d'attendre le premier), si bien que, vu de l'extérieur, cela ne ressemble pas à un blocage. Les requêtes reçoivent encore une réponse, mais aucune n'est exécutée, et la file se remplit malgré tout, car seule une prise en charge libère une entrée. Tous les autres signaux restent au vert pendant ce temps : les threads sont tous là, les plugins vont bien, les fichiers statiques sont servis normalement. Un pool encore dans cet état une minute plus tard ne se rétablit généralement pas de lui-même, et c'est pourquoi la sonde de readiness le signale.

Le serveur guette une combinaison qu'un pool en service ne maintient pas dans la durée : du travail en attente (des requêtes en file, ou des refus d'admission en hausse) alors qu'au moins un worker est inactif et que le pool n'a rien terminé depuis le scan précédent.

  • Deux scans consécutifs d'une seconde suffisent pour une ligne de journal.
  • La sonde de readiness attend une minute de plus. Un pool dont tous les workers se sont arrêtés en même temps (le plus souvent un pool d'un seul worker) présente exactement cette forme pendant qu'un remplaçant réexécute le bootstrap de l'application. Le nouveau worker sert de nouveau bien avant la fin de la minute, et retirer le réplica trop tôt le priverait du trafic dont il a besoin pour le montrer.
  • La sonde de readiness se rétablit lorsqu'un worker termine réellement une requête (une accalmie n'est pas un rétablissement), ou après une minute pendant laquelle le pool n'a pas du tout semblé bloqué. La seconde issue n'a besoin d'aucun trafic : un pool bloqué conserve sa file, il ne peut donc pas remplir cette condition, alors qu'un pool pris à tort pour bloqué le peut. Sans elle, l'état serait permanent, car le 503 arrête précisément les requêtes dont l'achèvement constitue l'autre issue.
Note

Le signal a besoin du nombre de requêtes traitées par les workers, que seul le mode worker (WORKER_MODE_ENABLED=true) tient à jour, si bien qu'il n'existe qu'en mode worker. Ailleurs, la sonde de readiness répond sur les trois autres conditions, et oxphp_pool_stalled est absente plutôt qu'exportée comme une constante 0.

Ce que les sondes ne signalent pas

La charge. Un serveur surchargé reste Ready. Une fois la file d'admission pleine et le budget d'attente épuisé, les requêtes sont refusées avec 529 Site is overloaded, et un réplica qui fait cela répond toujours, rapidement, avec un pool qui fonctionne. Le retirer de la rotation déplacerait son trafic vers ses voisins et, sous une surcharge uniforme, cela retire chaque réplica à tour de rôle jusqu'à ce que le Service n'ait plus aucun endpoint : un site dégradé devient un site injoignable, avec la charge toujours présente. Surveillez plutôt oxphp_admission_refused_total{reason=...} et ajoutez de la capacité. Lisez cependant oxphp_admission_wait_wasted_total en regard de oxphp_admission_wait_budget_us : un refus wait_timeout n'implique pas à lui seul un pool qui vient à bout de son travail, et lorsque le pool ne libère aucun worker pendant toute l'attente d'une requête, le remède est l'inverse : raccourcir le budget d'attente plutôt qu'agrandir le pool. Ce compteur ne compte que tant que le budget est à son plafond. Le serveur le dit aussi dans son journal : un WARN au scan qui commence à délester, répété environ une fois par minute tant que cela dure, et un INFO une fois qu'une minute entière s'est écoulée sans refus. Consultez Métriques Prometheus.

Un budget de connexions épuisé. Lorsque les permis MAX_CONNECTIONS sont épuisés, la boucle d'acceptation se met en attente et les nouveaux clients n'obtiennent aucune réponse, mais l'écouteur interne ne passe pas par ce budget, si bien que les sondes continuent de répondre. Il s'agit là aussi de charge plutôt que d'une panne, et cela ne modifie pas la readiness ; oxphp_accept_stalled le signale.

Les réponses lentes. Rien ici ne mesure la latence. Un pool qui répond à tout en retard est Ready.

Tous les endpoints de sonde renvoient Content-Type: text/plain avec le nom de la sonde comme corps de réponse (par exemple, readiness). Kubernetes n'inspecte que le code de statut HTTP.

bash
# Quick check curl -s -o /dev/null -w '%{http_code}' http://localhost:9090/health/readiness

GET /health

Renvoie l'état de santé complet du serveur au format JSON. Utilisez-le pour les tableaux de bord et les systèmes de surveillance, pas pour les sondes Kubernetes.

bash
curl http://localhost:9090/health

Réponse saine (200 OK) :

json
{ "active_connections": 7, "executor_healthy": true, "plugins": {}, "pool_stalled": false, "status": "ok", "total_requests": 48203, "uptime_secs": 3612 }

Réponse dégradée (503 Service Unavailable) :

json
{ "active_connections": 7, "executor_healthy": false, "plugins": {}, "pool_stalled": false, "status": "degraded", "total_requests": 48203, "uptime_secs": 3612 }
Champ Type Description
status string "ok" lorsque tous les sous-systèmes sont sains, "degraded" sinon. La charge n'est pas une défaillance de sous-système : un serveur qui renvoie des 529 sous surcharge signale "ok"
uptime_secs integer Secondes écoulées depuis le démarrage du serveur
total_requests integer Nombre total de requêtes HTTP traitées sur le port principal
active_connections integer Connexions actuellement ouvertes sur le port principal
executor_healthy boolean Indique si au moins un thread de worker PHP tourne encore. false signifie que le pool n'a plus aucun thread à qui confier une requête
pool_stalled boolean Indique si le pool est bloqué : des requêtes attendent des workers inactifs qui n'avancent sur rien. Toujours false en dehors du mode worker, où cet état n'est pas détecté (voir Le signal de pool bloqué). true fait basculer le statut HTTP vers 503
plugins object<string, string> Santé par plugin : les clés sont les noms des plugins, les valeurs sont "ok", "degraded" ou "failed". {} vide lorsqu'aucun plugin ne rapporte de santé. Un plugin "failed" fait basculer le statut HTTP vers 503 ; "degraded" apparaît ici mais maintient le statut à 200.

GET /metrics

Renvoie des métriques compatibles Prometheus au format d'exposition texte. Consultez Métriques Prometheus pour la référence complète des métriques.

bash
curl http://localhost:9090/metrics

GET /config

Renvoie la configuration active du serveur au format JSON. Les chemins du certificat et de la clé TLS, internal_addr et error_pages_dir sont expurgés de la réponse pour des raisons de sécurité.

bash
curl -s http://localhost:9090/config | jq .
json
{ "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": "info", "max_connections": 10000, "max_query_body": 524288, "php_workers": "8", "plugins": {}, "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": false, "trusted_proxies": false, "worker_max_memory_mib": 0, "worker_mode_enabled": false, "zstd_level": 6 }
Note

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 expurgés de la réponse servie.

Intégration Kubernetes

Utilisez des endpoints de sonde dédiés pour chaque type de sonde :

yaml
apiVersion: apps/v1 kind: Deployment spec: template: spec: containers: - name: oxphp image: ghcr.io/oxphp/oxphp:0.13.0 env: - name: INTERNAL_ADDR value: "0.0.0.0:9090" - name: INTERNAL_ALLOW_IPS # the Prometheus scraper's network, plus loopback value: "10.0.0.0/8,127.0.0.1/32" ports: - containerPort: 80 - containerPort: 9090 startupProbe: httpGet: path: /health/startup port: 9090 initialDelaySeconds: 1 periodSeconds: 2 failureThreshold: 15 livenessProbe: httpGet: path: /health/liveness port: 9090 periodSeconds: 10 failureThreshold: 3 readinessProbe: httpGet: path: /health/readiness port: 9090 periodSeconds: 5 failureThreshold: 2
Sonde Effet en cas d'échec
Startup Kubernetes attend — ne tue pas le pod pendant l'initialisation
Liveness Kubernetes redémarre le pod
Readiness Kubernetes retire le pod des endpoints du Service (pas de redémarrage)

Les alias courts (/healthz, /readyz, /startupz) sont totalement équivalents et peuvent être utilisés à la place.

Contrôle de santé Docker Compose

Cet exemple lie le serveur interne à 127.0.0.1, alors que celui pour Kubernetes le lie à 0.0.0.0 : Docker exécute la commande healthcheck dans le propre espace de noms réseau du conteneur, où le loopback atteint l'écouteur, tandis que le kubelet envoie ses sondes httpGet depuis l'extérieur de l'espace de noms réseau du pod, où ce n'est pas le cas.

compose.yaml
services: oxphp: image: ghcr.io/oxphp/oxphp:0.13.0 ports: - "8080:80" environment: INTERNAL_ADDR: "127.0.0.1:9090" healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1:9090/health"] interval: 10s timeout: 5s retries: 3 start_period: 5s

Docker marque le conteneur comme unhealthy après le nombre configuré d'échecs de tentatives, ce qui peut déclencher des politiques de redémarrage ou le retrait du répartiteur de charge.

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.

prompt
Mets en place les contrôles de santé OxPHP pour mon projet. Lis la page `operations/health-checks` via le serveur MCP de la documentation OxPHP. Ajoute un healthcheck à mon docker-compose.yml et des sondes liveness et readiness à mon Deployment Kubernetes, en te fondant sur les services et les ports que ce dépôt définit déjà.
prompt
Mon conteneur OxPHP est redémarré par son orchestrateur et je ne sais pas pourquoi. Lis la page `operations/health-checks` via le serveur MCP de la documentation OxPHP. Confronte ma configuration de contrôles de santé à ce que le serveur interne expose réellement, et dis-moi laquelle des deux est fausse.

Voir aussi

Une erreur ? Signalez-la →