Skip to content
~/sph.sh

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.

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.

typescript
// Kavramsal model: Kapasite nasıl dağıtılıyorinterface 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

typescript
// ANTI-PATTERN 1: Düşük kardinalite partition keyinterface 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 keyinterface 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 problemiinterface 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ı hakimiyetiinterface 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

typescript
// PATTERN 1: Kullanıcı kapsamlı partition key'lerinterface 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'lerinterface 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şikinterface 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 GSIinterface 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.

Rastgele Suffix Sharding

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

typescript
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ırconst 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 gerektirirconst 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:

typescript
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 yazmaconst 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 sorguconst 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:

typescript
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 yazmaconst 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

On-Demand Modu: Limitleri Anlamak

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

typescript
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:

typescript
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-scalingconst readScaling = table.autoScaleReadCapacity({  minCapacity: 10,  maxCapacity: 1000,});
readScaling.scaleOnUtilization({  targetUtilizationPercent: 70,  // Limitlere ulaşmadan önce ölçekle});
// Yazmalar için auto-scalingconst writeScaling = table.autoScaleWriteCapacity({  minCapacity: 5,  maxCapacity: 500,});
writeScaling.scaleOnUtilization({  targetUtilizationPercent: 70,});
// Tahmin edilebilir pattern'ler için zamanlanmış ölçeklemewriteScaling.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:

typescript
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:

typescript
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.

typescript
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 factoryinterface ClientFactory {  daxClient: DaxDocument;                 // Cache'lenebilir okumalar için  dynamoClient: DynamoDBDocumentClient;   // Yazmalar, strong consistency için}
// Kullanım pattern'i: DAX üzerinden okumalar, doğrudan yazmalarconst 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

typescript
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ı

typescript
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:

typescript
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:

typescript
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ımconst 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:

typescript
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 wrapperclass 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

typescript
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ı

typescript
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:

typescript
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

typescript
// 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

typescript
// 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ı

typescript
// 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

typescript
// YANLIŞ: BatchWriteItem'ın tüm item'ları işlediğini varsaymakconst 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 deneif (result.UnprocessedItems &&    Object.keys(result.UnprocessedItems).length > 0) {  // Exponential backoff retry uygula}

Tuzak 5: Partition Başına Metrikleri İzlememek

typescript
// 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