Caching Stratejileri: Yerel Bellekten Distributed Sistemlere
Çok katmanlı caching için pratik rehber: in-memory, Redis ve CDN katmanları, cache-aside ve write-through, ElastiCache ve MemoryDB, stampede önleme.
Etkili caching çok katmanlı bir problemdir. En hızlı katman süreç içi bir LRU, sonraki uzaktaki bir cache (Redis ya da Memcached), onun üstünde edge’de bir CDN. Her katmanın kendi geçersiz kılma semantiği, tutarlılık garantisi ve hata biçimi vardır. %15 hit rate’e sahip bir Redis cluster’ı genelde yanlış araç seçiminden çok, yanlış katmanda yapılan yanlış işi gösterir. Popüler bir anahtar expire olduğunda oluşan thundering herd ise bir sürü-koruma (stampede) problemidir ve tek katmanlı her cache bunu yaşar.
Read-heavy servislerin çoğunu karşılayan varsayılan, paylaşımlı bir Redis üzerinde cache-aside; önünde küçük bir süreç içi LRU, onun da önünde bir CDN. Write-through, MemoryDB ve DAX bu şekilden sapmalardır ve her birinin gerekçesi tek bir kısıttır: yazma sonrası okuma tutarlılığı, kalıcılık ya da yalnızca DynamoDB üzerinden giden erişim. Geri kalanı invalidation disiplini ve stampede korumasıdır.
Cache Pattern’leri Nasıl Çalışır#
Cache-aside ile write-through arasındaki seçim, hangi şikayeti alacağını belirler: stale okuma mı, yavaş write mı. Aşağıdaki pattern’lerin her biri production yükü altında farklı bir bedel taşır.
Cache-Aside (Lazy Loading)#
Uygulama hem cache’i hem database’i doğrudan yönetir. Okumada önce cache’i kontrol et. Miss durumunda database’den çek ve cache’i doldur. En yaygın pattern bu çünkü basit ve verimli.
class UserRepository {
private redis: Redis;
private db: Database;
async getUser(id: string): Promise<User> {
// Önce cache'i kontrol et
const cached = await this.redis.get(`user:${id}`);
if (cached) {
return JSON.parse(cached);
}
// Cache miss - database'den çek
const user = await this.db.users.findById(id);
// Cache'e TTL ile kaydet
await this.redis.set(
`user:${id}`,
JSON.stringify(user),
'EX',
3600 // 1 saat
);
return user;
}
}
Cache-aside ne zaman kullanılır:
- Tüm datanın sık erişilmediği read-heavy workload’lar
- Hafif staleness tolere edilebilen data
- Sadece gerçekten kullanılan şeyleri cache’lemek istiyorsan
Trade-off’lar:
- İlk request cache miss latency yaşar
- Popüler expire olan key’lerde cache stampede riski
- Verimli memory kullanımı çünkü sadece erişilen data cache’lenir
Write-Through Pattern#
Her yazma hem cache’e hem database’e gider. Cache her yazmada tazelenir, dolayısıyla okuyucu normalde son writer’ın kaydettiği değeri görür.
class UserRepository {
async updateUser(id: string, data: Partial<User>): Promise<User> {
// Önce database'i güncelle
const user = await this.db.users.update(id, data);
// Hemen cache'i güncelle
await this.redis.set(
`user:${id}`,
JSON.stringify(user),
'EX',
3600
);
return user;
}
async getUser(id: string): Promise<User> {
// Cache'i kontrol et (yeni güncellenen user'lar için her zaman orada olmalı)
const cached = await this.redis.get(`user:${id}`);
if (cached) {
return JSON.parse(cached);
}
// Cache miss için cache-aside fallback
const user = await this.db.users.findById(id);
await this.redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
return user;
}
}
Write-through ne zaman kullanılır:
- Yazma sonrası okuma tazeliği write latency’sinden daha önemli
- Write operasyonları sık
- Read-heavy workload’lar her yazmada tazelenen cache’den faydalanır
Trade-off’lar:
- Write latency artar (hem cache hem database güncellemeli)
- Hiç okunmayacak data’yı cache’ler
- Daha yüksek cache hit rate’leri çünkü cache her zaman dolu
- Eşzamanlı bir write ya da başarısız bir cache güncellemesi eski değeri cache’te bırakabilir; versiyon kontrolü kullan ya da key’i set etmek yerine sil
Write-Behind (Write-Back) Pattern#
Yazmalar hemen cache’e gider, sonra asenkron olarak database’e yazılır. Mükemmel write performance sağlar ama karmaşıklık ve potansiyel data loss riski getirir.
class AnalyticsRepository {
async trackEvent(event: Event): Promise<void> {
// Hemen cache'e yaz (hızlı response)
await this.redis.lpush(
'analytics:queue',
JSON.stringify(event)
);
// Background worker queue'yu asenkron işler
}
// Ayrı background worker
async processQueue(): Promise<void> {
while (true) {
// Queue'dan event'leri batch işle
const events = await this.redis.lrange('analytics:queue', 0, 99);
if (events.length > 0) {
// Database'e batch insert
await this.db.analytics.batchInsert(
events.map(e => JSON.parse(e))
);
// İşlenen event'leri sil
await this.redis.ltrim('analytics:queue', 100, -1);
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
Write-behind ne zaman kullanılır:
- Write-heavy workload’lar (analytics, log’lar, metrikler)
- Cache failure durumunda potansiyel data loss tolere edilebilir
- Database write performance bottleneck
Trade-off’lar:
- Cache persistence öncesi fail olursa data loss riski
- Daha karmaşık implementasyon ve monitoring
- Batching sayesinde mükemmel write performance
Cache Stampede’i Önlemek#
Cache stampede (thundering herd) popüler bir cache key expire olduğunda ve yüzlerce ya da binlerce request eşzamanlı onu yeniden oluşturmaya çalıştığında olur. Database connection pool’un tükenir ve her şey cascade eder.
Nasıl önlenir:
Probabilistic Early Expiration#
Cache expire olmasını beklemek yerine, kalan TTL’e göre probabilistik olarak expiration’dan önce refresh et. Bu refresh yükünü yayar.
async function getWithProbabilisticRefresh<T>(
key: string,
fetcher: () => Promise<T>,
ttl: number,
beta: number = 1.0
): Promise<T> {
const result = await redis.get(key);
if (result) {
const data = JSON.parse(result);
const now = Date.now();
const timeUntilExpiry = (data.expiresAt - now) / 1000;
// Probabilistic early refresh
// Expiry yaklaştıkça refresh olasılığı artar
const shouldRefresh =
timeUntilExpiry / ttl < Math.random() * beta;
if (shouldRefresh) {
// Background'da refresh et, blocking olmadan
void backgroundRefresh(key, fetcher, ttl);
}
return data.value;
}
// Cache miss - stampede önlemek için lock kullan
return getWithLock(key, fetcher, ttl);
}
Distributed Locking#
Cache miss olduğunda, data’yı kimin yeniden oluşturacağını koordine etmek için Redis kullan. Diğer request’ler kısaca bekler ve retry yapar.
async function getWithLock<T>(
key: string,
fetcher: () => Promise<T>,
ttl: number
): Promise<T> {
const lockKey = `lock:${key}`;
// Lock almaya çalış (10 saniye timeout)
const lockAcquired = await redis.set(
lockKey,
'1',
'NX', // Sadece yoksa set et
'EX',
10
);
if (lockAcquired) {
try {
// Lock'u aldık - data'yı çek
const value = await fetcher();
const data = {
value,
expiresAt: Date.now() + ttl * 1000,
};
await redis.set(
key,
JSON.stringify(data),
'EX',
ttl
);
return value;
} finally {
// Her zaman lock'u serbest bırak
await redis.del(lockKey);
}
} else {
// Başka request fetch ediyor - bekle ve retry yap
await new Promise(resolve => setTimeout(resolve, 100));
return getWithProbabilisticRefresh(key, fetcher, ttl);
}
}
Request Coalescing#
Aynı in-flight request’leri uygulama seviyesinde deduplicate et. Aynı cache key için 100 request gelirse, sadece biri gerçekten data çeker.
class CacheManager {
private inflightRequests = new Map<string, Promise<any>>();
async get<T>(
key: string,
fetcher: () => Promise<T>
): Promise<T> {
// Önce cache'i kontrol et
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
// Request zaten in-flight mi kontrol et
const existing = this.inflightRequests.get(key);
if (existing) {
// Mevcut request'e piggyback yap
return existing;
}
// Yeni request oluştur
const promise = fetcher()
.then(async value => {
await redis.set(
key,
JSON.stringify(value),
'EX',
300
);
this.inflightRequests.delete(key);
return value;
})
.catch(error => {
this.inflightRequests.delete(key);
throw error;
});
this.inflightRequests.set(key, promise);
return promise;
}
}
AWS Caching Servisleri: Ne Zaman Hangisi#
AWS ElastiCache, MemoryDB ve DAX sunuyor. Her biri kalıcılık, tutarlılık ve cache’in arkasındaki origin konusunda farklı bir soruya cevap verir.
ElastiCache for Redis#
En iyisi:
- Birden fazla uygulama sunucusu arasında session management
- Genel amaçlı caching katmanı (cache-aside pattern)
- Pub/sub messaging pattern’leri
- Leaderboard’lar, rate limiting, real-time analytics
Teknik özellikler:
- Latency: Sub-milisaniye
- Persistence: Opsiyonel snapshot’lar (real-time değil)
- Consistency: Eventual
- Fiyatlandırma: cache.r6g.large için ~$0.206/saat (13.07 GB) = node başına ~$150/ay
import Redis from 'ioredis';
const redis = new Redis.Cluster(
[
{
host: 'redis-cluster.xxx.cache.amazonaws.com',
port: 6379,
},
],
{
redisOptions: {
password: process.env.REDIS_PASSWORD,
tls: {},
},
clusterRetryStrategy: times =>
Math.min(100 * times, 3000),
enableReadyCheck: true,
maxRetriesPerRequest: 3,
}
);
MemoryDB for Redis#
En iyisi:
- Microservice’ler için birincil veri deposu, cache rolü dahil
- Durability gerektiren real-time analytics
- Redis hızı + ACID garantileri gereken mission-critical uygulamalar
- Finansal transaction’lar, inventory management
Teknik özellikler:
- Latency: Sub-milisaniye read’ler, tek haneli milisaniye write’lar
- Persistence: Transaction log ile tam durable persistence
- Consistency: Strong (senkron replication)
- Multi-AZ: Sıfır data loss ile otomatik failover
- Fiyatlandırma: db.r6g.large için ~$0.406/saat = ~$293/ay (ElastiCache’in 1.5x’i)
MemoryDB’yi ElastiCache yerine ne zaman seçmeli:
- Redis kayıt sistemi olarak görev yapıyor
- Hiç data loss tolere edemezsin
- Strong consistency garantileri gerekli
- Ayrı database + cache mimarisini ortadan kaldırmak istiyorsun
DynamoDB Accelerator (DAX)#
En iyisi:
- Sadece DynamoDB’ye özel hızlandırma
- Read-heavy DynamoDB workload’ları (gaming leaderboard’ları)
- Eventually consistent read’ler kabul edilebilir
- Scale’de mikrosaniye latency gerekli
Teknik özellikler:
- Latency: Cache’li read’ler için mikrosaniye
- Entegrasyon: Native DynamoDB API uyumluluğu
- Consistency: Sadece eventually consistent read’ler
- Fiyatlandırma: dax.r4.large için ~$0.40/saat
Önemli sınırlamalar:
- Yalnızca origin olarak DynamoDB ile çalışır
- Query/scan cache’i get/batch-get cache’inden ayrı
- Strongly consistent read desteği yok
- Conditional update’leri cache’leyemez
Karar Matrisi#
Distributed Cache’ler için Consistent Hashing#
Birden fazla cache node’un varsa, hangi node’un hangi key’i sakladığına nasıl karar verirsin? Basit modulo hashing (hash(key) % N) node’lar değiştiğinde büyük redistribution’a neden olur:
- Server ekle: ~%50 key taşınır
- Server kaldır: ~%50 key taşınır
Consistent hashing redistribution’ı ~1/N key’e minimize eder.
İmplementasyon#
import crypto from 'crypto';
class ConsistentHash {
private ring: Map<number, string> = new Map();
private sortedKeys: number[] = [];
private virtualNodes: number = 150;
private hash(key: string): number {
return parseInt(
crypto
.createHash('md5')
.update(key)
.digest('hex')
.substring(0, 8),
16
);
}
addServer(server: string): void {
// Eşit dağılım için virtual node'lar oluştur
for (let i = 0; i < this.virtualNodes; i++) {
const hash = this.hash(`${server}:vnode:${i}`);
this.ring.set(hash, server);
this.sortedKeys.push(hash);
}
this.sortedKeys.sort((a, b) => a - b);
}
removeServer(server: string): void {
for (let i = 0; i < this.virtualNodes; i++) {
const hash = this.hash(`${server}:vnode:${i}`);
this.ring.delete(hash);
const index = this.sortedKeys.indexOf(hash);
if (index > -1) {
this.sortedKeys.splice(index, 1);
}
}
}
getServer(key: string): string | undefined {
if (this.sortedKeys.length === 0) return undefined;
const hash = this.hash(key);
// Ring'de bir sonraki server için binary search
let idx = this.sortedKeys.findIndex(k => k >= hash);
if (idx === -1) idx = 0; // Wrap around
const serverHash = this.sortedKeys[idx];
return this.ring.get(serverHash);
}
}
// Kullanım
const hashRing = new ConsistentHash();
hashRing.addServer('cache-node-1');
hashRing.addServer('cache-node-2');
hashRing.addServer('cache-node-3');
const server = hashRing.getServer('user:12345');
// Döner: 'cache-node-2'
Virtual Node’ların Önemi#
Virtual node’lar olmadan basit consistent hashing dengesiz dağılım oluşturabilir. Virtual node’lar (vnode’lar) bunu çözer:
- Her fiziksel node ring üzerinde dağılmış 100-200 virtual node alır
- Daha uniform data dağılımı
- Node ekleme/çıkarma sırasında daha smooth load balancing
- Server’ları kapasiteye göre ağırlıklandırabilirsin (daha fazla vnode = daha fazla data)
// Kapasiteye göre ağırlıklandır: vnode sayısı server'ın payıyla ölçeklenir
const vnodesFor = (capacity: number, average: number): number =>
Math.ceil(150 * (capacity / average));
vnodesFor(1.5, 1); // 1.5x kapasiteli server için 225 vnode
vnodesFor(0.5, 1); // 0.5x kapasiteli server için 75 vnode
Multi-Tier Caching Mimarisi#
Kazancın büyük kısmı cache’leri bilinçli katmanlamaktan gelir. Pratik bir üç katmanlı düzen şöyle görünür:
L1: In-Process Memory Cache#
- Boyut: Instance başına 50-100 MB
- TTL: 30-60 saniye
- Amaç: Hot data için ultra-hızlı erişim
- Teknoloji: LRU cache
L2: Distributed Redis Cache#
- Boyut: 10-100 GB cluster
- TTL: 5-60 dakika
- Amaç: Instance’lar arası paylaşımlı cache
- Teknoloji: ElastiCache Redis cluster
L3: CDN Edge Cache#
- Boyut: Sınırsız (CloudFront)
- TTL: 1 saat - 1 yıl
- Amaç: Global edge dağıtımı
- Teknoloji: CloudFront
İmplementasyon#
import { LRUCache } from 'lru-cache';
class MultiTierCache {
private l1Cache: LRUCache<string, any>;
private l2Cache: Redis;
constructor() {
this.l1Cache = new LRUCache({
max: 500, // Max item
maxSize: 50 * 1024 * 1024, // 50 MB
sizeCalculation: (value) => {
return JSON.stringify(value).length;
},
ttl: 1000 * 60, // 1 dakika
});
}
async get<T>(
key: string,
fetcher: () => Promise<T>
): Promise<T> {
// L1: In-memory cache'i kontrol et
if (this.l1Cache.has(key)) {
return this.l1Cache.get(key);
}
// L2: Redis'i kontrol et
const l2Result = await this.l2Cache.get(key);
if (l2Result) {
const value = JSON.parse(l2Result);
// L1'i doldur
this.l1Cache.set(key, value);
return value;
}
// Cache miss - origin'den çek
const value = await fetcher();
// Tüm cache katmanlarını doldur
this.l1Cache.set(key, value);
await this.l2Cache.set(
key,
JSON.stringify(value),
'EX',
3600
);
return value;
}
async invalidate(key: string): Promise<void> {
// Tüm katmanları invalidate et
this.l1Cache.delete(key);
await this.l2Cache.del(key);
}
}
CloudFront Caching Stratejileri#
CDN caching uygulama caching’den farklı. İçeriği global olarak uzun TTL’lerle dağıtıyorsun, bu da invalidation stratejisinin önemli olduğu anlamına gelir.
Cache Behavior Yapılandırması#
Farklı içerik türleri farklı cache policy’leri gerektirir:
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as cdk from 'aws-cdk-lib';
// Static asset'ler (image'lar, CSS, JS)
const staticBehavior = {
pathPattern: '/static/*',
cachePolicy: new cloudfront.CachePolicy(
this,
'StaticCachePolicy',
{
minTtl: cdk.Duration.seconds(0),
defaultTtl: cdk.Duration.hours(24),
maxTtl: cdk.Duration.days(365),
enableAcceptEncodingGzip: true,
enableAcceptEncodingBrotli: true,
queryStringBehavior:
cloudfront.CacheQueryStringBehavior.none(),
headerBehavior:
cloudfront.CacheHeaderBehavior.none(),
cookieBehavior:
cloudfront.CacheCookieBehavior.none(),
}
),
};
// API response'ları (kısa ömürlü)
const apiCacheBehavior = {
pathPattern: '/api/public/*',
cachePolicy: new cloudfront.CachePolicy(
this,
'ApiCachePolicy',
{
minTtl: cdk.Duration.seconds(0),
defaultTtl: cdk.Duration.seconds(60),
maxTtl: cdk.Duration.minutes(5),
queryStringBehavior:
cloudfront.CacheQueryStringBehavior.all(),
headerBehavior:
cloudfront.CacheHeaderBehavior.allowList(
'Authorization'
),
}
),
};
// Dynamic içerik (cache yok)
const dynamicBehavior = {
pathPattern: '/api/user/*',
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
};
Invalidation Stratejisi#
CloudFront invalidation maliyetleri toplanır (ilk 1.000/ay’dan sonra path başına $0.005). Bunun yerine versiyonlu URL’ler kullan:
// Kötü: Invalidation gerektirir
const assetUrl = '/static/app.js';
await cloudfront.createInvalidation({
DistributionId: 'E1234567890',
InvalidationBatch: {
CallerReference: Date.now().toString(),
Paths: {
Quantity: 1,
Items: ['/static/app.js'],
},
},
});
// İyi: Versiyonlu URL (invalidation gerekmez)
const buildHash = process.env.BUILD_HASH;
const assetUrl = `/static/app.${buildHash}.js`;
// Yeni versiyon = yeni URL = otomatik cache busting
React Query ile Client-Side Caching#
Frontend caching genellikle gözden kaçırılır ama kullanıcı deneyimi için kritik. React Query (TanStack Query) stale-while-revalidate pattern ile sofistike client-side caching sağlar.
import {
useQuery,
useMutation,
useQueryClient,
} from '@tanstack/react-query';
function UserProfile({ userId }: { userId: string }) {
const queryClient = useQueryClient();
// Caching ve stale-while-revalidate ile query
const { data: user, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 dakika fresh
gcTime: 30 * 60 * 1000, // Cache'de 30 dakika tut
refetchOnWindowFocus: true,
refetchOnReconnect: true,
});
// Optimistic update'lerle mutation
const updateMutation = useMutation({
mutationFn: (data: Partial<User>) =>
updateUser(userId, data),
onMutate: async newData => {
// Giden refetch'leri iptal et
await queryClient.cancelQueries({
queryKey: ['user', userId],
});
// Önceki değeri snapshot'la
const previous = queryClient.getQueryData([
'user',
userId,
]);
// Cache'i optimistically güncelle
queryClient.setQueryData(
['user', userId],
(old: any) => ({
...old,
...newData,
})
);
return { previous };
},
onError: (err, variables, context) => {
// Hata durumunda rollback yap
queryClient.setQueryData(
['user', userId],
context?.previous
);
},
onSettled: () => {
// Mutation'dan sonra refetch
queryClient.invalidateQueries({
queryKey: ['user', userId],
});
},
});
return (
<div>
{isLoading ? 'Yükleniyor...' : user?.name}
<button
onClick={() =>
updateMutation.mutate({ name: 'Yeni İsim' })
}
>
Güncelle
</button>
</div>
);
}
Prefetching ile UX İyileştirmesi#
Kullanıcılar ihtiyaç duymadan önce data’yı prefetch et, anında navigasyon için:
function UserList() {
const queryClient = useQueryClient();
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
// Hover'da prefetch
const handleUserHover = (userId: string) => {
queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
};
return (
<ul>
{users?.map(user => (
<li
key={user.id}
onMouseEnter={() => handleUserHover(user.id)}
>
<Link to={`/user/${user.id}`}>
{user.name}
</Link>
</li>
))}
</ul>
);
}
Cache Monitoring ve Optimizasyon#
Ölçmediğin şeyi optimize edemezsin. İşte kritik metrikler:
Ana Metrikler#
1. Hit Rate
class CacheMetrics {
private hits = 0;
private misses = 0;
recordHit(): void {
this.hits++;
}
recordMiss(): void {
this.misses++;
}
getHitRate(): number {
const total = this.hits + this.misses;
return total === 0 ? 0 : (this.hits / total) * 100;
}
}
Hedef: Workload’a göre %85-95
- %80’in altında: Cache key tasarımını, TTL ayarlarını incele
- Formül:
(hit'ler / (hit'ler + miss'ler)) * 100
2. Latency Percentile’ları
- P50: Redis için ~1-2ms
- P99: <10ms olmalı
- P99.9: >50ms ise alert
3. Memory Kullanımı
- Hedef: %70-80 kullanım
- Alert: >%90 (eviction riski)
4. Eviction Rate
- Yüksek eviction = daha fazla memory ya da daha kısa TTL’ler gerekli
Monitoring İmplementasyonu#
import {
CloudWatchClient,
PutMetricDataCommand,
GetMetricStatisticsCommand,
} from '@aws-sdk/client-cloudwatch';
class CacheMonitor {
private cloudwatch: CloudWatchClient;
async trackMetrics(
hit: boolean,
latency: number
): Promise<void> {
await this.cloudwatch.send(
new PutMetricDataCommand({
Namespace: 'CustomCache',
MetricData: [
{
MetricName: 'CacheHitRate',
Value: hit ? 1 : 0,
Unit: 'Count',
Dimensions: [
{ Name: 'CacheLayer', Value: 'Redis' },
],
},
{
MetricName: 'CacheLatency',
Value: latency,
Unit: 'Milliseconds',
Dimensions: [
{ Name: 'CacheLayer', Value: 'Redis' },
],
},
],
})
);
}
async getCacheHitRate(
period: number = 300
): Promise<number> {
const result = await this.cloudwatch.send(
new GetMetricStatisticsCommand({
Namespace: 'CustomCache',
MetricName: 'CacheHitRate',
StartTime: new Date(Date.now() - period * 1000),
EndTime: new Date(),
Period: period,
Statistics: ['Average'],
Dimensions: [
{ Name: 'CacheLayer', Value: 'Redis' },
],
})
);
return result.Datapoints?.[0]?.Average ?? 0;
}
}
Yaygın Hatalar ve Dersler#
1. Dynamic Data’yı Aşırı Cache’lemek#
User’a özel data’yı uzun TTL ile cache’lemek kullanıcıların stale data görmesine ve destek bilet sayısının artmasına yol açar.
Çözüm: Data’yı volatility’sine göre sınıflandır:
const cacheStrategies = {
static: {
ttl: 86400 * 7, // 1 hafta
pattern: 'static:*',
},
config: {
ttl: 3600, // 1 saat
pattern: 'config:*',
},
userProfile: {
ttl: 300, // 5 dakika
pattern: 'user:*',
invalidateOn: ['user.updated'],
},
realtime: {
ttl: 0, // Cache'leme
pattern: 'inventory:*',
},
};
2. Kötü Cache Key Tasarımı#
Cache key’lerine timestamp ya da random değerler eklemek hit rate’i yok eder.
// Kötü: Gereksiz değişkenlik
const key = `user:${userId}:${timestamp}:${requestId}`;
// İyi: Deterministik ve minimal
const key = `user:${userId}`;
// İyi: Sadece anlamlı parametreleri dahil et
const key = `user:${userId}:posts:${page}`;
3. Cache Failure’ları Görmezden Gelmek#
Cache failure uygulamanı düşürmemeli. Her zaman fallback implement et:
class ResilientCache {
async get<T>(
key: string,
fetcher: () => Promise<T>
): Promise<T> {
try {
const cached = await Promise.race([
redis.get(key),
this.timeout(100), // 100ms timeout
]);
if (cached) return JSON.parse(cached);
} catch (error) {
// Log yap ama throw etme
logger.warn('Cache failure, origin kullanılıyor', {
key,
error,
});
}
// Her durumda origin'den çek
return fetcher();
}
}
4. CloudFront Invalidation İstismarı#
Sık invalidation maliyetleri artırır. Bunun yerine versiyonlu URL’ler kullan:
class AssetVersioning {
private buildHash: string;
constructor() {
this.buildHash =
process.env.BUILD_HASH || Date.now().toString();
}
// URL üzerinden otomatik cache busting
getAssetUrl(path: string): string {
return `${path}?v=${this.buildHash}`;
}
}
Maliyet Optimizasyonu#
AWS Servis Fiyatlandırması (us-east-1)#
ElastiCache Redis (cache.r6g.large: 13.07 GB):
- On-Demand: $0.206/saat = node başına ~$150/ay
- 3-node cluster: ~$450/ay
MemoryDB (db.r6g.large: 13.07 GB):
- On-Demand: $0.406/saat = node başına ~$293/ay
- 3-node cluster: ~$879/ay (ElastiCache’in 1.5x’i)
CloudFront:
- İlk 10 TB/ay: $0.085/GB
- HTTP/HTTPS request’leri: 10.000 başına $0.0075
- Invalidation: İlk 1.000 path ücretsiz, sonrası path başına $0.005
Right-Sizing Stratejisi#
class CacheOptimization {
async analyzeUtilization(): Promise<Report> {
const metrics = await this.getWeeklyMetrics();
const avgMemoryUsage = metrics.memory.average;
const currentCapacity = this.getCurrentCapacity();
const recommendations = [];
// Sürekli düşük kullanım
if (avgMemoryUsage < currentCapacity * 0.6) {
const recommendedSize =
this.calculateOptimalSize(metrics.memory.peak);
const savings = this.calculateSavings(
currentCapacity,
recommendedSize
);
recommendations.push({
type: 'DOWNSIZE',
currentSize: currentCapacity,
recommendedSize,
monthlySavings: savings,
});
}
// Yüksek eviction rate
if (metrics.evictions.perDay > 1000) {
recommendations.push({
type: 'UPSIZE',
reason: 'Yüksek eviction rate hit rate\'i etkiliyor',
impact: 'Hit rate %15-20 iyileşebilir',
});
}
return { metrics, recommendations };
}
}
Varsayılan Ne Zaman Geçerli#
Paylaşımlı bir Redis üzerinde cache-aside, üç koşul doğru olduğu sürece iş görür: origin bir miss’i kaldırabiliyordur, birkaç saniyelik stale data kabul edilebilirdir ve cache düştüğünde yalnızca latency artar, data kalıcı depoda durur. Başka bir pattern’e geçmeden önce bu üçünü kontrol et; hayal kırıklığı yaratan cache’lerin çoğu bunlardan birinde takılıyor.
Üç koşuldan biri kırıldığında varsayılanı değiştir. Okumaların yazmaları yakından takip etmesi gerekiyorsa write-through kullan ve karşılığında artan write latency’sini kabul et. Write-through tek başına stale pencereyi kapatmaz, yalnızca daraltır. Aynı kaydı iki writer güncelleyebiliyorsa cache yazmasını versiyon kontrolüyle koru ya da key’i set etmek yerine sil. Store aynı zamanda kayıt sistemiyse ve data loss kabul edilemezse MemoryDB kullan; aynı instance sınıfı için node fiyatı ElastiCache’in kabaca 1.5 katıdır. Origin DynamoDB ise ve eventually consistent read’ler yeterliyse DAX kullan; DAX’ın başka hiçbir kaynağı hızlandırmadığını akılda tut.
İki şeyi baştan koymak, bir incident’tan sonra eklemekten çok daha ucuza gelir: yeniden üretimin etrafında stampede koruması ve cache erişilemezken de trafiği karşılamaya devam eden bir kod yolu.
Kaynaklar#
- Redis Kullanan Veritabanı Önbellekleme Stratejileri: Önbellekleme Kalıpları (yeni sekmede açılır) - Cache-aside, write-through, write-behind ve refresh-ahead kalıplarını Redis ile ele alan AWS teknik belgesi.
- Redis Cache-Aside Belgesi (yeni sekmede açılır) - Cache-aside kalıbının uygulaması ve en iyi uygulamalar hakkında resmi Redis belgesi.
- Amazon ElastiCache En İyi Uygulamalar ve Önbellekleme Stratejileri (yeni sekmede açılır) - Önbellekleme stratejileri, tahliye politikaları ve operasyonel en iyi uygulamaları kapsayan resmi AWS ElastiCache kılavuzu.
- Amazon ElastiCache ile MemoryDB Karşılaştırması: Doğru Servisi Seçmek (yeni sekmede açılır) - Önbellekleme iş yükleri için ElastiCache ile kalıcı bellek içi veritabanı olan MemoryDB arasındaki resmi AWS karşılaştırması.
- Amazon ElastiCache Fiyatlandırması (yeni sekmede açılır) - cache.r6g ailesi dahil olmak üzere instance sınıfına göre on-demand ve reserved node fiyatları.
- Amazon MemoryDB Fiyatlandırması (yeni sekmede açılır) - MemoryDB node’ları için instance-saat ve yazılan veri fiyatlandırması.
- Amazon CloudFront Fiyatlandırması (yeni sekmede açılır) - Aylık ücretsiz invalidation kotası dahil olmak üzere veri transferi, istek ve invalidation fiyatlandırması.
- Amazon CloudFront Cache Davranış Ayarları (yeni sekmede açılır) - Cache davranışlarını, TTL değerlerini ve origin başlıklarını yapılandırmaya yönelik resmi CloudFront belgesi.
- Amazon CloudFront: Cache Süresini Yönetme (yeni sekmede açılır) - Minimum, maksimum ve varsayılan TTL ayarları aracılığıyla CloudFront edge cache’inde içeriğin ne kadar süre kalacağını kontrol etmeye yönelik AWS referansı.
- Redis Kodlama Kalıpları (yeni sekmede açılır) - Dağıtık kilitleme, hız sınırlama ve istek birleştirme gibi istemci tarafı kalıpları ele alan resmi Redis belgesi.
İlgili yazılar
Key-value storage'a temel bir rehber: ne olduğu, nerede kullanıldığı, takımların neden seçtiği ve hangi tech stack'lerde hangi çözümlerin geldiği.
redis · dynamodb · caching +4
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 · aws · reliability +4
Global uygulamalar için AWS edge computing çözümlerini seçme ve uygulama üzerine pratik örnekler ve maliyet optimizasyonu stratejileri içeren kapsamlı teknik rehber.
aws · cloudfront · lambda +6
Runtime seçimi, connection reuse, bundle disiplini, caching ve memory ayarıyla AWS Lambda warm path gecikmesini 10 ms bütçesinin içinde tutun.
aws · lambda · performance +4
SQL, NoSQL, NewSQL ve edge seçenekleri arasında doğru veritabanını seçme rehberi: her kategorinin trade-off'ları, seçim kriterleri ve bir karar çerçevesi.
data-storage-orm · postgresql · redis +6