sixty

pour les applications PHP et Laravel

Pas de thread de fond. Rien de perdu.

Un worker PHP ne peut pas faire tourner de minuterie, et tout ce qu’il a appris meurt avec la requête. C’est le problème que cet agent devait résoudre avant de pouvoir mesurer quoi que ce soit : les fenêtres sont mises en commun en mémoire partagée et fusionnées — sans perte, sinon les percentiles seraient de la fiction — et l’envoi se fait après que la réponse est déjà partie, donc un collecteur qui a une mauvaise journée ne coûte rien à votre lecteur.

Ce qu’il mesure

Chaque requête, et les méthodes en dessous
Laravel et Symfony branchent eux-mêmes le span de requête. Sixty::trace('OrdersQuery#forUser', fn () => …) marque le code entre le contrôleur et la base — une ligne plutôt qu’un décorateur, parce que PHP n’a pas d’étape de build qui réécrit les fonctions ni de point d’accroche qui se déclenche quand une méthode est définie.
Chaque requête, sans remplacer votre connexion
PDO est instrumenté en fixant la classe de statement sur la connexion qui existe déjà. Une sous-classe de PDO devrait ouvrir la sienne, doublant silencieusement le nombre de connexions de chaque déploiement — exactement le genre de chose qu’un outil de surveillance ne doit jamais faire.
Laravel et Doctrine, sur n’importe quel pilote
Les connexions Laravel sont récupérées à ConnectionEstablished, avec les lignes depuis rowCount(). Doctrine DBAL reçoit un middleware enregistré par le bundle, avec les lignes depuis le count du résultat lui-même. Les deux vous donnent la forme de la requête et l’attribution.
MongoDB, y compris Doctrine ODM
Via la surveillance des commandes du pilote. L’identité est construite à partir des clés uniquement, donc aucune de vos valeurs n’a de chemin vers elle — et les allers-retours du curseur sont comptés, le signal qui attrape une lecture arrivant en trois cents versements.
Ce que la base a fait pour répondre
Un EXPLAIN à plan générique sur Postgres, mis en cache par requête et émis en dehors du span de l’appelant, si bien que « cette requête a cessé d’utiliser son index » arrive sans qu’un seul de vos paramètres ait jamais été lié.

Ce qu’il trouve

Chacune de ces choses est comparée à la version précédente : le constat nomme donc le changement plutôt que le jour.

Une requête qui a cessé d’utiliser son index

Le constat qui nomme une cause plutôt qu’un symptôme. Il arrive souvent pendant que tout est encore rapide, parce que la table est encore petite — et devient une panne quelques semaines plus tard, quand elle ne l’est plus.

Un N+1 qui n’était pas là à la version précédente

Une requête par appel en est devenue vingt. Toutes rapides, toutes indexées, et la seule chose qui a changé, c’est le nombre.

Une requête qui s’est mise à lire toute la table

Les lignes par appel sont passées de 30 à 30 000, en général un where sorti de la requête et entré dans PHP lors d’une refonte.

Une lecture devenue des centaines d’attentes réseau

MongoDB répond à un curseur par lots. Quand un résultat dépasse un lot, une lecture devient des dizaines ou des centaines d’allers-retours successifs — des documents identiques, et rien ne bouge que l’horloge.

Votre propre code qui ralentit

Le temps dans la méthode elle-même plutôt que dans ce qu’elle a appelé, séparé, pour que vous regardiez dans le bon fichier du premier coup.

Une requête qui ne renvoie plus rien, en silence

Réussie, rien de levé, pas lente, et revenue vide là où elle revenait avec des lignes.

Ce qui arrive dans votre fil

Pas un tableau de bord à lire. Une carte par constat, avec les chiffres, la version qui les a changés, et les preuves dont votre agent de code a besoin pour écrire le correctif.

Ce sont les deux régressions qu’apps/demo-laravel livre exprès, contre le jeu de données que crée son seeder — environ 30 000 commandes. Vous pouvez le lancer et les regarder arriver.

lignesOrdersQuery#forUser
30 lignes30 000 lignes1000×

La contrainte est sortie de la requête et entrée dans PHP lors d’une refonte, donc chaque appel charge toute la table. Même réponse, même relecture de code, et quelques millisecondes sur une base de développement.

depuis la version v2, contre la v1

N+1OrdersQuery#enrich
1 requête20 requêtes20×

Une recherche groupée est devenue une recherche par commande. Chaque requête est rapide et indexée, donc aucune requête HTTP n’a l’air fausse — le nombre est la seule chose qui a bougé.

depuis la version v2, contre la v1

Comment ça s’installe

Laravel découvre le service provider ; Symfony a besoin que le bundle soit ajouté à config/bundles.php. Les deux branchent ensuite le span de requête, l’instrumentation des requêtes et l’envoi — il n’y a pas d’initializer à écrire et rien à appeler.

claude mcp add sixty \
  -e SIXTY_API_KEY=sixty_sk_… \
  -e SIXTY_ENDPOINT=https://ingest.sixty.sh \
  -- npx -y @sixty-sh/mcp

# or by hand:
composer require sixty-sh/sixty

SIXTY_API_KEY=sixty_sk_…
SIXTY_SERVICE=checkout-api

# On Laravel, that is the install — the provider is discovered.
# On Symfony, add the bundle to config/bundles.php.

# Anything else:
\Sixty\Sixty::init();
\Sixty\Instrument\Pdo::instrument($pdo);

# Your own code:
\Sixty\Sixty::trace('OrdersQuery#forUser', fn () => ...);

Installez aussi ext-apcu et les fenêtres de chaque worker fusionnent en une charge utile par intervalle. Sans lui l’agent fonctionne toujours et le dit une fois au démarrage plutôt que de faire semblant — c’est simplement beaucoup plus de trafic. L’identifiant de version vient tout seul de votre plateforme, ou de SIXTY_RELEASE.

Ce qu’il ne fait pas

  • Il refuse de s’activer sous les coroutines Swoole, et dit pourquoi. Là-bas, plusieurs requêtes partagent un worker et basculent à chaque frontière d’E/S, donc le span courant créditerait les requêtes de l’une au contrôleur d’une autre. FPM, CLI, RoadRunner et FrankenPHP sont un processus par requête et sont pleinement pris en charge.
  • Sans ext-apcu, chaque requête envoie sa propre fenêtre. Cela fonctionne toujours et l’agent vous le dit une fois au démarrage — mais la fusion sans perte entre workers est ce qui rend les percentiles identiques à ce qu’aurait remonté un processus unique mesurant tout, et sans elle vous payez en trafic.
  • PDO::query() et PDO::exec() n’atteignent jamais une classe de statement, donc une connexion que vous construisez vous-même et sur laquelle vous les appelez a besoin de TracedPdo. prepare() + execute() — chaque requête émise par Laravel et Doctrine — est couvert.
  • Si quelque chose d’autre possède déjà la classe de statement — un profileur, une barre de débogage — l’agent la laisse tranquille et ne mesure rien plutôt que de la casser. C’est le bon compromis et il est silencieux, donc il vaut la peine de savoir que cela peut arriver.
  • Le CPU et l’attente ne sont pas séparés, et les plans de requête ne concernent que Postgres. MySQL et MongoDB ne peuvent expliquer qu’une requête qui contient encore ses valeurs, et cet agent jette les valeurs dès qu’il les voit.

Si vous êtes ici pour l’une de ces raisons

Découvrez ce qu’a fait votre dernier changement.

Essayez !

Vous construisez autre chose ? Voyez services Node, applications Ruby et Rails ou services Python — ces pages sont différentes, et ce que nous voyons aussi.

Surveillance des performances pour PHP et Laravel — Postgres, MySQL et MongoDB — Sixty