Mönster för horisontell partitionering

Dela upp ett datalager i en uppsättning horisontella partitioner eller delar. Detta kan förbättra skalbarheten vid lagring och hämtning av stora datavolymer.

Kontext och problem

Ett datalager med en enda server som värd kan uppleva följande begränsningar:

  • Lagringsutrymme. Ett datalager för ett storskaligt molnprogram förväntas kunna innehålla en enorm datavolym som kan öka avsevärt med tiden. En server tillhandahåller normalt endast en begränsad mängd disklagring, men du kan byta ut befintliga diskar mot större eller lägga till ytterligare diskar i datorn vartefter datavolymerna växer. Systemet når dock till slut en gräns där det inte går att öka lagringskapaciteten på en viss server på ett enkelt sätt.

  • Bearbetningsresurser. Ett molnprogram krävs för att ge stöd åt ett stort antal samtidiga användare, där var och en kan köra frågor som hämtar information från datalagret. En enda server som är värd för datalagret kanske inte kan tillhandahålla den beräkningskraft som krävs för att stödja den här belastningen, vilket resulterar i längre svarstider för användare och ofta förekommande fel när program som försöker lagra och hämta data tar slut. Det kan vara möjligt att lägga till minne eller uppgradera processorer, men systemet når en gräns när det inte går att öka beräkningsresurserna ytterligare.

  • Nätverksbandbredd. Slutligen styrs prestanda för ett datalager som körs på en enda server av den hastighet med vilken servern kan ta emot begäranden och skicka svar. Det kan hända att nätverkstrafikens volym överskrider kapaciteten hos det nätverk som används för att ansluta till servern, vilket leder till misslyckade begäranden.

  • Geografi. Det kan vara nödvändigt att lagra data som genereras av specifika användare i samma region som de användarna av juridik-, efterlevnads- eller prestandaskäl, eller för att sänka svarstiden för dataåtkomst. Om användarna är utspridda över olika länder eller regioner kan det hända att det inte går att lagra hela datavolymen för programmet i ett enda datalager.

Vertikal skalning genom att lägga till mer diskkapacitet, bearbetningskraft, minne och nätverksanslutningar kan skjuta upp effekterna av några av dessa begränsningar, men det är troligtvis endast en tillfällig lösning. Ett kommersiellt molnprogram som kan ge stöd för ett stort antal användare och höga datavolymer måste kunna skalas nästan oändligt, så vertikal skalning är inte nödvändigtvis den bästa lösningen.

Lösning

Dela upp datalagret i horisontella partitioner eller shards. Varje shard har samma schema, men det innehåller en avskild delmängd av data. En shard är ett datalager på egen hand (den kan innehålla data för många entiteter av olika typer) och körs på en server som fungerar som en lagringsnod.

Det här mönstret har följande fördelar:

  • Du kan skala ut systemet genom att lägga till fler shards som körs på ytterligare lagringsnoder.

  • Ett system kan använda standardmaskinvara i stället för särskilda och dyra datorer för varje lagringsnod.

  • Du kan minska konkurrensen och förbättra prestanda genom att balansera arbetsbelastningen över shards.

  • I molnet kan shards ligga fysiskt nära de användare som ska komma åt data.

När du delar upp ett datalager i shards måste du avgöra vilka data som ska placeras i varje shard. En shard innehåller normalt objekt som ligger inom ett visst omfång som avgörs av ett eller flera attribut i data. Dessa attribut utgör shardnyckeln (kallas ibland partitionsnyckeln). Shardnyckeln ska vara statisk. Den får inte vara baserad på data som kan ändras.

Horisontell partitionering organiserar data fysiskt. När ett program lagrar och hämtar data dirigerar den horisontella partitioneringen programmet till lämplig shard. Den här logiken för horisontell partitionering kan implementeras som en del av dataåtkomstkoden i programmet, eller så kan den implementeras av datalagringssystemet om det har transparent stöd för horisontell partitionering.

Genom att abstrahera den fysiska platsen för data i logiken för horisontell partitionering går det att uppnå en hög styrningsnivå över vilka shards som innehåller vilka data. Det gör det också möjligt att migrera data mellan shards utan att göra om affärslogiken i ett program om data i shards behöver distribueras om senare (till exempel om shards blir obalanserade). Nackdelen är den ytterligare prestanda för dataåtkomst som krävs när platsen för varje dataobjekt fastställs när det hämtas.

Det är viktigt att dela upp data på ett sätt som är lämpligt för de typer av frågor som programmet kör om man vill uppnå bästa prestanda och skalbarhet. I många fall är det osannolikt att schemat för horisontell partitionering exakt överensstämmer med kraven för varje fråga. I ett system med flera innehavare kan ett program till exempel behöva hämta klientdata med hjälp av klientorganisations-ID:t, men det kan också behöva leta upp dessa data baserat på något annat attribut, till exempel klientens namn eller plats. För att hantera dessa strategier kan man implementera en strategi för horisontell partitionering med en shardnyckel som har stöd för de frågor som körs oftast.

Om frågorna ofta hämtar data med en kombination av attributvärden kan du antagligen definiera en shardnyckel genom att länka samman attribut. Du kan även använda ett mönster som Indextabell för att ge snabb sökning av data baserat på attribut som inte täcks av shardnyckeln.

Strategier för horisontell partitionering

Tre strategier används ofta vid val av shardnyckel och hur data ska distribueras över shards. Observera att det inte behöver finnas ett en-till-en-samband mellan shards och de servrar som är värdar för dem – en enda server kan vara värd för flera shards. Strategierna är:

Sökningsstrategin. I den här strategin implementerar logiken för horisontell partitionering en karta som dirigerar en databegäran till den shard som innehåller dessa data med hjälp av shardnyckeln. I ett program med flera innehavare kan alla data för en klient lagras tillsammans i en shard med klient-ID som shardnyckel. Flera klienter kan dela samma shard, men data för en enda klient sprids inte över flera shards. Bilden visar horisontell partitionering av klientdata baserat på klient-ID:n.

Bild 1 – Horisontell partitionering av klientdata baserat på klient-ID:n

Mappningen mellan shardnyckeln och den fysiska lagringen kan baseras på fysiska shards där varje shardnyckel mappar till en fysisk partition. En mer flexibel teknik för ombalansering av shards är virtuell partitionering, där shardnycklar mappar till samma antal virtuella shards, som i sin tur mappar till färre fysiska partitioner. Med den här metoden hittar ett program data med hjälp av en shardnyckel som hänvisar till en virtuell shard, och systemet mappar virtuella shards transparent till fysiska partitioner. Mappningen mellan en virtuell shard och en fysisk partition kan ändras utan att programkoden måste förändras så att den använder en annan uppsättning shardnycklar.

Omfångsstrategin. Den här strategin grupperar relaterade objekt i samma shard och beställer dem efter shardnyckel – shardnycklarna är sekventiella. Det här är praktiskt vid program som ofta hämtar objektuppsättningar med hjälp av omfångsfrågor (frågor som returnerar en uppsättning med dataobjekt för en shardnyckel som ligger inom ett visst omfång). Till exempel om ett program regelbundet behöver hitta alla beställningar som har gjorts under en viss månad går det att hämta dessa data snabbare om alla beställningar för en månad lagras i datum- och tidsordning i samma shard. Om varje beställning hade lagrats i olika shards hade det varit nödvändigt att hämta dem enskilt genom att utföra ett stort antal punktfrågor (frågor som returnerar ett enda dataobjekt). Nästa bild visar lagring av sekventiella uppsättningar (omfång) med data i en shard.

Bild 2 – Lagring av sekventiella uppsättningar (omfång) med data i en shard

I det här exemplet är shardnyckeln en sammansatt nyckel som innehåller beställningsmånaden som det viktigaste elementet, följt av dag och tid för beställningen. Data för beställningar sorteras naturligt när nya beställningar skapas och läggs till i en shard. Vissa datalager har stöd för tvådelade shardnycklar som innehåller ett partitionsnyckelelement som identifierar sharden och en radnyckel som identifierar ett objekt i sharden unikt. Data lagras vanligtvis i radnyckelordning i sharden. Objekt som omfattas av omfångsfrågor och behöver grupperas tillsammans kan använda en shardnyckel som har samma värde för partitionsnyckeln men ett unikt värde för radnyckeln.

Hash-strategin. Syftet med den här strategin är att minska risken för hotspots (shards som tar emot oproportionerligt hög belastning). Den fördelar data över shards på ett sätt som uppnår en balans mellan storleken på varje shard och den genomsnittliga belastning som varje shard möter. Logiken för horisontell partitionering beräknar den shard som ett objekt ska lagras i baserat på en hash för ett eller flera attribut i data. Den valda hash-funktionen ska fördela data jämnt över shards, eventuellt genom att införa ett slumpmässigt element i beräkningen. Nästa bild visar horisontell partitionering av klientdata baserat på hash för klient-ID:n.

Bild 3 – Horisontell partitionering av klientdata baserat på hash för klient-ID:n

För att förstå fördelen med hash-strategin jämfört med andra strategier för horisontell partitionering kan man se på hur ett program med flera innehavare som registrerar nya klienter sekventiellt kan tilldela klienterna till shards i datalagret. Om man använder omfångsstrategin kommer data för alla klienterna 1 till n att lagras i shard A, data för alla klienterna n+1 till m kommer att lagras i shard B osv. Om de senast registrerade klienterna också är de mest aktiva kommer den mesta dataaktiviteten att inträffa i ett litet antal shards, vilket kan leda till hotspots. Hash-strategin tilldelar i stället klienter till shards baserat på en hash av deras klient-ID. Det innebär att sekventiella klienter mest sannolikt tilldelas till olika shards, vilket fördelar belastningen mellan dem. Föregående bild visar detta för klienterna 55 och 56.

De tre strategierna för horisontell partitionering har följande fördelar och överväganden:

  • Slå upp. Det här ger mer kontroll över hur shards konfigureras och används. Med hjälp av virtuella shards minskas effekten vid ombalansering av data, eftersom de nya fysiska partitionerna kan läggas till för att jämna ut arbetsbelastningen. Mappningen mellan en virtuell shard och de fysiska partitioner som implementerar denna shard kan ändras utan att påverka programkod som använder en shardnyckel för att lagra och hämta data. Att leta upp platser för shards kan kräva ytterligare prestanda.

  • Intervall. Det här är enkelt att implementera och fungerar bra med omfångsfrågor, eftersom de ofta kan hämta flera dataobjekt från en enda shard i en enda åtgärd. Den här strategin ger enklare datahantering. Om exempelvis användare i samma region finns i samma shard kan uppdateringar schemaläggas i varje tidszon baserat på lokal belastning och begäransmönster. Den här strategin ger dock inte optimal balansering mellan shards. Det är svårt att ombalansera shards och det kanske inte löser problemet med ojämn belastning om den mesta aktiviteten gäller intilliggande shardnycklar.

  • Hash . Den här strategin ger större möjlighet till jämnare data- och belastningsfördelning. Routning av begäran kan åstadkommas direkt med hjälp av hash-funktionen. Det är inte nödvändigt att upprätthålla en karta. Tänk på att beräkning av hash kan kräva ytterligare prestanda. Det är även svårt att ombalansera shards.

De vanligaste systemen för horisontell partitionering implementerar en av de metoder som beskrivs ovan, men du bör även överväga programmens krav och deras mönster för dataanvändning. Till exempel i ett program med flera innehavare:

  • Du kan fragmentera data baserat på arbetsbelastning. Du kan särskilja data för ej beständiga klienter i separata shards. Dataåtkomsthastigheten för andra klienter kan därigenom förbättras.

  • Du kan fragmentera data baserat på klienternas plats. Du kan ta data för klienter i en viss geografisk region och utföra säkerhetskopiering och underhåll under perioder med låg belastning i den regionen, medan data för klienter i andra regioner fortfarande är online och tillgängliga under deras arbetstid.

  • Värdefulla klienter kan tilldelas privata shards med hög prestanda och låg belastning, medan mindre värdefulla klienter kanske behöver dela shards med högre belastning.

  • Data för klienter som behöver en hög grad av dataisolering och sekretess kan lagras på en helt separat server.

Åtgärder för skalning och dataförflyttning

Var och en av strategierna för horisontell partitionering innebär olika funktioner och komplexitetsnivåer för att hantera skalning in och ut, dataförflyttning och upprätthållande av tillstånd.

Sökningsstrategin tillåter att åtgärder för skalning och dataförflyttning utförs på användarnivå, antingen online eller offline. Tekniken innebär att pausa en del eller all användaraktivitet (kanske under perioder med låg belastning), flytta data till den nya virtuella partitionen eller fysiska sharden, ändra mappningarna, ogiltigförklara eller uppdatera eventuella cacheminnen som innehåller dessa data, och sedan tillåta att användaraktiviteten återupptas. Den här typen av åtgärd kan hanteras centralt. Sökningsstrategin kräver att tillståndet är cachelagringsbart i hög utsträckning och replikvänligt.

Omfångsstrategin innebär vissa begränsningar av åtgärder för skalning och dataförflyttning, som normalt måste utföras när en del av eller hela datalagret är offline, eftersom data måste delas och slås samman över shards. Flyttning av data för att balansera om shards kanske inte löser problemet med ojämn belastning om den mesta aktiviteten gäller intilliggande shardnycklar eller dataidentifierare som ligger inom samma intervall. Omfångsstrategin kan också kräva att ett visst tillstånd bibehålls för att mappa omfång till de fysiska partitionerna.

Hash-strategin gör åtgärder för skalning och dataförflyttning mer komplexa eftersom partitionsnycklarna är hashvärden av shardnycklarna eller dataidentifierarna. Den nya platsen för varje shard måste avgöras av hash-funktionen, eller funktionen som har ändrats för att tillhandahålla rätt mappningar. Hash-strategin kräver dock inte underhåll av tillstånd.

Problem och överväganden

Tänk på följande när du bestämmer hur du ska implementera mönstret:

  • Horisontell partitionering kompletterar andra former av partitionering, t.ex. vertikal partitionering och funktionell partitionering. En enda shard kan exempelvis innehålla entiteter som har partitionerats vertikalt, och en funktionell partition kan implementeras som flera shards. Mer information om partitionering finns i Vägledning om datapartitionering.

  • Se till att shards är balanserade så att de hanterar en liknande I/O-volym. När data infogas och raderas är det nödvändigt att regelbundet balansera om shards för att garantera jämn fördelning och minska risken för hotspots. Ombalansering kan vara en kostsam åtgärd. Minska behovet av ombalansering genom att planera för tillväxt genom att se till att varje shard innehåller tillräckligt med ledigt utrymme för att hantera den ändringsvolym som förväntas. Du bör också utveckla strategier och skript som du kan använda för att snabbt balansera om shards om detta blir nödvändigt.

  • Använd stabila data för shardnyckeln. Om shardnyckeln ändras kan motsvarande dataobjekt behöva flyttas mellan shards, vilket ökar den mängd arbete som kan utföras av uppdateringsåtgärder. Undvik därför att basera shardnyckeln på eventuell ej beständig information. Leta i stället efter attribut som inte varierar eller som bildar en nyckel på ett naturligt sätt.

  • Se till att shardnycklarna är unika. Undvik till exempel att använda fält som ökar automatiskt som shardnyckel. I vissa system kan automatiskt ökade fält inte koordineras över shards, vilket kan leda till att objekt i olika shards har samma shardnyckel.

    Värden som ökar automatiskt i andra fält som inte är shardnycklar kan också orsaka problem. Om du till exempel använder fält som ökar automatiskt för att generera unika ID:n kan två olika objekt som finns i olika shards tilldelas samma ID.

  • Det kanske inte går att utforma en shardnyckel som överensstämmer med kraven för varje tänkbar fråga mot data. Fragmentera data för att få stöd för de frågor som körs oftast, och skapa vid behov sekundära indextabeller som ger stöd för frågor som hämtar data med hjälp av kriterier baserat på attribut som inte är en del av shardnyckeln. Mer information finns i Mönster för indextabell.

  • Frågor som endast har åtkomst till en enda shard är mer effektiva än de som hämtar data från flera shards, så undvik att implementera ett shard-system som leder till att program utför stora mängder frågor som kopplar data som finns i olika shards. Tänk på att en enda shard kan innehålla data för flera typer av entiteter. Överväg att avnormalisera data för att hålla samman relaterade entiteter som ofta frågas (t.ex. information om kunder och beställningar som de har gjort) i samma shard för att minska antalet separata läsningar som ett program utför.

    Om en entitet i en shard hänvisar till en entitet som är lagrad i en annan shard, ska shardnyckeln inkluderas för den andra entiteten som en del av schemat för den första entiteten. Det kan hjälpa till att förbättra prestanda för frågor som hänvisar till relaterade data i olika shards.

  • Om ett program måste köra frågor som hämtar data från flera shards kan det vara möjligt att hämta dessa data med hjälp av parallella uppgifter. Exempel på detta kan vara förgreningsfrågor, där data från flera shards hämtas parallellt och sedan aggregeras till ett enda resultat. Den här metoden lägger dock oundvikligen till komplexitet i dataåtkomstlogiken för en lösning.

  • För många program kan det vara mer effektivt att skapa ett stort antal små shards i stället för att ha ett litet antal stora shards, eftersom de ger bättre möjligheter till belastningsutjämning. Det kan också vara användbart om du tror att du kommer att behöva migrera shards från en fysisk plats till en annan. Det går snabbare att flytta en liten shard än att flytta en stor.

  • Kontrollera att resurserna som är tillgängliga för varje shardlagringsnod är tillräckliga för att hantera skalbarhetskraven vad gäller datastorlek och dataflöde. Mer information finns i avsnittet "Designa partitioner för skalbarhet" i vägledningen för datapartitionering.

  • Överväg att replikera referensdata till alla shards. Om en åtgärd som hämtar data från en shard även hänvisar till statiska eller långsamma data som en del av samma fråga lägger du till dessa data i sharden. Programmet kan sedan enkelt hämta alla data för frågan enkelt utan att behöva göra ett ytterligare serveranrop till ett separat datalager.

    Om referensdata som lagras i flera shards ändras måste systemet synkronisera dessa ändringar över alla shards. Systemet kan uppleva viss inkonsekvens under den här synkroniseringen. Om du gör detta ska programmen utformas för att kunna hantera det.

  • Det kan vara svårt att bibehålla referensintegriteten och konsekvensen mellan shards, så du bör minimera de åtgärder som påverkar data i flera shards. Utvärdera om fullständig datakonsekvens faktiskt krävs om ett program måste ändra data över shards. En vanlig metod i molnet är att istället implementera eventuell (slutlig) konsekvens. Data i varje partition uppdateras separat och programlogiken måste ta ansvar för att se till att alla uppdateringar slutförs, samt hantera de inkonsekvenser som kan uppstå från att skicka frågor till data under tiden en konsekvent åtgärd körs. Mer information om hur du hanterar slutlig datakonsekvens finns i introduktionen till datakonsekvens.

  • Det kan vara en utmaning att konfigurera och hantera ett stort antal shards. Uppgifter som övervakning, säkerhetskopiering, kontroll av konsekvens och loggning eller granskning måste utföras på flera shards och servrar, som eventuellt finns på flera platser. De här uppgifterna implementeras troligtvis med skript eller andra automatiseringslösningar, men det är inte säkert att det helt tar bort de ytterligare administrativa kraven.

  • Shards kan geolokaliseras så att data de innehåller finns nära de instanser av ett program som använder dem. Med den här metoden går det att förbättra prestanda avsevärt, men det kräver ytterligare överväganden för uppgifter som måste komma åt flera shards på olika platser.

När du ska använda det här mönstret

Använd det här mönstret när ett datalager troligtvis behöver skalas bortom de resurser som är tillgängliga för en enda lagringsnod, eller för att förbättra prestanda genom att minska konkurrens i ett datalager.

Anteckning

Primärt fokus för horisontell partitionering är att förbättra prestanda och skalbarhet i ett system, men det kan även leda till att man förbättrar tillgängligheten till följd av hur data delas upp i separata partitioner. Ett fel i en partition hindrar inte nödvändigtvis ett program från att komma åt data som lagras i andra partitioner, och en operatör kan utföra underhåll eller återställning av en eller flera partitioner utan att göra alla data för ett program otillgängliga. Mer information finns i Vägledning om datapartitionering.

Exempel

Följande exempel i C# använder en uppsättning SQL Server-databaser som fungerar som shards. Varje databas innehåller en delmängd av de data som används av ett program. Programmet hämtar data som är fördelade på shards med sin egen logik för horisontell partitionering (detta är ett exempel på en förgreningsfråga). Information om de data som finns i varje shard returneras av en metod som kallas GetShards. Den här metoden returnerar en uppräkningsbar lista över ShardInformation-objekt, där typen ShardInformation innehåller en identifierare för varje shard och SQL Server-anslutningssträngen som ett program ska använda för att ansluta till sharden (anslutningssträngarna visas inte i kodexemplet).

private IEnumerable<ShardInformation> GetShards()
{
  // This retrieves the connection information from a shard store
  // (commonly a root database).
  return new[]
  {
    new ShardInformation
    {
      Id = 1,
      ConnectionString = ...
    },
    new ShardInformation
    {
      Id = 2,
      ConnectionString = ...
    }
  };
}

Koden nedan visar hur programmet använder listan över ShardInformation-objekt för att köra en fråga som hämtar data från varje shard parallellt. Informationen om frågan visas inte, men i det här exemplet innehåller data som hämtas en sträng som kan innehålla information, t.ex. namnet på en kund om shards innehåller information om kunder. Resultatet slås ihop till en ConcurrentBag-samling för bearbetning av programmet.

// Retrieve the shards as a ShardInformation[] instance.
var shards = GetShards();

var results = new ConcurrentBag<string>();

// Execute the query against each shard in the shard list.
// This list would typically be retrieved from configuration
// or from a root/primary shard store.
Parallel.ForEach(shards, shard =>
{
  // NOTE: Transient fault handling isn't included,
  // but should be incorporated when used in a real world application.
  using (var con = new SqlConnection(shard.ConnectionString))
  {
    con.Open();
    var cmd = new SqlCommand("SELECT ... FROM ...", con);

    Trace.TraceInformation("Executing command against shard: {0}", shard.Id);

    var reader = cmd.ExecuteReader();
    // Read the results in to a thread-safe data structure.
    while (reader.Read())
    {
      results.Add(reader.GetString(0));
    }
  }
});

Trace.TraceInformation("Fanout query complete - Record Count: {0}",
                        results.Count);

Nästa steg

Följande riktlinjer kan även vara relevanta när du implementerar det här mönstret:

  • Introduktion till datakonsekvens. Det kan vara nödvändigt att bibehålla konsekvensen för data som distribueras över olika shards. Sammanfattar problem med att bibehålla konsekvens över distribuerade data och beskriver fördelar och nackdelar med olika konsekvensmodeller.
  • Riktlinjer för datapartitionering. Horisontell partitionering av ett dataarkiv kan leda till ett antal ytterligare problem. Beskriver de här problemen i relation till partitionering av datalager i molnet för att förbättra skalbarhet, minska konkurrens och optimera prestanda.

Följande mönster kan också vara relevanta när du implementerar det här mönstret:

  • Indextabellmönster. Ibland är det inte möjligt att uppnå fullständigt stöd för frågor endast genom utformningen av shardnyckeln. Gör det möjligt för ett program att snabbt hämta data från ett stort datalager genom att ange en annan nyckel än shardnyckeln.
  • Mönster för materialiserad vy. Om du vill behålla prestandan för vissa frågeåtgärder är det praktiskt att skapa materialiserade vyer som sammanställer och sammanfattar data, särskilt om dessa sammanfattande data baseras på information som är fördelad i shards. Beskriver hur du skapar och fyller i dessa vyer.