Recentemente ho parlato di come utilizzare ricette standardizzate da un database PostgreSQL. Oggi parleremo di come rendere più efficace la scrittura nel database senza usare alcun "giro di vite" nella configurazione, semplicemente organizzando correttamente i flussi di dati.

#1. Секционирование
Articolo su come e perché è opportuno organizzare era già presente, mentre qui discuteremo della pratica di alcuni approcci nel nostro .
"Tempi di un passato remoto…"
Inizialmente, come ogni MVP, il nostro progetto è partito con un carico piuttosto ridotto: il monitoraggio avveniva soltanto per una decina di server particolarmente critici, tutte le tabelle erano relativamente compatte… Ma il tempo passava, gli host monitorati aumentavano, e dopo aver tentato un'altra volta di fare qualcosa con una delle tabelle delle dimensioni di 1.5TB, ci siamo resi conto che si poteva continuare a vivere in questo modo, ma era piuttosto scomodo.
I tempi erano quasi leggendari, erano in uso diverse versioni di PostgreSQL 9.x, quindi tutta la suddivisione doveva essere effettuata "manualmente" — tramite ereditarietà delle tabelle e trigger di instradamento dinamico. EXECUTE.

La soluzione risultante si è rivelata sufficientemente universale da poter essere applicata a tutte le tabelle:
- È stata dichiarata una tabella genitore "vuota" che descriveva tutti gli indici e trigger necessari.
- La scrittura dal punto di vista del cliente veniva effettuata nella tabella "radice", mentre all'interno, utilizzando il trigger di instradamento
BEFORE INSERTla scrittura veniva "fisicamente" inserita nella sezione corretta. Se non esisteva ancora, catturavamo l'eccezione e … - … utilizzando secondo il modello della tabella genitore veniva creata una sezione con un vincolo sulla data necessaria, in modo che durante l'estrazione dei dati la lettura avvenisse solo lì.
PG10: primo tentativo
Ma la suddivisione tramite ereditarietà si è dimostrata storicamente poco adattabile per lavorare con un flusso attivo di scrittura o un gran numero di sezioni figlie. Ad esempio, si può ricordare che l'algoritmo per scegliere la sezione corretta aveva complessità quadratica, il che, con oltre 100 sezioni, sapete come funziona…
In PG10 questa situazione è stata fortemente ottimizzata, implementando il supporto . Pertanto, abbiamo subito provato ad applicarlo subito dopo la migrazione dello storage, ma…
Come si è rivelato dopo aver esaminato il manuale, una tabella partizionata nativamente in questa versione:
- non supporta la descrizione degli indici
- non supporta trigger su di essa
- non può essere essa stessa un "discendente" di nessuno
- non supporta
INSERT ... ON CONFLICT - non è in grado di generare automaticamente la sezione
Dopo aver preso una botta in testa dalle scelte sbagliate, abbiamo capito che senza modifiche all'applicazione non ci saremmo riusciti, e abbiamo rinviato ulteriori ricerche di sei mesi.
PG10: una seconda possibilità
Quindi abbiamo iniziato a risolvere i problemi emersi uno alla volta:
- Poiché i trigger e
ON CONFLICTsi rivelavano in alcune occasioni comunque necessari, per gestirli abbiamo creato una tabella proxy. - Ci siamo liberati del "routing" nei trigger, cioè di
EXECUTE. - Abbiamo isolato una tabella template con tutti gli indici, affinché non fossero neppure presenti sulla tabella proxy.

Infine, dopo tutto questo, abbiamo partizionato nativamente la tabella principale. La creazione di una nuova sezione è rimasta ancora a carico dell'applicazione.
"Tagliamo" i dizionari
Come in qualsiasi sistema analitico, anche noi avevamo "fatti" e "dimensioni" (dizionari). Nel nostro caso, a questo scopo, fungevano, ad esempio, di query lente simili o il testo stesso della query.
"Fatti" erano già stati partizionati per giorni da tempo, quindi eliminavamo tranquillamente le sezioni obsolete, e non ci davano fastidio (eran solo log!). Ma con i dizionari è successo un guaio…
Non si può dire che ce ne fossero moltissimi, ma circa per 100TB di "fatti" si otteneva un dizionario di 2,5TB. Da una tabella così non si può comodamente eliminare nulla, non la si può comprimere in tempi adeguati, e anche la scrittura in essa stava diventando progressivamente più lenta.
Sembra un dizionario… in esso ogni registrazione dovrebbe essere rappresentata solo una volta… ed è corretto, ma!.. Nessuno ci vieta di avere un dizionario separato per ogni giorno! Sì, questo porta a una certa ridondanza, ma consente di:
- scrivere/leggere più velocemente grazie a dimensioni minori della sezione
- consumare meno memoria grazie a operare con indici più compatti
- memorizzare meno dati grazie alla possibilità di eliminare rapidamente i dati obsoleti
A seguito di tutto questo complesso di misure il carico della CPU è diminuito di circa il 30%, mentre quello del disco di circa il 50%:

Nel contempo, abbiamo continuato a scrivere nel database esattamente le stesse cose, ma con un carico minore.
#2. Эволюция и рефакторинг БД
Quindi, siamo arrivati al punto in cui abbiamo una sezione dedicata per ogni giorno con i dati. In effetti, CHECK (dt = '2018-10-12'::date) è la chiave della partizione e la condizione per l'inserimento di un record in una sezione specifica.
Poiché tutti i report nel nostro servizio sono basati su una data specifica, anche gli indici risalenti ai "tempi non partizionati" sono tutti del tipo (Server, Data, Modello di piano), (Server, Data, Nodo di piano), (Data, Classe di errore, Server),…
Ma ora in ogni sezione ci sono le proprie istanze di ciascun indice… E all'interno di ogni sezione la data è una costante… Risulta che ora in ogni indice del genere scriviamo banalmente una costante come uno dei campi, il che aumenta sia il suo volume che i tempi di ricerca, ma non porta a nessun risultato. Ci siamo messi in difficoltà, ops...

La direzione per l'ottimizzazione è chiara: basta rimuovere il campo della data da tutti gli indici nelle tabelle partizionate. Con i nostri volumi, il risparmio è di circa 1TB/settimana!
E ora notiamo che questo terabyte doveva anche essere registrato in qualche modo. Cioè, ora dobbiamo anche caricare meno il disco! In questa immagine si può vedere chiaramente l'effetto ottenuto dalla pulizia effettuata, alla quale abbiamo dedicato una settimana:

#3. «Размазываем» пиковую нагрузку
Una delle grandi problematiche dei sistemi sovraccarichi è la sincronizzazione eccessiva di operazioni non necessarie. A volte "perché non ci siamo accorti", altre volte "perché era più semplice", ma prima o poi bisogna liberarsene.
Avviciniamo l'immagine precedente e vediamo che il disco ha un carico che oscilla con un'ampiezza doppia tra le misurazioni vicine, cosa che chiaramente statisticamente non dovrebbe succedere con tale numero di operazioni:

Ottenerlo è piuttosto semplice. Avevamo già monitorato quasi 1000 server, ognuno trattato da un flusso logico separato, e ogni flusso rilascia le informazioni accumulate per l'invio al database con una certa periodicità, più o meno così:
setInterval(sendToDB, interval)Il problema risiede nel fatto che tutti i flussi partono più o meno nello stesso momento, quindi i momenti di invio coincidono quasi sempre "fino al punto". Ops n. 2...
Fortunatamente, questo è abbastanza facile da sistemare, aggiungendo una dispersione «casuale» in tempo:
setInterval(sendToDB, interval * (1 + 0.1 * (Math.random() - 0.5)))#4. Кэшируем, что нужно можно
Il terzo problema tradizionale del highload — mancanza di caching lì dove potrebbe essere. essere.
Ad esempio, abbiamo reso possibile l'analisi in base ai nodi di piano (tutti questi Seq Scan on users), ma subito pensare che siano tutti uguali, è stato dimenticato.
No, certo, nel database non viene scritto nulla di nuovo, ciò blocca il trigger con INSERT ... ON CONFLICT DO NOTHING. Ma questi dati comunque arrivano al database, e tocca comunque effettuare un ulteriore lettura per controllare i conflitti. Oops n. 3…
La differenza nel numero di record inviati al database prima/dopo l'attivazione della cache è evidente:

E questo è il conseguente calo del carico sullo storage:

Totale
«Terabyte al giorno» sembra spaventoso. Se fai tutto nel modo giusto, sono solo 2^40 byte / 86400 secondi = ~12.5MB/s, che riuscivano anche i dischi IDE da tavolo. 🙂
E se vogliamo essere seri, anche con un ‘picco’ di carico dieci volte superiore durante il giorno, puoi tranquillamente rimanere nei limiti degli attuali SSD.

Fonte: habr.com
