Wenn Caching eine Laravel-Anwendung langsamer macht
Caching ist das Erste, wonach gegriffen wird, und das Letzte, was gemessen wird. Vier Muster, die Latenz hinzufügen statt sie zu entfernen, und die Frage, die vor jedem davon steht.
Jede langsame Anwendung bekommt irgendwann einen Cache. Es ist der Eingriff mit der besten Geschichte: Die Arbeit ist teuer, also mache sie einmal und behalte die Antwort. Oft funktioniert das, und die Seite, die zwei Sekunden brauchte, braucht jetzt zweihundert Millisekunden.
Oft genug, um darüber zu schreiben, funktioniert es nicht. Hier sind die Formen, die es dann annimmt.
Ein Cache-Aufruf pro Zeile
Die häufigste Variante und die am schwersten zu sehende, weil jedes einzelne Stück davon korrekt ist.
foreach ($orders as $order) {
$rate = Cache::remember("fx.{$order->currency}", 3600, fn () =>
$this->rates->for($order->currency)
);
}Jeder Aufruf ist eine Millisekunde. Es gibt vierhundert Bestellungen, also verbringt die Seite vierhundert Millisekunden damit, einem schnellen System wiederholt dieselbe Handvoll Fragen zu stellen. Die ursprüngliche Abfrage, die ersetzt wurde, lief einmal und brauchte zwölf.
Der Cache hat hier nicht versagt. Er wurde in eine Schleife gesetzt, was eine teure Operation in hunderte billige verwandelt und sie dann aufsummiert. Cachen Sie die Sammlung statt des Einzelstücks, oder lesen Sie die Menge der Unterschiedlichen einmal, bevor die Schleife beginnt.
Etwas cachen, das billiger ist als die Suche danach
Ein indizierter Select über den Primärschlüssel einer warmen Tabelle liegt deutlich unter einer Millisekunde. Ein Redis-Roundtrip über das Netz ist vergleichbar, manchmal langsamer, und jetzt betreiben Sie zwei Systeme, wo eines gereicht hätte – plus die Invalidierung.
Das gehört deutlich gesagt, weil hier der meiste Aufwand für den geringsten Ertrag betrieben wird. Bevor Sie irgendetwas cachen, messen Sie, was Sie gleich cachen wollen. Liegt die Antwort im einstelligen Millisekundenbereich, ist der Cache mit ziemlicher Sicherheit nicht die Verbesserung, die Sie suchen.
Das Stampede
Ein Schlüssel mit einem Report, dessen Aufbau vier Sekunden dauert, läuft um Mitternacht ab, auf einer Seite mit Traffic um Mitternacht. Jede Anfrage, die in den nächsten vier Sekunden ankommt, findet den Schlüssel leer und beginnt ihn aufzubauen. Nichts wird aus dem Cache bedient, die Datenbank führt dieselbe schwere Abfrage hundertfach parallel aus, und der Ausfall wird vom Cache verursacht statt von ihm verhindert.
Laravel liefert die Antwort mit:
$report = Cache::lock('report:monthly', 10)->block(5, function () {
return Cache::remember('report:monthly', 3600, fn () => $this->build());
});Eine Anfrage baut neu auf; die übrigen warten und lesen dann, was sie geschrieben hat. Die Alternative – und die bessere, wo Veralterung hinnehmbar ist – besteht darin, den Schlüssel unter Last gar nicht erst ablaufen zu lassen: Aktualisieren Sie ihn aus einer geplanten Aufgabe, damit Lesende immer etwas vorfinden.
Invalidierung, die mehr kostet als die Lesevorgänge
Ein Cache ist wert, was er spart, minus dem, was es kostet ihn korrekt zu halten. Binden Sie einen Schlüssel an die Aktualisierungen eines Modells, und ein Massenimport über fünfzigtausend Zeilen feuert fünfzigtausend Invalidierungen, jede davon ein Schreibvorgang im Cache-Speicher. Die Lesevorgänge sparten je acht Millisekunden. Der Import dauert jetzt vier Minuten länger.
Dieselbe Falle steckt in zu breiten Tags. Ein Tag über alle Schlüssel eines Mandanten ist bequem, bis das Leeren einen großen Schlüsselraum scannen muss – und ab da ist die Invalidierung der langsame Pfad, und sie läuft bei jedem Schreibvorgang.
Die Frage, die zuerst kommt
Nicht was sollten wir cachen, sondern warum ist das langsam.
Ein Cache vor einem N+1 macht das N+1 billiger in der Ausführung, und es wird immer noch da sein, und es wird immer noch laufen – jetzt mit einem zweiten System und einem Korrektheitsproblem im Anhang. Ein Cache vor einem fehlenden Index ist eine Art, den Index nicht hinzuzufügen. Beides ist als Notbehelf legitim, aber beides sollte als solcher aufgeschrieben werden, denn ein Notbehelf, den niemand aufgeschrieben hat, ist im Folgejahr Architektur.
Wo die Antwort wirklich lautet, dass die Arbeit teuer und korrekt ist und ohnehin passieren muss – ein Aufruf bei Dritten mit Ratenbegrenzung, ein Aggregat über Millionen Zeilen, ein gerendertes Dokument – ist Caching genau richtig. Das ist eine engere Menge an Fällen, als der Reflex nahelegt.
Was zu messen ist
Drei Zahlen, alle billig zu bekommen und selten angesehen:
Trefferquote je Schlüsselpräfix. Unter achtzig Prozent frisst der Fehltreffer die Gewinne. Unter fünfzig zahlen Sie für einen Cache, den Sie faktisch nicht haben.
Cache-Aufrufe pro Anfrage. Einer ist ein Cache. Vierzig sind eine Schleife, die Sie noch nicht bemerkt haben.
Die Seite mit abgeschaltetem Cache. Ist der Unterschied klein, ist der Cache Komplexität ohne Gegenwert – und ihn zu löschen ist eine Performance-Verbesserung in dem einzigen Sinn, der zählt: die Anzahl der Dinge, die kaputtgehen können.
Oft war die Abfrage darunter von Anfang an das Problem, und N+1 ist die übliche Form davon. Geht es um den gesamten Request-Pfad und nicht um eine Abfrage, beginnt Performance-Arbeit mit einer Messung. Der Cache kommt später, oder er kommt gar nicht.
