Vi skriver i PostgreSQL på sublight: 1 vært, 1 dag, 1 TB
For nylig fortalte jeg dig hvordan, ved hjælp af standardopskrifter øge ydelsen af SQL-læseforespørgsler fra PostgreSQL-databasen. I dag vil vi tale om hvordan optagelse kan gøres mere effektivt i databasen uden at bruge nogen "twists" i konfigurationen - blot ved at organisere datastrømmene korrekt.
Som enhver MVP startede vores projekt oprindeligt under en forholdsvis let belastning - overvågning blev kun udført for et dusin af de mest kritiske servere, alle borde var relativt kompakte... Men som tiden gik, blev antallet af overvågede værter flere og mere, og endnu en gang prøvede vi at lave noget med en af borde 1.5TB i størrelse, indså vi, at selvom det var muligt at fortsætte med at leve sådan her, var det meget ubelejligt.
Tiderne var næsten som episke tider, forskellige versioner af PostgreSQL 9.x var relevante, så al partitionering skulle udføres "manuelt" - gennem tabel arv og triggere routing med dynamik EXECUTE.
Den resulterende løsning viste sig at være universel nok til at den kunne oversættes til alle tabeller:
En tom "header" overordnet tabel blev erklæret, som beskrev alle nødvendige indekser og triggere.
Optegnelsen fra klientens synspunkt blev lavet i "root" tabellen, og internt vha routing triggerBEFORE INSERT posten blev "fysisk" indsat i den påkrævede sektion. Hvis der ikke var sådan noget endnu, fangede vi en undtagelse og...
… ved hjælp af CREATE TABLE ... (LIKE ... INCLUDING ...) blev oprettet baseret på skabelonen for den overordnede tabel afsnit med en begrænsning på den ønskede datosåledes at når data hentes, udføres læsning kun i den.
PG10: første forsøg
Men partitionering gennem arv har historisk set ikke været velegnet til at håndtere en aktiv skrivestrøm eller et stort antal underordnede partitioner. For eksempel kan du huske, at algoritmen til at vælge den nødvendige sektion havde kvadratisk kompleksitet, at det fungerer med 100+ sektioner, forstår du selv hvordan...
I PG10 blev denne situation i høj grad optimeret ved at implementere support native partitionering. Derfor forsøgte vi straks at anvende det umiddelbart efter migrering af lageret, men...
Som det viste sig efter at have gravet igennem manualen, er den oprindeligt opdelte tabel i denne version:
understøtter ikke indeksbeskrivelser
understøtter ikke triggere på den
kan ikke være nogens "efterkommer"
ikke understøtter INSERT ... ON CONFLICT
kan ikke generere et afsnit automatisk
Efter at have modtaget et smertefuldt slag i panden med en rive, indså vi, at det ville være umuligt at gøre uden at ændre ansøgningen, og udsatte yderligere forskning i seks måneder.
PG10: anden chance
Så vi begyndte at løse de problemer, der opstod én efter én:
Fordi udløser og ON CONFLICT Vi fandt ud af, at vi stadig havde brug for dem hist og her, så vi lavede en mellemstadie for at finde ud af dem proxy tabel.
Slip af med "routing" i triggere - altså fra EXECUTE.
De tog den ud separat skabelontabel med alle indekserså de ikke engang er til stede i proxy-tabellen.
Til sidst, efter alt dette, partitionerede vi hovedtabellen indbygget. Oprettelsen af et nyt afsnit er stadig overladt til ansøgningens samvittighed.
"Save" ordbøger
Som i ethvert analytisk system havde vi også "fakta" og "nedskæringer" (ordbøger). I vores tilfælde handlede de i denne egenskab f.eks. skabelonens krop lignende langsomme forespørgsler eller selve forespørgslens tekst.
“Fakta” var allerede i lang tid opdelt efter dag, så vi slettede roligt forældede afsnit, og de generede os ikke (logfiler!). Men der var et problem med ordbøger...
Ikke for at sige, at der var mange af dem, men ca 100 TB "fakta" resulterede i en 2.5 TB ordbog. Du kan ikke bekvemt slette noget fra sådan en tabel, du kan ikke komprimere den i tilstrækkelig tid, og skrivning til den blev gradvist langsommere.
Som en ordbog... i den skal hver post præsenteres nøjagtigt én gang... og det er korrekt, men!.. Ingen forhindrer os i at have en separat ordbog for hver dag! Ja, dette medfører en vis redundans, men det tillader:
skrive/læse hurtigere på grund af mindre sektionsstørrelse
bruger mindre hukommelse ved at arbejde med mere kompakte indekser
gemme mindre data på grund af muligheden for hurtigt at fjerne forældede
Som et resultat af hele komplekset af foranstaltninger CPU-belastning faldt med ~30%, diskbelastning med ~50%:
Samtidig fortsatte vi med at skrive præcis det samme ind i databasen, bare med mindre belastning.
#2. Databaseudvikling og refactoring
Så vi blev enige om, hvad vi har hver dag har sit eget afsnit med data. Rent faktisk, CHECK (dt = '2018-10-12'::date) — og der er en partitioneringsnøgle og betingelsen for, at en post falder ind i en bestemt sektion.
Da alle rapporter i vores tjeneste er bygget i sammenhæng med en bestemt dato, har indekserne for dem siden "ikke-opdelte tider" været alle typer (Server, dato, planskabelon), (Server, dato, Plan node), (dato, Fejlklasse, Server), ...
Men nu bor de på hver sektion dine kopier hvert sådant indeks... Og inden for hver sektion dato er en konstant... Det viser sig, at nu er vi i hvert sådant indeks Indtast blot en konstant som et af felterne, hvilket øger både dets volumen og søgetiden for det, men ikke giver noget resultat. De overlod riven til sig selv, ups...
Optimeringsretningen er indlysende – enkel fjern datofeltet fra alle indekser på opdelte borde. I betragtning af vores mængder er gevinsten ca 1 TB/uge!
Lad os nu bemærke, at denne terabyte stadig skulle optages på en eller anden måde. Det vil sige, vi også disken skulle nu indlæse mindre! Dette billede viser tydeligt effekten opnået fra rengøringen, som vi brugte en uge til:
#3. "Spredning" af spidsbelastningen
En af de store problemer ved indlæste systemer er redundant synkronisering nogle operationer, der ikke kræver det. Nogle gange "fordi de ikke lagde mærke til det", nogle gange "det var nemmere på den måde", men før eller siden skal du af med det.
Lad os zoome ind på det forrige billede og se, at vi har en disk "pumper" under belastningen med dobbelt amplitude mellem tilstødende prøver, hvilket klart "statistisk" ikke burde ske med et sådant antal operationer:
Dette er ret nemt at opnå. Vi er allerede begyndt at overvåge næsten 1000 servere, hver behandles af en separat logisk tråd, og hver tråd nulstiller den akkumulerede information, der skal sendes til databasen med en bestemt frekvens, noget som dette:
setInterval(sendToDB, interval)
Problemet her ligger netop i, at alle tråde starter på nogenlunde samme tid, så deres afsendelsestidspunkter falder næsten altid sammen "to the point". Ups #2...
Heldigvis er dette ret nemt at rette, tilføjelse af et "tilfældigt" opløb Med tiden:
Det tredje traditionelle højbelastningsproblem er ingen cache hvor han er kunne at være.
For eksempel gjorde vi det muligt at analysere i form af planknudepunkter (alle disse Seq Scan on users), men tror straks, at de for det meste er de samme - de har glemt.
Nej, selvfølgelig bliver der ikke skrevet noget til databasen igen, dette afbryder triggeren med INSERT ... ON CONFLICT DO NOTHING. Men disse data når stadig databasen, og det er unødvendigt læsning for at tjekke for konflikt må gøre. Ups #3...
Forskellen i antallet af poster sendt til databasen før/efter caching er aktiveret er indlysende:
Og dette er det ledsagende fald i lagerbelastning:
I alt
"Terabyte-per-dag" lyder bare skræmmende. Hvis du gør alt rigtigt, så er det bare 2^40 bytes / 86400 sekunder = ~12.5 MB/sat selv desktop IDE skruer holdt. 🙂
Men seriøst, selv med en tidoblet "skævhed" af belastningen i løbet af dagen, kan du nemt møde mulighederne i moderne SSD'er.