5b. modulD1 a gyakorlatban — mélymerülés
Cloudflare for Devs · 5b. modul (kiegészítő mélymerülés)

D1 a gyakorlatban: lockok, toolok, multitenant, flotta-üzemeltetés

Az 5. modul után maradt négy jogos aggály: tényleg megbízható-e egy „SQLite-fájl"? Elérem-e a megszokott SQL-toolokkal? Hogyan vinném át a shared-DB multitenant SaaS-omat? És hogyan üzemeltetek több száz adatbázist? Egyenes válaszok következnek — a korlátokkal együtt.

5b.1„SQLite-fájl… nem lesz ezzel gond? Lock?"

Az idegenkedésed forrása valószínűleg a klasszikus SQLite-tapasztalat: egy .db fájl a diszken, amit több processz nyitogat, database is locked hibák, write-ok tolakodása, NFS-en való használat tiltólistája. Ez a problémakör a D1-re nem vihető át, mert az architektúra alapvetően más.

Egy D1 adatbázis a gyakorlatban egy Durable Object: egy egyszálú, állapotőrző mini-szerver, amelyhez az SQLite-motor és a tár fizikailag hozzá van kötve, és amelyből pontosan egy példány létezik a hálózaton. Minden írás ezen az egyetlen példányon megy át, sorban. A klasszikus SQLite-lockolás azért létezik, mert több független processz versenyez ugyanazért a fájlért — a D1-ben nincs több processz. Nincs lock-verseny, nincs „busy timeout" tuning, nincs korrupt fájl egy elszállt writer után. A tartósságot pedig nem egy lokális diszk adja: az írások replikált, tartós tárba kerülnek (ebből a változásfolyamból él a Time Travel is), a read-ekhez pedig a platform automatikus replikákat tart (5. modul).

Klasszikus SQLite-félelem proc A proc B proc C app.db fájl lock-verseny! D1 valósága Worker Worker Worker 1 db Durable Object egyszálú writer + saját tár a kérések sorba rendeződnek — lock-hiba helyett sorbanállás
5b/1. ábra — Nincs osztott fájl és nincs lock-verseny: minden írás egyetlen, egyszálú példányon megy át.

Az őszinte rész: a lock-probléma helyét egy másik korlát veszi át: az áteresztőképesség. Egy egyszálú writer másodpercenként nagyságrendileg néhány száz–ezer egyszerű írást bír — egyetlen D1 tehát nem egy vertikálisan hizlalható Aurora-writer. Kis-közepes írásterhelésű appnál (és a legtöbb SaaS ilyen) ez bőven elég; write-heavy központi adatbázisnak viszont egy darab D1 rossz választás. A platform válasza a horizontális szétosztás: sok adatbázis (per-tenant!), amelyek írásterhelése természetesen oszlik el — így lesz az aggályból mindjárt architektúra-érv a következő szakaszban.

5b.2„De hol van ez fizikailag? És mekkora latencyvel számoljak?"

Fontos, hogy ezt tisztán lásd, mert itt tér el a D1 a Workers „mindenhol vagyok" modelljétől: a Worker-kódod tényleg mindenhol fut — a D1 primary példánya viszont egy konkrét régióban él. Ez nem D1-gyengeség, hanem a single-writer adatbázisok fizikája: az írásoknak egyetlen konzisztens igazságforrás kell, és az valahol van. Az Aurorád ma pontosan ugyanígy működik.

Amiben viszont több ráhatásod van, mint elsőre tűnik:

És itt fordul előnnyé a dolog a per-tenant mintánál: ma az ausztrál ügyfeled számára minden lassú — az app-szerver is Frankfurtban van, a DB is. Cloudflare-en az assetek, az SSR és a cache-elt/replikázott readek eleve közeliek, a per-tenant DB-knél pedig tenantonként külön döntöd el a helyet: az ausztrál tenant DB-je --location oc-cal jön létre az onboarding-kódban, az európaié weur-rel — mindenki a saját adatához van közel. Aurorával ehhez régiónkénti külön klaszterek kellenének, azok teljes költségével; itt egy paraméter. Ha pedig kivételesen írás-közeli, globális adat kellene, arra más eszköz való: Durable Object (ott jön létre, ahol először használják) vagy KV (6–7. modul).

5b.3„Elérem a szokásos SQL-management toolokkal?"

Röviden: DBeaver/TablePlus/psql közvetlenül nem — a D1 nem beszél Postgres/MySQL wire-protokollt, nincs TCP-endpoint. Amit kapsz helyette, az négy hivatalos út és egy nagyon kényelmes lokális kiskapu:

EszközMire jó
Dashboard konzolwebes query-editor + táblaböngésző minden DB-hez; gyors nézelődésre, support-esetekre ez a leggyorsabb
wrangler d1 executead-hoc SQL terminálból: --command / --file, --local / --remote, --json kimenet (szkriptelhető, jq-zható)
REST HTTP API/d1/database/<id>/query — bármilyen külső tool vagy saját szkript alapja; API-tokennel hitelesítesz
Drizzle Studio és társaia közösségi/ORM-ökoszisztéma a HTTP API-ra épül: böngészhető, szerkeszthető felület D1 fölé
Lokálisan: bármia wrangler dev alatti D1 egy sima SQLite-fájl a .wrangler/state mappában — azt bármely SQLite-tool (DB Browser, TablePlus, DataGrip) natívan megnyitja
# ad-hoc lekérdezés élesben, JSON-ban
npx wrangler d1 execute tenant-acme --remote \
  --command "SELECT id, email, created_at FROM users ORDER BY created_at DESC LIMIT 20" \
  --json | jq '.[0].results'

# nagyobb elemzéshez: exportáld, és elemezd helyben bármivel
npx wrangler d1 export tenant-acme --remote --output=./acme.sql
Gyakorlati munkafolyamat: napi fejlesztéshez a lokális SQLite-fájl + kedvenc toolod; éles gyors ránézéshez dashboard-konzol vagy wrangler d1 execute --json; mély elemzéshez (riportok, adatvizsgálat) export lokálba. Amit tényleg elveszítesz a Postgres-világból: a „csatlakozom élesre DBeaverrel és turkálok" élményt — cserébe ez auditálhatóbb és biztonságosabb is.

5b.4Multitenant SaaS: az első jó hír — a shared minta változatlanul működik

A mostani rendszered — egy közös DB, mindenhol tenant_id oszlop — egy az egyben átvihető D1-re: ugyanaz a séma, ugyanazok a query-k (SQLite-dialektusban), Drizzle-lel akár ugyanaz a kódbázis. Ilyenkor egyetlen D1-ed van, és a korlátok is egyben érvényesek rá: 10 GB összesen és egyetlen writer az összes tenantnak. Ökölszabály: ha az összes tenant-adatod belátható időn belül néhány GB alatt marad, és az aggregált írásterhelés mérsékelt (nem tartósan több száz write/s), a shared minta D1-en teljesen jó — és a legkisebb migrációs munka.

A per-tenant izolációt ilyenkor ugyanúgy app-szinten fegyelmezed, mint most — azzal a különbséggel, hogy D1-ből olcsón jár a következő szint, ha kell.

5b.5DB-per-tenant: hogyan néz ki tényleg?

Itt jön a D1 igazi különlegessége: mivel egy adatbázis létrehozása egy API-hívás (nincs instance, nincs díj üresjáratban), a „minden tenantnak saját adatbázis" minta először válik olcsóvá. Amit cserébe meg kell oldanod, az két dolog: a dinamikus elérés és a flotta-menedzsment.

A központi elem: a registry

Kell egy központi „control-plane" adatbázis (maga is lehet D1), ami a tenant→adatbázis hozzárendelést és a séma-verziókat tartja nyilván. Minden más erre épül:

-- registry DB (központi D1)
CREATE TABLE tenants (
  id            TEXT PRIMARY KEY,      -- 'acme'
  d1_database_id TEXT NOT NULL,        -- a tenant D1-ének UUID-ja
  schema_version INTEGER NOT NULL,     -- hol tart a migrációban
  status        TEXT NOT NULL,         -- active | migrating | suspended
  created_at    TEXT NOT NULL
);

Tenant-onboardingkor a Workered a REST API-val hozza létre az új DB-t (POST /accounts/:id/d1/database, akár --jurisdiction eu-val), lefuttatja rajta az aktuális sémát, és beírja a registrybe.

Dinamikus elérés: a bindings határa és a megoldások

Az 1. modul bindings-modellje itt beleütközik egy határba: a binding statikus — a wrangler-konfigban felsorolt DB-kre vonatkozik, nem tudsz „futásidőben bindolni" a 400. tenant DB-jére. Három bevált megoldás van, növekvő komplexitással:

MintaHogyan működikMikor jó
1. HTTP API a Workerbőla Worker a registryből kikeresi a d1_database_id-t, és a REST /query endpointon kérdezi a tenant DB-jét (API-token secretben)legegyszerűbb; kis extra latency és a binding-kényelemről lemondasz — induláshoz ez a pragmatikus út
2. Workers for Platformstenantonként egy „user Worker" jön létre, mindegyik saját, statikus D1-bindinggal; a fő Workered dispatch-el hozzájukez a „hivatalos nagyüzemi" minta: natív binding-sebesség, teljes izoláció tenant-szinten, ezres flottára tervezve
3. Durable Objects SQLite-tárraltenantonként egy DO (idFromName(tenantId)), mindegyikben saját SQLite (10 GB) — a „DB" és a tenant-logika egy helyen élha a tenant-műveletek amúgy is koordinációt igényelnek; cserébe nincs D1-tooling (nincs dashboard-konzol, Time Travel, HTTP API)
1. minta vázlata — dinamikus tenant-query HTTP API-n
async function queryTenant(env, dbId: string, sql: string, params: unknown[]) {
  const res = await fetch(
    `https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/d1/database/${dbId}/query`,
    {
      method: "POST",
      headers: { Authorization: `Bearer ${env.D1_API_TOKEN}`,
                 "Content-Type": "application/json" },
      body: JSON.stringify({ sql, params }),
    });
  const { result } = await res.json();
  return result[0].results;
}

// használat: registry-lookup (cache-elhető KV-ban!) → tenant-query
const tenant = await env.REGISTRY.prepare(
  "SELECT d1_database_id FROM tenants WHERE id = ?").bind(tenantId).first();
const rows = await queryTenant(env, tenant.d1_database_id,
  "SELECT * FROM invoices WHERE status = ?", ["open"]);

5b.6Séma-frissítés több száz tenant-DB-n

Ez a DB-per-tenant minta valódi ára — de jól megoldható, és a 7. modulban érkező Workflows pontosan erre való. A recept:

  1. Verziózott, idempotens migrációk. Ugyanazok a sorszámozott .sql fájlok, mint eddig; a registry schema_version mezője mondja meg tenantonként, hol tart. Idempotencia (IF NOT EXISTS, kis lépések), mert újrafuttatás mindig lesz.
  2. Canary → flotta. Először 1–2 belső/teszt tenantra futtatod, aztán néhány kis tenantra, végül a teljes flottára — a registry status mezőjével jelölve, ki van folyamatban.
  3. Orkesztráció Workflow-val. Egy Workflow végigmegy a registryn, tenantonként lépésben (retry-jal!) alkalmazza a migrációt a HTTP API-n át, és frissíti a verziót. Ha a 137. tenantnál elhal, onnan folytatódik — nem kezdi elölről.
migrációs runner vázlata (Workflow-lépésként)
const pending = await registry.prepare(
  "SELECT id, d1_database_id FROM tenants WHERE schema_version < ? AND status = 'active'"
).bind(TARGET_VERSION).all();

for (const t of pending.results) {
  await step.do(`migrate-${t.id}`, async () => {   // retry + resume ingyen
    await queryTenant(env, t.d1_database_id, MIGRATION_SQL, []);
    await registry.prepare(
      "UPDATE tenants SET schema_version = ? WHERE id = ?"
    ).bind(TARGET_VERSION, t.id).run();
  });
}
Amit ne csinálj: ne futtass „drift-elt" flottát sokáig — az app-kódod egyszerre csak kevés séma-verziót tudjon kiszolgálni (ideálisan N és N−1), a migrációs hullám pedig órák-napok alatt érjen körbe, ne hetek alatt. És mindig legyen a migrációnak ellenőrző query-je (pl. PRAGMA user_version vagy a migrations-tábla), amivel a runner induláskor validálja, tényleg ott tart-e a DB, amit a registry hisz.

5b.7Üzemeltetés több száz DB-vel: „bele kell néznem a 217.-be"

A kulcs a következetesség: névkonvenció (tenant-<slug>), a registry mint egyetlen igazságforrás, és néhány begyakorolt fogás:

FeladatEszköz
„Melyik DB az Acme-é?"registry-lookup (vagy wrangler d1 list | grep acme)
Gyors ránézés egy tenant adatáradashboard-konzol (DB kiválasztása név szerint) vagy wrangler d1 execute tenant-acme --remote --command "…" --json
Support-vizsgálat kényelmesensaját mini admin-endpoint: read-only query bármely tenantra a HTTP API-n át, audit-loggal — fél nap munka, aranyat ér
Mély elemzés / riport egy tenantrólwrangler d1 export → lokális SQLite → bármelyik toolod
Flotta-szintű kérdés („hány usernek…?")kis szkript/Workflow, ami a registryn végigmenve aggregál — vagy az analitika-eseményeket eleve központi helyre (Analytics Engine, 9. modul) tolod
„Jaj." (adatvesztés egy tenantnál)Time Travel DB-nként: csak az érintett tenantot állítod vissza — a többieket nem érinti. Ez a minta egyik legszebb tulajdonsága.

5b.8Őszinte mérleg

AggályVerdikt
„SQLite-fájl, lockolni fog"alaptalan
„Egy DB write-áteresztése véges"valós
„Nincs DBeaver/psql élesre"valós
„Nem tudom, hol lesz a DB és milyen latencyvel"alaptalan — location hint + jurisdiction létrehozáskor, replikák a read-ekre
„A shared multitenant mintám nem vihető át"alaptalan
„A DB-per-tenant flotta üzemeltethetetlen"megoldott
Ajánlás a te SaaS-odra: első körben vidd át a shared mintát egy D1-be (vagy hagyd Aurorán Hyperdrive-val, ha a méret indokolja) — nulla architektúra-változás. A DB-per-tenant váltást akkor lépd meg, ha van rá termékoldali húzóerő is: adatszeparációt kérő enterprise ügyfél, EU-jurisdiction igény, tenantonkénti restore, „zajos szomszéd" probléma. Akkor viszont a fenti registry + Workflow recept a járt út.

5b.9Ellenőrizd magad

  1. Miért nem fordulhat elő D1-ben a klasszikus database is locked hiba — és milyen korlát lép a helyébe?
    Válasz

    Nincs több, fájlért versengő processz: minden írás egyetlen egyszálú Durable Object-példányon megy át, a kérések sorba rendeződnek. A helyébe lépő korlát az áteresztőképesség: egy DB writer-kapacitása véges (~száz-ezer egyszerű írás/s), ezért write-heavy központi DB-nek egy D1 nem való.

  2. Hogyan nézel bele élesben a tenant-acme DB-be úgy, hogy nincs TCP-endpoint?
    Válasz

    Dashboard-konzol; wrangler d1 execute tenant-acme --remote --command "…" --json; vagy a REST /query API (erre épülhet saját read-only support-endpoint audit-loggal). Mély elemzéshez: wrangler d1 export → lokális SQLite-tool.

  3. Miért nem elég a wrangler-konfig statikus D1-bindingja 400 tenant-DB-hez, és mi a három megoldási minta?
    Válasz

    A binding deploy-időben rögzített lista, futásidőben nem bővíthető. Minták: (1) registry + REST /query a Workerből, (2) Workers for Platforms — tenantonkénti user Worker saját bindinggel, (3) Durable Object tenantonként, benne SQLite-tárral.

  4. Mi a séma-migráció három pillére DB-per-tenant flottán?
    Válasz

    Verziózott, idempotens migrációk + registry a tenantonkénti schema_version-nel; canary→flotta sorrend; és orkesztráció Workflow-val (lépésenkénti retry és resume), a végén verzió-ellenőrzéssel.

  5. Ausztrál tenantod panaszkodik a lassúságra. Hogyan tervezed meg a latencyt DB-per-tenant mintában — mi utazik messzire és mi nem?
    Válasz

    A tenant DB-jét --location oc hinttel Óceániában hozod létre: a write-jai és readjei helyiek lesznek. Általánosan: a write mindig a primary régiójába utazik (ezt a létrehozáskori hint határozza meg), a read a replikák miatt a userhez közeli; sok query-s kéréseknél a Smart Placement magát a Workert viszi a DB mellé.

  6. Egy tenant support-kérése miatt tegnapi állapotra kellene visszanézni az adatát. Mi az előnye itt a DB-per-tenant mintának?
    Válasz

    A Time Travel adatbázisonként működik: csak az adott tenant DB-jét állítod vissza (vagy nézed meg egy múltbeli időpontban), a többi tenantot nem érinti — shared DB-nél ez sokkal fájdalmasabb művelet lenne.

Előző5. modul — Adatbázis II: D1, a natív SQL-adatbázis Következő 6. modul — Tárolás: R2 objektumtár és KV