Jenkins ile Performans Test Pipeline Kurulumu ve Yapılandırması
Performans testleri çoğu zaman “birileri arada bir çalıştırır” kategorisinde kalır. Proje teslim aşamasına geldiğinde panikle başlatılan JMeter scriptleri, kimsenin tam olarak anlamadığı raporlar ve “bence sisteme bir şeyler oldu” yorumlarıyla dolu toplantılar. Jenkins pipeline’ına entegre edilmiş düzgün bir performans test süreci bu kaosa son verebilir. Her commit’te ya da her gece otomatik çalışan, eşik değerlerini aşınca build’i kıran ve güzel raporlar üreten bir yapı kurmak aslında sanıldığı kadar karmaşık değil.
Neden Performans Testi CI/CD’ye Girmelidir
Çoğu ekip performans testini ayrı bir süreç olarak ele alır. “Fonksiyonel testler geçti, şimdi performansa bakalım” mantığı, problemi bulduğunuzda genellikle çok geç kalmış olduğunuz anlamına gelir. Kod tabanında yapılan küçük bir refactoring, yanlış yazılmış bir SQL sorgusu veya eklenen yeni bir dependency response time’ınızı iki katına çıkarabilir. Bunu iki sprint sonra değil, commit anında görmek istiyorsunuz.
Jenkins ile performans test pipeline’ı kurduğunuzda şu avantajları elde edersiniz:
- Regresyon tespiti: Her build’de baseline ile karşılaştırma yapılır
- Otomatik eşik kontrolü: Response time, error rate veya throughput sınırlarını aşan build’ler otomatik başarısız olur
- Tarihsel veri: Aylarca geriye dönük performans trendi görülebilir
- Geliştirici farkındalığı: “Benim kodumdan kaynaklanmıyor” tartışmalarının sonu gelir
Ortam Gereksinimleri
Başlamadan önce Jenkins sunucunuzun ve test ortamınızın hazır olması gerekiyor. Ben bu yazıda JMeter kullanacağım çünkü en yaygın araç ve Jenkins entegrasyonu güçlü. Gatling veya k6 kullanıyorsanız konsept aynı, sadece komutlar değişiyor.
Jenkins sunucusuna JMeter’ı kuralım:
# Jenkins sunucusunda JMeter kurulumu
JMETER_VERSION="5.6.3"
cd /opt
wget https://downloads.apache.org/jmeter/binaries/apache-jmeter-${JMETER_VERSION}.tgz
tar -xzf apache-jmeter-${JMETER_VERSION}.tgz
ln -s /opt/apache-jmeter-${JMETER_VERSION} /opt/jmeter
# PATH'e ekleyelim
echo 'export JMETER_HOME=/opt/jmeter' >> /etc/profile.d/jmeter.sh
echo 'export PATH=$PATH:$JMETER_HOME/bin' >> /etc/profile.d/jmeter.sh
source /etc/profile.d/jmeter.sh
# Doğrulama
jmeter --version
Jenkins’te Performance Plugin yükleyin. Jenkins yönetim panelinden Manage Jenkins > Plugins > Available kısmında “Performance” ve “HTML Publisher” plugin’lerini arayıp kurun. Bu plugin’ler JMeter JTL dosyalarını parse ederek güzel grafikler üretir.
Proje Yapısı ve JMeter Test Dosyası
Repository yapınızı şöyle organize edin:
project-root/
├── src/
├── tests/
│ ├── performance/
│ │ ├── test-plans/
│ │ │ ├── api-load-test.jmx
│ │ │ └── smoke-performance.jmx
│ │ ├── config/
│ │ │ ├── dev.properties
│ │ │ ├── staging.properties
│ │ │ └── prod-readonly.properties
│ │ └── scripts/
│ │ ├── run-perf-tests.sh
│ │ └── analyze-results.py
├── Jenkinsfile
└── Jenkinsfile.performance
Her ortam için ayrı properties dosyası tutmak kritik. staging.properties örneği:
# tests/performance/config/staging.properties
TARGET_HOST=staging-api.company.com
TARGET_PORT=443
PROTOCOL=https
THREAD_COUNT=50
RAMP_UP_SECONDS=60
TEST_DURATION=300
THINK_TIME=500
# Eşik değerleri
MAX_RESPONSE_TIME_MS=2000
MAX_ERROR_RATE_PERCENT=1
MIN_THROUGHPUT_RPS=100
Temel Jenkins Pipeline Yapısı
İki aşamalı bir yaklaşım benimsiyorum: Her PR’da hafif smoke performans testi, her gece tam yük testi. Bu yaklaşım hem hız hem de kapsamlı test arasında denge sağlar.
Jenkinsfile.performance dosyası:
pipeline {
agent any
parameters {
choice(
name: 'TEST_ENVIRONMENT',
choices: ['staging', 'dev'],
description: 'Test ortami secin'
)
choice(
name: 'TEST_TYPE',
choices: ['smoke', 'load', 'stress', 'soak'],
description: 'Test tipi'
)
string(
name: 'THREAD_COUNT',
defaultValue: '50',
description: 'Concurrent kullanici sayisi (load testi icin)'
)
}
environment {
JMETER_HOME = '/opt/jmeter'
TEST_RESULTS_DIR = "${WORKSPACE}/performance-results"
PROPERTIES_FILE = "${WORKSPACE}/tests/performance/config/${params.TEST_ENVIRONMENT}.properties"
}
stages {
stage('Hazirlik') {
steps {
sh 'mkdir -p ${TEST_RESULTS_DIR}'
sh 'mkdir -p ${TEST_RESULTS_DIR}/html-report'
// Hedef ortamın ayakta olup olmadığını kontrol et
script {
def props = readProperties file: "${PROPERTIES_FILE}"
def targetHost = props['TARGET_HOST']
def checkResult = sh(
script: "curl -s -o /dev/null -w '%{http_code}' https://${targetHost}/health",
returnStdout: true
).trim()
if (checkResult != '200') {
error "Hedef ortam hazir degil! HTTP Status: ${checkResult}"
}
echo "Hedef ortam hazir: ${targetHost}"
}
}
}
stage('Performans Testi') {
steps {
script {
def testPlan = ''
switch(params.TEST_TYPE) {
case 'smoke':
testPlan = 'smoke-performance.jmx'
break
case 'load':
case 'stress':
testPlan = 'api-load-test.jmx'
break
case 'soak':
testPlan = 'soak-test.jmx'
break
}
sh """
${JMETER_HOME}/bin/jmeter \
-n \
-t ${WORKSPACE}/tests/performance/test-plans/${testPlan} \
-p ${PROPERTIES_FILE} \
-Jthreads=${params.THREAD_COUNT} \
-l ${TEST_RESULTS_DIR}/results.jtl \
-e \
-o ${TEST_RESULTS_DIR}/html-report \
-j ${TEST_RESULTS_DIR}/jmeter.log
"""
}
}
}
stage('Sonuc Analizi') {
steps {
script {
// Python script ile eşik kontrolü
def analysisResult = sh(
script: """
python3 ${WORKSPACE}/tests/performance/scripts/analyze-results.py \
--jtl ${TEST_RESULTS_DIR}/results.jtl \
--props ${PROPERTIES_FILE}
""",
returnStatus: true
)
if (analysisResult != 0) {
currentBuild.result = 'UNSTABLE'
echo "Performans esik degerleri asildi! Detaylar icin raporu inceleyin."
}
}
}
}
stage('Raporlama') {
steps {
// JMeter HTML raporunu yayinla
publishHTML(target: [
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: "${TEST_RESULTS_DIR}/html-report",
reportFiles: 'index.html',
reportName: 'JMeter Performans Raporu'
])
// Performance plugin ile grafik
perfReport(
sourceDataFiles: "${TEST_RESULTS_DIR}/results.jtl",
errorFailedThreshold: 1,
errorUnstableThreshold: 0.5,
relativeFailedThresholdPositive: 20,
relativeUnstableThresholdPositive: 10
)
// Artifact olarak sakla
archiveArtifacts artifacts: "performance-results/**/*", allowEmptyArchive: true
}
}
}
post {
always {
script {
def buildColor = currentBuild.result == 'SUCCESS' ? 'good' :
currentBuild.result == 'UNSTABLE' ? 'warning' : 'danger'
slackSend(
color: buildColor,
message: """
Performans Test Sonucu: ${currentBuild.result}
Ortam: ${params.TEST_ENVIRONMENT}
Test Tipi: ${params.TEST_TYPE}
Rapor: ${BUILD_URL}JMeter_Performans_Raporu/
""".stripIndent()
)
}
}
failure {
emailext(
subject: "KRITIK: Performans Testi Basarisiz - Build #${BUILD_NUMBER}",
body: "Performans testi basarisiz oldu. Jenkins: ${BUILD_URL}",
to: "[email protected]"
)
}
}
}
Sonuç Analiz Script’i
Bu Python script’i JTL dosyasını okur ve properties dosyasındaki eşik değerleriyle karşılaştırır. CI ortamında olmadan da lokal çalıştırabilirsiniz:
#!/usr/bin/env python3
# tests/performance/scripts/analyze-results.py
import csv
import sys
import argparse
import statistics
from pathlib import Path
def load_properties(props_file):
props = {}
with open(props_file, 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
if '=' in line:
key, value = line.split('=', 1)
props[key.strip()] = value.strip()
return props
def analyze_jtl(jtl_file, props):
response_times = []
error_count = 0
total_count = 0
with open(jtl_file, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
total_count += 1
response_times.append(int(row['elapsed']))
if row['success'].lower() == 'false':
error_count += 1
if total_count == 0:
print("HATA: Hic test sonucu bulunamadi!")
sys.exit(1)
avg_response_time = statistics.mean(response_times)
p95_response_time = sorted(response_times)[int(len(response_times) * 0.95)]
p99_response_time = sorted(response_times)[int(len(response_times) * 0.99)]
error_rate = (error_count / total_count) * 100
max_rt = int(props.get('MAX_RESPONSE_TIME_MS', 2000))
max_error_rate = float(props.get('MAX_ERROR_RATE_PERCENT', 1))
print(f"=== Performans Test Sonuclari ===")
print(f"Toplam Istek: {total_count}")
print(f"Hata Sayisi: {error_count}")
print(f"Hata Orani: {error_rate:.2f}%")
print(f"Ortalama Response Time: {avg_response_time:.0f}ms")
print(f"P95 Response Time: {p95_response_time}ms")
print(f"P99 Response Time: {p99_response_time}ms")
print(f"n=== Esik Kontrolu ===")
failed = False
if p95_response_time > max_rt:
print(f"BASARISIZ: P95 response time {p95_response_time}ms > {max_rt}ms esigi")
failed = True
else:
print(f"GECTI: P95 response time {p95_response_time}ms <= {max_rt}ms esigi")
if error_rate > max_error_rate:
print(f"BASARISIZ: Hata orani {error_rate:.2f}% > %{max_error_rate} esigi")
failed = True
else:
print(f"GECTI: Hata orani {error_rate:.2f}% <= %{max_error_rate} esigi")
return 1 if failed else 0
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--jtl', required=True)
parser.add_argument('--props', required=True)
args = parser.parse_args()
props = load_properties(args.props)
exit_code = analyze_jtl(args.jtl, props)
sys.exit(exit_code)
Nightly Full Load Test için Zamanlanmış Pipeline
Gündüz PR smoke testleri yeterli olmaz. Geceleri tam yük testi çalışmalı:
// Ana Jenkinsfile içine eklenecek trigger tanımı
triggers {
// Her gece saat 02:00'de çalış
cron('0 2 * * *')
// Her PR'da smoke test tetikle
githubPush()
}
Zamanlanmış nightly test için ayrı bir job tanımı:
pipeline {
agent { label 'performance-agent' }
triggers {
cron('0 2 * * 1-5') // Hafta içi her gece
}
environment {
BASELINE_BUILD = 'lastSuccessfulBuild'
REGRESSION_THRESHOLD = '15' // %15 yavaşlama kabul edilemez
}
stages {
stage('Baseline Karsilastirma') {
steps {
script {
// Bir onceki basarili build'in sonuclarini indir
def lastBuildUrl = "${JENKINS_URL}job/perf-tests/${BASELINE_BUILD}/artifact/performance-results/summary.json"
def baselineExists = sh(
script: "curl -s -f '${lastBuildUrl}' -o ${WORKSPACE}/baseline.json",
returnStatus: true
) == 0
env.HAS_BASELINE = baselineExists.toString()
if (baselineExists) {
echo "Baseline bulundu, regresyon analizi yapilacak"
} else {
echo "Ilk calistirma, baseline olusturulacak"
}
}
}
}
stage('Tam Yuk Testi') {
steps {
sh """
/opt/jmeter/bin/jmeter \
-n \
-t ${WORKSPACE}/tests/performance/test-plans/api-load-test.jmx \
-p ${WORKSPACE}/tests/performance/config/staging.properties \
-Jthreads=200 \
-Jduration=1800 \
-l ${WORKSPACE}/performance-results/nightly-results.jtl \
-e \
-o ${WORKSPACE}/performance-results/nightly-html \
-j ${WORKSPACE}/performance-results/nightly-jmeter.log
"""
}
}
stage('Regresyon Analizi') {
when {
environment name: 'HAS_BASELINE', value: 'true'
}
steps {
sh """
python3 ${WORKSPACE}/tests/performance/scripts/regression-check.py \
--current ${WORKSPACE}/performance-results/nightly-results.jtl \
--baseline ${WORKSPACE}/baseline.json \
--threshold ${REGRESSION_THRESHOLD}
"""
}
}
}
}
Dağıtık Yük Testi için Jenkins + JMeter Remote
Tek Jenkins agent’tan 500+ concurrent user simüle etmek zorlaşır. JMeter remote modunu Jenkins ile birleştirebilirsiniz:
# JMeter remote agent başlatma scripti
# /opt/jmeter/bin/start-remote-agent.sh
#!/bin/bash
JMETER_HOME=/opt/jmeter
RMI_HOST=$(hostname -I | awk '{print $1}')
# RMI port ayarları
export RMI_HOST_DEF="-Djava.rmi.server.hostname=${RMI_HOST}"
${JMETER_HOME}/bin/jmeter-server
-Djava.rmi.server.hostname=${RMI_HOST}
-Dserver.rmi.localport=4000
-Dserver_port=1099 &
echo "JMeter remote agent baslatildi: ${RMI_HOST}:1099"
Jenkins pipeline’ında remote agent’ları kullanmak:
stage('Dagitik Yuk Testi') {
steps {
script {
def remoteAgents = [
'load-gen-01.internal:1099',
'load-gen-02.internal:1099',
'load-gen-03.internal:1099'
].join(',')
sh """
/opt/jmeter/bin/jmeter \
-n \
-t ${WORKSPACE}/tests/performance/test-plans/api-load-test.jmx \
-R ${remoteAgents} \
-Jclient.rmi.localport=5000 \
-l ${WORKSPACE}/performance-results/distributed-results.jtl \
-e \
-o ${WORKSPACE}/performance-results/distributed-html
"""
}
}
}
Gerçek Dünya Senaryosu: E-Ticaret API’si
Bir e-ticaret projesinde yaşanan gerçek bir durumu aktarayım. Ödeme servisi önce 200ms’de yanıt verirken, bir sprint sonrası 800ms’e çıkmıştı. Kimse fark etmemiş, monitoring sistemindeki alert eşiği 2000ms olarak ayarlanmıştı.
JMeter test planında kritik endpoint’leri şöyle organize ettik:
# JMeter test planını komut satırından parametrize çalıştırma
# Farklı senaryolar için tek komut
# Smoke test: 10 user, 60 saniye
/opt/jmeter/bin/jmeter -n
-t tests/performance/test-plans/ecommerce-flow.jmx
-Jhost=staging-api.ecommerce.com
-Jthreads=10
-Jrampup=10
-Jduration=60
-l results/smoke-$(date +%Y%m%d-%H%M%S).jtl
# Peak load simulation: Black Friday senaryosu
/opt/jmeter/bin/jmeter -n
-t tests/performance/test-plans/ecommerce-flow.jmx
-Jhost=staging-api.ecommerce.com
-Jthreads=500
-Jrampup=120
-Jduration=600
-Jpayment_threads=50
-Jsearch_threads=300
-Jcheckout_threads=150
-l results/blackfriday-$(date +%Y%m%d-%H%M%S).jtl
Bu yapıyı kurduğumuzda ödeme servisindeki yavaşlamayı o sprint’in ilk commit’inde yakaladık. Geliştirici N+1 query problemi yaratmıştı, pipeline başarısız olunca direkt göründü.
Pipeline Optimizasyonu İpuçları
Uzun süre çalıştırdıktan sonra öğrendiklerim:
- Test verilerini dinamik üretin: Her test çalıştığında aynı kullanıcı adlarını kullanmayın, test veritabanı kirlenir. JMeter CSV Data Set Config ile her run için unique data üretin.
- Staging ortamını izole edin: Performans testi sırasında başka deployment yaşandıysa sonuçlar anlamsız olur. Pipeline başında ortam lock mekanizması ekleyin.
- JTL dosyalarını arşivleyin, HTML raporları değil: HTML raporlar workspace’i şişirir. JTL dosyası küçük ve üzerinden her zaman yeniden rapor üretebilirsiniz.
- Log level’ı düşürün:
jmeter.logdosyası saat süren testlerde GB’larca büyüyebilir. Properties dosyasınalog_level.jmeter=WARNekleyin. - Ön ısınma süresi bırakın: JVM warm-up için ilk 30 saniyeyi hesaba katmayın. JMeter’ın “Scheduler” özelliğiyle delay başlatma yapabilirsiniz.
Sonuç
Jenkins ile performans test pipeline’ı kurmak başlangıçta iş yükü gibi görünür. Properties dosyaları, JMeter scriptleri, Python analiz kodları, Groovy pipeline tanımları… Ama bir kere çalışır hale getirdiğinizde kazanç çok büyük. Her sabah ofise geldiğinizde geceki testin raporunu açıp “dün gece ne bozuldu” sorusuna cevap bulabilmek, production’da müşteri şikayetiyle öğrenmekten çok daha iyi.
Başlangıç için küçük tutun. Tek bir kritik endpoint, iki eşik değeri, basit bir pipeline. Sistem çalışmaya başlayınca ekleyebileceğiniz çok şey var. Ama mükemmel pipeline için beklemeyin, çünkü o bekleme sürecinde kaçırdığınız her performans regresyonu maliyete dönüşüyor. Bugün çalışan basit bir şey, yarın çalışacak mükemmel bir şeyden değerlidir.
