Wir haben vor der Anwendung einen Varnish-Cache eingerichtet, der als Reverse Proxy dient. Alle Anfragen, die bereits im Varnish-Cache gespeichert sind, werden direkt von Varnish beantwortet. Dies erfolgt durch eine extrem effiziente Suche im hochoptimierten Varnish-System. Solche Anfragen werden in nur wenigen Millisekunden verarbeitet.

Metriken

Laut Varnish-Monitoring sieht die Anfragenfrequenz über 24 Stunden so aus:

Anfragen pro Minute in der Varnish Admin Console. Tagsüber zwischen 40'000 und 100'000 Anfragen/Minute, nachts deutlich weniger.

Dieser Screenshot wurde an einem typischen Tag aufgenommen. Varnish verarbeitet tagsüber zwischen 40'000 und 100'000 Anfragen pro Minute, mit gelegentlichen Spitzen darüber hinaus. Nachts nimmt die Aktivität deutlich ab.

Etwa ⅔ aller Anfragen sind Cache-Hits in Varnish. Die Trefferquote ist besonders hoch, wenn die Last am höchsten ist. Dadurch wird die Backend-Anwendung erheblich entlastet.

Cache-Hits und -Misses pro Minute, gemeldet von der Varnish Admin Console.

Diese Aktivität führt auch zu einem erheblichen Bandbreitenverbrauch:

Bandwidth usage is about 5M - 40M / minute, with peaks reaching 100M.

Caching optimal nutzen

Alle Anfragen an M-API müssen authentifiziert werden. Um trotzdem eine hohe Cache-Trefferquote zu erreichen, setzen wir das "User Context" Pattern ein: Anstatt pro authentifizierter Nutzer*in zu cachen, gruppieren wir API-Konsument*innen mit gleichen Berechtigungen. Mehr dazu gibt es in diesem Blogpost.

Da die Anwendung weiss, wann sich Daten ändern, setzen wir eine lange Cache-Lebensdauer und lassen die Anwendung aktiv Cache-Invalidierungen auslösen. Dafür nutzen wir das xkey-Feature von Varnish, mit dem Antworten durch Tags versehen und gezielt invalidiert werden können.

Viele Suchergebnisse bestehen aus Produktlisten (z. B. durch Freitextsuche oder Kategoriesuche). Änderungen an Daten können die Reihenfolge der Suchergebnisse beeinflussen. Da wir Paging verwenden, reicht xkey nicht aus: Eine Änderung betrifft nicht nur die Seite, auf der sich das Produkt vorher befand, sondern potenziell auch alle folgenden Seiten. Wir müssten daher alle Listen bei jeder Produktänderung invalidieren – was den Cache nutzlos machen würde.

Stattdessen nutzen wir Edge Side Includes (ESI). Die Listen enthalten nur ESI-Anweisungen, um die eigentlichen Produktdaten nachzuladen. Dadurch bleibt die Listengenerierung effizient, sodass wir eine kurze Cache-Lebensdauer für die Listen beibehalten können. Varnish interpretiert die ESI-Anweisungen, lädt die Produkte einzeln nach und setzt die Antwort für den Client zusammen. Die Produktdaten selbst werden länger gecacht und mit der Produkt-ID getaggt, sodass sie gezielt invalidiert werden können.

Zusätzlich haben wir den "Grace Mode" konfiguriert. Falls das Backend nicht verfügbar ist, liefert Varnish veraltete Inhalte, anstatt eine Fehlermeldung zurückzugeben. In vielen M-API-Anwendungsfällen ist eine leicht veraltete Information besser als gar keine Antwort.

Server Application

Nicht alles kann durch HTTP-Caching gelöst werden. Für Anfragen, die nicht gecacht werden können oder für die noch keine Cache-Antwort existiert, ist die Backend-Antwortzeit entscheidend.
Moderne PHP-Versionen, insbesondere das von uns verwendete Symfony-Framework, sind für schnelle Antwortzeiten optimiert.

PHP eignet sich von Natur aus gut für stateless Anwendungen. In M-API ist jede Anfrage vollständig unabhängig, was eine einfache horizontale Skalierung ermöglicht. Unsere Cloud-Infrastruktur überwacht die Auslastung und startet bei hoher Last automatisch zusätzliche Instanzen, während ungenutzte Instanzen wieder heruntergefahren werden.

Zur Optimierung der Antwortzeiten haben wir einen Profiler eingesetzt, um Engpässe zu identifizieren. Wir haben Daten, die oft abgefragt wurden, in einem Anwendungs-Cache (Redis) gespeichert, um Datenbankabfragen zu reduzieren. Ein grosser Teil der Verarbeitungszeit wurde für die JSON-Konvertierung benötigt. Nach der Evaluierung verschiedener Ansätze entwickelten wir eine eigene Lösung, die um ein Vielfaches schneller ist. Mehr dazu gibt es in diesem Blogpost.

Dank dieser Massnahmen erreichten wir Antwortzeiten von ca. 50 Millisekunden pro Produkt – selbst wenn ein einzelnes Produkt mehrere hundert KB bis über 1 MB JSON-Daten enthält.