Scriviamo in PostgreSQL su sublight: 1 host, 1 giorno, 1 TB

Recentemente ti ho detto come, usando ricette standard aumentare le prestazioni delle query di lettura SQL dal database PostgreSQL. Oggi parleremo di come la registrazione può essere eseguita in modo più efficiente nel database senza utilizzare "twist" nella configurazione, semplicemente organizzando correttamente i flussi di dati.

Scriviamo in PostgreSQL su sublight: 1 host, 1 giorno, 1 TB

#1. Sezionando

Un articolo su come e perché vale la pena organizzarsi partizionamento applicato “in teoria” è già stato, qui parleremo della pratica di applicare alcuni approcci all'interno del nostro servizio di monitoraggio per centinaia di server PostgreSQL.

"Cose di altri tempi..."

Inizialmente, come ogni MVP, il nostro progetto è iniziato con un carico abbastanza leggero: il monitoraggio è stato effettuato solo per i dieci server più critici, tutte le tabelle erano relativamente compatte... Ma col passare del tempo, il numero di host monitorati è aumentato sempre di più , e ancora una volta abbiamo provato a fare qualcosa con uno di tabelle di dimensioni pari a 1.5 TB, ci siamo resi conto che, sebbene fosse possibile continuare a vivere così, era molto scomodo.

Erano tempi quasi epici, diverse versioni di PostgreSQL 9.x erano rilevanti, quindi tutto il partizionamento doveva essere fatto "manualmente" - tramite ereditarietà delle tabelle e trigger routing con dinamica EXECUTE.

Scriviamo in PostgreSQL su sublight: 1 host, 1 giorno, 1 TB
La soluzione risultante si è rivelata sufficientemente universale da poter essere tradotta in tutte le tabelle:

  • È stata dichiarata una tabella genitore "intestazione" vuota, che descriveva tutto indici e trigger necessari.
  • La registrazione dal punto di vista del cliente è stata effettuata nella tabella “root” e internamente utilizzando trigger di instradamento BEFORE INSERT il record è stato inserito “fisicamente” nella sezione richiesta. Se ancora non esisteva una cosa del genere, abbiamo colto un'eccezione e...
  • … usando CREATE TABLE ... (LIKE ... INCLUDING ...) è stato creato in base al modello della tabella padre sezione con una restrizione sulla data desideratain modo che quando i dati vengono recuperati, la lettura viene eseguita solo al loro interno.

PG10: primo tentativo

Ma il partizionamento tramite ereditarietà storicamente non è stato adatto a gestire un flusso di scrittura attivo o un gran numero di partizioni figlie. Ad esempio, puoi ricordare che aveva l'algoritmo per selezionare la sezione richiesta complessità quadratica, che funziona con oltre 100 sezioni, tu stesso capisci come...

In PG10 questa situazione è stata notevolmente ottimizzata implementando il supporto partizionamento nativo. Pertanto, abbiamo subito provato ad applicarlo subito dopo la migrazione dello spazio di archiviazione, ma...

Come si è scoperto dopo aver esaminato il manuale, la tabella partizionata nativamente in questa versione è:

  • non supporta le descrizioni degli indici
  • non supporta i trigger su di esso
  • non può essere il “discendente” di nessuno
  • non supportano INSERT ... ON CONFLICT
  • non può generare una sezione automaticamente

Dopo aver ricevuto un doloroso colpo alla fronte con un rastrello, ci siamo resi conto che sarebbe stato impossibile fare a meno di modificare l'applicazione e abbiamo rimandato di sei mesi ulteriori ricerche.

PG10: seconda possibilità

Quindi, abbiamo iniziato a risolvere i problemi che si sono presentati uno per uno:

  1. Perché innesca e ON CONFLICT Abbiamo scoperto che ne avevamo ancora bisogno qua e là, quindi abbiamo creato una fase intermedia per elaborarli tabella proxy.
  2. Eliminato il "routing" nei trigger, ovvero da EXECUTE.
  3. L'hanno tolto separatamente tabella modello con tutti gli indiciin modo che non siano nemmeno presenti nella tabella proxy.

Scriviamo in PostgreSQL su sublight: 1 host, 1 giorno, 1 TB
Alla fine, dopo tutto questo, abbiamo partizionato la tabella principale in modo nativo. La creazione di una nuova sezione è ancora lasciata alla coscienza dell'applicazione.

Dizionari “Segare”.

Come in ogni sistema analitico, anche noi avevamo "fatti" e "tagli" (dizionari). Nel nostro caso, in tale veste hanno agito, ad esempio, corpo del modello query lente simili o il testo della query stessa.

I "fatti" erano suddivisi di giorno già da molto tempo, quindi abbiamo cancellato con calma le sezioni obsolete e non ci hanno disturbato (registri!). Ma c'era un problema con i dizionari...

Per non dire che ce n'erano molti, ma approssimativamente 100 TB di “fatti” hanno prodotto un dizionario da 2.5 TB. Non puoi eliminare facilmente nulla da una tabella del genere, non puoi comprimerla in tempo adeguato e la scrittura su di essa è diventata gradualmente più lenta.

Come in un dizionario... in esso ogni voce dovrebbe essere presentata esattamente una volta... e questo è corretto, ma!.. Nessuno ci impedisce di avere un dizionario separato per ogni giorno! Sì, questo comporta una certa ridondanza, ma consente:

  • scrivere/leggere più velocemente a causa delle dimensioni ridotte della sezione
  • consumare meno memoria lavorando con indici più compatti
  • memorizzare meno dati grazie alla capacità di rimuovere rapidamente i file obsoleti

Come risultato dell'intero complesso di misure Il carico della CPU è diminuito del ~30%, il carico del disco del ~50%:

Scriviamo in PostgreSQL su sublight: 1 host, 1 giorno, 1 TB
Allo stesso tempo, abbiamo continuato a scrivere esattamente la stessa cosa nel database, solo con meno carico.

#2. Evoluzione e refactoring del database

Quindi abbiamo deciso su quello che abbiamo ogni giorno ha la sua sezione con i dati. In realtà, CHECK (dt = '2018-10-12'::date) - ed è presente una chiave di partizionamento e la condizione affinché un record rientri in una sezione specifica.

Poiché tutti i report nel nostro servizio sono creati nel contesto di una data specifica, i relativi indici a partire dai "tempi non partizionati" sono stati di tutti i tipi (Server, Data, Modello di piano), (Server, Data, nodo Piano), (Data, Classe di errore, Server), ...

Ma ora vivono in ogni sezione le tue copie ciascuno di questi indici... E all'interno di ciascuna sezione la data è una costante... Si scopre che ora siamo in ciascuno di questi indici inserisci semplicemente una costante come uno dei campi, che aumenta sia il suo volume che il tempo di ricerca, ma non porta alcun risultato. Hanno lasciato il rastrello a loro stessi, ops...

Scriviamo in PostgreSQL su sublight: 1 host, 1 giorno, 1 TB
La direzione dell'ottimizzazione è ovvia: semplice rimuovere il campo data da tutti gli indici su tabelle partizionate. Considerati i nostri volumi, il guadagno è di circa 1 TB/settimana!

Ora notiamo che questo terabyte doveva ancora essere registrato in qualche modo. Cioè, anche noi il disco ora dovrebbe caricarsi meno! Questa immagine mostra chiaramente l'effetto ottenuto dalla pulizia, alla quale abbiamo dedicato una settimana:

Scriviamo in PostgreSQL su sublight: 1 host, 1 giorno, 1 TB

#3. “Distribuire” il carico di punta

Uno dei grandi problemi dei sistemi caricati è sincronizzazione ridondante alcune operazioni che non lo richiedono. A volte “perché non se ne sono accorti”, a volte “era più facile così”, ma prima o poi bisogna liberarsene.

Ingrandiamo l'immagine precedente e vediamo che abbiamo un disco “pompa” sotto il carico con doppia ampiezza tra campioni adiacenti, cosa che chiaramente “statisticamente” non dovrebbe accadere con un tale numero di operazioni:

Scriviamo in PostgreSQL su sublight: 1 host, 1 giorno, 1 TB

Questo è abbastanza facile da ottenere. Abbiamo già iniziato il monitoraggio quasi 1000 server, ognuno viene elaborato da un thread logico separato e ogni thread reimposta le informazioni accumulate da inviare al database con una certa frequenza, qualcosa del genere:

setInterval(sendToDB, interval)

Il problema qui sta proprio nel fatto che tutti i thread iniziano all'incirca nello stesso momento, quindi i loro tempi di invio coincidono quasi sempre “al punto”. Ops #2...

Fortunatamente, questo è abbastanza facile da risolvere, aggiungendo una rincorsa “casuale”. per tempo:

setInterval(sendToDB, interval * (1 + 0.1 * (Math.random() - 0.5)))

#4. Mettiamo in cache ciò di cui abbiamo bisogno

Il terzo problema tradizionale del carico elevato è nessuna cache dov'è potuto essere.

Ad esempio, abbiamo reso possibile l'analisi in termini di nodi del piano (tutti questi Seq Scan on users), ma pensano subito che siano, per la maggior parte, la stessa cosa: se ne sono dimenticati.

No, ovviamente, non viene più scritto nulla nel database, questo interrompe il trigger INSERT ... ON CONFLICT DO NOTHING. Ma questi dati raggiungono comunque il database e non sono necessari leggere per verificare eventuali conflitti dover fare. Ops #3...

La differenza nel numero di record inviati al database prima/dopo l'abilitazione della memorizzazione nella cache è evidente:

Scriviamo in PostgreSQL su sublight: 1 host, 1 giorno, 1 TB

E questo è il conseguente calo del carico di archiviazione:

Scriviamo in PostgreSQL su sublight: 1 host, 1 giorno, 1 TB

In totale

“Terabyte al giorno” sembra semplicemente spaventoso. Se fai tutto bene, allora è giusto 2^40 byte/86400 secondi = ~12.5 MB/sche anche le viti IDE desktop reggevano. 🙂

Ma sul serio, anche con una "distorsione" del carico dieci volte superiore durante il giorno, puoi facilmente soddisfare le capacità dei moderni SSD.

Scriviamo in PostgreSQL su sublight: 1 host, 1 giorno, 1 TB

Fonte: habr.com

Aggiungi un commento