Vi skriver i PostgreSQL på sublight: 1 vert, 1 dag, 1TB
Nylig fortalte jeg deg hvordan, ved å bruke standardoppskrifter øke ytelsen til SQL-lesespørringer fra PostgreSQL-databasen. I dag skal vi snakke om hvordan opptak kan gjøres mer effektivt i databasen uten å bruke noen "twists" i konfigurasjonen - ganske enkelt ved å organisere datastrømmene riktig.
Til å begynne med, som enhver MVP, startet prosjektet vårt under en ganske lett belastning - overvåking ble utført kun for de ti mest kritiske serverne, alle tabeller var relativt kompakte... Men etter hvert som tiden gikk, ble antallet overvåkede verter mer og mer , og nok en gang prøvde vi å gjøre noe med en av bord 1.5TB i størrelse, innså vi at selv om det var mulig å fortsette å leve slik, var det veldig upraktisk.
Tidene var nesten som episke tider, forskjellige versjoner av PostgreSQL 9.x var relevante, så all partisjonering måtte gjøres "manuelt" - gjennom tabell arv og triggere ruting med dynamikk EXECUTE.
Den resulterende løsningen viste seg å være universell nok til at den kunne oversettes til alle tabeller:
En tom "header" overordnet tabell ble erklært, som beskrev alle nødvendige indekser og triggere.
Oppføringen fra klientens synspunkt ble laget i "root"-tabellen, og internt vha rutingsutløserBEFORE INSERT posten ble "fysisk" satt inn i den nødvendige delen. Hvis det ikke var noe slikt ennå, fanget vi et unntak og...
… ved bruk av CREATE TABLE ... (LIKE ... INCLUDING ...) ble opprettet basert på malen til den overordnede tabellen seksjon med begrensning på ønsket datoslik at når data hentes, utføres lesing kun i den.
PG10: første forsøk
Men partisjonering gjennom arv har historisk sett ikke vært godt egnet til å håndtere en aktiv skrivestrøm eller et stort antall underordnede partisjoner. For eksempel kan du huske at algoritmen for å velge den nødvendige delen hadde kvadratisk kompleksitet, at det fungerer med 100+ seksjoner, forstår du selv hvordan...
I PG10 ble denne situasjonen sterkt optimalisert ved å implementere støtte innfødt partisjonering. Derfor prøvde vi umiddelbart å bruke den umiddelbart etter migrering av lagringen, men...
Som det viste seg etter å ha gravd gjennom manualen, er den opprinnelig partisjonerte tabellen i denne versjonen:
støtter ikke indeksbeskrivelser
støtter ikke triggere på den
kan ikke være noens "etterkommer"
ikke støtter INSERT ... ON CONFLICT
kan ikke generere en seksjon automatisk
Etter å ha fått et smertefullt slag i pannen med en rive, innså vi at det ville være umulig å gjøre uten å endre søknaden, og utsatte videre forskning i seks måneder.
PG10: andre sjanse
Så vi begynte å løse problemene som oppsto en etter en:
Fordi utløser og ON CONFLICT Vi fant ut at vi fortsatt trengte dem her og der, så vi lagde et mellomstadium for å finne ut av dem proxy-tabell.
Ble kvitt "ruting" i triggere - altså fra EXECUTE.
De tok den ut separat maltabell med alle indekserslik at de ikke en gang er tilstede i proxy-tabellen.
Til slutt, etter alt dette, partisjonerte vi hovedtabellen naturlig. Opprettelsen av en ny seksjon er fortsatt overlatt til søknadens samvittighet.
"Saging" ordbøker
Som i ethvert analytisk system hadde vi også "fakta" og "kutt" (ordbøker). I vårt tilfelle handlet de i denne egenskapen f.eks. maltekst lignende trege spørringer eller teksten til selve spørringen.
"Fakta" ble seksjonert etter dag i lang tid allerede, så vi slettet rolig utdaterte seksjoner, og de plaget oss ikke (logger!). Men det var et problem med ordbøker...
Ikke for å si at det var mange av dem, men ca 100 TB med "fakta" resulterte i en 2.5 TB ordbok. Du kan ikke enkelt slette noe fra en slik tabell, du kan ikke komprimere den i tilstrekkelig tid, og det ble gradvis tregere å skrive til den.
Som en ordbok... i den skal hver oppføring presenteres nøyaktig én gang... og dette er riktig, men!.. Ingen hindrer oss i å ha en egen ordbok for hver dag! Ja, dette gir en viss redundans, men det tillater:
skrive/lese raskere på grunn av mindre seksjonsstørrelse
bruker mindre minne ved å jobbe med mer kompakte indekser
lagre mindre data på grunn av muligheten til raskt å fjerne utdaterte
Som et resultat av hele komplekset av tiltak CPU-belastningen ble redusert med ~30 %, diskbelastningen med ~50 %:
Samtidig fortsatte vi å skrive nøyaktig det samme inn i databasen, bare med mindre belastning.
#2. Databaseevolusjon og refaktorering
Så vi bestemte oss for det vi har hver dag har sin egen del med data. Faktisk, CHECK (dt = '2018-10-12'::date) — og det er en partisjoneringsnøkkel og betingelsen for at en post skal falle inn i en bestemt seksjon.
Siden alle rapporter i tjenesten vår er bygget i sammenheng med en bestemt dato, har indeksene for dem siden "ikke-partisjonerte tider" vært alle typer (Server, Dato, planmal), (Server, Dato, Plan node), (Dato, Feilklasse, Server), ...
Men nå bor de på hver seksjon dine kopier hver slik indeks... Og innenfor hver seksjon dato er en konstant... Det viser seg at nå er vi i hver slik indeks bare angi en konstant som et av feltene, som øker både volumet og søketiden for det, men ikke gir noe resultat. De overlot raken til seg selv, ups...
Optimaliseringsretningen er åpenbar – enkel fjern datofeltet fra alle indekser på partisjonerte tabeller. Gitt våre volumer er gevinsten ca 1TB/uke!
La oss nå merke oss at denne terabyten fortsatt måtte spilles inn på en eller annen måte. Det vil si, vi også disken skal nå laste mindre! Dette bildet viser tydelig effekten oppnådd fra rengjøringen, som vi viet en uke til:
#3. "Spredning" av toppbelastningen
En av de store problemene med lastede systemer er redundant synkronisering noen operasjoner som ikke krever det. Noen ganger "fordi de ikke la merke til det", noen ganger "det var lettere sånn", men før eller siden må du bli kvitt det.
La oss zoome inn på forrige bilde og se at vi har en disk "pumper" under belastningen med dobbel amplitude mellom tilstøtende prøver, noe som klart "statistisk" ikke bør skje med et slikt antall operasjoner:
Dette er ganske enkelt å få til. Vi har allerede begynt å overvåke nesten 1000 servere, hver behandles av en separat logisk tråd, og hver tråd tilbakestiller den akkumulerte informasjonen som skal sendes til databasen med en viss frekvens, noe som dette:
setInterval(sendToDB, interval)
Problemet her ligger nettopp i det faktum at alle tråder starter omtrent samtidig, så sendetidene deres faller nesten alltid sammen "til poenget." Oops #2...
Heldigvis er dette ganske enkelt å fikse, legge til en "tilfeldig" oppkjøring etter tid:
Det tredje tradisjonelle høybelastningsproblemet er ingen cache hvor han er kunne å være.
For eksempel gjorde vi det mulig å analysere i form av plannoder (alle disse Seq Scan on users), men tenker umiddelbart at de for det meste er de samme - de glemte det.
Nei, selvfølgelig, ingenting skrives til databasen igjen, dette kutter av utløseren med INSERT ... ON CONFLICT DO NOTHING. Men disse dataene når fortsatt databasen, og det er unødvendig les for å se etter konflikt må gjøre. Oops #3...
Forskjellen i antall poster sendt til databasen før/etter caching er aktivert er åpenbar:
Og dette er den medfølgende nedgangen i lagringsmengden:
Totalt
"Terabyte-per-dag" høres bare skummelt ut. Hvis du gjør alt riktig, så er dette bare 2^40 byte / 86400 sekunder = ~12.5 MB/sat selv stasjonære IDE-skruer holdt. 🙂
Men seriøst, selv med en tidoblet "skjevhet" av belastningen i løpet av dagen, kan du enkelt møte egenskapene til moderne SSD-er.