Performans Testi Metrikleri: Latency, Throughput ve P99 Nedir?
Bir sistemin gerçekten ne kadar iyi çalıştığını anlamak istiyorsanız, kulağa hoş gelen “sistem stabil çalışıyor” cümlesinin ötesine geçmeniz gerekiyor. Yıllarca üretim ortamlarında yük testi yapıp sonuçları analiz ettikten sonra şunu net söyleyebilirim: çoğu ekip metrikleri yanlış okuyor. Latency ortalamasına bakıyor, P99’u görmezden geliyor, throughput’u tek başına yorumluyor. Bu yazıda performans testinin üç temel metriğini, aralarındaki ilişkiyi ve bunları k6, Locust ile JMeter’da nasıl doğru ölçeceğinizi anlatacağım.
Latency Nedir ve Neden Ortalama Sizi Yanıltır
Latency, bir isteğin sisteme girişinden yanıtın alınmasına kadar geçen süredir. Kulağa basit geliyor ama ölçüm noktası kritik. Client tarafında mı ölçüyorsunuz, server tarafında mı? Network gecikmesi hesaba katılıyor mu? Bu sorular üretim ortamında gerçekten önemli.
Peki neden ortalama latency yanıltıcı? Şöyle düşünün: 100 isteğiniz var. 99 tanesi 10ms’de tamamlanıyor, bir tanesi 5000ms’de tamamlanıyor. Ortalama yaklaşık 60ms gösteriyor. “Sistemimiz 60ms’de yanıt veriyor” diyorsunuz ama aslında bir kullanıcı 5 saniye bekliyor. Üretimde bu tablo çok daha kötü olabilir.
Aşağıda bunu simüle eden basit bir Python scripti var:
python3 - <<'EOF'
import statistics
latencies = [10] * 99 + [5000]
print(f"Ortalama (mean): {statistics.mean(latencies):.2f}ms")
print(f"Medyan (median): {statistics.median(latencies):.2f}ms")
print(f"P90: {sorted(latencies)[int(len(latencies)*0.90)]:.2f}ms")
print(f"P95: {sorted(latencies)[int(len(latencies)*0.95)]:.2f}ms")
print(f"P99: {sorted(latencies)[int(len(latencies)*0.99)]:.2f}ms")
EOF
Bu scripti çalıştırdığınızda göreceksiniz ki ortalama 60ms civarı gösterirken P99 zaten 5000ms’e işaret ediyor. Gerçek sorun orada.
Throughput: Sadece “Kaç İstek” Değil
Throughput, birim zamanda işlenen istek sayısıdır. Genellikle RPS (Requests Per Second) veya TPS (Transactions Per Second) olarak ifade edilir. Ama burada da bir tuzak var: yüksek throughput, iyi latency anlamına gelmiyor.
Bir e-ticaret platformunda şöyle bir senaryo yaşadık: saniyede 500 istek işleyebiliyorduk ama P95 latency 2 saniyeyi geçmişti. Throughput rakamı parlak görünüyordu, kullanıcı deneyimi berbattı. Sistem istekleri kabul edip kuyruğa atıyordu, yüksek throughput aslında arkada biriken işin göstergesiydi.
Throughput ve latency arasındaki ilişkiyi doğru anlamak için Little’s Kanunu çok işe yarıyor:
L = λ × W
- L: Sistemdeki ortalama istek sayısı (concurrency)
- λ (lambda): Throughput (istek/saniye)
- W: Ortalama latency (saniye)
Örnek: Sisteminizde aynı anda 50 aktif istek var ve ortalama latency 100ms ise, throughput = 50 / 0.1 = 500 RPS demektir. Bu formül kapasite planlamasında altın değerinde.
P99 ve Percentile Metrikleri
Percentile metrikleri, latency dağılımını anlamanın en doğru yolu. P99, tüm isteklerin %99’unun bu değerin altında tamamlandığını gösterir. Yani 1000 istekten 990 tanesi P99 değerinin altında.
Neden P99’a bu kadar önem veriyoruz?
- P50 (medyan): Tipik kullanıcı deneyimini gösterir
- P90: İsteklerin %90’ının deneyimini gösterir
- P95: SLA tanımlamalarında sıkça kullanılır
- P99: “Tail latency” denen kuyruk gecikmelerini yakalar
- P99.9 (P999): Yüksek hacimli sistemlerde kritik, 1000 istekten 1’inin deneyimi
Büyük sistemlerde P99.9 metriğini de izlemenizi öneririm. Günde 10 milyon istek işliyorsanız, P99.9 değeri 10.000 kullanıcıyı etkiliyor demektir.
k6 ile Performans Testi
k6, Go ile yazılmış modern bir load testing aracı. JavaScript ile test senaryosu yazıyorsunuz, sonuçları çok temiz raporluyor. Özellikle percentile eşikleri tanımlamak için harika.
Basit bir k6 test scripti:
cat > load_test.js << 'EOF'
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
const errorRate = new Rate('error_rate');
const customLatency = new Trend('custom_latency');
export const options = {
stages: [
{ duration: '2m', target: 50 }, // Ramp-up
{ duration: '5m', target: 50 }, // Steady state
{ duration: '2m', target: 100 }, // Spike
{ duration: '3m', target: 100 }, // Sustained load
{ duration: '2m', target: 0 }, // Ramp-down
],
thresholds: {
http_req_duration: [
'p(90)<500', // P90 500ms altında olmalı
'p(95)<1000', // P95 1 saniye altında olmalı
'p(99)<2000', // P99 2 saniye altında olmalı
],
error_rate: ['rate<0.01'], // Hata oranı %1 altında
http_req_failed: ['rate<0.05'],
},
};
export default function () {
const start = Date.now();
const res = http.get('https://api.example.com/v1/products', {
headers: { 'Authorization': 'Bearer token_here' },
timeout: '10s',
});
const duration = Date.now() - start;
customLatency.add(duration);
const success = check(res, {
'status 200': (r) => r.status === 200,
'response time < 2s': (r) => r.timings.duration < 2000,
'body not empty': (r) => r.body.length > 0,
});
errorRate.add(!success);
sleep(1);
}
EOF
k6 run load_test.js
k6’nın çıktısında dikkat etmeniz gereken alanlar:
http_req_durationaltındakip(90),p(95),p(99)değerlerihttp_req_failedoranıiterationsveiteration_durationdeğerleri
k6 ile InfluxDB ve Grafana Entegrasyonu
Sonuçları gerçek zamanlı izlemek için:
# InfluxDB'ye veri gönderme
k6 run --out influxdb=http://localhost:8086/k6db load_test.js
# Prometheus remote write ile
k6 run --out experimental-prometheus-rw load_test.js
# JSON çıktı almak için
k6 run --out json=results.json load_test.js
# Sonuçları analiz etmek için
cat results.json | jq '.metrics.http_req_duration.values |
{p90: .["p(90)"], p95: .["p(95)"], p99: .["p(99)"]}'
Locust ile Python Tabanlı Yük Testi
Locust, Python biliyorsanız çok hızlı senaryo yazmanızı sağlıyor. Gerçek dünya senaryolarını modellemek için özellikle esnek.
cat > locustfile.py << 'EOF'
from locust import HttpUser, task, between, events
from locust.runners import MasterRunner
import time
import json
class APIUser(HttpUser):
wait_time = between(1, 3)
def on_start(self):
"""Her kullanıcı başladığında login"""
response = self.client.post("/auth/login", json={
"username": "testuser",
"password": "testpass"
})
self.token = response.json().get("token", "")
@task(3)
def get_products(self):
"""En sık yapılan işlem - ağırlık 3"""
with self.client.get(
"/api/v1/products",
headers={"Authorization": f"Bearer {self.token}"},
name="/api/v1/products",
catch_response=True
) as response:
if response.elapsed.total_seconds() > 2.0:
response.failure(
f"Çok yavaş: {response.elapsed.total_seconds():.2f}s"
)
elif response.status_code != 200:
response.failure(f"Beklenmeyen status: {response.status_code}")
@task(1)
def create_order(self):
"""Az sıklıkta ama kritik işlem - ağırlık 1"""
payload = {
"product_id": 42,
"quantity": 1,
"shipping_address": "Test Mah. No:1"
}
with self.client.post(
"/api/v1/orders",
json=payload,
headers={"Authorization": f"Bearer {self.token}"},
name="/api/v1/orders [POST]",
catch_response=True
) as response:
if response.status_code not in [200, 201]:
response.failure(f"Sipariş oluşturulamadı: {response.status_code}")
@events.quitting.add_listener
def on_quit(environment, **kwargs):
"""Test bitiminde P99 kontrolü"""
stats = environment.stats.total
p99 = stats.get_response_time_percentile(0.99)
if p99 and p99 > 2000:
print(f"UYARI: P99 latency çok yüksek: {p99}ms")
environment.process_exit_code = 1
EOF
# Locust'u headless modda çalıştırma
locust -f locustfile.py
--headless
--users 100
--spawn-rate 10
--run-time 10m
--host https://api.example.com
--csv=results
--html=report.html
Locust’un CSV çıktısından percentile değerlerini çekmek için:
# results_stats.csv'den P99 değerini parse etme
awk -F',' 'NR>1 {print $1, $14, $15, $16}' results_stats.csv |
column -t -s' '
# Jq ile daha detaylı analiz (JSON loglama açıksa)
cat results_stats_history.csv |
awk -F',' 'NR>1 && $2=="GET /api/v1/products" {print $1,$14}' |
sort -n -k1
JMeter ile Enterprise Düzey Testler
JMeter eski ama üretimde hala çok yaygın. Özellikle kurumsal ortamlarda tercih ediliyor çünkü GUI var, non-technical ekipler de kullanabiliyor. Ben şahsen komut satırından çalıştırmayı tercih ediyorum.
# JMeter test planı oluşturma (XML formatında)
cat > test_plan.jmx << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2">
<hashTree>
<TestPlan testname="API Load Test">
<elementProp name="TestPlan.user_defined_variables"
elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="BASE_URL" elementType="Argument">
<stringProp name="Argument.name">BASE_URL</stringProp>
<stringProp name="Argument.value">api.example.com</stringProp>
</elementProp>
</collectionProp>
</elementProp>
</TestPlan>
</hashTree>
</jmeterTestPlan>
EOF
# JMeter'ı non-GUI modda çalıştırma (production için zorunlu)
jmeter -n
-t test_plan.jmx
-l results.jtl
-e
-o report_output/
-Jusers=100
-Jrampup=60
-Jduration=300
# Sonuç dosyasından percentile hesaplama
jmeter -g results.jtl -o aggregate_report/
JMeter sonuçlarını komut satırından analiz etmek:
# JTL dosyasından P99 hesaplama
awk -F',' 'NR>1 {print $2}' results.jtl |
sort -n |
awk 'BEGIN{count=0; a[0]=0}
{count++; a[count]=$1}
END{
p99_idx=int(count*0.99);
p95_idx=int(count*0.95);
p90_idx=int(count*0.90);
print "P90:", a[p90_idx], "ms";
print "P95:", a[p95_idx], "ms";
print "P99:", a[p99_idx], "ms";
print "Max:", a[count], "ms"
}'
Gerçek Dünya Senaryosu: E-Ticaret Platformu
Geçen yıl bir e-ticaret platformunun Black Friday hazırlığında şu tabloya denk geldik. Sistem normal günlerde gayet iyi çalışıyordu. Ortalama latency 120ms, throughput 200 RPS. Herkes mutluydu. Biz de k6 ile agresif bir yük testi yaptık.
cat > blackfriday_test.js << 'EOF'
import http from 'k6/http';
import { check, group } from 'k6';
import { Counter } from 'k6/metrics';
const checkoutErrors = new Counter('checkout_errors');
export const options = {
scenarios: {
browsing_users: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '5m', target: 500 },
{ duration: '10m', target: 500 },
{ duration: '2m', target: 0 },
],
gracefulRampDown: '30s',
},
checkout_users: {
executor: 'constant-arrival-rate',
rate: 50,
timeUnit: '1s',
duration: '15m',
preAllocatedVUs: 100,
maxVUs: 200,
},
},
thresholds: {
'http_req_duration{scenario:browsing_users}': ['p(99)<3000'],
'http_req_duration{scenario:checkout_users}': ['p(99)<5000'],
'checkout_errors': ['count<10'],
},
};
export default function () {
group('Ürün Listeleme', function () {
const res = http.get('/api/products?category=electronics&page=1');
check(res, { 'products loaded': (r) => r.status === 200 });
});
}
EOF
Sonuçlar ilginçti: 300 concurrent user’a kadar P99 1.2 saniyede kaldı. 400’e geçince P99 aniden 8 saniyeye fırladı. Throughput aynı kalmaya devam etti, bu yüzden monitoring sistemleri alarm vermedi. Neden? Database connection pool dolmuştu ve istekler connection bekliyordu. Throughput yüksek görünüyordu çünkü sistem hala istek alıyordu ama bunları yavaşça işliyordu.
Bu tür “knee point” analizi için:
# Farklı yük seviyelerinde P99 değerlerini karşılaştırma
for users in 50 100 200 300 400 500; do
echo "=== $users concurrent users ==="
k6 run
--vus $users
--duration 2m
--out json=results_${users}.json
load_test.js 2>/dev/null
cat results_${users}.json |
jq -r 'select(.metric == "http_req_duration") |
.data.tags.percentile + ": " + (.data.value | tostring) + "ms"' |
grep -E "p(9[059])"
done
Metrikleri Yorumlamak: Pratik Kılavuz
Test sonuçlarını yorumlarken şu hiyerarşiyi izliyorum:
İlk bakış: Hata oranı Hata oranı %1’in üstündeyse diğer metriklere bakmaya gerek yok. Sistem zaten çalışmıyor demektir.
İkinci bakış: Throughput stabilitesi Test boyunca throughput tutarlı mı arttı mı, azaldı mı? Throughput’ta düşüş genellikle sistemin kapasitesini aştığınızın işareti.
Üçüncü bakış: Latency dağılımı P50 ile P99 arasındaki fark çok büyükse (10 kattan fazla) tail latency probleminiz var. Bu genellikle şunlardan kaynaklanır:
- GC (Garbage Collection) pauzları, özellikle Java servislerde
- Database lock contention
- External API çağrıları
- Yetersiz thread pool boyutu
Dördüncü bakış: Latency trendi Test boyunca latency giderek artıyorsa memory leak veya connection pool tükenmesi ihtimali yüksek.
# Python ile basit trend analizi
python3 << 'EOF'
import json
from datetime import datetime
with open('results.json') as f:
data = [json.loads(line) for line in f if line.strip()]
duration_metrics = [
d for d in data
if d.get('metric') == 'http_req_duration'
and d.get('type') == 'Point'
]
# Her 30 saniyelik dilime ortalama latency
buckets = {}
for m in duration_metrics:
timestamp = m['data']['time']
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
bucket = dt.minute * 2 + (dt.second // 30)
if bucket not in buckets:
buckets[bucket] = []
buckets[bucket].append(m['data']['value'])
print("Zaman dilimi -> Ortalama Latency")
for bucket in sorted(buckets.keys()):
values = buckets[bucket]
avg = sum(values) / len(values)
print(f"Dilim {bucket:3d}: {avg:.1f}ms (n={len(values)})")
EOF
SLA Tanımlamada Metrik Kullanımı
Üretimde SLA tanımlamak için hangi metriği kullanacağınız önemli. “Sistem %99.9 uptime ile çalışacak” demek latency hakkında hiçbir şey söylemiyor.
Daha iyi bir SLA şöyle görünmeli:
- P50 latency: 200ms altında (kullanıcıların yarısının deneyimi)
- P95 latency: 500ms altında (SLA için temel eşik)
- P99 latency: 2000ms altında (en kötü deneyim sınırı)
- Hata oranı: %0.1 altında
- Throughput: Minimum 500 RPS kapasitesi
Bu değerleri belirleyip k6 threshold’larına dökmek deployment pipeline’ının bir parçası olmalı. Test geçemiyorsa deploy olmuyor.
Sonuç
Performans testinden gerçek değer almak için metrikleri doğru okumak şart. Ortalama latency sizi yanlış yönlendirir, P99 gerçeği söyler. Throughput tek başına bir şey ifade etmez, latency dağılımıyla birlikte değerlendirilmesi gerekir. Little’s Kanunu’nu aklınızdan çıkarmayın: concurrency, throughput ve latency birbirini doğrudan etkiler.
k6, Locust ve JMeter’ın her birinin farklı güçlü yanları var. k6 modern, hafif ve CI/CD entegrasyonu kolay. Locust Python ekibiyle iyi uyum sağlıyor, karmaşık senaryolar için esnek. JMeter kurumsal ortamlarda kaçınılmaz, non-GUI modda kullanmayı alışkanlık haline getirin.
En önemlisi: performans testini production’dan önce değil, sürekli yapın. Her deploy’dan sonra otomatik çalışan kısa bir yük testi, büyük sorunları küçükken yakalamanızı sağlar. “Sistem iyi çalışıyor” demek için sayılara ihtiyacınız var, his yetmiyor.
