Gatling ile Yüksek Eş Zamanlılık Performans Testi

Üretim ortamında beklenmedik bir trafik dalgasıyla karşılaştığınızda, sisteminizin gerçekte ne kadar dayanıklı olduğunu anlamak için saatlerinizi loglara bakarak geçirmenin ne kadar can sıkıcı olduğunu biliyorum. İşte bu yüzden yük testleri var. Ve bu işi ciddiye alıyorsanız, Gatling’i mutlaka tanımanız gerekiyor.

Gatling, Scala tabanlı bir yük testi aracı. JMeter’ın XML cehennemiyle boğuşanlar ya da Locust’un Python GIL sorunlarıyla tatminsiz kalanlar için ciddi bir alternatif. Özellikle yüksek eş zamanlılık senaryolarında, yani on binlerce eş zamanlı kullanıcıyı simüle etmeniz gerektiğinde, Gatling’in Akka ve Netty tabanlı mimarisi sizi kurtarıyor. JVM üzerinde çalışmasına rağmen, thread-per-user modelini kullanmıyor; bunun yerine asenkron, olay güdümlü bir yaklaşım benimsiyor. Bu da tek bir makineden ciddi miktarda sanal kullanıcı oluşturabildiğiniz anlamına geliyor.

Neden Gatling?

Bu soruyu sormak önemli çünkü araç seçimi gerçekten iş akışınızı etkiliyor. k6’yı seviyorum, JavaScript/TypeScript ile yazıyorsunuz, CI/CD entegrasyonu kolay. Locust’un Python’ı esnek. Ama ikisi de gerçek anlamda yüksek eş zamanlılıkta Gatling kadar verimli kaynak kullanmıyor.

Gatling’in öne çıktığı durumlar şunlar:

  • 100K+ eş zamanlı kullanıcı simülasyonu gereken senaryolar
  • Karmaşık HTTP oturum yönetimi ve dinamik parametre ihtiyacı
  • Detaylı HTML raporlaması gereken ekipler (Gatling raporları gerçekten etkileyici)
  • CI/CD pipeline’ına entegre edilecek Maven/Gradle tabanlı projeler
  • WebSocket ve SSE (Server-Sent Events) test ihtiyaçları

Kurulum ve İlk Adımlar

Gatling’i iki şekilde kullanabilirsiniz: standalone bundle olarak ya da Maven/Gradle projesi içinde. Ben üretim ortamları için Maven entegrasyonunu tercih ediyorum çünkü versiyon kontrolü ve CI/CD entegrasyonu çok daha temiz oluyor.

Standalone kurulum için:

# Java 11+ gerekli, kontrol edin
java -version

# Gatling bundle indirin (https://gatling.io/open-source)
wget https://repo1.maven.org/maven2/io/gatling/highcharts/gatling-charts-highcharts-bundle/3.10.3/gatling-charts-highcharts-bundle-3.10.3-bundle.zip

unzip gatling-charts-highcharts-bundle-3.10.3-bundle.zip
cd gatling-charts-highcharts-bundle-3.10.3

# İlk testi çalıştırın
./bin/gatling.sh

Maven projesi için pom.xml‘inize şunu ekleyin:

# Proje yapısı oluşturun
mvn archetype:generate 
  -DgroupId=com.sirketiniz.performans 
  -DartifactId=gatling-testleri 
  -DarchetypeArtifactId=maven-archetype-quickstart 
  -DinteractiveMode=false

# pom.xml'e Gatling plugin ekleyin (aşağıdaki içeriği kullanın)
cat > pom.xml << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.sirketiniz.performans</groupId>
  <artifactId>gatling-testleri</artifactId>
  <version>1.0</version>
  <properties>
    <gatling.version>3.10.3</gatling.version>
    <scala.version>2.13.12</scala.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>io.gatling.highcharts</groupId>
      <artifactId>gatling-charts-highcharts</artifactId>
      <version>${gatling.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>
EOF

İlk Simülasyon: Temel Yapı

Gatling’de her şey Simulation sınıfından türüyor. Scala sözdizimi ilk başta yabancı gelebilir ama bir kez alıştıktan sonra çok okunabilir bir DSL sunuyor.

// src/test/scala/simulations/BasitApiTesti.scala
package simulations

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._

class BasitApiTesti extends Simulation {

  // HTTP protokol konfigürasyonu
  val httpProtocol = http
    .baseUrl("https://api.sirketiniz.com")
    .acceptHeader("application/json")
    .contentTypeHeader("application/json")
    .userAgentHeader("Gatling-PerformansTest/1.0")
    .header("X-Test-Run", "true")

  // Senaryo tanımı
  val senaryo = scenario("Ürün Listesi Senaryosu")
    .exec(
      http("Ürün Listesi Al")
        .get("/api/v1/urunler")
        .queryParam("sayfa", "1")
        .queryParam("limit", "20")
        .check(status.is(200))
        .check(jsonPath("$.data").exists)
        .check(responseTimeInMillis.lte(2000))
    )
    .pause(1, 3) // 1-3 saniye arası bekleme

  // Yük profili
  setUp(
    senaryo.inject(
      atOnceUsers(10),              // Anında 10 kullanıcı
      rampUsers(100).during(30.seconds),  // 30 sn içinde 100'e çık
      constantUsersPerSec(50).during(2.minutes)  // 2 dk boyunca sabit 50/sn
    )
  ).protocols(httpProtocol)
   .assertions(
     global.responseTime.percentile(95).lt(1500),
     global.successfulRequests.percent.gt(99)
   )
}

Bu yapıyla çalıştırdığınızda Gatling size güzel bir HTML raporu üretiyor. Raporun içinde response time dağılımı, percentile’lar, başarısız istek oranları ve zaman serisi grafikleri oluyor.

Gerçek Dünya Senaryosu: E-ticaret Yük Testi

Şimdi işin gerçek kısmına gelelim. Bir e-ticaret sitesi için gerçekçi kullanıcı davranışı simüle etmek istiyorsunuz: arama yapma, ürün detayı görme, sepete ekleme, ödeme başlatma. Her adımın farklı ağırlıkları var.

// src/test/scala/simulations/EticaretSimulasyonu.scala
package simulations

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._

class EticaretSimulasyonu extends Simulation {

  val httpProtocol = http
    .baseUrl("https://www.magazaniz.com")
    .acceptHeader("application/json, text/html")
    .acceptLanguageHeader("tr-TR,tr;q=0.9")
    .acceptEncodingHeader("gzip, deflate, br")
    .userAgentHeader("Mozilla/5.0 (compatible; GatlingTest)")
    .disableFollowRedirect // Redirect'leri manuel yönet

  // CSV'den kullanıcı verisi yükle
  val kullaniciler = csv("kullanici_verileri.csv").circular

  // Oturum açma senaryosu
  val girisYap = exec(
    http("Login Sayfası")
      .get("/giris")
      .check(status.is(200))
      .check(css("input[name='_csrf']", "value").saveAs("csrfToken"))
  )
  .pause(2, 5)
  .feed(kullaniciler)
  .exec(
    http("Login İsteği")
      .post("/giris")
      .formParam("email", "#{email}")
      .formParam("sifre", "#{sifre}")
      .formParam("_csrf", "#{csrfToken}")
      .check(status.is(302))
      .check(header("Location").saveAs("yonlendirme"))
  )

  // Ürün arama senaryosu
  val urunAra = exec(
    http("Arama Yap")
      .get("/arama")
      .queryParam("q", "laptop")
      .queryParam("kategori", "elektronik")
      .check(status.is(200))
      .check(jsonPath("$.sonuclar[0].id").saveAs("ilkUrunId"))
  )
  .pause(1, 3)
  .exec(
    http("Ürün Detayı")
      .get("/urun/#{ilkUrunId}")
      .check(status.is(200))
      .check(jsonPath("$.stok").ofType[Int].gt(0))
  )

  // Sepet işlemleri
  val sepeteEkle = exec(
    http("Sepete Ekle")
      .post("/sepet/ekle")
      .body(StringBody("""{"urunId": "#{ilkUrunId}", "adet": 1}"""))
      .asJson
      .check(status.is(200))
      .check(jsonPath("$.sepetToplamı").saveAs("sepetToplam"))
  )

  // Ana senaryo zincirleme
  val anaSenaryo = scenario("E-ticaret Kullanıcı Akışı")
    .exec(girisYap)
    .pause(2)
    .repeat(3) {
      exec(urunAra)
      .pause(2, 5)
      .exec(sepeteEkle)
      .pause(1, 3)
    }

  setUp(
    anaSenaryo.inject(
      nothingFor(5.seconds),
      rampUsers(500).during(5.minutes),
      constantUsersPerSec(100).during(10.minutes),
      rampUsersPerSec(100).to(0).during(2.minutes)
    )
  ).protocols(httpProtocol)
   .assertions(
     global.responseTime.percentile(99).lt(3000),
     global.responseTime.mean.lt(800),
     global.failedRequests.percent.lt(1),
     forAll.responseTime.percentile(95).lt(2000)
   )
}

Bu simülasyonda dikkat edilmesi gereken birkaç nokta var. circular ile CSV datasını döngüsel kullanıyoruz, yoksa kullanıcı sayısı veri sayısını aşınca test çöküyor. CSRF token’ı CSS selector ile çekip session’a kaydediyoruz. Response’dan dinamik değer alıp sonraki isteğe besliyoruz.

Feeder ile Veri Yönetimi

Gerçekçi test verisi üretmek kritik. Aynı parametrelerle yapılan istekler cache’den dönüyor ve gerçek performansı ölçemiyorsunuz.

# Test verisi CSV dosyası oluşturun
cat > src/test/resources/kullanici_verileri.csv << 'EOF'
email,sifre,kullanici_id
[email protected],Test123!,1001
[email protected],Test123!,1002
[email protected],Test123!,1003
EOF

# Büyük veri seti için Python ile üretin
python3 << 'PYEOF'
import csv
import random

with open('src/test/resources/urun_aramalari.csv', 'w', newline='') as f:
    writer = csv.writer(f)
    writer.writerow(['arama_terimi', 'kategori', 'min_fiyat', 'max_fiyat'])
    
    terimler = ['laptop', 'telefon', 'tablet', 'kulaklık', 'klavye', 'mouse']
    kategoriler = ['elektronik', 'bilgisayar', 'aksesuar', 'ses-sistemleri']
    
    for i in range(10000):
        writer.writerow([
            random.choice(terimler),
            random.choice(kategoriler),
            random.randint(100, 1000),
            random.randint(1000, 50000)
        ])

print("Veri seti oluşturuldu: 10000 kayıt")
PYEOF

Scala tarafında bu CSV’yi şöyle kullanıyorsunuz:

// Farklı feeder stratejileri
val siraliFeeder = csv("kullanici_verileri.csv").queue      // Sıralı, veri bitince test durur
val donguselFeeder = csv("kullanici_verileri.csv").circular  // Döngüsel
val rastgeleFeeder = csv("kullanici_verileri.csv").random    // Rastgele

// Programatik feeder - dışarıdan veri çekme
val dinamikFeeder = Iterator.continually(Map(
  "urunId" -> (math.random() * 10000).toInt.toString,
  "adet" -> (math.random() * 5 + 1).toInt.toString,
  "zaman" -> System.currentTimeMillis().toString
))

WebSocket Testi: Gerçek Zamanlı Uygulamalar

Chat uygulamaları, anlık bildirimler, canlı veri akışı… WebSocket testleri ayrı bir dünya. Gatling burada da güçlü.

// src/test/scala/simulations/WebSocketTesti.scala
package simulations

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._

class WebSocketTesti extends Simulation {

  val httpProtocol = http
    .baseUrl("https://realtime.uygulamaniz.com")
    .wsBaseUrl("wss://realtime.uygulamaniz.com")

  val wsSenaryo = scenario("WebSocket Gerçek Zamanlı Test")
    .exec(
      http("HTTP Auth")
        .post("/auth/token")
        .body(StringBody("""{"kullanici": "wstest", "sifre": "gizli123"}"""))
        .asJson
        .check(jsonPath("$.token").saveAs("authToken"))
    )
    .exec(
      ws("WebSocket Bağlantısı Kur")
        .connect("/ws/canli?token=#{authToken}")
        .await(10.seconds)(
          ws.checkTextMessage("Bağlantı Onayı")
            .matching(jsonPath("$.tip").is("BAGLANDI"))
            .check(jsonPath("$.kullaniciId").saveAs("wsKullaniciId"))
        )
    )
    .repeat(20) {
      exec(
        ws("Mesaj Gönder")
          .sendText("""{"tip": "MESAJ", "icerik": "Merhaba dünya", "oda": "genel"}""")
      )
      .exec(
        ws("Mesaj Yanıtı Bekle")
          .await(5.seconds)(
            ws.checkTextMessage("Yanıt Kontrol")
              .matching(jsonPath("$.tip").is("MESAJ_ALINDI"))
          )
      )
      .pause(500.milliseconds, 2.seconds)
    }
    .exec(ws("WebSocket Kapat").close)

  setUp(
    wsSenaryo.inject(
      rampUsers(1000).during(2.minutes),
      constantUsersPerSec(50).during(5.minutes)
    )
  ).protocols(httpProtocol)
}

CI/CD Entegrasyonu: Jenkins ve GitLab CI

Gatling’i pipeline’a entegre etmek çok değerli çünkü her deploy sonrası otomatik performans regresyon testi yapabiliyorsunuz.

# Jenkins için Jenkinsfile
cat > Jenkinsfile << 'EOF'
pipeline {
    agent {
        docker {
            image 'maven:3.9-eclipse-temurin-17'
            args '-v $HOME/.m2:/root/.m2'
        }
    }
    
    environment {
        HEDEF_ORTAM = "${params.ORTAM ?: 'staging'}"
        BASE_URL = "${params.ORTAM == 'prod' ? 'https://api.sirket.com' : 'https://staging-api.sirket.com'}"
    }
    
    stages {
        stage('Performans Testi') {
            steps {
                sh """
                    mvn gatling:test 
                        -Dgatling.simulationClass=simulations.EticaretSimulasyonu 
                        -DbaseUrl=${BASE_URL} 
                        -Dortam=${HEDEF_ORTAM}
                """
            }
            post {
                always {
                    gatlingArchive()
                    publishHTML([
                        allowMissing: false,
                        alwaysLinkToLastBuild: true,
                        keepAll: true,
                        reportDir: 'target/gatling',
                        reportFiles: '*/index.html',
                        reportName: 'Gatling Performans Raporu'
                    ])
                }
                failure {
                    emailext(
                        subject: "Performans Testi BAŞARISIZ - ${env.JOB_NAME}",
                        body: "Assertion'lar karşılanmadı. Rapor: ${env.BUILD_URL}",
                        to: '[email protected]'
                    )
                }
            }
        }
    }
}
EOF

# GitLab CI için
cat > .gitlab-ci.yml << 'EOF'
performans-testi:
  image: maven:3.9-eclipse-temurin-17
  stage: test
  only:
    - main
    - /^release/.*/
  script:
    - mvn gatling:test -Dgatling.simulationClass=simulations.EticaretSimulasyonu
  artifacts:
    when: always
    paths:
      - target/gatling/
    expire_in: 30 days
    reports:
      junit: target/gatling/**/simulation.log
  variables:
    MAVEN_OPTS: "-Xmx2g -XX:+UseG1GC"
EOF

Dağıtık Yük Testi: Cluster Kurulumu

Tek makineden 100K+ kullanıcı simüle etmek bazen yeterli olmuyor. Gatling Enterprise bu iş için var ama open source versiyonuyla da cluster kurabilirsiniz.

#!/bin/bash
# Çoklu Gatling node'u koordine etme scripti
# Her node'da aynı simülasyonu parçalı çalıştırıp sonuçları birleştiriyoruz

NODLAR=("node1.test.sirket.com" "node2.test.sirket.com" "node3.test.sirket.com")
SIMULASYON="simulations.EticaretSimulasyonu"
TOPLAM_KULLANICI=30000
NODE_BASI_KULLANICI=$((TOPLAM_KULLANICI / ${#NODLAR[@]}))

echo "Dağıtık test başlatılıyor..."
echo "Toplam kullanıcı: $TOPLAM_KULLANICI"
echo "Node başı kullanıcı: $NODE_BASI_KULLANICI"

# Her node'a simülasyonu gönder ve paralel başlat
for NODE in "${NODLAR[@]}"; do
    ssh devops@$NODE "
        cd /opt/gatling-testleri && 
        nohup mvn gatling:test 
            -Dgatling.simulationClass=$SIMULASYON 
            -DkullaniciSayisi=$NODE_BASI_KULLANICI 
            -DnodeId=$NODE 
            > /tmp/gatling-$NODE.log 2>&1 &
        echo $! > /tmp/gatling.pid
        echo 'Node $NODE başlatıldı'
    " &
done

wait
echo "Tüm node'lar çalışıyor, sonuçlar bekleniyor..."

# Sonuçları topla
mkdir -p ./sonuclar-birlestirme
for NODE in "${NODLAR[@]}"; do
    scp -r devops@$NODE:/opt/gatling-testleri/target/gatling/. ./sonuclar-birlestirme/
done

echo "Sonuçlar toplandı: ./sonuclar-birlestirme/"

Performans Sorunlarını Yorumlamak

Test çalıştı, rapor üretildi. Şimdi ne yapacaksınız? Birkaç kritik metriği bilmek gerekiyor.

p99 response time en önemli metriklerinizden biri. Ortalama iyi görünse bile p99 yüksekse kullanıcıların %1’i çok kötü deneyim yaşıyor. 1 milyon günlük kullanıcınız varsa bu 10 bin kişi demek.

Throughput düşüşü: Kullanıcı sayısı artarken throughput’un düşmesi sıkışma noktasını gösterir. Bu genellikle veritabanı connection pool’u, uygulama thread pool’u ya da harici servis limitleri kaynaklıdır.

Error rate artışı: Yüksek eş zamanlılıkta birdenbire artan 5xx hataları ya connection timeout’ları ya da resource exhaustion gösteriyor.

# Gatling log dosyasını analiz etmek için
grep "KO" target/gatling/*/simulation.log | 
  awk -F't' '{print $4}' | 
  sort | uniq -c | sort -rn | 
  head -20

# Response time dağılımı
grep "REQUEST" target/gatling/*/simulation.log | 
  awk -F't' '{print $5}' | 
  awk '{
    if ($1 < 500) hizli++
    else if ($1 < 1000) orta++
    else if ($1 < 2000) yavas++
    else cok_yavas++
  }
  END {
    print "< 500ms:", hizli
    print "500ms-1s:", orta
    print "1s-2s:", yavas
    print "> 2s:", cok_yavas
  }'

JVM Ayarları ve Optimizasyon

Gatling’in kendisi de JVM üzerinde çalıştığı için, özellikle büyük testlerde JVM’i doğru yapılandırmak şart.

# JAVA_OPTS ayarları - büyük testler için
export JAVA_OPTS="-Xms1g -Xmx4g 
  -XX:+UseG1GC 
  -XX:MaxGCPauseMillis=200 
  -XX:ParallelGCThreads=4 
  -XX:ConcGCThreads=2 
  -XX:+DisableExplicitGC 
  -Djava.net.preferIPv4Stack=true 
  -Dio.netty.eventLoopThreads=8"

# OS seviyesinde açık dosya limiti artırın
# /etc/security/limits.conf içine ekleyin
echo "* soft nofile 65536" >> /etc/security/limits.conf
echo "* hard nofile 65536" >> /etc/security/limits.conf

# TCP parametreleri (yüksek eş zamanlılık için)
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
sysctl -w net.core.somaxconn=65535
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.tcp_fin_timeout=15

# Bu değerleri kalıcı yapmak için
cat >> /etc/sysctl.conf << 'EOF'
net.ipv4.ip_local_port_range = 1024 65535
net.core.somaxconn = 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
EOF
sysctl -p

Dikkat Edilmesi Gereken Tuzaklar

Yıllar içinde gördüğüm hatalar bunlar:

  • Üretim veritabanına karşı test yapmak: Staging ortamı her zaman kullanın. Bir keresinde bir ekip yanlışlıkla üretim DB’sine 50K eş zamanlı bağlantı açtı, saatler süren bir toparlanma süreci oldu.
  • Think time’ı unutmak: Gerçek kullanıcılar her istek arasında düşünür, okur, tıklar. pause() çağrılarını atlamak gerçekçi olmayan ve çok daha agresif bir yük profili oluşturur.
  • Cache’i hesaba katmamak: CDN ve uygulama cache’i devredeyse ilk istek ile sonraki istekler çok farklı davranır. Test senaryonuzda cache ısınma sürecini de simüle edin.
  • Test aracının kendisinin darboğaz olmasına izin vermek: Eğer Gatling makinenizin CPU’su %90’a çıkıyorsa, ölçtüğünüz şey sisteminizin değil test aracınızın limiti. Gatling makinesini izleyin.
  • Assertion’ları pas geçmek: setUp(...) bloğundaki .assertions() kısmını ihmal etmeyin. Sadece testin çalışması değil, tanımlı kalite kriterlerinin karşılanması önemli.

Sonuç

Gatling öğrenme eğrisi olan bir araç, bunu saklamak anlamsız. Scala sözdizimi ilk etapta zorlayıcı gelebiliyor, özellikle Java ya da Python kökenli geliyorsanız. Ama yatırımın karşılığını aldığınız an, 10 dakikada 50K kullanıcıyla sisteminizi zorladığınız ve bunu tek bir makineden, temiz bir HTML raporuyla yaptığınız o an geliyor.

Başlamak için karmaşık bir senaryo yazmak zorunda değilsiniz. Tek endpoint, 100 kullanıcı, iki assertion. Oradan büyütün. CI/CD’ye entegre edin ve her deployment sonrası otomatik koşun. Performans regresyonlarını kod revizyonlarında yakalamak, üretimde müşteri şikayetiyle öğrenmekten kat kat iyidir.

k6, Locust, JMeter… hepsinin yeri var. Ama yüksek eş zamanlılık, ayrıntılı raporlama ve kurumsal CI/CD entegrasyonu bir arada gerekiyorsa, Gatling’e ciddi bir şans vermenizi öneririm.

Bir yanıt yazın

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