.. _api_loadtesting: .. index:: single: Locust single: Api Loadtesting Loadtesting testen mit Locust ******************************* .. admonition:: 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: .. code-block:: bash (eventenv) pip install locust wir können auch gleich testen, ob die Installation erfolgreich war und lassen uns die Version anzeigen. .. code-block:: bash (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. .. code-block:: python 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. .. code-block:: python 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 .. code-block:: bash (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. .. image:: /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. .. image:: /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. .. code-block:: python 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