We schrijven in PostgreSQL over sublight: 1 host, 1 dag, 1TB
Onlangs vertelde ik je hoe, aan de hand van standaardrecepten verhoog de prestaties van SQL-leesquery's uit de PostgreSQL-database. Vandaag zullen we praten over hoe opnemen kan efficiënter in de database zonder enige “wendingen” in de configuratie te gebruiken - eenvoudigweg door de gegevensstromen correct te organiseren.
Aanvankelijk begon ons project, zoals elke MVP, onder een vrij lichte belasting - de monitoring werd alleen uitgevoerd voor de tien meest kritische servers, alle tabellen waren relatief compact... Maar naarmate de tijd verstreek, werd het aantal bewaakte hosts steeds groter , en opnieuw probeerden we iets met een van te doen tafels van 1.5 TB grootbeseften we dat het weliswaar mogelijk was om zo te blijven leven, maar dat het erg lastig was.
Het waren bijna epische tijden, verschillende versies van PostgreSQL 9.x waren relevant, dus alle partities moesten “handmatig” worden gedaan - via tabelovererving en triggers routering met dynamiek EXECUTE.
De resulterende oplossing bleek universeel genoeg te zijn om naar alle tabellen te kunnen worden vertaald:
Er werd een lege bovenliggende tabel met de koptekst gedeclareerd, waarin alles werd beschreven noodzakelijke indexen en triggers.
Het record vanuit het perspectief van de klant werd gemaakt in de "root" -tabel en werd intern gebruikt routeringstriggerBEFORE INSERT het record werd “fysiek” in de vereiste sectie ingevoegd. Als zoiets nog niet bestond, hebben we een uitzondering gemaakt en...
… door het gebruiken van CREATE TABLE ... (LIKE ... INCLUDING ...) is gemaakt op basis van de sjabloon van de bovenliggende tabel sectie met een beperking op de gewenste datumzodat wanneer gegevens worden opgehaald, er alleen daarin wordt gelezen.
PG10: eerste poging
Maar het partitioneren via overerving is historisch gezien niet erg geschikt geweest voor het omgaan met een actieve schrijfstroom of een groot aantal onderliggende partities. U kunt zich bijvoorbeeld herinneren dat het algoritme voor het selecteren van de vereiste sectie had kwadratische complexiteit, dat het werkt met 100+ secties, je begrijpt zelf hoe...
In PG10 werd deze situatie sterk geoptimaliseerd door het implementeren van ondersteuning inheemse partitie. Daarom hebben we het meteen geprobeerd toe te passen na het migreren van de opslag, maar...
Zoals bleek na het doornemen van de handleiding, is de native gepartitioneerde tabel in deze versie:
ondersteunt geen indexbeschrijvingen
ondersteunt geen triggers erop
kan niemands “afstammeling” zijn
niet ondersteunen INSERT ... ON CONFLICT
kan niet automatisch een sectie genereren
Nadat we met een hark een pijnlijke klap op het voorhoofd hadden gekregen, realiseerden we ons dat het onmogelijk zou zijn om te doen zonder de applicatie aan te passen, en stelden we verder onderzoek zes maanden uit.
PG10: tweede kans
Dus begonnen we de problemen die zich voordeden één voor één op te lossen:
Omdat triggers en ON CONFLICT Omdat we merkten dat we ze hier en daar nog nodig hadden, hebben we een tussenstap gemaakt om ze uit te werken proxy-tabel.
Weg met "routing" in triggers - dat wil zeggen, van EXECUTE.
Ze hebben het er apart uitgehaald sjabloontabel met alle indexenzodat ze niet eens aanwezig zijn in de proxytabel.
Tenslotte hebben we, na dit alles, de hoofdtabel native gepartitioneerd. Het creëren van een nieuwe sectie wordt nog steeds aan het geweten van de applicatie overgelaten.
Woordenboeken ‘zagen’
Zoals bij elk analytisch systeem hadden we dat ook "feiten" en "bezuinigingen" (woordenboeken). In ons geval handelden zij in deze hoedanigheid bijvoorbeeld sjabloonlichaam soortgelijke langzame zoekopdrachten of de tekst van de zoekopdracht zelf.
'Feiten' werden al lange tijd per dag onderverdeeld, dus hebben we rustig verouderde secties verwijderd, en we hebben er geen last van gehad (logs!). Maar er was een probleem met woordenboeken...
Om niet te zeggen dat het er veel waren, maar ongeveer 100 TB aan ‘feiten’ resulteerde in een woordenboek van 2.5 TB. Je kunt niet gemakkelijk iets uit zo'n tabel verwijderen, je kunt het niet binnen de juiste tijd comprimeren, en het schrijven ernaar werd geleidelijk langzamer.
Net als in een woordenboek... daarin zou elk item precies één keer moeten worden weergegeven... en dat klopt, maar!.. Niemand houdt ons tegen om voor elke dag een apart woordenboek! Ja, dit brengt een zekere redundantie met zich mee, maar het maakt het volgende mogelijk:
schrijf/lees sneller vanwege de kleinere sectiegrootte
verbruiken minder geheugen door met compactere indexen te werken
minder gegevens opslaan vanwege de mogelijkheid om verouderde bestanden snel te verwijderen
Als gevolg van het hele complex aan maatregelen CPU-belasting verlaagd met ~30%, schijfbelasting met ~50%:
Tegelijkertijd bleven we precies hetzelfde in de database schrijven, alleen met minder belasting.
#2. Database-evolutie en refactoring
Dus we hebben ons gebaseerd op wat we hebben elke dag heeft zijn eigen sectie met gegevens. Eigenlijk, CHECK (dt = '2018-10-12'::date) – en er is een partitiesleutel en de voorwaarde dat een record in een specifieke sectie valt.
Omdat alle rapporten in onze service zijn gebouwd in de context van een specifieke datum, zijn de indexen daarvoor sinds “niet-gepartitioneerde tijden” van alle typen geweest (Server, datum, Plansjabloon), (Server, datum, Planknooppunt), (datum, Foutklasse, Server), ...
Maar nu wonen ze op elke sectie uw kopieën elke dergelijke index... En binnen elke sectie datum is een constante... Het blijkt dat we nu in elk van deze indexen zitten voer gewoon een constante in als een van de velden, wat zowel het volume als de zoektijd ervoor vergroot, maar geen resultaat oplevert. Ze hebben de hark aan zichzelf overgelaten, oeps...
De richting van optimalisatie is duidelijk - eenvoudig verwijder het datumveld uit alle indexen op gepartitioneerde tabellen. Gezien onze volumes is de winst ongeveer 1TB/week!
Laten we er nu rekening mee houden dat deze terabyte op de een of andere manier nog steeds geregistreerd moest worden. Dat wil zeggen: wij ook de schijf zou nu minder moeten laden! Deze foto laat duidelijk het effect zien dat wordt verkregen door de schoonmaak, waaraan we een week hebben besteed:
#3. Het “spreiden” van de piekbelasting
Een van de grote problemen van geladen systemen is redundante synchronisatie sommige bewerkingen waarvoor dit niet nodig is. Soms “omdat ze het niet merkten”, soms “zo was het makkelijker”, maar vroeg of laat moet je er vanaf.
Laten we inzoomen op de vorige afbeelding en zien dat we een schijf hebben “pompt” onder de belasting met dubbele amplitude tussen aangrenzende monsters, wat duidelijk “statistisch” niet zou mogen gebeuren met een dergelijk aantal bewerkingen:
Dit is vrij eenvoudig te bereiken. We zijn al begonnen met monitoren bijna 1000 servers, elk wordt verwerkt door een afzonderlijke logische thread, en elke thread reset de verzamelde informatie die met een bepaalde frequentie naar de database moet worden verzonden, ongeveer als volgt:
setInterval(sendToDB, interval)
Het probleem ligt hier juist in het feit dat alle threads beginnen ongeveer op hetzelfde tijdstip, dus hun verzendtijden vallen bijna altijd ‘to the point’ samen. Oeps #2...
Gelukkig is dit vrij eenvoudig op te lossen, het toevoegen van een “willekeurige” aanloop tegen de tijd:
Het derde traditionele probleem met hoge belasting is geen cache waar hij is kon .
We hebben het bijvoorbeeld mogelijk gemaakt om te analyseren in termen van planknooppunten (al deze Seq Scan on users), maar denken meteen dat ze voor het grootste deel hetzelfde zijn - ze zijn het vergeten.
Nee, er wordt natuurlijk niets meer naar de database geschreven, hiermee wordt de trigger afgebroken INSERT ... ON CONFLICT DO NOTHING. Maar deze gegevens bereiken nog steeds de database en dat is niet nodig lezen om te controleren op conflicten moeten doen. Oeps #3...
Het verschil in het aantal records dat naar de database wordt verzonden vóór/nadat caching is ingeschakeld, is duidelijk:
En dit is de bijbehorende daling van de opslagbelasting:
In totaal
“Terabyte per dag” klinkt gewoon eng. Als je alles goed doet, dan is dit gewoon 2^40 bytes / 86400 seconden = ~12.5 MB/sdat zelfs desktop IDE-schroeven vasthielden. 🙂
Maar serieus, zelfs met een tienvoudige "scheefheid" van de belasting gedurende de dag kun je gemakkelijk voldoen aan de mogelijkheden van moderne SSD's.