Cloudflare Workers
D1
KV
R2
Bindings

Operai + D1 + KV + R2: compongono associazioni nello stesso servizio

La componibilità dei collegamenti in un singolo gestore è ciò che differenzia Workers da un proxy semplice, ma il limite alle sottorichieste addebita il costo di ogni operazione.

Operai + D1 + KV + R2: compongono associazioni nello stesso servizio

La proposta di valore di Workers diventa chiara quando vedi cosa è possibile fare in un singolo gestore: recuperare un record da D1 con SQL, controllare una voce in KV, recuperare un oggetto da R2, chiamare un servizio interno tramite Service Binding e accodare il lavoro asincrono in una coda - tutto all'interno della stessa invocazione, con ogni associazione inserita come proprietà di env e disponibile con una riga di codice. Nessun SDK separato da istanziare, nessuna configurazione di rete da gestire, nessuna credenziale fluttuante nelle variabili di ambiente. wrangler.toml dichiara i collegamenti, il runtime li consegna. Il problema è che ciascuna di queste operazioni consuma una sottorichiesta del budget di invocazione e il conto appare prima di quanto sembri.

Il modello di associazione e cosa risolve

Le associazioni rappresentano il modo in cui il runtime Workers connette il proprio codice alle funzionalità della piattaforma senza esporre credenziali o configurazione di rete. Quando dichiari [[d1_databases]] in wrangler.toml con un database_id, il runtime inserisce un oggetto con l'interfaccia D1 in env.DB. Quando dichiari [[kv_namespaces]] con id, il runtime inserisce l'interfaccia KV in env.CACHE. Non esiste un [token di autenticazione, non esiste un URL dell'endpoint, non esiste un SDK di terze parti: l'associazione è una chiamata diretta all'infrastruttura Cloudflare.

Ciò ha conseguenze sulla sicurezza e sul funzionamento. Un lavoratore compromesso non ha credenziali per l'esfiltrazione: può solo utilizzare i collegamenti che sono stati dichiarati per lui, con le autorizzazioni di cui tali collegamenti dispongono. Per un lavoratore che deve solo leggere dal KV, dichiari l'associazione come di sola lettura e il lavoratore non può letteralmente scrivere, indipendentemente da ciò che il codice tenta di fare. Questa separazione delle capacità è più forte di un controllo delle autorizzazioni nel codice.

Per la gestione temporanea e la produzione, ogni ambiente dispone dei propri ID di associazione in wrangler.toml. Il codice non cambia — env.DB rimane env.DB — ma nello staging punta a un diverso banco D1, un diverso spazio dei nomi KV, un diverso bucket R2. Non esiste il rischio che il codice di staging tocchi i dati di produzione perché le associazioni sono fisicamente separate.

Il budget della sottorichiesta e come viene utilizzato

Il piano gratuito prevede 50 sottorichieste per invocazione. Il piano a pagamento ne ha 1000. Ogni operazione che lascia isolata conta: fetch() per qualsiasi URL, env.KV.get(), env.KV.put(), env.DB.prepare().run(), env.BUCKET.get(), env.QUEUE.send(), env.SERVICE.fetch(). Una chiamata al numero env.DB.batch() con 5 richieste conta come 1 sottorichiesta: questo dettaglio è fondamentale.

Un tipico gestore di un'API che restituisce un profilo utente arricchito: cerca l'utente in D1 per ID (1), cerca le preferenze in KV (2), controlla una foto del profilo in R2 (3), chiama un servizio di autorizzazione tramite Service Binding (4). Totale: 4 sottorichieste nel percorso felice. Scala fino a 1000 utenti simultanei ed è ancora entro il limite: 4 sottorichieste per invocazione non sono un problema.

Il problema sorge con i loop. Un gestore che elabora un elenco di elementi ed effettua una query D1 per elemento (il classico N+1) esplode rapidamente. Cinquanta elementi con una query ciascuno raggiungono il limite del piano gratuito alla prima chiamata. Con 200 elementi nel piano a pagamento, è ancora entro 1000, ma la latenza accumulata di 200 query D1 sequenziali sarà di centinaia di millisecondi. L'errore quando viene raggiunto il limite arriva come errore di rete nella sottorichiesta che lo ha superato: nessun messaggio chiaro nel corpo della risposta al client, solo un'eccezione nella coda.

Ottimizzazioni concrete mediante rilegatura

Per D1, db.batch() è l'ottimizzazione più importante. Invece di fare prepare().run() in sequenza per più query, passi un array di istruzioni a batch() e ricevi un array di risultati, al costo di 1 sottorichiesta totale. Per le query che non dipendono l'una dall'altra (ad esempio, ricerca di configurazioni di tipo diverso), l'invio in batch elimina allo stesso tempo la latenza sequenziale e il costo delle sottorichieste.

Le query D1 parallele — Promise.all([db.query1, db.query2]) — contano ancora come sottorichieste separate, ma vengono eseguite in parallelo e la latenza è determinata dal più lento, non dalla somma. Utilizzare Promise.all() quando i risultati sono indipendenti e non è necessario il batch per un altro motivo. Usa batch() quando vuoi consolidare il costo della sottorichiesta.

Per KV, il modello più prezioso è la cache del modulo. KV ha una latenza di rete, anche nella migliore delle ipotesi, di pochi millisecondi e i dati di configurazione che cambiano raramente non necessitano di essere riletti a ogni invocazione. Un modulo può dichiarare una variabile in ambito globale:

let cachedConfig = null; export default { async fetch(request, env) { if (!cachedConfig) { cachedConfig = JSON.parse(await env.CONFIG.get('app-config')); } // usa cachedConfig } }

Finché l'isolato risiede nel PoP, le invocazioni successive riutilizzano il valore senza sprecare una sottorichiesta. Il rischio è l'obsolescenza: se la configurazione cambia, l'isolato memorizzato nella cache utilizza ancora la vecchia versione finché non viene eliminato. Per i dati che devono essere quasi in tempo reale, è più appropriato il KV con cacheTtl basso o la rilettura dell'invocazione. Per flag di funzionalità e configurazioni di infrastruttura che cambiano raramente, la cache del modulo è efficiente.

Per R2, l'associazione supporta richieste di intervallo — env.BUCKET.get(key, { range: { offset, length } }) — che ti consentono di recuperare solo la parte di un oggetto di cui hai bisogno anziché l'intero file. Per file di grandi dimensioni in cui sono necessari un'intestazione, metadati incorporati o le prime righe di un CSV, la richiesta di intervallo consente di risparmiare memoria e tempo di trasferimento. La risposta è un ReadableStream che può essere trasmesso direttamente al client senza buffering.

Composizione di più associazioni nella stessa richiesta

Il vero potere arriva quando sono necessari più tipi di dati per costruire una risposta. Un'API del prodotto che restituisce una pagina di ricerca: query SQL in D1 per gli ID che corrispondono al filtro, lettura parallela in KV per i prezzi (che cambiano frequentemente e risiedono in KV per le prestazioni di lettura) e URL prefirmato da R2 per l'immagine principale di ciascun prodotto.

Questa composizione funziona in modo naturale: env.DB, env.PRICES, env.ASSETS sono tutti disponibili nello stesso gestore. Il design che non funziona bene lo fa in sequenza per ogni elemento di un elenco. La versione corretta: la query D1 restituisce 20 ID, Promise.all() per le 20 letture KV in parallelo (20 sottorichieste, ma in parallelo), Promise.all() per i 20 URL R2 (altre 20 sottorichieste). Totale: 41 sottorichieste (1 D1 + 20 KV + 20 R2), entro il limite pagato di 1000, con latenza determinata dal più lento dei 40 fetch paralleli, non dalla somma di 41.

Monitoraggio dell'uso delle sottorichieste

Il runtime non espone i conteggi delle sottorichieste per invocazione direttamente nella coda. L'approccio è quello dello strumento: creare un semplice wrapper che incrementi un contatore con ogni operazione di associazione e registri il totale alla fine del gestore. Esportato tramite Logpush, questo numero consente di rilevare quando una distribuzione ha aumentato il consumo, prima di raggiungere un caso limite che supera il limite di produzione.

Un gestore che utilizza 500 sottorichieste quando potrebbe utilizzarne 50 con batch e parallelismo corretti sta sprecando inutilmente latenza e budget. Riprogettare questo gestore dopo un incidente nella produzione è più costoso che misurare il consumo dall'inizio e correggerlo mentre N è ancora piccolo.

Leggi anche