Nginx Split Clients Modülü ile A/B Testing Nasıl Yapılır?

Kullanıcılarınızın yarısına yeni tasarımı gösterip diğer yarısına eskisini göstermek istiyorsunuz ama bunun için ayrı bir uygulama katmanı kurmak istemiyorsunuz. İşte tam burada Nginx’in split_clients modülü devreye giriyor. Yük dengeleyici, ters proxy veya web sunucusu olarak kullandığınız Nginx’e birkaç satır ekleyerek, sunucu tarafında tamamen şeffaf bir A/B test altyapısı kurabilirsiniz. Bu yazıda bunu gerçek dünya senaryolarıyla ele alacağız.

split_clients Nedir ve Nasıl Çalışır

split_clients modülü, Nginx’in standart dağıtımına dahil olan bir modüldür. Kurulum gerektirmez, derleme seçeneğine de ihtiyaç duymaz. Temel mantığı şu: belirlediğiniz bir değişkeni (IP adresi, cookie, User-Agent vs.) MurmurHash2 algoritmasıyla hash’ler ve ortaya çıkan sayıyı yüzdelik dilimlere böler. Her dilim için farklı bir değer atarsınız, o değeri de bir Nginx değişkenine yazarsınız. Sonrasında bu değişkeni proxy_pass, root, rewrite veya istediğiniz her yerde kullanırsınız.

Temel sözdizimi şöyle:

split_clients "${kaynak_deger}" $hedef_degisken {
    50%     deger_A;
    50%     deger_B;
}

Yüzdeler toplamı 100’ü geçmemeli ve son satıra * koyarak “geri kalanı” belirtebilirsiniz. Bu sayede 50/30/20 gibi üçlü bölümler de yapabilirsiniz.

Kurulum Öncesi: Modül Kontrolü

Nginx kurulumunuzda modülün aktif olup olmadığını kontrol etmek için:

nginx -V 2>&1 | grep -o split_clients

Eğer çıktı boş gelirse endişelenmeyin. split_clients Nginx’in http modülünün bir parçasıdır ve --without-http_split_clients_module ile açıkça devre dışı bırakılmadığı sürece her zaman mevcuttur. Ubuntu/Debian’da nginx-full veya nginx-extras paketi kullanıyorsanız kesinlikle dahildir.

Nginx sürümünüzü de kontrol edin:

nginx -v
# Örnek çıktı: nginx version: nginx/1.24.0

1.18 ve üzeri sürümlerde split_clients için ek bir şey yapmanıza gerek yoktur.

Senaryo 1: Yeni Ödeme Sayfasının A/B Testi

Diyelim ki e-ticaret sitenizde mevcut ödeme sayfanızı yeniden tasarladınız. Kullanıcıların %20’sine yeni sayfayı göstermek, geri kalan %80’ine ise eskisini göstermek istiyorsunuz. En temiz yöntem backend upstream’lerini ayırmak ve split_clients ile birini seçmektir.

http {
    # IP adresi + URI kombinasyonunu hash kaynağı olarak kullan
    split_clients "${remote_addr}${uri}" $odeme_versiyonu {
        20%     "yeni";
        *       "eski";
    }

    upstream odeme_eski {
        server 127.0.0.1:8080;
    }

    upstream odeme_yeni {
        server 127.0.0.1:8081;
    }

    server {
        listen 80;
        server_name example.com;

        location /checkout {
            if ($odeme_versiyonu = "yeni") {
                proxy_pass http://odeme_yeni;
                break;
            }
            proxy_pass http://odeme_eski;
        }
    }
}

Burada bir sorun var: if bloğu içinde proxy_pass kullanmak Nginx’te önerilmez. Daha temiz bir yaklaşım için değişkeni upstream adı olarak kullanmak:

http {
    split_clients "${remote_addr}" $backend_grup {
        20%     "odeme_yeni";
        *       "odeme_eski";
    }

    upstream odeme_eski {
        server 127.0.0.1:8080;
    }

    upstream odeme_yeni {
        server 127.0.0.1:8081;
    }

    server {
        listen 80;
        server_name example.com;

        location /checkout {
            proxy_pass http://$backend_grup;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            # Hangi versiyonu gördüğünü loglamak için header ekle
            add_header X-AB-Test $backend_grup;
        }
    }
}

Bu yaklaşımla proxy_pass değişken içerebilir ve Nginx bunu resolver üzerinden çözümler. Ancak değişken kullanıldığında Nginx upstream bloğunu farklı işler; bunu aklınızda tutun.

Senaryo 2: Statik Dosya Dizini Bölme (Frontend A/B)

Backend değil, frontend’i test etmek istiyorsanız, yani farklı HTML/CSS/JS dosyalarını servis edecekseniz, root dizini değişkeni kullanmak daha pratiktir:

http {
    split_clients "${remote_addr}" $site_versiyonu {
        30%     "/var/www/html_v2";
        *       "/var/www/html_v1";
    }

    server {
        listen 80;
        server_name example.com;

        root $site_versiyonu;

        location / {
            try_files $uri $uri/ /index.html;
        }

        # Statik assetler her zaman aynı yerden gelsin
        location /assets/ {
            root /var/www/shared;
            expires 30d;
        }
    }
}

Dikkat etmeniz gereken nokta: root $site_versiyonu şeklinde değişken kullanmak Nginx’in open_file_cache optimizasyonunu devre dışı bırakır. Yüksek trafikli sitelerde bu fark edilebilir olabilir. Böyle durumlarda backend’e yönlendirmek daha sağlıklıdır.

Senaryo 3: Cookie Bazlı Tutarlı Test

IP bazlı bölümleme bir sorunu beraberinde getirir: aynı kullanıcı her sayfada farklı versiyonu görebilir. Çünkü URI değişkene dahilse hash değeri değişir. Tutarlı bir deneyim için cookie kullanmak gerekir.

Strateji şu: Kullanıcı ilk geldiğinde cookie atayın, sonraki isteklerde bu cookie’yi hash kaynağı olarak kullanın.

http {
    # Önce cookie'ye bak, yoksa IP'yi kullan
    map $cookie_ab_test $ab_kaynak {
        default     $cookie_ab_test;
        ""          $remote_addr;
    }

    split_clients "$ab_kaynak" $ab_grup {
        50%     "A";
        *       "B";
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            # Cookie yoksa set et
            if ($cookie_ab_test = "") {
                add_header Set-Cookie "ab_test=$ab_grup; Path=/; Max-Age=2592000; HttpOnly";
            }

            proxy_pass http://backend_$ab_grup;
            proxy_set_header X-AB-Group $ab_grup;
        }
    }
}

Bu yapıda map direktifi ile cookie varsa cookie’yi, yoksa IP’yi kaynak olarak seçiyoruz. Böylece kullanıcı bir kez A grubuna düştüyse, cookie silinene kadar hep A grubunu görür.

Hash Kaynağı Seçimi

split_clients ile kullanabileceğiniz hash kaynakları ve ne zaman kullanacağınız:

  • $remote_addr: Basit, tutarlı. Aynı IP hep aynı gruba düşer. NAT arkasındaki kullanıcılar için sorun çıkarabilir.
  • $remote_addr$http_user_agent: IP + tarayıcı kombinasyonu. NAT sorununu kısmen çözer.
  • $cookie_session_id: Oturum bazlı bölme. Login gerektiren uygulamalar için ideal.
  • $request_id: Her istek için farklı grup. Tutarlılık istemiyorsanız, sadece yük dağıtımı yapıyorsanız kullanın.
  • $arg_user_id: Query string’deki kullanıcı ID’si. API testleri için kullanışlı.
  • $http_x_device_id: Mobil uygulamalardan gelen cihaz ID’leri için harika.
# Oturum cookie'si bazlı bölme örneği
split_clients "$cookie_PHPSESSID" $ab_test_grubu {
    33.3%   "kontrol";
    33.3%   "varyant_a";
    *       "varyant_b";
}

Loglama ve Metrik Toplama

A/B testinin işe yaraması için hangi kullanıcının hangi versiyonu gördüğünü kaydetmeniz gerekir. Nginx log formatına AB test değişkenini ekleyin:

http {
    split_clients "$cookie_user_id" $ab_test {
        50%     "variant_b";
        *       "control";
    }

    log_format ab_test_log '$remote_addr - $remote_user [$time_local] '
                           '"$request" $status $body_bytes_sent '
                           '"$http_referer" "$http_user_agent" '
                           'ab_group=$ab_test '
                           'request_id=$request_id';

    server {
        listen 80;
        server_name example.com;

        access_log /var/log/nginx/ab_test.log ab_test_log;

        location / {
            proxy_pass http://app_$ab_test;
            proxy_set_header X-Request-ID $request_id;
            proxy_set_header X-AB-Test $ab_test;
        }
    }
}

Bu logları daha sonra awk veya Elasticsearch ile analiz edebilirsiniz:

# Hangi gruptan kaç istek geldi?
awk '{for(i=1;i<=NF;i++) if($i~/^ab_group=/) print $i}' /var/log/nginx/ab_test.log | sort | uniq -c

# Grup başına ortalama response size
awk '{
    for(i=1;i<=NF;i++) {
        if($i~/^ab_group=/) grup=$i
    }
    print grup, $10
}' /var/log/nginx/ab_test.log | awk '{sum[$1]+=$2; count[$1]++} END {for(k in sum) print k, sum[k]/count[k]}'

Senaryo 4: Kademeli Rollout (Canary Release)

Split_clients’i sadece A/B test için değil, yeni versiyonu kademeli açmak için de kullanabilirsiniz. Bu yaklaşıma “canary deployment” denir:

http {
    # Başlangıçta sadece %5 yeni versiyona git
    split_clients "${remote_addr}" $app_version {
        5%      "v2";
        *       "v1";
    }

    upstream app_v1 {
        server 10.0.0.10:8080;
        server 10.0.0.11:8080;
    }

    upstream app_v2 {
        server 10.0.0.20:8080;  # Yeni sunucu
    }

    server {
        listen 80;
        server_name api.example.com;

        location / {
            proxy_pass http://app_$app_version;
            proxy_connect_timeout 5s;
            proxy_read_timeout 30s;

            # Hata durumunda eski versiyona fallback
            proxy_next_upstream error timeout http_502 http_503;
        }
    }
}

Yeni versiyonda sorun çıkmazsa oranı artırırsınız: 5% -> 20% -> 50% -> 100%. Her değişiklikten sonra nginx -s reload yapmanız yeterli, kesinti olmaz.

Önemli not: Oranı artırdığınızda hash dağılımı değişir. Yani 5%’te A grubuna düşen bir kullanıcı, 20%’ye geçtiğinizde hala A grubunda olmayabilir. Bu tutarlılık gerektiren senaryolarda (ör. ödeme akışı) sorun çıkarabilir. Cookie bazlı yaklaşım bu durumda daha iyidir.

map ile split_clients Kombinasyonu

Bazı durumlarda belirli kullanıcıları her zaman belirli bir gruba yönlendirmek istersiniz. Örneğin QA ekibiniz her zaman yeni versiyonu görsün, beta kullanıcıları da. Bunu map ile split_clients’ı birleştirerek yapabilirsiniz:

http {
    # Beta kullanıcıları her zaman yeni versiyonu görsün
    map $cookie_user_role $force_version {
        "beta"  "yeni";
        "qa"    "yeni";
        "admin" "yeni";
        default "";
    }

    # Normal kullanıcılar için split
    split_clients "$remote_addr" $split_version {
        30%     "yeni";
        *       "eski";
    }

    # İkisini birleştir: force varsa onu kullan, yoksa split sonucunu
    map $force_version $aktif_versiyon {
        ""      $split_version;
        default $force_version;
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            proxy_pass http://app_$aktif_versiyon;
            add_header X-Version $aktif_versiyon;
        }
    }
}

Bu yapı son derece esnektir. QA ekibiniz cookie’yi elle set ederek istediği versiyonu test edebilir, split_clients ise normal kullanıcılar için çalışmaya devam eder.

Konfigürasyon Test ve Doğrulama

Herhangi bir değişiklik yapmadan önce mutlaka config’i test edin:

# Syntax kontrolü
nginx -t

# Detaylı kontrol
nginx -T | grep -A 10 split_clients

# Değişiklikleri graceful reload ile uygula (kesinti yok)
nginx -s reload

# Belirli bir IP'nin hangi gruba düşeceğini test etmek için
# (Nginx'te doğrudan test aracı yok, bu yüzden curl kullanıyoruz)
curl -H "X-Forwarded-For: 1.2.3.4" -I http://localhost/ 2>&1 | grep X-AB

Log’dan hangi dağılımın oluştuğunu kontrol etmek için:

# Son 1000 isteğin grup dağılımı
tail -1000 /var/log/nginx/ab_test.log | grep -oP 'ab_group=K[^ ]+' | sort | uniq -c | awk '{printf "%s: %s (%.1f%%)n", $2, $1, $1/10}'

Yaygın Hatalar ve Çözümleri

Upstream adında değişken kullanınca resolver hatası:

# YANLIŞ: resolver tanımlanmamış
proxy_pass http://$backend_grup;

# DOĞRU: resolver ekle (Docker, Kubernetes için)
resolver 127.0.0.1 valid=30s;
proxy_pass http://$backend_grup;

# YA DA: upstream bloklarını kullan ve map ile seç
map $ab_grup $backend_host {
    "A"     "127.0.0.1:8080";
    "B"     "127.0.0.1:8081";
}
proxy_pass http://$backend_host;

Yüzde toplamının 100’ü aşması:

# YANLIŞ
split_clients "$remote_addr" $grup {
    60%     "A";
    50%     "B";  # Toplam 110%, hata!
}

# DOĞRU
split_clients "$remote_addr" $grup {
    60%     "A";
    *       "B";  # Geri kalanı B'ye ver
}

Cache ile çakışma:

Eğer Nginx’te proxy_cache kullanıyorsanız, farklı gruplara giden isteklerin birbirinin cache’ini görmemesi için cache key’e AB değişkenini ekleyin:

proxy_cache_key "$scheme$request_method$host$request_uri$ab_test";

Sonuç

Nginx split_clients modülü, uygulama katmanına hiç dokunmadan güçlü bir A/B test ve kademeli rollout altyapısı kurmanızı sağlıyor. IP bazlı basit bölmelerden cookie destekli tutarlı deneylere, canary deployment’tan QA bypass’ına kadar pek çok senaryoyu kapsıyor.

Öne çıkan noktalara bakmak gerekirse, modül Nginx’in standart kurulumunda geliyor ve ek derleme gerektirmiyor. MurmurHash2 algoritması sayesinde dağılım gerçekten uniform, yani rastgele gibi görünse de deterministik. Log formatına değişken eklemek analiz sürecini çok kolaylaştırıyor. map ile kombinasyonu son derece esnek senaryolar açıyor. nginx -s reload ile kesintisiz oran değiştirme ise production ortamlarda paha biçilmez.

Büyük ölçekli, çok katmanlı A/B test ihtiyaçlarınız için Nginx’in üstüne özel çözümler (LaunchDarkly, Unleash gibi feature flag sistemleri) katmak mantıklı olabilir. Ama altyapı seviyesinde, hızlı ve güvenilir bir yönlendirme için split_clients hala en pratik silah olma özelliğini koruyor.

Yorum yapın