NGINXとNGINX Plusを使用したA/Bテストの実施

NGINX | 2016年7月25日

アプリケーションへの変更をテストする際、開発用のテスト環境ではなく、本番環境でしか測定できない要素があります。たとえば、UIの変更がユーザーの行動に与える影響や、全体的なパフォーマンスへの影響などです。一般的なテスト手法として、A/Bテスト(別名:スプリットテスト)があります。この手法では、(通常は少数の)ユーザーをアプリケーションの新しいバージョンに振り分ける一方で、大半のユーザーには引き続き現行バージョンを使用してもらいます。

このブログ記事では、Webアプリケーションの新しいバージョンをデプロイする際にA/Bテストを実施することが重要な理由と、ユーザーに表示するアプリケーションのバージョンを制御するために、NGINXとNGINX Plusを使用する方法について説明します。また、NGINXおよびNGINX Plusのディレクティブ、パラメーター、変数を使用して、正確かつ測定可能なA/Bテストを実現する方法を設定例で示します。

A/Bテストを行う理由

前述のとおり、A/Bテストでは、2つのバージョン間でアプリケーションのパフォーマンスや有効性の違いを測定できます。たとえば、開発チームがUI上のボタン配置を変更したり、ショッピングカートのプロセス全体を刷新したりする際に、その変更が意図したビジネス効果をもたらすかどうかを確認するため、取引の成約率を比較したい場合があります。A/Bテストを使用すると、トラフィックのうち指定した割合を新しいバージョンに、残りを古いバージョンに振り分け、アプリケーションの両バージョンの有効性を測定できます。

あるいは、ユーザーの行動への影響よりも、パフォーマンスへの影響が懸念される場合もあります。たとえば、Webアプリケーションに大規模な変更を加えてデプロイする予定があり、品質保証環境でのテストだけでは、本番環境でパフォーマンスに及ぶ可能性のある影響を十分に把握できないとします。この場合、A/Bデプロイを使用すると、少数の一定割合の訪問者に新しいバージョンを公開して変更によるパフォーマンスへの影響を測定し、その割合を徐々に増やしながら、最終的に変更後のアプリケーションをすべてのユーザーに展開できます。

NGINXとNGINX Plusを使用したA/Bテスト

NGINXとNGINX Plusには、Webアプリケーションのトラフィックの送信先を制御する2つの方法があります。1つ目の方法は両方の製品で利用できますが、2つ目の方法はNGINX Plusでのみ利用できます。

どちらの方法も、クライアントの特性(IPアドレスなど)またはリクエストURIの特性(名前付き引数など)を取得する1つ以上のNGINX変数の値に基づいて、リクエストの送信先を選択します。ただし、両者には違いがあるため、それぞれ異なるA/Bテストのユースケースに適しています。

  • The split_clientsメソッドは、リクエストから抽出した変数値のハッシュに基づいて、リクエストの送信先を選択します。取り得るすべてのハッシュ値の集合をアプリケーションの各バージョンに分割し、バージョンごとに異なる割合を割り当てることができます。その結果、送信先はランダムに選択されます。
  • The sticky routeメソッドを使用すると、各リクエストの送信先をより細かく制御できます。アプリケーションはハッシュではなく変数値そのものに基づいて選択されるため、特定の変数値を持つリクエストを受信するアプリケーションを明示的に設定できます。また、正規表現を使用して変数値の一部のみに基づいて判断したり、判断基準として特定の変数を他の変数より優先したりすることもできます。

Using the split_clientsメソッドの使用

この方法では、split_clients設定ブロックがリクエストごとに変数を設定し、その変数によってproxy_passディレクティブがリクエストを送信するアップストリームグループが決まります。以下の設定例では、$appversion変数の値によって、proxy_passディレクティブがリクエストを送信する先が決まります。split_clientsブロックはハッシュ関数を使用して、変数の値を2つのアップストリームグループ名(version_1aまたは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;
        }
    }
}

The first parameter to the split_clientsディレクティブの第1パラメーターは、リクエストごとにMurmurHash2関数でハッシュ化される文字列(この例では"${arg_token}")です。URI引数は、$arg_nameという変数としてNGINXで使用できます。この例では、$arg_token変数がtokenというURI引数を取得します。ハッシュ化する文字列には、任意のNGINX変数または複数の変数を組み合わせた文字列を使用できます。たとえば、クライアントのIPアドレス($remote_addr変数)、ポート($remote_port)、またはその両方の組み合わせをハッシュ化できます。NGINXがリクエストを処理する前に生成される変数を使用してください。クライアントの最初のリクエストに関する情報を含む変数が適しています。たとえば、前述のクライアントのIPアドレスとポート、リクエストURI、さらにはHTTPリクエストヘッダーも使用できます。

The second parameter to the split_clientsディレクティブの第2パラメーター(この例では$appversion)は、第1パラメーターのハッシュに応じて動的に設定される変数です。波括弧内の各ステートメントは、ハッシュテーブルを「バケット」に分割します。各バケットには、生成され得るハッシュのうち指定した割合が割り当てられます。バケットは任意の数だけ作成でき、すべてを同じサイズにする必要はありません。ハッシュの数が指定した割合で均等に分割できない場合があるため、最後のバケットの割合は、特定の数値ではなく常にアスタリスク(*)で表します。

この例では、ハッシュの95%をversion_1aアップストリームグループに関連付けられたバケットに格納し、残りをversion_1bに関連付けられた2つ目のバケットに格納します。ハッシュ値の範囲は0~4,294,967,295であるため、最初のバケットには0~約4,080,218,930(全体の95%)の値が含まれます。$appversion変数には、$arg_token変数のハッシュを含むバケットに関連付けられたアップストリームが設定されます。具体例として、ハッシュ値100,000,000は最初のバケットに含まれるため、$appversionには動的にversion_1aが設定されます。

Testing the split_clients設定のテスト

To verify that the split_clients設定ブロックが意図したとおりに機能することを確認するため、上記と同じ比率(95%と残り)でリクエストを2つのアップストリームグループに振り分けるテスト設定を作成しました。各グループの仮想サーバーは、リクエストを処理したグループがversion_1aかversion_1bかを示す文字列を返すように設定しました(テスト設定はこちらで確認できます)。次に、curlを使用して20件のリクエストを生成しました。その際、URI引数tokenの値は、urandomファイルに対してcatコマンドを実行してランダムに設定しました。これは、デモとランダム化のみを目的としています。意図したとおり、20件中1件のリクエスト(95%)がversion_1bから処理されました(簡潔にするため、リクエストは10件のみ示します)。

# 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.

Using the sticky routeメソッドの使用

場合によっては、NGINX変数の値の全部または一部に基づいてクライアントのルーティングを決定し、静的ルートを定義することがあります。これは、NGINX Plusでのみ使用できるsticky routeディレクティブで実現できます。このディレクティブは1つ以上のパラメーターのリストを受け取り、リスト内で最初に見つかった空でないパラメーターの値をルートに設定します。この機能を使用すると、リクエスト内のどの変数で宛先を選択するかに優先順位を付けられるため、1つの設定で複数のトラフィック分割方法に対応できます。

この方法には、2つの異なるアプローチがあります。

  • Using the クライアント側のアプローチでは、クライアントのIPアドレスや、クライアントのUser-Agentなど、クライアントから最初に直接送信される値を含むNGINX変数に基づいてルートを選択できます。
  • With the サーバー側またはアプリケーション側のアプローチでは、アプリケーションが初回ユーザーを割り当てるテストグループを決定し、選択したグループを表すルートインジケーターを含むCookieまたはリダイレクトURIをそのユーザーに送信します。クライアントが次回リクエストを送信するときに、そのCookieを提示するか、リダイレクトURIを使用します。sticky routeディレクティブはルートインジケーターを抽出し、リクエストを適切なサーバーに転送します。

この例では、アプリケーション側のアプローチを使用しています。sticky routeディレクティブは、upstreamグループ内で、サーバーから提供されたCookieに指定されている値($route_from_cookieに格納)を優先的にルートとして設定します。クライアントにCookieがない場合は、リクエストURIの引数から取得した値($route_from_uri)をルートとして設定します。その後、ルート値に基づいて、upstreamグループ内のどのserverがリクエストを受け取るかが決まります。ルートがaの場合は1番目のサーバー、bの場合は2番目のサーバーです(この2台のサーバーは、アプリケーションの2つのバージョンに対応しています)。

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;
}

ただし、実際のCookieまたはURIでは、aまたはbは、はるかに長い文字列に埋め込まれています。この文字だけを抽出するために、CookieとURIのそれぞれに対してmap設定ブロックを構成します。

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

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

最初のmapブロックでは、$cookie_route変数は、ROUTEという名前のCookieの値を表します。2行目の正規表現は、Perl互換正規表現(PCRE)構文を使用して、値の一部(この場合はピリオドの後にある文字列(w+))を名前付きキャプチャグループrouteに抽出し、同じ名前の内部変数に割り当てます。この値は1行目の$route_from_cookie変数にも割り当てられるため、sticky routeディレクティブに渡すことができます。

例として、最初の「map」ブロックは、このCookieから値「a」を抽出し、$route_from_cookieに割り当てます。

ROUTE=iDmDe26BdBDS28FuVJlWc1FH4b13x4fn.a

2つ目の「map」ブロックでは、「$arg_route」変数は、リクエストURI内の「route」という名前の引数を表します。Cookieの場合と同様に、2行目の正規表現はURIの一部を抽出します。ここでは、「w+)という文字列を「route」引数内のピリオドの後から抽出します。この値は名前付きキャプチャグループに読み込まれ、内部変数に割り当てられるとともに、「$route_from_uri」変数にも割り当てられます。

例として、2つ目の「map」ブロックは、このURIから値「b」を抽出し、$route_from_uriに割り当てます。

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

完全な設定例は次のとおりです。

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;
        }
    }
}

Testing the sticky route設定のテスト

As for the split_clients方式については、テスト用の設定を作成しました。設定にはこちらからアクセスできます。curlを使用して、ROUTEという名前のCookieを送信するか、URIにroute引数を含めました。Cookieまたは引数の値には、catコマンドをurandomファイルに対して実行して生成したランダムな文字列の末尾に、.aまたは.bを付加したものを使用しました。

まず、末尾が「.a」のCookieを使用してテストします:

# 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

次に、末尾が「.b」のCookieを使用してテストします。

# 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

最後に、Cookieを使用せず、代わりに末尾が「route」引数をリクエストURIに指定してテストします。 .a。出力から、Cookieがない場合([Cookie]の[Value]フィールドが空の場合)、NGINX PlusはURIから取得したroute値を使用することを確認できます。

# 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

結果のログ記録と分析

ここで説明するテストは、構成によってリクエストが意図どおりに分散されることを確認するには十分です。ただし、実際のA/Bテストの結果を解釈するには、リクエストの処理方法について、より詳細なログ記録と分析が必要です。適切なログ記録と分析の方法は多くの要因によって異なり、本記事の範囲外ですが、NGINXとNGINX Plusには、リクエスト処理の高度なログ記録および監視機能が組み込まれています。

You can use the log_formatディレクティブを使用すると、任意のNGINX変数を含むカスタムログ形式を定義できます。NGINXログに記録された変数値は、後で分析に使用できます。カスタムロギングとランタイム監視の詳細については、「NGINX Plus Admin Guide」を参照してください。

最後に考慮すべき点

実験またはA/Bテストの計画を設計する際は、アプリケーションの各バージョンにリクエストを振り分ける方法によって、結果があらかじめ決まってしまわないようにしてください。完全にランダムな実験を行うには、split_clientsメソッドを使用し、複数の変数を組み合わせてハッシュ化する方法が最適です。たとえば、リクエストのCookieとユーザーIDの組み合わせに基づいて一意の実験用トークンを生成すると、クライアントのブラウザーの種類とバージョンだけをハッシュ化する場合よりも、ランダム性の高いテストパターンを実現できます。多くのユーザーが同じ種類とバージョンのブラウザーを使用している可能性が高く、その場合、全員が同じバージョンのアプリケーションに振り分けられるためです。

また、多くのユーザーが、いわゆる「混在グループ」に属していることも考慮する必要があります。このようなユーザーは、職場と自宅のコンピューターに加え、タブレットやスマートフォンなどのモバイルデバイスといった複数のデバイスからWebアプリケーションにアクセスします。そのため、複数のクライアントIPアドレスを持つことになります。アプリケーションのバージョンを選択する基準としてクライアントIPアドレスを使用すると、両方のバージョンが表示され、実験結果の信頼性が損なわれる可能性があります。

おそらく最も簡単な解決策は、ユーザーにログインを求め、例に示したsticky routeメソッドのようにセッションCookieを追跡することです。これによりユーザーを追跡し、テストに初めてアクセスしたときに表示されたものと常に同じバージョンを提供できます。この方法を使用できない場合は、テスト期間中に変わる可能性が低いグループにユーザーを分けることが適切な場合もあります。たとえば、位置情報を使用して、ロサンゼルスのユーザーには一方のバージョンを、サンフランシスコのユーザーにはもう一方のバージョンを表示します。

まとめ

A/Bテストは、代替サーバー間でトラフィックを異なる割合に分割することで、アプリケーションへの変更を分析・追跡し、アプリケーションのパフォーマンスを監視する効果的な方法です。NGINXとNGINX Plusはどちらも、A/Bテストの堅牢なフレームワークの構築に使用できるディレクティブ、パラメーター、変数を提供しています。また、各リクエストに関する有用な詳細情報をログに記録できます。ぜひA/Bテストをお試しください。

NGINX Plusとsticky routeメソッドをぜひお試しください。今すぐ30日間の無料トライアルを開始するか、ユースケースについてお問い合わせください。


共有
タグ: F5 NGINX, Tech

著者について

Kevin Jones
Kevin Jonesシニアプロダクトマネージャー | F5 NGINX

Kevin Jonesのその他のブログ記事

関連するブログ記事

チュートリアル:NGINXを使用してアプリケーション向けにOpenTelemetryを構成する
NGINX | 2024/01/18

チュートリアル:NGINXを使用してアプリケーション向けにOpenTelemetryを構成する

OpenTelemetryを使用してNGINXサーバーの状態と健全性をより明確に把握することで、問題のトラブルシューティングを行い、Webアプリケーションのパフォーマンスを向上できます。

AkitaのNGINX Plus認定モジュールでAPIエンドポイントの問題を特定・修正
NGINX | 2023/05/31

AkitaのNGINX Plus認定モジュールでAPIエンドポイントの問題を特定・修正

Akita SoftwareのAPIオブザーバビリティ向け新しいNGINX Plus Certified Moduleを活用して、APIエンドポイントの問題をより迅速に特定し、修正できます。

JavaプロジェクトをスキャフォールディングしてKubernetesネイティブアプリを構築する方法
NGINX | 2023/02/09

JavaプロジェクトをスキャフォールディングしてKubernetesネイティブアプリを構築する方法

Red HatのシニアプリンシパルデベロッパーアドボケイトであるDaniel Oh氏が、Javaプロジェクトをゼロから構築するためのひな形を作成し、Kubernetesネイティブアプリケーションとしてビルドする方法を解説します。

F5 NGINXaaS for Azureのご紹介:Microsoft Azureでネイティブに利用できるSaaS型ロードバランシング
NGINX | 2022/05/24

F5 NGINXaaS for Azureのご紹介:Microsoft Azureでネイティブに利用できるSaaS型ロードバランシング

NGINXaaS for Azureは、NGINX PlusをAzureネイティブのSaaSとして利用できるサービスです。NGINXの設定を容易にリフト&シフトできるため、クラウド導入とモダンアプリの展開を加速できます。

SELinuxでNGINXおよびNGINX Plusを使用する
NGINX | 2018/08/17

SELinuxでNGINXおよびNGINX Plusを使用する

Red Hat Enterprise Linux(RHEL)および関連ディストリビューションでSecurity-Enhanced Linux(SELinux)を有効にすると、デフォルト設定によりNGINXとNGINX Plusの一部の操作が制限されます。この記事では、すべての機能を利用できるようにSELinuxの設定を変更する方法について説明します。

NGINX PlusによるAWS Auto Scalingグループの負荷分散
NGINX | 2017/03/06

NGINX PlusによるAWS Auto Scalingグループの負荷分散

新しいnginx-asg-sync統合ソフトウェアにより、NGINX PlusはAWS Auto Scalingグループの負荷を直接分散できるため、クラウドでの負荷分散がさらに柔軟になります