Loadtesting testen mit Locust

Was ist Load Testing?

Unter Load-Testing (deutsch Lasttests) versteht man das Testen der API-Endpunkte bbzw. allgemein auch sonstiger Programme, mit den zu erwartenden Zugriffszahlen. User werden dabei vom Testsystem emuliert. Load-Testing ist verwandt aber nicht gleichzusetzen mit Stress-Testing, welches genutzt wird, um ein System ans Limit zu bringen und letzendlich zu prüfen, wann das System einknickt. Eine DDoS-Attacke könnte zum Beispiel mit einem Stresstest simuliert werden.

LocustIO, ein in Python geschriebenes Open-Source-Tool, wird für Lasttests von Webanwendungen verwendet. Es ist einfach und leicht mit einer Benutzeroberfläche zu verwenden, um die Testergebnisse anzuzeigen.

LocustIO hilft uns dabei, die Benutzer zu emulieren, die diese Aufgaben in unserer Webanwendung ausführen. Die Grundidee der Leistungsmessung besteht darin, eine Anzahl von Anfragen für verschiedene Aufgaben zu stellen und diese Anfragen zu analysieren.

Installation von Locust

Zuerst installieren wir via pip das Locust-Framework in unserer virtuellen Umgebung:

(eventenv) pip install locust

wir können auch gleich testen, ob die Installation erfolgreich war und lassen uns die Version anzeigen.

(eventenv) locust -V
locust 2.8.6

Anlegen eines Test-Skripts

Locust-Tests werden in Dateien abgebildet, ähnlich, wie man das mit Unit-Tests macht. Wir erstellen also unter event_manager/events/ die Datei locustfile.py. Wenn dieser Dateiname gewählt wird, muss man später beim Aufruf von Locust den Dateinamen nicht spezifieren.

In der Datei event_manager/events/locustfile beginnen wir mit ein paar Imports.

from locust import HttpUser, task, between

Danach definieren wir die Locust-Testklasse. Den Namen der Klasse können wir frei vergeben, wir müssen allerdings von HttpUser erben, damit alles funktioniert. Wir nennen die Klasse EventManagerUser.

Oberhalb der Klasse definieren wir ein paar Konstanten, die wir im Code später benötigen werden. Das sind zum Beispiel die URLs zu den API-Endpunkten die wir testen wollen oder die Zugangsdaten für einen User, um einen Token zu erstellen.

TOKEN_URL = "http://127.0.0.1:8000/api/users/token"
CATEGORIES_URL = "http://127.0.0.1:8000/api/events/category/"
EVENTS_URL = "http://127.0.0.1:8000/api/events/events/"

USERNAME = "Bob"
PASSWORD = "abc"


class EventManagerUser(HttpUser):
    wait_time = between(1, 2)


    @task
    def add_page_request(self):
        self.client.post(
            "/api/page-request/",
            json=dict(url=random.choice(test_urls)),
            headers=dict(Authorization=f"Token {self.token}"),
        )

    @task
    def event_categories_request(self):
        self.client.get(
            CATEGORIES_URL,
            headers=dict(Authorization=f"Token {self.token}"),
        )

    @task
    def event_events_request(self):
        self.client.get(
            EVENTS_URL,
            headers=dict(Authorization=f"Token {self.token}"),
        )


    def on_start(self):
        response = self.client.post(
            TOKEN_URL, json=dict(username=USERNAME, password=PASSWORD)
        )
        if not response.status_code == 200:
            raise ValueError
        data = response.json()
        self.token = data.get("token")

In der Klasse EventManagerUser findet sich die on_start-Methode, die immer zu Beginn eines Locust-Tests aufgerufen wird. Hier ist ein guter Ort, die Api mit den User-Daten anzufragen und einen Auth-Token zu holen. Mit dem Locust client fragen wir via HTTP-POST die entsprechende URL an und senden Username und Passwort mit. Falls das alles geklappt hat und die URL erreichbar war, erhalten wir den Token, den wir für einige Api-Endpunkte benötigen werden.

Neben der on_start-Methode finden sich jetzt Test-Methoden in der Klasse EventManagerUser, die mit dem @task-Dekorator dekoriert sind. Das sind die eigentlichen Testfunktionen. Hier sprechen wir die API-Endpunkte an und senden im Header den Token mit. Für unsere beiden Test-Methoden nutzen wir die GET Methode.

Starten des Tests

Im Grunde kann es jetzt losgehen. Wir switchen ins Verzeichnis event_manager/events und starten den Locus-Server

(eventenv) locust

Wenn Locust dort im Verzeichnis die Datei locustfile.py findet, sollte Locust fehlerfrei starten.

Wichtig: Der Runserver muss natürlich die ganze Zeit laufen, sonst können die Endpunkte nicht angesprochen werden.

Das Locust Backend öffnen

Jetzt ist es an der Zeit, den Lasttest durchzuführen. Dazu wechseln wir im Browser auf die URL http://0.0.0.0:8089/ und sehen das Locust Eingabefenster.

../_images/locust_dashboard.png

Hier können wir jetzt die Anzahl der User, die User-Spawnrate und den Host angeben, den wir testen wollen. Da unsere Testapplikation auf 127.0.0.1:8000 läuft, wählen wir diese als Host.

Auf der Übersichtsseite sehen wir dann für jeden unserer Tasks einen Eintrag. Der Locust-Server läuft solange, bis wir ihn explizit stoppen.

../_images/locust_dashboard2.png

Wir können Statistiken für jeden Endpunkt, Gesamtanforderungen, Medienresonanz, Antwortperzentil und RPS (Requests per Second) anzeigen. Locust bietet auch Diagramme mit der Statistik der Gesamtanfrage pro Sekunde, der Antwortzeit und der Erhöhung der Anzahl der Benutzer.

Es fällt auf, dass die Kategorie-Liste schon bei 10 Usern im Durchschnitt ca. 49.000 Millisekunden benötigt, dass sind 49 Sekunden. Dieser Endpunkt müsste also nochmal kontrekt überarbeit werden, vermutlich würde hier ein prefetch-related das Problem lösen. Wir können das prefetch-reated auch für verschachtelte Foreign-Key Beziehungen anlegen: Die Autor-Objekte laden wir vor, indem wir events__author prefetchen.

def get_queryset(self):
        order_by = self.request.query_params.get("ordering", None)
        queryset = models.Category.objects.prefetch_related(
                "events",
                "events__author"
        ).all()

        if order_by == "date":

            queryset = queryset.annotate(
                num_events=Count("events")
            ).order_by("-" + order_by)

        return queryset