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.
Akılda tutulması gereken kritik limitler:
| Kaynak | Limit |
|---|---|
| Partition Başına Read Capacity | 3.000 RCU |
| Partition Başına Write Capacity | 1.000 WCU |
| Partition Başına Depolama | 10 GB |
| Item Boyutu | 400 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.
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
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ör | On-Demand | Provisioned + Auto-Scaling |
|---|---|---|
| Trafik tahmin edilebilirliği | Tahmin edilemez/ani | Kademeli değişimlerle sabit |
| Gereken ölçekleme hızı | Anında (2x içinde) | 1-2 dk gecikme kabul edilebilir |
| Maliyet hassasiyeti | Düşük öncelik | Yü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 Senaryosu | DAX 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ükleri | Düşük |
| Strong consistency gereksinimleri | Yok |
| 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
- Önce Partition Key’leri Tasarla: Hot partition’lar throttling sorunlarının %90’ına neden oluyor
- Partition Başına Limitleri Anla: 3.000 RCU / 1.000 WCU per partition gerçek kısıt
- Write Sharding Çalışıyor: 10 shard = aynı erişim pattern’i için 10x yazma throughput’u
- Adaptive Kapasitenin Limitleri Var: Yeniden dengeleme anında, ama split-for-heat dakikalar alır; hiçbiri tek hot key’lere yardımcı olmaz
- On-Demand’ın Limitleri Var: 30 dakika içinde 2x ölçekleme, sınırsız değil
- GSI Throttling Yazmaları Engelliyor: Kapasite eşleştirme şart
- DAX Yüksek Hit Rate Gerektirir: %80 cache hit rate’in altında, ROI negatif
- Contributor Insights’ı İzle: Single Table Design’da hot key’leri belirlemenin tek yolu
- Unprocessed Item’ları Yeniden Dene: SDK batch işlem başarısızlıklarını otomatik yeniden denemiyor
- 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
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.
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.
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.
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?
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ı.