Performans Testi Sonuçlarını Analiz Etme ve Raporlama

Yük testleri çalıştırmak aslında işin kolay kısmı. Asıl mesele, binlerce satır metrik veriyi alıp “şu servis prod’a çıkabilir mi, çıkamaz mı?” sorusuna dürüst bir cevap üretebilmek. Bu yazıda k6, Locust ve JMeter’dan gelen ham sonuçları nasıl analiz edeceğimizi, hangi metriklere odaklanmamız gerektiğini ve yöneticilere/geliştiricilere sunulabilir raporlar nasıl hazırlanır, bunları somut örneklerle ele alacağız.

Hangi Metrikler Gerçekten Önemli?

Bir performans testi bittikten sonra elinizde genellikle onlarca farklı metrik olur. Hepsine eşit ağırlık vermeye çalışırsanız raporunuz anlamsız bir tablo yığınına dönüşür. Önce neye bakmanız gerektiğini netleştirmek lazım.

Yanıt Süresi Yüzdelikleri (Percentiles)

Ortalama yanıt süresine bakıp “iyi görünüyor” demek klasik bir tuzak. Diyelim ki p50 (medyan) 200ms, p95 ise 4500ms. Bu, kullanıcıların %5’inin 4.5 saniyeden fazla beklediği anlamına gelir. Bir e-ticaret sitesinde günde 100.000 işlem yapıyorsanız, bu 5.000 kötü kullanıcı deneyimi demek.

Bakmanız gereken yüzdelikler şunlar:

  • p50: Tipik kullanıcı deneyimi
  • p90: “Kötü şans” kullanıcı deneyimi
  • p95: SLA eşikleri için genellikle bu kullanılır
  • p99: Uç durumlar, genellikle altyapı sorunlarını gösterir
  • p99.9: Sadece kritik ödeme/güvenlik akışları için takip edin

Hata Oranı

%1 hata oranı masum görünür. Ama saniyede 500 istek gönderiyorsanız, bu saniyede 5 başarısız işlem demek. Hata oranını ham sayıyla birlikte değerlendirin.

Throughput (Verim)

Saniye başına kaç isteği başarıyla tamamladınız? Bu sayı, sistem yük altında ne kadar iş yapabildiğini gösterir. Yanıt süresi düşük ama throughput da düşükse, muhtemelen sistemin bir yerde tıkandığını gösterir.

k6 Sonuçlarını İşlemek

k6, varsayılan olarak terminale özet çıktı verir ama bu çıktıyı daha sonra işlemek için JSON formatına almanız gerekir.

# k6 testini JSON çıktısıyla çalıştır
k6 run --out json=test_results.json load_test.js

# Özet istatistikleri ayrı dosyaya al
k6 run --summary-export=summary.json load_test.js

# Her ikisini birden
k6 run 
  --out json=raw_metrics.json 
  --summary-export=summary.json 
  load_test.js

JSON çıktısı oldukça kalabalık olabilir. Sadece HTTP metriklerini filtrelemek için jq kullanabilirsiniz:

# p95 yanıt sürelerini filtrele
cat summary.json | jq '.metrics.http_req_duration.values'

# Hata oranını hesapla
cat summary.json | jq '
  .metrics.http_req_failed.values.rate * 100 |
  . * 100 | round / 100
' 

# Tüm önemli metrikleri temiz formatta çıkar
cat summary.json | jq '{
  "toplam_istek": .metrics.http_reqs.values.count,
  "hata_orani_yuzde": (.metrics.http_req_failed.values.rate * 100 | . * 100 | round / 100),
  "p50_ms": .metrics.http_req_duration.values["p(50)"],
  "p95_ms": .metrics.http_req_duration.values["p(95)"],
  "p99_ms": .metrics.http_req_duration.values["p(99)"],
  "ortalama_ms": .metrics.http_req_duration.values.avg,
  "rps": .metrics.http_reqs.values.rate
}'

Bunu bir CI pipeline’ına bağlamak istiyorsanız, threshold kontrolü de ekleyebilirsiniz:

#!/bin/bash
# k6_check.sh - CI için performans eşik kontrolü

SUMMARY_FILE="summary.json"
P95_THRESHOLD=500   # ms
ERROR_THRESHOLD=1   # yüzde

P95=$(cat $SUMMARY_FILE | jq '.metrics.http_req_duration.values["p(95)"]')
ERROR_RATE=$(cat $SUMMARY_FILE | jq '.metrics.http_req_failed.values.rate * 100')

echo "P95 Yanit Suresi: ${P95}ms (Esik: ${P95_THRESHOLD}ms)"
echo "Hata Orani: ${ERROR_RATE}% (Esik: ${ERROR_THRESHOLD}%)"

if (( $(echo "$P95 > $P95_THRESHOLD" | bc -l) )); then
  echo "BASARISIZ: P95 yanit suresi esigi asildi!"
  exit 1
fi

if (( $(echo "$ERROR_RATE > $ERROR_THRESHOLD" | bc -l) )); then
  echo "BASARISIZ: Hata orani esigi asildi!"
  exit 1
fi

echo "Tum esikler gecildi. Test basarili."
exit 0

Locust Sonuçlarını Analiz Etmek

Locust’un CSV çıktısı k6’ya göre daha kolay işlenebilir bir formatta gelir. Test bittikten sonra elimizde genellikle iki dosya olur: _stats.csv ve _stats_history.csv.

# Locust'u headless modda çalıştır ve CSV çıktısı al
locust -f locustfile.py 
  --headless 
  --users 100 
  --spawn-rate 10 
  --run-time 5m 
  --csv=test_results 
  --host=https://api.example.com

# Sonuç dosyalarını listele
ls -la test_results_*.csv
# test_results_stats.csv
# test_results_stats_history.csv
# test_results_failures.csv

stats_history.csv dosyası zaman serisi verisi içerir ve bottleneck’leri bulmak için çok değerlidir. Python ile basit bir analiz:

#!/usr/bin/env python3
# analyze_locust.py - Locust CSV analizci

import pandas as pd
import sys

def analyze_locust_results(stats_file, history_file):
    # Genel istatistikler
    stats = pd.read_csv(stats_file)
    
    # Sadece aggregate satiri degil, endpoint bazli goster
    endpoint_stats = stats[stats['Name'] != 'Aggregated'].copy()
    
    print("=== ENDPOINT BAZLI OZET ===")
    for _, row in endpoint_stats.iterrows():
        print(f"n{row['Method']} {row['Name']}")
        print(f"  Toplam Istek : {row['Request Count']}")
        print(f"  Basarisiz    : {row['Failure Count']}")
        print(f"  Hata Orani   : {row['Failure Count']/row['Request Count']*100:.2f}%")
        print(f"  Medyan (ms)  : {row['Median Response Time']}")
        print(f"  P95 (ms)     : {row['95%']}")
        print(f"  P99 (ms)     : {row['99%']}")
        print(f"  Max (ms)     : {row['Max Response Time']}")
    
    # Zaman serisi analizi - darbogazlari bul
    history = pd.read_csv(history_file)
    history['Timestamp'] = pd.to_datetime(history['Timestamp'], unit='s')
    
    # P95 en yuksek oldugu anlari bul
    top_slowdowns = history.nlargest(5, '95%')[['Timestamp', 'User count', '95%', 'Requests/s']]
    
    print("n=== EN YAVAŞ 5 AN ===")
    print(top_slowdowns.to_string(index=False))
    
    # Kullanici sayisi ile p95 korelasyonu
    corr = history['User count'].corr(history['95%'])
    print(f"nKullanici sayisi ile P95 korelasyonu: {corr:.3f}")
    if corr > 0.8:
        print("UYARI: Yuksek korelasyon - sistem kullanici sayisina duyarli!")

if __name__ == "__main__":
    analyze_locust_results(
        "test_results_stats.csv",
        "test_results_stats_history.csv"
    )

JMeter Sonuçlarını İşlemek

JMeter, JTL (XML veya CSV) formatında sonuç üretir. JTL dosyaları büyük testlerde gigabaytlara ulaşabilir, bu yüzden ham dosyayla çalışmak yerine önce özetini çıkarmak gerekir.

# JMeter'ı CLI modunda çalıştır
jmeter -n 
  -t test_plan.jmx 
  -l results.jtl 
  -e 
  -o ./html_report 
  -Jthreads=100 
  -Jrampup=60 
  -Jduration=300

# JTL dosyasından CSV özet çıkar (JMeter kurulu olmalı)
java -jar $JMETER_HOME/lib/ext/CMDRunner.jar 
  --tool Reporter 
  --generate-csv summary.csv 
  --input-jtl results.jtl 
  --plugin-type AggregateReport

# Alternatif: awk ile hızlı p95 hesapla (CSV JTL için)
# JTL başlığı: timeStamp,elapsed,label,responseCode,success,bytes,latency...
awk -F',' 'NR>1 {print $2}' results.jtl | 
  sort -n | 
  awk 'BEGIN{c=0} {a[c++]=$1} END{
    p95=int(c*0.95);
    p99=int(c*0.99);
    print "P95: " a[p95] "ms";
    print "P99: " a[p99] "ms";
    print "Toplam Istek: " c
  }'

Gerçek Dünya Senaryosu: Servis Yükseltmesi Öncesi Karşılaştırma

Geçen ay bir mikroservis mimarisinde authentication servisinin yeni versiyonunu prod’a alacaktık. Tek bir testin sonuçlarına bakmak yerine, eski ve yeni versiyonu aynı yük profiliyle test edip karşılaştırdık.

Strateji şuydu: Aynı k6 script’ini iki farklı ortama çalıştır, sonuçları karşılaştır. Basit ama etkili.

#!/bin/bash
# compare_versions.sh

SCRIPT="auth_test.js"
DURATION="5m"
VUS=50

echo "Eski versiyon testi basliyor..."
k6 run 
  --vus $VUS 
  --duration $DURATION 
  --summary-export=results_v1.json 
  -e BASE_URL=https://auth-v1.staging.example.com 
  $SCRIPT

sleep 30  # Sistem soğuması için bekle

echo "Yeni versiyon testi basliyor..."
k6 run 
  --vus $VUS 
  --duration $DURATION 
  --summary-export=results_v2.json 
  -e BASE_URL=https://auth-v2.staging.example.com 
  $SCRIPT

# Karşılaştır
echo "=== VERSIYON KARSILASTIRMASI ==="
for METRIC in "p(95)" "p(99)" "avg"; do
  V1=$(cat results_v1.json | jq ".metrics.http_req_duration.values["$METRIC"]")
  V2=$(cat results_v2.json | jq ".metrics.http_req_duration.values["$METRIC"]")
  DIFF=$(echo "scale=2; ($V2-$V1)/$V1*100" | bc)
  echo "$METRIC: v1=${V1}ms | v2=${V2}ms | Degisim: ${DIFF}%"
done

Bu scriptin çıktısı bize yeni versiyonun p95’de %23 iyileşme sağladığını, p99’da ise %8 kötüleştiğini gösterdi. Geliştiricilerle bu veriyi masaya getirdiğimizde, uç durumları handle eden bir kod bloğunda potansiyel sorun buldular ve deploy’u erteledik.

Raporlama: Kime, Ne Kadar Detay?

Performans testi raporu hazırlamanın en büyük hatası, tek bir rapor formatı kullanmaya çalışmak. Yazılım geliştirme ekibi ile CTO’nun aynı rapora ihtiyacı yok.

Teknik Ekip için Rapor İçeriği:

  • Her endpoint için p50/p90/p95/p99 değerleri
  • Zaman serisi grafikleri (yük arttıkça yanıt süresi nasıl değişiyor?)
  • Hata detayları ve stack trace’ler
  • Sistem kaynak kullanımı (CPU, RAM, network I/O)
  • Darboğaz tespiti ve öneriler

Yönetim için Rapor İçeriği:

  • SLA hedeflerine göre geçti/kaldı özeti
  • Risk değerlendirmesi (prod’a çıkabilir mi?)
  • Bir önceki test ile karşılaştırma (iyileşme/kötüleşme)
  • Eğer sorun varsa, çözüm için tahmini efor

Markdown formatında basit bir teknik rapor şablonu:

# Performans Test Raporu
**Test Tarihi:** 2024-01-15
**Test Süresi:** 10 dakika
**Maksimum VU:** 200
**Ortam:** Staging (prod ile birebir)

## Özet
| Kriter | Hedef | Sonuç | Durum |
|--------|-------|-------|-------|
| P95 Yanıt Süresi | < 500ms | 342ms | GEÇTİ |
| Hata Oranı | < 1% | 0.3% | GEÇTİ |
| Throughput | > 150 RPS | 187 RPS | GEÇTİ |

## Kritik Bulgular
- /api/checkout endpoint'i 180+ VU'da lineer olmayan yavaşlama gösteriyor
- Redis bağlantı havuzu 150 VU'da doluyor (max_connections: 100 yetersiz)

## Öneriler
1. Redis bağlantı havuzunu 200'e çıkar
2. /api/checkout için DB sorgu optimizasyonu gerekli

Otomasyon için HTML rapor oluşturucu:

#!/usr/bin/env python3
# generate_report.py

import json
from datetime import datetime

def generate_html_report(summary_file, output_file="perf_report.html"):
    with open(summary_file) as f:
        data = json.load(f)
    
    metrics = data['metrics']
    duration = data.get('http_req_duration', {})
    
    p95 = metrics['http_req_duration']['values'].get('p(95)', 0)
    p99 = metrics['http_req_duration']['values'].get('p(99)', 0)
    error_rate = metrics['http_req_failed']['values'].get('rate', 0) * 100
    rps = metrics['http_reqs']['values'].get('rate', 0)
    total = metrics['http_reqs']['values'].get('count', 0)
    
    status_p95 = "gecti" if p95 < 500 else "kaldi"
    status_err = "gecti" if error_rate < 1 else "kaldi"
    
    html = f"""<!DOCTYPE html>
<html lang="tr">
<head>
    <meta charset="UTF-8">
    <title>Performans Test Raporu - {datetime.now().strftime('%Y-%m-%d')}</title>
    <style>
        body {{ font-family: sans-serif; max-width: 900px; margin: 40px auto; }}
        .gecti {{ color: green; font-weight: bold; }}
        .kaldi {{ color: red; font-weight: bold; }}
        .metric {{ background: #f5f5f5; padding: 15px; margin: 10px 0; border-radius: 4px; }}
    </style>
</head>
<body>
    <h1>Performans Test Raporu</h1>
    <p>Tarih: {datetime.now().strftime('%Y-%m-%d %H:%M')}</p>
    
    <h2>Metrikler</h2>
    <div class="metric">
        <strong>P95 Yanit Suresi:</strong> {p95:.1f}ms 
        <span class="{status_p95}">({status_p95.upper()})</span>
    </div>
    <div class="metric">
        <strong>P99 Yanit Suresi:</strong> {p99:.1f}ms
    </div>
    <div class="metric">
        <strong>Hata Orani:</strong> {error_rate:.2f}%
        <span class="{status_err}">({status_err.upper()})</span>
    </div>
    <div class="metric">
        <strong>Throughput:</strong> {rps:.1f} istek/saniye
    </div>
    <div class="metric">
        <strong>Toplam Istek:</strong> {total:,}
    </div>
</body>
</html>"""
    
    with open(output_file, 'w', encoding='utf-8') as f:
        f.write(html)
    
    print(f"Rapor olusturuldu: {output_file}")

if __name__ == "__main__":
    generate_html_report("summary.json")

Yaygın Analiz Hataları

Yıllar içinde gördüğüm en sık yapılan hatalar:

  • Ortalamayla yanıltılmak: “Ortalama 150ms, harika!” deyip p99’un 8 saniye olduğunu görmezden gelmek. Ortalamayı referans metrik olarak kullanmayın.
  • Yeterince ısınmamak (warm-up): Uygulamaların JVM’i, connection pool’u, cache’i ısınması için zaman lazım. İlk 1-2 dakikanın verisi analize dahil edilmemeli.
  • Tek bir yük profili: Sadece “100 kullanıcı sabit” testi yeterli değil. Yük artışı (ramp-up), ani spike, yavaş yavaş düşüş senaryolarının hepsi farklı sorunları ortaya çıkarır.
  • Test ortamını prod’la karıştırmak: Staging’de 4 core’lu bir makineyle test edip prod’da 32 core’lu makineye scale etmeyi “test ettik” saymak yanlış. Oransal düşünün, mutlak değil.
  • Network latency’yi ihmal etmek: Test aracını aynı datacenter’da mı çalıştırıyorsunuz, yoksa gerçek kullanıcı konumlarından mı? Bu fark, özellikle TLS handshake sürelerinde ciddi yanıltıcı sonuçlar doğurur.

Sonuç

Performans testi sonuçlarını analiz etmek, ham sayıları bir tabloya koymaktan çok daha fazlası. Doğru metriklere odaklanmak (yüzdelikler, hata oranı, throughput), zaman serisi verilerini okumak, darboğazları tespit etmek ve tüm bunları farklı kitlelere anlaşılır biçimde sunmak ayrı bir disiplin gerektiriyor.

k6, Locust ve JMeter’ın her biri farklı güçlü yanlarla gelir ama analiz mantığı temelde aynı: ham veriyi işle, eşiklerle karşılaştır, trend’leri gör, karar ver. Bunu otomasyon pipelines’ına gömerseniz, her deployment öncesinde güvenilir bir güvenlik ağınız olur.

En önemli tavsiye: sonuçları asla tek başına değerlendirmeyin. Aynı testleri farklı koşullarda, farklı zamanlarda çalıştırın ve trendlere bakın. Tek bir test size “şu an iyi” der; serisi size “gitgide kötüleşiyor mu” sorusunu cevaplar.

Bir yanıt yazın

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