Cloudflare
KV
Cloudflare KV
Feature Flags
Rate Limiting
Configuração
Edge

KV per limitazione della velocità, flag di funzionalità e configurazione distribuita: dove funziona e dove si interrompe

Analisi tecnica di tre casi d'uso comuni di Cloudflare KV: limitazione della velocità (non funziona), flag di funzionalità (funziona con avvertenze) e configurazione distribuita (caso d'uso migliore).

KV per limitazione della velocità, flag di funzionalità e configurazione distribuita: dove funziona e dove si interrompe

La tentazione di utilizzare KV per limitare la velocità è comprensibile. Hai già il KV nell'associazione, è globale e la limitazione della velocità sembra semplice: incrementa un contatore per chiave IP e rifiutalo quando supera il limite. Il problema è che questa implementazione non funziona e il difetto non è abbastanza sottile da essere evidenziato durante i test.

KV non ha operazioni atomiche. Non c'è confronto e scambio, non c'è incremento atomico. Quando due Lavoratori corrono simultaneamente per lo stesso IP, entrambi guadagnano get del contatore – ottenendo, diciamo, il valore 5 – entrambi guadagnano put con il valore 6, e uno degli incrementi viene perso. Nel traffico reale, il tasso di perdita incrementale cresce con la concorrenza. Il limitatore di velocità conta meno del dovuto e le richieste che dovrebbero essere bloccate passano.

Questo non è un bug di implementazione che puoi risolvere riprovando. E' la diretta conseguenza dell'assenza delle primitive di sincronizzazione in KV. L'architettura è stata progettata per un altro tipo di carico di lavoro.

Perché la limitazione della velocità necessita di atomicità

Un contatore di limitazione della velocità deve garantire che la sequenza di lettura-incremento-scrittura sia atomica. Se due processi eseguono questa sequenza simultaneamente sullo stesso contatore, il risultato corretto è il valore originale più due. Senza atomicità, il risultato è spesso il valore originale più uno.

Cloudflare ha due soluzioni a questo problema. La prima è la funzionalità nativa di Rate Limiting, configurabile tramite regole nella dashboard o tramite Rulesets API, che opera al di sotto del livello Worker e utilizza l'infrastruttura interna con le corrette garanzie di sincronizzazione. Il secondo è Oggetti durevoli, che offre un isolamento con stato persistente e accesso serializzato: puoi implementare un contatore esatto perché solo un lavoratore alla volta viene eseguito all'interno dell'oggetto durevole per quella chiave.

KV non fa parte della soluzione per la limitazione esatta della velocità. I tentativi di implementare la limitazione della velocità con KV finiscono in sistemi che rifiutano meno traffico di quanto dovrebbero, esattamente nelle ore di punta dove la limitazione della velocità è più importante.

Flag di funzionalità con KV: cosa funziona e cosa no

I flag di funzionalità sono il caso d'uso più citato per KV e funzionano bene entro i limiti corretti. Il modello base è semplice: scrivi un oggetto JSON in una chiave con tutti i flag di sistema e ogni Worker legge questa chiave per decidere il comportamento.

// escrita (admin) await env.FLAGS.put('feature-flags', JSON.stringify({ newCheckout: true, betaSearch: false, darkMode: true })); // leitura (worker) const flags = await env.FLAGS.get('feature-flags', { type: 'json' }); if (flags.newCheckout) { /* ... */ }

Il modello funziona perché il carico di lavoro è pesante in lettura e la scrittura avviene raramente. Una bandiera cambia alcune volte a settimana. Viene letto da ciascuna richiesta di ciascun lavoratore in ciascun PoP. Il rapporto lettura/scrittura è eccellente per KV.

La vera limitazione è la propagazione di 60 secondi. L'abilitazione di un flag non lo attiva per tutti gli utenti contemporaneamente: c'è una finestra in cui parte dei PoP serve il vecchio comportamento e parte serve il nuovo. Per la maggior parte dei feature flag questo è tollerabile. Per un'implementazione critica per la sicurezza in cui è necessario che un flag raggiunga tutti gli utenti contemporaneamente, si tratta di un vero e proprio vincolo operativo.

Il punto in cui il modello non funziona è nella valutazione dei flag dinamici per utente. Se è necessario valutare i flag in base agli attributi dell'utente (piano di abbonamento, gruppo di test A/B, regione, entità specifica), i flag globali JSON non seguono questa logica. È necessaria una ricerca per utente, che di solito significa una chiamata a D1 o a un servizio esterno. Il KV rimane come una cache di configurazione globale, non come un sistema di flag completo.

Per implementazioni graduali in base alla percentuale di utenti (10% vedere la funzionalità), l'implementazione con KV richiede di codificare la logica di campionamento nel Worker e utilizzare KV solo per memorizzare la percentuale target. Il campionamento stesso è senza stato, eseguito nel Worker in base all'hash dell'ID utente, quindi il KV viene utilizzato correttamente come archivio di configurazione.

Configurazione distribuita: il miglior caso d'uso per KV

Se i flag di funzionalità sono un buon caso d'uso, la configurazione distribuita è il caso d'uso ideale. La differenza riguarda la granularità e la frequenza del cambiamento.

Modifiche alla configurazione dell'applicazione tramite un'azione operativa deliberata: aggiornamento dell'endpoint del servizio esterno, regolazione del timeout, elenco degli IP consentiti, parametri aziendali. Queste modifiche si verificano molto raramente (ore o giorni tra gli aggiornamenti) e devono essere lette per ogni richiesta.

In questo caso, il modello di memorizzazione nella cache a livello di modulo estrae il massimo da KV. Le variabili nell'ambito del modulo di un lavoratore persistono finché l'isolamento è attivo, potenzialmente per migliaia di richieste:

let config = null; export default { async fetch(request, env, ctx) { config = config ?? await env.CONFIG.get('app-settings', { type: 'json' }); // config está disponível para todos os requests // sem read do KV após o primeiro const timeout = config.upstreamTimeoutMs; // ... } };

La prima richiesta per ciascun isolato legge il KV. Tutte le richieste successive dallo stesso isolato utilizzano il valore in memoria: zero latenza KV, zero operazioni di lettura addebitate. Un aggiornamento della configurazione si propaga ai nuovi isolati man mano che gli isolati esistenti vengono eliminati dal runtime.

Il tempo di propagazione effettivo non è più pari ai 60 secondi della propagazione KV globale: è pari a 60 secondi più la durata degli isolati attivi. Gli isolati a lunga vita possono mantenere la vecchia configurazione più a lungo. Per la maggior parte delle modifiche operative, questo è accettabile. Per le situazioni di emergenza che richiedono una propagazione immediata, puoi forzare il riavvio dei Worker tramite API.

Il costo dell'operazione in ogni scenario

Per i tre casi d’uso, il modello di costo KV crea pressioni diverse. La limitazione della velocità verrebbe scritta per richiesta, irrealizzabile per qualsiasi volume. I flag di funzionalità hanno un costo minimo di scrittura e un costo di lettura che dipende da quanti lavoratori leggono la chiave in quanti PoP al secondo. Con la memorizzazione nella cache a livello di modulo, anche i flag di funzionalità letti da milioni di richieste possono consumare sorprendentemente poche operazioni di lettura: un isolato che serve 10.000 richieste legge il KV una volta.

La configurazione distribuita con memorizzazione nella cache a livello di modulo rappresenta lo scenario di costo operativo più basso possibile in KV. Una scrittura per modifica di configurazione, una lettura per isolamento per riavvio: il costo mensile delle operazioni KV per questo modello è trascurabile anche nelle applicazioni a traffico elevato.

Cosa rivelano questi tre casi su KV

L'analisi della limitazione della velocità, dei flag di funzionalità e della configurazione distribuita chiarisce la frontiera del KV: dati che si scrivono raramente e si leggono spesso, dove una finestra di incoerenza fino a 60 secondi è tollerabile e dove non è necessaria l'atomicità.

Quando una qualsiasi di queste tre condizioni non viene soddisfatta, KV produrrà bug silenziosi (nessuna atomicità) o incoerenza inaccettabile (finestra di propagazione) o costi proibitivi (alta frequenza di scrittura). Riconoscere questo limite prima dell'implementazione consente di risparmiare la sessione di debug che spesso rappresenta il modo più costoso per scoprire dove uno strumento non è applicabile.

Leggi anche