İçeriğe atla

2026-01-28

DynamoDB Rate Limiting: Single Table Design'da Ölçekte Stratejiler

Single Table Design uygulamalarında DynamoDB throttling'i önleme ve yönetme stratejileri. Partition key tasarımı, write sharding, kapasite modları, DAX caching, retry pattern'leri ve yüksek throughput sistemler için CloudWatch monitoring konularını kapsar.

DynamoDB’yi ölçekte kullanırken throttling kaçınılmaz bir sorun haline geliyor. ProvisionedThroughputExceededException hatası, tablo seviyesinde yeterli kapasite olmasına rağmen sıkça karşılaşılan bir durum. Bunun nedenini anlamak için DynamoDB’nin iç mekanizmalarına inmek gerekiyor.

Bu rehberde, Single Table Design uygulamalarında throttling’i önleme ve yönetme konusunda kanıtlanmış pattern’leri ele alacağız. Partition key stratejilerinden, sorunları kullanıcıları etkilemeden yakalayan monitoring yapılandırmalarına kadar her şeyi kapsıyoruz.

DynamoDB’nin Throttling Mekanizmasını Anlamak

DynamoDB, rate limiting için token bucket algoritması kullanıyor. Her partition, provisioned kapasiteyle eşleşen bir hızda doldurulan kendi read ve write token bucket’ına sahip. Token’lar tükendiğinde, istekler throttle ediliyor.

Evet

Hayır

Evet

Hayır

Evet

Hayır

Gelen İstek

Partition'da

token var mı?

İsteği İşler

Burst kapasite

mevcut mu?

Burst Token Kullan

Adaptive kapasite

yardımcı olabilir mi?

Yeniden Dengeleme Bekle

ProvisionedThroughput

ExceededException

İsteği Tekrarla

Exponential Backoff

Başarılı Yanıt

Akılda tutulması gereken kritik limitler:

KaynakLimit
Partition Başına Read Capacity3.000 RCU
Partition Başına Write Capacity1.000 WCU
Partition Başına Depolama10 GB
Item Boyutu400 KB (kesin limit)

İşte işleri zorlaştıran nokta: provisioned kapasite partition’lara dağıtılıyor. 100 RCU ve 3 partition’a sahip bir tablo, her partition’a yaklaşık 33 RCU veriyor demek. Eğer bir partition trafiğin %80’ini alıyorsa, tabloda headroom olmasına rağmen throttle olacak.

// Kavramsal model: Kapasite nasıl dağıtılıyor
interface PartitionCapacity {
  // Tablo seviyesi ayarları
  tableRCU: 100;
  tableWCU: 50;
  partitionCount: 3;

  // Partition başına gerçeklik
  perPartitionRCU: 33;  // ~100/3
  perPartitionWCU: 17;  // ~50/3

  // Sorun: dengesiz trafik
  actualTraffic: {
    partition1: { rcu: 80 };  // 80 > 33 = THROTTLED
    partition2: { rcu: 10 };  // Az kullanılıyor
    partition3: { rcu: 10 };  // Az kullanılıyor
  };
}

Partition Key Tasarımı: Temel

Hot partition’lar çoğu throttling sorununun nedeni. Partition key tasarımını doğru yapmak, hiçbir kapasite artışının çözemeyeceği sorunları önlüyor.

Kaçınılması Gereken Anti-Pattern’ler

// ANTI-PATTERN 1: Düşük kardinalite partition key
interface BadDesign1 {
  PK: 'STATUS#active' | 'STATUS#inactive';  // Sadece 2 değer
  SK: `USER#${string}`;
}
// Sonuç: Tüm aktif kullanıcılar tek partition'da
// 100.000 aktif kullanıcıyla: anında throttling

// ANTI-PATTERN 2: Zaman bazlı partition key
interface BadDesign2 {
  PK: `DATE#${string}`;  // örn., "DATE#2024-01-15"
  SK: `EVENT#${string}`;
}
// Sonuç: Bugünün tüm event'leri tek partition'a düşüyor
// Yoğun saatler hot partition yaratıyor

// ANTI-PATTERN 3: Viral içerik problemi
interface BadDesign3 {
  PK: `POST#${string}`;  // Viral post ID
  SK: `LIKE#${string}`;
}
// Sonuç: Milyonlarca beğenisi olan viral post
// Tek partition yükü kaldıramıyor

// ANTI-PATTERN 4: Büyük kiracı hakimiyeti
interface BadDesign4 {
  PK: `TENANT#${string}`;
  SK: `ORDER#${string}`;
}
// Sonuç: Siparişlerin %80'ine sahip enterprise kiracı
// Onların partition'ı her zaman sıcak

İşe Yarayan Yüksek Kardinalite Pattern’leri

// PATTERN 1: Kullanıcı kapsamlı partition key'ler
interface GoodDesign1 {
  PK: `USER#${userId}`;  // Kullanıcı başına benzersiz
  SK: `ORDER#${timestamp}#${orderId}`;
}
// Sonuç: Milyonlarca benzersiz partition key
// Trafik doğal olarak dağılıyor

// PATTERN 2: Multi-tenant için composite key'ler
interface GoodDesign2 {
  PK: `TENANT#${tenantId}#USER#${userId}`;
  SK: string;
}
// Sonuç: Kiracılar içinde ve arasında eşit dağılım
// Büyük kiracının kullanıcıları yine partition'lara yayılıyor

// PATTERN 3: PK seviyesinde yüksek kardinaliteli hiyerarşik
interface GoodDesign3 {
  PK: `REGION#${region}#STORE#${storeId}`;
  SK: `PRODUCT#${category}#${productId}`;
}
// Sonuç: Sorgular mağaza seviyesinde kapsamlı
// Her mağazanın kendi partition alanı var

// PATTERN 4: Düşük kardinalite sorgular için GSI
interface GoodDesign4 {
  PK: `USER#${userId}`;
  SK: 'METADATA';
  status: 'active' | 'inactive';
  GSI1PK: `STATUS#${status}#SHARD#${shardId}`;  // Shard'lı!
  GSI1SK: `USER#${userId}`;
}
// Ana tablo: Yüksek kardinalite (kullanıcılar)
// GSI: Shard'lama ile status sorgularını yönetiyor

Write Sharding: Hot Key’leri Dağıtmak

İş gereksinimleri düşük kardinaliteli erişim pattern’lerini zorunlu kıldığında, write sharding yükü birden fazla partition’a dağıtıyor.

Sonuclar

DynamoDB Partition'ları

Shard Seçimi

Uygulama

Yazma İsteği

Okuma İsteği

Rastgele Shard ID

Tüm Shard'ları Sorgula

SHARD#0

SHARD#1

SHARD#2

Sonuçları Birleştir

Yanıt

Rastgele Suffix Sharding

Okuma agregasyonunun kabul edilebilir olduğu yazma-yoğun pattern’ler için en uygun:

import { DynamoDBDocumentClient, PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';

const SHARD_COUNT = 10;

const getRandomShard = (): number => {
  return Math.floor(Math.random() * SHARD_COUNT);
};

// Rastgele shard ile yazma - yazmaları eşit dağıtır
const writeToShardedPartition = async (
  client: DynamoDBDocumentClient,
  status: string,
  userId: string,
  userData: Record<string, unknown>
): Promise<void> => {
  const shardId = getRandomShard();

  await client.send(new PutCommand({
    TableName: 'MainTable',
    Item: {
      PK: `STATUS#${status}#SHARD#${shardId}`,
      SK: `USER#${userId}`,
      ...userData
    }
  }));
};

// Okuma tüm shard'larda scatter-gather gerektirir
const readFromAllShards = async (
  client: DynamoDBDocumentClient,
  status: string
): Promise<Record<string, unknown>[]> => {
  const promises = Array.from({ length: SHARD_COUNT }, (_, i) =>
    client.send(new QueryCommand({
      TableName: 'MainTable',
      KeyConditionExpression: 'PK = :pk',
      ExpressionAttributeValues: {
        ':pk': `STATUS#${status}#SHARD#${i}`
      }
    }))
  );

  const results = await Promise.all(promises);
  return results.flatMap(r => r.Items ?? []);
};

Deterministik Sharding

Scatter-gather olmadan belirli item’ları okuman gerektiğinde:

import { createHash } from 'crypto';

const getDeterministicShard = (entityId: string): number => {
  const hash = createHash('md5').update(entityId).digest('hex');
  return parseInt(hash.substring(0, 8), 16) % SHARD_COUNT;
};

// Sipariş ID'sine göre tutarlı shard ile yazma
const writeOrderWithShard = async (
  client: DynamoDBDocumentClient,
  date: string,
  orderId: string,
  orderData: Record<string, unknown>
): Promise<void> => {
  const shardId = getDeterministicShard(orderId);

  await client.send(new PutCommand({
    TableName: 'MainTable',
    Item: {
      PK: `ORDERS#DATE#${date}#SHARD#${shardId}`,
      SK: `ORDER#${orderId}`,
      ...orderData
    }
  }));
};

// Belirli siparişi oku - shard hesapla, tek sorgu
const readOrder = async (
  client: DynamoDBDocumentClient,
  date: string,
  orderId: string
): Promise<Record<string, unknown> | undefined> => {
  const shardId = getDeterministicShard(orderId);

  const result = await client.send(new GetCommand({
    TableName: 'MainTable',
    Key: {
      PK: `ORDERS#DATE#${date}#SHARD#${shardId}`,
      SK: `ORDER#${orderId}`
    }
  }));

  return result.Item;
};

GSI Write Sharding

GSI throttling’inin ana tablo yazmalarını engellemesini önlemek için Global Secondary Index’lere aynı pattern’i uygula:

import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';

// Shard'lı GSI ile CDK tanımı
const table = new dynamodb.Table(this, 'MainTable', {
  partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING },
  sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING },
  billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
});

table.addGlobalSecondaryIndex({
  indexName: 'GSI1',
  partitionKey: { name: 'GSI1PK', type: dynamodb.AttributeType.STRING },
  sortKey: { name: 'GSI1SK', type: dynamodb.AttributeType.STRING },
  projectionType: dynamodb.ProjectionType.ALL,
});

// GSI sharding ile yazma
const writeOrderWithGSIShard = async (
  client: DynamoDBDocumentClient,
  userId: string,
  orderId: string,
  orderDate: string
): Promise<void> => {
  const shardId = getRandomShard();

  await client.send(new PutCommand({
    TableName: 'MainTable',
    Item: {
      PK: `USER#${userId}`,
      SK: `ORDER#${orderDate}#${orderId}`,
      EntityType: 'Order',
      // Shard'lı GSI key'leri
      GSI1PK: `ORDERS#DATE#${orderDate}#SHARD#${shardId}`,
      GSI1SK: `USER#${userId}#ORDER#${orderId}`
    }
  }));
};

Warning: GSI throttling, ana tablo yazmalarına backpressure yaratır. GSI’ın ana tablo yazma hızına ayak uyduramıyorsa, tüm yazmalar başarısız olur. GSI kapasitesini her zaman ana tablo ihtiyaçlarıyla eşleştir.

Kapasite Modu Seçimi

Hayır

Evet

Hayır

Evet

Hayır

Evet

Kapasite Modunu Seç

Trafik

tahmin edilebilir mi?

On-Demand Modu

Kullanım

> %30 mu?

Tepe/Ortalama

oranı < 4:1 mi?

Provisioned + Auto-Scaling

İstek başına yüksek maliyet

Limitler dahilinde anlık ölçekleme

Ölçekte düşük maliyet

1-2 dk ölçekleme gecikmesi

On-Demand Modu: Limitleri Anlamak

On-demand kapasitenin ekipleri hazırlıksız yakalayan ölçekleme kısıtlamaları var:

interface OnDemandBehavior {
  // Yeni tablolar için başlangıç kapasitesi
  initialCapacity: {
    rcu: 12000;  // 4 partition * 3.000 RCU
    wcu: 4000;  // 4 partition * 1.000 WCU
  };

  scaling: {
    // Önceki tepeye anında ölçekle
    previousPeak: 'instant';

    // Önceki tepenin ötesinde: sınırlı büyüme
    beyondPeak: {
      rate: 'Her 30 dakikada ikiye katla';
      limit: '30 dk penceresi içinde 2x aşılamaz';
    };
  };

  // Hesap seviyesi limitleri
  accountLimits: {
    defaultPerTable: 40000;  // RCU ve WCU
    requestIncrease: true;
  };
}

Trafik ani artışlarında bu 2x limiti önemli. Normal trafiğin 10 katı olan bir flash satış, on-demand tarafından hemen karşılanamaz. Tablonun kademeli olarak “ısınması” veya önceden provision edilmiş kapasite kullanması gerekiyor.

Auto-Scaling ile Provisioned

Maliyet hassasiyeti olan tahmin edilebilir iş yükleri için:

import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as appautoscaling from 'aws-cdk-lib/aws-applicationautoscaling';
import { Duration } from 'aws-cdk-lib';

const table = new dynamodb.Table(this, 'MainTable', {
  tableName: 'ProductionTable',
  partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING },
  sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING },
  billingMode: dynamodb.BillingMode.PROVISIONED,
  readCapacity: 100,
  writeCapacity: 50,
});

// Okumalar için auto-scaling
const readScaling = table.autoScaleReadCapacity({
  minCapacity: 10,
  maxCapacity: 1000,
});

readScaling.scaleOnUtilization({
  targetUtilizationPercent: 70,  // Limitlere ulaşmadan önce ölçekle
});

// Yazmalar için auto-scaling
const writeScaling = table.autoScaleWriteCapacity({
  minCapacity: 5,
  maxCapacity: 500,
});

writeScaling.scaleOnUtilization({
  targetUtilizationPercent: 70,
});

// Tahmin edilebilir pattern'ler için zamanlanmış ölçekleme
writeScaling.scaleOnSchedule('ScaleUpMorning', {
  schedule: appautoscaling.Schedule.cron({ hour: '8', minute: '0' }),
  minCapacity: 100,
  maxCapacity: 500,
});

writeScaling.scaleOnSchedule('ScaleDownNight', {
  schedule: appautoscaling.Schedule.cron({ hour: '22', minute: '0' }),
  minCapacity: 5,
  maxCapacity: 100,
});

Karar Çerçevesi

FaktörOn-DemandProvisioned + Auto-Scaling
Trafik tahmin edilebilirliğiTahmin edilemez/aniKademeli değişimlerle sabit
Gereken ölçekleme hızıAnında (2x içinde)1-2 dk gecikme kabul edilebilir
Maliyet hassasiyetiDüşük öncelikYüksek öncelik
Tepe/ortalama oranı> 4:1< 4:1
Geliştirme/testÖnerilenÖnerilmez
Kullanım oranı< %30 ortalama> %30 ortalama

Burst ve Adaptive Kapasite

DynamoDB, dengesiz trafik pattern’lerinde yardımcı olan iki otomatik mekanizma sağlıyor.

Burst Kapasite

Kullanılmayan kapasite 5 dakikaya kadar birikir ve trafik artışlarında tüketilebilir:

interface BurstCapacity {
  accumulation: {
    source: 'Kullanılmayan provision edilmiş kapasite';
    maxRetention: '5 dakika (300 saniye)';
    refillRate: 'Kullanılmayan her RCU/WCU için saniyede 1 token';
  };

  consumption: {
    trigger: 'Trafik provision edilmiş kapasiteyi aşıyor';
    speed: 'Provision edilmiş hızdan daha hızlı tüketilebilir';
    limit: 'Burst bucket tükenene kadar';
  };

  // Önemli kısıtlamalar
  warnings: [
    'Geçici güvenlik önlemi, kapasite planlaması yerine geçmez',
    'DynamoDB arka plan bakımı için kullanabilir',
    'Müsaitlik garantisi yok',
    'İzlenemez veya güvenilmez'
  ];
}

Adaptive Kapasite ve Split-for-Heat

DynamoDB kapasiteyi otomatik olarak hot partition’lara doğru yeniden dengeler ve gerektiğinde onları bölebilir:

interface AdaptiveCapacity {
  behavior: {
    detection: 'Partition başına trafik pattern lerini izler';
    action: 'Throughput u soğuk partition lardan sıcak olanlara yeniden dağıtır';
    limit: 'Partition maksimumunu aşamaz (3.000 RCU, 1.000 WCU)';
  };

  splitForHeat: {
    trigger: 'Tek partition da sürekli yüksek throughput';
    action: 'Partition ı otomatik olarak ikiye böler';
    result: 'O key aralığı için mevcut kapasiteyi ikiye katlar';
    timing: 'Birkaç dakika sürer';
  };

  // Ne zaman yardımcı olur
  scenarios: [
    'Geçici trafik artışları',
    'Kademeli hot partition oluşumu',
    'Dengesiz ama dağıtık erişim pattern leri'
  ];

  // Ne zaman YARDIMCI OLMAZ
  limitations: [
    'Tek hot key (ünlü kişi problemi)',
    'Tüm yazmalar aynı partition key değerine',
    'Düşük kardinaliteli partition key ler',
    'LSI li item collection lar bölünemez'
  ];
}

Note: Adaptive kapasite yeniden dengeleme anında gerçekleşir (Mayıs 2019’dan beri), ancak split-for-heat (partition bölme) birkaç dakika alır. Flash satış senaryoları veya viral içerik için tek bir hot partition key’e her iki mekanizma da yardımcı olamaz. Adaptive kapasiteye güvenmek yerine partition key’leri düzgün tasarla.

Okuma-Yoğun İş Yükleri için DAX

DynamoDB Accelerator (DAX), okuma trafiğini DynamoDB’den yükler, hem gecikmeyi hem de kapasite tüketimini azaltır.

Note: JavaScript için DAX SDK v3 (@amazon-dax-sdk/lib-dax) Mart 2025’te yayınlandı. Standart DynamoDB SDK v3’teki .send() pattern’i yerine aggregated method’lar (.get(), .query()) kullanıyor.

import { DaxDocument } from '@amazon-dax-sdk/lib-dax';
import { DynamoDBDocumentClient, UpdateCommand } from '@aws-sdk/lib-dynamodb';

// DAX client kurulumu (AWS SDK v3 uyumlu)
const createDaxClient = (endpoints: string[]): DaxDocument => {
  return new DaxDocument({
    endpoints,
    region: process.env.AWS_REGION ?? 'us-east-1',
  });
};

// İşlem tipine göre seçim için client factory
interface ClientFactory {
  daxClient: DaxDocument;  // Cache'lenebilir okumalar için
  dynamoClient: DynamoDBDocumentClient;  // Yazmalar, strong consistency için
}

// Kullanım pattern'i: DAX üzerinden okumalar, doğrudan yazmalar
const productService = {
  // DAX üzerinden okuma (mikrosaniye gecikme, DynamoDB'yi rahatlatır)
  // Not: DaxDocument .send() yerine agregat metodlar kullanır
  getProduct: async (
    factory: ClientFactory,
    productId: string
  ): Promise<Record<string, unknown> | undefined> => {
    const result = await factory.daxClient.get({
      TableName: 'Products',
      Key: { PK: `PRODUCT#${productId}`, SK: 'METADATA' }
    });
    return result.Item;
  },

  // DAX üzerinden sorgu (cache'lenmiş sonuç setleri)
  getProductsByCategory: async (
    factory: ClientFactory,
    category: string
  ): Promise<Record<string, unknown>[]> => {
    const result = await factory.daxClient.query({
      TableName: 'Products',
      IndexName: 'GSI1',
      KeyConditionExpression: 'GSI1PK = :category',
      ExpressionAttributeValues: { ':category': `CATEGORY#${category}` }
    });
    return result.Items ?? [];
  },

  // Doğrudan DynamoDB'ye yaz
  // ÖNEMLİ: DAX sadece DAX ÜZERİNDEN yapılan yazmalar için cache'i
  // otomatik invalidate eder. DynamoDB'ye doğrudan yapılan yazmalar
  // (DAX'ı bypass eden), TTL süresi dolana kadar DAX cache'ine yansımaz.
  // Write-through caching için daxClient.put() kullan.
  updateProduct: async (
    factory: ClientFactory,
    productId: string,
    updates: Record<string, unknown>
  ): Promise<void> => {
    await factory.dynamoClient.send(new UpdateCommand({
      TableName: 'Products',
      Key: { PK: `PRODUCT#${productId}`, SK: 'METADATA' },
      UpdateExpression: 'SET #name = :name, #price = :price',
      ExpressionAttributeNames: { '#name': 'name', '#price': 'price' },
      ExpressionAttributeValues: updates
    }));
  }
};

DAX Ne Zaman Mantıklı

Kullanım SenaryosuDAX Değeri
Ürün katalogları (yüksek okuma, düşük yazma)Yüksek
Kullanıcı oturumları (çoğunlukla okuma)Yüksek
Yapılandırma verisi (nadiren değişir)Yüksek
Flash satış ürün sayfalarıÇok Yüksek
Yazma-yoğun iş yükleriDüşük
Strong consistency gereksinimleriYok
Düşük trafik (< 200 istek/sn)Negatif (maliyet yükü)
Rastgele erişim pattern’leri (< %80 hit rate)Düşük

Veri Tipine Göre TTL Stratejisi

const daxTTLStrategy = {
  staticData: {
    ttl: 3600000,  // 1 saat
    examples: ['Ürün kataloğu', 'Kategori listesi', 'Yapılandırma']
  },
  semiStatic: {
    ttl: 300000,  // 5 dakika (varsayilan)
    examples: ['Kullanıcı profilleri', 'Ayarlar', 'Tercihler']
  },
  dynamic: {
    ttl: 60000,  // 1 dakika
    examples: ['Stok sayıları', 'Müsaitlik', 'Fiyatlandırma']
  }
};

Retry Stratejileri ve Circuit Breaker’lar

Throttling’i düzgün yönetmek uygun retry mantığı gerektirir. AWS SDK yerleşik retry sağlar, ancak batch işlemler ek handling gerektiriyor.

SDK Yapılandırması

import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';

const createClientWithRetry = (): DynamoDBDocumentClient => {
  const client = new DynamoDBClient({
    maxAttempts: 10,
    retryMode: 'adaptive',  // DynamoDB için önerilen
    // Adaptive mod kaynak başına throttling'i izler
    // ve throttle edilen tablolar için throughput'u azaltır
  });

  return DynamoDBDocumentClient.from(client);
};

Batch İşlemler: Unprocessed Item’ları Yönetmek

SDK batch işlemlerden dönen unprocessed item’ları otomatik olarak yeniden DENEMIYOR:

import { DynamoDBDocumentClient, BatchWriteCommand } from '@aws-sdk/lib-dynamodb';

const batchWriteWithRetry = async (
  client: DynamoDBDocumentClient,
  tableName: string,
  items: Record<string, unknown>[],
  maxRetries: number = 5
): Promise<void> => {
  const chunks = chunkArray(items, 25);  // BatchWrite limiti

  for (const chunk of chunks) {
    let unprocessed: Record<string, unknown>[] | undefined = chunk;
    let attempts = 0;

    while (unprocessed && unprocessed.length > 0 && attempts < maxRetries) {
      const result = await client.send(new BatchWriteCommand({
        RequestItems: {
          [tableName]: unprocessed.map(item => ({
            PutRequest: { Item: item }
          }))
        }
      }));

      const unprocessedItems = result.UnprocessedItems?.[tableName];

      if (unprocessedItems && unprocessedItems.length > 0) {
        unprocessed = unprocessedItems
          .map(req => req.PutRequest?.Item as Record<string, unknown>)
          .filter(Boolean);

        // Jitter ile exponential backoff
        const delay = Math.min(100 * Math.pow(2, attempts), 5000);
        const jitter = delay * 0.2 * Math.random();
        await sleep(delay + jitter);

        attempts++;
      } else {
        unprocessed = undefined;
      }
    }

    if (unprocessed && unprocessed.length > 0) {
      throw new Error(
        `${maxRetries} denemeden sonra ${unprocessed.length} item yazilamadi`
      );
    }
  }
};

const chunkArray = <T>(array: T[], size: number): T[][] => {
  const chunks: T[][] = [];
  for (let i = 0; i < array.length; i += size) {
    chunks.push(array.slice(i, i + size));
  }
  return chunks;
};

const sleep = (ms: number): Promise<void> =>
  new Promise(resolve => setTimeout(resolve, ms));

Sürekli Throttling için Circuit Breaker

Throttling devam ettiğinde, circuit breaker retry fırtınalarını önler:

import {
  ProvisionedThroughputExceededException,
  ThrottlingException
} from '@aws-sdk/client-dynamodb';

interface CircuitBreakerConfig {
  failureThreshold: number;  // Açılmadan önceki başarısızlık sayısı
  resetTimeout: number;  // Tekrar denemeden önceki süre (ms)
}

class DynamoDBCircuitBreaker {
  private failures = 0;
  private lastFailure: number = 0;
  private state: 'closed' | 'open' | 'half-open' = 'closed';

  constructor(private config: CircuitBreakerConfig) {}

  async execute<T>(operation: () => Promise<T>): Promise<T> {
    if (this.state === 'open') {
      if (Date.now() - this.lastFailure > this.config.resetTimeout) {
        this.state = 'half-open';
      } else {
        throw new Error('Circuit breaker açık - istek reddedildi');
      }
    }

    try {
      const result = await operation();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure(error);
      throw error;
    }
  }

  private onSuccess(): void {
    this.failures = 0;
    this.state = 'closed';
  }

  private onFailure(error: unknown): void {
    if (
      error instanceof ProvisionedThroughputExceededException ||
      error instanceof ThrottlingException
    ) {
      this.failures++;
      this.lastFailure = Date.now();

      if (this.failures >= this.config.failureThreshold) {
        this.state = 'open';
      }
    }
  }
}

// Kullanım
const circuitBreaker = new DynamoDBCircuitBreaker({
  failureThreshold: 5,
  resetTimeout: 30000,  // 30 saniye
});

const writeWithProtection = async (
  client: DynamoDBDocumentClient,
  item: Record<string, unknown>
): Promise<void> => {
  await circuitBreaker.execute(async () => {
    await client.send(new PutCommand({
      TableName: 'MainTable',
      Item: item
    }));
  });
};

İstemci Taraflı Rate Limiting

İstek oranlarını proaktif olarak sınırlamak, throttling’in oluşmasını önler:

class TokenBucket {
  private tokens: number;
  private lastRefill: number;

  constructor(
    private maxTokens: number,
    private refillRate: number  // saniyede token
  ) {
    this.tokens = maxTokens;
    this.lastRefill = Date.now();
  }

  async acquire(count: number = 1): Promise<boolean> {
    this.refill();

    if (this.tokens >= count) {
      this.tokens -= count;
      return true;
    }

    // Token'ların kullanılabilir olmasını bekle
    const waitTime = ((count - this.tokens) / this.refillRate) * 1000;
    await sleep(waitTime);
    this.refill();
    this.tokens -= count;
    return true;
  }

  private refill(): void {
    const now = Date.now();
    const elapsed = (now - this.lastRefill) / 1000;
    this.tokens = Math.min(
      this.maxTokens,
      this.tokens + elapsed * this.refillRate
    );
    this.lastRefill = now;
  }
}

// Rate-limited DynamoDB wrapper
class RateLimitedDynamoDB {
  private readBucket: TokenBucket;
  private writeBucket: TokenBucket;

  constructor(
    private client: DynamoDBDocumentClient,
    readCapacity: number,
    writeCapacity: number
  ) {
    // Headroom bırakmak için kapasitenin %80'ini kullan
    this.readBucket = new TokenBucket(readCapacity * 0.8, readCapacity * 0.8);
    this.writeBucket = new TokenBucket(writeCapacity * 0.8, writeCapacity * 0.8);
  }

  async get(
    tableName: string,
    key: Record<string, unknown>
  ): Promise<Record<string, unknown> | undefined> {
    await this.readBucket.acquire(1);  // <4KB item için 1 RCU

    const result = await this.client.send(new GetCommand({
      TableName: tableName,
      Key: key
    }));

    return result.Item;
  }

  async put(
    tableName: string,
    item: Record<string, unknown>
  ): Promise<void> {
    const itemSize = JSON.stringify(item).length;
    const wcuNeeded = Math.ceil(itemSize / 1024);  // KB başına 1 WCU

    await this.writeBucket.acquire(wcuNeeded);

    await this.client.send(new PutCommand({
      TableName: tableName,
      Item: item
    }));
  }
}

CloudWatch Monitoring ve Alerting

Düzgün monitoring, throttling’i kullanıcıları etkilemeden önce yakalar.

Temel Metrikler

const throttlingMetrics = {
  primary: [
    {
      name: 'ThrottledRequests',
      description: 'Throttle edilen herhangi bir istek',
      alarm: '1 dakika içinde Sum > 0',
      action: 'Hemen araştır'
    },
    {
      name: 'ReadThrottleEvents',
      description: 'Bireysel okuma throttle event leri',
      alarm: 'Dakikada Sum > 10',
      action: 'Partition key tasarımını kontrol et veya kapasiteyi artır'
    },
    {
      name: 'WriteThrottleEvents',
      description: 'Bireysel yazma throttle event leri',
      alarm: 'Dakikada Sum > 10',
      action: 'Write sharding uygula'
    }
  ],

  utilization: [
    {
      name: 'ConsumedReadCapacityUnits',
      alarm: '5 dakika boyunca ortalama > provision edilmişin %80 i',
      action: 'Ölçekle veya auto-scaling aktive et'
    },
    {
      name: 'ConsumedWriteCapacityUnits',
      alarm: '5 dakika boyunca ortalama > provision edilmişin %80 i',
      action: 'Ölçekle veya auto-scaling aktive et'
    }
  ],

  gsi: [
    {
      name: 'OnlineIndexThrottleEvents',
      description: 'GSI throttling (backpressure yaratır)',
      alarm: 'Herhangi bir oluşum',
      action: 'GSI kapasitesini artır'
    }
  ],

  // Detaylı throttle metrikleri (belirli sorunları teşhis etmek için faydalı)
  advanced: [
    { name: 'ReadMaxOnDemandThroughputThrottleEvents', description: 'On-demand max throughput aşıldı' },
    { name: 'WriteMaxOnDemandThroughputThrottleEvents', description: 'On-demand max throughput aşıldı' },
    { name: 'ReadAccountLimitThrottleEvents', description: 'Hesap seviyesi limit aşıldı' },
    { name: 'WriteAccountLimitThrottleEvents', description: 'Hesap seviyesi limit aşıldı' },
    { name: 'ReadKeyRangeThroughputThrottleEvents', description: 'Partition seviyesi limit aşıldı' },
    { name: 'WriteKeyRangeThroughputThrottleEvents', description: 'Partition seviyesi limit aşıldı' }
  ]
};

CDK Alarm Yapılandırması

import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as cloudwatch_actions from 'aws-cdk-lib/aws-cloudwatch-actions';
import * as sns from 'aws-cdk-lib/aws-sns';
import { Duration } from 'aws-cdk-lib';

const createThrottlingAlarms = (
  table: dynamodb.Table,
  alertTopic: sns.Topic
): cloudwatch.Alarm[] => {
  const alarms: cloudwatch.Alarm[] = [];

  // Throttled requests alarmı - acil dikkat
  alarms.push(new cloudwatch.Alarm(table, 'ThrottlingAlarm', {
    alarmName: `${table.tableName}-Throttling`,
    metric: table.metricThrottledRequestsForOperations({
      operations: [
        dynamodb.Operation.GET_ITEM,
        dynamodb.Operation.PUT_ITEM,
        dynamodb.Operation.QUERY,
        dynamodb.Operation.SCAN
      ],
      period: Duration.minutes(1)
    }),
    threshold: 1,
    evaluationPeriods: 1,
    comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
    treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
  }));

  // Yüksek okuma kullanımı - erken uyarı
  alarms.push(new cloudwatch.Alarm(table, 'HighReadUtilization', {
    alarmName: `${table.tableName}-HighReadUtilization`,
    metric: new cloudwatch.MathExpression({
      expression: 'm1 / m2 * 100',
      usingMetrics: {
        m1: table.metricConsumedReadCapacityUnits({ period: Duration.minutes(5) }),
        m2: table.metricProvisionedReadCapacityUnits({ period: Duration.minutes(5) })
      }
    }),
    threshold: 80,
    evaluationPeriods: 3,
    comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
  }));

  // SNS action'larını ekle
  alarms.forEach(alarm => {
    alarm.addAlarmAction(new cloudwatch_actions.SnsAction(alertTopic));
  });

  return alarms;
};

Hot Key Tespiti için Contributor Insights

Hangi partition key’lerin throttling’e neden olduğunu belirlemek için Contributor Insights’ı etkinleştir:

import { DynamoDBClient, UpdateContributorInsightsCommand } from '@aws-sdk/client-dynamodb';

// Mod seçenekleri:
// - ACCESSED_AND_THROTTLED_KEYS: Tüm erişilen key'ler + throttle edilen key'ler (varsayılan, daha maliyetli)
// - THROTTLED_KEYS: Sadece throttle edilen key'ler (throttle debug'ı için maliyet-etkin)

const enableContributorInsights = async (
  client: DynamoDBClient,
  tableName: string
): Promise<void> => {
  await client.send(new UpdateContributorInsightsCommand({
    TableName: tableName,
    ContributorInsightsAction: 'ENABLE',
  }));
};

// Contributor Insights şunları ortaya çıkarır:
// - Tüketilen kapasiteye göre en yoğun partition key'ler
// - Throttle edilen partition key'ler
// - Zaman içindeki erişim pattern'leri
// Single Table Design throttling debug'ı için temel
// İpucu: Sadece throttling debug'ı için THROTTLED_KEYS modunu kullan (daha düşük maliyet)

Yaygın Tuzaklar ve Çözümler

Tuzak 1: Adaptive Kapasiteye Güvenmek

// YANLIŞ: "DynamoDB hot partition'ları otomatik yönetir" varsaymak
// Gerçeklik: Adaptive yeniden dengeleme anında, ama split-for-heat dakikalar alıyor
// Hiçbiri tek hot partition key'e yardımcı olmaz (ünlü kişi problemi)
// Tek key'de flash satış veya viral içerik = ne olursa olsun throttling

// DOĞRU: En başından eşit dağılım için tasarla
// Bilinen düşük kardinaliteli pattern'ler için write sharding kullan

Tuzak 2: GSI Kapasitesini Göz Ardı Etmek

// YANLIŞ: GSI kapasitesini ana tablodan düşük ayarlamak
// Varsayım: "GSI daha az trafik alıyor"
// Sonuç: GSI throttling TÜM ana tablo yazmalarını engelliyor

// DOĞRU: GSI kapasitesi >= ana tablo yazma kapasitesi
// Veya otomatik ölçekleme için on-demand kullan

Tuzak 3: On-Demand Ölçekleme Varsayımları

// YANLIŞ: "On-demand herhangi bir seviyeye anında ölçeklenir"
// Gerçeklik: 30 dakikalık pencerelerde 2x ölçekleme limiti
// 50k istek/sn'den 250k istek/sn'ye ~1 saat alıyor

// DOĞRU: Beklenen artışlardan önce ön ısıtma yap
// Veya planlanan etkinlikler için yüksek kapasiteli provisioned kullan
// İpucu: Yeni veya restore edilmiş tablolarda daha yüksek başlangıç
// throughput değerleri için AWS'nin "warm throughput" özelliğini düşün

Tuzak 4: Batch Retry Mantığını Kaçırmak

// YANLIŞ: BatchWriteItem'ın tüm item'ları işlediğini varsaymak
const result = await client.send(new BatchWriteCommand({ ... }));
// Bazı item'lar başarısız olmuş olabilir!

// DOĞRU: Her zaman unprocessed item'ları kontrol et ve yeniden dene
if (result.UnprocessedItems &&
    Object.keys(result.UnprocessedItems).length > 0) {
  // Exponential backoff retry uygula
}

Tuzak 5: Partition Başına Metrikleri İzlememek

// YANLIŞ: Sadece tablo seviyesi kapasiteyi izlemek
// "Tabloda 500 WCU mevcut, neden throttling var?"

// DOĞRU: Contributor Insights'ı etkinleştir
// Ortaya çıkarır: Bir partition key 1.000 WCU limitini tüketiyor
// Tablo seviyesi headroom, partition seviyesi throttling'e yardımcı olmaz

Temel Çıkarımlar

  1. Önce Partition Key’leri Tasarla: Hot partition’lar throttling sorunlarının %90’ına neden oluyor
  2. Partition Başına Limitleri Anla: 3.000 RCU / 1.000 WCU per partition gerçek kısıt
  3. Write Sharding Çalışıyor: 10 shard = aynı erişim pattern’i için 10x yazma throughput’u
  4. Adaptive Kapasitenin Limitleri Var: Yeniden dengeleme anında, ama split-for-heat dakikalar alır; hiçbiri tek hot key’lere yardımcı olmaz
  5. On-Demand’ın Limitleri Var: 30 dakika içinde 2x ölçekleme, sınırsız değil
  6. GSI Throttling Yazmaları Engelliyor: Kapasite eşleştirme şart
  7. DAX Yüksek Hit Rate Gerektirir: %80 cache hit rate’in altında, ROI negatif
  8. Contributor Insights’ı İzle: Single Table Design’da hot key’leri belirlemenin tek yolu
  9. Unprocessed Item’ları Yeniden Dene: SDK batch işlem başarısızlıklarını otomatik yeniden denemiyor
  10. Etkinlikler için Ön Isıtma Yap: Hem provisioned hem de on-demand, trafik artışları için hazırlık gerektiriyor

Throttling’e dayanıklı DynamoDB uygulamaları oluşturmak, bu mekanizmaları anlamayı ve her katmanda uygun pattern’leri uygulamayı gerektiriyor. Partition key tasarımıyla başla, gerektiğinde sharding ekle, düzgün retry’lar uygula ve agresif bir şekilde izle. Sonuç, beklenmedik throttling incident’ları olmadan öngörülebilir şekilde ölçeklenen bir sistem.

İlgili yazılar

Caching Stratejileri: Yerel Bellekten Distributed Sistemlere

In-memory uygulama cache'lerinden distributed Redis cluster'lara ve CDN edge caching'e kadar çok katmanlı caching stratejilerini uygulamaya yönelik kapsamlı bir rehber. Cache-aside ve write-through pattern'leri ne zaman kullanılır, ElastiCache ile MemoryDB arasında nasıl seçim yapılır ve production'da cache stampede nasıl önlenir öğrenin.

cachingredisaws+5
AWS AppSync & GraphQL: Production-Ready Real-time API'ler Geliştirmek

AWS AppSync ile ölçeklenebilir real-time API'ler geliştirmek için kapsamlı bir rehber: JavaScript resolver'lar, subscription filtering, caching stratejileri ve infrastructure as code pattern'leri.

awsappsyncgraphql+5
DynamoDB Single-Table Design: Kapsamlı Modelleme Rehberi

DynamoDB single-table design'ı ilişkileri modelleme, GSI ve LSI seçimi, DAX optimizasyonu ve production NoSQL sistemlerinde yaygın hataları önleme konularında pratik örneklerle öğren.

dynamodbnosqlaws+4
Key-Value Storage Temelleri - Doğru Çözümü Anlama ve Seçme Rehberi

Key-value storage hakkında dört temel soruyu yanıtlayan kapsamlı bir temel rehber: KV storage nedir? Nerede kullanılır? Neden KV storage seçilir? Hangi tech stack'lerde hangi çözümler var?

redisdynamodbkey-value-storage+5
AWS CDK Link Shortener Bölüm 4: Production Deployment ve Optimizasyon

Multi-environment deployment stratejileri, ölçekte performans optimizasyonu, ve maliyet yönetimi. Production deneyimleri ve öğrenilen dersler ile doğru monitoring ve incident response pattern'ları.

aws-cdklambdadynamodb+6