İçeriğe atla

AWS Lambda Memory Allocation ve Performance Tuning: Kapsamlı Rehber

AWS Lambda performance tuning: memory-CPU modeli, Power Tuning ile benchmarking, maliyet analizi ve adaptive allocation pattern'ları.

Ayhan Sipahi Ayhan Sipahi

Serinin ilk bölümünde cold start’ları optimize ettikten sonra, sıradaki zorluk Lambda function’larınızın warm olduklarında verimli çalışmasını sağlamak. Memory allocation, etki alanı en geniş konfigürasyon ayarı; tek bir değer aynı anda CPU payını, execution süresini ve faturayı belirliyor.

Pratik varsayılan: 1024 MB ile başlayın, sonra yalnızca ölçülmüş bir eğrinin gösterdiği yöne doğru hareket edin. 128 MB varsayılanı hesaplama yapan hiçbir function için doğru değil; doğrudan 3008 MB’ye sıçrama refleksi ise çoğunlukla tek thread’li kodun harcayamayacağı bir kapasite satın alır.

Memory boyutu yalnızca bir girdi. Taşımaya değer model şu: ayrılan memory, onunla birlikte gelen CPU ve ikisinden doğan GB-saniye faturası.

Lambda’nın Memory-CPU Modeli#

CPU’nun Memory ile Ölçeklenmesi#

Lambda tek bir kaynak ayarı sunuyor. Memory değeri aynı zamanda function’ın ne kadar CPU alacağını belirliyor:

MemoryYaklaşık vCPUNe kazandırır
128 MB~0,07En yavaş execution; CPU-bound işler sürünür
512 MB~0,29Hafif API handler’ları için yaygın baseline
1024 MB~0,58Çoğu workload için dengeli başlangıç
1769 MB1,00Tam bir vCPU
3008 MB~1,70Birden fazla core; yalnızca paralel kod faydalanır
10240 MB~5,79Maksimum allocation

CPU, memory ile baştan sona orantılı ölçekleniyor; vCPU sütunu memory değerinin 1769’a bölünmüş hali. Bu çizginin ötesinde birden fazla core’a sahip olursunuz ama tek thread’li Node.js kodu hızlanmayı bırakır, milisaniye başına ücret artmaya devam eder.

Memory-Süre Eğrisi#

Her function’ın kendi eğrisi var; harekete geçmeye değer tek sayı, kendi ölçtüğünüz sayıdır. Lambda Power Tuning (yeni sekmede açılır), aynı payload’ı birkaç memory ayarında çağırıp her biri için süre ve maliyet raporlayan bir Step Functions state machine’i:

{
  "lambdaARN": "arn:aws:lambda:us-east-1:123456789012:function:image-resize",
  "powerValues": [128, 512, 1024, 1769, 3008],
  "num": 50,
  "payload": { "key": "sample.jpg" },
  "parallelInvocation": true,
  "strategy": "balanced"
}

Sık karşılaşılan iki şekil var. CPU-bound işler (image resize, sıkıştırma, hashing) bir vCPU’ya yaklaşana kadar dik biçimde düşer ve bu aralıkta invocation başına maliyet neredeyse sabit kalır: kısalan süre, artan milisaniye ücretini dengeler. I/O-bound işler çok daha erken düzleşir; dirseğin ötesinde aynı network çağrısını beklemek için milisaniye başına daha fazla ödersiniz.

1024 MB’yi başlangıç noktası yapan şey bu. Genelde maliyet eğrisinin düz kısmında durur ve CPU-bound koda bir core’un büyük bölümünü verir; oradan hangi yöne gideceğinizi ölçtüğünüz eğri söyler.

Benchmarking Framework’ü Kurmak#

Kapsamlı Performance Test Kurulumu#

Power Tuning konfigürasyonlar arasındaki eğriyi verir. Function içi ölçüm ise tek bir invocation’da zamanın nereye gittiğini gösterir:

// comprehensive-benchmark.ts
import { performance, PerformanceObserver } from 'perf_hooks';

interface BenchmarkResult {
  memoryUsed: number;
  executionTime: number;
  coldStart: boolean;
  gcEvents: number;
  cpuIntensive: boolean;
}

// Module scope, aynı ortamdaki invocation'lar arasında yaşar
let isWarm = false;

export class LambdaBenchmark {
  private results: BenchmarkResult[] = [];
  private coldStart = !isWarm;
  private gcCount = 0;

  constructor() {
    isWarm = true;
    this.monitorGC();
  }

  private monitorGC() {
    // PerformanceObserver, engine'in yaptığı her collection'ı raporlar.
    // global.gc'yi patch'lemek yalnızca kendi tetiklediğiniz çağrıları sayar
    // ve global.gc --expose-gc ister; Lambda bu flag'i açmaz.
    const observer = new PerformanceObserver((list) => {
      this.gcCount += list.getEntries().length;
    });
    observer.observe({ entryTypes: ['gc'] });
  }

  async benchmark<T>(
    operation: () => Promise<T>,
    label: string
  ): Promise<{ result: T; metrics: BenchmarkResult }> {
    this.gcCount = 0;
    const startMemory = process.memoryUsage();
    const startTime = performance.now();

    const result = await operation();

    const endTime = performance.now();
    const endMemory = process.memoryUsage();

    const metrics: BenchmarkResult = {
      memoryUsed: endMemory.heapUsed - startMemory.heapUsed,
      executionTime: endTime - startTime,
      coldStart: this.coldStart,
      gcEvents: this.getGCEvents(),
      cpuIntensive: this.detectCPUIntensiveOperation(endTime - startTime)
    };

    console.log(`Benchmark [${label}]:`, metrics);
    this.results.push(metrics);

    return { result, metrics };
  }

  private detectCPUIntensiveOperation(duration: number): boolean {
    // >100ms süren operasyonlar muhtemelen CPU-bound
    return duration > 100;
  }

  private getGCEvents(): number {
    return this.gcCount;
  }
}

// Lambda'nızda kullanım
export const handler = async (event: any) => {
  const benchmark = new LambdaBenchmark();
  
  const { result } = await benchmark.benchmark(async () => {
    return await processLargeDataset(event.data);
  }, 'data-processing');

  return result;
};

Production Benchmarking Stratejisi#

Aynı taramayı state machine olmadan yapmak isterseniz, tek bir function’ı memory ayarları arasında gezdirin ve billed duration ile max memory used değerlerini taşıyan REPORT satırını okuyun:

for memory in 512 1024 1536 1769 3008; do
  aws lambda update-function-configuration \
    --function-name image-resize \
    --memory-size "${memory}" > /dev/null

  # Konfigürasyon güncellemesi asenkron; erken çağırırsanız eski değere denk gelirsiniz
  aws lambda wait function-updated-v2 --function-name image-resize

  echo "== ${memory} MB =="
  aws lambda invoke \
    --function-name image-resize \
    --cli-binary-format raw-in-base64-out \
    --payload file://test-payload.json \
    --log-type Tail \
    --query 'LogResult' --output text \
    "response-${memory}.json" | base64 --decode | grep REPORT
done

Ayar başına tek invocation gürültü ölçer. Sıralamaya güvenmeden önce döngüyü tekrarlayıp medyan alın.

Memory Optimizasyon Stratejileri#

Strateji 1: Workload Tipine Göre Right-Sizing#

Farklı workload’ların farklı optimal memory allocation’ları var:

// Workload tipine göre memory allocation
const workloadOptimization = {
  // API Gateway proxy function'ları
  simpleAPI: {
    memoryMB: 512,
    reason: "Düşük CPU, hızlı response time önceliği"
  },
  
  // Database operasyonları
  databaseIntensive: {
    memoryMB: 1024, 
    reason: "Query processing + connection overhead için balanced CPU"
  },
  
  // Image/file processing
  fileProcessing: {
    memoryMB: 1769,
    reason: "CPU-intensive, tam vCPU'dan faydalanır"
  },
  
  // ML inference
  machineLearning: {
    memoryMB: 3008,
    reason: "Model için memory + inference için multi-core"
  },
  
  // Data transformation
  dataETL: {
    memoryMB: 1769,
    reason: "CPU-bound operasyonlar, optimal maliyet/performans"
  }
};

Strateji 2: Memory Leak Önleme#

Arka planda dönen bir watchdog burada yanlış çözüm. Lambda, invocation’lar arasında execution environment’ı dondurur; bu yüzden setInterval yalnızca bir istek işlenirken tıklar ve tıkladığı an hangi invocation çalışıyorsa ona denk gelir. Bunun yerine handler içinde bilinen noktalarda örnekleme yapın:

// Invocation içinde memory basıncı örneklemesi
export class MemoryManager {
  private readonly allocatedBytes =
    parseInt(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE || '512', 10) * 1024 * 1024;
  private readonly threshold = 0.8; // Ayrılan memory'nin %80'i

  check(label: string): void {
    const usage = process.memoryUsage();
    // Lambda tüm sandbox üzerinden faturalandırır ve öldürür; REPORT
    // satırındaki "Max Memory Used" ile eşleşen değer heapUsed değil rss
    const ratio = usage.rss / this.allocatedBytes;

    if (ratio > this.threshold) {
      console.warn('Yüksek memory kullanımı tespit edildi:', {
        label,
        rss: Math.round(usage.rss / 1024 / 1024) + 'MB',
        heapUsed: Math.round(usage.heapUsed / 1024 / 1024) + 'MB',
        external: Math.round(usage.external / 1024 / 1024) + 'MB',
        usage: Math.round(ratio * 100) + '%'
      });
    }
  }
}

// Kullanım pattern'i
const memoryManager = new MemoryManager();

export const handler = async (event: any) => {
  memoryManager.check('invocation-start');
  const result = await processEvent(event);
  memoryManager.check('invocation-end');
  return result;
};

Leak kendini, aynı ortamdaki ardışık isteklerde invocation-start değerinin tırmanmasıyla belli eder. Tek seferlik pahalı bir istek ise yalnızca invocation-end içinde görünür.

Strateji 3: Garbage Collection Optimizasyonu#

NODE_OPTIONS V8 flag’lerinin yalnızca küçük bir alt kümesini kabul eder. --expose-gc, --gc-interval ve --optimize-for-size reddedilir; runtime bunları görmezden gelmek yerine başlamayı reddeder. Yani if (global.gc) ile korunan her satır, managed runtime’da ölü koddur:

// Lambda environment variable olarak ayarlanır
const gcOptimizations = {
  NODE_OPTIONS: [
    '--max-old-space-size=1024',  // heap'i ayarlanan memory'nin altında tut
    '--max-semi-space-size=32'    // daha büyük young generation, daha az scavenge
  ].join(' ')
};

// Manuel collection aramak yerine çalışma setini sınırla
const processLargeDataset = async (data: any[]) => {
  const results = [];

  for (const chunk of chunkArray(data, 1000)) {
    results.push(await processChunk(chunk));
    // Yukarıdaki hiçbir satır bir önceki chunk'ın ara sonuçlarına
    // referans tutmaz; collector bunları kendiliğinden geri alır
  }

  return results;
};

Chunking transform adımını sınırlar, girdiyi değil. Sandbox’ı dolduran şey veri setinin kendisiyse, S3’ten indirip dilimlemek yerine stream edin.

Maliyet Analizi Framework’ü#

Memory Allocation’ın Gerçek Maliyeti#

Tüm değişkenleri hesaba katan kapsamlı bir maliyet analizi kurun:

// cost-calculator.ts
interface LambdaCostParams {
  memoryMB: number;
  avgExecutionMs: number;
  invocationsPerMonth: number;
  region: 'us-east-1' | 'us-west-2' | 'eu-west-1';
}

interface CostBreakdown {
  computeCost: number;
  requestCost: number;
  totalMonthlyCost: number;
  costPerInvocation: number;
  performanceRating: number;
}

export class LambdaCostCalculator {
  // x86 on-demand ücretleri; bu üç region'da aynı değerler geçerli.
  // arm64 GB-saniye başına daha ucuz, yani mimari de bir kaldıraç.
  private pricing: Record<LambdaCostParams['region'], {
    computePerGBSecond: number;
    requestPer1M: number;
  }> = {
    'us-east-1': { computePerGBSecond: 0.0000166667, requestPer1M: 0.20 },
    'us-west-2': { computePerGBSecond: 0.0000166667, requestPer1M: 0.20 },
    'eu-west-1': { computePerGBSecond: 0.0000166667, requestPer1M: 0.20 }
  };

  calculateCost(params: LambdaCostParams): CostBreakdown {
    const { memoryMB, avgExecutionMs, invocationsPerMonth, region } = params;
    const pricing = this.pricing[region];

    // Memory'yi GB'ye ve execution time'ı saniyeye çevir
    const memoryGB = memoryMB / 1024;
    const executionSeconds = avgExecutionMs / 1000;

    // Compute cost hesapla
    const gbSeconds = memoryGB * executionSeconds * invocationsPerMonth;
    const computeCost = gbSeconds * pricing.computePerGBSecond;

    // Request cost hesapla
    const requestCost = (invocationsPerMonth / 1000000) * pricing.requestPer1M;

    const totalMonthlyCost = computeCost + requestCost;
    const costPerInvocation = totalMonthlyCost / invocationsPerMonth;

    // Performance rating (düşük execution time = yüksek rating)
    const performanceRating = Math.max(1, 10 - (avgExecutionMs / 100));

    return {
      computeCost,
      requestCost,
      totalMonthlyCost,
      costPerInvocation,
      performanceRating
    };
  }

  findOptimalMemory(
    baseParams: Omit<LambdaCostParams, 'memoryMB'>,
    performanceProfile: { memory: number; executionMs: number }[]
  ): { memory: number; cost: number; savings: number } {
    const scenarios = performanceProfile.map(profile => ({
      ...profile,
      cost: this.calculateCost({
        ...baseParams,
        memoryMB: profile.memory,
        avgExecutionMs: profile.executionMs
      })
    }));

    // En iyi cost-performance oranına sahip konfigürasyonu bul
    const optimal = scenarios.reduce((best, current) => 
      (current.cost.totalMonthlyCost / current.cost.performanceRating) <
      (best.cost.totalMonthlyCost / best.cost.performanceRating) 
        ? current : best
    );

    const baseline = scenarios[0]; // İlkinin baseline olduğunu varsayıyor
    const savings = baseline.cost.totalMonthlyCost - optimal.cost.totalMonthlyCost;

    return {
      memory: optimal.memory,
      cost: optimal.cost.totalMonthlyCost,
      savings
    };
  }
}

// Kullanım örneği
const calculator = new LambdaCostCalculator();

// Süreler bu sayfadan değil, kendi Power Tuning çalıştırmanızdan gelir
const performanceData = [
  { memory: 512, executionMs: 2100 },
  { memory: 1024, executionMs: 1300 },
  { memory: 1769, executionMs: 900 },
  { memory: 3008, executionMs: 800 }
];

const optimal = calculator.findOptimalMemory({
  avgExecutionMs: 0, // Override edilecek
  invocationsPerMonth: 1000000,
  region: 'us-east-1'
}, performanceData);

console.log(`Optimal konfigürasyon: ${optimal.memory}MB`);
console.log(`Aylık tasarruf: $${optimal.savings.toFixed(2)}`);

Gelişmiş Performance Pattern’ları#

Pattern 1: Adaptive Memory Allocation#

Mevcut memory’ye göre processing’i dinamik olarak ayarlayın:

// adaptive-processing.ts
export class AdaptiveProcessor {
  private availableMemoryMB: number;
  private processingStrategy: 'small' | 'medium' | 'large';

  constructor() {
    this.availableMemoryMB = parseInt(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE || '512');
    this.processingStrategy = this.determineStrategy();
  }

  private determineStrategy(): 'small' | 'medium' | 'large' {
    if (this.availableMemoryMB >= 3008) return 'large';
    if (this.availableMemoryMB >= 1024) return 'medium';
    return 'small';
  }

  async processData(data: any[]): Promise<any[]> {
    switch (this.processingStrategy) {
      case 'large':
        // Paralel operasyonlarla her şeyi memory'de işle
        return await this.parallelProcessing(data);
      
      case 'medium':
        // Orta memory kullanımıyla batch processing
        return await this.batchProcessing(data, 1000);
      
      case 'small':
        // Memory kullanımını minimize etmek için stream processing
        return await this.streamProcessing(data, 100);
    }
  }

  private async parallelProcessing(data: any[]): Promise<any[]> {
    // Node tek thread çalışır: bu I/O'yu üst üste bindirir, ekstra core kullanmaz.
    // 1769 MB üstünde CPU-bound iş için ikinci core'a worker_threads ile ulaşılır.
    const chunks = this.chunkArray(data, Math.ceil(data.length / 4));
    const promises = chunks.map(chunk => this.processChunk(chunk));
    const results = await Promise.all(promises);
    return results.flat();
  }

  private async batchProcessing(data: any[], batchSize: number): Promise<any[]> {
    const results = [];
    for (let i = 0; i < data.length; i += batchSize) {
      const batch = data.slice(i, i + batchSize);
      const processed = await this.processChunk(batch);
      results.push(...processed);
    }
    return results;
  }

  private async streamProcessing(data: any[], chunkSize: number): Promise<any[]> {
    const results = [];
    for (let i = 0; i < data.length; i += chunkSize) {
      const chunk = data.slice(i, i + chunkSize);
      const processed = await this.processChunk(chunk);
      results.push(...processed);
    }
    return results;
  }

  private chunkArray<T>(array: T[], size: number): T[][] {
    return Array.from({ length: Math.ceil(array.length / size) }, (_, i) =>
      array.slice(i * size, i * size + size)
    );
  }

  private async processChunk(chunk: any[]): Promise<any[]> {
    // Gerçek processing mantığınız buraya
    return chunk.map(item => ({ ...item, processed: true }));
  }
}

Pattern 2: Memory-Aware Caching#

Mevcut memory’ye göre caching davranışını uyarlayın:

// memory-aware-cache.ts
export class MemoryAwareCache {
  private cache = new Map<string, any>();
  private maxMemoryUsage = 0.6; // Cache için mevcut memory'nin maksimum %60'ını kullan
  private availableMemoryBytes: number;

  constructor() {
    this.availableMemoryBytes = parseInt(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE || '512') * 1024 * 1024;
  }

  set(key: string, value: any): void {
    const currentUsage = process.memoryUsage().heapUsed;
    const maxCacheMemory = this.availableMemoryBytes * this.maxMemoryUsage;

    if (currentUsage < maxCacheMemory) {
      this.cache.set(key, {
        value,
        timestamp: Date.now(),
        size: this.estimateObjectSize(value)
      });
    } else {
      // Cache dolu, LRU eviction implement et
      this.evictLeastRecentlyUsed();
      this.cache.set(key, {
        value,
        timestamp: Date.now(),
        size: this.estimateObjectSize(value)
      });
    }
  }

  get(key: string): any {
    const entry = this.cache.get(key);
    if (entry) {
      // LRU için timestamp güncelle
      entry.timestamp = Date.now();
      return entry.value;
    }
    return null;
  }

  private evictLeastRecentlyUsed(): void {
    let oldestKey = '';
    let oldestTime = Date.now();

    for (const [key, entry] of this.cache.entries()) {
      if (entry.timestamp < oldestTime) {
        oldestTime = entry.timestamp;
        oldestKey = key;
      }
    }

    if (oldestKey) {
      this.cache.delete(oldestKey);
    }
  }

  private estimateObjectSize(obj: any): number {
    // Memory'deki object boyutunun kaba tahmini
    return JSON.stringify(obj).length * 2; // Kaba yaklaşım
  }

  getCacheStats(): {
    entries: number;
    estimatedMemoryMB: number;
    memoryUsagePercent: number;
  } {
    let totalSize = 0;
    for (const entry of this.cache.values()) {
      totalSize += entry.size;
    }

    return {
      entries: this.cache.size,
      estimatedMemoryMB: totalSize / 1024 / 1024,
      memoryUsagePercent: (totalSize / this.availableMemoryBytes) * 100
    };
  }
}

Production Monitoring ve Profiling#

Gelişmiş CloudWatch Custom Metric’leri#

Lambda’nın sizin için yayınlamadığı sayıları takip edin. Her putMetricData, invocation yolunda senkron bir API çağrısıdır; saniyede birkaç isteğin üstüne çıkınca bunun yerine stdout’a CloudWatch Embedded Metric Format satırları yazın ve metric’leri log agent çıkarsın:

// performance-monitor.ts
import { CloudWatch } from '@aws-sdk/client-cloudwatch';

export class PerformanceMonitor {
  private cloudWatch: CloudWatch;
  private functionName: string;

  constructor() {
    this.cloudWatch = new CloudWatch({});
    this.functionName = process.env.AWS_LAMBDA_FUNCTION_NAME || 'unknown';
  }

  async trackPerformanceMetrics(
    executionTime: number,
    memoryUsed: number,
    cpuIntensive: boolean
  ): Promise<void> {
    const metrics = [
      {
        MetricName: 'ExecutionTime',
        Value: executionTime,
        Unit: 'Milliseconds',
        Dimensions: [
          { Name: 'FunctionName', Value: this.functionName },
          { Name: 'MemorySize', Value: process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE || '512' }
        ]
      },
      {
        MetricName: 'MemoryUtilization',
        Value: memoryUsed,
        Unit: 'Bytes',
        Dimensions: [
          { Name: 'FunctionName', Value: this.functionName }
        ]
      },
      {
        MetricName: 'CPUIntensiveOperations',
        Value: cpuIntensive ? 1 : 0,
        Unit: 'Count',
        Dimensions: [
          { Name: 'FunctionName', Value: this.functionName }
        ]
      }
    ];

    await this.cloudWatch.putMetricData({
      Namespace: 'Lambda/Performance',
      MetricData: metrics
    });
  }

  async trackCostMetrics(estimatedCost: number): Promise<void> {
    await this.cloudWatch.putMetricData({
      Namespace: 'Lambda/Cost',
      MetricData: [
        {
          MetricName: 'EstimatedCost',
          Value: estimatedCost,
          Unit: 'None',
          Dimensions: [
            { Name: 'FunctionName', Value: this.functionName }
          ]
        }
      ]
    });
  }
}

X-Ray Performance Profiling#

Detaylı performance insight’ları için X-Ray kullanın:

// x-ray-profiling.ts
import * as AWSXRay from 'aws-xray-sdk-core';

export const handler = async (event: any) => {
  // Facade segment'i Lambda sizin için açar. captureAsyncFunc, sarmalanmış
  // bir handler döndürmek yerine gövdeyi module load anında çalıştırırdı.
  const segment = AWSXRay.getSegment();
  
  // Memory allocation tracking
  const memorySubsegment = segment?.addNewSubsegment('memory-tracking');
  const initialMemory = process.memoryUsage();
  memorySubsegment?.addAnnotation('initial_memory_mb', Math.round(initialMemory.heapUsed / 1024 / 1024));
  
  try {
    // Subsegment'lerle business logic
    const processingSegment = segment?.addNewSubsegment('data-processing');
    const result = await processData(event.data);
    processingSegment?.close();

    // Processing sonrası memory kullanımı
    const finalMemory = process.memoryUsage();
    memorySubsegment?.addAnnotation('final_memory_mb', Math.round(finalMemory.heapUsed / 1024 / 1024));
    memorySubsegment?.addAnnotation('memory_delta_mb', Math.round((finalMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024));
    
    return result;
  } finally {
    memorySubsegment?.close();
  }
};

const processData = async (data: any) => {
  const segment = AWSXRay.getSegment();
  const subsegment = segment?.addNewSubsegment('data-transformation');
  
  try {
    // Performance analizi için metadata ekle
    subsegment?.addMetadata('input_size', JSON.stringify(data).length);
    subsegment?.addAnnotation('cpu_intensive', true);
    
    const result = await heavyProcessingOperation(data);
    
    subsegment?.addMetadata('output_size', JSON.stringify(result).length);
    return result;
  } finally {
    subsegment?.close();
  }
};

Pratik Dersler: Memory Optimizasyonu Yanlış Gittiğinde#

Over-Allocation Tuzağı#

Bir tuning turunun faturaya geri tepmesinin en yaygın yolu over-allocation. “Daha fazla her zaman daha iyidir” varsayımıyla bir function’ı 1024 MB’den 3008 MB’ye taşımak, milisaniye başına ücreti neredeyse üçe katlar. Bu ancak süre de yaklaşık aynı oranda düşerse kendini amorti eder; function bir vCPU’yu geçtikten ve tek thread’de kaldıktan sonra düşmez.

Kural: her performance değişikliğinden sonra GB-saniyeyi yeniden hesaplayın. Tek başına süre grafiği, faturayı yükselten bir değişikliği onaylar.

Ölçekte Ortaya Çıkan Memory Leak#

Leak’ler düşük concurrency’de saklanır. Eşzamanlı her istek kendi execution environment’ını alır ve her biri bağımsız olarak birikir; bu yüzden logging veya tracing kütüphanesindeki yavaş bir leak on invocation’da görünmez, on bin invocation’da öldürücü olur.

Sinyal, REPORT satırındaki Max Memory Used. Aynı ortamın ardışık invocation’larında oturmak yerine tırmanıyorsa, bir şey istekler arasında referans tutuyordur.

// OOM kill function içinde yakalanamaz: runtime fırlatılmaz, sonlandırılır.
// Bunun yerine pahalı yolu başlamadan önce koruyun.
class MemoryGuard {
  private readonly allocatedBytes =
    parseInt(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE || '512', 10) * 1024 * 1024;
  private readonly ceiling = 0.75;

  async run<T>(operation: () => Promise<T>): Promise<T> {
    const { rss } = process.memoryUsage();

    if (rss / this.allocatedBytes > this.ceiling) {
      // Yazma işleminin ortasında ölmek yerine retry edilebilir bir hata ver
      throw new Error(
        `Memory tavanına ulaşıldı: ${Math.round(rss / 1024 / 1024)}MB zaten kullanımda`
      );
    }

    return operation();
  }
}

Az Memory Vermenin Bedeli#

Bunun aynadaki karşılığı, topluca memory kısmak. Memory’yi yarıya indirmek milisaniye başına ücreti de yarıya indirir; ama süre iki katından fazla artarsa GB-saniye yükselir ve daha yavaş bir function için fazladan ödemiş olursunuz. Az memory ayrıca her invocation’ı timeout’a yaklaştırır ve retry tetikleyen bir timeout aynı işi iki kez faturalandırır.

Sonraki Adım: Production Monitoring Derinlemesine#

Bir memory ayarı, ancak onu şekillendiren trafik sürdüğü sürece geçerlidir. Serinin sonraki bölümü, eğrinin ayağınızın altından kaydığını size söyleyen monitoring, error tracking ve debugging tekniklerini ele alıyor.

O bölümde ele alınanlar:

  • Gelişmiş CloudWatch dashboard’ları ve alert’leri
  • X-Ray trace analizi ve performance insight’ları
  • Error handling ve circuit breaker pattern’ları
  • Production debugging araçları ve teknikleri

Varsayılan ve Sınırları#

Arkasında ölçülmüş bir eğri olan 1024 MB, Node.js function’larının çoğunu karşılar: API handler’ları, veritabanı okumaları, orta ölçekli transform’lar. Bunu geçersiz kılmayı üç durum haklı çıkarır. Süresi büyük ölçüde bir downstream çağrısına bağlı ince bir proxy’de aşağı inin; ağı beklerken fazladan CPU hiçbir şey kazandırmaz. 1769 MB’nin üstüne yalnızca iş gerçekten paralelse (worker thread’ler veya kendi içinde thread açan native kütüphaneler) ya da bir model veya veri seti memory’ye sığmak zorundaysa çıkın. Daha geniş memory’ye geçmeden önce arm64’ü deneyin: Graviton, aynı memory ayarında GB-saniye başına yaklaşık %20 daha ucuz.

Bir bağımlılık yükseltmesinden, runtime değişiminden veya payload boyutundaki kaymadan sonra Power Tuning’i tekrar çalıştırın. Hedef mümkün olan en hızlı execution değil; latency bütçenizi hâlâ karşılayan en düşük GB-saniye değeri.

Kaynaklar#

AWS Lambda Production Rehberi: 5 Yıllık Gerçek Dünya Deneyimi

5+ yıllık production deneyimine dayalı kapsamlı AWS Lambda rehberi. Cold start optimizasyonu, performans ayarlama, monitoring ve maliyet optimizasyonu ile gerçek savaş hikayeleri ve pratik çözümler.

İlerleme 2/4 yazı tamamlandı

İlgili yazılar