La relazione presenta alcuni approcci che permettono di monitorare le prestazioni delle query SQL, quando queste sono milioni al giorno, e i server PostgreSQL controllati sono centinaia.
Quali soluzioni tecniche ci permettono di elaborare in modo efficace un tale volume di informazioni e come questo semplifica la vita agli sviluppatori comuni.

A chi è interessato l'analisi di problemi specifici e diverse tecniche di ottimizzazione delle query SQL e soluzioni per compiti tipici dei DBA in PostgreSQL — può anche su questo tema.

Mi chiamo Kirill Borovikov, rappresento . In particolare, mi specializzo nel lavoro con i database nella nostra azienda.
Oggi vi parlerò di come ottimizziamo le query, quando non è necessario «scavare» nelle prestazioni di una singola query, ma risolvere il problema in modo massiccio. Quando ci sono milioni di query e devi trovare alcuni approcci alla soluzione di questo grande problema.
In effetti, «Tenzor» per un milione dei nostri clienti è : una rete sociale aziendale, soluzioni per videochiamate, per il flusso documentale interno ed esterno, sistemi di contabilità per la contabilità e il magazzino,… Quindi un vero e proprio «mega-combinato» per la gestione complessa del business, con oltre 100 diversi progetti interni.
Affinché tutti funzionino bene e si sviluppino — abbiamo 10 centri di sviluppo in tutto il paese, con più di 1000 sviluppatori.
Lavoriamo con PostgreSQL dal 2008 e abbiamo accumulato un grande volume di dati che elaboriamo — si tratta di dati dei clienti, statistici, analitici, dati provenienti da sistemi informativi esterni — oltre 400TB. Solo in produzione ci sono circa 250 server, e in totale i server DB che monitoriamo sono circa 1000.

SQL è un linguaggio dichiarativo. Descrivi non «come» qualcosa deve funzionare, ma «cosa» vuoi ottenere. Il DBMS sa meglio come fare un JOIN - come unire le tue tabelle, quali condizioni applicare, cosa andrà per indice, cosa no…
Alcuni DBMS accettano suggerimenti: «No, unisci queste due tabelle in un certo ordine», ma PostgreSQL non funziona così. Questa è una posizione consapevole dei principali sviluppatori: «Meglio miglioriamo l'ottimizzatore di query che permettere agli sviluppatori di utilizzare suggerimenti».
Tuttavia, nonostante PostgreSQL non permetta di essere gestito "dall'esterno", offre ottime possibilità di vedere cosa succede "dentro" di esso, quando esegui la tua query e dove sorgono i suoi problemi.

In generale, quali sono i problemi classici che un sviluppatore [da DBA] solitamente incontra? "Abbiamo eseguito la query e tutto è lento, tutto si è bloccato, succede qualcosa... È davvero un problema!"
Le cause sono praticamente sempre le stesse:
- algoritmo di query inefficace
Sviluppatore: "Ora ho 10 tabelle in SQL collegate tramite JOIN..." e si aspetta che le sue condizioni si "sblocchino" magicamente in modo efficiente, ottenendo tutto rapidamente. Ma non ci sono miracoli, e qualsiasi sistema con una tale varietà (10 tabelle in un'unica FROM) darà sempre qualche errore. [] - statistiche non aggiornate
Questo è un punto molto attuale soprattutto per PostgreSQL, quando carichi un grande dataset sul server, fai una query — e lui fa un "sequential scan" su una tabella. Perché ieri c'erano 10 record, mentre oggi ci sono 10 milioni, ma PostgreSQL non ne è ancora a conoscenza e deve essere avvisato. [] - colli di bottiglia nelle risorse
Hai messo un grande e pesante database carico su un server debole, che non ha abbastanza disco, memoria e potenza del processore. E tutto... Da qualche parte c'è un limite di prestazioni oltre il quale non puoi andare. - di blocco
Un aspetto complesso, ma è particolarmente rilevante per varie query modificanti (INSERT, UPDATE, DELETE) — questo è un tema a parte e ampio.
Ottenere il piano
… E per tutto il resto abbiamo bisogno di un piano! Abbiamo bisogno di vedere cosa succede dentro il server.

Il piano di esecuzione della query in PostgreSQL è un albero dell'algoritmo di esecuzione della query in formato testuale. È proprio quell'algoritmo che, a seguito dell'analisi da parte del pianificatore, è stato riconosciuto come il più efficiente.
Ogni nodo dell'albero rappresenta un'operazione: estrazione di dati da una tabella o da un indice, costruzione di una bitmap, unione di due tabelle, fusione, intersezione o esclusione delle selezioni. L'esecuzione della query implica il passaggio attraverso i nodi di questo albero.
Per ottenere il piano di query, il modo più semplice è eseguire l'operatore EXPLAIN. Per ottenere tutti gli attributi reali, ovvero eseguire effettivamente la query sul database — EXPLAIN (ANALYZE, BUFFERS) SELECT ....
Momento negativo: quando lo esegui, accade qui e ora, quindi è adatto solo per il debugging locale. Se prendi un server molto carico, sotto un forte flusso di cambiamenti nei dati, e vedi: "Ah! Ecco dove si stava eseguendo lentamentesi la richiesta." Mezzo ora, un'ora fa — mentre correvi a recuperare questa richiesta dai log, portandola di nuovo al server, tutto il tuo dataset e la statistica erano cambiati. Lo esegui per il debugging — e viene eseguito rapidamente! E non riesci a capire "perché", perché era lento.

Per capire cosa stava succedendo esattamente nel momento in cui la richiesta viene eseguita sul server, persone esperte hanno scritto . È presente praticamente in tutte le distribuzioni più comuni di PostgreSQL e può essere facilmente attivato nel file di configurazione.
Se capisce che una richiesta sta impiegando più tempo di quanto tu abbia specificato, fa uno "snapshot" del piano di questa richiesta e lo scrive insieme nei log.

Sembra che tutto ora vada bene, andiamo nel log e vediamo lì… [portions of text]. Ma non possiamo dire niente su di esso, salvo il fatto che è un ottimo piano, perché è stato eseguito in 11 ms.
Sembra che tutto vada bene — ma non è chiaro cosa stesse realmente accadendo. Oltre al tempo totale, non vediamo molto altro. Perché guardare un tale "trattato" in testo semplice è comunque poco pratico.
Ma anche se poco pratico, ci sono problemi più rilevanti:
- Nel nodo viene indicato il totale delle risorse di tutto il sottoalbero sotto di esso. Quindi non possiamo semplicemente sapere quanto tempo è stato speso specificamente in questo Index Scan — se sotto di esso c'è una qualche condizione annidata. Dobbiamo esaminare dinamicamente se ci sono "figli" e variabili condizionali, CTE — e sottrarre tutto ciò "nella mente".
- Secondo punto: il tempo che viene indicato nel nodo è il tempo di esecuzione singolo del nodo. Se questo nodo è stato eseguito come risultato, ad esempio, di un ciclo attraverso le righe della tabella, più volte, il numero di loop — cicli di questo nodo aumenta nel piano. Ma il tempo stesso di esecuzione atomica rimane lo stesso nel piano. Quindi, per capire quanto è stato eseguito complessivamente questo nodo, è necessario moltiplicare uno per l'altro — di nuovo "nella mente".
In questo contesto, capire "Chi è il anello più debole?" è praticamente impossibile. Pertanto, anche gli stessi sviluppatori nel "manuale" scrivono che "Comprendere il piano è un'arte, che bisogna apprendere, esperienza...".
Ma abbiamo 1000 sviluppatori, e non puoi trasferire questa esperienza a ognuno di loro. Io, tu, lui — sanno, ma qualcuno laggiù — già no. Forse imparerà, forse no, ma deve già lavorare — e da dove potrebbe prendere questa esperienza?
Visualizzazione del piano
Pertanto, abbiamo capito che per affrontare questi problemi, abbiamo bisogno di una buona visualizzazione del piano.

Siamo partiti prima "dal mercato" — cerchiamo su internet cosa esiste.
Ma ci siamo resi conto che ci sono pochissime soluzioni "vive" che si sviluppano in modo più o meno attivo — praticamente solo una: di Hubert Lubaczewski. Nel campo di input "inserisci" una rappresentazione testuale del piano, ti mostra una tabella con i dati analizzati:
- tempo di lavorazione del nodo
- tempo totale su tutto il sottopiano
- numero di record che sono stati estratti e quelli statisticamente previsti
- il corpo stesso del nodo
Inoltre, questo servizio ha la possibilità di condividere un archivio di link. Hai caricato il tuo piano e dici: "Ehi, Vasya, ecco il link, c'è qualcosa che non va".

Ma ci sono anche alcuni piccoli problemi.
In primo luogo, una quantità enorme di "copiaincolla". Prendi un pezzo di log, lo inserisci lì, e di nuovo, e di nuovo.
In secondo luogo, non c'è analisi della quantità di dati letti — quei buffer, che restituisce EXPLAIN (ANALYZE, BUFFERS), qui non lo vediamo. Semplicemente non può analizzarli, comprenderli e lavorare con essi. Quando leggi molti dati e comprendi che potresti "distribuirli" in modo errato sul disco e nella cache di memoria, queste informazioni sono molto importanti.
Il terzo aspetto negativo — sviluppo molto debole di questo progetto. I commit sono molto piccoli, speriamo almeno una volta ogni sei mesi, e il codice è in Perl.

Ma tutta questa è "poesia", si potrebbe vivere in qualche modo con questo, ma c'è una cosa che ci ha allontanato notevolmente da questo servizio. Si tratta degli errori nell'analisi delle Common Table Expression (CTE) e dei vari nodi dinamici come InitPlan/SubPlan.
Se crediamo a questa immagine, il tempo totale di esecuzione di ciascun nodo singolo è maggiore del tempo totale di esecuzione di tutta la query. È semplice — non è stato sottratto il tempo di generazione di questa CTE dal nodo CTE Scan. Quindi non sappiamo più la risposta corretta su quanto tempo abbia richiesto la scansione CTE.

Qui abbiamo capito che era ora di scrivere il nostro — evviva! Ogni sviluppatore dice: "Adesso scriviamo il nostro, sarà super semplice!"
Abbiamo preso uno stack tipico per i servizi web: core in Node.js + Express, abbiamo installato Bootstrap e per diagrammi carini — D3.js. E le nostre aspettative sono state pienamente soddisfatte: abbiamo ottenuto il primo prototipo in 2 settimane:
- un parser del piano
Cioè ora possiamo analizzare qualsiasi piano generato da PostgreSQL. - analisi corretta dei nodi dinamici — CTE Scan, InitPlan, SubPlan
- analisi della distribuzione dei buffers — dove le pagine dei dati vengono lette dalla memoria, dove dal cache locale, dove dal disco
- abbiamo ottenuto chiarezza visiva
Per non dover "scavare" tutto questo nei log, ma vedere "il punto più debole" subito in immagine.

Abbiamo ottenuto un'immagine simile — subito con evidenziazione della sintassi. Ma solitamente i nostri sviluppatori lavorano già non con una visione completa del piano, ma con qualcosa di più breve. Tutti i numeri li abbiamo già analizzati e messi da parte, lasciando nel mezzo solo la prima riga che indica di che nodo si tratta: CTE Scan, generazione CTE o Seq Scan su qualche tabella.
Questa rappresentazione ridotta la chiamiamo modello del piano.

Cosa sarebbe comodo? Sarebbe comodo vedere quale percentuale di tempo totale è distribuita su quale nodo — e semplicemente "attaccare" un grafico a torta.
Ci posizioniamo sul nodo e vediamo — ci rendiamo conto che il Seq Scan ha occupato meno di un quarto del tempo totale, mentre le restanti 3/4 sono stati spesi per CTE Scan. Terribile! Questo è un piccolo appunto riguardo la "velocità" di CTE Scan, se li utilizzate attivamente nelle vostre query. Non sono molto veloci — perdono persino rispetto alla scansione tabellare normale.
Ma di solito questi diagrammi sono più interessanti, più complessi, quando ci posizioniamo immediatamente su un segmento e vediamo, ad esempio, che più della metà del tempo è "mangiata" da un certo Seq Scan. E ci potrebbe essere un Filter interno, molte registrazioni scartate... Puoi lanciare direttamente questa immagine allo sviluppatore e dire: "Vasja, qui la situazione è davvero critica! Fai un controllo, qualcosa non va!"

Naturalmente, non sono mancati i "tranelli".
La prima cosa su cui ci siamo "concentrati" è il problema dell'arrotondamento. Il tempo del nodo di ciascun singolo piano è specificato con una precisione di 1 microsecondo. E quando il numero di cicli del nodo supera, ad esempio, 1000 — dopo che PostgreSQL ha calcolato "con precisione fino a", durante il calcolo inverso otteniamo un tempo totale "da 0,95 ms a 1,05 ms". Quando si tratta di microsecondi, va bene, ma quando già si parla di [millisecondi] — durante il "disimpegno" delle risorse tra i nodi del piano, bisogna considerare questa informazione per "chi ha consumato cosa".

Il secondo aspetto, più complicato, è la distribuzione delle risorse (quei famosi buffer) tra i nodi dinamici. Questo ci costò nelle prime 2 settimane sul prototipo ulteriori 4 settimane.
Ottenere un problema del genere è piuttosto semplice: facciamo un CTE e in esso leggiamo qualcosa che apparentemente si sta leggendo. In realtà, PostgreSQL è "intelligente" e non leggerà direttamente nulla lì. Poi prendiamo la prima registrazione da essa e a essa — la centunesima dalla stessa CTE.

Osserviamo il piano e capiamo — è strano, abbiamo avuto 3 buffer (pagine di dati) "consumati" in Seq Scan, 1 in CTE Scan e 2 in un secondo CTE Scan. Quindi, se sommiamo tutto, otteniamo 6, ma dalla tabella abbiamo letto solo 3! CTE Scan infatti non legge nulla da nessuna parte, ma lavora direttamente con la memoria del processo. Quindi qui c'è chiaramente qualcosa che non va!
In realtà, si scopre che tutte e 3 le pagine di dati richieste da Seq Scan sono state prima richieste dal 1° CTE Scan e poi dal 2°, e hanno letto altre 2. Pertanto, sono state lette in totale 3 pagine di dati, non 6.

E questa immagine ci ha portato a comprendere che l'esecuzione del piano non è più un albero, ma semplicemente un grafo aciclico. Abbiamo quindi ottenuto un diagramma di questo tipo, per capire "cosa è venuto da dove". Qui abbiamo creato un CTE da pg_class, e lo abbiamo richiesto due volte, e praticamente tutto il tempo è stato speso lungo il ramo quando l'abbiamo richiesto per la seconda volta. È chiaro che leggere la registrazione 101 è di gran lunga più costoso che semplicemente la 1° dalla tabella.

Per un po' abbiamo tirato un sospiro di sollievo. Abbiamo detto: "Ora, Neo, sai il kung-fu! Ora la nostra esperienza è proprio sullo schermo. Ora puoi usarla."
Consolidamento log
I nostri 1000 sviluppatori hanno tirato un sospiro di sollievo. Ma noi capivamo che avevamo solo un centinaio di server "operativi", e tutto questo "copiaincolla" da parte degli sviluppatori non era affatto comodo. Abbiamo capito che dovevamo farlo noi stessi.

In realtà, c'è un modulo di stato che può raccogliere statistiche, ma deve essere attivato anche nel file di configurazione — è . Ma non ci ha soddisfatto.
Innanzitutto, assegna ID di query diversi a richieste identiche in schemi diversi all'interno di un unico database . Quindi, se prima facciamoSET search_path = '01'; SELECT * FROM user LIMIT 1; , e poiSET search_path = '02'; e la stessa richiesta, nella statistica di questo modulo ci saranno voci diverse, e non potrò raccogliere statistiche complessive proprio in base a questo profilo di richiesta, senza considerare gli schemi. Il secondo problema che ci ha impedito di usarlo è l'
assenza di piani . Nessun piano — c'è solo la richiesta stessa. Possiamo vedere cosa ha causato il rallentamento, ma non comprendiamo il perché. E qui torniamo al problema del dataset in rapido cambiamento.E l'ultimo problema è l'
assenza di "fatti" . Non possiamo riferirci a un'istanza specifica dell'esecuzione della query — non esiste, c'è solo statistica aggregata. Sebbene si possa lavorare anche con questo, è semplicemente molto complesso.Pertanto, abbiamo deciso di combattere contro il "copia e incolla" e abbiamo iniziato a scrivere

un raccoglitore Il raccoglitore si collega tramite SSH, stabilisce una connessione protetta al server del database tramite certificato e.
tail -F "si attacca" al file di log. In questo modo, in questa sessione otteniamo uno "specchio" completo dell'intero file di log , generato dal server. Il carico sul server stesso è minimo, poiché non stiamo analizzando nulla, stiamo semplicemente rispecchiando il traffico.Poiché abbiamo già iniziato a scrivere l'interfaccia su Node.js, abbiamo continuato a scrivere il raccoglitore su quella piattaforma. E questa tecnologia si è rivelata valida, perché è molto comodo usare JavaScript per lavorare con dati testuali scarsamente formattati, come i log. Inoltre, l'infrastruttura di Node.js come piattaforma backend consente di lavorare facilmente e comodamente con le connessioni di rete e, in generale, con flussi di dati.
Poiché abbiamo già iniziato a scrivere l'interfaccia su Node.js, abbiamo continuato a sviluppare anche il collector su di esso. E questa tecnologia si è rivelata efficace, perché per lavorare con dati testuali poco formattati, come i log, utilizzare JavaScript è molto conveniente. Inoltre, l'infrastruttura di Node.js come piattaforma backend consente di lavorare facilmente e comodamente con le connessioni di rete e, in generale, con vari flussi di dati.
Di conseguenza, stiamo "tessendo" due connessioni: la prima per "ascoltare" il log stesso e raccoglierlo, la seconda per chiedere periodicamente al database. "Ecco, nel log è arrivato che la tabella con oid 123 è stata bloccata", ma questo non dice nulla allo sviluppatore, e sarebbe utile chiedere al database "Cosa significa, alla fine, OID = 123?" E così chiediamo periodicamente al database ciò che non sappiamo ancora.

"C'è solo una cosa che non hai considerato, esiste una sorta di api elephant-like!.." Abbiamo iniziato a sviluppare questo sistema quando volevamo monitorare 10 server. I più critici secondo noi, sui quali si presentavano alcuni problemi, con i quali era difficile interagire. Ma nel primo trimestre abbiamo avuto cento server da monitorare — perché il sistema "ha funzionato", tutti lo hanno voluto, a tutti è sembrato comodo.
Tutto questo bisogna accumularlo, i flussi di dati sono grandi e attivi. In realtà, ciò che monitoriamo e con cui riusciamo a interagire è ciò che usiamo. Utilizziamo PostgreSQL anche come archivio dati. Non c'è nulla di più veloce per "inserire" i dati, se non l'operatore. COPY non c'è.
Ma semplicemente "inserire" dati non è esattamente la nostra tecnologia. Perché se hai cento server su cui avvengono circa 50k richieste al secondo, questo genera 100-150GB di log al giorno. Pertanto abbiamo dovuto "scolpire" il database con attenzione.
Innanzitutto, abbiamo fatto partizionamento per giorni, perché, in sostanza, a nessuno interessa la correlazione tra i giorni. Che differenza fa ciò che avevi ieri, se stanotte hai rilasciato una nuova versione dell'applicazione — e già hai una nuova statistica.
In secondo luogo, abbiamo imparato (siamo stati costretti a) scrivere in modo molto, molto veloce usando COPY. Cioè non solo COPY, perché è più veloce di INSERISCI, ma anche più veloce.

Il terzo punto — abbiamo dovuto abbandonare i trigger e, di conseguenza, anche i Foreign Keys. Cioè non abbiamo affatto integrità referenziale. Perché se hai una tabella con alcune FK e dici nella struttura del DB che "questa registrazione del log fa riferimento a FK, per esempio, a un gruppo di registrazioni", allora quando la inserisci, PostgreSQL non ha altra scelta che prendere e eseguire onestamente SELECT 1 FROM master_fk1_table WHERE ... con l'identificatore che stai cercando di inserire — solo per verificare che quella registrazione sia presente, per assicurarti di non "rompere" il Foreign Key con la tua inserzione.
Riceviamo invece di una sola voce nella tabella di destinazione e i suoi indici, anche oltre alla lettura di tutte le tabelle a cui si riferisce. E a noi questo non serve affatto: il nostro obiettivo è registrare il più possibile e il più velocemente possibile con il minor carico. Quindi via le FK!
Il momento successivo è l'aggregazione e l'hashing. Inizialmente erano implementati nel database, ed è comodo avere tutto a portata di mano quando arriva una registrazione, fare in una certa tabella «più uno» direttamente nel trigger. Bene, comodo, ma è problematico perché inserisci una registrazione e sei costretto a leggere e scrivere ancora qualcosa da un'altra tabella. Inoltre, non solo leggere e scrivere, ma anche farlo ogni volta.
Ora immaginate di avere una tabella in cui contate semplicemente il numero di richieste passate per un host specifico: +1, +1, +1, ..., +1. E in sostanza non vi serve, tutto questo può essere sommato in memoria sul collector e inviato al database in una sola volta +10.
Sì, in caso di problemi la vostra ‘integrità logica’ potrebbe ‘crollare’, ma questo è un caso praticamente irrealistico, perché avete un server normale, con una batteria nel controller, avete un registro delle transazioni, un registro sul sistema file... In sintesi, non ne vale la pena. Non vale la pena della perdita di prestazioni che si ottiene a causa del funzionamento dei trigger/FK, di quei costi che si sostengono.
Lo stesso vale per l'hashing. Arriva una richiesta, calcolate nel database un certo identificatore, scrivete nel database e poi lo comunicate a tutti. Tutto bene, fino a quando, nel momento della scrittura, non arriva un secondo intento di scrivere lo stesso—e qui avrete un blocco, e questo è già un problema. Pertanto, se potete spostare la generazione di alcuni ID sul client (rispetto al database), è meglio farlo.
Si è rivelato perfetto utilizzare MD5 dal testo—richiesta, piano, modello,... Lo calcoliamo dal lato del collector e ‘versiamo’ nel database un ID già pronto. La lunghezza di MD5 e la partizione giornaliera ci consentono di non preoccuparci di eventuali collisioni.

Ma per registrare tutto questo velocemente, abbiamo dovuto modificare la procedura di registrazione stessa.
Come vengono solitamente scritti i dati? Abbiamo un dataset, lo distribuiamo su più tabelle e poi utilizziamo COPY — prima nella prima, poi nella seconda, nella terza... È scomodo, perché stiamo praticamente scrivendo un flusso di dati in tre passaggi consecutivi. Scomodo. Si può fare più velocemente? Certo!
Per fare ciò basta distribuire questi flussi in parallelo tra di loro. Risulta che ci sono errori, richieste, modelli, blocchi, che si muovono in flussi separati... e noi li scriviamo tutti in parallelo. Per questo basta tenere aperto costantemente un canale COPY per ogni singola tabella di destinazione.

Cioè, il collezionista ha sempre uno stream, nel quale posso scrivere i dati di cui ho bisogno. Ma affinché il database veda questi dati, e nessuno resti bloccato in attesa che questi dati vengano registrati, COPY deve essere interrotto con una certa regolarità. Per noi, il periodo più efficiente risulta essere intorno ai 100ms — chiudiamo e subito riapriamo per la stessa tabella. E se non abbiamo abbastanza flussi durante alcuni picchi, facciamo polling fino a un certo limite.
Inoltre, abbiamo scoperto che per tale profilo di carico qualsiasi aggregazione, quando le registrazioni vengono raccolte in pacchetti, è dannosa. Il male classico è INSERT ... VALUES e successivamente 1000 registrazioni. Perché in quel momento si verifica un picco di scrittura sul supporto, e tutti gli altri che cercano di scrivere su disco dovranno aspettare.
Per eliminare tali anomalie, semplicemente non aggregare nulla, non bufferizzare affatto. E se la bufferizzazione su disco si verifica comunque (fortunatamente, lo Stream API in Node.js permette di scoprirlo) — rimanda quella connessione. Quando ricevi l'evento che è di nuovo libero — scrivi in essa dalla coda accumulata. Finché è occupata, prendi il prossimo disponibile dal pool e scrivi in esso.
Prima di implementare questo approccio alla scrittura dei dati avevamo circa 4K write ops, e con questo metodo abbiamo ridotto il carico di 4 volte. Ora siamo aumentati ulteriormente di 6 volte grazie a nuovi database osservabili — fino a 100MB/s. E ora conserviamo i log degli ultimi 3 mesi per un totale di circa 10-15TB, sperando che in tre mesi qualsiasi problema possa essere risolto da qualsiasi sviluppatore.
Comprendiamo i problemi
Ma semplicemente raccogliere tutti questi dati è buono, utile, pertinente, ma poco — è necessario comprenderli. Perché sono milioni di piani diversi al giorno.

Ma milioni sono ingestibili, bisogna prima fare "meno". E, prima di tutto, bisogna decidere come organizzare questo "meno".
Abbiamo evidenziato tre punti chiave:
- chi questa richiesta è stata inviata da
Cioè da quale applicazione è "arrivata": interfaccia web, backend, sistema di pagamento o qualcos'altro. - dove è successo
Su quale server specifico. Perché se hai diversi server per una sola applicazione, e improvvisamente uno "si blocca" (perché "il disco è rotto", "la memoria è in crisi", qualche altro guaio), allora è necessario indirizzarsi specificamente al server. - come proprio in quel piano si manifestava il problema
Per capire "chi" ci ha inviato la richiesta, utilizziamo un mezzo standard: impostare una variabile di sessione: SET application_name = '{bl-host}:{bl-method}'; — otterremo il nome dell'host della logica aziendale da cui proviene la richiesta e il nome del metodo o dell'applicazione che l'ha innescata.
Dopo aver passato il "proprietario" della richiesta, è necessario registrarlo nel log — per fare ciò configuriamo la variabile log_line_prefix = ' %m [%p:%v] [%d] %r %a'. Chi è interessato può , per vedere cosa significa tutto questo. Risultato, nel log vediamo:
- il tempo
- identificatori di processo e transazione
- nome del database
- IP di chi ha inviato questa richiesta
- e nome del metodo

Dopo abbiamo capito che non è molto interessante guardare la correlazione per una singola richiesta tra diversi server. Non si verifica spesso la situazione in cui una singola applicazione "sbaglia" in modo identico qui e là. Ma anche se è identica — guardate uno qualsiasi di questi server.
Ecco, il taglio "un server — un giorno" ci è sembrato sufficiente per qualsiasi analisi.
Il primo taglio analitico è proprio quello "modello" — una forma abbreviata di presentazione del piano, ripulita da tutte le misure numeriche. Il secondo taglio — l'applicazione o il metodo, e il terzo — è un nodo specifico del piano che ci ha causato problemi.
Quando siamo passati da istanze specifiche a modelli, abbiamo subito ottenuto due vantaggi:
- una riduzione drastica del numero di oggetti da analizzare
Non si deve più affrontare il problema su migliaia di richieste o piani, ma su decine di modelli. - timeline
Cioè, riassumendo i "fatti" all'interno di un certo contesto, è possibile mostrare la loro comparsa durante il giorno. Qui puoi capire che se hai un certo modello che si verifica, ad esempio, una volta all'ora, mentre dovrebbe essere una volta al giorno, vale la pena riflettere su cosa sia andato storto: chi e perché l'ha attivato, forse non dovrebbe nemmeno essere qui. Questo è un altro modo di analisi non numerica, puramente visivo.

Le altre modalità si basano sugli indicatori che estraiamo dal piano: quante volte è avvenuto quel modello, tempo totale e medio, quanti dati sono stati letti dal disco e quanti dalla memoria…
Perché, ad esempio, arrivi alla pagina di analisi dell'host e vedi che qualcosa sta leggendo troppo dal disco. Il disco sul server non riesce a gestire — e chi sta leggendo?
E puoi ordinare per qualsiasi colonna e decidere con cosa ti occuperai proprio ora — con il carico sulla CPU o sul disco, o con il numero totale di richieste… Hai ordinato, guardato i "top", riparato — hai lanciato una nuova versione dell'applicazione.
E subito puoi vedere diverse applicazioni che seguono lo stesso modello di richiesta tipo SELECT * FROM users WHERE login = 'Vasya'. Frontend, backend, processing… E ti chiedi perché il processing debba leggere l'utente se non interagisce con lui.
L'approccio inverso — vedere immediatamente cosa fa l'applicazione. Ad esempio, il frontend — questo, questo, e anche quest'altro una volta all'ora (il timeline aiuta proprio in questo). E subito sorge la domanda — sembra che non sia compito del frontend fare qualcosa una volta all'ora…

Dopo un po' di tempo abbiamo capito che ci mancava una statistica aggregata sui nodi del piano.Abbiamo estratto dai piani solo quei nodi che fanno qualcosa con i dati delle tabelle stesse (leggono/scrivono secondo l'indice o meno). Fondamentalmente, rispetto all'immagine precedente, si aggiunge solo un aspetto — quante righe questo nodo ci ha fornito, e quante ha scartato (Rows Removed by Filter).
Non hai un indice adatto sulla tabella, fai una richiesta a essa, passa oltre l'indice, finisci in Seq Scan… hai filtrato tutte le righe, tranne una. Ma a cosa ti serve avere 100 milioni di righe filtrate in un giorno? Non sarebbe meglio implementare un indice?

Analizzando tutti i piani per i nodi, abbiamo capito che ci sono alcune strutture standard nei piani che con un'alta probabilità sembrano sospette. E sarebbe utile suggerire allo sviluppatore: «Amico, qui prima leggi per indice, poi ordini e poi tagli» — di solito, c'è solo una registrazione.
Tutti coloro che hanno scritto query con questo pattern si sono sicuramente imbattuti in: «Dammi l'ultimo ordine di Vasya, la sua data» E se non avete un indice sulla data, o nell'indice utilizzato non c'è la data, allora proprio su queste «trappole» cadrete.
Ma sappiamo che queste sono «trappole» — quindi perché non suggerire subito allo sviluppatore cosa dovrebbe fare. Pertanto, aprendo ora il piano, il nostro sviluppatore vede subito un'immagine bella con suggerimenti, dove gli viene detto subito: «Hai problemi qui e qui, e si risolvono in questo e in quel modo.»
Di conseguenza, il volume dell'esperienza necessaria per risolvere problemi all'inizio e adesso è diminuito notevolmente. Ecco che strumento abbiamo ottenuto.

Fonte: habr.com
