Sınırda Kalıcılık Sorunu

V8, Cloudflare Çalışanlarının tasarım gereği vatansız oldukları gücünü izole eder: her biri izole edilmiştir mikrosaniyeler içinde oluşturulabilir ve yok edilebilir, ancak yerel JavaScript durumu istekler arasında devam ediyor (veya aynı anda eşzamanlılık nedeniyle yalnızca ara sıra devam ediyor) izole edilmiştir). Gerçek uygulamalar oluşturmak için harici kalıcılığa ihtiyacınız vardır.

Cloudflare, her biri bir kullanım senaryosu için tasarlanmış üç yerel depolama ürünü sunar spesifik. Yanlış olanı seçmek yavaş sorgular ve yüksek maliyetler anlamına gelebilir veya hata ayıklaması zor tutarsızlıklar. Bu makale bunları ayrıntılı olarak karşılaştırmaktadır. Gerçek kod örnekleri.

Ne Öğreneceksiniz

  • Workers KV: sonuçta tutarlı mimari, API, sınırlar ve ideal kullanım örnekleri
  • Cloudflare R2: Çıkış ücreti olmadan S3 uyumlu nesne depolama, çok parçalı yükleme
  • D1 SQLite: uçta ilişkisel veritabanı, SQL sorguları, Drizzle ORM ile geçişler
  • Maliyet karşılaştırması: Farklı kullanım ölçeklerinde KV, R2 ve D1
  • Entegrasyon Modeli: Akıllı önbelleğe alma için KV + D1 nasıl birleştirilir?
  • Her depolama türü için wrangler.toml'deki bağlamaları yapılandırma

Workers KV: Küresel Anahtar-Değer ve Sonunda Tutarlı

Workers KV, küresel olarak dağıtılmış bir anahtar/değer veri deposudur. Yazdığında bir değer, saniyeler içinde tüm Cloudflare PoP'larında çoğaltılır. Okuduğunuzda, en yakın PoP'tan değer elde edin: okuma gecikmesi genellikle 1 ms'den azdır.

Temel değiş-tokuş: KV sonuçta tutarlı. Bir yazı küresel olarak görünür hale gelmesi 60 saniye kadar sürebilir. Bu onu yapar sık değişen ve hemen okunması gereken veriler için uygun değildir yazdıktan sonra.

wrangler.toml'de KV yapılandırması

# wrangler.toml

[[kv_namespaces]]
binding = "CACHE"          # Nome del binding nel codice TypeScript
id = "xxxxxxxxxxxxxxxxxx"  # ID del namespace in produzione
preview_id = "yyyyyyyyyy"  # ID del namespace per wrangler dev

[[kv_namespaces]]
binding = "SESSIONS"
id = "aaaaaaaaaaaaaaaaa"
preview_id = "bbbbbbbbbbbbb"

# Per creare i namespace:
# wrangler kv namespace create CACHE
# wrangler kv namespace create CACHE --preview

TypeScript'te Workers KV API'si

// src/services/cache.service.ts

export interface Env {
  CACHE: KVNamespace;
  SESSIONS: KVNamespace;
}

// -------- OPERAZIONI BASE --------

// WRITE: put con TTL opzionale (in secondi)
await env.CACHE.put('user:123', JSON.stringify({ name: 'Mario', role: 'admin' }), {
  expirationTtl: 3600,  // scade tra 1 ora
});

// WRITE senza TTL (valore permanente)
await env.CACHE.put('config:features', JSON.stringify({ darkMode: true }));

// WRITE con expiration assoluta (Unix timestamp)
await env.CACHE.put('promo:summer', 'active', {
  expiration: Math.floor(Date.now() / 1000) + 86400,  // scade tra 24h
});

// READ: get con tipo di decodifica
const raw = await env.CACHE.get('user:123');
// raw: string | null

const parsed = await env.CACHE.get<{ name: string; role: string }>('user:123', 'json');
// parsed: { name: string; role: string } | null

// READ con metadata
const { value, metadata } = await env.CACHE.getWithMetadata('user:123', 'json');

// DELETE
await env.CACHE.delete('user:123');

// -------- LISTING (con limitazioni) --------
// KV supporta listing, ma e lento e con limite di 1000 chiavi per chiamata
const listing = await env.CACHE.list({ prefix: 'user:', limit: 100 });
for (const key of listing.keys) {
  console.log(key.name, key.expiration, key.metadata);
}

// -------- PATTERN: cache-aside --------
async function getUserCached(userId: string, env: Env): Promise<User | null> {
  const cacheKey = `user:${userId}`;

  // 1. Controlla la cache KV
  const cached = await env.CACHE.get<User>(cacheKey, 'json');
  if (cached) {
    return cached;  // Cache hit: risposta da KV < 1ms
  }

  // 2. Cache miss: fetch dall'origine
  const user = await fetchUserFromDatabase(userId);
  if (!user) return null;

  // 3. Popola la cache (eventually consistent, ok per profili utente)
  await env.CACHE.put(cacheKey, JSON.stringify(user), {
    expirationTtl: 300,  // 5 minuti
  });

  return user;
}
karakteristik KV detayı
Tutarlılık modeli Sonunda tutarlı (maksimum 60 saniye yayılma)
Gecikmeyi okuma < 1 ms (ısınma sonrasında yerel PoP'tan)
Gecikme yazma ~100 ms (onay), eşzamansız küresel çoğaltma
Maksimum değer boyutu Değer başına 25MB
Maksimum anahtar boyutu 512 bayt
Okuma maliyeti Milyon başına 0,50 ABD doları (ücretsiz: 10 milyon/ay)
Yazma maliyeti Milyon başına 5 ABD doları (ücretsiz: 1 milyon/ay)
Şunun için idealdir: Yapılandırma, özellik işaretleri, oturum deposu, API önbelleğe alma
için uygun değil Gerçek zamanlı sayaçlar, her saniye değişen veriler

Cloudflare R2: Çıkış Ücreti Olmayan S3 Uyumlu Nesne Depolama

R2, 2022 lansmanında en çok ses getiren ürün oldu: nesne depolama AWS S3 API'leriyle uyumlu, sıfır çıkış maliyeti. S3'te, 1 TB veriyi internete aktarmanın maliyeti ~90dır. R2'de ücretsizdir.

R2 şunlar için idealdir: kullanıcı dosyalarını, statik varlıkları, yedeklemeleri, arşivlenmiş günlükleri yüklemek, tarayıcılar veya diğer Çalışanlar tarafından alınması gereken herhangi bir ikili nesne.

wrangler.toml'de R2 yapılandırması

# wrangler.toml

[[r2_buckets]]
binding = "ASSETS"         # Nome del binding TypeScript
bucket_name = "my-assets"  # Nome del bucket in Cloudflare

# Per creare il bucket:
# wrangler r2 bucket create my-assets

TypeScript'te R2 API'si

// src/services/storage.service.ts

export interface Env {
  ASSETS: R2Bucket;
}

// -------- UPLOAD --------

// Upload di testo semplice
await env.ASSETS.put('documents/readme.txt', 'Contenuto del file', {
  httpMetadata: { contentType: 'text/plain; charset=utf-8' },
  customMetadata: { uploadedBy: 'user-123', version: '1' },
});

// Upload di JSON
const data = { users: [{ id: 1, name: 'Mario' }] };
await env.ASSETS.put('data/users.json', JSON.stringify(data), {
  httpMetadata: { contentType: 'application/json' },
});

// Upload di un file binario da una Request (multipart form)
async function handleFileUpload(request: Request, env: Env): Promise<Response> {
  const formData = await request.formData();
  const file = formData.get('file') as File | null;

  if (!file) {
    return new Response('No file provided', { status: 400 });
  }

  // Genera un nome unico per evitare collisioni
  const key = `uploads/${Date.now()}-${file.name}`;

  await env.ASSETS.put(key, file.stream(), {
    httpMetadata: {
      contentType: file.type,
      contentLength: file.size,
    },
    customMetadata: {
      originalName: file.name,
      uploadedAt: new Date().toISOString(),
    },
  });

  return Response.json({ key, size: file.size, type: file.type });
}

// -------- DOWNLOAD --------

async function serveAsset(key: string, env: Env): Promise<Response> {
  const object = await env.ASSETS.get(key);

  if (!object) {
    return new Response('Not Found', { status: 404 });
  }

  // Leggi i metadata HTTP
  const headers = new Headers();
  object.writeHttpMetadata(headers);
  headers.set('etag', object.httpEtag);

  // Aggiungi headers di caching appropriati
  headers.set('Cache-Control', 'public, max-age=31536000, immutable');

  return new Response(object.body, { headers });
}

// -------- LISTING --------

async function listUploads(prefix: string, env: Env) {
  const listed = await env.ASSETS.list({
    prefix: `uploads/${prefix}`,
    limit: 100,
    // cursor: paginationCursor  // per paginare
  });

  return listed.objects.map(obj => ({
    key: obj.key,
    size: obj.size,
    uploaded: obj.uploaded.toISOString(),
    etag: obj.etag,
    customMetadata: obj.customMetadata,
  }));
}

// -------- DELETE --------

await env.ASSETS.delete('uploads/old-file.txt');

// Delete multiplo (fino a 1000 chiavi)
await env.ASSETS.delete(['file1.txt', 'file2.txt', 'file3.txt']);

// -------- HEAD: verifica esistenza senza download --------

const headResult = await env.ASSETS.head('uploads/documento.pdf');
if (headResult) {
  console.log('Size:', headResult.size);
  console.log('Uploaded:', headResult.uploaded);
}

Genel Erişimli ve Önceden İmzalanmış URL'li R2

// Pattern: servire file pubblicamente tramite Worker con access control

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    // URL pattern: /files/{key}
    const key = url.pathname.replace('/files/', '');

    if (!key) {
      return new Response('Key required', { status: 400 });
    }

    // Implementa qui la tua logica di autenticazione/autorizzazione
    const authorized = await checkAccess(request, key, env);
    if (!authorized) {
      return new Response('Forbidden', { status: 403 });
    }

    // Gestisci il conditional request (ETag/If-None-Match)
    const etag = request.headers.get('If-None-Match');
    const object = await env.ASSETS.get(key, {
      onlyIf: etag ? { etagDoesNotMatch: etag } : undefined,
      range: request.headers.get('Range') ?? undefined,
    });

    if (!object) {
      // Potrebbe essere 404 o 304 Not Modified
      const head = await env.ASSETS.head(key);
      if (!head) return new Response('Not Found', { status: 404 });
      return new Response(null, { status: 304, headers: { etag: head.httpEtag } });
    }

    const headers = new Headers();
    object.writeHttpMetadata(headers);
    headers.set('etag', object.httpEtag);

    if (object.range) {
      headers.set('Content-Range', `bytes ${object.range.offset}-${
        (object.range.offset ?? 0) + (object.range.length ?? 0) - 1
      }/${object.size}`);
      return new Response(object.body, { status: 206, headers });
    }

    return new Response(object.body, { headers });
  },
};

D1 SQLite: Uçta İlişkisel Veritabanı

D1 en yeni ve en iddialı üründür: uçta yerel SQLite. JOIN'ler, işlemler, karmaşık sorgular ve her şeyi içeren eksiksiz bir ilişkisel veritabanı Çalışanlar tarafından soğuk başlatma bağlantısı olmadan, çıkış ücreti olmadan erişilebilir, tüm PoP'lara otomatik kopyalama ile.

D1, SQLite'ı dahili olarak kullanır ve bunu Cloudflare bölgesel veri merkezlerine kopyalar. Yazmalar birincil düğüme gider (kopyalar için olası tutarlılık), okumalar minimum gecikme için yerel bir kopya görevi görebilirler.

wrangler.toml'de D1 yapılandırması

# wrangler.toml

[[d1_databases]]
binding = "DB"
database_name = "mio-database"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

# Per creare il database:
# wrangler d1 create mio-database

D1 ile Geçişler ve Şema

# Crea il file di migration
wrangler d1 migrations create mio-database "create users table"
# Crea: migrations/0001_create_users_table.sql

# migrations/0001_create_users_table.sql
-- migrations/0001_create_users_table.sql
CREATE TABLE IF NOT EXISTS users (
  id        INTEGER PRIMARY KEY AUTOINCREMENT,
  email     TEXT UNIQUE NOT NULL,
  name      TEXT NOT NULL,
  role      TEXT NOT NULL DEFAULT 'user',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);

CREATE TABLE IF NOT EXISTS sessions (
  id         TEXT PRIMARY KEY,
  user_id    INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  expires_at DATETIME NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
# Applica le migration in locale (wrangler dev)
wrangler d1 migrations apply mio-database --local

# Applica le migration in produzione
wrangler d1 migrations apply mio-database

TypeScript'te D1 API: Doğrudan Sorgular

// src/services/user.service.ts

export interface Env {
  DB: D1Database;
}

interface User {
  id: number;
  email: string;
  name: string;
  role: string;
  created_at: string;
}

// -------- SELECT --------

// Query prepared statement (SEMPRE usare prepared statements: previene SQL injection)
async function getUserById(id: number, env: Env): Promise<User | null> {
  const result = await env.DB
    .prepare('SELECT * FROM users WHERE id = ?')
    .bind(id)
    .first<User>();
  return result;
}

// SELECT multipli
async function getUsersByRole(role: string, env: Env): Promise<User[]> {
  const { results } = await env.DB
    .prepare('SELECT id, email, name, role, created_at FROM users WHERE role = ? ORDER BY created_at DESC')
    .bind(role)
    .all<User>();
  return results;
}

// SELECT con pagination
async function getUsers(page: number, pageSize: number, env: Env) {
  const offset = (page - 1) * pageSize;

  const [{ results }, { total }] = await Promise.all([
    env.DB
      .prepare('SELECT * FROM users LIMIT ? OFFSET ?')
      .bind(pageSize, offset)
      .all<User>(),
    env.DB
      .prepare('SELECT COUNT(*) as total FROM users')
      .first<{ total: number }>()
      .then(r => r ?? { total: 0 }),
  ]);

  return {
    users: results,
    pagination: { page, pageSize, total: total.total, totalPages: Math.ceil(total.total / pageSize) },
  };
}

// -------- INSERT --------

async function createUser(
  email: string,
  name: string,
  env: Env,
): Promise<User> {
  const result = await env.DB
    .prepare('INSERT INTO users (email, name) VALUES (?, ?) RETURNING *')
    .bind(email, name)
    .first<User>();

  if (!result) {
    throw new Error('Failed to create user');
  }

  return result;
}

// -------- UPDATE --------

async function updateUser(id: number, updates: Partial<Pick<User, 'name' | 'role'>>, env: Env) {
  const setClauses: string[] = [];
  const values: (string | number)[] = [];

  if (updates.name !== undefined) {
    setClauses.push('name = ?');
    values.push(updates.name);
  }
  if (updates.role !== undefined) {
    setClauses.push('role = ?');
    values.push(updates.role);
  }

  if (setClauses.length === 0) return;

  setClauses.push('updated_at = CURRENT_TIMESTAMP');
  values.push(id);

  await env.DB
    .prepare(`UPDATE users SET ${setClauses.join(', ')} WHERE id = ?`)
    .bind(...values)
    .run();
}

// -------- TRANSAZIONI (batch) --------

async function createUserWithSession(
  email: string,
  name: string,
  sessionId: string,
  expiresAt: Date,
  env: Env,
): Promise<User> {
  // D1 supporta batch per eseguire piu statement in una singola round-trip
  const [userResult] = await env.DB.batch([
    env.DB
      .prepare('INSERT INTO users (email, name) VALUES (?, ?) RETURNING *')
      .bind(email, name),
    env.DB
      .prepare('INSERT INTO sessions (id, user_id, expires_at) VALUES (?, last_insert_rowid(), ?)')
      .bind(sessionId, expiresAt.toISOString()),
  ]);

  const user = userResult.results[0] as User;
  return user;
}

Drizzle ORM'li D1: Tam Güvenlik Tipi

Drizzle ORM, D1'i yerel olarak destekler ve mükemmel bir geliştirici deneyimi sunar şemadan otomatik tür çıkarımıyla:

// npm install drizzle-orm
// npm install -D drizzle-kit @types/better-sqlite3

// src/db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm';

export const users = sqliteTable('users', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  email: text('email').unique().notNull(),
  name: text('name').notNull(),
  role: text('role', { enum: ['user', 'admin', 'moderator'] }).default('user').notNull(),
  createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`),
});

export const sessions = sqliteTable('sessions', {
  id: text('id').primaryKey(),
  userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
  expiresAt: text('expires_at').notNull(),
  createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`),
});

// Tipi TypeScript inferiti dallo schema
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
// src/db/index.ts
import { drizzle } from 'drizzle-orm/d1';
import * as schema from './schema';

export function createDb(d1: D1Database) {
  return drizzle(d1, { schema });
}

// src/services/user.drizzle.service.ts
import { eq, desc, count } from 'drizzle-orm';
import { createDb } from '../db';
import { users, type User, type NewUser } from '../db/schema';

export async function getUserById(id: number, env: { DB: D1Database }): Promise<User | undefined> {
  const db = createDb(env.DB);
  return db.select().from(users).where(eq(users.id, id)).get();
}

export async function createUser(data: NewUser, env: { DB: D1Database }): Promise<User> {
  const db = createDb(env.DB);
  const [user] = await db.insert(users).values(data).returning();
  return user;
}

export async function getAdminUsers(env: { DB: D1Database }): Promise<User[]> {
  const db = createDb(env.DB);
  return db.select()
    .from(users)
    .where(eq(users.role, 'admin'))
    .orderBy(desc(users.createdAt))
    .all();
}

Karşılaştırma: KV vs R2 vs D1

Kriter İşçiler KV'si R2 D1 SQLit
Veri türü Anahtar/değer çifti (dize/blob) İkili nesneler (dosyalar) İlişkisel (SQL tabloları)
Tutarlılık Sonunda tutarlı Nihai (İşçiler ile güçlü) Güçlü (birincil), nihai (kopya)
Sorgu yetenekleri Yalnızca anahtarla al/koy/sil Yalnızca anahtarla al/koy/sil Tam SQL: JOIN'ler, toplamalar, dizinler
Maksimum boyut Değer başına 25MB Öğe başına 5 TB 10 GB (beta: 2 GB)
Depolama maliyeti 0,50 USD/GB/ay 0,015 USD/GB/ay 0,75 USD/GB/ay
Operasyon maliyeti 5 ABD doları/milyon yazma, 0,50 ABD doları/milyon okuma 4,50 USD/milyon yazma, 0,36 USD/milyon okuma 0,001 USD/M satır yazma, 0,001 USD/M satır okuma
Gecikmeyi okuma < 1 ms (PoP önbelleğinden) ~10-50ms (depolamadan) ~1-5 ms (basit sorgular)
İdeal kullanım durumları Oturumlar, yapılandırma, API önbelleği Dosyaları, varlıkları ve yedekleri yükleyin CRUD uygulaması, katalog, kullanıcılar

Gelişmiş Model: D1 için Önbellek Katmanı olarak KV

Etkili bir model, D1'i (yetkili veriler) KV (hızlı önbellek) ile birleştirir:

// src/services/cached-user.service.ts

export interface Env {
  DB: D1Database;
  CACHE: KVNamespace;
}

const USER_CACHE_TTL = 300; // 5 minuti

export async function getUserCached(userId: number, env: Env) {
  const cacheKey = `user:v2:${userId}`;

  // 1. Controlla KV cache prima (sub-ms)
  const cached = await env.CACHE.get<User>(cacheKey, 'json');
  if (cached) return cached;

  // 2. Miss: leggi da D1 (query SQL)
  const user = await getUserById(userId, env);
  if (!user) return null;

  // 3. Popola la cache KV in background (non blocca la risposta)
  // (usa ctx.waitUntil() nel fetch handler per non bloccare)
  await env.CACHE.put(cacheKey, JSON.stringify(user), {
    expirationTtl: USER_CACHE_TTL,
  });

  return user;
}

export async function invalidateUserCache(userId: number, env: Env): Promise<void> {
  await env.CACHE.delete(`user:v2:${userId}`);
}

// Quando aggiorni un utente, invalida la cache
export async function updateUserAndInvalidate(
  userId: number,
  updates: Partial<User>,
  env: Env,
): Promise<void> {
  await updateUser(userId, updates, env);
  await invalidateUserCache(userId, env);
}

Sonuçlar ve Sonraki Adımlar

Depolama katmanı seçimi veri türüne bağlıdır: Anahtar/değer çiftleri için KV yüksek okuma hızı ve düşük yazma hızıyla ikili dosyalar için R2 ve varlıklar, karmaşık ilişkilere ve sorgulara sahip yapılandırılmış veriler için D1. Birçok uygulamada gerçek üçünü de bir arada kullanacaksınız.

Serideki Sonraki Yazılar

  • Madde 4: Dayanıklı Nesneler - Son Derece Tutarlı Durum ve WebSocket: KV yeterli olmadığında ve tutarlılığa ihtiyacınız olduğunda Güçlü ve dağıtılmış koordinasyon.
  • Madde 5: İşçilerin Yapay Zekası - Yüksek Lisans ve Vizyon Modellerinin Çıkarımı Doğrudan Uç Noktaya: AI bağlamayı kullanarak İşçilerde AI modelleri nasıl çalıştırılır.
  • Madde 10: Uçta Tam Yığın Mimariler — bir örnek durum Workers + D1 + R2 + CI/CD'yi GitHub Actions ile birleştiren eksiksiz çalışma.