Serverless Fonksiyon Soğuk Başlatma Optimizasyonu

Serverless mimaride en sinir bozucu sorunlardan biri olan soğuk başlatma (cold start) problemini çözmek, production ortamında kullanıcı deneyimini doğrudan etkiliyor. Lambda fonksiyonunuz ilk çağrıldığında ya da uzun süre çağrılmadıktan sonra tetiklendiğinde, container başlatma, runtime yükleme ve kodunuzu initialize etme süreçleri yüzünden ciddi gecikmeler yaşanabilir. Bu gecikme bazen 100ms, bazen de 3-4 saniye olabiliyor. Kullanıcı bir API endpoint’ine istek atıyor, ekranda spinner dönüyor… İşte bu yazıda bu problemi derinlemesine inceleyip, gerçek dünya senaryolarıyla birlikte nasıl çözeceğimizi konuşacağız.

Soğuk Başlatma Neden Oluşur?

Serverless platformlar, fonksiyonlarınızı çalıştırmak için container benzeri izole ortamlar oluşturur. AWS Lambda, Google Cloud Functions veya Azure Functions fark etmez, temel mekanizma benzerdir. Bir fonksiyon ilk kez çağrıldığında veya mevcut “sıcak” instance’lar yetmediğinde, platform şu adımları sırayla gerçekleştirir:

  • Container başlatma: İzole çalışma ortamının hazırlanması
  • Runtime yükleme: Node.js, Python, Java gibi runtime’ın başlatılması
  • Kod paketi yükleme: Deployment paketinizin çıkartılması ve yüklenmesi
  • Initialization kodu çalıştırma: Handler fonksiyonu dışındaki global kodların execute edilmesi

Python ve Node.js gibi diller genellikle düşük cold start sürelerine sahipken, Java ve .NET gibi diller JVM veya CLR başlatma süresi nedeniyle çok daha yavaş olabiliyor. Bir e-ticaret şirketinde çalışırken, ürün arama API’sinin Java tabanlı Lambda fonksiyonunun ilk çağrıda 4.5 saniye sürdüğünü gördüm. Bu rakam gerçekten kabul edilemez bir kullanıcı deneyimi yaratıyordu.

Gerçek Dünya: Sorunun Boyutunu Ölçmek

Optimize etmeden önce ölçmeniz gerekiyor. CloudWatch üzerinden cold start metriklerini çekmenin en pratik yolu şu script:

#!/bin/bash
# cold_start_analyzer.sh - Lambda cold start metriklerini analiz eder

FUNCTION_NAME="my-api-function"
START_TIME=$(date -d '24 hours ago' +%s000)
END_TIME=$(date +%s000)
REGION="eu-west-1"

echo "=== Cold Start Analizi: $FUNCTION_NAME ==="
echo "Son 24 saatin metrikleri..."

# Init duration metriği cold start'ları gösterir
aws logs filter-log-events 
  --log-group-name "/aws/lambda/$FUNCTION_NAME" 
  --start-time $START_TIME 
  --end-time $END_TIME 
  --filter-pattern "REPORT" 
  --region $REGION 
  --query 'events[*].message' 
  --output text | grep "Init Duration" | 
  awk '{
    for(i=1;i<=NF;i++) {
      if($i == "Duration:") init=$i $(i+1)
    }
    sum += $(NF-1); count++
    print "Init Duration:", $(NF-1), "ms"
  }
  END {
    if(count > 0) print "nOrtalama Init Duration:", sum/count, "ms"
    print "Toplam Cold Start:", count
  }'

Bu scripti çalıştırdıktan sonra elimde somut veriler oluyor. “Init Duration” değeri olan satırlar doğrudan cold start’ları temsil ediyor.

Deployment Paket Boyutunu Küçültmek

En hızlı kazanım deployment paket boyutunu azaltmaktan geliyor. Büyük paketler daha uzun cold start süresi demek. Node.js projelerinde bu konuya özellikle dikkat etmek gerekiyor çünkü node_modules dizini inanılmaz şişebiliyor.

#!/bin/bash
# lambda_package_analyzer.sh - Paket boyutu analizi

TEMP_DIR=$(mktemp -d)
FUNCTION_NAME="my-api-function"

echo "Lambda deployment paketi analiz ediliyor..."

# Mevcut deployment paketini indir
aws lambda get-function 
  --function-name $FUNCTION_NAME 
  --query 'Code.Location' 
  --output text | xargs curl -s -o "$TEMP_DIR/function.zip"

# Paket boyutu
PACKAGE_SIZE=$(du -sh "$TEMP_DIR/function.zip" | cut -f1)
echo "Toplam paket boyutu: $PACKAGE_SIZE"

# İçeriği çıkart ve boyuta göre sırala
cd $TEMP_DIR && unzip -q function.zip -d extracted/
echo ""
echo "En büyük 20 dosya/dizin:"
du -sh extracted/* | sort -rh | head -20

# node_modules varsa analiz et
if [ -d "extracted/node_modules" ]; then
  echo ""
  echo "En büyük npm paketleri:"
  du -sh extracted/node_modules/* | sort -rh | head -10
fi

rm -rf $TEMP_DIR

Pratikte aws-sdk, moment.js veya büyük utility kütüphaneleri gereksiz yere pakete dahil ediliyor. AWS Lambda ortamında aws-sdk zaten mevcut olduğundan onu devDependencies‘e taşıyabilirsiniz.

Lambda Layer Kullanımı ile Dependency Yönetimi

Lambda Layer’ları hem paket boyutunu küçültmek hem de container reuse oranını artırmak için kullanılabilir. Aynı layer’ı kullanan fonksiyonlar, layer’ın cache’de kalması sayesinde daha hızlı başlar.

#!/bin/bash
# create_dependencies_layer.sh - Bağımlılıkları ayrı layer olarak paketler

LAYER_NAME="nodejs-common-deps"
RUNTIME="nodejs18.x"
REGION="eu-west-1"
BUILD_DIR=$(mktemp -d)

echo "Dependencies layer oluşturuluyor..."

# Layer için gerekli dizin yapısı
mkdir -p "$BUILD_DIR/nodejs/node_modules"

# Production bağımlılıklarını layer dizinine kopyala
cp package.json "$BUILD_DIR/nodejs/"
cd "$BUILD_DIR/nodejs" && npm install --production --silent

# Layer boyutunu kontrol et (250MB limiti var)
LAYER_SIZE=$(du -sm "$BUILD_DIR" | cut -f1)
echo "Layer boyutu: ${LAYER_SIZE}MB"

if [ $LAYER_SIZE -gt 250 ]; then
  echo "UYARI: Layer boyutu 250MB limitini aşıyor!"
  exit 1
fi

# Layer'ı zip'le
cd $BUILD_DIR && zip -r layer.zip nodejs/ -q

# AWS'ye publish et
LAYER_ARN=$(aws lambda publish-layer-version 
  --layer-name $LAYER_NAME 
  --description "Common Node.js dependencies" 
  --zip-file fileb://layer.zip 
  --compatible-runtimes $RUNTIME 
  --region $REGION 
  --query 'LayerVersionArn' 
  --output text)

echo "Layer oluşturuldu: $LAYER_ARN"

# Layer'ı fonksiyona ekle
aws lambda update-function-configuration 
  --function-name my-api-function 
  --layers $LAYER_ARN 
  --region $REGION

rm -rf $BUILD_DIR
echo "Tamamlandı!"

Provisioned Concurrency: Sıcak Tutma Stratejisi

AWS Lambda’nın sunduğu en doğrudan çözüm Provisioned Concurrency. Belirli sayıda instance’ı her zaman “sıcak” tutarak cold start’ı tamamen ortadan kaldırıyor. Ancak maliyeti var ve bu maliyeti yönetmek için akıllı bir zamanlama stratejisi gerekiyor.

#!/bin/bash
# manage_provisioned_concurrency.sh
# İş saatlerinde yüksek, gece düşük provisioned concurrency ayarlar

FUNCTION_NAME="my-api-function"
QUALIFIER="production"
REGION="eu-west-1"

CURRENT_HOUR=$(date +%H)
DAY_OF_WEEK=$(date +%u)  # 1=Pazartesi, 7=Pazar

# Hafta içi mi?
if [ $DAY_OF_WEEK -le 5 ]; then
  # İş saatleri: 08:00-20:00
  if [ $CURRENT_HOUR -ge 8 ] && [ $CURRENT_HOUR -lt 20 ]; then
    CONCURRENCY=10
    echo "İş saati: Provisioned Concurrency = $CONCURRENCY"
  elif [ $CURRENT_HOUR -ge 6 ] && [ $CURRENT_HOUR -lt 8 ]; then
    # Sabah warm-up
    CONCURRENCY=5
    echo "Sabah warm-up: Provisioned Concurrency = $CONCURRENCY"
  else
    # Gece
    CONCURRENCY=2
    echo "Gece: Provisioned Concurrency = $CONCURRENCY"
  fi
else
  # Hafta sonu
  CONCURRENCY=3
  echo "Hafta sonu: Provisioned Concurrency = $CONCURRENCY"
fi

# Mevcut ayarı kontrol et
CURRENT_CONCURRENCY=$(aws lambda get-provisioned-concurrency-config 
  --function-name $FUNCTION_NAME 
  --qualifier $QUALIFIER 
  --region $REGION 
  --query 'RequestedProvisionedConcurrentExecutions' 
  --output text 2>/dev/null || echo "0")

if [ "$CURRENT_CONCURRENCY" != "$CONCURRENCY" ]; then
  echo "Güncelleniyor: $CURRENT_CONCURRENCY -> $CONCURRENCY"
  aws lambda put-provisioned-concurrency-config 
    --function-name $FUNCTION_NAME 
    --qualifier $QUALIFIER 
    --provisioned-concurrent-executions $CONCURRENCY 
    --region $REGION
  echo "Güncelleme tamamlandı."
else
  echo "Değişiklik gerekmedi, mevcut ayar zaten $CONCURRENCY."
fi

Bu scripti bir cron job olarak saatlik çalıştırırsanız, gereksiz provisioned concurrency maliyetinden %40-50 oranında tasarruf edebilirsiniz.

Scheduled Warm-Up: Düşük Maliyetli Alternatif

Provisioned Concurrency bütçenizi zorluyorsa, düzenli ping ile fonksiyonları sıcak tutmak ikinci en iyi seçenek. Özellikle trafik patternini bildiğiniz durumlar için çok işlevsel.

#!/bin/bash
# lambda_warmer.sh - Lambda fonksiyonlarını warm tutan script

FUNCTIONS=(
  "api-users-function"
  "api-products-function"
  "api-orders-function"
)
REGION="eu-west-1"
CONCURRENCY_LEVEL=3  # Aynı anda kaç instance ısıtılacak

warm_function() {
  local func_name=$1
  local pids=()
  
  echo "Isıtılıyor: $func_name (${CONCURRENCY_LEVEL} concurrent invoke)"
  
  for i in $(seq 1 $CONCURRENCY_LEVEL); do
    aws lambda invoke 
      --function-name $func_name 
      --region $REGION 
      --payload '{"source": "warmer", "warmer": true}' 
      --invocation-type Event 
      --cli-binary-format raw-in-base64-out 
      /tmp/warmup_response_${func_name}_${i}.json 
      > /dev/null 2>&1 &
    pids+=($!)
  done
  
  # Tüm invoke'ların tamamlanmasını bekle
  for pid in "${pids[@]}"; do
    wait $pid
  done
  
  echo "$func_name isıtma tamamlandı"
  rm -f /tmp/warmup_response_${func_name}_*.json
}

echo "=== Lambda Warm-Up Başlıyor ==="
echo "Zaman: $(date)"

for func in "${FUNCTIONS[@]}"; do
  warm_function $func &
done

wait
echo "=== Tüm Fonksiyonlar Isıtıldı ==="

Fonksiyonunuzun içinde warmer çağrısını erkenden yakalayıp gereksiz işlemi atlatmak için şu pattern’i kullanın:

// handler.js - Warmer kontrolü
exports.handler = async (event) => {
  // Warmer ping'ini erkenden yakala
  if (event.source === 'warmer' || event.warmer === true) {
    console.log('Warm-up çağrısı, erken dönülüyor...');
    return { statusCode: 200, body: 'warm' };
  }
  
  // Normal iş mantığı buraya
  // ...initialization pahalıysa, lazy loading kullan
  
  return {
    statusCode: 200,
    body: JSON.stringify({ message: 'success' })
  };
};

Kod Seviyesinde Optimizasyon

Sadece altyapı değişikliği yeterli değil. Kodun kendisi de cold start süresini doğrudan etkiliyor.

Lazy Loading ile Initialization Erteleme

# Python Lambda - Lazy Loading örneği
import boto3
import json

# YANLIŞ: Bu import her cold start'ta çalışır
# import heavy_ml_library  # 2-3 saniye yükleme süresi!

# DOĞRU: Global cache değişkenleri tanımla
_dynamodb_client = None
_s3_client = None
_model = None

def get_dynamodb():
    """DynamoDB client'ı lazy load eder"""
    global _dynamodb_client
    if _dynamodb_client is None:
        _dynamodb_client = boto3.client('dynamodb', region_name='eu-west-1')
    return _dynamodb_client

def get_model():
    """ML modelini lazy load eder - sadece gerektiğinde"""
    global _model
    if _model is None:
        # Bu sadece modeli gerçekten kullanacak fonksiyonlarda çalışır
        import heavy_ml_library
        _model = heavy_ml_library.load_model('/opt/model.pkl')
    return _model

def handler(event, context):
    route = event.get('path', '')
    
    if route == '/predict':
        # Sadece bu route için model yüklenir
        model = get_model()
        result = model.predict(event['body'])
        return {'statusCode': 200, 'body': json.dumps(result)}
    
    # Diğer route'lar model yüklemez
    db = get_dynamodb()
    # ... normal işlemler
    return {'statusCode': 200, 'body': 'ok'}

Memory Ayarı ile CPU Optimizasyonu

Lambda’da memory ile CPU doğrudan ilişkili. Daha fazla memory, daha fazla CPU gücü anlamına geliyor ve bu cold start süresini azaltıyor. Aşağıdaki script farklı memory ayarlarında fonksiyonu test eder:

#!/bin/bash
# memory_optimizer.sh - Farklı memory ayarlarını test eder

FUNCTION_NAME="my-api-function"
REGION="eu-west-1"
TEST_PAYLOAD='{"test": true, "path": "/health"}'
MEMORY_CONFIGS=(128 256 512 1024 1769 3008)
ITERATIONS=5

echo "Memory Optimizasyon Testi - $FUNCTION_NAME"
echo "============================================"

for MEMORY in "${MEMORY_CONFIGS[@]}"; do
  echo ""
  echo "Memory: ${MEMORY}MB test ediliyor..."
  
  # Memory ayarını güncelle
  aws lambda update-function-configuration 
    --function-name $FUNCTION_NAME 
    --memory-size $MEMORY 
    --region $REGION > /dev/null
  
  # Config güncellemesinin yayılmasını bekle
  sleep 5
  
  TOTAL_DURATION=0
  TOTAL_BILLED=0
  
  for i in $(seq 1 $ITERATIONS); do
    RESULT=$(aws lambda invoke 
      --function-name $FUNCTION_NAME 
      --region $REGION 
      --payload "$TEST_PAYLOAD" 
      --log-type Tail 
      --cli-binary-format raw-in-base64-out 
      /tmp/test_output.json 2>&1)
    
    # Log'dan duration bilgisini çıkart
    LOG=$(echo $RESULT | jq -r '.LogResult' | base64 -d 2>/dev/null)
    DURATION=$(echo $LOG | grep -oP 'Duration: K[d.]+')
    BILLED=$(echo $LOG | grep -oP 'Billed Duration: K[d]+')
    
    if [ -n "$DURATION" ]; then
      TOTAL_DURATION=$(echo "$TOTAL_DURATION + $DURATION" | bc)
      TOTAL_BILLED=$(echo "$TOTAL_BILLED + $BILLED" | bc)
    fi
  done
  
  AVG_DURATION=$(echo "scale=2; $TOTAL_DURATION / $ITERATIONS" | bc)
  AVG_BILLED=$(echo "scale=0; $TOTAL_BILLED / $ITERATIONS" | bc)
  COST_UNIT=$(echo "scale=6; ($MEMORY / 1024) * $AVG_BILLED * 0.0000000167" | bc)
  
  echo "  Ortalama Duration: ${AVG_DURATION}ms"
  echo "  Ortalama Billed: ${AVG_BILLED}ms"
  echo "  Tahmini maliyet/çağrı: $${COST_UNIT}"
done

rm -f /tmp/test_output.json
echo ""
echo "Test tamamlandı. Optimal memory ayarını seçin."

Container Reuse: Bağlantı Pooling

Database bağlantıları cold start sürecinin önemli bir parçası. Her invocation’da yeni bağlantı açmak yerine, container reuse sayesinde bağlantıları canlı tutabilirsiniz.

# db_connection_pool.py - Lambda için bağlantı yönetimi

import psycopg2
import os
import logging

logger = logging.getLogger()

# Global scope'ta tanımla - container reuse sayesinde paylaşılır
_db_connection = None

def get_db_connection():
    """
    Var olan bağlantıyı döner, yoksa yenisini oluşturur.
    Container reuse durumunda bağlantı kurma maliyeti sıfıra düşer.
    """
    global _db_connection
    
    # Bağlantı yok veya kapanmış
    if _db_connection is None or _db_connection.closed:
        logger.info("Yeni DB bağlantısı oluşturuluyor...")
        _db_connection = psycopg2.connect(
            host=os.environ['DB_HOST'],
            port=os.environ.get('DB_PORT', 5432),
            database=os.environ['DB_NAME'],
            user=os.environ['DB_USER'],
            password=os.environ['DB_PASSWORD'],
            connect_timeout=5,
            # Keep-alive ayarları
            keepalives=1,
            keepalives_idle=30,
            keepalives_interval=10,
            keepalives_count=5
        )
    else:
        # Var olan bağlantının sağlığını kontrol et
        try:
            _db_connection.cursor().execute('SELECT 1')
        except psycopg2.OperationalError:
            logger.warning("DB bağlantısı kopmuş, yeniden bağlanılıyor...")
            _db_connection = None
            return get_db_connection()
    
    return _db_connection

def handler(event, context):
    conn = get_db_connection()
    
    with conn.cursor() as cursor:
        cursor.execute("SELECT * FROM products WHERE active = true LIMIT 10")
        results = cursor.fetchall()
    
    return {
        'statusCode': 200,
        'body': str(results)
    }

Monitoring ve Alert Kurulumu

Tüm optimizasyonları yaptıktan sonra, cold start metriklerini izlemeye devam etmek gerekiyor. Bir regresyon olduğunda hemen haber almak için CloudWatch alarm kuruyoruz:

#!/bin/bash
# setup_cold_start_alarm.sh - Cold start monitörü

FUNCTION_NAME="my-api-function"
REGION="eu-west-1"
SNS_TOPIC_ARN="arn:aws:sns:eu-west-1:123456789012:sysadmin-alerts"

echo "Cold start alarm kuruluyor..."

# CloudWatch Logs Metric Filter - Init Duration'ı yakala
aws logs put-metric-filter 
  --log-group-name "/aws/lambda/$FUNCTION_NAME" 
  --filter-name "ColdStartFilter" 
  --filter-pattern "[report_label="REPORT", ..., init_label="Init", duration_label="Duration:", init_duration, ...]" 
  --metric-transformations 
    metricName=ColdStartCount,metricNamespace=Lambda/Performance,metricValue=1,defaultValue=0 
  --region $REGION

echo "Metric filter oluşturuldu."

# Cold start sayısı için alarm
aws cloudwatch put-metric-alarm 
  --alarm-name "$FUNCTION_NAME-HighColdStartRate" 
  --alarm-description "Lambda cold start oranı yüksek" 
  --namespace Lambda/Performance 
  --metric-name ColdStartCount 
  --dimensions Name=FunctionName,Value=$FUNCTION_NAME 
  --statistic Sum 
  --period 300 
  --evaluation-periods 3 
  --threshold 20 
  --comparison-operator GreaterThanThreshold 
  --alarm-actions $SNS_TOPIC_ARN 
  --ok-actions $SNS_TOPIC_ARN 
  --treat-missing-data notBreaching 
  --region $REGION

echo "Alarm kurulumu tamamlandı: $FUNCTION_NAME-HighColdStartRate"
echo "5 dakikalık pencerede 20'den fazla cold start olursa bildirim gelecek."

Pratik Öncelik Sırası

Bir sistemi optimize ederken nereden başlayacağınızı bilmek kritik. Tecrübelerime göre şu sırayı takip ediyorum:

  • İlk adım: Ölçüm yapın, cold start sıklığını ve süresini belirleyin. Sorun olmayan yerde optimizasyon yapmak boşa vakit harcamak demek.
  • İkinci adım: Deployment paket boyutunu küçültün. En hızlı kazanım buradan geliyor, maliyetsiz ve etkili.
  • Üçüncü adım: Kod seviyesinde lazy loading ve connection pooling uygulayın. Bu da maliyetsiz.
  • Dördüncü adım: Memory ayarını test edin. Bazen 512MB’dan 1024MB’a çıkmak hem hızı artırır hem de maliyeti düşürür (çünkü daha kısa sürede tamamlanıyor).
  • Beşinci adım: Eğer SLA’nız gerçekten sıkıysa ve kritik fonksiyonlarınız varsa Provisioned Concurrency devreye alın. Ama zamanlamalı açıp kapatma script’i olmadan yapmayın, fatura şokla karşılaşabilirsiniz.
  • Altıncı adım: Warm-up script’ini cron job olarak çalıştırın. Özellikle kullanım paterni tahmin edilebilir olan servisler için idealdir.

Runtime Seçimi de Önemli

Eğer yeni bir proje başlatıyorsanız runtime seçimi cold start üzerinde büyük etkiye sahip. Python 3.12 ve Node.js 20 genellikle en hızlı cold start sürelerini veriyor. Java 21 ile Snapstart özelliği ciddi iyileştirme sağlıyor ama konfigürasyon gerektiriyor. GraalVM native image ile derlenen Java da artık çok makul cold start sürelerine ulaşıyor.

Mevcut sistemde runtime değiştirmek büyük bir iş ama greenfield projelerde bu kararı baştan vermek uzun vadede hayat kurtarıyor.

Sonuç

Serverless cold start optimizasyonu tek bir sihirli çözümü olmayan, katmanlı bir yaklaşım gerektiren bir problem. Tecrübelerimden çıkardığım en önemli ders şu: önce ölç, sonra optimize et. Kör kör kod değişikliği yapmak yerine gerçek cold start metriklerinize bakın, hangi fonksiyonların ne sıklıkla cold start yaşadığını görün.

Çoğu durumda deployment paketi küçültme ve lazy loading ile cold start sürelerini %50-70 oranında düşürmek mümkün oluyor. Provisioned Concurrency’yi ise gerçekten kritik, SLA’sı sıkı fonksiyonlar için saklayın ve mutlaka zamanlamalı yönetin. Warm-up script’leri ise bütçe kısıtı olan durumlar için iyi bir orta yol sunuyor.

Monitoring olmadan optimizasyon kör uçuştur. Alarm ve metrik filtrelerinizi kurun, bir regresyon olduğunda anında haberdar olun. Production’da görünmez sorunlar zaman içinde büyür ve o büyüyünce çözmek çok daha maliyetli hale geliyor.

Bir yanıt yazın

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