DynamoDB Throttling: Hot Partition Çözümleri, Write Sharding ve Retry Stratejileri
Single Table Design'da DynamoDB throttling'i önleme ve yönetme: partition key tasarımı, write sharding, kapasite modları, DAX caching ve retry pattern'leri.
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şılıyor; çünkü DynamoDB kapasiteyi her partition için ayrı ayrı uyguluyor.
En çok fark yaratan kaldıraç partition key tasarımı. Write sharding, kapasite modları, DAX ve retry mantığı bu seçimin üzerine ek pay kazandırıyor. Single Table Design’da pratik sıra şöyle: önce yüksek kardinaliteli key’ler, yüksek kardinalite kurulamayan erişim pattern’leri için sharding, sonra geriye kalanın etrafına retry ve monitoring.
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ı#
Hot partition’lar çoğu throttling sorununun nedeni.
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 ile Hot Key Dağıtımı#
İş 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 ile 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';
import { GetCommand } from '@aws-sdk/lib-dynamodb';
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 Sınırlamaları#
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,
});
Hangi Modu Seçmeli#
| 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'lı 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.
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 (varsayılan)
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 Yazmalarda Unprocessed Item Yönetimi#
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 yazılamadı`
);
}
}
};
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 Tarafında 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)
Sık Tekrarlanan Throttling Hataları#
Adaptive Kapasiteye Güvenmek#
DynamoDB’nin hot partition’ları otomatik yönettiğini varsaymak, burada sık yapılan hata. Adaptive yeniden dengeleme anında gerçekleşiyor ama split-for-heat dakikalar alıyor; ikisi de tek bir hot partition key’de (ünlü kişi problemi) işe yaramıyor. Tek key’de flash satış veya viral içerik, tablo seviyesindeki headroom’dan bağımsız olarak throttling’e yol açıyor. En başından eşit dağılım için tasarla ve doğası gereği düşük kardinalite kalan erişim pattern’leri için write sharding’e başvur.
GSI Kapasitesini Göz Ardı Etmek#
GSI kapasitesini ana tablodan düşük ayarlamak da sık görülen bir hata; genellikle GSI’nin daha az trafik aldığı varsayımıyla yapılıyor. Bunun önemi yok: GSI throttle olduğunda, beslediği tüm ana tablo yazmalarını engelliyor. GSI yazma kapasitesini ana tablonunkine eşit veya üstünde tut, ya da otomatik ölçekleme için on-demand’a geç.
On-Demand Ölçekleme Varsayımları#
On-demand’ın herhangi bir seviyeye anında ölçeklendiği varsayımı doğru değil. Ölçekleme limiti 30 dakikalık bir pencerede 2x; yani 50k istek/sn’den 250k istek/sn’ye çıkmak yaklaşık 1 saat alıyor. Beklenen artışlardan önce ön ısıtma yap, ya da planlanan etkinlikler için yüksek kapasiteli provisioned’a geç. Yeni veya restore edilmiş tablolarda daha yüksek başlangıç throughput değerleri için AWS’nin “warm throughput” özelliği burada devreye giren araç.
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
}
Partition Başına Metrikleri İzlememek#
Sadece tablo seviyesi kapasiteyi izlemek asıl soruyu gizliyor: 500 WCU’su müsait görünen bir tablo neden throttle ediliyor? Contributor Insights bu soruyu doğrudan yanıtlıyor ve bir partition key’in tek başına 1.000 WCU limitini tükettiğini gösteriyor. Kısıt partition seviyesindeyken tablo seviyesindeki headroom işe yaramıyor.
Key tasarımını öne alan bu sıra, erişim pattern’inde key olarak kullanılabilecek doğal ve yüksek kardinaliteli bir alan bulunduğu sürece geçerli. İş gereksinimi düşük kardinaliteli bir okuma yolunu zorunlu kıldığında sıra değişiyor: durum panoları, günlük sipariş özetleri, tek bir viral kayıt. Bu durumda sharding artı scatter-gather okuma doğru takas; bedeli okuma fan-out’u ve daha karmaşık bir sorgu katmanı olarak ödeniyor. Trafik tahmin edilemez ya da okuma ağırlıklıysa on-demand kapasite ve DAX’a daha erken başvurmak mantıklı.
Kaynaklar#
- Amazon DynamoDB’de Throttling Sorunlarını Giderme (yeni sekmede açılır) - DynamoDB throttling sorunlarını teşhis etmek ve çözmek için resmi rehber
- DynamoDB Burst ve Adaptive Kapasite (yeni sekmede açılır) - Adaptive kapasitenin hot partition’lara throughput’u otomatik olarak nasıl yeniden dağıttığı
- DynamoDB’de Partition Key Tasarımı için En İyi Uygulamalar (yeni sekmede açılır) - Write sharding, yüksek kardinaliteli key’ler ve hot partition’dan kaçınma
- DynamoDB On-Demand Kapasite Modu (yeni sekmede açılır) - Sunucusuz throughput ölçekleme ve tablo başına tepe trafik limitleri
- DynamoDB Accelerator (DAX) ile Bellek İçi Hızlandırma (yeni sekmede açılır) - DAX cache mimarisi, item cache ile query cache farkı ve TTL yapılandırması
- DynamoDB için CloudWatch Contributor Insights ile Veri Erişimini Analiz Etme (yeni sekmede açılır) - Single Table Design’da hot key’leri ve en çok throttle edilen item’ları belirleme
- DynamoDB ile Tasarım için En İyi Uygulamalar (yeni sekmede açılır) - Tablo tasarımı, GSI’lar ve kapasite planlaması için resmi AWS önerileri
İlgili yazılar
DynamoDB'de OFFSET, keyfi ORDER BY ve ucuz COUNT yok. İmzalı next/prev cursor sunun, sıralamayı sort key ile modelleyin, toplam sayıyı listeden çıkarın.
dynamodb · aws · data-storage-orm +2
Çok katmanlı caching için pratik rehber: in-memory, Redis ve CDN katmanları, cache-aside ve write-through, ElastiCache ve MemoryDB, stampede önleme.
caching · redis · aws +4
Transactional Outbox Pattern'in dağıtık sistemlerdeki dual-write problemini nasıl çözdüğünü, PostgreSQL, DynamoDB ve CDC araçlarıyla pratik implementasyonlarını öğren.
distributed-systems · microservices · event-driven +5
AWS AppSync ile ölçeklenebilir real-time API'ler: JavaScript resolver'lar, subscription filtering, caching stratejileri ve infrastructure as code pattern'leri.
aws · graphql · serverless +4
DynamoDB single-table design'da ilişki modelleme, GSI ve LSI seçimi, DAX optimizasyonu ve production'da yaygın hatalardan kaçınmayı pratik örneklerle öğren.
dynamodb · data-storage-orm · aws +1