Un approfondimento su come scaliamo Postgres in tempo reale
di Carlota Soto
Scegliere la dimensione di un'istanza di database prima di conoscere il carico di lavoro è un vecchio approccio di progettazione. Il processo è generalmente instabile e sembra un grande spreco di risorse di calcolo, specialmente ora che il calcolo sta diventando un lusso.
Lakebase Postgres elimina del tutto la necessità di dimensionamento grazie all'autoscaling. La reattività dell'autoscaling deriva dal ridimensionamento delle VM in loco e da un algoritmo che monitora la CPU, la memoria e il working set del database.

Ecco come si presenta l'autoscaling per un campione arbitrario di database Lakebase Postgres. Nota come questo rappresenti solo un'ora.
Il Postgres tradicional viene eseguito come un processo stateful legato a una macchina e ai suoi dischi; la sostituzione o il ridimensionamento di tale macchina è un'operazione del database perché la macchina possiede sia l'esecuzione che lo stato persistente. Ma l'architettura di Lakebase Postgres separa queste responsabilità:
Un nodo di calcolo può quindi avviarsi, arrestarsi, spostarsi o cambiare dimensione senza spostare il database sottostante. Questa è una base essenziale.

Ora, quando si tratta di implementare l'autoscaling, ci sono due aspetti da considerare: in primo luogo, bisogna determinare quando aumentare o diminuire la capacità e, in secondo luogo, come farlo senza arrestare Postgres.
Esaminiamoli entrambi in ordine.
Per dedurre quando effettuare il ridimensionamento, l'algoritmo di autoscaling di Lakebase Postgres monitora tre segnali, ognuno dei quali produce la propria dimensione di calcolo target:
cpuGoalCUmemGoalCUlfcGoalCUIl target di scalabilità finale è il maggiore tra i tre, vincolato alle dimensioni minime e massime di calcolo configurate dall'utente per quel database (i limiti di autoscaling):
La CPU è il più semplice dei tre segnali. L'algoritmo tiene costantemente sotto controllo l'intensità di lavoro del processore:
cpuGoalCU aumenta. Quando il carico prolungato diminuisce, l'obiettivo diminuisce di conseguenza.L'uso di una media a un minuto filtra le fluttuazioni molto brevi, pur rispondendo a variazioni significative della domanda. L'intervallo di polling di cinque secondi consente al sistema di aggiornare il target man mano che tale media varia.
La sola CPU, tuttavia, non è sufficiente per gestire correttamente l'autoscaling di Postgres. Una query in attesa di dati dalla rete può mostrare un basso utilizzo della CPU pur offrendo prestazioni scarse. L'algoritmo deve tenere conto anche della pressione sulla memoria e sulla cache.
La memoria presenta una modalità di guasto diversa rispetto alla CPU. Se la domanda supera brevemente la CPU disponibile, le query rallentano; ma se Postgres alloca più memoria di quella disponibile nella VM, il kernel può terminare i processi. L'autoscaler ha quindi bisogno di un segnale di esaurimento della memoria molto più rapido rispetto a quello della CPU.
Pertanto, il sistema monitora la memoria a due frequenze:
L'obiettivo di memoria mantiene l'utilizzo al di sotto del 75% della RAM allocata. Questo margine di sicurezza offre al sistema lo spazio necessario per rispondere a nuove allocazioni e lascia memoria libera per il sistema operativo guest e altri processi.
Il vm-monitor controlla anche ogni proposta di riduzione della scala (downscale). La memoria non può essere rimossa se ciò lasciasse i processi in esecuzione senza spazio sufficiente.
Un po' di storia: questo approccio di polling ha sostituito un design precedente basato sull'evento cgroup memory.high. Il superamento di memory.high causava il recupero della memoria da parte di Linux e la limitazione (throttling) dei processi all'interno del cgroup. Il polling si è rivelato più prevedibile e stabile, offrendo comunque al sistema una panoramica ogni 100 millisecondi della memoria di Postgres.
Il terzo segnale misura se i dati attivi del carico di lavoro si trovano vicino a Postgres. In sintesi, la situazione è questa:
Lakebase Postgres separa l'archiviazione e il calcolo; quando una pagina non è disponibile localmente, il nodo di calcolo la richiede al pageserver; la pagina restituita viene memorizzata nella cache per le letture successive. La cache di calcolo, che originariamente chiamavamo Local File Cache o (LFC), è una cache supportata da disco dimensionata per adattarsi alla page cache del kernel. Funge da estensione ridimensionabile dei buffer condivisi (shared buffers) di Postgres. Quando una risorsa di calcolo cresce, il vm-monitor espande la cache per utilizzare parte della memoria aggiunta.
Per molti carichi di lavoro OLTP, le prestazioni cambiano drasticamente una volta che il working set rientra nella memoria locale. Ciò evidenzia un punto cieco nell'autoscaling basato solo sulla CPU: i cache miss lasciano le query in attesa di richieste di rete, riducendo l'utilizzo della CPU. Di conseguenza, il sistema potrebbe rilevare una bassa pressione sulla CPU proprio nel momento in cui una cache più grande migliorerebbe le prestazioni. Quindi, in Lakebase Postgres, c'è un terzo segnale di autoscaling che stima direttamente il working set di Postgres.
Questa è la parte più interessante dell'algoritmo, quindi vediamo come funziona questa stima.
Il working set di un carico di lavoro è l'insieme di pagine di database e indici a cui accede ripetutamente in un determinato periodo. Contare esattamente ogni pagina ai fini dell'autoscaling richiederebbe troppa memoria, quindi il modo classico per risolvere questo problema consiste nell'affidarsi a HyperLogLog, uno stimatore probabilistico di cardinalità in grado di stimare il numero di elementi distinti in un insieme utilizzando una quantità di stato ridotta e fissa.
Per ogni accesso alle pagine di Postgres, un'implementazione standard di HyperLogLog:
La distribuzione di tali valori di registro fornirebbe una stima di quante pagine distinte sono state osservate.

Tuttavia, c'è un problema nell'utilizzare semplicemente HyperLogLog per l'autoscaling: un HyperLogLog standard cresce soltanto. Una volta che un registro ha osservato un valore, non può dire quale elemento lo abbia prodotto o quando tale elemento sia stato visto l'ultima volta.
Questo lo rende utile per rispondere a domande come: "A quante pagine distinte ha effettuato l'accesso questo nodo di calcolo dall'avvio di Postgres?". Ma l'autoscaling ha bisogno di una risposta diversa, più vicina a: "Quante pagine distinte appartengono al carico di lavoro attualmente in esecuzione?".
Senza un limite temporale, una vecchia importazione o una query analitica rimarrebbero nella stima, mantenendo il nodo di calcolo sovradimensionato molto tempo dopo la fine di quel lavoro. Abbiamo quindi modificato ciò che memorizzano i registri di HyperLogLog.
Ecco come funzionano effettivamente le cose in Lakebase Postgres:
Invece di impostare un bit quando viene osservato un hash, lo stimatore memorizza il timestamp corrente in quella posizione. Per stimare la cardinalità a partire dal momento T, considera come impostate le posizioni aggiornate dopo T e come non impostate quelle più vecchie.

HyperLogLog modificato nell'autoscaling di Lakebase Postgres.
Questo produce una stima per qualsiasi finestra che termina nel presente, tra cui
Quindi, tornando all'algoritmo, ecco come funziona effettivamente la granularità: ogni 20 secondi, l'autoscaler-agent raccoglie stime del working set per finestre temporali da uno a 60 minuti.
Ma la storia non finisce qui. Come avrai sicuramente notato, si tratta di una finestra temporale ampia. Come facciamo a sceglierla?
Il problema è questo: non esiste una finestra universale che descriva il working set corrente di un database. Se scegliamo una finestra breve, il motore di autoscaling risponde rapidamente al termine di un carico di lavoro, ma scarterebbe la cache in modo troppo aggressivo tra un picco e l'altro. Se scegliamo una finestra lunga, l'algoritmo proteggerebbe la cache, ma manterrebbe anche la memoria allocata per attività che non sono più in esecuzione.
L'algoritmo risolve questo problema analizzando come cambia il working set nel tempo. Ad esempio: per un carico di lavoro stabile, il numero stimato di pagine inizialmente cresce e poi si stabilizza. Estendere la finestra aggiunge tempo, ma vengono aggiunte poche nuove pagine, perché si accede ripetutamente allo stesso working set.

Ora, considera un carico di lavoro pesante terminato di recente. Le finestre brevi contengono solo il carico di lavoro corrente, più leggero; ma una volta che la finestra si estende abbastanza nel passato da includere il carico di lavoro precedente, la stima subisce un balzo. L'algoritmo cerca quel balzo, che segna la fine del plateau corrente.

In breve:
L'implementazione avvia la ricerca dopo cinque minuti. Ciò evita che la risorsa di calcolo si riduca immediatamente durante una breve pausa per poi crescere nuovamente al picco successivo. Ma se l'algoritmo non rileva alcun aumento netto, utilizza la stima a 60 minuti, che è il risultato previsto per un carico di lavoro stabile il cui working set rimane attivo per tutta l'ora.

C'è un ultimo tassello. Misurare il working set corrente arriva leggermente troppo tardi: supponiamo che un carico di lavoro inizi a scansionare un nuovo set di pagine. Se la cache di calcolo cresce solo dopo che quelle pagine sono state lette, le prime pagine potrebbero essere già state rimosse per fare spazio a quelle successive. La cache deve quindi recuperare nuovamente parte degli stessi dati.
Quindi l'algoritmo proietta in avanti anche la crescita del working set. Esamina come la stima aumenta da una durata all'altra e alloca abbastanza cache per il working set previsto per l'intervallo di controllo successivo.
Poiché le metriche della cache vengono recuperate ogni 20 secondi, la proiezione copre solo una frazione di minuto. Proiezioni più lunghe reagirebbero prima, ma amplificherebbero anche i brevi picchi e farebbero oscillare la risorsa di calcolo.

La dimensione proiettata (finalmente!) diventa lfcGoalCU. E l'obiettivo dell'algoritmo è far rientrare il working set nella porzione di memoria disponibile per la cache di calcolo, fino al 75% della RAM della risorsa di calcolo.
Ricapitolando: l'obiettivo di scalabilità era,
Questi tre segnali indicano al sistema a quale dimensione puntare. Applicare tale dimensione significa modificare la CPU e la memoria su una VM in esecuzione senza interrompere Postgres.
Ogni istanza Postgres in Lakebase Postgres viene eseguita all'interno della propria macchina virtuale in un cluster Kubernetes. Utilizziamo le VM perché forniscono un forte confine di isolamento e, a differenza di una allocazione di container convenzionale, consentono di aggiungere o rimuovere CPU e memoria da un guest in esecuzione.
Quattro componenti coordinano ogni ridimensionamento della risorsa di calcolo:

Come abbiamo appena visto, lo scaling up avviene quando uno dei tre obiettivi richiede più risorse di calcolo di quelle attualmente disponibili per la VM. Un upscale segue questa sequenza:
Lo scheduler è l'unica fonte di verità per l'allocazione. Vede sia la pianificazione ordinaria di Kubernetes sia le richieste di autoscaling. Senza questo coordinamento, lo scheduler potrebbe posizionare un nuovo carico di lavoro su un nodo nello stesso momento in cui l'autoscaler alloca la memoria rimanente a una VM Postgres.
Se un nodo è troppo pieno per crescere in loco, NeonVM può migrare a caldo (live-migrate) la VM su un altro nodo. La VM mantiene il suo indirizzo IP, quindi le connessioni esistenti rimangono aperte. Le risorse di calcolo di Lakebase Postgres hanno poco stato locale persistente da spostare, quindi la migrazione riguarda principalmente la memoria della VM e lo stato di runtime.
Un downscale utilizza esattamente gli stessi componenti, con un controllo aggiuntivo all'interno della VM. Il vm-monitor conferma che la rimozione della memoria ne lascerà comunque a sufficienza per Postgres e il resto del guest. In caso contrario, il downscale non procede.
Nota: lo scaling down conta quanto lo scaling up. Alcuni sistemi di autoscaling sono rapidi nell'aggiungere capacità ma lenti nel restituirla, lasciando i database sovradimensionati molto tempo dopo il passaggio di un picco. Lakebase Postgres gestisce entrambe le direzioni allo stesso modo. L'obiettivo è monitorare il carico di lavoro il più fedelmente possibile momento per momento, in modo da smettere di pagare per la capacità non appena non ne hai più bisogno.
Lakebase Postgres monitora il carico di lavoro durante l'esecuzione e ridimensiona le risorse di calcolo in tempo reale per adeguarle. L'architettura lakebase rende tutto questo possibile: poiché lo storage è disaccoppiato e persistente di per sé, la risorsa di calcolo è libera di spostarsi senza doversi preoccupare dei dati.
Il sistema risultante scala in entrambe le direzioni, su un database attivo, senza interrompere le connessioni. Aspetto ancora più importante, va oltre il segnale ovvio: il solo monitoraggio della CPU non rileverebbe un carico di lavoro bloccato a causa di cache miss, quindi l'algoritmo monitora anche la pressione sulla memoria e una stima del working set sensibile al fattore tempo.
Il ciclo finale viene eseguito su tre scale temporali:
Ecco come un database di produzione può cambiare dimensione più di 32.000 volte al mese.

Poiché le risorse di calcolo diventano sempre più costose e contese, pagare per un picco che si raggiunge raramente è un modello di progettazione che potrebbe non essere più sostenibile a breve. L'autoscaling prepara Postgres per carichi di lavoro in cui lo spreco di risorse di calcolo non è un'opzione praticabile.
Chiedi al tuo agente di distribuire Lakebase Postgres e metti alla prova l'autoscaling. Inizia qui.
Lakebase Postgres può essere utilizzato come database standalone, e puoi anche integrarlo con il resto della piattaforma Databricks Data + AI: governance di Unity Catalog, analisi lakehouse, notebook e workflow IA.
(Questo post sul blog è stato tradotto utilizzando strumenti basati sull'intelligenza artificiale) Post originale
Iscriviti al nostro blog e ricevi gli ultimi articoli direttamente nella tua casella di posta.