Captchas in Locust lösen, ohne Ihre Statistiken zu verfälschen

Ein Captcha-Lösen in Locust muss sich aus zwei Bereichen heraushalten: aus Ihren Statistiken und aus dem Pfad, der bei jeder Iteration durchlaufen wird. Locust meldet jede Anfrage, die über self.client läuft, sodass ein Lösen über diesen Client die Antwortzeiten des Lösers in genau den Bericht schreibt, den Sie eigentlich lesen wollen. Und ein Lösen innerhalb einer Task läuft bei jeder Iteration jedes Benutzers. Beides lässt sich leicht vermeiden, sobald Sie wissen, wo die Grenzen verlaufen.
Was Sie brauchen
- Locust 2.x auf Python 3.10 oder neuer, was auch der CapSkip-Client benötigt.
- CapSkip läuft auf einer Windows-Maschine, und der Python-Client ist neben Ihrem locustfile installiert.
- Der sitekey und die Seiten-URL des geschützten Formulars, übergeben statt von jedem Benutzer neu ermittelt.
- Server-Modus, falls Locust irgendwo anders läuft als auf der Maschine des Lösers, und das schließt jeden Container und jeden Worker-Rechner ein. Es ist eine einzige Einstellung unter den Verbindungseinstellungen.
# pip install capskip pip install -U locust capskip
Schritt 1: Halten Sie das Lösen von self.client fern
Das ist der Teil, der einen Bericht klammheimlich ruiniert. Die Dokumentation von Locust ist eindeutig, was self.client ist: eine Instanz von HttpSession, die eine Unterklasse und ein Wrapper von requests.Session ist, und was sie ergänzt, ist die Meldung der Anfrageergebnisse an Locust. Erfolg und Fehler, Antwortzeit, Antwortlänge, Name. Alles, was darüber gesendet wird, landet in der Statistiktabelle.
Ein Lösen dauert Sekunden. Die Endpunkte Ihrer Anwendung brauchen Millisekunden. Stecken Sie das eine mit dem anderen in dieselbe Tabelle, und Ihr fünfundneunzigstes Perzentil misst nur noch, wie lange ein Captcha gedauert hat, und das ist keine Zahl, nach der irgendjemand gefragt hat.
Die gute Nachricht ist, dass die Lösung darin besteht, nichts zu tun. Der CapSkip-Client hat einen eigenen HTTP-Transport und berührt self.client nie, sodass ein Lösen für die Statistiken von Locust standardmäßig unsichtbar ist. Locust protokolliert nur das, was durch seine eigene Session läuft.
# pip install capskip
import os
from capskip import CapSkip
# CAPSKIP_HOST is the solver machine. 127.0.0.1 only works when
# Locust and CapSkip run on the same Windows box.
solver = CapSkip(
host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
port=int(os.environ.get("CAPSKIP_PORT", "8080")),
apiKey=os.environ.get("CAPSKIP_API_KEY", "capskip"),
)
def fresh_token(sitekey, page_url):
result = solver.recaptcha(sitekey=sitekey, url=page_url)
return result["code"] # the token, and Locust never sees itWenn Sie lieber selbst gegen die rohen Endpunkte programmieren, gilt dieselbe Regel: Verwenden Sie eine einfache requests.Session, nicht self.client. Anfragen, die direkt mit der requests-Bibliothek gestellt werden, protokolliert Locust nicht, und genau das wollen Sie hier. Diese beiden Endpunkte sind dokumentiert in der CapSkip-API-Dokumentation.
Das Gegenteil ist nur dann richtig, wenn Sie den Löser selbst absichtlich einem Lasttest unterziehen. Dann schicken Sie es über self.client mit einem name-Argument, damit sich jedes Lösen in einer einzigen Zeile sammelt statt in einer Zeile pro sitekey, und lesen diese Zeile getrennt vom Rest.
Schritt 2: Wie oft läuft Ihr Lösen tatsächlich?
Locust bietet Ihnen vier Stellen dafür, und sie unterscheiden sich um Größenordnungen. Rechnen Sie den Multiplikator aus, bevor Sie eine davon auswählen.
| Wo das Lösen sitzt | Wie oft es läuft |
|---|---|
| In einer Task-Funktion | Einmal pro Iteration und Benutzer. Tausende pro Minute |
| In der Methode on_start | Einmal pro simuliertem Benutzer. Fünfhundert Benutzer bedeuten fünfhundert Lösungen |
| In einem test_start-Listener | Einmal pro Knoten, was nicht einmal pro Lauf bedeutet. Siehe Schritt 3 |
| In einem test_start-Listener, der auf den Master beschränkt ist | Einmal pro Lauf, und dann muss es verteilt werden |
Die Standardantwort für fast alle ist on_start. Ein Benutzer ruft on_start auf, wenn er zu laufen beginnt, sodass jeder simulierte Benutzer seinen eigenen Token bekommt, ihn für seine eigene Session behält und nichts zwischen Greenlets weitergegeben werden muss. Das entspricht auch genau der Form dessen, was ein echter Benutzer tut.
from locust import HttpUser, task, between
class Signup(HttpUser):
wait_time = between(1, 3)
def on_start(self):
# One solve per simulated user, off the statistics.
self.token = fresh_token(SITEKEY, PAGE_URL)
@task
def submit(self):
self.client.post("/signup", data={
"email": "[email protected]",
"g-recaptcha-response": self.token,
})Fünfhundert Lösungen klingen teuer, weil sie es bei einem abgerechneten Dienst auch sind. Genau deshalb verrenken sich die meisten Lasttest-Anleitungen, um einmal zu lösen und das Ergebnis zu teilen. Mit einem Löser auf Ihrer eigenen Hardware ist die Zahl keine Budgetfrage mehr, sondern eine Kapazitätsfrage, und die lässt sich viel leichter beantworten: Fahren Sie die Rampe hoch und beobachten Sie die Maschine.
Schritt 3: test_start wird auf jedem Knoten ausgelöst, nicht einmal pro Lauf
Das ist das Locust-Detail, das die Leute überrascht, und es zeigt sich als eine Lösungszahl, die ein glattes Vielfaches von irgendetwas ist. Die Dokumentation sagt, dass test_start auf jedem Knoten ausgelöst wird, wenn ein neuer Lasttest gestartet wird. Laufen Sie mit vier Worker-Prozessen, haben Sie fünf Knoten, sodass ein Lösen in einem einfachen test_start-Listener fünfmal läuft.
Sichern Sie das ab, indem Sie den Runner-Typ prüfen. Locust liefert MasterRunner und WorkerRunner genau dafür mit, und das Muster in der eigenen Anleitung zu verteilten Läufen besteht darin, zu prüfen, auf welchem von beiden Sie sich befinden.
from locust import events
from locust.runners import WorkerRunner
@events.init.add_listener
def on_init(environment, **kwargs):
# Workers listen. Registering here runs before the test starts.
if isinstance(environment.runner, WorkerRunner):
environment.runner.register_message("captcha_token", take_token)
@events.test_start.add_listener
def on_test_start(environment, **kwargs):
# The master solves once and broadcasts. Workers skip this.
if not isinstance(environment.runner, WorkerRunner):
environment.runner.send_message(
"captcha_token", fresh_token(SITEKEY, PAGE_URL)
)
def take_token(environment, msg, **kwargs):
environment.shared_token = msg.dataZwei Dinge zu diesem Block. Die Signatur des Handlers liegt fest: Er nimmt die Umgebung, die Nachricht und Schlüsselwortargumente entgegen, und die Nutzdaten kommen als msg.data an. Und wenn ein Handler länger dauern wird, registrieren Sie ihn mit concurrent auf True, damit er in seinem eigenen Greenlet läuft, statt den Heartbeat von Locust und andere Systemnachrichten zu blockieren. Ein Handler, der nur einen String speichert, braucht das nicht. Ein Handler, der selbst löst, schon.
Lesen Sie den nächsten Abschnitt, bevor Sie irgendetwas davon bauen, denn eine Lösung pro Lauf ist ohnehin meist das falsche Ziel.
Schritt 4: Ein Token übersteht keinen langen Test
Ein reCAPTCHA-Token ist etwa zwei Minuten lang gültig. Ein Lasttest dauert normalerweise länger als zwei Minuten. Die saubere Architektur, also ein Lösen beim Teststart und dann an jeden Worker verteilt, erzeugt damit einen Lauf, bei dem die ersten paar Minuten durchgehen und alles danach an einem abgelaufenen Token scheitert, wobei die Fehler Ihrer Anwendung zugeschrieben werden.
Diesen Fehler sollten Sie auf den ersten Blick erkennen, und es ist derselbe, der Leute erwischt, die Queue-Jobs verketten. Genau darum geht es im Leitfaden zum Thema Gültigkeitsdauer von reCAPTCHA-Token.
Verwenden Sie das Muster mit geteiltem Token also nur, wenn der Test kurz ist oder wenn der Token nur einmal während des Hochfahrens gebraucht wird und nicht bei jeder Iteration. Lösen Sie ansonsten pro Benutzer in on_start, und aktualisieren Sie bei einem Dauertest, der stundenlang läuft, nach einem Zeitplan innerhalb des Benutzers.
import time
class Signup(HttpUser):
wait_time = between(1, 3)
def on_start(self):
self.token = fresh_token(SITEKEY, PAGE_URL)
self.solved_at = time.monotonic()
@task
def submit(self):
# Refresh before the token ages out, not after it fails.
if time.monotonic() - self.solved_at > 90:
self.token = fresh_token(SITEKEY, PAGE_URL)
self.solved_at = time.monotonic()
self.client.post("/signup", data={
"g-recaptcha-response": self.token,
})Neunzig Sekunden statt hundertzwanzig Sekunden, damit die Aktualisierung erfolgt, solange der Token noch gültig ist.
Schritt 5: Locust ist gevent, verwenden Sie also den synchronen Client
Locust führt jeden Benutzer in einem eigenen Greenlet aus und arbeitet ereignisbasiert mit gevent. Die Dokumentation betont, dass Sie Ihre Tests dadurch als ganz normalen blockierenden Python-Code schreiben können und nicht mit Callbacks. Blockieren ist hier der native Stil, also ist der einfache CapSkip-Client die richtige Wahl.
Greifen Sie in einem locustfile nicht zu AsyncCapSkip. In Python ist das eine echte asynchrone Implementierung und kein Alias, was es in einem asyncio-Programm zum richtigen Client macht, und Locust ist keines. Es wartet keine Event-Loop darauf, und pro Benutzer eine innerhalb eines Greenlets zu starten, ist sehr viel Maschinerie, um zurückzubekommen, was gevent ohnehin schon leistet.
Auch das Polling-Verhalten hilft hier. Der Client pollt nicht in einem festen Intervall. Er startet bei einer Viertelsekunde und nähert sich schrittweise pollingInterval an, sodass ein schnelles Lösen schnell zurückkommt, statt eine feste Verzögerung abzuwarten. Über fünfhundert Greenlets hinweg, die gemeinsam hochfahren, macht dieser Unterschied den Großteil Ihrer Rampe aus. Wenn Sie anderswo in einem asyncio-Programm tatsächlich einen Stapel Captchas auf einmal lösen müssen, gibt es dafür einen eigenen Leitfaden zum parallelen Lösen von Captchas in Python.
Schritt 6: Den Löser dort betreiben, wo die Worker ihn erreichen
Lastgeneratoren sitzen selten auf derselben Maschine wie sonst etwas. Sie bekommen einen eigenen Rechner, oder mehrere, oder einen Pool von Containern, gerade damit die Last echt ist. CapSkip läuft unter Windows, Ihre Worker vielleicht nicht.
Es gibt zwei Verbindungsmodi. Der Local-Modus bindet an 127.0.0.1 und antwortet nur diesem Gerät. Der Server-Modus bindet an Ihre Netzwerkadresse oder Ihre öffentliche IP, sodass ein anderer Rechner, ein Container-Host oder ein gehosteter Runner dieselbe Windows-Maschine über die API erreichen kann. Beide finden Sie unter Verbindungseinstellungen, und der Server-Modus ändert nur, auf welcher Adresse der Löser lauscht. Es ist weiterhin Ihre Hardware, und es wird weiterhin nicht pro Lösung abgerechnet.
| Wo Locust läuft | Welcher Verbindungsmodus |
|---|---|
| Dieselbe Windows-Maschine wie CapSkip, ein einzelner Prozess | Local-Modus, der Host bleibt 127.0.0.1 |
| Worker-Prozesse auf anderen Maschinen in Ihrem Netzwerk | Server-Modus mit der LAN-Adresse des Solvers |
| Container oder ein gehosteter Runner | Server-Modus mit einer statischen öffentlichen IP und einer Firewallregel |
Lesen Sie die Adresse aus der Umgebung statt aus dem locustfile. Der Python-Client liest weder CAPSKIP_HOST noch CAPSKIP_PORT noch CAPSKIP_API_KEY von sich aus, deshalb liest der Solver oben diese Variablen aus und übergibt sie, sodass dieselbe Datei unverändert auf Ihrem Laptop und auf einer Worker-Flotte funktioniert.
Häufige Fehler und was sie bedeuten
| Was Sie sehen | Ursache | Beheben |
|---|---|---|
| Solver-Zeilen in der Locust-Statistiktabelle | Das Lösen lief über self.client | Verwenden Sie den CapSkip-Client oder eine einfache requests.Session |
| Perzentile weit über dem, was die App wirklich leistet | Gleiche Ursache. Die Lösungszeiten fließen in den Durchschnitt ein | Gleiche Behebung. Sonst muss sich nichts ändern |
| Die Lösungszahl ist ein Vielfaches Ihrer Worker-Anzahl | test_start wird auf jedem Knoten ausgelöst | Sichern Sie den Listener mit einer WorkerRunner-Prüfung ab |
| Ein paar Minuten nach Beginn des Laufs schlägt alles fehl | Ein einziger Token wurde beim Teststart gelöst und ist abgelaufen | Lösen Sie pro Benutzer, oder aktualisieren Sie innerhalb der Task |
| NetworkException von allen Benutzern gleichzeitig | CapSkip liegt auf dem Loopback und Locust anderswo | Wechseln Sie in den Server-Modus und setzen Sie CAPSKIP_HOST |
| Heartbeat-Warnungen während eines verteilten Laufs | Ein langsamer Nachrichten-Handler blockiert den Runner | Registrieren Sie ihn mit concurrent auf True |
| ERROR_WRONG_USER_KEY in einer ApiException | CAPSKIP_API_KEY ist in der Worker-Umgebung nicht gesetzt | Setzen Sie ihn auf den Workern und starten Sie diese neu |
Für die letzte Zeile gibt es einen eigenen Leitfaden, denn dieselbe Antwort deckt sowohl einen fehlenden als auch einen schlicht falschen Schlüssel ab: so beheben Sie ERROR_WRONG_USER_KEY.
FAQ
Soll ich einmal pro Test oder einmal pro Benutzer lösen?
Einmal pro Benutzer, es sei denn, der gesamte Test dauert weniger als zwei Minuten. Ein Token läuft lange vor dem Ende eines echten Lasttests ab, sodass die Variante mit geteiltem Token still und leise zu einem Test Ihres Fehlerpfads wird. Das Lösen pro Benutzer ist außerdem die ehrlichere Simulation, denn echte Benutzer bringen jeweils ihren eigenen Token mit. Der einzige Grund, es zu vermeiden, ist die Abrechnung pro Lösung, und ein Löser auf Ihrer eigenen Hardware nimmt diesen Grund weg.
Zählt das Lösen zu meinen Anfragen pro Sekunde?
Nein, solange es nicht über self.client läuft. Locust baut seine Statistiken aus der eigenen Session auf, sodass alles, was mit dem CapSkip-Client oder einer einfachen requests.Session gesendet wird, für den Bericht unsichtbar ist. Das ist das Standardverhalten und erfordert keine Konfiguration.
Funktioniert das mit FastHttpUser?
Ja, und es ändert sich nichts. FastHttpUser tauscht den Client gegen eine schnellere Implementierung, was sich lohnt, wenn der Lastgenerator selbst der Engpass ist, aber das Lösen lief ohnehin nie über diesen Client. Die Methode on_start und die Event-Listener verhalten sich identisch.
Worin unterscheidet sich das vom k6-Leitfaden?
Der Rat läuft fast genau andersherum, und das aus gutem Grund. k6 kann kein Node-Paket installieren, deshalb geht dieser Leitfaden über die rohe HTTP-API und löst einmal in der Setup-Phase, weil eine Wiederholung dort wirklich unhandlich ist. Locust ist Python, der Client lässt sich normal installieren, und das Lösen pro Benutzer ist sowohl einfach als auch genauer. Der Vergleich steht im Leitfaden für k6-Lasttests.
Die Kurzfassung
Lösen Sie mit dem CapSkip-Client, damit die Anfrage nie self.client erreicht und nie in Ihren Statistiken landet. Setzen Sie den Aufruf in on_start, damit es einen Token pro simuliertem Benutzer gibt, und aktualisieren Sie ihn innerhalb der Task, wenn der Lauf länger als etwa neunzig Sekunden dauert. Wenn Sie doch in einem test_start-Listener lösen, sichern Sie ihn mit einer WorkerRunner-Prüfung ab, denn dieses Ereignis wird auf jedem Knoten ausgelöst. Bleiben Sie beim synchronen Client, denn die Nebenläufigkeit von Locust ist gevent. Betreiben Sie CapSkip im Server-Modus, wann immer die Lastgeneratoren nicht auf der Maschine des Lösers laufen.
- Die Client-Oberfläche und alle Captcha-Typen, die sie abdeckt, finden Sie auf der Python-Captcha-Solver-Seite.
- Die Checkbox-Challenge selbst erklärt Ihnen die reCAPTCHA-v2-Solver-Seite.
Eines sollten Sie klären, bevor Sie die Rampe dimensionieren: CapSkip ist ein unbegrenzter Captcha-Löser und läuft auf Hardware, die Ihnen bereits gehört, sodass tausend simulierte Benutzer, die jeweils ihre eigene Challenge lösen, genau so viel kosten wie zehn.
