Serverless Mimari: Hangi Projeler için Uygun Değil
Serverless konusunda yıllardır süregelen bir hype var ve herkes bu trene atlamak istiyor. Açıkçası ben de bir dönem bu heyecanın içindeydim. Ama sahada yeterince proje gördükten sonra şunu net olarak söyleyebilirim: serverless her şeyin çözümü değil, hatta bazı projeler için aktif olarak zararlı bir seçim olabiliyor.
Bu yazıda serverless’ın ne olduğunu anlatmayacağım. AWS Lambda, Azure Functions, Google Cloud Run bilmiyorsan önce o temellere bak. Burada konuşacağımız şey: hangi senaryolarda serverless seni duvara çarpar ve bunu önceden nasıl anlarsın.
Serverless’ın Gerçek Maliyeti: Faturadan Öte
Çoğu zaman serverless tartışmaları maliyet üzerinden döner. “Kullandığın kadar öde” modeli kulağa çok cazip geliyor. Ama sahadaki gerçeklik daha karmaşık.
Bir e-ticaret projesinde danışmanlık yaparken şöyle bir senaryo gördüm: Ekip, sipariş işleme pipeline’ını tamamen Lambda üzerine kurmuştu. İlk birkaç ay her şey güzeldi. Sonra trafik arttı ve ciddi bir sorunla karşılaştılar: her Lambda çağrısı arasındaki state yönetimi için DynamoDB kullanıyorlardı ve okuma/yazma işlemleri astronomik bir maliyete ulaştı. Başlangıçta EC2’ya kıyasla ucuz görünen sistem, belirli bir ölçeğin üzerinde çok daha pahalıya geldi.
Maliyeti hesaplarken şunları göz önünde bulundurman gerekiyor:
- Execution süresi ve bellek: Lambda’da milisaniye bazında ücretlendirilirsin, uzun süren işlemler burada pahalıya patlar
- Downstream servis maliyetleri: Her function çağrısı beraberinde API Gateway, SQS, DynamoDB gibi servislere istek götürür
- Cold start overhead: Ciddi trafik varsa warm-up maliyetleri de hesaba katılmalı
- Monitoring ve observability: Distributed bir sistemin düzgün izlenmesi için Datadog, X-Ray gibi araçlar ciddi ek maliyet getirir
Yüksek Frekanslı, Uzun Süren İşlemler
Serverless platformlarının execution limitleri var. AWS Lambda’da maksimum 15 dakika, Azure Functions’da consumption planında 10 dakika. Bu sınırlar bazı iş süreçlerini tamamen eliyor.
Büyük veri işleme pipeline’ları bunun en bariz örneği. Diyelim ki her gece 10 GB’lık log dosyasını parse edip analiz ediyorsun. Lambda’da bu işi parçalamak zorunda kalırsın ve bu beraberinde ciddi mimari karmaşıklık getirir.
# Bu tür işlemler için Lambda yerine ne kullanmalısın?
# AWS Batch ile uzun süren job örneği
aws batch submit-job
--job-name "nightly-log-processor"
--job-queue "production-queue"
--job-definition "log-processor-job"
--container-overrides '{
"environment": [
{"name": "INPUT_BUCKET", "value": "logs-bucket"},
{"name": "DATE", "value": "2024-01-15"}
]
}'
Aynı iş AWS Batch veya düzgün boyutlandırılmış bir EC2 instance’ında çok daha temiz çalışır. Lambda’da bunu yapmaya kalktığında şuna benzer bir mimari ortaya çıkar:
# Lambda ile büyük dosya işleme - anti-pattern
import boto3
import json
def lambda_handler(event, context):
s3 = boto3.client('s3')
# Dosyayı chunk'lara böl
chunk_size = 1000 # satır
chunks = split_file_into_chunks(event['file_key'], chunk_size)
# Her chunk için yeni Lambda tetikle
lambda_client = boto3.client('lambda')
for i, chunk in enumerate(chunks):
lambda_client.invoke(
FunctionName='process-chunk',
InvocationType='Event', # Async
Payload=json.dumps({
'chunk_id': i,
'data': chunk,
'total_chunks': len(chunks),
'job_id': event['job_id']
})
)
# Tüm chunk'ların tamamlanmasını takip etmek için
# başka bir mekanizma gerekiyor - işte bu noktada
# gereksiz karmaşıklık başlıyor
return {"status": "dispatched", "chunks": len(chunks)}
Bu yaklaşımın sorunlarını sıralayayım:
- Chunk’ların tamamlanması için ayrı bir tracking mekanizması gerekiyor
- Hata durumunda partial failure yönetimi karmaşıklaşıyor
- Debug etmek çok zorlaşıyor
- Toplam maliyet, tek bir uzun-süren process’e kıyasla çok daha yüksek olabiliyor
Düşük Latency Gereksinimleri
Cold start meselesi serverless’ın Achilles topuğu. Özellikle Java veya .NET runtime kullanan Lambda fonksiyonlarında cold start süreleri 1-3 saniyeye çıkabiliyor. Provisioned Concurrency ile bu sorunu çözebilirsin ama o zaman “serverless ekonomisi” de büyük ölçüde ortadan kalkıyor.
Gerçek zamanlı oyun sunucusu, yüksek frekanslı trading sistemi, telepresence uygulaması gibi senaryolarda serverless kesinlikle yanlış seçim.
# Cold start etkisini ölçmek için basit bir test
# CloudWatch Logs'tan init duration'ı çek
aws logs filter-log-events
--log-group-name "/aws/lambda/my-function"
--filter-pattern "REPORT"
--start-time $(date -d '1 hour ago' +%s000)
| jq '.events[].message'
| grep "Init Duration"
# Çıktıda "Init Duration: 1243.45 ms" gibi değerler görürsen
# cold start probleminiz var demektir
Bir fintech projesinde şöyle bir tablo gördük: Ortalama response time 45ms iken cold start olan isteklerde bu 1800ms’ye çıkıyordu. Kullanıcı deneyimi açısından bu kabul edilemez bir tutarsızlık.
Durum Yönetimi Gerektiren Uygulamalar
Serverless fonksiyonlar stateless olacak şekilde tasarlanmış. Bu temel prensibi ihlal etmeye kalktığında işler çok çirkinleşiyor.
WebSocket tabanlı gerçek zamanlı uygulamalar bunun güzel bir örneği. API Gateway WebSocket’i Lambda ile kullanabilirsin ama her mesaj geldiğinde yeni bir Lambda instance’ı çalışır ve önceki bağlantının state’ini bilmez.
# WebSocket bağlantı yönetimi - serverless'ta zorunlu karmaşıklık
import boto3
import json
import os
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['CONNECTIONS_TABLE'])
def connection_handler(event, context):
connection_id = event['requestContext']['connectionId']
route_key = event['requestContext']['routeKey']
if route_key == '$connect':
# Bağlantıyı DynamoDB'ye kaydet
table.put_item(Item={
'connectionId': connection_id,
'user_id': event['queryStringParameters'].get('user_id'),
'room_id': event['queryStringParameters'].get('room_id'),
'connected_at': str(int(time.time()))
})
elif route_key == '$disconnect':
# Bağlantıyı sil
table.delete_item(Key={'connectionId': connection_id})
return {'statusCode': 200}
def message_handler(event, context):
# Odadaki tüm bağlantıları bul
body = json.loads(event['body'])
room_id = body.get('room_id')
# Her mesaj için tüm bağlantıları DynamoDB'den çek
# Bu ciddi bir okuma maliyeti ve latency oluşturuyor
response = table.scan(
FilterExpression='room_id = :room',
ExpressionAttributeValues={':room': room_id}
)
apigw = boto3.client('apigatewaymanagementapi',
endpoint_url=f"https://{event['requestContext']['domainName']}/{event['requestContext']['stage']}")
for connection in response['Items']:
try:
apigw.post_to_connection(
ConnectionId=connection['connectionId'],
Data=json.dumps(body).encode()
)
except apigw.exceptions.GoneException:
# Bağlantı kopmuş, temizle
table.delete_item(Key={'connectionId': connection['connectionId']})
Gördüğün gibi basit bir mesajlaşma senaryosu için ciddi bir altyapı yükü oluşuyor. Aynı şeyi bir WebSocket sunucusu (Socket.io, SignalR, vb.) ile çok daha temiz yapabilirsin.
Öngörülemeyen Debug ve Observability Sorunları
Monolitik bir uygulamada bir hata aldığında stack trace’e bakarsın, log dosyasını incelersin, gerekirse bir breakpoint koyarsın. Serverless’ta distributed tracing olmadan bir isteğin ne zaman patladığını bulmak gerçek bir işkenceye dönüşebilir.
# Dağıtık sistemde hata takibi için minimum gereksinim
# AWS X-Ray ile trace analizi
aws xray get-trace-summaries
--start-time $(date -d '1 hour ago' --utc +"%Y-%m-%dT%H:%M:%SZ")
--end-time $(date --utc +"%Y-%m-%dT%H:%M:%SZ")
--filter-expression 'fault = true OR error = true'
--query 'TraceSummaries[*].{Id:Id,Duration:Duration,HasFault:HasFault}'
--output table
# Belirli bir trace'in detayına inmek için
aws xray batch-get-traces
--trace-ids "1-5f84c7a2-0123456789abcdef01234567"
| jq '.Traces[0].Segments[].Document' | jq -r '.' | jq '.'
Local development deneyimi de serverless’ta acı verici olabiliyor. AWS SAM veya Serverless Framework ile local emülasyon mümkün ama gerçek AWS servislerini tam olarak taklit etmek zor.
# AWS SAM ile local test - gerçek ortamdan farklılıklar kaçınılmaz
sam local invoke MyFunction
--event events/test-event.json
--env-vars env.json
--docker-network host
# Sorun: Local DynamoDB, local SQS gibi mock servisleri
# kurmak ve maintain etmek ayrı bir yük
# docker-compose ile tüm local stack'i ayağa kaldırma
cat docker-compose-local.yml
version: '3.8'
services:
dynamodb-local:
image: amazon/dynamodb-local
ports:
- "8000:8000"
localstack:
image: localstack/localstack
ports:
- "4566:4566"
environment:
- SERVICES=sqs,s3,sns,lambda
- DEBUG=1
Bu stack’i kurmak, güncel tutmak, production davranışıyla arasındaki farkları yönetmek başlı başına bir iş yükü. Bir geliştirici güne “önce local ortamı çalıştırayım” diye başlayıp yarım saatini buna harcıyorsa bir şeyler yanlış gidiyor demektir.
Ağır Dependency Gereksinimleri
Lambda deployment package’ının 50 MB (zip) ve 250 MB (unzipped) sınırı var. Container image kullanarak 10 GB’a çıkabilirsin ama bu durumda cold start süreleri de dramatik olarak artıyor.
Makine öğrenmesi servisleri bu açıdan serverless için gerçek bir kâbus. PyTorch veya TensorFlow modellerini Lambda’da çalıştırmaya çalıştığında:
# ML model deployment - Lambda package boyutu sorunu
# requirements.txt ile paket boyutlarına bakalım
pip install torch --dry-run 2>/dev/null | tail -1
# torch tek başına 750MB+ yer kaplıyor
# Lambda layer boyut sınırını kontrol et
aws lambda get-layer-version
--layer-name "pytorch-inference"
--version-number 1
--query 'Content.CodeSize'
--output text
# Alternatif: ECS Fargate ile container tabanlı çözüm
aws ecs run-task
--cluster ml-inference-cluster
--task-definition pytorch-inference:3
--launch-type FARGATE
--network-configuration '{
"awsvpcConfiguration": {
"subnets": ["subnet-12345678"],
"securityGroups": ["sg-12345678"],
"assignPublicIp": "ENABLED"
}
}'
Makine öğrenmesi inference için SageMaker Endpoints veya ECS Fargate çok daha makul seçenekler. Modeli her cold start’ta yüklemek hem süre hem de bellek açısından kabul edilemez.
Vendor Lock-in ve Taşınabilirlik Sorunu
Bu konuda farklı fikirler olduğunu biliyorum ama benim için önemli bir kriter. Lambda’ya özgü event formatları, IAM entegrasyonları, VPC konfigürasyonları yazarken; birkaç yıl sonra başka bir bulut sağlayıcısına geçmek istediğinde sıfırdan başlamak zorunda kalabilirsin.
# Lambda-specific kod - taşınabilirlik düşük
def lambda_handler(event, context):
# event formatı Lambda'ya özgü
records = event.get('Records', [])
for record in records:
if record.get('eventSource') == 'aws:sqs':
# SQS record formatı AWS'e özgü
body = json.loads(record['body'])
# context objesi Lambda'ya özgü
remaining_time = context.get_remaining_time_in_millis()
process_message(body, remaining_time)
# Konteyner tabanlı alternatif - taşınabilir
from flask import Flask, request
import os
app = Flask(__name__)
@app.route('/process', methods=['POST'])
def process():
body = request.get_json()
process_message(body)
return {'status': 'ok'}
if __name__ == '__main__':
app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 8080)))
İkinci yaklaşım Lambda’da, ECS’te, Kubernetes’te veya herhangi bir VPS’te çalışabilir. Birincisi sadece AWS’de çalışır.
Kompleks Transaction Gereksinimleri
Dağıtık sistemlerde ACID transaction yönetimi zaten zor. Serverless’ta bu daha da karmaşık bir hal alıyor. Birden fazla servisi kapsayan bir işlemi rollback etmen gerektiğinde saga pattern veya benzeri yaklaşımları implement etmek zorunda kalırsın.
# Serverless'ta distributed transaction - saga pattern
# Bu karmaşıklık seviyesine gerçekten ihtiyaç var mı?
import boto3
import json
def order_saga_orchestrator(event, context):
sfn = boto3.client('stepfunctions')
# Step Functions ile saga koordinasyonu
response = sfn.start_execution(
stateMachineArn=os.environ['ORDER_SAGA_ARN'],
input=json.dumps({
'order_id': event['order_id'],
'items': event['items'],
'payment': event['payment'],
'compensation_steps': []
})
)
return response['executionArn']
# Step Functions state machine tanımı ayrıca yönetilmeli
# Her adım ayrı Lambda, her hata için compensation Lambda
# Bu sadece bir sipariş oluşturmak için 8-10 Lambda fonksiyonu demek
Monolitik bir uygulamada veya iyi tasarlanmış bir mikroservis mimarisinde tek bir veritabanı transaction’ı ile halledebileceğin şeyler için bu kadar karmaşıklık gerçekten gerekli mi? Çoğu zaman değil.
Peki Serverless Ne Zaman Mantıklı?
Karanlık tablonun karşısında da adil olmak lazım. Serverless gerçekten parlıyor bazı senaryolarda:
- Event-driven, async işlemler: Görsel yeniden boyutlandırma, bildirim gönderme, webhook işleme
- Düşük frekanslı arka plan görevleri: Haftalık rapor üretimi, nightly cleanup işlemleri
- API prototipler ve MVP’ler: Hızlıca market’e çıkman gereken ürünler
- Trafik patternı çok değişken olan sistemler: Sabah saatlerinde 1000 istek/dakika, gece 0 istek
- Glue code: İki servis arasında basit veri dönüştürme veya yönlendirme
Fark şu: Bu senaryolarda serverless’ın kısıtları seni sıkmaz çünkü iş yüküyle örtüşüyor.
Karar Verirken Kullanabileceğin Sorular
Bir projeye başlarken bu soruları kendinize sorun:
- Execution süreleri ne kadar? 15 dakika sınırını zorlayacak işlemler var mı?
- Latency SLA’nız ne? P99 için 100ms altı hedefliyorsanız serverless riskli
- State yönetimi ne kadar karmaşık? Bağlantı state’i mi tutuyorsunuz?
- Paket boyutları ne kadar? 250MB sınırını aşacak dependency var mı?
- Debug ekibiniz nasıl? Distributed tracing konusunda deneyim var mı?
- Trafik patternı nasıl? Sürekli yüksek trafik varsa maliyet avantajı azalır
- Vendor lock-in toleransınız nedir? Gelecekte platform değiştirecek misiniz?
# Mevcut uygulamanızı serverless'a taşımadan önce
# execution sürelerini ölçün
# Uygulama log'larından işlem sürelerini çıkar
grep "Processing completed" /var/log/app/application.log
| awk '{print $NF}'
| sort -n
| awk '
BEGIN {count=0; sum=0}
{
count++; sum+=$1; values[count]=$1
}
END {
print "Count:", count
print "Average:", sum/count "ms"
print "P95:", values[int(count*0.95)] "ms"
print "P99:", values[int(count*0.99)] "ms"
print "Max:", values[count] "ms"
}'
Sonuç
Serverless güzel bir araç ama her çiviye çekiç olmak zorunda değil. Ben sahadaki ekiplere şunu söylüyorum: eğer serverless’ı seçiyorsanız, kısıtlarını tam olarak anlayarak seçin ve “bu kısıtlar bizim use case’imizle uyumlu” diyebildiğinizde seçin.
En sık gördüğüm hata şu: ekip serverless’ı seçiyor çünkü “modern” ve “cloud-native” görünüyor. Sonra kısıtlarla karşılaşınca etrafında çalışmalar yapmaya başlıyorlar. Her çalışma yeni bir karmaşıklık katmanı ekliyor. Altı ay sonra kimsenin tam olarak anlayamadığı, debug edilmesi imkansız, beklenenden pahalı bir sistem ortaya çıkıyor.
Bazen en doğru teknik karar, en heyecan verici olanı değil, iş gereksinimlerinize en uygun olanıdır. Container’lar, sanal makineler ve klasik uygulama sunucuları hala geçerli ve çoğu zaman daha uygun seçenekler. Bunları seçmek “eski kafalı” olmak değil, pragmatik olmaktır.
Serverless’ı alet çantanızda bulundurun ama her iş için onu çıkarmayın.
