Dit zouden meer ontwikkelaars over databases moeten weten

Opmerking. vert.: Jaana Dogan is een ervaren ingenieur bij Google die momenteel werkt aan de waarneembaarheid van de productieservices van het bedrijf, geschreven in Go. In dit artikel, dat grote populariteit verwierf onder het Engelssprekende publiek, verzamelde ze in 17 punten belangrijke technische details over DBMS'en (en soms gedistribueerde systemen in het algemeen) die nuttig zijn om te overwegen voor ontwikkelaars van grote/veeleisende applicaties.

Dit zouden meer ontwikkelaars over databases moeten weten

De overgrote meerderheid van computersystemen houdt hun staat bij en heeft daarom een ​​soort gegevensopslagsysteem nodig. Ik heb gedurende een lange periode kennis over databases opgebouwd, waarbij ik gaandeweg ontwerpfouten heb gemaakt die tot gegevensverlies en -storingen hebben geleid. In systemen die grote hoeveelheden informatie verwerken, vormen databases de kern van de systeemarchitectuur en fungeren ze als een sleutelelement bij het kiezen van de optimale oplossing. Ondanks dat er veel aandacht wordt besteed aan het werk van de database, zijn de problemen waar applicatieontwikkelaars op proberen te anticiperen vaak slechts het topje van de ijsberg. In deze reeks artikelen deel ik enkele ideeën die nuttig zullen zijn voor ontwikkelaars die niet gespecialiseerd zijn op dit gebied.

  1. Je hebt geluk als het netwerk 99,999% van de tijd geen problemen veroorzaakt.
  2. ZUUR betekent veel verschillende dingen.
  3. Elke database heeft zijn eigen mechanismen om consistentie en isolatie te garanderen.
  4. Optimistische blokkering komt te hulp als het moeilijk is om de gebruikelijke te handhaven.
  5. Naast vuile leesbewerkingen en gegevensverlies zijn er nog andere afwijkingen.
  6. De database en de gebruiker zijn het niet altijd eens over de handelswijze.
  7. Sharding op toepassingsniveau kan buiten de toepassing worden verplaatst.
  8. Automatisch verhogen kan gevaarlijk zijn.
  9. Verouderde gegevens kunnen nuttig zijn en hoeven niet te worden vergrendeld.
  10. Vervormingen zijn typisch voor alle tijdbronnen.
  11. Vertraging heeft vele betekenissen.
  12. Prestatie-eisen moeten worden geëvalueerd voor een specifieke transactie.
  13. Geneste transacties kunnen gevaarlijk zijn.
  14. Transacties mogen niet aan de applicatiestatus worden gekoppeld.
  15. Queryplanners kunnen u veel vertellen over databases.
  16. Onlinemigratie is moeilijk, maar mogelijk.
  17. Een aanzienlijke uitbreiding van de database brengt een toename van de onvoorspelbaarheid met zich mee.

Ik wil Emmanuel Odeke, Rein Henrichs en anderen bedanken voor hun feedback op een eerdere versie van dit artikel.

Je hebt geluk als het netwerk 99,999% van de tijd geen problemen veroorzaakt.

De vraag blijft hoe betrouwbaar moderne netwerktechnologieën zijn en hoe vaak systemen uitvallen als gevolg van netwerkstoringen. Informatie hierover is schaars en het onderzoek wordt vaak gedomineerd door grote organisaties met gespecialiseerde netwerken, apparatuur en personeel.

Met een beschikbaarheidspercentage van 99,999% voor Spanner (de wereldwijd gedistribueerde database van Google) beweert Google dat alleen 7,6% problemen hebben te maken met het netwerk. Tegelijkertijd noemt het bedrijf zijn gespecialiseerde netwerk de “belangrijkste pijler” van hoge beschikbaarheid. Studie Bailis en Kingsbury, uitgevoerd in 2014, daagt een van de “misvattingen over gedistribueerd computergebruik", die Peter Deutsch in 1994 formuleerde. Is het netwerk echt betrouwbaar?

Uitgebreid onderzoek buiten gigantische bedrijven, uitgevoerd voor het bredere internet, bestaat eenvoudigweg niet. Er zijn ook niet genoeg gegevens van de grote spelers over welk percentage van de problemen van hun klanten netwerkgerelateerd is. We zijn ons terdege bewust van storingen in de netwerkstack van grote cloudproviders die een heel deel van het internet urenlang plat kunnen leggen, simpelweg omdat het spraakmakende gebeurtenissen zijn die gevolgen hebben voor een groot aantal mensen en bedrijven. Netwerkstoringen kunnen in veel meer gevallen problemen veroorzaken, ook al staan ​​niet al deze gevallen in de schijnwerpers. Ook klanten van clouddiensten weten niets over de oorzaken van problemen. Als er een storing optreedt, is het bijna onmogelijk om dit toe te schrijven aan een netwerkfout aan de kant van de serviceprovider. Voor hen zijn diensten van derden zwarte dozen. Zonder een grote dienstverlener is het onmogelijk om de impact in te schatten.

Gezien wat de grote spelers over hun systemen rapporteren, kun je met zekerheid zeggen dat je geluk hebt als netwerkproblemen slechts een klein percentage van de potentiële downtimeproblemen vertegenwoordigen. Netwerkcommunicatie heeft nog steeds te lijden onder alledaagse zaken als hardwarestoringen, topologiewijzigingen, wijzigingen in de administratieve configuratie en stroomstoringen. Onlangs was ik verrast toen ik hoorde dat de lijst met mogelijke problemen was toegevoegd haaienbeten (ja, je hoort het goed).

ZUUR betekent veel verschillende dingen

De afkorting ACID staat voor Atomicity, Consistentie, Isolatie, Betrouwbaarheid. Deze eigenschappen van transacties zijn bedoeld om hun geldigheid te garanderen in geval van storingen, fouten, hardwarestoringen, enz. Zonder ACID of vergelijkbare systemen zou het voor applicatieontwikkelaars moeilijk zijn om onderscheid te maken tussen waar zij verantwoordelijk voor zijn en waar de database verantwoordelijk voor is. De meeste relationele transactiedatabases proberen ACID-compatibel te zijn, maar nieuwe benaderingen zoals NoSQL hebben aanleiding gegeven tot veel databases zonder ACID-transacties omdat de implementatie ervan duur is.

Toen ik voor het eerst in de branche kwam, vertelde onze technische leider hoe relevant het ACID-concept was. Eerlijk gezegd wordt ACID eerder als een ruwe beschrijving beschouwd dan als een strikte implementatienorm. Tegenwoordig vind ik het vooral nuttig omdat het een specifieke categorie problemen aan de orde stelt (en een reeks mogelijke oplossingen suggereert).

Niet elk DBMS is ACID-compatibel; Tegelijkertijd begrijpen database-implementaties die ACID ondersteunen de reeks vereisten op een andere manier. Een van de redenen waarom ACID-implementaties fragmentarisch zijn, is te wijten aan de vele afwegingen die moeten worden gemaakt om ACID-vereisten te implementeren. Makers kunnen hun databases presenteren als ACID-compatibel, maar de interpretatie van randgevallen kan dramatisch variëren, evenals het mechanisme voor het afhandelen van "onwaarschijnlijke" gebeurtenissen. Op zijn minst kunnen ontwikkelaars een goed inzicht krijgen in de complexiteit van basisimplementaties, zodat ze een goed inzicht krijgen in hun speciale gedrag en ontwerptrade-offs.

Het debat over de vraag of MongoDB voldoet aan de ACID-vereisten gaat zelfs na de release van versie 4 door. MongoDB wordt al lange tijd niet meer ondersteund loggen, hoewel gegevens standaard niet vaker dan één keer per 60 seconden op schijf worden vastgelegd. Stel je het volgende scenario voor: een applicatie plaatst twee schrijfbewerkingen (w1 en w2). MongoDB slaat w1 met succes op, maar w2 gaat verloren als gevolg van een hardwarefout.

Dit zouden meer ontwikkelaars over databases moeten weten
Diagram dat het scenario illustreert. MongoDB crasht voordat het gegevens naar schijf kan schrijven

Het vastleggen op schijf is een duur proces. Door frequente commits te vermijden, verbeteren ontwikkelaars de opnameprestaties, wat ten koste gaat van de betrouwbaarheid. MongoDB ondersteunt momenteel logboekregistratie, maar vuile schrijfbewerkingen kunnen nog steeds de gegevensintegriteit beïnvloeden, omdat logboeken standaard elke 100 ms worden vastgelegd. Dat wil zeggen dat een soortgelijk scenario nog steeds mogelijk is voor logboeken en de daarin gepresenteerde wijzigingen, hoewel het risico veel lager is.

Elke database heeft zijn eigen consistentie- en isolatiemechanismen

Van de ACID-vereisten kennen consistentie en isolatie het grootste aantal verschillende implementaties, omdat het scala aan afwegingen groter is. Het moet gezegd worden dat consistentie en isolatie vrij dure functies zijn. Ze vereisen coördinatie en vergroten de concurrentie op het gebied van dataconsistentie. De complexiteit van het probleem neemt aanzienlijk toe als het nodig is om de database horizontaal over meerdere datacenters te schalen (vooral als deze zich in verschillende geografische regio's bevinden). Het bereiken van een hoog niveau van consistentie is erg moeilijk, omdat het ook de beschikbaarheid vermindert en de netwerksegmentatie vergroot. Voor een meer algemene uitleg van dit fenomeen raad ik u aan te verwijzen naar CAP-stelling. Het is ook vermeldenswaard dat applicaties met kleine hoeveelheden inconsistenties om kunnen gaan, en dat programmeurs de nuances van het probleem goed genoeg kunnen begrijpen om extra logica in de applicatie te implementeren om met inconsistenties om te gaan, zonder sterk afhankelijk te zijn van de database om deze af te handelen.

DBMS'en bieden vaak verschillende isolatieniveaus. Applicatieontwikkelaars kunnen de meest effectieve kiezen op basis van hun voorkeuren. Lage isolatie zorgt voor hogere snelheid, maar verhoogt ook het risico op een datarace. Een hoge isolatie verkleint deze kans, maar vertraagt ​​het werk en kan leiden tot concurrentie, wat tot zulke remmen in de basis zal leiden dat storingen beginnen.

Dit zouden meer ontwikkelaars over databases moeten weten
Beoordeling van bestaande gelijktijdigheidsmodellen en de relaties daartussen

De SQL-standaard definieert slechts vier isolatieniveaus, hoewel er in theorie en praktijk nog veel meer zijn. Jepson.io biedt een uitstekend overzicht van bestaande concurrency-modellen. Google Spanner garandeert bijvoorbeeld externe serialiseerbaarheid met kloksynchronisatie, en hoewel dit een strengere isolatielaag is, is deze niet gedefinieerd in standaard isolatielagen.

De SQL-standaard noemt de volgende isolatieniveaus:

  • serializable (meest streng en duur): Serialiseerbare uitvoering heeft hetzelfde effect als een aantal opeenvolgende transactie-uitvoeringen. Sequentiële uitvoering betekent dat elke volgende transactie pas begint nadat de vorige is voltooid. Opgemerkt moet worden dat het niveau serializable vaak geïmplementeerd als zogenaamde snapshot-isolatie (bijvoorbeeld in Oracle) vanwege interpretatieverschillen, hoewel snapshot-isolatie zelf niet wordt weergegeven in de SQL-standaard.
  • Herhaalbare lezingen: Niet-vastgelegde records in de huidige transactie zijn beschikbaar voor de huidige transactie, maar wijzigingen die door andere transacties zijn aangebracht (zoals nieuwe rijen) Niet zichtbaar.
  • Lees toegewijd: Niet-vastgelegde gegevens zijn niet beschikbaar voor transacties. In dit geval kunnen transacties alleen vastgelegde gegevens zien en kunnen fantoomlezingen optreden. Als een transactie nieuwe rijen invoegt en vastlegt, kan de huidige transactie deze zien wanneer erom wordt gevraagd.
  • Lezen niet-vastgelegd (minst strikte en dure niveau): Dirty reads zijn toegestaan, transacties kunnen niet-vastgelegde wijzigingen zien die door andere transacties zijn aangebracht. In de praktijk kan dit niveau nuttig zijn voor ruwe schattingen, zoals query's COUNT(*) op de tafel.

Niveau serializable minimaliseert het risico op dataraces, is tegelijkertijd het duurst om te implementeren en resulteert in de hoogste concurrentiedruk op het systeem. Andere isolatieniveaus zijn eenvoudiger te implementeren, maar vergroten de kans op dataraces. Bij sommige DBMS'en kunt u een aangepast isolatieniveau instellen, bij andere zijn er sterke voorkeuren en worden niet alle niveaus ondersteund.

Ondersteuning voor isolatieniveaus wordt vaak geadverteerd in een bepaald DBMS, maar alleen een zorgvuldige studie van het gedrag ervan kan onthullen wat er feitelijk gebeurt.

Dit zouden meer ontwikkelaars over databases moeten weten
Beoordeling van gelijktijdigheidsafwijkingen op verschillende isolatieniveaus voor verschillende DBMS'en

Martin Kleppmann in zijn project kluizenaarschap Vergelijkt verschillende isolatieniveaus, bespreekt afwijkingen in de gelijktijdigheid en of de database zich aan een bepaald isolatieniveau kan houden. Kleppmanns onderzoek laat zien hoe verschillend databaseontwikkelaars denken over isolatieniveaus.

Optimistische blokkering komt te hulp als het moeilijk is om de gebruikelijke te handhaven.

Blokkeren kan erg duur zijn, niet alleen omdat het de concurrentie in de database vergroot, maar ook omdat het vereist dat de applicatieservers voortdurend verbinding maken met de database. Netwerksegmentatie kan exclusieve vergrendelingssituaties verergeren en leiden tot impasses die moeilijk te identificeren en op te lossen zijn. In gevallen waarin exclusieve vergrendeling niet geschikt is, helpt optimistische vergrendeling.

Optimistisch slot is een methode waarbij bij het lezen van een string rekening wordt gehouden met de versie, de controlesom of het tijdstip van de laatste wijziging. Hierdoor kunt u ervoor zorgen dat er geen atomaire versiewijziging plaatsvindt voordat u een invoer wijzigt:

UPDATE products
SET name = 'Telegraph receiver', version = 2
WHERE id = 1 AND version = 1

In dit geval wordt de tabel bijgewerkt products wordt niet uitgevoerd als een andere bewerking eerder wijzigingen in deze rij heeft aangebracht. Als er geen andere bewerkingen op deze rij zijn uitgevoerd, zal de wijziging voor één rij plaatsvinden en kunnen we zeggen dat de update succesvol was.

Naast vuile leesbewerkingen en gegevensverlies zijn er nog andere afwijkingen

Als het gaat om gegevensconsistentie, ligt de nadruk op de mogelijkheid van raceomstandigheden die kunnen leiden tot vuile leesbewerkingen en gegevensverlies. Gegevensafwijkingen houden echter niet op.

Een voorbeeld van dergelijke afwijkingen is de opnamevervorming (schrijf scheef). Vertekeningen zijn lastig op te sporen, omdat er doorgaans niet actief naar wordt gezocht. Ze zijn niet te wijten aan vuile leesbewerkingen of gegevensverlies, maar aan schendingen van logische beperkingen die aan de gegevens zijn gesteld.

Laten we bijvoorbeeld een monitoringtoepassing overwegen waarbij één operator altijd bereikbaar moet zijn:

BEGIN tx1;                      BEGIN tx2;
SELECT COUNT(*)
FROM operators
WHERE oncall = true;
0                               SELECT COUNT(*)
                                FROM operators
                                WHERE oncall = TRUE;
                                0
UPDATE operators                UPDATE operators
SET oncall = TRUE               SET oncall = TRUE
WHERE userId = 4;               WHERE userId = 2;
COMMIT tx1;                     COMMIT tx2;

In de bovenstaande situatie zal er sprake zijn van recordcorruptie als beide transacties met succes zijn uitgevoerd. Hoewel er geen sprake was van vuile leesbewerkingen of gegevensverlies, kwam de integriteit van de gegevens in gevaar: nu worden twee mensen tegelijkertijd als oproepbaar beschouwd.

Serialiseerbare isolatie, schemaontwerp of databasebeperkingen kunnen schrijfcorruptie helpen elimineren. Ontwikkelaars moeten dergelijke afwijkingen tijdens de ontwikkeling kunnen identificeren om ze tijdens de productie te voorkomen. Tegelijkertijd zijn opnamevervormingen uiterst moeilijk te vinden in de codebasis. Vooral in grote systemen, wanneer verschillende ontwikkelingsteams verantwoordelijk zijn voor het implementeren van functies op basis van dezelfde tabellen en het niet eens zijn over de details van de gegevenstoegang.

De database en de gebruiker zijn het niet altijd eens over wat ze moeten doen

Een van de belangrijkste kenmerken van databases is de garantie van een uitvoeringsorder, maar deze order zelf is mogelijk niet transparant voor de softwareontwikkelaar. Databases voeren transacties uit in de volgorde waarin ze worden ontvangen, niet in de volgorde die programmeurs bedoelen. De volgorde van transacties is moeilijk te voorspellen, vooral in zwaarbelaste parallelle systemen.

Tijdens de ontwikkeling, vooral bij het werken met niet-blokkerende bibliotheken, kunnen een slechte stijl en slechte leesbaarheid ertoe leiden dat gebruikers geloven dat transacties opeenvolgend worden uitgevoerd, terwijl ze in feite in willekeurige volgorde in de database kunnen aankomen.

Op het eerste gezicht worden in het onderstaande programma T1 en T2 opeenvolgend aangeroepen, maar als deze functies niet-blokkerend zijn en het resultaat onmiddellijk in de vorm retourneren belofte, dan wordt de volgorde van de oproepen bepaald door de momenten waarop ze de database binnenkwamen:

result1 = T1() // echte resultaten zijn beloftes
resultaat2 = T2()

Als atomiciteit vereist is (dat wil zeggen: alle operaties moeten worden voltooid of afgebroken) en de volgorde is van belang, dan moeten operaties T1 en T2 binnen één transactie worden uitgevoerd.

Sharding op toepassingsniveau kan buiten de toepassing worden verplaatst

Sharding is een methode om een ​​database horizontaal te partitioneren. Sommige databases kunnen gegevens automatisch horizontaal splitsen, terwijl andere dat niet kunnen of er niet zo goed in zijn. Wanneer data-architecten/-ontwikkelaars precies kunnen voorspellen hoe gegevens zullen worden benaderd, kunnen ze horizontale partities in de gebruikersruimte creëren in plaats van dit werk aan de database te delegeren. Dit proces wordt 'sharding op toepassingsniveau' genoemd (sharding op toepassingsniveau).

Helaas zorgt deze naam vaak voor de misvatting dat sharding voorkomt in applicatieservices. Het kan zelfs als een aparte laag vóór de database worden geïmplementeerd. Afhankelijk van de gegevensgroei en schema-iteraties kunnen shardingvereisten behoorlijk complex worden. Sommige strategieën kunnen baat hebben bij de mogelijkheid om te herhalen zonder dat applicatieservers opnieuw hoeven te worden geïmplementeerd.

Dit zouden meer ontwikkelaars over databases moeten weten
Een voorbeeld van een architectuur waarin applicatieservers gescheiden zijn van de shardingservice

Door sharding naar een aparte service te verplaatsen, wordt de mogelijkheid uitgebreid om verschillende shardingstrategieën te gebruiken zonder dat applicaties opnieuw hoeven te worden geïmplementeerd. snelheden is een voorbeeld van een dergelijk shardingsysteem op applicatieniveau. Vitess biedt horizontale sharding voor MySQL en stelt klanten in staat er verbinding mee te maken via het MySQL-protocol. Het systeem segmenteert de gegevens in verschillende MySQL-knooppunten die niets van elkaar weten.

Automatisch verhogen kan gevaarlijk zijn

AUTOINCREMENT is een gebruikelijke manier om primaire sleutels te genereren. Er zijn vaak gevallen waarin databases worden gebruikt als ID-generatoren, en de database bevat tabellen die zijn ontworpen om identificatiegegevens te genereren. Er zijn verschillende redenen waarom het genereren van primaire sleutels met behulp van automatische ophoging verre van ideaal is:

  • In een gedistribueerde database is automatisch ophogen een serieus probleem. Om de ID te genereren is een globale vergrendeling vereist. In plaats daarvan kunt u een UUID genereren: hiervoor is geen interactie tussen verschillende databaseknooppunten vereist. Automatisch verhogen met vergrendelingen kan tot conflicten leiden en de prestaties op wisselplaten in gedistribueerde situaties aanzienlijk verminderen. Sommige DBMS'en (bijvoorbeeld MySQL) vereisen mogelijk een speciale configuratie en meer zorgvuldige aandacht om de master-master-replicatie goed te organiseren. En het is gemakkelijk om fouten te maken bij het configureren, wat tot opnamefouten zal leiden.
  • Sommige databases hebben partitie-algoritmen op basis van primaire sleutels. Opeenvolgende ID's kunnen leiden tot onvoorspelbare hotspots en verhoogde belasting op sommige partities, terwijl andere inactief blijven.
  • Een primaire sleutel is de snelste manier om toegang te krijgen tot een rij in een database. Met betere manieren om records te identificeren, kunnen sequentiële ID's de belangrijkste kolom in tabellen veranderen in een nutteloze kolom gevuld met betekenisloze waarden. Kies daarom, waar mogelijk, een globaal unieke en natuurlijke primaire sleutel (bijvoorbeeld gebruikersnaam).

Voordat u een aanpak kiest, moet u rekening houden met de impact van het automatisch verhogen van ID's en UUID's op het indexeren, partitioneren en sharden.

Verouderde gegevens kunnen nuttig zijn en vereisen geen vergrendeling

Multiversion Concurrency Control (MVCC) implementeert veel van de consistentievereisten die hierboven kort zijn besproken. Sommige databases (bijvoorbeeld Postgres, Spanner) gebruiken MVCC om transacties te ‘voeden’ met snapshots – oudere versies van de database. Snapshot-transacties kunnen ook worden geserialiseerd om consistentie te garanderen. Bij het lezen van een oude momentopname worden verouderde gegevens gelezen.

Het lezen van enigszins verouderde gegevens kan bijvoorbeeld handig zijn bij het genereren van analyses op basis van de gegevens of het berekenen van geschatte geaggregeerde waarden.

Het eerste voordeel van het werken met oudere gegevens is de lage latentie (vooral als de database over verschillende regio’s is verspreid). De tweede is dat alleen-lezen-transacties zonder vergrendeling zijn. Dit is een aanzienlijk voordeel voor applicaties die veel lezen, zolang ze maar overweg kunnen met verouderde gegevens.

Dit zouden meer ontwikkelaars over databases moeten weten
De applicatieserver leest gegevens van de lokale replica die 5 seconden verouderd zijn, zelfs als de nieuwste versie beschikbaar is aan de andere kant van de Stille Oceaan

DBMS'en verwijderen automatisch oudere versies en bieden u in sommige gevallen de mogelijkheid dit op verzoek te doen. Met Postgres kunnen gebruikers dit bijvoorbeeld doen VACUUM op verzoek, en voert deze handeling ook periodiek automatisch uit. Spanner heeft een garbage collector om snapshots ouder dan een uur te verwijderen.

Bronnen zijn altijd onderhevig aan vervorming

Het best bewaarde geheim in de computerwetenschap is dat alle timing-API’s liegen. Onze machines kennen zelfs niet de exacte huidige tijd. Computers bevatten kwartskristallen die trillingen genereren die worden gebruikt om de tijd bij te houden. Ze zijn echter niet nauwkeurig genoeg en kunnen voor/achterlopen op de exacte tijd. De dienst kan 20 seconden per dag bedragen. Daarom moet de tijd op onze computers periodiek worden gesynchroniseerd met die op het netwerk.

NTP-servers worden gebruikt voor synchronisatie, maar het synchronisatieproces zelf is onderhevig aan netwerkvertragingen. Zelfs het synchroniseren met een NTP-server in hetzelfde datacenter kost enige tijd. Het is duidelijk dat het werken met een publieke NTP-server tot nog grotere vervorming kan leiden.

Atoomklokken en hun GPS-tegenhangers zijn beter voor het bepalen van de huidige tijd, maar ze zijn duur en vereisen een complexe installatie, zodat ze niet op elke auto kunnen worden geïnstalleerd. Daarom gebruiken datacenters een gelaagde aanpak. Atoom- en/of GPS-klokken geven de exacte tijd weer, waarna deze via secundaire servers naar andere machines wordt uitgezonden. Dit betekent dat elke machine een bepaalde afwijking ten opzichte van de exacte tijd ervaart.

De situatie wordt verergerd door het feit dat applicaties en databases zich vaak op verschillende machines bevinden (zo niet in verschillende datacentra). De tijd zal dus niet alleen verschillen op DB-knooppunten die over verschillende machines zijn verdeeld. Op de applicatieserver zal het ook anders zijn.

Google TrueTime hanteert een geheel andere benadering. De meeste mensen geloven dat de vooruitgang van Google in deze richting wordt verklaard door de banale overgang naar atoom- en GPS-klokken, maar dit is slechts een deel van het grote geheel. Zo werkt TrueTime:

  • TrueTime gebruikt twee verschillende bronnen: GPS en atoomklokken. Deze klokken hebben niet-gecorreleerde faalmodi. [zie pagina 5 voor details hier — ca. vert.), zodat het gezamenlijke gebruik ervan de betrouwbaarheid vergroot.
  • TrueTime heeft een ongebruikelijke API. Het retourneert de tijd als een interval waarin meetfouten en onzekerheid zijn ingebouwd. Het werkelijke tijdstip ligt ergens tussen de boven- en ondergrenzen van het interval. Spanner, de gedistribueerde database van Google, wacht eenvoudigweg totdat het veilig is om te zeggen dat de huidige tijd buiten bereik is. Deze methode introduceert enige latentie in het systeem, vooral als de onzekerheid op de masters groot is, maar garandeert juistheid, zelfs in een wereldwijd verspreide situatie.

Dit zouden meer ontwikkelaars over databases moeten weten
De Spanner-componenten gebruiken TrueTime, waarbij TT.now() een interval retourneert, zodat de Spanner eenvoudigweg slaapt tot het punt waarop hij er zeker van kan zijn dat de huidige tijd een bepaald punt is gepasseerd

Verminderde nauwkeurigheid bij het bepalen van de huidige tijd betekent een toename van de duur van Spanner-bewerkingen en een afname van de prestaties. Daarom is het belangrijk om de hoogst mogelijke nauwkeurigheid te behouden, ook al is het onmogelijk om een ​​volledig nauwkeurig horloge te verkrijgen.

Vertraging heeft vele betekenissen

Als je aan een tiental experts vraagt ​​wat vertraging is, krijg je waarschijnlijk verschillende antwoorden. In DBMS wordt latentie vaak "databaselatentie" genoemd en verschilt van wat de klant ervaart. Feit is dat de client de som van de netwerkvertraging en de databasevertraging waarneemt. De mogelijkheid om het type latentie te isoleren is van cruciaal belang bij het opsporen van groeiende problemen. Probeer bij het verzamelen en weergeven van statistieken altijd beide typen in de gaten te houden.

Prestatie-eisen moeten worden geëvalueerd voor een specifieke transactie

Soms worden de prestatiekenmerken van een DBMS en de beperkingen ervan gespecificeerd in termen van schrijf-/leesdoorvoer en latentie. Dit biedt een algemeen overzicht van de belangrijkste systeemparameters, maar bij het evalueren van de prestaties van een nieuw DBMS is een veel omvattendere aanpak het afzonderlijk evalueren van kritieke bewerkingen (voor elke query en/of transactie). Voorbeelden:

  • Schrijfdoorvoer en latentie bij het invoegen van een nieuwe rij in tabel X (met 50 miljoen rijen) met gespecificeerde beperkingen en rijopvulling in gerelateerde tabellen.
  • Vertraging bij het weergeven van vrienden van vrienden van een bepaalde gebruiker wanneer het gemiddelde aantal vrienden 500 is.
  • Latentie bij het ophalen van de top 100 vermeldingen uit de geschiedenis van een gebruiker wanneer de gebruiker 500 andere gebruikers volgt met X vermeldingen per uur.

Evaluatie en experimenten kunnen dergelijke kritieke gevallen omvatten totdat u er zeker van bent dat de database aan de prestatievereisten voldoet. Een soortgelijke vuistregel houdt ook rekening met deze uitsplitsing bij het verzamelen van latentiestatistieken en het bepalen van SLO's.

Houd rekening met hoge kardinaliteit bij het verzamelen van statistieken voor elke bewerking. Gebruik logboeken, gebeurtenisverzameling of gedistribueerde tracering om krachtige foutopsporingsgegevens te verkrijgen. In het artikel "Wilt u de latentie debuggen?» je kunt jezelf vertrouwd maken met methodologieën voor het debuggen van vertragingen.

Geneste transacties kunnen gevaarlijk zijn

Niet elk DBMS ondersteunt geneste transacties, maar als dat wel het geval is, kunnen dergelijke transacties resulteren in onverwachte fouten die niet altijd gemakkelijk te detecteren zijn (dat wil zeggen dat het duidelijk moet zijn dat er sprake is van een bepaalde anomalie).

U kunt het gebruik van geneste transacties vermijden door clientbibliotheken te gebruiken die deze kunnen detecteren en omzeilen. Als geneste transacties niet kunnen worden afgebroken, wees dan extra voorzichtig bij de implementatie ervan om onverwachte situaties te voorkomen waarin voltooide transacties per ongeluk worden afgebroken vanwege geneste transacties.

Het inkapselen van transacties in verschillende lagen kan leiden tot onverwachte geneste transacties, en vanuit het oogpunt van codeleesbaarheid kan het moeilijk worden de bedoelingen van de auteur te begrijpen. Kijk eens naar het volgende programma:

with newTransaction():
   Accounts.create("609-543-222")
   with newTransaction():
       Accounts.create("775-988-322")
       throw Rollback();

Wat zal de output zijn van bovenstaande code? Zal het beide transacties terugdraaien, of alleen de binnenste? Wat gebeurt er als we vertrouwen op meerdere lagen van bibliotheken die het creëren van transacties voor ons inkapselen? Zullen we dergelijke gevallen kunnen identificeren en verbeteren?

Stel je een datalaag voor met meerdere bewerkingen (bijv. newAccount) is al geïmplementeerd in zijn eigen transacties. Wat gebeurt er als je ze uitvoert als onderdeel van bedrijfslogica op een hoger niveau die binnen zijn eigen transactie draait? Wat zou in dit geval de isolatie en consistentie zijn?

function newAccount(id string) {
  with newTransaction():
      Accounts.create(id)
}

In plaats van te zoeken naar antwoorden op zulke eindeloze vragen, is het beter om geneste transacties te vermijden. Uw datalaag kan immers eenvoudig bewerkingen op hoog niveau uitvoeren zonder eigen transacties te creëren. Bovendien is de bedrijfslogica zelf in staat een transactie te initiëren, er bewerkingen op uit te voeren, een transactie te plegen of af te breken.

function newAccount(id string) {
   Accounts.create(id)
}
// In main application:
with newTransaction():
   // Read some data from database for configuration.
   // Generate an ID from the ID service.
   Accounts.create(id)
   Uploads.create(id) // create upload queue for the user.

Transacties mogen niet aan de applicatiestatus worden gekoppeld

Soms is het verleidelijk om de applicatiestatus in transacties te gebruiken om bepaalde waarden te wijzigen of queryparameters aan te passen. De kritische nuance waarmee rekening moet worden gehouden, is het juiste toepassingsgebied. Clients herstarten transacties vaak wanneer er netwerkproblemen zijn. Als de transactie vervolgens afhankelijk is van een toestand die door een ander proces wordt gewijzigd, kan deze de verkeerde waarde kiezen, afhankelijk van de mogelijkheid van een datarace. Bij transacties moet rekening worden gehouden met het risico op datarace-omstandigheden in de applicatie.

var seq int64
with newTransaction():
    newSeq := atomic.Increment(&seq)
    Entries.query(newSeq)
    // Other operations...

De bovenstaande transactie verhoogt het volgnummer elke keer dat deze wordt uitgevoerd, ongeacht het eindresultaat. Als de commit mislukt vanwege netwerkproblemen, wordt de aanvraag uitgevoerd met een ander volgnummer wanneer u het opnieuw probeert.

Queryplanners kunnen u veel vertellen over een database

Queryplanners bepalen hoe een query in een database wordt uitgevoerd. Ze analyseren ook verzoeken en optimaliseren deze voordat ze worden verzonden. Planners kunnen slechts enkele mogelijke schattingen geven op basis van de signalen waarover zij beschikken. Wat is bijvoorbeeld de beste zoekmethode voor de volgende zoekopdracht?

SELECT * FROM articles where author = "rakyll" order by title;

De resultaten kunnen op twee manieren worden opgevraagd:

  • Volledige tabelscan: U kunt elke invoer in de tabel bekijken, artikelen met een overeenkomende auteursnaam retourneren en deze vervolgens bestellen.
  • Indexscan: u kunt een index gebruiken om overeenkomende ID's te vinden, die rijen op te halen en ze vervolgens te ordenen.

De taak van de queryplanner is om te bepalen welke strategie de beste is. Het is de moeite waard om te bedenken dat queryplanners slechts beperkte voorspellende mogelijkheden hebben. Dit kan tot slechte beslissingen leiden. DBA's of ontwikkelaars kunnen ze gebruiken om slecht presterende zoekopdrachten te diagnosticeren en te verfijnen. Nieuwe versies van het DBMS kunnen queryplanners configureren, en zelfdiagnose kan helpen bij het bijwerken van de database als de nieuwe versie tot prestatieproblemen leidt. Langzame querylogboeken, rapporten over latentieproblemen of statistieken over de uitvoeringstijd kunnen helpen bij het identificeren van query's die moeten worden geoptimaliseerd.

Sommige statistieken die door de queryplanner worden gepresenteerd, kunnen onderhevig zijn aan ruis (vooral bij het schatten van de latentie of CPU-tijd). Een goede aanvulling op planners zijn tools voor het traceren en volgen van het uitvoeringspad. Ze stellen u in staat dergelijke problemen te diagnosticeren (helaas bieden niet alle DBMS'en dergelijke hulpmiddelen).

Onlinemigratie is moeilijk maar mogelijk

Onlinemigratie, livemigratie of realtimemigratie betekent verhuizen van de ene database naar de andere zonder downtime of gegevenscorruptie. Livemigratie is eenvoudiger uit te voeren als de transitie binnen hetzelfde DBMS/engine plaatsvindt. De situatie wordt ingewikkelder wanneer het nodig is om over te stappen naar een nieuw DBMS met andere prestatie- en schemavereisten.

Er zijn verschillende online migratiemodellen. Hier is er een van:

  • Schakel dubbele invoer in beide databases in. De nieuwe database beschikt in dit stadium nog niet over alle gegevens, maar accepteert alleen de meest recente gegevens. Zodra u dit zeker weet, kunt u doorgaan naar de volgende stap.
  • Schakel lezen uit beide databases in.
  • Configureer het systeem zo dat lezen en schrijven voornamelijk op de nieuwe database wordt uitgevoerd.
  • Stop met schrijven naar de oude database terwijl u doorgaat met het lezen van gegevens daaruit. In dit stadium bevat de nieuwe database nog steeds geen enkele gegevens. Ze moeten worden gekopieerd uit de oude database.
  • De oude database is alleen-lezen. Kopieer de ontbrekende gegevens van de oude database naar de nieuwe. Nadat de migratie is voltooid, wijzigt u de paden naar de nieuwe database, stopt u de oude en verwijdert u deze van het systeem.

Voor meer informatie raad ik aan contact op te nemen статье, waarin de migratiestrategie van Stripe op basis van dit model wordt beschreven.

Een aanzienlijke uitbreiding van de database brengt een toename van de onvoorspelbaarheid met zich mee

De groei van de database leidt tot onvoorspelbare problemen die verband houden met de omvang ervan. Hoe meer we weten over de interne structuur van een database, hoe beter we kunnen voorspellen hoe deze zal schalen. Sommige momenten zijn echter nog steeds niet te voorzien.
Naarmate de basis groeit, kunnen eerdere aannames en verwachtingen met betrekking tot datavolume en netwerkbandbreedtevereisten achterhaald raken. Dit is het moment waarop de vraag rijst van grote ontwerprevisies, grootschalige operationele verbeteringen, het heroverwegen van implementaties of migratie naar andere DBMS'en om potentiële problemen te voorkomen.

Maar denk niet dat uitstekende kennis van de interne structuur van de bestaande database het enige is dat nodig is. Nieuwe schalen zullen nieuwe onbekenden met zich meebrengen. Onvoorspelbare pijnpunten, ongelijkmatige datadistributie, onverwachte bandbreedte- en hardwareproblemen, steeds toenemend verkeer en nieuwe netwerksegmenten zullen u dwingen uw databasebenadering, datamodel, implementatiemodel en databasegrootte te heroverwegen.

...

Toen ik begon na te denken over het publiceren van dit artikel, stonden er al vijf extra items op mijn oorspronkelijke lijst. Toen kwam er een groot aantal nieuwe ideeën over wat er nog meer gedekt kan worden. Daarom gaat het artikel in op de minst voor de hand liggende problemen die maximale aandacht vereisen. Dit betekent echter niet dat het onderwerp is uitgeput en dat ik er in mijn toekomstige materialen niet meer op zal terugkomen en geen wijzigingen zal aanbrengen in het huidige.

PS

Lees ook op onze blog:

Bron: www.habr.com

Voeg een reactie