Nginx ile A/B Testing Yapılandırması
A/B testing deyince aklınıza sadece frontend framework’leri ya da üçüncü parti servisler geliyor olabilir. Ama işin aslı şu ki, Nginx’in sunduğu araçlarla oldukça güçlü bir A/B testing altyapısı kurabilirsiniz. Hem maliyetsiz hem de altyapınızın tam kontrolünde. Bu yazıda, gerçek dünya senaryolarına dayanan Nginx A/B testing yapılandırmalarını adım adım inceleyeceğiz.
A/B Testing Nedir ve Nginx Neden Bu İş İçin Uygun?
A/B testing, kullanıcılarınızın bir kısmını farklı bir versiyona yönlendirerek hangi versiyonun daha iyi performans gösterdiğini ölçme sürecidir. Klasik yaklaşım şudur: kullanıcıların %80’i mevcut versiyona (A), %20’si yeni versiyona (B) gider.
Nginx bu iş için neden mantıklı bir seçim?
- Altyapı düzeyinde kontrol: Uygulama koduna dokunmadan yönlendirme yapabilirsiniz
- Düşük gecikme: Karar verme mantığı, isteğin işlenmesinden önce çalışır
- Cookie tabanlı tutarlılık: Kullanıcı her seferinde aynı versiyona gider
- Kolay geri alma: Bir konfigürasyon değişikliği yeterlidir
- Upstream entegrasyonu: Birden fazla backend ile sorunsuz çalışır
Bir e-ticaret şirketinde çalışırken yaşadığım şu senaryoyu düşünün: Yeni ödeme sayfası versiyonunu canlıya almak istiyoruz ama risk almak istemiyoruz. Nginx ile önce %5 trafiği yeni versiyona açtık, metrikler iyiydi, %20’ye çıkardık, sonra %50, ardından tam geçiş. Bütün bu süreçte uygulama sunucularına tek satır kod değişikliği gitmedi.
Temel Yapılandırma: Split_Clients Modülü
Nginx’in yerleşik split_clients modülü, A/B testing için tasarlanmış gibidir. Hash tabanlı çalışır, yani aynı kullanıcı (aynı IP veya değer) her seferinde aynı gruba düşer.
# /etc/nginx/nginx.conf veya ayrı bir conf dosyası
http {
# IP bazlı split - kullanıcı her seferinde aynı gruba gider
split_clients "${remote_addr}" $variant {
20% "b";
* "a";
}
upstream backend_a {
server 10.0.0.10:8080;
server 10.0.0.11:8080;
}
upstream backend_b {
server 10.0.0.20:8080;
server 10.0.0.21:8080;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend_$variant;
proxy_set_header X-Variant $variant;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
Bu yapılandırmada split_clients, remote_addr değerini hash’leyerek %20 oranında “b”, geri kalan %80 için “a” değeri atar. $variant değişkeni bu noktadan itibaren kullanılabilir hale gelir.
Dikkat: IP bazlı bölme, NAT arkasında birden fazla kullanıcı olan ofis ağlarında sapmalara yol açabilir. Bunun için daha sonra cookie tabanlı yaklaşıma geçeceğiz.
Cookie Tabanlı Tutarlı Yönlendirme
Gerçek dünya uygulamalarında en sık kullandığım yaklaşım cookie tabanlı olanıdır. Kullanıcı bir kez gruba atandıktan sonra, cookie silinmedikçe hep aynı grupta kalır.
# /etc/nginx/conf.d/ab_testing.conf
http {
# Cookie yoksa IP'ye göre group belirle, cookie varsa cookie kullan
map $cookie_ab_variant $ab_group {
"b" "b";
"a" "a";
default "";
}
split_clients "${remote_addr}${http_user_agent}" $split_result {
30% "b";
* "a";
}
upstream app_a {
server 192.168.1.10:3000;
keepalive 32;
}
upstream app_b {
server 192.168.1.20:3000;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name api.example.com;
# SSL ayarları burada...
location / {
# Cookie yoksa split_clients sonucunu kullan
set $effective_variant $ab_group;
if ($effective_variant = "") {
set $effective_variant $split_result;
}
# Cookie'yi set et (30 gün geçerli)
add_header Set-Cookie "ab_variant=$effective_variant; Path=/; Max-Age=2592000; SameSite=Lax";
proxy_pass http://app_$effective_variant;
proxy_set_header X-AB-Variant $effective_variant;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
Bu yapıda map direktifi cookie değerini okur. Cookie yoksa boş string döner, ardından split_clients devreye girer ve kullanıcıya bir variant atanır.
Gelişmiş Senaryo: API Gateway ile A/B Testing
Bir API gateway arkasında microservice mimarisi kuruyorsanız, A/B testing çok daha kritik bir hal alır. Diyelim ki yeni bir öneri algoritması servisini test ediyorsunuz.
# /etc/nginx/conf.d/recommendation_ab.conf
upstream recommendation_v1 {
server rec-v1-1.internal:8080 weight=1;
server rec-v1-2.internal:8080 weight=1;
keepalive 16;
}
upstream recommendation_v2 {
server rec-v2-1.internal:8080 weight=1;
server rec-v2-2.internal:8080 weight=1;
keepalive 16;
}
# Belirli kullanıcı segmentlerini hedefle
map $http_x_user_id $user_segment {
~*^[0-9]*[02468]$ "even"; # Çift user ID'ler
default "odd";
}
split_clients "$http_x_user_id" $rec_variant {
15% "v2";
* "v1";
}
server {
listen 80;
server_name api-gateway.internal;
location /api/v1/recommendations {
# Özel header ile override imkanı (QA ekibi için)
set $final_variant $rec_variant;
if ($http_x_force_variant != "") {
set $final_variant $http_x_force_variant;
}
proxy_pass http://recommendation_$final_variant;
proxy_set_header X-Variant $final_variant;
proxy_set_header X-Original-URI $request_uri;
# Logging için özel format
access_log /var/log/nginx/ab_recommendations.log ab_format;
}
location /api/ {
proxy_pass http://main_backend;
}
}
Burada ilginç bir özellik var: $http_x_force_variant header’ı. QA ekibiniz test sırasında bu header’ı göndererek istediği versiyona bağlanabilir. Production ortamında gerçek kullanıcılar bu header’ı göndermez, ama dahili test araçlarınız gönderir.
Nginx Lua ile Dinamik A/B Testing
Eğer OpenResty ya da Nginx Lua modülü kullanıyorsanız, çok daha sofistike kararlar verebilirsiniz. Redis ile entegrasyon yaparak kullanıcı bazlı dinamik yönetim mümkün olur.
# /etc/nginx/conf.d/lua_ab.conf
# OpenResty gerektirir: apt install openresty
http {
lua_shared_dict ab_config 1m;
lua_shared_dict ab_cache 10m;
init_by_lua_block {
-- Başlangıçta default oranları ayarla
local ab = ngx.shared.ab_config
ab:set("variant_b_ratio", 20) -- %20 oranında B versiyonu
ab:set("feature_new_checkout", true)
}
server {
listen 80;
location /api/checkout {
rewrite_by_lua_block {
local ab_config = ngx.shared.ab_config
local ratio = ab_config:get("variant_b_ratio") or 0
-- Kullanıcı cookie'sini kontrol et
local cookie_variant = ngx.var.cookie_ab_variant
if cookie_variant then
ngx.var.ab_upstream = "checkout_" .. cookie_variant
return
end
-- Hash ile variant belirle
local user_ip = ngx.var.remote_addr
local hash = ngx.crc32_short(user_ip .. ngx.today())
local position = hash % 100
local variant = "a"
if position < ratio then
variant = "b"
end
-- Cookie'yi set et
ngx.header["Set-Cookie"] = "ab_variant=" .. variant ..
"; Path=/; Max-Age=86400; HttpOnly"
ngx.var.ab_upstream = "checkout_" .. variant
}
proxy_pass http://$ab_upstream;
}
# Admin endpoint - oranı değiştir
location /internal/ab/ratio {
allow 10.0.0.0/8;
deny all;
content_by_lua_block {
local ratio = tonumber(ngx.var.arg_ratio)
if not ratio or ratio < 0 or ratio > 100 then
ngx.status = 400
ngx.say("Invalid ratio")
return
end
ngx.shared.ab_config:set("variant_b_ratio", ratio)
ngx.say("Ratio updated to " .. ratio .. "%")
}
}
}
}
Bu yapının güzelliği şu: Nginx’i yeniden başlatmadan A/B oranını değiştirebilirsiniz. /internal/ab/ratio?ratio=50 isteği gönderdiğinizde, anında %50 oranına geçiş olur. Yalnızca 10.0.0.0/8 subnet’inden erişilebilir olduğundan dışarıdan erişim engellenir.
Log Yapılandırması: Test Sonuçlarını Ölçmek
A/B test yapmak güzel ama ölçemezseniz anlamsız. Nginx log formatını özelleştirerek hangi variant’ın nasıl performans gösterdiğini takip edebilirsiniz.
# /etc/nginx/nginx.conf
http {
log_format ab_format escape=json
'{'
'"timestamp":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"request":"$request",'
'"status":$status,'
'"body_bytes_sent":$body_bytes_sent,'
'"request_time":$request_time,'
'"upstream_response_time":"$upstream_response_time",'
'"ab_variant":"$upstream_http_x_variant",'
'"session_id":"$cookie_session_id",'
'"user_agent":"$http_user_agent",'
'"referer":"$http_referer"'
'}';
server {
listen 80;
location /api/ {
access_log /var/log/nginx/ab_test.log ab_format;
# Upstream'den gelen variant bilgisini log'a ekle
proxy_pass http://backend_$variant;
}
}
}
Bu JSON log formatını Elasticsearch’e ya da bir log aggregator’a göndererek Kibana veya Grafana üzerinde gerçek zamanlı A/B metrikleri izleyebilirsiniz.
Log analizi için basit bir shell scripti:
#!/bin/bash
# /usr/local/bin/ab_report.sh
# A/B test günlük raporu
LOG_FILE="/var/log/nginx/ab_test.log"
DATE=$(date -d "yesterday" +%Y-%m-%d)
echo "=== A/B Test Raporu: $DATE ==="
echo ""
# Her variant için istek sayısı ve ortalama yanıt süresi
echo "Variant bazlı istatistikler:"
cat $LOG_FILE | grep "$DATE" | python3 -c "
import sys, json
from collections import defaultdict
stats = defaultdict(lambda: {'count': 0, 'total_time': 0, 'errors': 0})
for line in sys.stdin:
try:
data = json.loads(line)
variant = data.get('ab_variant', 'unknown')
req_time = float(data.get('request_time', 0))
status = int(data.get('status', 200))
stats[variant]['count'] += 1
stats[variant]['total_time'] += req_time
if status >= 400:
stats[variant]['errors'] += 1
except:
pass
for variant, s in stats.items():
avg_time = s['total_time'] / s['count'] if s['count'] > 0 else 0
error_rate = (s['errors'] / s['count'] * 100) if s['count'] > 0 else 0
print(f' Variant {variant}: {s["count"]} istek, '
f'avg {avg_time:.3f}s, hata orani %{error_rate:.1f}')
"
Bu scripti crontab’a ekleyerek her sabah mail alabilirsiniz:
# crontab -e
0 8 * * * /usr/local/bin/ab_report.sh | mail -s "Gunluk AB Test Raporu" [email protected]
Canary Deployment: A/B Testing’in Kardeşi
Canary deployment aslında A/B testing’in özel bir halidir. Yeni versiyonu önce küçük bir kullanıcı kitlesine açar, sorun yoksa yavaş yavaş genişletirsiniz.
# /etc/nginx/conf.d/canary.conf
upstream stable {
server app-stable-1:8080;
server app-stable-2:8080;
keepalive 32;
}
upstream canary {
server app-canary-1:8080;
keepalive 8;
}
# Canary oranini environment variable ile kontrol et
# nginx -g "env CANARY_WEIGHT=10;"
# Nginx'i env var'dan okuyacak şekilde yapılandır
geo $internal_tester {
default 0;
10.0.0.0/8 1; # İç ağdan gelenler her zaman canary görür
192.168.0.0/16 1;
}
split_clients "${remote_addr}${date_gmt}" $canary_split {
5% "canary";
* "stable";
}
server {
listen 80;
server_name app.example.com;
location / {
set $upstream_target $canary_split;
# İç ağdan gelen testerlar her zaman canary'e gider
if ($internal_tester) {
set $upstream_target "canary";
}
proxy_pass http://$upstream_target;
# Hangi versiyonu gördüğünü response header'ına ekle
add_header X-Served-By $upstream_target always;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Canary için ayrı timeout değerleri
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
# Sağlık kontrolü
location /health {
access_log off;
return 200 "OKn";
}
}
Hata Yönetimi ve Fallback Mekanizması
Canary veya B versiyonu hata verirse otomatik olarak A’ya dönmek isteyebilirsiniz. Nginx’in proxy_next_upstream direktifi tam bu iş için var.
# /etc/nginx/conf.d/fallback.conf
upstream backend_a {
server 10.0.1.10:8080;
server 10.0.1.11:8080;
}
upstream backend_b {
server 10.0.2.10:8080 max_fails=2 fail_timeout=10s;
# B başarısız olursa A'ya düşmek için server ekliyoruz
server 10.0.1.10:8080 backup;
}
server {
listen 80;
location /api/ {
split_clients "$remote_addr" $variant {
25% "b";
* "a";
}
proxy_pass http://backend_$variant;
# 5xx hataları ve zaman aşımında bir sonraki sunucuya geç
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 5s;
# Intercept hataları
proxy_intercept_errors on;
error_page 502 503 504 @fallback;
}
location @fallback {
# B versiyonu tamamen çöktüyse A'ya yönlendir
proxy_pass http://backend_a;
add_header X-Fallback "true" always;
}
}
Bu yapıda B versiyonu sürekli hata verirse Nginx otomatik olarak onu devre dışı bırakır (max_fails=2 fail_timeout=10s). 10 saniye boyunca B’ye istek göndermez, ardından tekrar dener.
Yapılandırma Testi ve Doğrulama
Herhangi bir değişiklik yapmadan önce mutlaka test edin:
# Nginx yapılandırmasını test et
sudo nginx -t
# Daha detaylı çıktı için
sudo nginx -T 2>&1 | grep -A5 "split_clients"
# Yapılandırmayı yeniden yükle (zero-downtime)
sudo nginx -s reload
# A/B dağılımını test et - 1000 istek gönder ve kaçı B'ye gitmiş say
for i in $(seq 1 1000); do
IP="$((RANDOM % 256)).$((RANDOM % 256)).$((RANDOM % 256)).$((RANDOM % 256))"
curl -s -o /dev/null -w "%{http_code}n"
-H "X-Forwarded-For: $IP"
http://localhost/api/test
done | sort | uniq -c
# Hangi variant'a gittiğini header ile kontrol et
curl -v http://api.example.com/products 2>&1 | grep -i "x-variant|x-served-by|set-cookie"
# Cookie bazlı testi simüle et
# Önce cookie olmadan istek at, atanan variant'ı al
VARIANT=$(curl -s -c /tmp/cookies.txt -D - http://api.example.com/ | grep "ab_variant" | grep -oP 'ab_variant=K[^;]+')
echo "Atanan variant: $VARIANT"
# Aynı cookie ile tekrar istek at - her seferinde aynı variant gelmeli
for i in $(seq 1 5); do
curl -s -b /tmp/cookies.txt -D - http://api.example.com/ | grep -i "x-variant"
done
Güvenlik Noktaları
A/B testing yapılandırmasında göz ardı edilen bazı güvenlik konuları var:
- Rate limiting uygulayın: Her iki upstream için de rate limit tanımlayın, yoksa B versiyonu DDoS’a karşı daha savunmasız olabilir
- İç endpoint’leri koruyun:
/internal/ab/gibi yönetim endpoint’lerini mutlaka IP kısıtlamasıyla koruyun - Log’larda PII sorunu: Kullanıcı IP’lerini log’larken GDPR uyumluluğunu göz önünde bulundurun. IP’yi hashleyerek saklayabilirsiniz
- Cookie güvenliği:
HttpOnlyveSecureflag’lerini eksik bırakmayın - Header injection: Dışarıdan gelen
X-Force-Variantgibi header’ları sadece güvenilir IP’lerden kabul edin
# Header validation örneği
map $remote_addr $allow_override {
10.0.0.0/8 1;
default 0;
}
server {
location /api/ {
# Dışarıdan gelen override header'ını temizle
proxy_set_header X-Force-Variant "";
# Sadece iç ağdan izin ver
if ($allow_override) {
proxy_set_header X-Force-Variant $http_x_force_variant;
}
proxy_pass http://backend_$variant;
}
}
Sonuç
Nginx ile A/B testing, altyapınıza entegre, düşük maliyetli ve esnek bir çözüm sunar. Basit split_clients kullanımından Lua tabanlı dinamik yönerime kadar ihtiyacınıza göre ölçekleyebilirsiniz.
Önerdiğim yaklaşım şu sırayla ilerlemek:
- Önce
split_clientsile basit bir yüzde bazlı bölme yapın - Cookie tabanlı tutarlılığı ekleyin
- Log formatını JSON’a çevirin ve metrikleri takip edin
- Sorun yoksa Lua ile dinamik yönetim katmanını ekleyin
En kritik nokta ölçmek. Test yapıyorsunuz ama sonuçları analiz etmiyorsanız, test yapmıyorsunuz demektir. Log’larınızı mutlaka bir analiz platformuna gönderin ve karar vermek için yeterli veri birikene kadar sabırlı olun. Acele geri alma kararları, testinizin anlamlı sonuçlar üretmesini engeller.
Production’da bu yapılandırmaları denemeden önce staging ortamında test etmek de şart. Özellikle cookie mantığını ve fallback mekanizmasını kapsamlı şekilde doğrulayın.
