Packt mein Shop das?

Realistische Performancetests für operative Sicherheit
10
Dez

Packt mein Shop das?

So einfach simple Performancetests mit ab oder siegeauszuführen sind, so schwer ist es sicherzustellen, dass eine Webseite die zu erwartende Last tatsächlich aushält. Regelmäßig kollabieren Webseiten unter der Last eines Relaunches oder einer geplanten Werbekampagne. Ich zeige, wie man sich sicher sein kann, ein solches Ereignis erfolgreich zu überstehen.

Gerade bei dem Relauch einer neuen Webseite oder einer neuen Version einer Webapplikation hat man oft erst kurz vor dem Relaunch eine Applikation zur Verfügung, die vollständig genug ist, um einen Lasttest durchzuführen. Der Einfachheit halber und weil man oft nicht von vornherein Zeit dafür eingeplant hat, wird dann oft auf einfache Tools wie ab (Apache Benchmark) oder siege zurückgegriffen. Beide Tools erlauben es, einen einzelnen URL oft und parallel aufzurufen und geben einem eine erste Indikation für die Antwortzeiten einer Webseite. Das Resultat beider Tools sind einfache Zahlen, die sich gut kommunizieren lassen: Requests pro Sekunde sowie minimale, maximale und durchschnittliche Antwortzeit.

Ich rate allerdings in den meisten Fällen davon ab, diesen verführerisch einfachen Weg zu gehen. Keines der Probleme, die ich mit realistischen Lasttests bei Kunden gefunden habe, wäre damit aufgedeckt worden, und die genannten Zahlen haben nur eine begrenzte tatsächliche Aussagekraft. Das Gleiche gilt für die Probleme, die einen gescheiterten Relaunch verursacht haben. Auf der anderen Seite konnte ich bislang durch Lasttests zu 100 Prozent sicherstellen, dass die erwartete Last durch die getestete Webseite bewältigt wurde. Wie genau kann man das also erreichen?

Realistische Nutzerszenarien

Der erste und wahrscheinliche wichtigste Aspekt sind realistische Nutzerszenarien. Dazu stellt man sich die Frage, wozu die Webseite verwendet wird, was also Besucher üblicherweise auf der Webseite an Aktionen durchführen und in welchem Mengenverhältnis diese Aktionen zueinander stehen. Am Beispiel eines Onlineshops könnten das zum Beispiel die folgenden Szenarien sein:

  • Anonymer Random-Browser
  • Anmeldung eines Neukunden
  • Eingeloggter Random-Browser
  • Check-out eines gefüllten Warenkorbs

Dazu können je nach Onlineshop weitere relevante Nutzerszenarien kommen, wenn zum Beispiel beliebte Konfiguratoren existieren oder Merkzettel eine wichtige Rolle spielen. Zu jedem Szenario sollte dazu bekannt sein, wie hoch der Anteil dieses Szenarios am gesamten Traffic der Webseite ist. Diese Zahlen sind manchmal schwer abzuschätzen, weil man die Nutzerverhalten nach einem Relaunch, einer neuen Webseite oder auch während einer Werbekampagne nicht unbedingt perfekt einschätzen kann. Das Verwenden bisheriger Zahlen oder von üblichen Zahlen aus dem eigenen Geschäftsbereich liefert meist aber eine ausreichend genaue Abschätzung.

Warum diese Szenarien?

Der primäre Grund für realistische Szenarien ist Caching. Damit ist nicht nur eigenes Caching in der Webapplikation oder in Reverse Caching Proxies (varnish, nginx) gemeint, sondern auch das Caching verschiedenster Layer, die man nicht direkt unter Kontrolle hat, wie Datenbankserver, Opcode-Caches oder auch Kernel-Caches. Wenn, wie bei ab und siege, immer wieder eine kleine Zahl gleichbleibender URLs aufgerufen wird, können all diese Layer sehr einfach und effizient die entsprechenden Daten im Speicher vorhalten. Leider entspricht dieses Verhalten nicht dem zu erwartenden Verhalten durch echte Nutzer, sodass diese Tests keine Aussagekraft besitzen.

 

Abb. 1: Einfache schematische Darstellung eines Onlineshops

 

Wenn man sich die einfache schematische Darstellung eines Onlineshops in Abbildung 1 anschaut, besteht ein solcher Shop heute aus vielen Komponenten, die häufig ihre eigenen Cachelaufzeiten und -kontexte besitzen. Während Kopfzeile und Navigation oft vergleichsweise statisch sind, ändern sich Kommentare, wenn sie freigegeben werden, Produktbeschreibungen, wenn neue Daten aus dem ERP kommen, und der Produktbestand, wenn Produkte verkauft werden. Elemente wie der Warenkorb hängen sogar direkt an dem Nutzer und werden meist gar nicht gecacht.

Oft werden entweder Reverse Caching Proxies mit Edge-side Includes (ESI) eingesetzt, um solche einzelnen Bestandteile zu cachen, oder dies findet in der Webapplikation direkt statt. Selbst wenn all das nicht der Fall ist, cachen Datenbankserver gleiche Queries, oder der Kernel cacht Anfragen an immer gleiche Dateien. Die Berechnung oder Neuberechnung eines Inhalts ist jedoch genau die Aktion, die am meisten Ressourcen auf dem Server kostet, damit den größten Effekt auf die Last der Systeme hat und entsprechend in einem möglichst realistischem Maß simuliert werden muss.

Aus diesem Grund werden Szenarien wie die obigen entworfen und müssen parallel und mit verschiedenen Nutzern und Sessions ausgeführt werden. Dadurch werden dann in allen Systemen Cache Misses verursacht, Caches neuberechnet und eventuelle Cache-Hits sinken auf ein normales Maß.

Anzahl der Aufrufe

Wenn man die Nutzerszenarien kennt, muss man noch wissen, in welcher Anzahl man sie parallel laufen lässt. Grundsätzlich gibt es natürlich die Möglichkeit, die Anzahl an parallelen Nutzern so lange immer höher zu setzen, bis die eigenen Server unter der Last zusammenbrechen. Wirtschaftlich sinnvoller ist jedoch meist, zu verstehen, wie viele Nutzer erwartet werden, um dann die Server daraufhin zu optimieren. Dabei muss beachtet werden, dass die Nutzer bei den meisten Webseiten nicht gleichverteilt über den Tag kommen, sondern es spezifische Kernzeiten gibt, die simuliert werden sollten.

Wenn in einem deutschen Onlineshop mit einer Kernzeit von 18:00 bis 22:00 Uhr zum Beispiel nur bekannt ist dass es 240 000 Page Impressions (PI) pro Tag gibt, sollten wir nicht anpeilen, 10 000 PI/Stunde (1 Request/Sekunde) zu simulieren, sondern wahrscheinlich eher 40 000 PI/Stunde (3 Requests/Sekunde). Wegen fehlender Gleichverteilung im normalen Nutzerverhalten ist man in diesem Fall mit einer Simulation von 5 Requests/Sekunde wahrscheinlich auf der sicheren Seite. Im optimalen Fall stehen Access-Logs zur Verfügung, mit deren Hilfe man nicht nur die sinnvolle Anzahl von Anfragen pro Sekunde herausfindet, sondern gleichzeitig auch die Verteilung auf die einzelnen Nutzerszenarien.

JMeter

Es gibt mittlerweile mehrere Tools und Frameworks, die abseits von ab und siege sinnvolle Lasttests anhand von realistischen Nutzerszenarien durchführen können. Schon lange auf dem Markt, frei verfügbar, Open Source und funktional sehr vollständig ist Apache JMeter [1]. Auch wenn die Erstellung der Nutzerszenarien im Normalfall über ein gewöhnungsbedürftiges Userinterface stattfindet, ist das meiner Meinung nach immer noch das sinnvollste Tool, um größere und realistische Lasttests durchzuführen. JMeter erlaubt die automatische Fernsteuerung von Tests, kann Cluster von Servern nutzen, um die notwendige Last zu erzeugen (was selten notwendig ist), und implementiert alle nur denkbaren Protokolle neben HTTP(S), um auch besondere Webapplikationen testen zu können. Über das Userinterface wird lediglich eine XML-Konfiguration des Testszenarios erzeugt, die dann auf anderen Servern beliebig abgespielt werden kann, versionierbar ist und auch über verschiedene Versionen von JMeter funktioniert. Gewöhnen muss man sich zu Beginn an die Bezeichnungen für die einzelnen Konzepte – allerdings hat sich gezeigt, dass sich darüber sinnvolle und wiederverwendbare Tests erstellen lassen:

  • Thread Group: Eine Thread Group ist genau ein Nutzerszenario, wie es oben beschrieben wurde. So kann es zum Beispiel eine Thread Group „Random Surfer“ geben, in
    der wir einen Suchmaschinen-Bot oder einen nicht angemeldeten Nutzer simulieren.
  • Timer: Ein Timer definiert die Abstände zwischen mehreren Aktionen in einer Thread-Group. Das ist wichtiger, als man zunächst denkt, denn echte Nutzer warten nicht
    immer den exakt gleichen Zeitraum zwischen zwei Klicks ab. Üblicherweise verteilen sich die Klickintervalle von Nutzern in einer gaußschen Normalverteilung um einen definierten
    Wert. Genau dafür gibt es dann in JMeter auch einen konfigurierbaren Gaussian Timer.
  • Configuration Elements: Über Konfigurationselemente werden Daten bereitgestellt. Das kann ein Cookiemanager sein oder Beispieldaten aus eines CSV-Datei, um JMeter
    im Testsystem existierende Nutzerdaten für Log-ins zur Verfügung zu stellen.
  • Sampler: Die Sampler führen auf Basis der zuvor genannten Elemente die tatsächlichen Anfragen aus. Bei Webseiten sind das normalerweise HTTPS-Anfragen. JMeter
    unterstützt allerdings auch FTP, SOAP und viele weitere Protokolle.

Mit diesen Elementen lassen sich problemlos auch komplexere Interaktionen wie auszufüllende Formulare oder XMLHttpRequest-basierte Webseiteninteraktionen vollständig simulieren. Relativ einfache und übliche Aufgaben wie das Nachladen von Bildern und anderen Assets oder das Management von Session-Cookies für einzelne Nutzer übernimmt JMeter dabei von sich aus. Eine Interpretation von JavaScript auf der Webseite und die dadurch theoretische automatische Simulation der Nutzung einer Single Page Application unterstützt es aber nicht. Dafür kann man JMeter problemlos clustern, so auch sehr hohe Last generieren und die Resultate sinnvoll aggregieren.

Richtig messen

Zu Beginn wurde davon gesprochen, dass ab und siege einfache und gut zu kommunizierende Zahlen wie Requests pro Sekunde und die durchschnittliche Antwortzeit liefern. Das tut JMeter natürlich auch, aber die Aussagekraft dieser Zahlen ist eher begrenzt. Als aussagekräftiger sehe ich an:

  • Fehlerraten: Die Anzahl der Fehler (Statuscode ≥ 500), die einzelne Systeme zurückgeben. Gerade unter hoher Last treten diese oft verstärkt auf und erfordern dann
    eine detailliertere Analyse.
  • 95 % Percentile: Als 95 % Percentile wird die Antwortzeit bezeichnet, unter der 95 Prozent aller Antworten lagen. Diese Zahl ist wesentlich relevanter als der
    Durchschnitt aller Antwortzeiten, weil sie eine Abschätzung liefert, wie lange Nutzer tatsächlich warten müssen, und einzelne Ausreißer (Minimum, Maximum) besser ignoriert.
    Zusätzlich werden häufig noch weitere Percentiles wie 50 Prozent, 90 Prozent, 98 Prozent und 99 Prozent betrachtet. Diese lassen sich aus den von JMeter gelieferten Daten korrekt
    berechnen.
  • Requests pro Sekunde: Die Requests pro Sekunde betrachten wir eigentlich nur, um festzustellen, ob die avisierte Menge an Anfragen tatsächlich von uns im Lasttest
    erreicht wurde.

Neben diesen Statistiken kommt beim Messen noch ein weiterer entscheidender Punkt hinzu: Wir wollen normalerweise nicht nur wissen ob die Webseite die Last aushält, sondern auch, welche Systeme auf welche Weise an ihre Grenzen kommen.

 

Abb. 2: Schematische Darstellung der Serverlandschaft mit Messwerkzeugen

 

In Abbildung 2 ist schematisch und vereinfacht eine übliche Serverlandschaft von Webapplikationen zu sehen. An allen relevanten Schnittstellen und auf allen Systemen sollten wir versuchen zu messen, wo die jeweiligen Flaschenhälse sind. Der Netzwerkdurchsatz lässt sich meist einfach mit ifstat oder iftopüberwachen. Die wichtigsten Systemmetriken, wie Speicherauslastung, IO-Wait und Systemlast bekommt man über vmstat. Von externen Systemen will man zumindest die Antwortzeiten messen, wenn man nicht Monitoringlösungen wie Tideways[2] einsetzt, die das für PHP-Code sowieso tun. Je nach Service will man dazu eventuell noch weitere dezidierte Monitoringlösungen wie Tideways, New Relic oder App Dynamics einsetzen.

Wenn man all diese Daten zusammen analysiert hat, kann man in den allermeisten Fällen schon sehr genau auf die Ursachen eventueller Performanceprobleme schließen. Dazu kann es sich lohnen, eigene Experten aus Operations und Entwicklung zusammenzusetzen oder externe Experten hinzuzuziehen.

Tipps und Tricks

Externe Services werden in Abbildung 2 zum ersten Mal erwähnt, und natürlich sollten hier auch die Antwortzeiten unter Last beobachtet werden. Unter externen Services verstehen wir einfache Komponenten wie Mailer, aber auch externe oder eigene Web Services (Microservices), die in die eigene Applikation integriert sind. Bei externen Web Services sollte man die Betreiber auf jeden Fall über einen bevorstehenden Lasttest informieren. Es wäre nicht das erste Mal, dass ein Lasttest der eigenen Systeme einen verwendeten externen Web Service in die Knie zwingt.

Eine andere Option ist das Deaktivieren (Mocken) externer Services während eines Lasttestlaufs. Das kann aus Kostengründen manchmal sinnvoll sein, allerdings ist die Information, ob externe Services die zu erwartende Last aushalten, natürlich auch extrem wichtig für den späteren Betrieb.

Als Hardware während eines Lasttestlaufs sollte in jedem Fall die echte, später produktiv verwendete Hardware für die Webapplikation verwendet werden. Ein Docker-Container oder eine virtuelle Maschine verhalten sich unter Last komplett anders, als es ein Server bei Amazon S3 oder ein Bare-Metal-Server tun. In PHP-Applikationen ist häufig die IO-Wait (warten auf die Festplatte) einer der Flaschenhälse. Gerade virtualisierte Dateisysteme verhalten sich aber genau ganz anders als es echte tun, die auf echten Festplatten oder SSDs operieren.

Die Hardware und die Anbindung der Testserver, also der Server, auf dem zum Beispiel JMeter läuft, ist ebenfalls relevant. Wenn nicht die Netzanbindung des Hosters getestet werden soll, raten wir normalerweise dazu, Testsysteme im gleichen Rechenzentrum wie die Systeme zu stellen, die unter Last gesetzt werden. Primär sorgt es dafür, dass sich die Last zuverlässig simulieren lässt, ohne dass eventuell die Testsysteme durch die DDOS-Erkennung (Distributed Denial of Service) des Hosters blockiert werden. Sekundär ist das auch eine Kostenfrage – ein Lasttest kann natürlich sehr viel Traffic erzeugen. Durch fehlerhaftes Routing eines Hosters, sodass eigentlich interner Traffic über eine externe Leitung ging, sollte ein Kunde zum Beispiel mehrere zehntausend Euro Traffic-Kosten nachzahlen. In jedem Fall sollte man entsprechend auch den Hoster nach Möglichkeit vorab über derartige Tests informieren.

Gefundene Fehler

Viele Fehler, die ich und meine Kollegen durch Lasttests gefunden haben, entsprachen nicht den Erwartungen der Entwickler der Webapplikation, und fast alle wären ohne einen dezidierten Lasttests erst im Produktivbetrieb aufgefallen.

Varnish wird zusammen mit Edge-Side Includes (ESI) eingesetzt, um einzelne Bestandteile der Webseite mit unterschiedlichen Cachekontexten und -laufzeiten zu versehen und entsprechend zu cachen. Auf einem großen Onlineshop wurde dies sehr granular eingesetzt und funktionierte während der Entwicklung und der üblichen Tests wunderbar. Durch den sehr granularen Einsatz der ESIs und die kombinatorische Explosion der Cachekontexte konnte Varnish während des Lasttests allerdings nicht mehr alle notwendigen Varianten im Speicher halten. Eine Cache-Hit-Rate von < 10 % führte dann dazu, dass das PHP-Framework für den Onlineshop nicht nur einmal für die gesamte Seite angefragt wurde, sondern bis zu vierzigmal für einen einzelnen Seitenaufruf eines Besuchers. Diese stark erhöhte Menge an Anfragen war viel zu viel für die Applikationsserver. Über eine starke Reduktion der ESIs ließ sich das Problem aber relativ einfach lösen.

NFS (Network File System) wird in Webapplikationen gerne eingesetzt, um statische Dateien zwischen mehreren Applikationsservern zu synchronisieren. NFS verhält sich unter hoher Last jedoch sehr oft ganz anders als unter niedriger Last, weil mehrere parallele Schreibzugriffe sehr häufig zu kompletten Blockaden führen, die Minuten anhalten können. Ein Effekt, der in normalen Tests nicht auftritt, aber unter Last sehr oft beobachtet werden kann. Vermeiden lässt sich das im Übrigen, wenn man bei der Verwendung von NFS Dateien nur genau einmal schreibt und nie wieder verändert. Mehrere lesende Server sind meist kein Problem.

Ein von einem Hoster vorkonfigurierter Cluster hielt ein weiteres Problem für mein Team bereit, das im Produktivbetrieb für sehr viele Fehler gesorgt hätte, aber im Testbetrieb nicht aufgefallen ist. Der Apache-Server akzeptiert schlichtweg in etwa doppelt so viele Verbindungen wie der MySQL-Server. Das führte dazu, dass unter Last 50 % aller Anfragen mit einem Fehler endeten, weil keine Verbindung mehr zu dem MySQL-Server aufgebaut werden konnte. Ein eigentlich triviales Problem, das aber einen Relaunch oder eine Werbekampagne komplett scheitern lassen kann.

In keinem der von uns getesteten Fälle war im Übrigen der MySQL-Server zu langsam, was die häufigste Annahme der Entwickler ist. Bis auf einen Fall hätten die von uns bislang getesteten Systeme die avisierte Last nie ausgehalten. Nach den Tests, Messungen und entsprechenden Fehlerbehebungen hat dann aber bislang jedes System das geplante Ereignis problemlos überstanden. Auch wenn die Investition über eigenen Wissensaufbau oder externe Experten in einem derartigen Lasttests erst einmal groß wirkt, rentiert sie sich jedoch genau darüber wieder.

Checkliste

Bevor ein Lasttest durchgeführt wird, kann eine Checkliste helfen, die wichtigsten Punkte zu identifizieren:

  • Dezidierte Hardware für den Testserver: Die Tests sollten nie auf dem zu testenden System ausgeführt werden – dies würde die Messungen stark verfälschen.
  • Ausreichende Testhardware im selben Rechenzentrum: Sowohl Netzwerkdurchsatz als auch die Auslastung sollte auf den Testservern ebenfalls gemessen und beobachtet
    werden, um sicherzustellen, dass die avisierte Last zuverlässig erzeugt werden kann.
  • Die reale Hardware testen:Es führt kein Weg daran vorbei, die reale Hardware zu testen, wenn man aussagekräftige Resultate benötigt. Infrastrukturautomatisierung
    (Ansible, Puppet …) kann helfen, eventuell existierende Systeme zu duplizieren.
  • Realitätsnahe Daten verwenden:Die Datengrößen und -strukturen in der getesteten Software sollten möglichst nah an denen der realen Umgebung sein. Gerade
    Indexgrößen in Datenbanken und deren entsprechender Speicherverbrauch sind oft entscheidend, wenn es um die Performance der entsprechenden Systeme geht.
  • Externe Dienstleister informieren:Auf jeden Fall sollten alle Betreiber eingebundener externer Dienste, die mitgetestet werden, informiert werden. Oft muss
    vereinbart werden, wie mit den im Testzeitraum anfallenden Daten umzugehen ist.
  • Realistische Userszenarien definieren:Mit realistischen Userszenarien steht und fällt die Aussagekraft eines Lasttests. Diese sollten zusammen mit dem Product
    Owner ausgearbeitet werden. Dabei ist es ebenfalls wichtig zu verstehen, in welcher Anzahl welche Aktionen vorkommen.

Fazit

Einen wirklich sinnvollen Lasttest aufzusetzen und durchzuführen, ist mehr Arbeit als der Aufruf eines kurzen Skripts. Dafür kann ein solcher Lasttest einem die Sicherheit geben, dass eine Werbekampagne oder ein Relaunch die erwarteten Nutzerzahlen aushält, und er erlaubt eine deutlich genauere Planung der dafür notwendigen Hardware, was langfristig Kosten reduzieren kann. Letztendlich ist eine gescheiterte Werbekampagne oft teurer, als es ein Test vorab sein kann – die Sicherheit und Zuversicht, die ein sinnvoller Lasttest gibt, ist dagegen unbezahlbar.

Links & Literatur

[1] https://jmeter.apache.org/
[2] https://tideways.io/

AUF DEM LAUFENDEN BLEIBEN!

BEHIND THE TRACKS

Web Design & Development
Design thinking out of the box

Online Marketing
Die neuesten Marketing Trends

User Experience Design
User Experiences im Fokus

Ideation & Design Thinking
Von Kleinstunternehmen bis Enterprise-Level