Scriem în PostgreSQL pe sublight: 1 gazdă, 1 zi, 1TB

Recent v-am spus cum, folosind rețete standard crește performanța interogărilor de citire SQL din baza de date PostgreSQL. Astăzi vom vorbi despre cum înregistrarea se poate face mai eficient în baza de date fără a utiliza „întorsături” în configurație - pur și simplu prin organizarea corectă a fluxurilor de date.

Scriem în PostgreSQL pe sublight: 1 gazdă, 1 zi, 1TB

#1. Secționarea

Un articol despre cum și de ce merită organizat împărțirea aplicată „în teorie” a fost deja, aici vom vorbi despre practica aplicării unor abordări în cadrul nostru serviciu de monitorizare pentru sute de servere PostgreSQL.

„Lucruri din zilele trecute...”

Inițial, ca orice MVP, proiectul nostru a început cu o sarcină destul de ușoară - monitorizarea s-a efectuat doar pentru cele mai critice zece servere, toate tabelele erau relativ compacte... Dar pe măsură ce trecea timpul, numărul gazdelor monitorizate a devenit din ce în ce mai mare. , și încă o dată am încercat să facem ceva cu unul dintre tabele cu dimensiunea de 1.5 TB, ne-am dat seama că, deși era posibil să trăim în continuare așa, era foarte incomod.

Vremurile erau aproape ca vremuri epice, diferite versiuni ale PostgreSQL 9.x erau relevante, așa că toate partiționările trebuiau făcute „manual” - prin moștenirea tabelului și declanșatorii rutare cu dinamică EXECUTE.

Scriem în PostgreSQL pe sublight: 1 gazdă, 1 zi, 1TB
Soluția rezultată s-a dovedit a fi suficient de universală încât să poată fi tradusă în toate tabelele:

  • A fost declarată un tabel părinte „antet” gol, care a descris totul indicii și declanșatorii necesari.
  • Înregistrarea din punctul de vedere al clientului a fost făcută în tabelul „rădăcină”, și utilizând intern declanșatorul de rutare BEFORE INSERT înregistrarea a fost introdusă „fizic” în secțiunea necesară. Dacă nu a existat încă așa ceva, am prins o excepție și...
  • … prin utilizarea CREATE TABLE ... (LIKE ... INCLUDING ...) a fost creat pe baza șablonului tabelului părinte secțiune cu o restricție privind data dorităastfel încât atunci când datele sunt preluate, citirea se efectuează numai în ea.

PG10: prima încercare

Dar, din punct de vedere istoric, partiționarea prin moștenire nu a fost prea potrivită pentru a face față unui flux de scriere activ sau a unui număr mare de partiții copii. De exemplu, vă puteți aminti că a avut algoritmul de selectare a secțiunii necesare complexitate pătratică, că funcționează cu peste 100 de secțiuni, chiar tu înțelegi cum...

În PG10 această situație a fost mult optimizată prin implementarea suportului partiţionarea nativă. Prin urmare, am încercat imediat să-l aplicăm imediat după migrarea spațiului de stocare, dar...

După cum s-a dovedit după ce am căutat manualul, tabelul partiționat nativ din această versiune este:

  • nu acceptă descrierile de index
  • nu acceptă declanșatoare pe acesta
  • nu poate fi „descendentul” nimănui
  • nu suporta INSERT ... ON CONFLICT
  • nu poate genera automat o secțiune

După ce am primit o lovitură dureroasă în frunte cu o greblă, ne-am dat seama că ar fi imposibil să facem fără modificarea aplicației și am amânat cercetările suplimentare timp de șase luni.

PG10: a doua șansă

Deci, am început să rezolvăm problemele care au apărut una câte una:

  1. Pentru că declanșează și ON CONFLICT Am constatat că mai avem nevoie de ele ici și colo, așa că am făcut o etapă intermediară pentru a le rezolva tabel proxy.
  2. Am scăpat de „rutare” în declanșatoare – adică din EXECUTE.
  3. L-au scos separat tabel șablon cu toți indecșiiastfel încât nici măcar nu sunt prezenți în tabelul proxy.

Scriem în PostgreSQL pe sublight: 1 gazdă, 1 zi, 1TB
În cele din urmă, după toate acestea, am partiționat tabelul principal în mod nativ. Crearea unei noi secțiuni este încă lăsată la conștiința aplicației.

Dicționare „de ferăstrău”.

Ca în orice sistem analitic, am avut și noi „fapte” și „tăieri” (dicționare). În cazul nostru, în această calitate au acționat, de exemplu, corpul șablonului interogări lente similare sau textul interogării în sine.

„Faptele” au fost secționate pe zi deja de mult timp, așa că am șters calm secțiunile învechite și nu ne-au deranjat (jurnalele!). Dar a fost o problemă cu dicționarele...

Ca să nu spun că au fost mulți, dar aproximativ 100 TB de „fapte” au dus la un dicționar de 2.5 TB. Nu puteți șterge în mod convenabil nimic dintr-un astfel de tabel, nu îl puteți comprima în timp adecvat și scrierea pe el a devenit treptat mai lentă.

Ca un dicționar... în el, fiecare intrare ar trebui prezentată exact o dată... și asta e corect, dar!.. Nimeni nu ne împiedică să avem un dicționar separat pentru fiecare zi! Da, aceasta aduce o anumită redundanță, dar permite:

  • scrie/citește mai repede datorită dimensiunii mai mici a secțiunii
  • consumă mai puțină memorie prin lucrul cu indici mai compacti
  • stocați mai puține date datorită capacității de a elimina rapid vechile

Ca urmare a întregului complex de măsuri Sarcina procesorului a scăzut cu ~30%, încărcarea discului cu ~50%:

Scriem în PostgreSQL pe sublight: 1 gazdă, 1 zi, 1TB
În același timp, am continuat să scriem exact același lucru în baza de date, doar cu mai puțină încărcare.

#2. Evoluția și refactorizarea bazei de date

Așa că ne-am hotărât cu ceea ce avem fiecare zi are propria sa secțiune cu date. De fapt, CHECK (dt = '2018-10-12'::date) — și există o cheie de partiționare și condiția ca o înregistrare să se încadreze într-o anumită secțiune.

Deoarece toate rapoartele din serviciul nostru sunt construite în contextul unei anumite date, indicii pentru ele de la „timii nepartiționați” au fost de toate tipurile (Server, Data, șablon de plan), (Server, Data, nodul Plan), (Data, clasa de eroare, server), ...

Dar acum trăiesc în fiecare secțiune copiile tale fiecare astfel de index... Și în cadrul fiecărei secțiuni data este o constantă... Se pare că acum suntem în fiecare astfel de index introduceți pur și simplu o constantă ca unul dintre câmpuri, care îi mărește atât volumul, cât și timpul de căutare a acestuia, dar nu aduce niciun rezultat. Au lăsat grebla pentru ei înșiși, hopa...

Scriem în PostgreSQL pe sublight: 1 gazdă, 1 zi, 1TB
Direcția de optimizare este evidentă - simplă eliminați câmpul de dată din toți indecșii pe mesele compartimentate. Având în vedere volumele noastre, câștigul este de aproximativ 1TB/săptămână!

Acum să observăm că acest terabyte mai trebuia înregistrat cumva. Adică și noi discul ar trebui să se încarce acum mai puțin! Această imagine arată clar efectul obținut în urma curățării, căreia i-am dedicat o săptămână:

Scriem în PostgreSQL pe sublight: 1 gazdă, 1 zi, 1TB

#3. „Răspândirea” sarcinii de vârf

Una dintre marile probleme ale sistemelor încărcate este sincronizare redundantă unele operatii care nu o necesita. Uneori „pentru că nu au observat”, alteori „a fost mai ușor așa”, dar mai devreme sau mai târziu trebuie să scapi de asta.

Să mărim imaginea anterioară și să vedem că avem un disc „pompe” sub sarcină cu amplitudine dublă între eșantioanele adiacente, ceea ce în mod clar „statistic” nu ar trebui să se întâmple cu un astfel de număr de operațiuni:

Scriem în PostgreSQL pe sublight: 1 gazdă, 1 zi, 1TB

Acest lucru este destul de ușor de realizat. Am început deja monitorizarea aproape 1000 de servere, fiecare este procesat de un fir logic separat, iar fiecare fir de execuție resetează informațiile acumulate pentru a fi trimise în baza de date la o anumită frecvență, cam așa:

setInterval(sendToDB, interval)

Problema aici constă tocmai în faptul că toate firele încep aproximativ în același timp, așa că orele lor de trimitere coincid aproape întotdeauna „până la obiect”. Hopa #2...

Din fericire, acest lucru este destul de ușor de remediat, adăugarea unui avans „aleatoriu”. cu timpul:

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

#4. Memorăm în cache ceea ce avem nevoie

A treia problemă tradițională de încărcare mare este fără cache unde este el ar putea a fi.

De exemplu, am făcut posibilă analiza în termeni de noduri de plan (toate acestea Seq Scan on users), dar gândește-te imediat că sunt, în cea mai mare parte, la fel - au uitat.

Nu, desigur, nimic nu este scris din nou în baza de date, acest lucru oprește declanșatorul cu INSERT ... ON CONFLICT DO NOTHING. Dar aceste date ajung în continuare în baza de date și nu sunt necesare citind pentru a verifica dacă există conflicte Trebuie să facem. Hopa #3...

Diferența dintre numărul de înregistrări trimise în baza de date înainte/după activarea stocării în cache este evidentă:

Scriem în PostgreSQL pe sublight: 1 gazdă, 1 zi, 1TB

Și aceasta este scăderea însoțitoare a încărcăturii de stocare:

Scriem în PostgreSQL pe sublight: 1 gazdă, 1 zi, 1TB

În total

„Terabyte-pe-day” sună doar înfricoșător. Dacă faci totul bine, atunci asta este doar 2^40 octeți / 86400 secunde = ~12.5 MB/scare chiar și șuruburile IDE pentru desktop au ținut. 🙂

Dar serios, chiar și cu o „înclinare” de zece ori a încărcăturii în timpul zilei, puteți îndeplini cu ușurință capacitățile SSD-urilor moderne.

Scriem în PostgreSQL pe sublight: 1 gazdă, 1 zi, 1TB

Sursa: www.habr.com

Adauga un comentariu