A/B-Tests mit NGINX und NGINX Plus durchführen

NGINX | 25. Juli 2016

Beim Testen von Änderungen an einer Anwendung lassen sich einige Faktoren nur in einer Produktionsumgebung und nicht in einer Entwicklungstestumgebung messen. Dazu gehören beispielsweise die Auswirkungen von Änderungen an der Benutzeroberfläche auf das Nutzerverhalten und auf die Gesamtleistung. Eine gängige Testmethode ist das A/B-Testing – auch bekannt als Split-Testing –, bei dem ein (in der Regel kleiner) Teil der Nutzer zur neuen Version einer Anwendung weitergeleitet wird, während die meisten Nutzer weiterhin die aktuelle Version verwenden.

In diesem Blogbeitrag erläutern wir, warum A/B-Tests bei der Bereitstellung neuer Versionen Ihrer Webanwendung wichtig sind und wie Sie mit NGINX und NGINX Plus steuern können, welche Version einer Anwendung den Benutzern angezeigt wird. Konfigurationsbeispiele veranschaulichen, wie Sie mit Direktiven, Parametern und Variablen von NGINX und NGINX Plus präzise und messbare A/B-Tests durchführen können.

Warum sollte man A/B-Tests durchführen?

Wie bereits erwähnt, können Sie mithilfe von A/B-Tests die Unterschiede bei der Leistung oder Effektivität zweier Anwendungsversionen messen. Vielleicht möchte Ihr Entwicklungsteam die visuelle Anordnung der Schaltflächen in der Benutzeroberfläche ändern oder den gesamten Warenkorbprozess überarbeiten, dabei jedoch die Abschlussquote der Transaktionen vergleichen, um sicherzustellen, dass die Änderung die gewünschte geschäftliche Wirkung erzielt. Mit A/B-Tests können Sie einen festgelegten Prozentsatz des Datenverkehrs an die neue Version und den verbleibenden Anteil an die alte Version weiterleiten und so die Effektivität beider Anwendungsversionen messen.

Oder vielleicht geht es Ihnen weniger um die Auswirkungen auf das Nutzerverhalten als vielmehr um die Auswirkungen auf die Performance. Angenommen, Sie planen, umfangreiche Änderungen an Ihrer Webanwendung bereitzustellen, und sind der Ansicht, dass Tests in Ihrer Qualitätssicherungsumgebung die möglichen Auswirkungen auf die Performance in der Produktionsumgebung nicht realistisch abbilden. In diesem Fall können Sie die neue Version im Rahmen einer A/B-Bereitstellung zunächst einem kleinen, festgelegten Prozentsatz der Besucher zugänglich machen, um die Auswirkungen der Änderungen auf die Performance zu messen. Anschließend erhöhen Sie diesen Anteil schrittweise, bis Sie die geänderte Anwendung schließlich für alle Benutzer bereitstellen.

NGINX und NGINX Plus für A/B-Tests verwenden

NGINX und NGINX Plus bieten zwei Methoden, mit denen sich steuern lässt, wohin der Datenverkehr von Webanwendungen geleitet wird. Die erste Methode ist in beiden Produkten verfügbar, die zweite hingegen nur in NGINX Plus.

Beide Methoden wählen das Ziel einer Anfrage anhand der Werte einer oder mehrerer NGINX-Variablen aus, die Merkmale des Clients (z. B. seine IP-Adresse) oder des Anfrage-URI (z. B. ein benanntes Argument) erfassen. Aufgrund ihrer Unterschiede eignen sie sich jedoch für verschiedene A/B-Testing-Anwendungsfälle:

  • Die Methode split_clients wählt das Ziel einer Anfrage anhand eines Hashwerts der aus der Anfrage extrahierten Variablenwerte aus. Die Menge aller möglichen Hashwerte wird auf die Anwendungsversionen aufgeteilt, wobei Sie jeder Anwendung einen anderen Anteil der Menge zuweisen können. Dadurch erfolgt die Auswahl des Ziels nach dem Zufallsprinzip.
  • Die sticky route-Methode bietet Ihnen wesentlich mehr Kontrolle über das Ziel jeder Anfrage. Die Anwendung wird anhand der Variablenwerte selbst (und nicht anhand eines Hashwerts) ausgewählt. So können Sie explizit festlegen, welche Anwendung Anfragen mit bestimmten Variablenwerten erhält. Sie können außerdem reguläre Ausdrücke verwenden, um die Entscheidung nur auf Teile eines Variablenwerts zu stützen, und eine Variable gegenüber einer anderen als Entscheidungsgrundlage bevorzugen.

Verwendung der split_clients-Methode

Bei dieser Methode legt der Konfigurationsblock split_clients für jede Anfrage eine Variable fest, die bestimmt, an welche Upstream-Gruppe die Direktive proxy_pass die Anfrage sendet. In der folgenden Beispielkonfiguration bestimmt der Wert der Variablen $appversion, wohin die Direktive proxy_pass eine Anfrage sendet. Der Block split_clients verwendet eine Hash-Funktion, um den Wert der Variablen dynamisch auf einen von zwei Namen für Upstream-Gruppen festzulegen: entweder version_1a oder version_1b.

http {    # ...
    # application version 1a 
    upstream version_1a {
        server 10.0.0.100:3001;
        server 10.0.0.101:3001;
    }

    # application version 1b
    upstream version_1b {
        server 10.0.0.104:6002;
        server 10.0.0.105:6002;
    }

    split_clients "${arg_token}" $appversion {
        95%     version_1a;
        *       version_1b;
    }

    server {
        # ...
        listen 80;
        location / {
            proxy_set_header Host $host;
            proxy_pass http://$appversion;
        }
    }
}

Der erste Parameter der split_clients-Direktive ist die Zeichenfolge ("${arg_token}" in unserem Beispiel), die bei jeder Anfrage mithilfe einer MurmurHash2-Funktion gehasht wird. URI-Argumente stehen NGINX als Variablen mit der Bezeichnung $arg_name zur Verfügung – in unserem Beispiel erfasst die Variable $arg_token das URI-Argument namens token. Sie können eine beliebige NGINX-Variable oder eine Zeichenfolge aus Variablen als zu hashende Zeichenfolge verwenden. Beispielsweise können Sie die IP-Adresse des Clients (die Variable $remote_addr), den Port ($remote_port) oder eine Kombination aus beidem hashen. Verwenden Sie eine Variable, die erzeugt wird, bevor NGINX die Anfrage verarbeitet. Variablen, die Informationen über die ursprüngliche Anfrage des Clients enthalten, sind ideal. Beispiele hierfür sind die bereits erwähnte IP-Adresse bzw. der Port des Clients, der Anfrage-URI oder sogar HTTP-Anfrage-Header.

Der zweite Parameter der split_clients-Direktive ($appversion in unserem Beispiel) ist die Variable, die dynamisch anhand des Hashwerts des ersten Parameters festgelegt wird. Die Anweisungen innerhalb der geschweiften Klammern unterteilen die Hash-Tabelle in „Buckets“, die jeweils einen bestimmten Prozentsatz der möglichen Hashwerte enthalten. Sie können beliebig viele Buckets erstellen, die nicht alle gleich groß sein müssen. Beachten Sie, dass der Prozentsatz für den letzten Bucket immer durch ein Sternchen (*) und nicht durch eine bestimmte Zahl dargestellt wird, da sich die Anzahl der Hashwerte möglicherweise nicht gleichmäßig auf die angegebenen Prozentsätze verteilen lässt.

In unserem Beispiel legen wir 95 % der Hashes in einem Bucket ab, der der Upstream-Gruppe version_1a zugeordnet ist, und den Rest in einem zweiten Bucket, der version_1b zugeordnet ist. Der Bereich möglicher Hashwerte reicht von 0 bis 4.294.967.295. Der erste Bucket enthält daher Werte von 0 bis etwa 4.080.218.930 (95 % der Gesamtmenge). Die Variable $appversion wird auf den Upstream gesetzt, der dem Bucket zugeordnet ist, in dem sich der Hash der Variable $arg_token befindet. Ein konkretes Beispiel: Der Hashwert 100.000.000 fällt in den ersten Bucket, daher wird $appversion dynamisch auf version_1a gesetzt.

Testen der split_clients-Konfiguration

Um zu überprüfen, ob der Konfigurationsblock split_clients wie vorgesehen funktioniert, haben wir eine Testkonfiguration erstellt, die Anfragen im gleichen Verhältnis wie oben (95 % und der Rest) auf zwei Upstream-Gruppen verteilt. Wir haben die virtuellen Server in den Gruppen so konfiguriert, dass sie eine Zeichenfolge zurückgeben, die angibt, welche Gruppe – version_1a oder version_1b – die Anfrage verarbeitet hat (die Testkonfiguration finden Sie hier). Anschließend haben wir mit curl 20 Anfragen erzeugt, wobei der Wert des URI-Arguments token zufällig festgelegt wurde, indem der Befehl cat auf die Datei urandom angewendet wurde. Dies dient ausschließlich der Veranschaulichung und Randomisierung. Wie beabsichtigt, wurde 1 von 20 Anfragen (95 %) von version_1b verarbeitet (der Kürze halber zeigen wir nur 10 der Anfragen).

# for x in {1..20}; do curl 127.0.0.1?token=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1); doneToken: p3Fsa86HfJDFwZ9ZnYz4QbaZLzcb70Ka		Served from site version_1a. 
Token: a7z7afUz7zQijXwsLGfj6mWyPU8sCRIZ		Served from site version_1a. 
Token: CjIr6W57NtPzChf4qbYMzD1Estif7jOH		Served from site version_1a. 
                 ... output for 10 requests omitted ...
Token: gXq8cbG3jhl1LuYmICPTfDQT855gaO5y		Served from site version_1a. 
Token: VVqGuNN3cRU2slsl5AWOR93aQX8nXOpk		Served from site version_1a. 
Token: z7KnewxTX5Sp6wscT0fgmTDohTsQuCmy		Served from site version_1b!!
Token: fWOgH9WRrR0kLJZcIaYchpLhceaQgPD1		Served from site version_1a. 
Token: mTADMXrVnwnr1cd5JE6QCSkgTwfWUnDk		Served from site version_1a.
Token: w7AzSNmNJtxWZaH6cXe2PWIFqst2o3oP		Served from site version_1a. 
Token: QR7ay0dA39MmVlXtzgOVsj6SBTPi8ECC		Served from site version_1a.

Verwendung der sticky route-Methode

In einigen Fällen möchten Sie möglicherweise eine statische Route definieren, indem Sie Routing-Entscheidungen für Clients auf Grundlage des vollständigen Werts oder eines Teils des Werts einer NGINX-Variablen treffen. Hierzu können Sie die sticky route-Direktive verwenden, die nur in NGINX Plus verfügbar ist. Die Direktive akzeptiert eine Liste mit einem oder mehreren Parametern und legt als Route den Wert des ersten nicht leeren Parameters in der Liste fest. Mit dieser Funktion können Sie priorisieren, welche Variable aus der Anfrage die Auswahl des Ziels steuert, und so mehrere Methoden zur Aufteilung des Datenverkehrs in einer einzigen Konfiguration berücksichtigen.

Für die Anwendung dieser Methode gibt es zwei verschiedene Ansätze.

  • Beim clientseitigen Ansatz können Sie die Route anhand von NGINX-Variablen auswählen, die Werte enthalten, welche ursprünglich direkt vom Client gesendet werden, beispielsweise die IP-Adresse des Clients oder browserspezifische HTTP-Anfrageheader wie den User-Agent.
  • Beim server- oder anwendungsseitigen Ansatz entscheidet Ihre Anwendung, welcher Testgruppe ein neuer Benutzer zugewiesen wird, und sendet ihm ein Cookie oder einen Umleitungs-URI, der eine Routenkennung für die ausgewählte Gruppe enthält. Wenn der Client das nächste Mal eine Anfrage sendet, übermittelt er das Cookie oder verwendet den Umleitungs-URI. Die Direktive sticky route extrahiert die Routenkennung und leitet die Anfrage an den entsprechenden Server weiter.

In unserem Beispiel verwenden wir den anwendungsseitigen Ansatz: Die sticky route-Direktive in der upstream-Gruppe setzt die Route bevorzugt auf den Wert, der in dem von einem Server bereitgestellten Cookie angegeben ist (erfasst in $route_from_cookie). Wenn der Client kein Cookie hat, wird die Route auf einen Wert aus einem Argument des Anfrage-URI gesetzt ($route_from_uri). Der Routenwert bestimmt dann, welcher server in der Upstream-Gruppe die Anfrage erhält – der erste Server, wenn die Route a ist, und der zweite Server, wenn die Route b ist (die beiden Server entsprechen den beiden Versionen unserer Anwendung).

upstream backend {    zone backend 64k;
    server 10.0.0.200:8098 route=a;
    server 10.0.0.201:8099 route=b;

    sticky route $route_from_cookie $route_from_uri;
}

Doch das a bzw. b ist im tatsächlichen Cookie oder URI in eine wesentlich längere Zeichenfolge eingebettet. Um nur den Buchstaben zu extrahieren, konfigurieren wir jeweils einen map-Konfigurationsblock für das Cookie und den URI:

map $cookie_route $route_from_cookie {    ~.(?P<route>w+)$ $route;
}

map $arg_route $route_from_uri {
    ~.(?P<route>w+)$ $route;
}

Im ersten map-Block stellt die Variable $cookie_route den Wert eines Cookies namens ROUTE dar. Der reguläre Ausdruck in der zweiten Zeile, der die Syntax Perl-kompatibler regulärer Ausdrücke (PCRE) verwendet, extrahiert einen Teil des Werts – in diesem Fall die Zeichenfolge (w+) nach dem Punkt – in die benannte Erfassungsgruppe route und weist ihn der gleichnamigen internen Variable zu. Der Wert wird außerdem der Variable $route_from_cookie in der ersten Zeile zugewiesen, sodass er an die Direktive sticky route übergeben werden kann.

Beispielsweise extrahiert der erste map-Block den Wert „a“ aus diesem Cookie und weist ihn $route_from_cookie zu:

ROUTE=iDmDe26BdBDS28FuVJlWc1FH4b13x4fn.a

Im zweiten map-Block stellt die Variable $arg_route ein Argument namens route im Anfrage-URI dar. Wie beim Cookie extrahiert der reguläre Ausdruck in der zweiten Zeile einen Teil des URI – in diesem Fall die Zeichenfolge (w+) nach dem Punkt im Argument route. Der Wert wird in die benannte Erfassungsgruppe eingelesen, einer internen Variablen und außerdem der Variablen $route_from_uri zugewiesen.

Beispielsweise extrahiert der zweite map-Block den Wert b aus diesem URI und weist ihn $route_from_uri zu:

www.example.com/shopping/my-cart?route=iLbLr35AeAET39GvWK2Xd2GI5c24y5go.b

Hier ist die vollständige Beispielkonfiguration.

http {    # ...
    map $cookie_route $route_from_cookie {
        ~.(?P<route>w+)$ $route;
    }

    map $arg_route $route_from_uri {
        ~.(?P<route>w+)$ $route;
    }

    upstream backend {
        zone backend 64k;
        server 10.0.0.200:8098 route=a;
        server 10.0.0.201:8099 route=b;

        sticky route $route_from_cookie $route_from_uri;
    }

    server {
        listen 80;

        location / {
            # ...
            proxy_pass http://backend;
        }
    }
}

Testen der sticky route-Konfiguration

Für die split_clients-Methode haben wir eine Testkonfiguration erstellt, die Sie hier aufrufen können. Mit curl haben wir entweder ein Cookie namens ROUTE gesendet oder ein route-Argument in den URI eingefügt. Der Wert des Cookies bzw. Arguments ist eine zufällige Zeichenfolge, die durch Ausführen des Befehls cat auf die Datei urandom erzeugt und um .a bzw. .b ergänzt wird.

Zunächst testen wir mit einem Cookie, das auf .a endet:

# curl --cookie "ROUTE=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1).a" 127.0.0.1Cookie Value: R0TdyJOJvxBkLC3f75Coa29I1pPySOeQ.a
Request URI: /
Results: Site A - Running on port 8089

Anschließend testen wir mit einem Cookie, das auf .b endet.

# curl --cookie "ROUTE=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1).b" 127.0.0.1Cookie Value: JhdJZrScTnPBLhqmzK3podNRcJAIc8ST.b
Request URI: /
Results: Site B - Running on port 8099

Abschließend führen wir den Test ohne Cookie durch und verwenden stattdessen ein route-Argument im Anfrage-URI, das auf .a endet. Die Ausgabe bestätigt, dass NGINX Plus den aus dem URI abgeleiteten route-Wert verwendet, wenn kein Cookie vorhanden ist (das Feld Cookie Value ist leer).

# curl 127.0.0.1?route=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1).aCookie Value:
Request URI: /?route=yNp8pHskvukXK6XqbWefhVUcOBjbJv4v.a
Results: Site A - Running on port 8089

Protokollieren und Analysieren Ihrer Ergebnisse

Die hier beschriebene Art von Tests reicht aus, um zu überprüfen, ob eine Konfiguration die Anfragen wie vorgesehen verteilt. Die Auswertung der Ergebnisse tatsächlicher A/B-Tests erfordert jedoch eine wesentlich detailliertere Protokollierung und Analyse der Anfrageverarbeitung. Wie Protokollierung und Analyse am besten umgesetzt werden, hängt von vielen Faktoren ab und würde den Rahmen dieses Beitrags sprengen. NGINX und NGINX Plus bieten jedoch ausgefeilte, integrierte Funktionen zur Protokollierung und Überwachung der Anfrageverarbeitung.

Mit der Direktive „log_format“ können Sie ein benutzerdefiniertes Protokollformat definieren, das beliebige NGINX-Variablen enthält. Die in den NGINX-Protokollen aufgezeichneten Variablenwerte stehen dann für eine spätere Analyse zur Verfügung. Weitere Informationen zur benutzerdefinierten Protokollierung und Laufzeitüberwachung finden Sie im NGINX Plus-Administratorhandbuch.

Einige abschließende Überlegungen

Achten Sie beim Entwerfen eines Experiments oder eines A/B-Testplans darauf, dass die Art und Weise, wie Sie Anfragen auf Ihre Anwendungsversionen verteilen, die Ergebnisse nicht vorwegnimmt. Wenn Sie ein vollständig zufälliges Experiment durchführen möchten, erzielen Sie die besten Ergebnisse, indem Sie die Methode split_clients verwenden und eine Kombination aus mehreren Variablen hashen. Wenn Sie beispielsweise ein eindeutiges Experiment-Token auf Grundlage der Kombination aus dem Cookie und der Benutzer-ID einer Anfrage generieren, erhalten Sie ein stärker randomisiertes Testmuster, als wenn Sie lediglich den Browsertyp und die Browserversion des Clients hashen. Denn es ist sehr wahrscheinlich, dass viele Benutzer denselben Browsertyp und dieselbe Version verwenden und daher alle zur selben Version der Anwendung weitergeleitet werden.

Sie müssen außerdem berücksichtigen, dass viele Benutzer zur sogenannten gemischten Gruppe gehören. Sie greifen von mehreren Geräten auf Webanwendungen zu – beispielsweise von ihrem Arbeits- und ihrem Heimcomputer sowie von einem Mobilgerät wie einem Tablet oder Smartphone. Solche Benutzer haben mehrere Client-IP-Adressen. Wenn Sie also die Client-IP-Adresse als Grundlage für die Auswahl der Anwendungsversion verwenden, sehen sie möglicherweise beide Versionen Ihrer Anwendung, wodurch Ihre Versuchsergebnisse verfälscht werden.

Die wahrscheinlich einfachste Lösung besteht darin, von den Benutzern eine Anmeldung zu verlangen, damit Sie ihr Sitzungscookie nachverfolgen können, wie in unserem Beispiel für die Methode sticky route. Auf diese Weise können Sie die Benutzer nachverfolgen und ihnen immer dieselbe Version anzeigen, die sie beim ersten Aufrufen des Tests gesehen haben. Wenn dies nicht möglich ist, kann es sinnvoll sein, Benutzer in Gruppen einzuteilen, die sich während des Tests voraussichtlich nicht ändern. Beispielsweise können Sie mithilfe der Geolokalisierung Benutzern in Los Angeles eine Version und Benutzern in San Francisco eine andere Version anzeigen.

Fazit

A/B-Tests sind eine effektive Methode, um Änderungen an Ihrer Anwendung zu analysieren und nachzuverfolgen sowie deren Leistung zu überwachen, indem unterschiedlich große Anteile des Datenverkehrs auf alternative Server verteilt werden. Sowohl NGINX als auch NGINX Plus bieten Direktiven, Parameter und Variablen, mit denen sich ein solides Framework für A/B-Tests erstellen lässt. Außerdem können Sie damit wertvolle Details zu jeder Anfrage protokollieren. Viel Spaß beim Testen!

Testen Sie NGINX Plus und die Methode sticky route selbst – starten Sie noch heute Ihre kostenlose 30-Tage-Testversion oder kontaktieren Sie uns, um Ihre Anwendungsfälle zu besprechen.


Teilen
Schlagwörter: F5 NGINX, Tech

Über den Autor

Kevin Jones
Kevin JonesSenior-Produktmanager | F5 NGINX

Weitere Blogbeiträge von Kevin Jones

Ähnliche Blogbeiträge

Die Zukunft von NGINX: Zurück zu unseren Open-Source-Wurzeln
NGINX | 23.08.2022

Die Zukunft von NGINX: Zurück zu unseren Open-Source-Wurzeln

Beim NGINX Sprint 2022 besinnen wir uns auf unsere Open-Source-Wurzeln. Erfahren Sie mehr über die Initiativen, die wir im kommenden Jahr verfolgen, und teilen Sie uns mit, was Sie sich wünschen.

Ein Leitfaden zur Auswahl eines Ingress-Controllers, Teil 1: Ermitteln Sie Ihre Anforderungen
NGINX | 08.09.2021

Ein Leitfaden zur Auswahl eines Ingress-Controllers, Teil 1: Ermitteln Sie Ihre Anforderungen

Ein Ingress-Controller kann eines der leistungsstärksten Tools in Ihrem Kubernetes-Stack sein. Erfahren Sie, wie Sie Ihre Ingress-Anforderungen ermitteln, um die beste Option auszuwählen.

Bereitstellung des NGINX Ingress Controllers auf Amazon EKS: So haben wir getestet
NGINX | 16.08.2021

Bereitstellung des NGINX Ingress Controllers auf Amazon EKS: So haben wir getestet

Wir haben kürzlich den Lösungsleitfaden zum NGINX Ingress Controller um Empfehlungen zur Dimensionierung für Amazon Elastic Kubernetes Service (EKS) ergänzt. In diesem Blogbeitrag erläutern wir, wie wir diese Empfehlungen erarbeitet haben, und stellen Ihnen alle Informationen zur Verfügung, die Sie benötigen, um selbst ähnliche Tests durchzuführen.

Protokollierung des Datenverkehrs zwischen NGINX und Upstream-Servern bei CDN77
NGINX | 12.12.2018

Protokollierung des Datenverkehrs zwischen NGINX und Upstream-Servern bei CDN77

Bei der Fehlersuche bei Datenverkehrsproblemen zwischen NGINX und Upstream-Servern konnte CDN77 die benötigten Daten nur aus dem Fehlerprotokoll mit aktivierter Debug-Ausgabe beziehen, was die Leistung beeinträchtigt und riesige Dateien erzeugt. Daher entwickelte das Team ein neues benutzerdefiniertes Modul, das ausschließlich die benötigten Daten protokolliert.

Ankündigung von NGINX Plus R16
NGINX | 05.09.2018

Ankündigung von NGINX Plus R16

NGINX Plus R16 ist eine einheitliche, elastische Ingress- und Egress-Schicht für Anwendungen, die die Funktionen eines Load Balancers, API-Gateways und einer WAF vereint und neue Features wie clusterfähige Ratenbegrenzung, Schlüssel-Wert-Speicher, „Random with Two Choices“, verbessertes UDP-Load-Balancing und vieles mehr bietet.

5 Gründe für den Umstieg auf softwarebasiertes Load Balancing
NGINX | 13.10.2016

5 Gründe für den Umstieg auf softwarebasiertes Load Balancing

Entdecken Sie fünf Gründe für den Wechsel zu einem softwarebasierten Load Balancer wie NGINX Plus, darunter Kosteneinsparungen, die Einführung von DevOps und eine einheitliche Lösung für alle Umgebungen.