k6 ile Yük Testi: Kurulum ve İlk Senaryo Yazımı
Performans testleri söz konusu olduğunda çoğu ekip hâlâ JMeter’ı açıyor, birkaç saat XML ile boğuşuyor ve sonunda “yeterince yakın” diyerek bir şeyler üretiyor. Ben de yıllarca öyle yaptım. Sonra k6 ile tanıştım ve o günden beri geri dönmedim. Grafana Labs tarafından geliştirilen bu araç, yük testini gerçek bir geliştirici deneyimine dönüştürüyor: JavaScript ile senaryo yazıyorsunuz, binary tek dosya, CI/CD’ye entegrasyon son derece kolay. Bu yazıda k6’yı sıfırdan kuracağız, ilk senaryomuzu yazacağız ve gerçek dünyada işe yarayan birkaç pattern’ı inceleyeceğiz.
k6 Nedir ve Neden Önemli?
k6, Go ile yazılmış bir çekirdek üzerine JavaScript (ES6+) ile senaryo yazmanıza olanak tanıyan açık kaynaklı bir yük test aracıdır. JMeter’dan farkı sadece daha hızlı olması değil, aynı zamanda kod olarak test yaklaşımını birinci sınıf vatandaş olarak kabul etmesidir. Testleriniz Git’te yaşar, code review’dan geçer, CI pipeline’ınızda çalışır.
Birkaç önemli özelliği var:
- Düşük kaynak tüketimi: 10.000 sanal kullanıcıyı tek bir makinede rahatlıkla kaldırabilir
- JavaScript API: Öğrenme eğrisi minimal, her geliştirici kolayca adapte olabilir
- Yerleşik metrikler: HTTP istek süresi, başarı oranı, veri transferi otomatik toplanır
- Esnek çıktı: Prometheus, InfluxDB, Grafana Cloud gibi sistemlere doğrudan aktarım
- Threshold sistemi: “P95 latency 300ms altında kalmalı” gibi kurallar tanımlayıp test otomatik başarısız/başarılı olabilir
Kurulum
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
Linux (RHEL/CentOS/Rocky)
sudo dnf install https://dl.k6.io/rpm/repo.rpm
sudo dnf install k6
macOS
brew install k6
Docker ile Hızlı Başlangıç
Docker tercih ediyorsanız, hiçbir şey kurmadan hemen kullanabilirsiniz:
docker run --rm -i grafana/k6 run - <script.js
Kurulumu doğrulamak için:
k6 version
# k6 v0.49.0 (go1.21.5, linux/amd64)
İlk Senaryo: Basit HTTP GET Testi
k6 testleri en temel haliyle bir JavaScript dosyasıdır. export default function() zorunlu giriş noktanızdır. Her sanal kullanıcı bu fonksiyonu sürekli olarak çağırır.
// basit-test.js
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
vus: 10, // 10 sanal kullanıcı
duration: '30s', // 30 saniye boyunca
};
export default function () {
http.get('https://test.k6.io');
sleep(1); // Her iterasyon arasında 1 saniye bekle
}
Çalıştırmak için:
k6 run basit-test.js
Terminal çıktısında şunları göreceksiniz: toplam istek sayısı, başarı oranı, ortalama/medyan/P90/P95/P99 gecikme süreleri ve veri transfer hızı. İlk kez çalıştırdığınızda bu sayıları görmek oldukça tatmin edici.
Senaryo Yapısını Anlamak
k6 testleri dört yaşam döngüsü aşamasından oluşur:
// lifecycle.js
// 1. INIT aşaması - her VU için bir kez çalışır, dosya bazında
import http from 'k6/http';
import { check, sleep } from 'k6';
// 2. SETUP aşaması - test başlamadan önce BİR KEZ çalışır
export function setup() {
console.log('Test ortamı hazırlanıyor...');
// Login token alınabilir, test verisi hazırlanabilir
const loginRes = http.post('https://api.orneksite.com/auth/login', {
username: 'testuser',
password: 'testpass123',
});
return { token: loginRes.json('access_token') };
}
// 3. DEFAULT aşaması - her VU sürekli çalışır
export default function (data) {
// setup'tan gelen data burada kullanılabilir
const headers = { Authorization: `Bearer ${data.token}` };
http.get('https://api.orneksite.com/products', { headers });
sleep(1);
}
// 4. TEARDOWN aşaması - test bitince BİR KEZ çalışır
export function teardown(data) {
console.log('Test tamamlandı, temizlik yapılıyor...');
}
Bu yapıyı anlamak kritik. setup() fonksiyonu sadece bir kez çalışır ve döndürdüğü değer tüm VU’lara aktarılır. Örneğin ortak bir auth token almak için mükemmel bir yer.
Gerçek Dünya Senaryosu: E-ticaret Akışı
Sadece bir endpoint’e istek atmak gerçekçi bir yük testi değildir. Gerçek kullanıcılar bir akış içinde hareket eder. İşte tipik bir e-ticaret kullanıcı yolculuğu:
// eticaret-test.js
import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { Rate } from 'k6/metrics';
const errorRate = new Rate('errors');
export const options = {
stages: [
{ duration: '2m', target: 20 }, // 2 dakikada 20 VU'ya çık
{ duration: '5m', target: 20 }, // 5 dakika 20 VU'da tut
{ duration: '2m', target: 50 }, // 2 dakikada 50 VU'ya çık
{ duration: '5m', target: 50 }, // 5 dakika 50 VU'da tut
{ duration: '2m', target: 0 }, // 2 dakikada sıfıra in
],
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
errors: ['rate<0.05'], // Hata oranı %5'in altında kalsın
http_req_failed: ['rate<0.01'],
},
};
const BASE_URL = 'https://api.orneksite.com';
export default function () {
group('Ürün Listesi', function () {
const res = http.get(`${BASE_URL}/products?page=1&limit=20`);
check(res, {
'status 200': (r) => r.status === 200,
'ürünler mevcut': (r) => r.json('data').length > 0,
'yanıt süresi < 300ms': (r) => r.timings.duration < 300,
});
errorRate.add(res.status !== 200);
sleep(Math.random() * 2 + 1); // 1-3 saniye arası rastgele bekleme
});
group('Ürün Detayı', function () {
const productId = Math.floor(Math.random() * 100) + 1;
const res = http.get(`${BASE_URL}/products/${productId}`);
check(res, {
'status 200 veya 404': (r) => [200, 404].includes(r.status),
});
sleep(Math.random() * 3 + 2);
});
group('Sepete Ekle', function () {
const payload = JSON.stringify({
product_id: Math.floor(Math.random() * 100) + 1,
quantity: 1,
});
const params = {
headers: { 'Content-Type': 'application/json' },
};
const res = http.post(`${BASE_URL}/cart/add`, payload, params);
check(res, {
'sepete eklendi': (r) => r.status === 201 || r.status === 200,
});
errorRate.add(res.status >= 400);
sleep(1);
});
}
Bu senaryoda dikkat etmeniz gereken birkaç nokta var. stages ile ramping pattern tanımlıyoruz, yani gerçek trafik gibi yavaş yavaş yük artıyor. thresholds ile başarı kriterlerini kodun içine gömüyoruz. group() kullanmak, raporlarda hangi adımın nerede yavaşladığını görmek için çok değerli.
Threshold Sistemi: Testleri Anlamlı Kılmak
Threshold’lar k6’nın en güçlü özelliklerinden biri. CI/CD pipeline’ınızda bir deploy’un performansı kötüleştirip kötüleştirmediğini otomatik olarak anlayabilirsiniz:
export const options = {
thresholds: {
// HTTP istek süreleri
http_req_duration: [
'p(50)<100', // Medyan 100ms altında
'p(90)<300', // P90 300ms altında
'p(95)<500', // P95 500ms altında
'p(99)<1000', // P99 1 saniye altında
],
// Belirli bir URL için özel threshold
'http_req_duration{url:https://api.orneksite.com/checkout}': [
'p(95)<800', // Checkout endpoint'i biraz daha toleranslı
],
// Başarısız istek oranı
http_req_failed: ['rate<0.005'], // Binde 5'in altında hata
// Özel metrikler
'errors': ['rate<0.1'],
// Belirli grup için süre
'group_duration{group:::Sepete Ekle}': ['p(95)<600'],
},
};
Threshold karşılanmazsa k6 exit code 99 ile çıkar. Bu sayede CI/CD pipeline’ınızda k6 run komutunun başarı durumunu kontrol ederek deployment’ı otomatik durdurabilirsiniz.
Parametrize Testler: CSV ve JSON ile Test Verisi
Gerçek senaryolarda aynı kullanıcı bilgileriyle test etmek istemezsiniz. k6’nın SharedArray özelliği ile harici veriyi verimli bir şekilde yükleyebilirsiniz:
// parametrize-test.js
import http from 'k6/http';
import { check } from 'k6';
import { SharedArray } from 'k6/data';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';
// Tüm VU'lar arasında paylaşılan, bellekte tek kopya tutulan veri
const users = new SharedArray('kullaniciler', function () {
const csvData = open('./users.csv');
return papaparse.parse(csvData, { header: true }).data;
});
export const options = {
vus: 50,
duration: '1m',
};
export default function () {
// Her VU rastgele bir kullanıcı seçer
const user = users[Math.floor(Math.random() * users.length)];
const payload = JSON.stringify({
email: user.email,
password: user.password,
});
const res = http.post('https://api.orneksite.com/auth/login', payload, {
headers: { 'Content-Type': 'application/json' },
});
check(res, {
'login başarılı': (r) => r.status === 200,
'token alındı': (r) => r.json('access_token') !== undefined,
});
}
users.csv dosyasının formatı:
# users.csv
email,password
[email protected],Passw0rd!
[email protected],Secure123!
[email protected],Test456!
SharedArray kritik bir optimizasyon: 50 VU olsa bile CSV verisi bellekte tek kopya olarak tutulur.
HTTP Oturumu ve Cookie Yönetimi
Bazı uygulamalar session-based auth kullanır. k6’da her VU otomatik olarak kendi cookie jar’ını yönetir, ama bazen daha fazla kontrol gerekir:
// session-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { CookieJar } from 'k6/http';
export default function () {
const jar = new CookieJar();
const params = { jar: jar };
// Login - cookie otomatik jar'a yazılır
const loginRes = http.post(
'https://app.orneksite.com/login',
{ username: 'testuser', password: 'testpass' },
params
);
check(loginRes, { 'login OK': (r) => r.status === 200 });
sleep(1);
// Session cookie otomatik gönderilir
const dashboardRes = http.get(
'https://app.orneksite.com/dashboard',
params
);
check(dashboardRes, {
'dashboard yüklendi': (r) => r.status === 200,
'kullanıcı adı görünüyor': (r) => r.body.includes('Hoş geldiniz'),
});
sleep(2);
// Logout
http.post('https://app.orneksite.com/logout', null, params);
}
Sonuçları InfluxDB + Grafana ile Görselleştirme
Terminal çıktısı hızlı kontrol için yeterli, ama gerçek analizler için görsel dashboard lazım. Yerel bir stack kurmak için:
# Docker Compose ile InfluxDB + Grafana
cat > docker-compose.yml << 'EOF'
version: '3'
services:
influxdb:
image: influxdb:1.8
ports:
- "8086:8086"
environment:
- INFLUXDB_DB=k6
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
volumes:
- grafana_data:/var/lib/grafana
volumes:
grafana_data:
EOF
docker-compose up -d
# k6'yı InfluxDB'ye yönlendirerek çalıştır
k6 run --out influxdb=http://localhost:8086/k6 eticaret-test.js
Grafana’da 2587 numaralı k6 dashboard’unu import edin. Birkaç dakika içinde gerçek zamanlı metriklerinizi güzel bir dashboard üzerinden takip edebilirsiniz. Bu kombinasyon, uzun süreli yük testlerinde nerede darboğaz oluştuğunu görmek için vazgeçilmez.
CI/CD Entegrasyonu: GitHub Actions Örneği
k6’yı pipeline’a entegre etmek düşündüğünüzden çok daha kolay:
# .github/workflows/performance-test.yml
name: Performance Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
k6-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: k6 Kur
run: |
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
- name: Smoke Test (Hızlı Kontrol)
run: k6 run --vus 1 --duration 30s tests/smoke-test.js
env:
BASE_URL: ${{ secrets.STAGING_URL }}
- name: Yük Testi
if: github.ref == 'refs/heads/main'
run: k6 run tests/load-test.js
env:
BASE_URL: ${{ secrets.STAGING_URL }}
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
PR’larda sadece smoke test çalışır, main’e merge sonrası tam yük testi devreye girer. Bu ayrım önemli, her PR için 10 dakika yük testi çalıştırmak pratik değil.
Yaygın Hatalar ve Çözümleri
Yeni başlayanların en sık yaptığı hatalar:
sleep()kullanmamak: Gerçek kullanıcılar sayfalar arasında zaman geçirir. Sleep olmadan sanal kullanıcılarınız saniyede yüzlerce istek atar, bu gerçekçi değil ve hedef sistemi gereksiz yere ezer- Statik veri kullanmak: Aynı kullanıcı adı ile giriş yapmak cache’i yanıltır, gerçek yükü yansıtmaz.
SharedArrayile çeşitli test verisi kullanın - Threshold koymamak: Threshold olmadan testiniz her zaman “geçer”. Anlamsız hale gelir. Mutlaka P95 ve hata oranı threshold’ları ekleyin
- VU’yu kullanıcı sayan bir araç gibi görmek: k6’da VU, sürekli istek atan bir goroutine’dir. 100 VU, 1 saniyelik sleep ile yaklaşık 100 eşzamanlı istek demektir
Sonuç
k6, yük testini “bir kere çalıştırılıp unutulan araç” olmaktan çıkarıp yazılım geliştirme sürecinin organik bir parçasına dönüştürüyor. Testleriniz kod, dolayısıyla Git’te yaşıyor, review ediliyor, CI’da çalışıyor. Bir özellik eklendiğinde veya bir refactoring yapıldığında performans regresyonunu otomatik yakalayabiliyorsunuz.
Bu yazıda anlattıklarım bir başlangıç noktası. Bundan sonraki adımlar için birkaç öneri: k6 Browser modülü ile gerçek tarayıcı testleri yapabilirsiniz, k6 Operator ile Kubernetes üzerinde dağıtık yük testi çalıştırabilirsiniz, Grafana Cloud k6 ile test altyapısını tamamen buluta taşıyabilirsiniz.
Ama önce basit başlayın. Tek bir kritik endpoint için bir threshold koyun, bunu CI’a ekleyin. Bir hafta içinde performans sorunlarını production’a ulaşmadan yakaladığınızda bu aracın değerini anlayacaksınız.
