Slow Query Analizi: MongoDB Profiler Kullanımı

Prodüksiyonda bir MongoDB kümeniz var ve uygulama yavaşlamaya başladı. Kullanıcılar şikayetçi, loglara bakıyorsunuz ama tam olarak neyin yavaş olduğunu göremiyorsunuz. İşte bu noktada MongoDB Profiler devreye giriyor. Profiler, veritabanınızda çalışan sorguları izlemenizi, yavaş olanları tespit etmenizi ve optimizasyon için gereken verileri toplamanızı sağlayan güçlü bir araçtır. Bu yazıda profiler’ı nasıl aktive edeceğinizden başlayarak, gerçek dünya senaryolarında nasıl kullanacağınıza kadar her şeyi ele alacağız.

MongoDB Profiler Nedir ve Nasıl Çalışır

MongoDB Profiler, veritabanı operasyonlarını izleyen ve bunları system.profile koleksiyonuna yazan dahili bir mekanizmadır. Her veritabanı için ayrı ayrı etkinleştirilebilir ve üç farklı seviyede çalışabilir.

Profiler seviyeleri şöyle özetlenebilir:

  • Seviye 0: Profiler kapalı, hiçbir şey loglanmaz
  • Seviye 1: Sadece yavaş sorgular loglanır (slowOpThreshold değerini aşanlar)
  • Seviye 2: Tüm operasyonlar loglanır (dikkatli kullanın, yoğun sistemlerde tehlikeli olabilir)

system.profile koleksiyonu varsayılan olarak 1MB boyutunda bir capped collection’dır. Yoğun sistemlerde bu dolabilir ve eski kayıtların üzerine yazılabilir. Buna dikkat etmek gerekir.

Profiler’ı Aktive Etmek

Mevcut Durumu Kontrol Etmek

Önce hangi seviyede olduğunuzu kontrol edin:

mongosh --eval "db.getProfilingStatus()" --quiet

Ya da mongosh içinde doğrudan:

mongosh
use myapp_production
db.getProfilingStatus()

Çıktı şuna benzer olacaktır:

{
  was: 0,
  slowms: 100,
  sampleRate: 1,
  ok: 1
}

Burada was: 0 profiler’ın kapalı olduğunu, slowms: 100 ise yavaş sorgu eşiğinin 100ms olarak ayarlandığını gösterir.

Profiler’ı Açmak

Seviye 1 ile başlayın. Bu hem güvenli hem de yeterince bilgi verir:

mongosh --eval "
use myapp_production
db.setProfilingLevel(1, { slowms: 50, sampleRate: 1.0 })
" --quiet

Buradaki parametreler:

  • slowms: Bu değerin üzerindeki sorgular loglanır (milisaniye cinsinden)
  • sampleRate: 0.0 ile 1.0 arasında, kaç sorgunun örnekleneceği. 0.5 değeri yavaş sorguların %50’sini loglar
  • slowOpSampleRate: MongoDB 4.0+ için bazı sürümlerde bu parametre kullanılır

Prodüksiyonda tüm sorguları loglamak istiyorsanız ama sistemi boğmak istemiyorsanız sampleRate: 0.3 gibi bir değer mantıklı olabilir.

Replica Set Ortamında Profiler

Replica set kullanıyorsanız profiler her node üzerinde ayrı ayrı etkinleştirilmeli. Bunu script ile yapabilirsiniz:

#!/bin/bash
# replica_profiler_enable.sh

HOSTS=("mongo1:27017" "mongo2:27017" "mongo3:27017")
DB_NAME="myapp_production"
SLOW_MS=50

for HOST in "${HOSTS[@]}"; do
    echo "Profiler aktif ediliyor: $HOST"
    mongosh "mongodb://${HOST}/${DB_NAME}" --eval 
        "db.setProfilingLevel(1, { slowms: ${SLOW_MS} })" 
        --quiet
    echo "Tamamlandi: $HOST"
done

echo "Tum node'larda profiler aktif."

system.profile Koleksiyonunu Anlamak

Profiler etkin olduğunda sorgular system.profile koleksiyonuna yazılır. Bir profiler kaydının yapısını anlamak çok önemli:

mongosh
use myapp_production
db.system.profile.findOne()

Tipik bir kayıt şuna benzer:

{
  op: 'query',
  ns: 'myapp_production.users',
  command: {
    find: 'users',
    filter: { email: '[email protected]', status: 'active' },
    sort: { createdAt: -1 },
    limit: 10
  },
  keysExamined: 15420,
  docsExamined: 15420,
  cursorExhausted: true,
  numYield: 120,
  nreturned: 3,
  millis: 234,
  planSummary: 'COLLSCAN',
  ts: ISODate("2024-01-15T10:23:45.123Z"),
  client: '10.0.1.45',
  appName: 'myapp-backend'
}

Bu kayıtta dikkat edilmesi gereken alanlar:

  • millis: Sorgunun kaç milisaniye sürdüğü
  • keysExamined: İncelenen index anahtarı sayısı
  • docsExamined: İncelenen döküman sayısı
  • nreturned: Döndürülen döküman sayısı
  • planSummary: COLLSCAN görüyorsanız index yok demektir, tehlike!
  • numYield: Sorgunun kaç kez yield ettiği (yüksekse lock contention olabilir)

Kritik oran: docsExamined / nreturned oranı yüksekse verimsiz bir sorgunuz var. 15420 döküman inceleyip 3 tane döndürmek tam bir kırmızı bayrak.

Yavaş Sorguları Bulmak

Temel Analiz Sorguları

En yavaş 10 sorguyu bulmak için:

mongosh
use myapp_production

db.system.profile.find().sort({ millis: -1 }).limit(10).forEach(function(doc) {
    print("---");
    print("Süre: " + doc.millis + "ms");
    print("Operasyon: " + doc.op);
    print("Koleksiyon: " + doc.ns);
    print("Plan: " + doc.planSummary);
    print("Incelenen: " + doc.docsExamined + " dok / " + doc.keysExamined + " key");
    print("Dönen: " + doc.nreturned);
    if (doc.command) {
        print("Sorgu: " + JSON.stringify(doc.command.filter || doc.command.query));
    }
});

Zaman Aralığına Göre Filtreleme

Son 1 saatteki yavaş sorguları bulmak:

mongosh
use myapp_production

var oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);

db.system.profile.find({
    ts: { $gte: oneHourAgo },
    millis: { $gte: 100 }
}).sort({ millis: -1 }).limit(20).pretty()

COLLSCAN Yapan Sorguları Bulmak

Bu sorgular index kullanmıyor ve en tehlikelileri:

mongosh
use myapp_production

db.system.profile.find({
    planSummary: "COLLSCAN",
    ts: { $gte: new Date(Date.now() - 3600000) }
}).sort({ millis: -1 }).forEach(function(doc) {
    print("Koleksiyon: " + doc.ns);
    print("Süre: " + doc.millis + "ms");
    print("Incelenen dokuman: " + doc.docsExamined);
    if (doc.command) {
        printjson(doc.command.filter || doc.command.query || {});
    }
    print("---");
});

Koleksiyon Bazında Yavaş Sorgu Özeti

mongosh
use myapp_production

db.system.profile.aggregate([
    {
        $match: {
            ts: { $gte: new Date(Date.now() - 3600000) }
        }
    },
    {
        $group: {
            _id: "$ns",
            avgMillis: { $avg: "$millis" },
            maxMillis: { $max: "$millis" },
            minMillis: { $min: "$millis" },
            count: { $sum: 1 },
            totalMillis: { $sum: "$millis" },
            collscans: {
                $sum: {
                    $cond: [{ $eq: ["$planSummary", "COLLSCAN"] }, 1, 0]
                }
            }
        }
    },
    { $sort: { totalMillis: -1 } }
])

Bu aggregation size hangi koleksiyonun toplam olarak en çok zaman harcadığını gösterir. Tek bir sorgu 500ms olabilir ama dakikada 1000 kez çalışan 50ms’lik sorgu çok daha fazla kaynak tüketir.

Gerçek Dünya Senaryosu: E-ticaret Platformu

Diyelim ki bir e-ticaret platformu yönetiyorsunuz. Saat 14:00-16:00 arası her gün yavaşlama var, kullanıcılar ürün arama ve sipariş listeleme sayfalarından şikayet ediyor.

Adım 1: Problemi Tespit Etmek

# Profiler'ı aktive et
mongosh "mongodb://mongo-primary:27017/ecommerce" --eval 
    "db.setProfilingLevel(1, { slowms: 30 })" --quiet

echo "Profiler aktif, 2 saat bekleniyor..."
sleep 7200

# Sonuclari analiz et
mongosh "mongodb://mongo-primary:27017/ecommerce" --eval "
db.system.profile.aggregate([
    {
        $match: {
            millis: { $gte: 30 },
            ts: { $gte: new Date(Date.now() - 7200000) }
        }
    },
    {
        $group: {
            _id: {
                ns: '$ns',
                plan: '$planSummary'
            },
            count: { $sum: 1 },
            avgMs: { $avg: '$millis' },
            maxMs: { $max: '$millis' }
        }
    },
    { $sort: { count: -1 } },
    { $limit: 15 }
])
" --quiet

Adım 2: Spesifik Sorguları Analiz Etmek

Diyelim ki ecommerce.orders koleksiyonunun sorunlu olduğunu gördünüz. Bu koleksiyondaki en problematik sorguyu inceleyelim:

mongosh
use ecommerce

// En yavaş orders sorgusunu bul
var slowQuery = db.system.profile.findOne(
    {
        ns: "ecommerce.orders",
        millis: { $gte: 200 }
    },
    {},
    { sort: { millis: -1 } }
);

// Sorguyu explain ile analiz et
var filterQuery = slowQuery.command.filter || slowQuery.command.query;
print("Yavaş sorgu filtresi:");
printjson(filterQuery);

// Explain çalıştır
db.orders.find(filterQuery).explain("executionStats")

Adım 3: Explain Çıktısını Yorumlamak

Explain çıktısında dikkat edilmesi gerekenler:

  • COLLSCAN: Index yok, tüm koleksiyon taranıyor
  • IXSCAN: Index kullanılıyor, iyi
  • totalDocsExamined: İncelenen döküman sayısı
  • nReturned: Döndürülen döküman sayısı
  • executionTimeMillis: Gerçek çalışma süresi

Bu senaryoda orders koleksiyonunda userId + status + createdAt üçlüsünü sorgulayan bir sorgunun sadece userId indexi üzerinden çalıştığını ve gereksiz yere binlerce dökümanı taradığını buldunuz.

Adım 4: Index Oluşturmak

mongosh
use ecommerce

// Mevcut indexleri kontrol et
db.orders.getIndexes()

// Compound index oluştur
db.orders.createIndex(
    {
        userId: 1,
        status: 1,
        createdAt: -1
    },
    {
        name: "idx_user_status_date",
        background: true  // Eski MongoDB sürümleri için
    }
)

// MongoDB 4.2+ için
db.orders.createIndex(
    { userId: 1, status: 1, createdAt: -1 },
    { name: "idx_user_status_date" }
)

// Index oluşturma durumunu izle
db.currentOp({ "command.createIndexes": { $exists: true } })

Adım 5: Sonucu Doğrulamak

Index oluşturduktan sonra profiler verilerini tekrar kontrol edin:

mongosh
use ecommerce

// Onceki ve sonraki periyot karsilastirmasi
// Bu ornekte indexi olusturduktan 1 saat sonra
db.system.profile.aggregate([
    {
        $match: {
            ns: "ecommerce.orders",
            ts: { $gte: new Date(Date.now() - 3600000) }
        }
    },
    {
        $group: {
            _id: "$planSummary",
            count: { $sum: 1 },
            avgMs: { $avg: "$millis" },
            maxMs: { $max: "$millis" }
        }
    }
])

COLLSCAN sayısı sıfıra düşmüş ve ortalama sorgu süresi 234ms’den 8ms’ye gerilemiş olmalı.

Profiler Loglarını Dışa Aktarmak ve Analiz Etmek

Uzun vadeli analiz için profiler verilerini dışa aktarabilirsiniz:

#!/bin/bash
# export_slow_queries.sh

DATE=$(date +%Y%m%d_%H%M%S)
OUTPUT_FILE="/var/log/mongodb/slow_queries_${DATE}.json"
DB_NAME="myapp_production"
THRESHOLD_MS=50

mongoexport 
    --host localhost 
    --port 27017 
    --db "$DB_NAME" 
    --collection "system.profile" 
    --query "{"millis": {"$gte": ${THRESHOLD_MS}}}" 
    --sort "{"millis": -1}" 
    --out "$OUTPUT_FILE"

echo "Slow query logu disa aktarildi: $OUTPUT_FILE"
echo "Toplam kayit: $(wc -l < "$OUTPUT_FILE")"

Bu script’i cron ile çalıştırarak günlük raporlar oluşturabilirsiniz:

# crontab -e
# Her gece 02:00'de slow query raporu al
0 2 * * * /usr/local/bin/export_slow_queries.sh >> /var/log/mongodb/profiler_cron.log 2>&1

system.profile Koleksiyonunu Yönetmek

Varsayılan 1MB limit yetersiz gelebilir. Özellikle yoğun sistemlerde boyutu artırmak gerekir:

mongosh
use myapp_production

// Mevcut profil koleksiyonu boyutunu kontrol et
db.system.profile.stats()

// Profiler'i kapat, koleksiyonu yeniden oluştur, sonra aç
db.setProfilingLevel(0)
db.system.profile.drop()

// 50MB capped collection oluştur
db.createCollection("system.profile", {
    capped: true,
    size: 50 * 1024 * 1024,  // 50MB
    max: 100000               // Maksimum 100k kayit
})

// Profiler'i tekrar aktif et
db.setProfilingLevel(1, { slowms: 50 })

Profiler’ı Otomatize Etmek

Prodüksiyonda profiler’ı sürekli açık bırakmak tercih edilmeyebilir. İşte akıllı bir izleme scripti:

#!/bin/bash
# smart_profiler.sh
# Sistemi izle, yük yukseldiyse profiler'i ac

MONGO_HOST="localhost"
MONGO_PORT="27017"
DB_NAME="myapp_production"
LOG_FILE="/var/log/mongodb/smart_profiler.log"
SLOW_MS=50
CHECK_INTERVAL=60  # saniye

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}

get_current_ops() {
    mongosh "mongodb://${MONGO_HOST}:${MONGO_PORT}/admin" --quiet --eval 
        "db.currentOp().inprog.length"
}

enable_profiler() {
    mongosh "mongodb://${MONGO_HOST}:${MONGO_PORT}/${DB_NAME}" --quiet --eval 
        "db.setProfilingLevel(1, { slowms: ${SLOW_MS} })"
    log "Profiler AKTIF edildi (currentOp: $1)"
}

disable_profiler() {
    mongosh "mongodb://${MONGO_HOST}:${MONGO_PORT}/${DB_NAME}" --quiet --eval 
        "db.setProfilingLevel(0)"
    log "Profiler KAPATILDI (currentOp: $1)"
}

PROFILER_ACTIVE=false

while true; do
    CURRENT_OPS=$(get_current_ops)
    
    if [ "$CURRENT_OPS" -gt 50 ] && [ "$PROFILER_ACTIVE" = false ]; then
        enable_profiler "$CURRENT_OPS"
        PROFILER_ACTIVE=true
    elif [ "$CURRENT_OPS" -lt 20 ] && [ "$PROFILER_ACTIVE" = true ]; then
        disable_profiler "$CURRENT_OPS"
        PROFILER_ACTIVE=false
    fi
    
    sleep "$CHECK_INTERVAL"
done

Aggregation Pipeline’larını Profil Etmek

Aggregation sorgular da profiler’a düşer ve bunlar genellikle en karmaşık olanlardır:

mongosh
use myapp_production

// Son 24 saatteki yavaş aggregation'lari bul
db.system.profile.find({
    op: "command",
    "command.aggregate": { $exists: true },
    millis: { $gte: 100 },
    ts: { $gte: new Date(Date.now() - 86400000) }
}).sort({ millis: -1 }).limit(5).forEach(function(doc) {
    print("=== YAVAS AGGREGATION ===");
    print("Koleksiyon: " + doc.ns);
    print("Süre: " + doc.millis + "ms");
    print("Incelenen: " + doc.docsExamined + " dok");
    print("Pipeline:");
    if (doc.command && doc.command.pipeline) {
        doc.command.pipeline.forEach(function(stage, i) {
            print("  Stage " + (i+1) + ": " + JSON.stringify(Object.keys(stage)));
        });
    }
    print("");
});

Aggregation pipeline’larında yavaşlığın en yaygın nedeni $match stage’inin pipeline’ın başında olmamasıdır. $match her zaman en başa alınmalı ki MongoDB gereksiz dökümanları işlemesin.

İpuçları ve Dikkat Edilmesi Gerekenler

Profiler kullanırken bazı önemli noktalara dikkat etmek gerekir:

  • Seviye 2’den kaçının: Tüm operasyonları loglamak ciddi performans cezasına yol açabilir. Sadece geliştirme ortamında veya çok kısa süreli testlerde kullanın.
  • sampleRate kullanın: Prodüksiyonda yüksek trafik varsa sampleRate: 0.1 ile başlayın. Yavaş sorguların %10’unu yakalamak bile çok değerli veri sağlar.
  • Profiler’ı geçici açın: Sorunu tespit ettikten sonra profiler’ı kapatmayı unutmayın. Kalıcı olarak açık bırakmak istemiyorsanız bir timer koyun.
  • system.profile koleksiyonunu izleyin: Capped collection dolduğunda eski veriler kaybolur. Boyutu sisteminize göre ayarlayın.
  • Atlas kullanıcıları için: MongoDB Atlas’ta Performance Advisor otomatik olarak yavaş sorguları tespit eder ve index önerileri sunar. Ancak kendi deployment’ınızda manuel yapmanız gerekir.
  • Index oluştururken dikkatli olun: Prodüksiyonda index oluşturma işlemi I/O yoğundur. Büyük koleksiyonlar için bunu yoğun olmayan saatlerde yapın ve mongosh‘dan çıkmadan önce işlemin tamamlanmasını bekleyin.

Sonuç

MongoDB Profiler, veritabanı performans sorunlarını tespit etmede en değerli araçlardan biridir. Doğru kullanıldığında hangi sorguların sorun çıkardığını, hangi koleksiyonların index’e ihtiyaç duyduğunu ve uygulamanızın veritabanını nasıl kullandığını net biçimde görmenizi sağlar.

Genel yaklaşımı şöyle özetleyebiliriz: Önce seviye 1 ile başlayın ve slowms değerini sisteminizin normal sorgu sürelerinin 2-3 katına ayarlayın. Bir kaç saat veri toplayın. system.profile üzerinde aggregation sorguları çalıştırarak problematik koleksiyonları ve sorguları bulun. Explain ile analiz edin, gerekli index’leri oluşturun ve sonucu doğrulayın.

Profiler tek başına yeterli değildir. db.stats(), db.collection.stats() ve mongotop ile birlikte kullanıldığında çok daha kapsamlı bir görünüm elde edersiniz. Ama yavaş sorgu analizinde başlangıç noktası her zaman profiler olmalıdır. Sisteminiz yavaşladığında sezgiyle değil, veriyle hareket edin.

Yorum yapın