Serverless Maliyet Optimizasyonu Stratejileri

Buluta geçtiğinizde “artık sunucu yönetmiyorum, maliyet de düşecek” diye düşünürsünüz. Serverless mimaride bu kısmen doğru, ama fatura sonunda geldiğinde “bu kadar mı ödeyeceğim?” diye şaşırabilirsiniz. AWS Lambda, Google Cloud Functions veya Azure Functions kullanıyor olun, fark etmez; yanlış yapılandırılmış bir serverless ortam, aynı iş yükü için geleneksel bir VM’den çok daha pahalıya gelebilir. Bu yazıda gerçek dünya senaryolarıyla serverless maliyet optimizasyonunu masaya yatırıyoruz.

Serverless Maliyeti Nasıl Hesaplanır?

Önce temel mantığı anlamak lazım. AWS Lambda özelinde konuşursak, iki temel bileşen var:

  • İstek sayısı: Her ay ilk 1 milyon istek ücretsiz, sonrası $0.20/milyon
  • Hesaplama süresi: GB-saniye cinsinden ölçülür, 512 MB bellek ile 1 saniye çalışan bir fonksiyon 0.5 GB-saniye tüketir

Kulağa basit geliyor, değil mi? Ama şu senaryoya bakın: 100 ms’de bitecek bir fonksiyon, yanlış yapılandırma nedeniyle 3000 ms bekliyor. Bu, aynı işlem için 30 kat fazla para ödüyorsunuz demek. Veya 128 MB bellek yeterli olan bir fonksiyona 1024 MB ayırdınız; yine gereksiz maliyet.

Mevcut fonksiyonlarınızın ne kadar harcadığını görmek için şu AWS CLI komutunu çalıştırabilirsiniz:

# Son 30 günün Lambda maliyet dökümünü çek
aws ce get-cost-and-usage 
  --time-period Start=2024-01-01,End=2024-01-31 
  --granularity MONTHLY 
  --filter '{"Dimensions": {"Key": "SERVICE", "Values": ["AWS Lambda"]}}' 
  --metrics "BlendedCost" "UsageQuantity" 
  --group-by Type=DIMENSION,Key=USAGE_TYPE 
  --output json | jq '.ResultsByTime[].Groups[] | {type: .Keys[0], cost: .Metrics.BlendedCost.Amount}'

Bu çıktı size hangi kullanım tipinin (istek sayısı mı, hesaplama mı) daha pahalıya patladığını gösterir. Çoğu zaman sürprizle karşılaşırsınız.

Bellek Optimizasyonu: En Hızlı Kazanç

Serverless’ta CPU doğrudan seçilmiyor; bellek miktarı ile orantılı olarak CPU tahsis ediliyor. Bu ilginç bir durum yaratıyor: Bazen belleği artırmak, fonksiyonu daha hızlı çalıştırarak toplam maliyeti düşürüyor.

Bunu test etmek için AWS Lambda Power Tuning aracını kullanabilirsiniz. Bu araç, farklı bellek konfigürasyonlarında fonksiyonunuzu çalıştırıp maliyet/performans grafiği çıkarıyor.

# AWS SAM ile Power Tuning aracını deploy et
sam deploy 
  --template-file template.yaml 
  --stack-name lambda-power-tuning 
  --capabilities CAPABILITY_IAM 
  --parameter-overrides 
    lambdaResource="arn:aws:lambda:eu-west-1:123456789:function:my-api-handler" 
    powerValues="128,256,512,1024,2048,3008" 
    num=50 
    payload='{"test": "data"}' 
    parallelInvocation=true 
    strategy="cost"

Gerçek bir örnekle açıklayayım: Bir e-ticaret projemizde ürün arama fonksiyonu 1024 MB ile yapılandırılmıştı. Power Tuning testi yaptığımızda, 512 MB’da neredeyse aynı sürede çalıştığını gördük. Aylık maliyet yarıya düştü. Ama ilginç olan, başka bir fonksiyonda 512 MB’dan 1024 MB’a çıkınca sürenin 2000 ms’den 900 ms’e düşmesi ve toplam GB-saniye maliyetinin azalmasıydı.

Kural basit: Her zaman test et, varsayım yapma.

Cold Start Optimizasyonu

Cold start, serverless’ın en sevilen sorunu. Fonksiyon uzun süre çağrılmamışsa, bulut sağlayıcı container’ı tekrar ayağa kaldırmak zorunda kalıyor ve bu gecikme yaşatıyor. Ama burada gözden kaçan bir maliyet boyutu var: Cold start süresinde fonksiyon çalışıyor sayılıyor ve ücretlendiriliyor.

Python veya Node.js fonksiyonları için cold start genellikle 100-300 ms arasında. Java veya .NET için bu süre 1-3 saniyeye çıkabiliyor. Ayda 100.000 cold start yaşayan bir Java fonksiyonu, sadece başlangıç süresinden önemli bir maliyet üretiyor.

Çözüm stratejileri:

  • Provisioned Concurrency: Belirli sayıda instance’ı sıcak tut, ama dikkatli kullan
  • Dil seçimi: I/O yoğun işler için Node.js veya Python tercih et
  • Paket boyutunu küçült: Büyük deployment paketi cold start’ı uzatır
# Fonksiyonun cold start oranını CloudWatch'tan çek
aws logs filter-log-events 
  --log-group-name /aws/lambda/my-function 
  --start-time $(date -d '7 days ago' +%s000) 
  --filter-pattern "Init Duration" 
  --query 'events[*].message' 
  --output text | 
  awk '{
    for(i=1;i<=NF;i++) {
      if($i=="Duration:") duration=$(i+1)
      if($i=="Init") init_dur=$(i+2)
    }
    if(init_dur) print "Cold start:", init_dur, "ms | Total:", duration, "ms"
  }'

Provisioned Concurrency konusunda çok dikkatli olmak gerekiyor. Üretim ortamında bir müşterimizin 10 adet fonksiyonu için tüm saatler provisioned concurrency açmıştı. Maliyet, fonksiyonları hiç kullanmasa bile saatte doluyordu. Doğru yaklaşım şu:

# Sadece iş saatlerinde Provisioned Concurrency aç (Application Auto Scaling ile)
aws application-autoscaling register-scalable-target 
  --service-namespace lambda 
  --resource-id function:my-api-handler:prod 
  --scalable-dimension lambda:function:ProvisionedConcurrency 
  --min-capacity 2 
  --max-capacity 10

# Sabah 8'de aç, akşam 8'de kapat (scheduled scaling)
aws application-autoscaling put-scheduled-action 
  --service-namespace lambda 
  --resource-id function:my-api-handler:prod 
  --scalable-dimension lambda:function:ProvisionedConcurrency 
  --scheduled-action-name scale-up-morning 
  --schedule "cron(0 8 ? * MON-FRI *)" 
  --scalable-target-action MinCapacity=5,MaxCapacity=10

Bu yapıyla hafta içi iş saatleri dışında ve hafta sonları provisioned concurrency sıfıra iniyor. Aylık tasarruf ciddi olabiliyor.

Timeout Ayarları: Görünmez Maliyet Bombası

Lambda’nın varsayılan timeout değeri 3 saniye, maksimum 15 dakika. Birçok ekip fonksiyonları için makul bir timeout belirlemek yerine 15 dakikayı set edip geçiyor. Bu büyük hata.

Bir fonksiyon normalde 500 ms’de bitiyorsa ve 15 dakika timeout varsa, bir bug nedeniyle askıda kalıp 15 dakika çalışmaya devam ederse ne olur? Binlerce böyle çağrı olursa fatura şok edici seviyelere çıkar.

Doğru yaklaşım: fonksiyonun normal çalışma süresinin 3-4 katı bir timeout belirleyin.

# Tüm fonksiyonların timeout değerlerini ve ortalama sürelerini karşılaştır
for func in $(aws lambda list-functions --query 'Functions[*].FunctionName' --output text); do
  timeout=$(aws lambda get-function-configuration 
    --function-name $func 
    --query 'Timeout' 
    --output text)
  
  avg_duration=$(aws cloudwatch get-metric-statistics 
    --namespace AWS/Lambda 
    --metric-name Duration 
    --dimensions Name=FunctionName,Value=$func 
    --start-time $(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%S) 
    --end-time $(date -u +%Y-%m-%dT%H:%M:%S) 
    --period 86400 
    --statistics Average 
    --query 'Datapoints[0].Average' 
    --output text 2>/dev/null)
  
  echo "Fonksiyon: $func | Timeout: ${timeout}s | Ortalama Süre: ${avg_duration}ms"
done

Bu scripti çalıştırdığınızda, 15 dakika timeout’u olan ve ortalama 200 ms’de biten fonksiyonları göreceksiniz. Bunlar potansiyel maliyet tuzağı.

Concurrent Execution Limitleri ve Throttling Yönetimi

AWS’de hesap başına varsayılan concurrent execution limiti 1000. Bunu aşan istekler throttle ediliyor. Throttle olan istekler genellikle retry mekanizmasıyla tekrar deneniyor, bu da hem maliyet hem de gecikme yaratıyor.

Daha da kötüsü, bir fonksiyon tüm limiti tüketirse diğer kritik fonksiyonlar da çalışamaz hale geliyor. Buna “noisy neighbor” problemi diyoruz.

Çözüm: Fonksiyon bazında reserved concurrency belirlemek.

# Kritik fonksiyonlara reserved concurrency ata
aws lambda put-function-concurrency 
  --function-name payment-processor 
  --reserved-concurrent-executions 100

# Batch işlemler için düşük limit koy (maliyet kontrolü için)
aws lambda put-function-concurrency 
  --function-name nightly-report-generator 
  --reserved-concurrent-executions 5

# Mevcut concurrency kullanımını izle
aws cloudwatch get-metric-statistics 
  --namespace AWS/Lambda 
  --metric-name ConcurrentExecutions 
  --dimensions Name=FunctionName,Value=payment-processor 
  --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) 
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) 
  --period 60 
  --statistics Maximum 
  --output table

Batch işlemler için reserved concurrency’i kasıtlı olarak düşük tutmak, hem maliyet kontrolü sağlar hem de sistemin geri kalanını korur. Raporlama fonksiyonunuz yavaş çalışsın ama ödeme fonksiyonunuz asla etkilenmesin.

Mimari Seviyesinde Optimizasyon

Kod ve yapılandırma optimizasyonunun ötesinde, mimarinin kendisi büyük maliyet farkları yaratıyor.

Asenkron Mümkünse Asenkron

Bir web hook alıp işleme senaryosunu düşünün. Senkron yaklaşımda, tüm iş bitmeden HTTP yanıtı dönmüyorsunuz. Bu süre boyunca fonksiyon çalışıyor ve ücretlendiriliyor.

Asenkron yaklaşımda ise: hook geldi, SQS’e at, anında 200 OK dön. Fonksiyon belki 50 ms çalıştı. Arka planda SQS trigger’ı ile başka bir fonksiyon işi yapıyor. Her iki fonksiyon da sadece aktif çalıştığı süre kadar ücretlendiriliyor.

# SQS kuyruğu oluştur ve Lambda trigger'ı ekle
aws sqs create-queue 
  --queue-name webhook-processing-queue 
  --attributes '{
    "VisibilityTimeout": "300",
    "MessageRetentionPeriod": "86400",
    "ReceiveMessageWaitTimeSeconds": "20"
  }'

# Lambda event source mapping ekle (batch işleme için)
aws lambda create-event-source-mapping 
  --event-source-arn arn:aws:sqs:eu-west-1:123456789:webhook-processing-queue 
  --function-name webhook-processor 
  --batch-size 10 
  --maximum-batching-window-in-seconds 5

batch-size 10 ve maximum-batching-window-in-seconds 5 ayarlarına dikkat edin. Bu kombinasyon şunu yapıyor: 5 saniye bekle veya 10 mesaj dolsun, hangisi önce olursa Lambda’yı tetikle. Tek tek tetiklemek yerine toplu çalışıyor. 10.000 mesaj için 1000 invocation yerine 1000 invocation yapabilirsiniz. İstek maliyeti 10 kat düşüyor.

Step Functions vs Lambda Zinciri

Birden fazla Lambda’yı birbirini çağırarak zincirleme yapmak yaygın ama pahalı bir anti-pattern. A fonksiyonu B’yi çağırıyor, B C’yi çağırıyor derken tüm bu süre boyunca A ve B fonksiyonları “bekliyor” durumunda ücretlendiriliyor.

AWS Step Functions Express Workflow bu durumda çok daha ekonomik olabilir. Orkestrasyon mantığı Step Functions’a taşınıyor, her Lambda sadece kendi işini yapıp bitiyor.

# Step Functions state machine oluştur
aws stepfunctions create-state-machine 
  --name order-processing-workflow 
  --type EXPRESS 
  --definition '{
    "Comment": "Siparis isleme",
    "StartAt": "ValidateOrder",
    "States": {
      "ValidateOrder": {
        "Type": "Task",
        "Resource": "arn:aws:lambda:eu-west-1:123456789:function:validate-order",
        "Next": "ProcessPayment",
        "TimeoutSeconds": 10
      },
      "ProcessPayment": {
        "Type": "Task",
        "Resource": "arn:aws:lambda:eu-west-1:123456789:function:process-payment",
        "Next": "SendConfirmation",
        "TimeoutSeconds": 15
      },
      "SendConfirmation": {
        "Type": "Task",
        "Resource": "arn:aws:lambda:eu-west-1:123456789:function:send-email",
        "End": true,
        "TimeoutSeconds": 5
      }
    }
  }' 
  --role-arn arn:aws:iam::123456789:role/StepFunctionsRole

Express Workflow fiyatlandırması state geçişi başına, Standard Workflow ise süre bazlı. Kısa süren yüksek hacimli iş akışları için Express çok daha ucuz.

CloudWatch Log Maliyetleri: Göz Ardı Edilen Kalem

Lambda fonksiyonları otomatik olarak CloudWatch’a log yazıyor. Bu harika bir özellik, ama kontrolsüz bırakıldığında ciddi maliyet yaratıyor. CloudWatch log ingestion ve depolama bedava değil.

Karşılaştığım en kötü durumlardan biri: Her istek için tüm request/response body’sini debug seviyesinde loglayan bir fonksiyon. Günde 10 milyon istek, her biri ortalama 2 KB log. Bu aylık ciddi bir CloudWatch maliyeti.

# Log gruplarının retention sürelerini ayarla (varsayılan sonsuz!)
# Tüm Lambda log gruplarını bul ve retention ekle
for log_group in $(aws logs describe-log-groups 
  --log-group-name-prefix /aws/lambda/ 
  --query 'logGroups[?retentionInDays==`null`].logGroupName' 
  --output text); do
  
  echo "Retention ayarlaniyor: $log_group"
  aws logs put-retention-policy 
    --log-group-name $log_group 
    --retention-in-days 14
done

# Log seviyesini environment variable ile kontrol et
aws lambda update-function-configuration 
  --function-name my-api-handler 
  --environment Variables='{
    "LOG_LEVEL": "WARN",
    "NODE_ENV": "production"
  }'

14 gün retention çoğu production ortamı için yeterli. Compliance gerektiren durumlar hariç, logları sonsuza kadar tutmanın anlamı yok. Bu tek değişiklikle aylık yüzlerce dolar tasarruf sağlayabilirsiniz.

Ayrıca lambda fonksiyon kodunuzda log seviyesi kontrolü mutlaka olmalı:

# Lambda'daki log boyutunu düzenli kontrol et
aws logs describe-log-groups 
  --log-group-name-prefix /aws/lambda/ 
  --query 'logGroups[*].{Name:logGroupName, SizeGB:to_number(storedBytes)/1073741824, RetentionDays:retentionInDays}' 
  --output json | 
  jq 'sort_by(-.SizeGB) | .[:10] | .[] | 
    "Grup: (.Name) | Boyut: (.SizeGB | . * 100 | round / 100) GB | Retention: (.RetentionDays // "SONSUZ") gun"'

ARM/Graviton ile Maliyet Düşürme

AWS Lambda, ARM tabanlı Graviton2 işlemcileri destekliyor. x86’ya kıyasla hem daha ucuz (yaklaşık %20) hem de genellikle daha hızlı. Özellikle CPU yoğun iş yükleri için fark belirgin.

Geçiş genellikle sadece yapılandırma değişikliği gerektiriyor:

# Mevcut fonksiyonu ARM mimarisine geçir
aws lambda update-function-configuration 
  --function-name my-api-handler 
  --architectures arm64

# Yeni fonksiyon oluştururken ARM belirt
aws lambda create-function 
  --function-name new-processor 
  --runtime nodejs20.x 
  --architectures arm64 
  --role arn:aws:iam::123456789:role/LambdaRole 
  --handler index.handler 
  --zip-file fileb://function.zip 
  --memory-size 512 
  --timeout 30

Dikkat etmeniz gereken nokta: Eğer fonksiyonunuzun native binary bağımlılıkları varsa (örneğin C extensionları olan Python paketleri), ARM için ayrıca derlemeniz gerekiyor. Docker multi-platform build kullanmanız lazım.

Maliyet Anomali Tespiti

Tüm optimizasyonları yapsanız bile beklenmedik maliyet artışları olabiliyor. Bunu erken yakalamak için otomatik alarm kurmak şart.

# Cost Anomaly Detection monitörü oluştur
aws ce create-anomaly-monitor 
  --anomaly-monitor '{
    "MonitorName": "Lambda-Cost-Monitor",
    "MonitorType": "DIMENSIONAL",
    "MonitorDimension": "SERVICE"
  }'

# Anomali subscription ekle (günlük $20 üstü artışlarda bildir)
aws ce create-anomaly-subscription 
  --anomaly-subscription '{
    "MonitorArnList": ["arn:aws:ce::123456789:anomalymonitor/abc123"],
    "Subscribers": [
      {
        "Address": "[email protected]",
        "Type": "EMAIL"
      }
    ],
    "Threshold": 20,
    "Frequency": "DAILY",
    "SubscriptionName": "Lambda-Daily-Anomaly-Alert"
  }'

# Budget alarm ekle
aws budgets create-budget 
  --account-id 123456789 
  --budget '{
    "BudgetName": "Lambda-Monthly-Budget",
    "BudgetLimit": {"Amount": "500", "Unit": "USD"},
    "TimeUnit": "MONTHLY",
    "BudgetType": "COST",
    "CostFilters": {
      "Service": ["AWS Lambda"]
    }
  }' 
  --notifications-with-subscribers '[
    {
      "Notification": {
        "NotificationType": "ACTUAL",
        "ComparisonOperator": "GREATER_THAN",
        "Threshold": 80,
        "ThresholdType": "PERCENTAGE"
      },
      "Subscribers": [
        {
          "SubscriptionType": "EMAIL",
          "Address": "[email protected]"
        }
      ]
    }
  ]'

Bu iki alarm birbirini tamamlıyor. Budget alarm bütçenin %80’ine gelince uyarıyor, anomaly detection ise olağandışı artışları pattern’den sapma olarak tespit ediyor.

Gerçek Dünya Vaka: Aylık $3000’dan $800’e

Son olarak gerçek bir optimizasyon hikayesiyle bitirelim. Bir SaaS şirketinin Lambda faturası ani şekilde aylık $3000’a fırlamıştı. Ekip nedenini anlayamamıştı.

İncelediğimizde bulduklarımız şunlardı:

  • Ana API fonksiyonu 3 GB bellek ile yapılandırılmıştı (Power Tuning sonucu: 512 MB yeterliydi)
  • Tüm fonksiyonlarda 15 dakika timeout vardı, ortalama çalışma süresi 800 ms’di
  • CloudWatch logları sonsuza kadar saklanıyordu, 200 GB birikmişti
  • Senkron Lambda zinciri vardı: API -> Validator -> Enricher -> Database Writer
  • Hiçbir fonksiyonda batch processing yoktu, SQS mesajları tek tek işleniyordu

Adım adım düzelttik:

  • Bellek 3 GB’dan 512 MB’a düşürüldü: %40 maliyet düşüşü
  • Timeout gerçekçi değerlere çekildi
  • Log retention 14 güne ayarlandı, eski loglar temizlendi
  • Lambda zinciri Step Functions Express’e taşındı
  • SQS batch size 10’a çıkarıldı

Sonuç: Aylık fatura $800’e düştü. Aynı iş yükü, %73 daha ucuz. Performans da iyileşti, çünkü batch processing gecikmeyi azalttı.

Sonuç

Serverless maliyet optimizasyonu tek seferlik bir iş değil, sürekli izleme ve iyileştirme gerektiren bir süreç. Başlamanız için öncelik sırasıyla öneriler:

  • İlk hafta: Tüm fonksiyonlarda bellek optimizasyonu yap, Power Tuning çalıştır
  • İkinci hafta: Timeout değerlerini gözden geçir, CloudWatch log retention ayarla
  • İlk ay: Asenkron mimariye geçiş yapılabilecek yerleri tespit et, batch processing uygula
  • Sürekli: Anomaly detection ve budget alarmlarını kur, aylık maliyet raporunu incele

Serverless güçlü bir paradigma. Doğru yapılandırıldığında hem operasyonel yükü azaltıyor hem de gerçekten maliyet avantajı sağlıyor. Ama “kurup unut” yaklaşımı kesinlikle işe yaramıyor. Faturanızı anlamak ve kontrol etmek sizin sorumluluğunuz.

Bir yanıt yazın

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