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.
