NGINX 및 NGINX Plus를 사용한 A/B 테스트 수행

NGINX | 2016년 7월 25일

애플리케이션의 변경 사항을 테스트할 때는 개발 테스트 환경이 아닌 프로덕션 환경에서만 측정할 수 있는 요소가 있습니다. 예를 들어 UI 변경이 사용자 행동에 미치는 영향과 전반적인 성능에 미치는 영향이 있습니다. 일반적인 테스트 방법으로는 A/B 테스트(다른 이름으로 분할 테스트)가 있습니다. 이 방법에서는 일부 사용자(일반적으로 소수)에게 새 버전의 애플리케이션을 제공하고, 대부분의 사용자는 계속해서 현재 버전을 사용하도록 합니다.

이 블로그 게시물에서는 웹 애플리케이션의 새 버전을 배포할 때 A/B 테스트를 수행하는 것이 중요한 이유와 NGINX 및 NGINX Plus를 사용하여 사용자에게 표시할 애플리케이션 버전을 제어하는 방법을 살펴봅니다. 구성 예제를 통해 NGINX 및 NGINX Plus의 지시문, 매개변수, 변수를 사용하여 정확하고 측정 가능한 A/B 테스트를 수행하는 방법을 설명합니다.

A/B 테스트를 하는 이유는 무엇인가요?

앞서 언급했듯이 A/B 테스트를 사용하면 두 버전 간 애플리케이션 성능 또는 효과의 차이를 측정할 수 있습니다. 개발 팀에서 UI 버튼의 시각적 배치를 변경하거나 전체 장바구니 프로세스를 개편하려고 하지만, 변경 사항이 의도한 비즈니스 효과를 내는지 확인하기 위해 거래 성사율을 비교하고 싶을 수 있습니다. A/B 테스트를 사용하면 트래픽의 일정 비율을 새 버전으로 보내고 나머지는 이전 버전으로 보내 두 애플리케이션 버전의 효과를 측정할 수 있습니다.

또는 사용자 행동에 미치는 영향보다는 성능에 미치는 영향이 더 중요할 수도 있습니다. 웹 애플리케이션에 대규모 변경 사항을 배포할 계획이지만, 품질 보증 환경에서의 테스트만으로는 프로덕션 환경의 성능에 미칠 수 있는 영향을 제대로 파악하기 어렵다고 가정해 보겠습니다. 이 경우 A/B 배포를 통해 새 버전을 사전에 정한 소수의 방문자에게 노출하여 변경 사항이 성능에 미치는 영향을 측정한 다음, 그 비율을 점진적으로 늘려 최종적으로 변경된 애플리케이션을 모든 사용자에게 배포할 수 있습니다.

A/B 테스트에 NGINX 및 NGINX Plus 사용

NGINX와 NGINX Plus는 웹 애플리케이션 트래픽이 전송되는 위치를 제어하는 두 가지 방법을 제공합니다. 첫 번째 방법은 두 제품 모두에서 사용할 수 있지만, 두 번째 방법은 NGINX Plus에서만 사용할 수 있습니다.

두 방법 모두 클라이언트의 특성(예: IP 주소)이나 요청 URI의 특성(예: 명명된 인수)을 나타내는 하나 이상의 NGINX 변수 값을 기준으로 요청의 대상을 선택하지만, 두 방법의 차이로 인해 각각 서로 다른 A/B 테스트 사용 사례에 적합합니다:

  • The split_clients 메서드는 요청에서 추출한 변수 값의 해시를 기반으로 요청의 대상을 선택합니다. 가능한 모든 해시 값의 집합은 애플리케이션 버전별로 나뉘며, 각 애플리케이션에 서로 다른 비율을 할당할 수 있습니다. 결과적으로 대상은 무작위로 선택됩니다.
  • The sticky route 메서드를 사용하면 각 요청의 대상을 훨씬 더 세밀하게 제어할 수 있습니다. 애플리케이션은 해시가 아닌 변수 값 자체를 기준으로 선택되므로, 특정 변수 값을 가진 요청을 어느 애플리케이션이 받을지 명시적으로 설정할 수 있습니다. 또한 정규식을 사용하여 변수 값의 일부만을 기준으로 결정할 수 있으며, 결정 기준으로 한 변수를 다른 변수보다 우선적으로 선택할 수도 있습니다.

Using the split_clients 메서드 사용하기

이 방법에서는 split_clients 구성 블록이 각 요청에 대해 변수를 설정하며, 이 변수는 proxy_pass 지시문이 요청을 전송할 업스트림 그룹을 결정합니다. 아래의 구성 예시에서는 $appversion 변수의 값에 따라 proxy_pass 지시문이 요청을 전송할 위치가 결정됩니다. split_clients 블록은 해시 함수를 사용하여 변수 값을 두 업스트림 그룹 이름 중 하나인 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 지시문의 첫 번째 매개변수는 각 요청 시 MurmurHash2 함수로 해싱되는 문자열(이 예에서는 "${arg_token}")입니다. URI 인수는 NGINX에서 $arg_name이라는 변수로 사용할 수 있습니다. 이 예에서 $arg_token 변수는 token이라는 URI 인수를 캡처합니다. 해싱할 문자열로 어떤 NGINX 변수든 사용할 수 있으며, 여러 변수를 조합한 문자열도 사용할 수 있습니다. 예를 들어 클라이언트의 IP 주소($remote_addr 변수), 포트($remote_port) 또는 이 둘의 조합을 해싱할 수 있습니다. NGINX가 요청을 처리하기 전에 생성되는 변수를 사용하는 것이 좋습니다. 클라이언트의 최초 요청에 관한 정보가 포함된 변수가 가장 적합합니다. 앞서 언급한 클라이언트의 IP 주소/포트, 요청 URI, HTTP 요청 헤더 등이 그 예입니다.

The second parameter to the split_clients 지시문의 두 번째 매개변수(이 예에서는 $appversion)는 첫 번째 매개변수의 해시값에 따라 동적으로 설정되는 변수입니다. 중괄호 안의 문은 해시 테이블을 여러 “버킷”으로 나누며, 각 버킷에는 가능한 해시값 중 일정 비율이 포함됩니다. 버킷은 원하는 만큼 만들 수 있으며, 모두 같은 크기일 필요는 없습니다. 해시값의 개수가 지정된 비율로 균등하게 나누어지지 않을 수 있으므로, 마지막 버킷의 비율은 항상 특정 숫자 대신 별표(*)로 표시됩니다.

이 예에서는 해시의 95%를 version_1a 업스트림 그룹과 연결된 버킷에 넣고, 나머지는 version_1b와 연결된 두 번째 버킷에 넣습니다. 가능한 해시 값의 범위는 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%와 나머지)로 요청을 두 업스트림 그룹에 분배하는 테스트 구성을 만들었습니다. 각 그룹의 가상 서버가 요청을 처리한 그룹이 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 변수 값의 전체 또는 일부를 기반으로 클라이언트 라우팅을 결정하여 정적 경로를 정의해야 할 수 있습니다. 이 작업은 sticky route 지시어를 사용하여 수행할 수 있으며, 이 지시어는 NGINX Plus에서만 사용할 수 있습니다. 이 지시어는 하나 이상의 매개변수 목록을 받아 목록에서 비어 있지 않은 첫 번째 매개변수의 값으로 경로를 설정합니다. 이 기능을 사용하면 요청에 포함된 변수 중 대상 선택을 제어할 변수의 우선순위를 지정할 수 있으므로, 하나의 구성에서 둘 이상의 트래픽 분할 방식을 지원할 수 있습니다.

이 방법을 사용하는 데는 두 가지 접근 방식이 있습니다.

  • Using the 클라이언트 측 접근 방식을 사용하면 클라이언트의 IP 주소나 클라이언트의 User-Agent와 같은 브라우저별 HTTP 요청 헤더처럼, 처음에 클라이언트에서 직접 전송된 값을 포함하는 NGINX 변수를 기반으로 경로를 선택할 수 있습니다.
  • With the 서버 측 또는 애플리케이션 측 접근 방식을 사용하면 애플리케이션이 최초 사용자를 어느 테스트 그룹에 할당할지 결정한 후, 선택한 그룹을 나타내는 경로 표시기가 포함된 쿠키나 리디렉션 URI를 사용자에게 전송합니다. 다음번에 클라이언트가 요청을 보낼 때 해당 쿠키를 제시하거나 리디렉션 URI를 사용하면, sticky route 지시문이 경로 표시기를 추출하여 요청을 적절한 서버로 전달합니다.

이 예제에서는 애플리케이션 측 접근 방식을 사용합니다. sticky route 지시문은 upstream 그룹에서 서버가 제공한 쿠키에 지정된 값($route_from_cookie에 캡처됨)을 우선적으로 라우트로 설정합니다. 클라이언트에 쿠키가 없으면 요청 URI의 인수 값($route_from_uri)이 라우트로 설정됩니다. 그런 다음 라우트 값에 따라 업스트림 그룹의 어느 server가 요청을 받을지가 결정됩니다. 라우트가 a이면 첫 번째 서버가, b이면 두 번째 서버가 요청을 받습니다(두 서버는 애플리케이션의 두 버전에 각각 해당합니다).

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

하지만 실제 쿠키나 URI에서는 a 또는 b가 훨씬 더 긴 문자열 안에 포함되어 있습니다. 해당 문자만 추출하기 위해 쿠키와 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라는 이름의 쿠키 값을 나타냅니다. 두 번째 줄의 정규식은 Perl 호환 정규식(PCRE) 구문을 사용하여 값의 일부(이 경우 마침표 뒤의 문자열(w+))를 route라는 명명된 캡처 그룹으로 추출하고 같은 이름의 내부 변수에 할당합니다. 이 값은 첫 번째 줄의 $route_from_cookie 변수에도 할당되므로 sticky route 지시문에 전달할 수 있습니다.

예를 들어 첫 번째 map 블록은 이 쿠키에서 값 “a”를 추출하여 $route_from_cookie에 할당합니다:

ROUTE=iDmDe26BdBDS28FuVJlWc1FH4b13x4fn.a

두 번째 map 블록에서 $arg_route 변수는 요청 URI의 route라는 인수를 나타냅니다. 쿠키와 마찬가지로 두 번째 줄의 정규식은 URI의 일부를 추출합니다. 이 경우에는 route 인수에서 마침표 뒤에 오는 문자열(w+)입니다. 이 값은 명명된 캡처 그룹으로 읽혀 내부 변수에 할당되며, $route_from_uri 변수에도 할당됩니다.

예를 들어 두 번째 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라는 쿠키를 전송하거나 URI에 route 인수를 포함했습니다. 쿠키 또는 인수의 값은 urandom 파일에 cat 명령을 실행하여 생성한 임의의 문자열에 .a 또는 .b를 덧붙인 것입니다.

먼저, 끝이 .a인 쿠키로 테스트합니다:

# 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인 쿠키로 테스트합니다.

# 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

마지막으로 쿠키 없이 테스트하고, 대신 route 인수로 끝이 .a인 요청 URI를 사용합니다. 출력 결과를 통해 쿠키가 없을 때(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 관리자 가이드를 참조하세요.

마지막으로 고려할 몇 가지 사항

실험 또는 A/B 테스트 계획을 설계할 때는 애플리케이션 버전 간에 요청을 분배하는 방식이 결과를 미리 결정하지 않도록 해야 합니다. 완전히 무작위로 실험하려면 split_clients 메서드를 사용하고 여러 변수를 조합해 해시하는 것이 가장 효과적입니다. 예를 들어 요청의 쿠키와 사용자 ID를 조합하여 고유한 실험용 토큰을 생성하면 클라이언트의 브라우저 유형과 버전만 해시하는 것보다 더 무작위적인 테스트 패턴을 얻을 수 있습니다. 많은 사용자가 동일한 브라우저 유형과 버전을 사용할 가능성이 높아 모두 같은 애플리케이션 버전으로 연결될 수 있기 때문입니다.

또한 많은 사용자가 이른바 혼합 그룹에 속한다는 점도 고려해야 합니다. 이들은 여러 기기(예: 직장과 가정의 컴퓨터, 태블릿이나 스마트폰 같은 모바일 기기)에서 웹 애플리케이션에 접속합니다. 이러한 사용자는 여러 클라이언트 IP 주소를 사용하므로, 클라이언트 IP 주소를 기준으로 애플리케이션 버전을 선택하면 애플리케이션의 두 버전을 모두 보게 되어 실험 결과가 왜곡될 수 있습니다.

아마도 가장 쉬운 해결책은 사용자가 로그인하도록 하여, 앞서 살펴본 sticky route 메서드의 예처럼 세션 쿠키를 추적할 수 있게 하는 것입니다. 이렇게 하면 사용자를 추적하여 테스트에 처음 접속했을 때 본 것과 항상 동일한 버전으로 보낼 수 있습니다. 이 방법을 사용할 수 없다면 테스트 기간 동안 바뀔 가능성이 낮은 기준으로 사용자를 그룹화하는 것이 적절할 때도 있습니다. 예를 들어 위치 정보를 사용하여 로스앤젤레스 사용자에게는 한 버전을, 샌프란시스코 사용자에게는 다른 버전을 보여줄 수 있습니다.

결론

A/B 테스트는 대체 서버 간에 서로 다른 비율로 트래픽을 분산하여 애플리케이션의 변경 사항을 분석 및 추적하고 애플리케이션 성능을 모니터링하는 효과적인 방법입니다. NGINX와 NGINX Plus는 모두 견고한 A/B 테스트 프레임워크를 구축하는 데 사용할 수 있는 지시어, 매개변수 및 변수를 제공합니다. 또한 각 요청에 대한 유용한 세부 정보를 로그에 기록할 수 있습니다. 즐겁게 테스트해 보세요!

NGINX Plus와 sticky route 방식을 직접 사용해 보세요. 지금 30일 무료 평가판을 시작하거나 사용 사례에 대해 논의하려면 문의하세요.


공유
태그: F5 NGINX, Tech

작성자 소개

관련 블로그 게시물

시청하기: KubeCon North America 2023에서의 NGINX Gateway Fabric
NGINX | 2023. 11. 30.

시청하기: KubeCon North America 2023에서의 NGINX Gateway Fabric

이 영상에서는 KubeCon의 NGINX 부스를 찾아가, 저희가 공개한 Kubernetes Gateway API 구현을 살펴봅니다.

F5 NGINX로 대규모 WAF 플릿을 확인하고 관리하는 2가지 방법
NGINX | 2023. 03. 23.

F5 NGINX로 대규모 WAF 플릿을 확인하고 관리하는 2가지 방법

앱 보안을 강화하는 두 가지 방법을 소개합니다. NGINX Management Suite를 통해 WAF 운영에 대한 가시성을 높이고, NGINX App Protect WAF를 활용해 코드형 보안을 구현하는 것입니다.

Ingress 컨트롤러 선택 가이드, 4부: NGINX Ingress Controller 옵션
NGINX | 2021. 09. 27.

Ingress 컨트롤러 선택 가이드, 4부: NGINX Ingress Controller 옵션

개발 주체, 개발 철학, 프로덕션 환경 준비도, 보안 및 지원을 기준으로 어떤 NGINX Ingress 컨트롤러가 가장 적합한지 알아보세요.

진화하는 공격 환경에 NGINX App Protect Denial of Service가 적응하는 방법
NGINX | 2021. 07. 06.

진화하는 공격 환경에 NGINX App Protect Denial of Service가 적응하는 방법

NGINX App Protect Denial of Service(DoS)가 계층 7 DoS 공격을 방어하기 위해 완화 방법을 실시간으로 조정하는 방식을 설명합니다. 정상적인 사용자 행동과 서버 상태에 대한 모델을 생성하여 공격을 나타내는 이상 징후를 감지하고 맞춤형 완화 시그니처를 적용할 수 있습니다.

NGINX Instance Manager를 활용한 대규모 구성 업데이트 및 인증서 관리
NGINX | 2021. 06. 30.

NGINX Instance Manager를 활용한 대규모 구성 업데이트 및 인증서 관리

NGINX Instance Manager 버전 1.0에는 두 가지 새로운 기능이 도입되었습니다. NGINX 인스턴스에 태그와 사용자 역할을 지정하면 전체 그룹에 구성을 한 번에 적용할 수 있습니다. 인증서 검사는 만료 예정인 인증서를 감지하여 서비스 중단을 방지할 수 있도록 인증서를 교체할 수 있게 해줍니다.

Netflix가 자사 CDN의 핵심으로 NGINX를 선택한 이유
NGINX | 2015. 03. 25.

Netflix가 자사 CDN의 핵심으로 NGINX를 선택한 이유

Netflix가 글로벌 콘텐츠 전송 네트워크인 Open Connect의 스트리밍 미디어 기술로 NGINX를 선택한 이유를 NGINX 개발자 Gleb Smirnoff에게서 알아보세요.