k6 ile Gerçekçi Yük Testi Senaryoları Oluşturma

Yük testine başladığınızda genellikle şunu yaparsınız: bir script yazarsınız, 100 kullanıcı koyarsınız, çalıştırırsınız, “geçti” dersiniz ve işi bitirirsiniz. Sonra production’da gerçek trafik gelince sistem çöker. Çünkü yazdığınız test, gerçek dünyayı hiç yansıtmıyordu. k6 ile bu sorunu çözmek mümkün, ama bunun için aracı doğru kullanmak gerekiyor.

Bu yazıda k6’nın sadece “nasıl kurulur” kısmından değil, gerçekten üretim trafiğini simüle eden, anlamlı sonuçlar veren senaryolar nasıl yazılır, ondan bahsedeceğim. Birkaç yıldır farklı ölçeklerde sistemlerin yük testlerini yazan biri olarak söylüyorum: k6’nın JavaScript tabanlı yapısı başta garip gelir ama alıştıktan sonra JMeter’in XML cehenneminden çok daha temiz bir deneyim sunuyor.

k6’nın Temel Felsefesini Anlamak

k6, Grafana tarafından geliştirilen ve Go ile yazılmış bir yük testi aracıdır. Script’leri JavaScript/TypeScript ile yazıyorsunuz ama altında çalışan motor Go’dur, bu yüzden performansı son derece iyi. Tek bir makineden binlerce sanal kullanıcı (VU) çalıştırabilirsiniz.

En önemli kavramı baştan netleştirelim: k6’da her VU, script’inizi baştan sona döngüsel olarak çalıştırır. Bir VU bir iteration tamamlar, hemen bir sonrakine başlar. Bu mimariyi anlamadan yazdığınız testler yanıltıcı olur.

Kurulum son derece basit:

# Linux (Debian/Ubuntu)
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg 
  --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" 
  | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install k6

# macOS
brew install k6

# Docker ile çalıştırma
docker run --rm -i grafana/k6 run - <script.js

İlk Gerçekçi Senaryo: Statik Yük Değil, Dinamik Davranış

Çoğu yük testi şöyle görünür: 50 kullanıcı, her biri aynı endpoint’e istek atıyor, hepsi. Bu testin size söylediği şey son derece sınırlıdır.

Gerçek kullanıcılar şöyle davranır: Siteye gelirler, birkaç saniye bakarlar, tıklarlar, sonucu beklerler, başka bir şeye tıklarlar. Aralarında “think time” vardır. Bazıları hemen çıkar, bazıları derinlemesine gezer. Bunu yansıtmak için sleep kullanmak şart, ama yetmez.

import http from 'k6/http';
import { sleep, check } from 'k6';
import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js';

export const options = {
  stages: [
    { duration: '2m', target: 50 },   // Yavaş yüksel
    { duration: '5m', target: 50 },   // Sabit tut
    { duration: '2m', target: 100 },  // Zirveye çık
    { duration: '5m', target: 100 },  // Zirveyi koru
    { duration: '2m', target: 0 },    // Nazikçe in
  ],
  thresholds: {
    http_req_duration: ['p(95)<500', 'p(99)<1500'],
    http_req_failed: ['rate<0.01'],
  },
};

export default function () {
  // Ana sayfa
  const homeRes = http.get('https://api.example.com/');
  check(homeRes, {
    'ana sayfa 200': (r) => r.status === 200,
    'ana sayfa 2 saniyeden hızlı': (r) => r.timings.duration < 2000,
  });

  // Kullanıcı sayfayı okuyor (3-7 saniye arası rastgele)
  sleep(randomIntBetween(3, 7));

  // Ürün listesi
  const listRes = http.get('https://api.example.com/products?page=1&limit=20');
  check(listRes, {
    'liste 200': (r) => r.status === 200,
    'liste boş değil': (r) => JSON.parse(r.body).items.length > 0,
  });

  sleep(randomIntBetween(2, 5));
}

Burada stages kullanımına dikkat edin. Doğrudan 100 kullanıcıyla başlamak gerçekçi değil. Trafik her zaman bir rampa ile gelir.

Kimlik Doğrulama ve Oturum Yönetimi

Çoğu uygulama authentication gerektiriyor. Token’ı bir kez alıp tüm test boyunca kullanmak için setup() fonksiyonunu kullanabilirsiniz. Ama dikkat: setup bir kez çalışır ve tek bir token tüm VU’lara dağıtılır. Bu da gerçekçi değil, çünkü her kullanıcının kendi oturumu olmalı.

Doğru yaklaşım şu:

import http from 'k6/http';
import { check, sleep } from 'k6';
import { SharedArray } from 'k6/data';

// Test kullanıcıları JSON dosyasından yükle
const users = new SharedArray('users', function () {
  return JSON.parse(open('./test-users.json'));
});

export const options = {
  vus: 50,
  duration: '10m',
};

export default function () {
  // Her VU kendi kullanıcısını seçiyor (round-robin değil, rastgele)
  const user = users[Math.floor(Math.random() * users.length)];

  // Login
  const loginRes = http.post('https://api.example.com/auth/login', JSON.stringify({
    email: user.email,
    password: user.password,
  }), {
    headers: { 'Content-Type': 'application/json' },
  });

  check(loginRes, {
    'login başarılı': (r) => r.status === 200,
    'token var': (r) => JSON.parse(r.body).access_token !== undefined,
  });

  const token = JSON.parse(loginRes.body).access_token;

  // Token ile korumalı endpoint
  const profileRes = http.get('https://api.example.com/users/me', {
    headers: { Authorization: `Bearer ${token}` },
  });

  check(profileRes, {
    'profil 200': (r) => r.status === 200,
  });

  sleep(2);
}

test-users.json dosyası şöyle görünür:

[
  {"email": "[email protected]", "password": "Test1234!"},
  {"email": "[email protected]", "password": "Test1234!"},
  {"email": "[email protected]", "password": "Test1234!"}
]

SharedArray kullanımı önemli: Bu yapı veriyi tüm VU’lar arasında bellekte bir kez tutar. Büyük veri setleri için bellek tasarrufu sağlar.

Gerçekçi Kullanıcı Akışları: Senaryo Tabanlı Testler

Tek bir fonksiyon içinde tüm akışı yazmak yerine, k6’nın scenarios özelliğini kullanarak farklı kullanıcı tiplerini aynı anda simüle edebilirsiniz. E-ticaret uygulaması örneği düşünelim:

import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js';

const BASE_URL = 'https://api.example.com';

export const options = {
  scenarios: {
    // %70 kullanıcı sadece geziniyor
    browsers: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '3m', target: 70 },
        { duration: '10m', target: 70 },
        { duration: '2m', target: 0 },
      ],
      exec: 'browsing',
    },
    // %25 kullanıcı arama yapıyor
    searchers: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '3m', target: 25 },
        { duration: '10m', target: 25 },
        { duration: '2m', target: 0 },
      ],
      exec: 'searching',
    },
    // %5 kullanıcı satın alıyor
    buyers: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '3m', target: 5 },
        { duration: '10m', target: 5 },
        { duration: '2m', target: 0 },
      ],
      exec: 'purchasing',
    },
  },
  thresholds: {
    'http_req_duration{scenario:buyers}': ['p(95)<2000'],
    'http_req_duration{scenario:browsers}': ['p(95)<1000'],
  },
};

export function browsing() {
  group('Gezinme Akışı', function () {
    http.get(`${BASE_URL}/`);
    sleep(randomIntBetween(3, 8));

    http.get(`${BASE_URL}/categories`);
    sleep(randomIntBetween(2, 5));

    http.get(`${BASE_URL}/products/featured`);
    sleep(randomIntBetween(4, 10));
  });
}

export function searching() {
  const queries = ['laptop', 'telefon', 'kulaklık', 'klavye', 'monitör'];
  const query = queries[Math.floor(Math.random() * queries.length)];

  group('Arama Akışı', function () {
    const searchRes = http.get(`${BASE_URL}/search?q=${query}&sort=relevance`);
    check(searchRes, { 'arama sonuçları geldi': (r) => r.status === 200 });
    sleep(randomIntBetween(2, 6));

    // İlk ürüne tıklıyor
    const results = JSON.parse(searchRes.body);
    if (results.items && results.items.length > 0) {
      const productId = results.items[0].id;
      http.get(`${BASE_URL}/products/${productId}`);
      sleep(randomIntBetween(5, 15));
    }
  });
}

export function purchasing() {
  group('Satın Alma Akışı', function () {
    // Sepete ekle
    const cartRes = http.post(`${BASE_URL}/cart/items`, JSON.stringify({
      product_id: '12345',
      quantity: 1,
    }), { headers: { 'Content-Type': 'application/json' } });

    check(cartRes, { 'sepete eklendi': (r) => r.status === 201 });
    sleep(randomIntBetween(3, 8));

    // Ödeme sayfası
    const checkoutRes = http.get(`${BASE_URL}/checkout`);
    check(checkoutRes, { 'ödeme sayfası açıldı': (r) => r.status === 200 });
    sleep(randomIntBetween(10, 20));
  });
}

Bu yaklaşım çok daha gerçekçi sonuçlar verir. Threshold’larda da scenario bazında ayrım yapabiliyorsunuz, alışveriş akışı için toleransı artırmak mantıklı.

Executor Seçimi: Hangi Durumda Ne Kullanılır

k6’da farklı executor tipleri vardır ve doğru seçim kritik:

ramping-vus: Kullanıcı sayısını kademeli artırır/azaltır. Çoğu senaryo için en uygun. Sisteminizin yük altında nasıl davrandığını görmek için idealdir.

constant-arrival-rate: Saniyede sabit sayıda istek gönderir. Backend’in belirli bir RPS’e nasıl tepki verdiğini test etmek için kullanın. VU sayısını değil, istek hızını kontrol edersiniz.

per-vu-iterations: Her VU belirli sayıda iteration çalıştırır. Toplam işlem sayısını garanti etmeniz gerektiğinde işe yarar.

export const options = {
  scenarios: {
    // Sabit 100 RPS, sistem bunu karşılayabilir mi?
    sabit_yuk: {
      executor: 'constant-arrival-rate',
      rate: 100,
      timeUnit: '1s',
      duration: '10m',
      preAllocatedVUs: 200,
      maxVUs: 500,
    },
  },
};

preAllocatedVUs ve maxVUs farkına dikkat: Sistem önceden 200 VU hazırlar, gerekirse 500’e kadar çıkar. Eğer 500 VU bile 100 RPS’i karşılayamıyorsa test başarısız sayılır.

Özel Metrikler ile Daha Anlamlı Raporlama

k6’nın varsayılan metrikleri iyi bir başlangıç noktasıdır ama iş mantığına özgü metrikleri kendiniz tanımlamak çok daha değerli sonuçlar verir:

import http from 'k6/http';
import { check, sleep } from 'k6';
import { Counter, Gauge, Rate, Trend } from 'k6/metrics';

// Özel metrikler
const odemeBasarisizligi = new Counter('odeme_basarisizligi_toplam');
const aktifSepetSayisi = new Gauge('aktif_sepet_sayisi');
const odemeGecikme = new Trend('odeme_islem_suresi_ms');
const sepetDonusumOrani = new Rate('sepet_donusum_orani');

export default function () {
  // Ürün detay sayfası
  http.get('https://api.example.com/products/999');
  sleep(2);

  // Sepete ekle
  const sepetRes = http.post('https://api.example.com/cart', JSON.stringify({
    productId: 999,
    qty: 1
  }), { headers: { 'Content-Type': 'application/json' } });

  if (sepetRes.status === 201) {
    aktifSepetSayisi.add(1);
    sleep(5);

    // Ödeme dene
    const start = Date.now();
    const odemeRes = http.post('https://api.example.com/checkout/pay', JSON.stringify({
      method: 'card',
      card_token: 'tok_test_12345'
    }), { headers: { 'Content-Type': 'application/json' } });

    odemeGecikme.add(Date.now() - start);

    if (odemeRes.status === 200) {
      sepetDonusumOrani.add(1);
      aktifSepetSayisi.add(-1);
    } else {
      odemeBasarisizligi.add(1);
      sepetDonusumOrani.add(0);
    }

    check(odemeRes, {
      'ödeme tamamlandı': (r) => r.status === 200,
    });
  }
}

Bu metrikler Grafana’ya göndereceğinizde dashboard üzerinde gerçek zamanlı takip edebilirsiniz. “p95 latency” yazan bir grafik yerine “kaç ödeme başarısız oldu” yazması karar vericilere çok daha anlamlı gelir.

Grafana ile Entegrasyon ve Gerçek Zamanlı İzleme

k6’nın Grafana ile entegrasyonu için InfluxDB üzerinden veya doğrudan Grafana Cloud üzerinden çalışabilirsiniz:

# InfluxDB ile yerel kurulum
docker network create k6-net

docker run -d --name influxdb --network k6-net 
  -p 8086:8086 
  -e INFLUXDB_DB=k6 
  influxdb:1.8

docker run -d --name grafana --network k6-net 
  -p 3000:3000 
  grafana/grafana

# k6 testi InfluxDB'ye yönlendirerek çalıştır
k6 run --out influxdb=http://localhost:8086/k6 script.js

Grafana Cloud kullanıyorsanız daha da kolay:

# Ortam değişkenleri
export K6_CLOUD_TOKEN="your-grafana-cloud-token"
export K6_CLOUD_PROJECT_ID="12345"

# Sonuçları Grafana Cloud'a gönder
k6 run --out cloud script.js

CI/CD Pipeline’a Entegrasyon

Yük testlerini CI/CD’ye entegre ettiğinizde bazı önemli noktalara dikkat etmeniz gerekiyor. Her commit’te tam yük testi çalıştırmak çok maliyetli. Bunun yerine aşamalar oluşturun:

#!/bin/bash
# pipeline-load-test.sh

# Hangi ortam?
ENVIRONMENT=${1:-staging}
BASE_URL="https://${ENVIRONMENT}.api.example.com"

echo "Smoke test çalışıyor: ${BASE_URL}"

# Önce smoke test (hafif, hızlı)
k6 run 
  --env BASE_URL=${BASE_URL} 
  --vus 1 
  --duration 30s 
  --threshold 'http_req_failed<0.01' 
  smoke-test.js

if [ $? -ne 0 ]; then
  echo "SMOKE TEST BAŞARISIZ - Pipeline durduruluyor"
  exit 1
fi

echo "Smoke test geçti, yük testi başlıyor..."

# Staging'de kısa yük testi
k6 run 
  --env BASE_URL=${BASE_URL} 
  --out json=results-${ENVIRONMENT}.json 
  load-test.js

# Sonuç dosyasını parse et
FAILED=$(cat results-${ENVIRONMENT}.json | 
  python3 -c "import sys,json; d=[json.loads(l) for l in sys.stdin if l.strip()]; 
  failed=[x for x in d if x.get('type')=='Point' and 
  x.get('metric')=='checks' and x.get('data',{}).get('value')==0]; 
  print(len(failed))")

echo "Başarısız check sayısı: ${FAILED}"
exit ${FAILED}

Sık Yapılan Hatalar ve Kaçınma Yolları

Bunları sayıyorum çünkü hepsini bizzat yaşadım ya da başkalarının yaşadığına tanıklık ettim.

Test verisi çakışması: Birden fazla VU aynı kullanıcıyı kullanıyorsa oturum çakışmaları yaşanır. SharedArray ile yeterli kullanıcı havuzu oluşturun ve VU sayısına göre hesap açın.

Think time’ı atlamak: sleep() koymadan yazılan testler sistemi gerçekçi olmayan şekilde zorlar. Her gerçek kullanıcı eylemler arasında düşünür, bekler.

Sadece başarılı akışları test etmek: Gerçek kullanıcıların bir kısmı yanlış şifre girer, geçersiz formlar gönderir. Hata akışlarını da teste dahil edin.

Threshold’ları çok gevşek koymak: p(99)<10000 gibi threshold’lar test geçer ama 10 saniye yanıt süresi production’da felaket olur. Gerçek SLA’larınıza göre threshold belirleyin.

Correlation yapmamak: Bir endpoint’ten dönen ID’yi sonraki isteğe hardcode koymak yerine dinamik olarak alın. Aksi halde test gerçeği yansıtmaz, hep aynı kaydı silersiniz, silecek kayıt kalmaz, hatalar artar.

Sonuç

k6 ile gerçekçi yük testi yazmak başta fazla iş gibi görünüyor: kullanıcı senaryoları, think time, özel metrikler, scenario ayarları. Ama bu zahmeti çekmeyen testler size ya “her şey yolunda” yalaningini söyler ya da production’a benzer olmadığı için bulunan bottleneck’ler yanıltıcı olur.

Özetleyecek olursam şu üç şeyi hep aklınızda tutun. Birincisi, gerçek kullanıcı davranışını simüle edin: sleep ekleyin, farklı akışlar oluşturun, veri çeşitlendirin. İkincisi, iş metriklerini izleyin: p95 latency önemli ama “kaç sipariş tamamlandı” ya da “kaç ödeme başarısız oldu” daha anlamlı. Üçüncüsü, testleri pipeline’a alın: Yük testi bir kerelik etkinlik değil, kod değiştikçe tekrarlanan bir güvence mekanizması olmalı.

k6’nın JavaScript tabanlı yapısı, versiyon kontrolü ve kod incelemesi açısından da büyük avantaj sağlıyor. Test scriptleriniz uygulama kodunuzla birlikte yaşıyor, birlikte değişiyor ve birlikte review ediliyor. Bu kültürü oturttuğunuzda yük testi “deployment öncesi bir kez çalıştırılan şey” olmaktan çıkıp gerçek bir kalite güvencesi mekanizmasına dönüşüyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir