sixty

PHP と Laravel のアプリ向け

バックグラウンドスレッドなし。失うものもなし。

PHP のワーカーはタイマーを走らせられず、学んだことはすべてリクエストとともに死にます。それが、このエージェントが何かを測る前に解決しなければならなかった問題です。ウィンドウは共有メモリにプールして統合します — 無損失で。でなければパーセンタイルは作り話になります — そして送信はレスポンスがすでに出たあとに行うので、コレクタの調子が悪い日でも、読んでいる人には何のコストもかかりません。

何を測るか

すべてのリクエストと、その下のメソッド
Laravel と Symfony はリクエストのスパンを自分で配線します。Sixty::trace('OrdersQuery#forUser', fn () => …) がコントローラとデータベースのあいだのコードに印を付けます — デコレータではなく1行なのは、PHP には関数を書き換えるビルド段階も、メソッド定義時に発火するフックもないからです。
接続を置き換えずに、すべてのクエリを
PDO は、すでに存在する接続に statement クラスを設定することで計測します。PDO のサブクラスなら自前の接続を開くことになり、あらゆるデプロイの接続数を黙って倍にします — 監視ツールが絶対にやってはいけない類のことです。
Laravel と Doctrine を、どのドライバでも
Laravel の接続は ConnectionEstablished で拾い、行数は rowCount() から取ります。Doctrine DBAL にはバンドルが登録するミドルウェアが入り、行数は結果自身の count から取ります。どちらでも文の形と帰属が得られます。
MongoDB、Doctrine ODM も含めて
ドライバのコマンド監視を通して。identity はキーだけから作るので、あなたの値がそこへ入る経路はありません — そしてカーソルの往復回数を数えます。1回の読み取りが300回に分けて届くのを捕まえるシグナルです。
データベースが答えるために何をしたか
Postgres 上の汎用プラン EXPLAIN を、文ごとにキャッシュし、呼び出し側のスパンの外で発行します。だから「この文はインデックスを使わなくなった」が、あなたのパラメータを一度も束縛せずに届きます。

何を見つけるか

どれも1つ前のリリースと比較されます。だから検出結果は、日付ではなく変更を名指しします。

インデックスを使わなくなったクエリ

症状ではなく原因を名指しする検出です。まだすべてが速いうちに届くことがよくあります。テーブルがまだ小さいからで、数週間後、小さくなくなったときに障害になります。

前のリリースにはなかった N+1

1リクエストにつき1本だったクエリが20本に。どれも速く、どれもインデックスが効いていて、変わったのは回数だけです。

テーブル全体を読み始めたクエリ

1回あたりの行数が30から30,000へ。たいていはリファクタリングでクエリの外に出て PHP 側に移った where です。

数百回のネットワーク待ちに化けた読み取り

MongoDB はカーソルをバッチで返します。結果が1バッチを超えると、1回の読み取りが数十回〜数百回の連続した往復になります — 同じドキュメントで、動くのは時計だけです。

自分のコードが遅くなっている

呼び出した先ではなくメソッド自身の中の時間を分けてあるので、1回目で正しいファイルを見られます。

静かに何も返さなくなったクエリ

成功し、例外も投げず、遅くもなく、以前は行が返っていたのに空で返ってきた。

フィードに何が届くか

読むためのダッシュボードではありません。検出1件につきカード1枚。数値と、それを変えたリリースと、コーディングエージェントが修正を書くのに必要な証拠が付いています。

これは apps/demo-laravel がわざと出荷している2つの回帰で、その seeder が作るデータ — およそ30,000件の注文 — が相手です。実際に走らせて、届くところを見られます。

行数OrdersQuery#forUser
30行30,000行1000×

リファクタリングで制約がクエリの外に出て PHP 側に移り、呼び出しのたびにテーブル全体を読み込んでいます。同じ答え、同じコードレビュー、そして開発用データベースなら数ミリ秒です。

リリース v2 以降、v1 との比較

N+1OrdersQuery#enrich
1本20本20×

まとめて引いていたものが、注文ごとの取得になりました。どのクエリも速くインデックスも効いているので、リクエストとして見ればおかしなところはありません — 動いたのは回数だけです。

リリース v2 以降、v1 との比較

導入方法

Laravel はサービスプロバイダを自動検出します。Symfony は config/bundles.php にバンドルを追加する必要があります。どちらもそのあとリクエストのスパン、クエリの計測、送信を配線します — 書くべき initializer も、呼ぶべきものもありません。

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 () => ...);

ext-apcu も入れると、各ワーカーのウィンドウが1間隔あたり1つのペイロードに統合されます。なくてもエージェントは動き、そのことを起動時に一度言います。ごまかしはしません — 単に通信量がかなり増えるだけです。リリース識別子はプラットフォームから自動で、あるいは SIXTY_RELEASE から来ます。

できないこと

  • Swoole のコルーチン下では有効化を拒み、その理由を言います。そこでは多数のリクエストが1つのワーカーを共有し、I/O のたびに切り替わるので、現在のスパンがあるリクエストのクエリを別のリクエストのコントローラに帰属させてしまいます。FPM、CLI、RoadRunner、FrankenPHP はリクエストごとに1プロセスで、完全に対応しています。
  • ext-apcu がないと、各リクエストが自分のウィンドウを送ります。それでも動きますし、エージェントは起動時に一度そう伝えます — ただし、ワーカーをまたぐ無損失の統合こそが、パーセンタイルを「すべてを測る単一プロセスが報告したはずの値」にしているものであり、それがなければ通信量で払うことになります。
  • PDO::query() と PDO::exec() は statement クラスに届かないので、自分で作った接続に対してそれらを呼ぶ場合は TracedPdo が必要です。prepare() + execute() — Laravel と Doctrine が出すすべてのクエリ — は対象です。
  • 他の何かがすでに statement クラスを持っている場合 — プロファイラやデバッグバー — エージェントはそれに触れず、壊す代わりに何も測りません。これは正しい判断で、しかも黙って行われるので、起こりうると知っておく価値があります。
  • CPU と待ち時間は分けず、クエリプランは Postgres のみです。MySQL と MongoDB は値が残っているクエリしか説明できず、このエージェントは値を見た瞬間に捨てます。

これが理由でここに来たのなら

最後の変更が何をしたのか、確かめてください。

試してみる

別のものを作っていますか? Node のサービス、Ruby と Rails のアプリ、Python のサービス のページもあります — 見えるものが違うので、内容も違います。

PHP と Laravel 向けパフォーマンス監視 — Postgres・MySQL・MongoDB — Sixty