A/B 테스트 NGINX 및 NGINX Plus로 수행하기
이 포스트에서는 웹 애플리케이션의 새 버전을 배포할 때 A/B 테스트 를 수행하는 것이 왜 중요한지, 그리고 NGINX와 NGINX Plus를 사용하여 사용자에게 표시되는 애플리케이션 버전을 제어하는 방법을 살펴봅니다. 구성 예제에서는 정확하고 측정 가능한 A/B 테스트 를 수행하기 위해 NGINX 및 NGINX Plus 지시문, 매개변수 및 변수를 사용하는 방법을 설명합니다.
애플리케이션의 변경 사항을 테스트할 때 개발 Test Bed가 아닌 Production 환경에서만 측정할 수 있는 몇 가지 요소가 있습니다. 예를 들어 UI 변경이 사용자 행동에 미치는 영향과 전반적인 성능에 미치는 영향이 있습니다. 일반적인 테스트 방법은 분할 테스트라고도 하는 A/B 테스트 로, 대부분의 사용자가 현재 버전을 계속 사용하는 동안 일부 사용자만 애플리케이션의 새 버전으로 이동시키는 것입니다.
목차
1. A/B 테스트 를 하는 이유는 무엇인가요?
2. A/B 테스트 에 NGINX 및 NGINX Plus 사용
2-1. split_clients 메서드 사용
2-1-1. split_clients 구성 테스트
2-2. sticky route 메서드 사용
2-2-1. sticky route 구성 테스트
3. 결과 기록 및 분석
4. 마지막으로 고려해야 할 몇 가지 사항
5. 결론
1. A/B 테스트 를 하는 이유는 무엇인가요?
앞서 언급했듯이 A/B 테스트 를 통해 두 버전 간의 애플리케이션 성능 또는 효율성의 차이를 측정할 수 있습니다. 개발팀에서 UI의 버튼 시각적 배열을 변경하거나 전체 장바구니 프로세스를 점검하고 싶지만 거래 성사율을 비교하여 변경 사항이 원하는 비즈니스에 영향을 미치는지 확인하고자 할 수도 있습니다. A/B 테스트 를 사용하면 트래픽의 일정 비율을 새 버전으로 보내고 나머지는 이전 버전으로 보내 두 버전의 애플리케이션의 효과를 측정할 수 있습니다.
또는 사용자 행동에 미치는 영향보다는 성능에 미치는 영향에 더 관심이 있을 수도 있습니다. 웹 애플리케이션에 대규모 변경 사항을 배포할 계획인데 품질 보증 환경 내 테스트만으로는 Production 환경의 성능에 미칠 수 있는 영향을 제대로 파악할 수 없다고 생각한다고 가정해 보겠습니다. 이 경우 A/B 배포를 사용하면 변경 사항의 성능 영향을 측정하기 위해 정의된 소수의 방문자에게 새 버전을 노출하고, 최종적으로 모든 사용자에게 변경된 애플리케이션을 배포할 때까지 점진적으로 비율을 늘릴 수 있습니다.
2. A/B 테스트 에 NGINX 및 NGINX Plus 사용
NGINX와 NGINX Plus는 웹 애플리케이션 트래픽이 전송되는 위치를 제어하기 위한 몇 가지 방법을 제공합니다. 첫 번째 방법은 두 제품 모두에서 사용할 수 있는 반면, 두 번째 방법은 NGINX Plus에서만 사용할 수 있습니다.
두 방법 모두 클라이언트(예: IP 주소) 또는 요청 URI(예: 지정된 인수)의 특성을 캡처하는 하나 이상의 NGINX 변수 값을 기반으로 요청의 대상을 선택하지만, 두 방법 간의 차이점으로 인해 서로 다른 A/B 테스트 사용 사례에 적합합니다.
- split_clients 메서드는 요청에서 추출한 변수 값의 해시를 기반으로 요청의 대상을 선택합니다. 가능한 모든 해시 값 집합은 애플리케이션 버전 간에 분할되며 집합의 다른 비율을 각 애플리케이션에 할당할 수 있습니다. 목적지 선택은 무작위로 이루어집니다.
- sticky route 메서드는 각 요청의 대상에 대해 훨씬 더 많은 제어를 제공합니다. 애플리케이션 선택은 해시가 아닌 변수 값 자체를 기반으로 하므로 특정 변수 값이 있는 요청을 수신하는 애플리케이션을 명시적으로 설정할 수 있습니다. 또한 정규식을 사용하여 변수 값의 일부만을 기준으로 결정을 내릴 수 있으며 결정의 기준으로 한 변수를 다른 변수보다 우선적으로 선택할 수 있습니다.
2-1. split_clients 메서드 사용
이 방법에서 split_clients 구성 블록은 각 요청에 대해 proxy_pass 지시문을 통해 요청을 전송할 upstream 그룹을 결정하는 변수를 설정합니다. 아래 샘플 구성에서는 $appversion 변수의 값에 따라 proxy_pass 지시문이 요청을 전송하는 위치가 결정됩니다. split_clients 블록은 해시 함수를 사용하여 변수 값을 version_1a
또는 version_1b
의 두 upstream 그룹 이름 중 하나로 동적으로 설정합니다.
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;
}
}
}
split_clients 지시문의 첫 번째 매개 변수는 각 요청 중에 MurmurHash2 함수를 사용하여 해시 되는 문자열(예제에서는 “${arg_token}
“)입니다. URI 인수는 $arg_name
이라는 변수로 사용할 수 있으며, 이 예제에서는 $arg_token
변수가 token이라는 URI 인수를 캡처합니다. 해시할 문자열로 모든 NGINX 변수 또는 변수 문자열을 사용할 수 있습니다. 예를 들어 클라이언트의 IP 주소($remote_addr
변수), 포트($remote_port
) 또는 이 둘의 조합을 해시할 수 있습니다. NGINX에서 요청이 처리되기 전에 생성된 변수를 사용하려고 합니다. 클라이언트의 초기 요청에 대한 정보가 포함된 변수가 가장 이상적이며, 예를 들어 앞서 언급한 클라이언트의 IP 주소/포트, 요청 URI 또는 HTTP 요청 헤더 등이 있습니다.
split_clients
지시문의 두 번째 매개변수(이 예제에서는 $appversion
)는 첫 번째 매개변수의 해시에 따라 동적으로 설정되는 변수입니다. 중괄호 안의 문은 해시 테이블을 “Bucket”으로 나누며, 각 Bucket은 가능한 해시의 백분율을 포함합니다. Bucket은 원하는 수만큼 만들 수 있으며 모두 같은 크기일 필요는 없습니다. 마지막 Bucket의 백분율은 항상 특정 숫자가 아닌 별표(*)로 표시되는데, 이는 해시 수를 지정된 백분율로 균등하게 나눌 수 없을 수 있기 때문입니다.
이 예제에서는 해시의 95%를 version_1a
Upstream 그룹과 연결된 Bucket에 넣고, 나머지는 version_1b
와 연결된 두 번째 Bucket에 넣었습니다. 가능한 해시 값의 범위는 0에서 4,294,967,295까지이므로 첫 번째 Bucket에는 0에서 약 4,080,218,930(전체의 95%)까지의 값이 포함됩니다. $appversion
변수는 $arg_token
변수의 해시를 포함하는 Bucket과 연결된 Upstream으로 설정됩니다. 구체적인 예로, 해시 값 100,000,000이 첫 번째 Bucket에 해당하므로 $appversion
은 version_1a
로 동적으로 설정됩니다.
2-1-1. split_clients 구성 테스트
split_clients 구성 블록이 의도한 대로 작동하는지 확인하기 위해 위와 동일한 비율(95%와 나머지)로 요청을 두 개의 Upstream 그룹으로 나누는 테스트 구성을 만들었습니다. 요청을 처리한 그룹(version_1a
또는 version_1b
)을 나타내는 문자열을 반환하도록 그룹의 가상 서버를 구성했습니다(테스트 구성은 여기에서 확인할 수 있습니다). 그런 다음 curl을 사용하여 urandom 파일에서 cat 명령을 실행하여 URI 인수 Token
의 값을 임의로 설정한 20개의 요청을 생성했습니다. 이것은 순전히 데모 테스트 목적으로 무작위 설정을 한 것입니다. 의도한 대로 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); done
Token: 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.
2-2. sticky route 메서드 사용
경우에 따라 NGINX 변수 값의 전부 또는 일부를 기반으로 클라이언트 라우팅을 결정하여 static route를 정의하고 싶을 수 있습니다. 이 경우 NGINX Plus에서만 사용할 수 있는 sticky route
지시문을 사용하여 이 작업을 수행할 수 있습니다. 이 지시문은 하나 이상의 매개변수 목록을 가져와서 목록에서 비어 있지 않은 첫 번째 매개변수 값으로 경로를 설정합니다. 이 기능을 사용하면 요청에서 어떤 변수가 목적지 선택을 제어하는지 우선순위를 정할 수 있으므로 단일 구성에서 둘 이상의 트래픽 분할 방법을 수용할 수 있습니다.
이 방법을 사용하는 메서드는 두 가지 접근 방식이 있습니다.
- 클라이언트 측 접근 방식을 사용하면 클라이언트의 IP 주소 또는 클라이언트의 사용자 에이전트와 같은 브라우저별 HTTP 요청 헤더와 같이 클라이언트에서 처음에 직접 전송되는 값을 포함하는 NGINX 변수를 기반으로 경로를 선택할 수 있습니다.
- 서버 측 또는 애플리케이션 측 접근 방식을 사용하면 애플리케이션이 첫 번째 사용자에게 할당할 테스트 그룹을 결정하고 선택한 그룹을 나타내는 경로 표시기가 포함된 Cookie 또는 리다이렉션 URI를 보냅니다. 다음에 클라이언트가 요청을 보낼 때 Cookie를 제공하거나 리다이렉션 URI를 사용하면,
sticky route
지시문은 경로 지시문을 추출하여 요청을 적절한 서버로 전달합니다.
이 예제에서는 애플리케이션 측 접근 방식을 사용하고 있습니다. Upstream 그룹의 sticky route 지시문은 우선적으로 서버에서 제공한 Cookie에 지정된 값으로 경로를 설정합니다($route_from_cookie에 캡처됨). 클라이언트에 Cookie가 없는 경우 경로는 요청 URI에 대한 인수의 값으로 설정됩니다($route_from_uri). 그런 다음 경로 값에 따라 Upstream 그룹에서 요청을 수신할 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;
}
하지만 실제 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의 값을 나타냅니다. 두 번째 줄의 정규식은 PCRE(Perl Compatible Regular Expression) 구문을 사용하여 값의 일부(이 경우 마침표 뒤의 문자열(w+))를 지정된 캡처 그룹 경로로 추출하여 해당 이름을 가진 내부 변수에 할당합니다. 또한 이 값은 첫 번째 줄의 $route_from_cookie
변수에 할당되어 Sticky Route
지시문에 전달할 수 있습니다.
예를 들어, 첫 번째 Map 블록은 이 Cookie에서 값 “a”를 추출하여 $route_from_cookie
에 할당합니다.
ROUTE=iDmDe26BdBDS28FuVJlWc1FH4b13x4fn.a
두 번째 Map 블록에서 $arg_route
변수는 요청 URI에서 route
라는 인수를 나타냅니다. Cookie와 마찬가지로 두 번째 줄의 정규식은 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;
}
}
}
2-2-1. sticky route 구성 테스트
split_clients
메서드의 경우 테스트 구성을 만들었으며 여기에서 액세스할 수 있습니다. curl을 사용하여 ROUTE
라는 이름의 Cookie를 보내거나 URI에 route
인수를 포함시켰습니다. Cookie 또는 인수의 값은 임의의 파일에서 cat 명령을 실행하여 생성된 임의의 문자열이며 .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.1
Cookie 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.1
Cookie Value: JhdJZrScTnPBLhqmzK3podNRcJAIc8ST.b
Request URI: /
Results: Site B - Running on port 8099
마지막으로 Cookie 없이 대신 .a
로 끝나는 요청 URI의 route
인수를 사용하여 테스트합니다. 출력은 Cookie가 없는 경우( Cookie Value
필드가 비어 있음) NGINX Plus가 URI에서 파생된 경로 값을 사용함을 확인합니다.
# curl 127.0.0.1?route=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1).a
Cookie Value:
Request URI: /?route=yNp8pHskvukXK6XqbWefhVUcOBjbJv4v.a
Results: Site A - Running on port 8089
3. 결과 로깅 및 분석
여기서 설명하는 종류의 테스트는 구성이 의도한 대로 요청을 배포하는지 확인하는 데 충분하지만 실제 A/B 테스트 결과를 해석하려면 요청이 처리되는 방식에 대한 훨씬 더 자세한 로깅 및 분석이 필요합니다. 로깅 및 분석을 수행하는 올바른 방법은 여러 요인에 따라 다르며 이 글의 범위를 벗어나는 것이지만, NGINX와 NGINX Plus는 요청 처리에 대한 정교한 기본 제공 로깅 및 모니터링 기능을 제공합니다.
log_format 지시문을 사용하여 NGINX 변수를 포함하는 사용자 정의 로그 형식을 정의할 수 있습니다. 그러면 NGINX 로그에 기록된 변수 값을 나중에 분석에 사용할 수 있습니다.
4. 마지막으로 고려해야 할 몇 가지 사항
A/B 테스트 를 설계할 때 애플리케이션 버전 간에 요청을 배포하는 방식이 결과를 미리 결정하지 않도록 하세요. 완전히 무작위 실험을 원한다면 split_clients
메서드를 사용하여 여러 변수의 조합을 해싱 하는 것이 가장 좋은 결과를 제공합니다. 예를 들어, 요청에서 쿠키와 사용자 ID의 조합을 기반으로 고유한 테스트 Token을 생성하면 많은 사용자가 동일한 유형의 브라우저와 버전을 가지고 있어 모두 동일한 버전의 애플리케이션으로 연결될 가능성이 높기 때문에 클라이언트의 브라우저 유형과 버전만 해싱 하는 것보다 더 무작위적인 테스트 패턴을 적용할 수 있습니다.
또한 많은 사용자가 혼합 그룹에 속해 있다는 점도 고려해야 합니다. 이들은 업무용 및 가정용 컴퓨터와 태블릿 또는 스마트폰과 같은 모바일 장치 등 여러 장치에서 웹 애플리케이션에 액세스합니다. 이러한 사용자는 여러 개의 클라이언트 IP 주소를 가지고 있으므로 클라이언트 IP 주소를 애플리케이션 버전 선택의 기준으로 사용하면 두 가지 버전의 애플리케이션이 모두 표시되어 테스트 결과가 왜곡될 수 있습니다.
가장 쉬운 해결책은 sticky route
메서드의 예에서와 같이 세션 Cookie를 추적할 수 있도록 사용자에게 로그인을 요구하는 것입니다. 이렇게 하면 사용자를 추적하여 사용자가 처음 테스트에 액세스했을 때 본 것과 동일한 버전으로 항상 보낼 수 있습니다. 이렇게 할 수 없는 경우에는 지리적 위치를 사용하여 로스앤젤레스에 있는 사용자에게는 한 버전을, 샌프란시스코에 있는 사용자에게는 다른 버전을 표시하는 등 테스트가 진행되는 동안 변경될 가능성이 없는 그룹에 사용자를 배치하는 것이 좋습니다.
5. A/B 테스트 결론
A/B 테스트 는 애플리케이션의 변경 사항을 분석 및 추적하고 대체 서버 간에 서로 다른 양의 트래픽을 분할하여 애플리케이션 성능을 모니터링하는 효과적인 방법입니다. NGINX와 NGINX Plus는 모두 A/B 테스트 를 위한 견고한 프레임워크를 구축하는 데 사용할 수 있는 지시문, 매개 변수 및 변수를 제공합니다. 또한 각 요청에 대한 중요한 세부 정보를 기록할 수 있습니다. 테스트를 즐겨보세요!
NGINX Plus 및 sticky route를 직접 사용해 보거나 테스트해 보려면 지금 30일 무료 평가판을 신청하거나 사용 사례에 대해 최신 소식을 빠르게 전달받고 싶으시면 아래 뉴스레터를 구독하세요.