Symfony Performance Monitoring: Die Metriken, die Probleme vorhersagen, bevor sie zu Incidents werden

#symfony performance monitoring
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

Ein Performance-Audit ist eine Momentaufnahme. Du profilst die Anwendung, behebst die schlimmsten Ausreißer und machst weiter. Symfony Performance Monitoring ist etwas anderes: die fortlaufende Praxis, eine kleine Menge an Metriken zu beobachten, sodass eine Regression als Graph auftaucht, der in die falsche Richtung tendiert, und nicht als Support-Ticket. Die meisten Teams, mit denen wir arbeiten, haben Blackfire oder den Symfony Profiler für einmalige Untersuchungen, aber nichts, das kontinuierlich läuft, was bedeutet, dass das erste Anzeichen eines Problems eine Kundenbeschwerde oder ein Anruf um 2 Uhr nachts ist.

Dieser Beitrag behandelt, was kontinuierlich zu messen ist, wie du es mit Prometheus und OpenTelemetry verkabelst, und die Alerting-Regel, die eine echte Regression vom gewöhnlichen Rauschen des Tagesgeschäfts trennt.

Symfony Performance Monitoring versus ein einmaliges Audit

Ein Audit beantwortet "warum ist diese Route gerade jetzt langsam". Monitoring beantwortet "wird irgendetwas langsamer, und seit wann". Unser Leitfaden zum Symfony-Performance-Audit behandelt die erste Frage im Detail: Profiling mit Blackfire, die Timeline lesen, N+1-Queries beheben. Dieser Beitrag setzt dort an, wo jener aufhört. Du kannst das beste Audit der Welt durchführen, einen sauberen Fix ausliefern und trotzdem drei Monate später gepiept werden, weil ein neues Feature still und leise 40 Queries zu einem Hot Path hinzugefügt hat und es niemand bemerkt hat, bis die Antwortzeit über das hinaus kroch, was Nutzer tolerieren würden.

Die sechs Kategorien unten sind das, was wir bei jeder Symfony-Anwendung instrumentieren, für die wir laufende Verantwortung übernehmen. Keine davon erfordert exotisches Tooling. Prometheus, die Event-Listener von symfony/postgresql-adapter oder doctrine/doctrine-bundle und das PHP-SDK von OpenTelemetry decken das alles ab.

Request-Dauer-Histogramm, nach Route und Methode

Eine einzelne "durchschnittliche Antwortzeit"-Zahl verbirgt mehr, als sie zeigt. Eine Route, die 95% der Requests in 80ms und 5% in 4 Sekunden bedient, hat einen Durchschnitt, der gut aussieht, und ein p99, das die wahre Geschichte erzählt. Zeichne die Request-Dauer als Histogramm auf, gelabelt nach Route und HTTP-Methode, und lies Perzentile statt des Mittelwerts.

Mit promphp/prometheus_client_php zeichnet eine Middleware oder ein Event-Subscriber bei kernel.terminate die Beobachtung auf:

$histogram = $registry->getOrRegisterHistogram(
    'symfony',
    'http_request_duration_seconds',
    'Request duration',
    ['route', 'method'],
    [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
);
$histogram->observe($duration, [$route, $method]);

Das PromQL für p95 und p99 pro Route:

histogram_quantile(0.95, sum(rate(symfony_http_request_duration_seconds_bucket[5m])) by (le, route))
histogram_quantile(0.99, sum(rate(symfony_http_request_duration_seconds_bucket[5m])) by (le, route))

Behalte beide im Blick. Ein p95, das flach bleibt, während p99 steigt, bedeutet meist einen bestimmten langsamen Pfad (ein Kunde mit einem großen Datensatz, ein Query-Plan, der gelegentlich falsch läuft) statt einer allgemeinen Verschlechterung.

Anzahl der Datenbank-Queries pro Request

Die Anzahl der Queries pro Request ist das früheste und günstigste Signal für N+1-Regressionen in Produktion. Eine Route, die mit 8 Queries ausgeliefert wurde und über ein paar Wochen auf 30 klettert, wurde nicht über Nacht 4-mal langsamer. Jemand hat eine Beziehung hinzugefügt, ein Template hat begonnen, darüber zu iterieren, und niemand hat das Audit erneut durchgeführt.

Doctrine legt das über Doctrine\DBAL\Logging\SQLLogger offen, oder in neueren Versionen über eine Middleware, die die Verbindung umschließt. Zähle Queries pro Request und zeichne sie genauso als Histogramm auf wie die Dauer, gelabelt nach Route:

histogram_quantile(0.95, sum(rate(symfony_doctrine_query_count_bucket[10m])) by (le, route))

Richte das ein, bevor du es brauchst. Wenn die Query-Anzahl zusammen mit der Request-Dauer steigt, ist das der schnellste Weg, die Ursache zu bestätigen, ohne Blackfire öffnen zu müssen.

Speicherverbrauch pro Request

Symfony-Worker, die unter PHP-FPM oder FrankenPHP laufen, werden vom OOM-Killer beendet oder stoßen an memory_limit, lange bevor die meisten Teams merken, dass Speicher der Engpass ist, weil CPU- und Antwortzeit-Dashboards bis kurz vor dem Absturz des Prozesses normal aussehen. memory_get_peak_usage(true), pro Request aufgezeichnet, wieder als Histogramm, gibt dir den Trend.

Die Schwelle, bei der sich ein Alert lohnt, ist keine absolute Zahl, da diese je nach Anwendung und Worker-Konfiguration variiert. Was einen Incident vorhersagt, ist ein Peak-Speicherverbrauch, der sich dem konfigurierten memory_limit nähert. Wenn Worker auf 256MB eingestellt sind und der p99-Peak-Verbrauch bei 220MB liegt, schließt sich diese Lücke und ein proaktiver Fix lohnt sich eher als ein Abwarten. Ein Batch-Job oder ein Export-Endpunkt, der zu viel auf einmal in den Speicher lädt, ist meist die Ursache, und es ist deutlich günstiger, das als Trend zu erkennen als als Kaskade von Worker-Neustarts unter Last.

Queue-Tiefe und Verarbeitungslatenz pro Nachrichtentyp

Wenn die Anwendung Symfony Messenger mit Doctrine-, AMQP- oder Redis-Transports nutzt, ist die Queue-Tiefe ein Frühindikator, den auf HTTP-Requests fokussierte Dashboards komplett übersehen. Eine Queue, die schneller wächst, als sie sich leert, bedeutet entweder, dass ein Consumer hängt, eine nachgelagerte Abhängigkeit langsamer geworden ist, oder dass das Nachrichtenvolumen das übersteigt, was die aktuelle Worker-Anzahl bewältigen kann. Alle drei werden irgendwann für den Nutzer sichtbar (eine Willkommens-E-Mail, die eine Stunde zu spät kommt, ein Report, der nie generiert wird), aber die Queue-Tiefe erfasst es, während es noch eine interne Metrik ist und kein Support-Ticket.

Tracke zwei Dinge pro Nachrichtentyp: Queue-Tiefe (ein Gauge, periodisch gesampelt) und Verarbeitungsdauer (ein Histogramm, pro Handler). Der Middleware-Stack von Symfony Messenger ist der richtige Ort, um Letzteres aufzuzeichnen:

public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
    $start = microtime(true);
    $envelope = $stack->next()->handle($envelope, $stack);
    $this->histogram->observe(
        microtime(true) - $start,
        [get_class($envelope->getMessage())]
    );
    return $envelope;
}

Alarmiere relativ zur eigenen jüngsten Baseline der Queue-Tiefe, nicht bei einer festen Zahl, da die normale Tiefe je nach Tageszeit und Nachrichtentyp variiert.

Cache-Trefferquote pro Pool

Eine fallende Cache-Trefferquote ist meist die stille Ursache hinter einer steigenden Datenbanklast, die sonst unerklärlich wirkt. Die Cache-Komponente von Symfony legt Pool-Level-Statistiken offen, wenn du den Adapter umschließt oder in Produktion den TraceableAdapter nutzt (mit dem Overhead, den das mit sich bringt, also lieber samplen als jeden Request im großen Maßstab zu tracen). Tracke Hits und Misses als Counter pro Pool und berechne das Verhältnis:

sum(rate(symfony_cache_hits_total[15m])) by (pool)
/
sum(rate(symfony_cache_hits_total[15m]) + rate(symfony_cache_misses_total[15m])) by (pool)

Ein Pool, der historisch bei 92% liegt und auf 60% fällt, ist es wert, untersucht zu werden, bevor er sich als Datenbank-CPU zeigt. Häufige Ursachen sind ein Deploy, das einen Cache-Key geändert hat (was alles invalidiert), eine Redis-Eviction unter Speicherdruck, oder eine TTL, die zu kurz für die tatsächliche Nutzung des Pools eingestellt wurde.

Dauer externer API-Aufrufe, pro Anbieter

Jeder ausgehende Aufruf an einen Zahlungsdienstleister, einen E-Mail-Dienst oder eine externe API ist eine Abhängigkeit, die die Anwendung nicht kontrolliert, und auch eine häufige Ursache für Incidents, die wie eine Langsamkeit im eigenen Code aussehen, es aber nicht sind. Umschließe Aufrufe von Symfony\Contracts\HttpClient\HttpClientInterface, oder nutze die Auto-Instrumentierung von OpenTelemetry für symfony/http-client, und zeichne die Dauer pro Anbieter und Statuscode auf.

Das ist aus zwei Gründen wichtig. Erstens sagt es dir während eines Incidents, wo du nachsehen musst, ohne raten zu müssen. Zweitens sagt es dir, über Wochen getrackt, welche Anbieter langsamer oder unzuverlässiger werden, bevor deren Statusseite es zugibt.

Bei relativer Veränderung alarmieren, nicht bei absoluten Schwellenwerten

Der schnellste Weg, ein Monitoring-Setup nutzlos zu machen, ist, bei festen Schwellenwerten wie "alarmiere, wenn p99 2 Sekunden übersteigt" zu alarmieren. Manche Routen sind legitim langsamer als andere (ein Endpunkt zur Reportgenerierung gegenüber einem Health-Check), und ein fester Schwellenwert alarmiert entweder ständig auf der langsam-aber-normalen Route oder übersieht eine echte 3-fache Regression auf einer normalerweise schnellen Route.

Die Regel, die in der Praxis besser funktioniert: Alarmiere, wenn p99 für eine Route um mehr als 20% relativ zu ihrer eigenen 7-Tage-Baseline steigt, anhaltend über ein Zeitfenster, das lang genug ist, um einen kurzen Ausschlag auszuschließen.

groups:
  - name: symfony-performance
    rules:
      - alert: SymfonyP99Regression
        expr: |
          histogram_quantile(0.99, sum(rate(symfony_http_request_duration_seconds_bucket[10m])) by (le, route))
          >
          1.2 * avg_over_time(
            histogram_quantile(0.99, sum(rate(symfony_http_request_duration_seconds_bucket[10m])) by (le, route))[7d:1h]
          )
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: 'p99 für {{ $labels.route }} liegt 20% über der 7-Tage-Baseline'

Kombiniere das mit einem Grafana-Dashboard, das Request-Dauer, Query-Anzahl, Speicher, Queue-Tiefe und Cache-Trefferquote auf einem Bildschirm pro Route oder Worker-Pool zusammenfasst. Wenn ein Alert auslöst, sollte die diensthabende Person innerhalb einer Minute erkennen können, ob die Ursache eine Query-Regression, ein Speicherproblem oder eine externe Abhängigkeit ist, statt bei Blackfire anzufangen und sich rückwärts vorzuarbeiten.

Wo das zu einem einmaligen Audit passt

Nichts davon ersetzt ein periodisches Audit. Monitoring sagt dir, dass sich etwas geändert hat; ein Audit sagt dir warum und wie du es richtig behebst. Die beiden arbeiten zusammen: Monitoring verkürzt die Zeit zwischen dem Auftreten einer Regression und dem Moment, in dem sie jemand bemerkt, und ein Audit bleibt das richtige Werkzeug für die tiefere Untersuchung, sobald du weißt, wo du hinschauen musst.

Wenn deine Symfony-Anwendung noch keins von beidem hat, oder du ein Monitoring-Setup geerbt hast, das zu oft alarmiert, um nützlich zu sein, deckt unsere Arbeit im Bereich Code-Quality-Consulting beides ab: das Einrichten der Metriken, die zählen, und das Feintuning des Alertings, damit es echte Probleme erfasst, ohne die diensthabende Person auszubrennen. Für Anwendungen, die darüber hinaus umfassendere Arbeit brauchen, von der Architekturprüfung bis zum kompletten Neubau, ist Custom Software Development der breitere Service, der das abdeckt.

Fragen zur Integration in ein bestehendes Symfony-Deployment, oder zu spezifischen Metrik-Schwellenwerten für dein Setup, sind willkommen unter hello@wolf-tech.io. Mehr darüber, wie wir arbeiten, findest du auf wolf-tech.io.