KV Storage ile Cloudflare Workers’da Veri Saklama

Edge computing dünyasında veri saklamak her zaman bir baş ağrısı olmuştur. Stateless yapısıyla edge function’lar, geleneksel veritabanı bağlantılarını desteklemiyor; latency sorunları ortaya çıkıyor ve global dağıtık bir ortamda tutarlı veri yönetimi neredeyse imkânsız hale geliyor. İşte bu noktada Cloudflare KV Storage devreye giriyor ve edge’de veri saklama sorununu çok zarif bir şekilde çözüyor.

Bu yazıda KV Storage’ı sıfırdan kuracağız, gerçek dünya senaryolarıyla nasıl kullanacağımızı göreceğiz ve production’da dikkat etmeniz gereken kritik noktalara değineceğiz.

KV Storage Nedir ve Nasıl Çalışır?

Cloudflare KV, globally dağıtık bir key-value store’dur. Cloudflare’in 300’den fazla PoP (Point of Presence) noktasında çalışır ve verilerinizi kullanıcıya en yakın edge node’da cache’ler. Bu sayede okuma işlemleri inanılmaz düşük latency ile gerçekleşir.

Ancak burada önemli bir kavramı anlamak gerekiyor: KV Storage, eventual consistency modeli kullanır. Yani bir key’e yazdığınızda bu değişiklik anlık olarak tüm dünyaya yayılmaz; birkaç saniye ile birkaç dakika arasında değişen bir sürede yayılır. Bu durum bazı senaryolar için problem yaratabilir ama çoğu kullanım durumu için gayet yeterlidir.

KV’nin temel karakteristikleri şunlardır:

  • Okuma işlemleri: Çok hızlı, edge’den serve edilir
  • Yazma işlemleri: Biraz daha yavaş, merkezi sistemlere gider
  • Veri boyutu: Key başına maksimum 25 MB değer saklayabilirsiniz
  • Key sayısı: Free plan’da 1 milyar key’e kadar çıkabilirsiniz
  • TTL desteği: Key’lere otomatik silinme süresi tanımlayabilirsiniz
  • Metadata: Her key’e ek metadata ekleyebilirsiniz

Wrangler ile KV Namespace Oluşturma

İlk adım olarak bir KV namespace oluşturmanız gerekiyor. Wrangler CLI üzerinden bu işlemi yapacağız.

# Wrangler'ı global olarak yükleyin
npm install -g wrangler

# Cloudflare hesabınıza giriş yapın
wrangler login

# Production için KV namespace oluşturun
wrangler kv:namespace create "MY_KV_STORE"

# Preview/development için ayrı bir namespace oluşturun
wrangler kv:namespace create "MY_KV_STORE" --preview

Bu komutlar size namespace ID’leri verecek. Bu ID’leri wrangler.toml dosyanıza eklemeniz gerekiyor:

# wrangler.toml içeriği
name = "my-worker"
main = "src/index.js"
compatibility_date = "2024-01-01"

[[kv_namespaces]]
binding = "MY_KV_STORE"
id = "production_namespace_id_buraya"
preview_id = "preview_namespace_id_buraya"

Buradaki binding değeri önemli. Bu değer, Worker kodunuzda KV’ye erişirken kullanacağınız global değişken adıdır. MY_KV_STORE yazdıysanız, kodda MY_KV_STORE.get() ve MY_KV_STORE.put() şeklinde kullanırsınız.

Temel CRUD İşlemleri

KV Storage’ın API’si oldukça sade ve anlaşılırdır. Önce temel işlemlere bakalım:

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const path = url.pathname;

    // Veri yazma
    if (path === "/set") {
      await env.MY_KV_STORE.put("kullanici:1001", JSON.stringify({
        ad: "Ahmet Yilmaz",
        email: "[email protected]",
        rol: "admin",
        olusturma_tarihi: new Date().toISOString()
      }));
      return new Response("Veri kaydedildi", { status: 200 });
    }

    // Veri okuma
    if (path === "/get") {
      const deger = await env.MY_KV_STORE.get("kullanici:1001");
      
      if (!deger) {
        return new Response("Veri bulunamadi", { status: 404 });
      }
      
      return new Response(deger, {
        headers: { "Content-Type": "application/json" }
      });
    }

    // Veri silme
    if (path === "/delete") {
      await env.MY_KV_STORE.delete("kullanici:1001");
      return new Response("Veri silindi", { status: 200 });
    }

    return new Response("Gecersiz endpoint", { status: 400 });
  }
};

Bu kadar basit. Ama gerçek dünyada bu kadar sade olmayacak tabii ki. Şimdi daha karmaşık senaryolara geçelim.

TTL ile Oturum Yönetimi

En yaygın kullanım senaryolarından biri oturum yönetimidir. Kullanıcı login olduğunda bir session token oluşturuyorsunuz ve bu token’ı belirli bir süre sonra otomatik olarak silmek istiyorsunuz. KV’nin TTL özelliği tam bu iş için biçilmiş kaftan:

import { v4 as uuidv4 } from 'uuid';

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    
    if (request.method === "POST" && url.pathname === "/login") {
      const body = await request.json();
      
      // Kullanıcı doğrulama mantığınız buraya gelecek
      const kullanici = await kullaniciDogrula(body.email, body.sifre, env);
      
      if (!kullanici) {
        return new Response(
          JSON.stringify({ hata: "Gecersiz kimlik bilgileri" }),
          { status: 401, headers: { "Content-Type": "application/json" } }
        );
      }
      
      // Session token oluştur
      const sessionToken = uuidv4();
      const sessionVerisi = {
        kullanici_id: kullanici.id,
        email: kullanici.email,
        rol: kullanici.rol,
        olusturma: Date.now(),
        ip: request.headers.get("CF-Connecting-IP")
      };
      
      // 24 saat TTL ile kaydet (saniye cinsinden)
      await env.MY_KV_STORE.put(
        `session:${sessionToken}`,
        JSON.stringify(sessionVerisi),
        { expirationTtl: 86400 }
      );
      
      return new Response(
        JSON.stringify({ token: sessionToken, sure: "24 saat" }),
        { 
          status: 200, 
          headers: { 
            "Content-Type": "application/json",
            "Set-Cookie": `session=${sessionToken}; HttpOnly; Secure; SameSite=Strict; Max-Age=86400`
          } 
        }
      );
    }
    
    return new Response("Not found", { status: 404 });
  }
};

Metadata Kullanımı ve Gelişmiş Okuma

KV, değerlerin yanında metadata saklamamıza da imkân tanır. Bu metadata küçük olmalı (1024 byte sınırı var) ama filtreleme ve listeleme için çok işe yarıyor:

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    
    // Ürün kaydetme - metadata ile birlikte
    if (request.method === "POST" && url.pathname === "/urun") {
      const urun = await request.json();
      const urunId = `urun:${Date.now()}:${Math.random().toString(36).substr(2, 9)}`;
      
      await env.MY_KV_STORE.put(
        urunId,
        JSON.stringify(urun),
        {
          expirationTtl: 604800, // 7 gün
          metadata: {
            kategori: urun.kategori,
            fiyat: urun.fiyat,
            stok: urun.stok > 0 ? "var" : "yok",
            olusturma: new Date().toISOString()
          }
        }
      );
      
      return new Response(JSON.stringify({ id: urunId }), {
        status: 201,
        headers: { "Content-Type": "application/json" }
      });
    }
    
    // Metadata ile birlikte okuma
    if (url.pathname.startsWith("/urun/") && request.method === "GET") {
      const urunId = url.pathname.replace("/urun/", "");
      
      // getWithMetadata kullanarak hem değeri hem metadata'yı alıyoruz
      const { value, metadata } = await env.MY_KV_STORE.getWithMetadata(urunId, "json");
      
      if (!value) {
        return new Response(JSON.stringify({ hata: "Urun bulunamadi" }), {
          status: 404,
          headers: { "Content-Type": "application/json" }
        });
      }
      
      return new Response(JSON.stringify({ urun: value, meta: metadata }), {
        headers: { "Content-Type": "application/json" }
      });
    }
    
    return new Response("Not found", { status: 404 });
  }
};

Key Listeleme ve Pagination

KV’de tüm key’leri listelemek mümkün, ama dikkatli kullanmak gerekiyor. Liste işlemleri yavaştır ve büyük veri setlerinde sorun yaratabilir:

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    
    if (url.pathname === "/urunler" && request.method === "GET") {
      const kategori = url.searchParams.get("kategori");
      const cursor = url.searchParams.get("cursor") || undefined;
      const limit = parseInt(url.searchParams.get("limit") || "10");
      
      // Prefix ile filtreleme yapıyoruz
      const liste = await env.MY_KV_STORE.list({
        prefix: "urun:",
        limit: limit,
        cursor: cursor
      });
      
      // Metadata'yı kullanarak client'a fazladan istek attırmadan
      // kategori filtresi uygulayabiliriz
      const filtrelenmis = kategori
        ? liste.keys.filter(k => k.metadata?.kategori === kategori)
        : liste.keys;
      
      const sonuc = {
        urunler: filtrelenmis.map(k => ({
          id: k.name,
          metadata: k.metadata
        })),
        toplam: filtrelenmis.length,
        tamamlandi: liste.list_complete,
        sonraki_cursor: liste.cursor
      };
      
      return new Response(JSON.stringify(sonuc), {
        headers: { "Content-Type": "application/json" }
      });
    }
    
    return new Response("Not found", { status: 404 });
  }
};

Rate Limiting ile KV Kullanımı

Gerçek dünyada sık karşılaştığım bir senaryo: API rate limiting. KV bunu yapmak için mükemmel bir araç:

export default {
  async fetch(request, env, ctx) {
    const ip = request.headers.get("CF-Connecting-IP");
    const rateLimitKey = `ratelimit:${ip}:${Math.floor(Date.now() / 60000)}`;
    
    // Mevcut istek sayısını al
    const mevcutSayi = await env.MY_KV_STORE.get(rateLimitKey);
    const istek_sayisi = mevcutSayi ? parseInt(mevcutSayi) : 0;
    
    // Dakikada 100 istek limiti
    const LIMIT = 100;
    
    if (istek_sayisi >= LIMIT) {
      return new Response(
        JSON.stringify({
          hata: "Cok fazla istek",
          mesaj: `Dakikada maksimum ${LIMIT} istek yapabilirsiniz`,
          kalan_sure: 60 - (Math.floor(Date.now() / 1000) % 60)
        }),
        {
          status: 429,
          headers: {
            "Content-Type": "application/json",
            "Retry-After": String(60 - (Math.floor(Date.now() / 1000) % 60)),
            "X-RateLimit-Limit": String(LIMIT),
            "X-RateLimit-Remaining": "0"
          }
        }
      );
    }
    
    // Sayacı artır (dakika sonunda otomatik silinsin diye TTL koy)
    ctx.waitUntil(
      env.MY_KV_STORE.put(rateLimitKey, String(istek_sayisi + 1), {
        expirationTtl: 120 // 2 dakika TTL, güvenli taraf
      })
    );
    
    // Asıl isteği işle
    const response = await asılIstegiIsle(request, env);
    
    // Rate limit header'larını ekle
    const yeniResponse = new Response(response.body, response);
    yeniResponse.headers.set("X-RateLimit-Limit", String(LIMIT));
    yeniResponse.headers.set("X-RateLimit-Remaining", String(LIMIT - istek_sayisi - 1));
    
    return yeniResponse;
  }
};

async function asılIstegiIsle(request, env) {
  // API mantığınız buraya gelecek
  return new Response(JSON.stringify({ mesaj: "Basarili" }), {
    headers: { "Content-Type": "application/json" }
  });
}

Burada ctx.waitUntil() kullanımına dikkat edin. Bu sayede sayacı artırma işlemini arka planda yapıyoruz ve kullanıcı cevabı beklemeden alıyor. Response dönüldükten sonra da Worker çalışmaya devam edebiliyor.

Feature Flag Sistemi

KV’nin bir diğer harika kullanım alanı feature flag yönetimidir. Deployment yapmadan özelliklerinizi açıp kapatabilirsiniz:

export default {
  async fetch(request, env, ctx) {
    // Feature flag'leri cache'den veya KV'den al
    const flagCache = await env.MY_KV_STORE.get("feature_flags", "json");
    
    const flagler = flagCache || {
      yeni_arama: false,
      beta_dashboard: false,
      dark_mode: true,
      ai_onerileri: false
    };
    
    const url = new URL(request.url);
    
    // Flag yönetim endpoint'i (sadece admin erişimi)
    if (url.pathname === "/admin/flags" && request.method === "POST") {
      const adminToken = request.headers.get("X-Admin-Token");
      
      if (adminToken !== env.ADMIN_SECRET) {
        return new Response("Yetkisiz erisim", { status: 403 });
      }
      
      const yeniFlagler = await request.json();
      
      await env.MY_KV_STORE.put(
        "feature_flags",
        JSON.stringify({ ...flagler, ...yeniFlagler }),
        { expirationTtl: 86400 * 30 } // 30 gün
      );
      
      return new Response(JSON.stringify({ basarili: true, flagler: yeniFlagler }), {
        headers: { "Content-Type": "application/json" }
      });
    }
    
    // Arama endpoint'i - flag'e göre farklı davranış
    if (url.pathname === "/ara") {
      if (flagler.yeni_arama) {
        return await yeniAramaMotoru(request, env);
      } else {
        return await eskiAramaMotoru(request, env);
      }
    }
    
    return new Response(JSON.stringify({ flagler }), {
      headers: { "Content-Type": "application/json" }
    });
  }
};

Wrangler ile KV Yönetimi

Geliştirme sürecinde komut satırından KV’yi yönetmek çok işe yarıyor:

# Key-value çifti ekle
wrangler kv:key put --binding=MY_KV_STORE "kullanici:1001" '{"ad":"Test User","rol":"admin"}'

# JSON dosyasından değer ekle
wrangler kv:key put --binding=MY_KV_STORE "config" --path=./config.json

# Değer oku
wrangler kv:key get --binding=MY_KV_STORE "kullanici:1001"

# Tüm key'leri listele
wrangler kv:key list --binding=MY_KV_STORE

# Prefix ile filtrele
wrangler kv:key list --binding=MY_KV_STORE --prefix="kullanici:"

# Key sil
wrangler kv:key delete --binding=MY_KV_STORE "kullanici:1001"

# TTL ile ekle (Unix timestamp)
wrangler kv:key put --binding=MY_KV_STORE "gecici_key" "deger" --expiration=1735689600

# Bulk import (JSON array formatında)
wrangler kv:bulk put --binding=MY_KV_STORE ./bulk_data.json

Bulk import için JSON formatı şu şekilde olmalı:

# bulk_data.json içeriği bu formatta olmalı
# [{"key": "anahtar1", "value": "deger1"}, {"key": "anahtar2", "value": "deger2"}]

cat bulk_data.json
# [
#   {"key": "urun:001", "value": "{"ad":"Laptop","fiyat":15000}"},
#   {"key": "urun:002", "value": "{"ad":"Mouse","fiyat":250}"},
#   {"key": "urun:003", "value": "{"ad":"Klavye","fiyat":800}"}
# ]

wrangler kv:bulk put --binding=MY_KV_STORE ./bulk_data.json

Production’da Dikkat Edilmesi Gereken Noktalar

KV Storage’ı production’da kullanırken bazı kritik noktalara dikkat etmeniz gerekiyor.

Eventual consistency tuzağı: KV, strong consistency sağlamaz. Bir değeri güncelledikten hemen sonra okursanız eski değeri görebilirsiniz. Banka hesabı bakiyesi gibi kritik veriler için KV kullanmayın. Bunun yerine Cloudflare Durable Objects tercih edin.

Hot key sorunu: Çok fazla istek alan bir key, edge cache’inde tutulur ve okuma performansı mükemmel olur. Ama yazma işlemleri her zaman merkezi sisteme gider. Saniyede binlerce yazma yapıyorsanız throttling ile karşılaşabilirsiniz. Yazma limitini düşünün: ücretsiz planda saniyede 1 yazma, ücretli planda saniyede 1000 yazma.

Key tasarımı önemli: Prefix bazlı listeleme yaptığınız için key’lerinizi düzenli tutun. kullanici:ID, session:TOKEN, cache:URL gibi namespace’li key yapısı kullanın. Bu hem okunabilirliği artırır hem de listeleme işlemlerini kolaylaştırır.

Değer boyutu sınırı: 25 MB üst sınır var ama pratikte 1 MB’ın üzerine çıkmamaya çalışın. Büyük objeler için R2 Object Storage’ı kullanın ve KV’de sadece referans saklayın.

Cache invalidation: KV verileri edge’de cache’lenir ve bu cache’i anında temizleyemezsiniz. Bir key’i sildikten sonra eski değer birkaç dakika daha görünebilir. Bu davranışı göz önünde bulundurarak sisteminizi tasarlayın.

Maliyet hesabı: Ücretsiz planda günde 100.000 okuma ve 1.000 yazma hakkınız var. Workers Paid planında okuma başına $0.50/milyon, yazma başına $5/milyon ücretlendirilirsiniz. Gereksiz liste işlemlerinden kaçının, çünkü her liste isteği ayrı sayılıyor.

Local Geliştirme Ortamı

Wrangler’ın local geliştirme modu KV’yi simüle eder. Production’a dokunmadan test yapabilirsiniz:

# Local geliştirme modunda çalıştır
# KV değerleri .wrangler/state/v3/kv/ klasöründe saklanır
wrangler dev

# Miniflare ile daha gelişmiş local test
npx miniflare --kv MY_KV_STORE src/index.js

# Local KV'ye değer ekle (dev modunda)
wrangler kv:key put --binding=MY_KV_STORE "test_key" "test_value" --local

# Local KV'den değer oku
wrangler kv:key get --binding=MY_KV_STORE "test_key" --local

Local geliştirmede --preview flag’ini kullanarak preview namespace’ini de test edebilirsiniz. Bu sayede production verilerinizi kirletmeden geliştirebilirsiniz.

Sonuç

Cloudflare KV Storage, edge computing dünyasında veri saklama problemine pragmatik bir çözüm sunuyor. Özellikle yüksek okuma, düşük yazma senaryolarında performansı gerçekten etkileyici. Session yönetimi, feature flag’ler, rate limiting, config saklama ve cache layer olarak kullanımda KV’nin rakibi çok az.

Ama her şeyi KV ile yapmaya çalışmayın. Strong consistency gerektiren veriler için Durable Objects, büyük dosyalar için R2, ilişkisel veriler için D1 SQLite daha uygun seçenekler. KV’yi güçlü yanlarıyla, yani eventual consistent, yüksek okuma performanslı senaryolarda kullanırsanız işiniz gerçekten kolaylaşıyor.

Eventual consistency davranışını baştan anlayın ve sisteminizi buna göre tasarlayın. Veri modelinizi düşünerek key isimlendirme yapın. TTL özelliğini agresif kullanın, gereksiz veriyi biriktirmeyin. Ve her zaman local ortamda test edip production’a alın; KV ile yapılan bir hata sonradan düzeltmesi zor olabilecek veri tutarsızlıklarına yol açabiliyor.

Edge’de veri saklamak artık bu kadar basit. Denemesi ücretsiz, başlamak için tek yapmanız gereken wrangler kv:namespace create komutunu çalıştırmak.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir