
av St-Pete
Hej alla! Jag heter Mons Anderson, plattformsarkitekt , och jag kommer att berÀtta hur vi byggde vÄrt S3-lagring, hur det fungerar, vilka lösningar som var framgÄngsrika och vilka vi skulle ha Àndrat om vi skulle starta ett liknande projekt frÄn början idag.
Artikeln Àr baserad pÄ en presentation pÄ av Mail.ru Cloud Solutions & Tarantool. I artikeln kommer vi att diskutera:
- hur lagringen pÄ Mail.ru var organiserad, som vi byggde vÄrt S3-lagring pÄ;
- vad vi lade till för att skapa Mail.ru Cloud Storage;
- hur objektlagringsmodellen fungerar och vilka steg som har tagits för att komma till produktion;
- om förbÀttringar av produktionssystemet: failover och skalning;
- hur vi implementerade shardning och resharding;
- samt om hantering av SSL-certifikat.
Om du inte vill lÀsa kan du .
Hur lagringen pÄ Mail.ru var organiserad, som vi byggde vÄrt S3-lagring pÄ
Utvecklingen av vÄrt S3 började över Mail.ru Cloud-lagringen, dÀrför Àr det viktigt att först berÀtta hur det Àr organiserat och vad det kan göra.
Mail.ru:s molnlagring bestĂ„r av servrar med diskar. I genomsnitt Ă€r en modern lagringsserver 36 diskar pĂ„ 12â14 terabyte. Tidigare var diskarna mindre, men under de senaste tre Ă„ren har diskstorlekarna ökat och idag Ă€r det nĂ€stan en halv petabyte rĂ„data.
Disken frÄn olika servrar i lagringen kombineras i sÄ kallade "par". Ett par Àr en enhet för lagring av filer. I grund och botten Àr det en disk som Àr monterad i en viss partition pÄ en viss vÀg, dÀr filer som identifieras med hashar kan lagras.
Par Àr en historisk benÀmning som har kvarstÄtt till idag, Àven om det nu inte nödvÀndigtvis bara Àr tvÄ diskar i ett par. Det kan finnas tre diskar, och det kan Àven finnas olika hybrida lagringar, till exempel 3/2.

Par (pair) â enheter för objektlagring
Alla par lagras i PairDB â en applikation baserad pĂ„ Tarantool. Alla databaser i vĂ„rt lagringssystem, frĂ„n de allra första, Ă€r Tarantool; vi anvĂ€nder inga andra databaser.
PairDB lagrar alla par, deras tillstÄnd, ledigt utrymme, felöverskridande möjligheter och senaste fel. Det kan ocksÄ sjÀlv kontrollera paren, uppdatera deras status och verifiera om de fungerar eller inte. SÄ PairDB Àr en allmÀn översikt över tillstÄndet för alla diskar i vÄrt system.

Pair DB: databas med tillstÄndet hos par
I systemet lagras filer, och för att veta vilken fil som finns pĂ„ vilken enhet, behövs en annan databas â FileDB. Den lagrar en mappning som definierar kopplingen: filen lagras pĂ„ denna enhet, samt ett litet antal nödvĂ€ndiga attribut.
File DB: platsen dÀr filen lagras
En annan viktig del Àr tjÀnsten Nylon, en router för att arbeta med databaser. Den fungerar som en enda ingÄngspunkt och gör det möjligt att arbeta via ett enhetligt grÀnssnitt med bÄde PairDB och FileDB. Det Àr en stateless-tjÀnst, den utför belastningsbalansering av förfrÄgningar, förstÄr vilken shard FileDB som ska anvÀndas, och vet vilka enheter som Àr aktiva och vilka som inte Àr det.

Nylon: router för arbete med databaser
InnehĂ„ll mĂ„ste ocksĂ„ pĂ„ nĂ„got sĂ€tt lĂ€ggas till i lagringen. För detta finns en tjĂ€nst â Streamer. Den tillhandahĂ„ller tvĂ„ HTTP-metoder: PUT-metoden för att ladda upp innehĂ„ll till lagringen och GET-metoden för att hĂ€mta det dĂ€rifrĂ„n. HTTP Ă€r ett ganska populĂ€rt och bekvĂ€mt protokoll för datatransfer.
NÀr vi kontaktar Streamer, tar den kontakt med PairDB via Nylon, tar reda pÄ vilken enhet filen kan laddas upp till, för att sedan överföra data via WebDAV till den enheten.
I grunden Àr varje lagringsserver nginx plus diskar som Àr monterade enligt angivna sökvÀgar. Vi kan ladda upp en fil till lagringen frÄn Streamer, ta bort den, döpa om den eller kontrollera dess integritet. Det Àr ett bekvÀmt grÀnssnitt för lÄg-nivÄ interaktion med lagringen.

Streamer: ingÄngspunkt till lagringen
Vad vi har lagt till för att skapa S3-lagring
SÄ vi har granskat den allmÀnna baskonstruktionen av lagringen vid tidpunkten dÄ vi planerade att lansera S3-lagringen. Med PUT-metoden kunde vi lÀgga in godtyckligt innehÄll dÀr och fÄ en hash som identifierare för dessa data. Med denna identifierare kunde vi senare ÄtervÀnda och hÀmta den ursprungliga filen. Men detta rÀcker inte för att implementera S3. I S3-protokollet, förutom sjÀlva lagringen av objekt, finns:
- lagring av metadata â ytterligare egenskaper hos objekten;
- organisation av Ätkomst till objekten via HTTP;
- gruppering av objekt i samlingar â buckets;
- HTTP-S3 Endpoint. S3 organiserar data i specifika strukturer â buckets, dĂ€r varje bucket fungerar som en ingĂ„ngspunkt för lagring av filer.
För att implementera denna logik behövdes en separat tjÀnst. Vi ville ocksÄ omedelbart ta hÀnsyn till arkitekturen för vidare tillvÀxt av tjÀnsten med linjÀr skalbarhet.
De första komponenterna
Demon som implementerar S3 API. Detta Àr det standardiserade S3 API frÄn Amazon som stöder arbete med XML för metadata och möjliggör direkt överföring av innehÄll. Vi behövde inte uppfinna nÄgot, allt Àr beskrivits och dokumenterat.
Vi stÀllde ocksÄ Nginx framför tjÀnsten. Vi anvÀnde det för SSL-termering, belastningsbalansering samt för viss logik i Lua (metrik, loggning och spÄrning).
För lagring av S3-metadata valde vi ocksÄ Tarantool. I den första versionen hÀmtade S3-demon metadata frÄn denna databas, medan innehÄllet lagrades i ett stort lagringsutrymme via Streamer.

Nginx + S3 API + metadata
Objektschema för lagring
LÄt oss se hur S3 fungerar. AnvÀndaren kan skapa en bucket - en samling av objekt. Buckets adresseras med hjÀlp av vÀrdnamnet och Àr en underdomÀn av tjÀnsten. Inom en bucket kan anvÀndaren skapa objekt. Objektets identifierare kommer att vara URL:en. InnehÄllet i objektet Àr en blob, en mÀngd binÀrdata som vi kommer att lagra i lagringsutrymmet. Varje objekt har ocksÄ attribut: namn - samma URL, ACL (Ätkomstkontrollista), och andra tillÀggs- eller godtyckliga attribut - allt detta sparas i metadata.
Det normaliserade schemat för dessa data kan se ut sÄ hÀr: det finns projekt som Àger buckets, som innehar objekt, och objekten kan vara sammansatta. Eftersom ett sÀtt att ladda upp objekt Àr i delar, finns det tvÄ hjÀlptabeller för uppladdning: uploads och chunks. Projekten har ocksÄ autentiseringsuppgifter för Ätkomst och fakturering.

Dataschema
Eftersom vi skapade en B2B-tjÀnst med betalabonnemang behövdes det en faktureringsmodul i detta schema.
Faktureringssystemet implementerade vi ocksÄ pÄ Tarantool.

FörbÀttringar av S3-lagring: steg mot produktion
Vi har redan skapat en fungerande modell som kan anvÀndas: objekt och metadata lagrades, men för att gÄ i produktion saknades nÄgra punkter.
För det första Àr det frÄga om ett ratbegrÀnsningssystem. Om vi kör tjÀnsten utan den, kunde vi vid hög belastning oavsiktligt överbelasta en del av systemet. RatbegrÀnsningen bör fungera sÄ hÀr: varje S3-förfrÄgan kommer till en specifik vÀrd, den hÀr vÀrden Àr identifieraren för en bucket, och bucketen tillhör en viss kund. Vi behöver definiera en funktion för bucketen som skulle möjliggöra berÀkning av ratbegrÀnsningen.
Dessutom bör ratbegrÀnsningssystemet vara tillrÀckligt effektivt för att hantera den belastning som kommer till S3.
HÀr anvÀnde vi Äterigen Tarantool. RatbegrÀnsningarna Àr en kluster av 21 instanser, instanserna Àr uppdelade i grupper, fördelade över tre fysiska noder och sammanfogade i ett stort topologiskt kluster. KonfigurationsÀndringar sprids automatiskt över klustret: ratbegrÀnsningar, standardvÀrden och konfiguration. Varje bucket hanteras strikt av en instans. NÀr en förfrÄgan kommer till en specifik bucket berÀknas den ansvariga instansen för den bucketen. Inom denna nod görs en rÀkning av den aktuella begÀran baserat pÄ en algoritm som liknar Token Bucket. DÀrefter, baserat pÄ aktuella belastningsmÄtt och egenskaper som har stÀllts in för den specifika bucketen, avgör ratbegrÀnsningssystemet om förfrÄgan kan utföras eller inte. Kontroll av begrÀnsningarna görs i det tidigaste skedet av S3-förfrÄgan, vilket skyddar alla andra delar av systemet frÄn överbelastning.

Det Àr ocksÄ ganska svÄrt att klara sig utan cache under belastning. I S3 förutsÀtts upprepade förfrÄgningar av samma objekt, det vill sÀga det Àr ett varmt lagringsutrymme. I normala fall hanteras förfrÄgningar till enstaka filer av hela kedjan: Streamer, FileDB, PairDB, Storage. Men vid upprepade förfrÄgningar till en fil optimerar vi Ätkomsten till detta innehÄll med hjÀlp av en lokal cache.
Cachen Àr flerskiktad och implementerad med hjÀlp av nginx, lokala, SSD- och RAM-diskar. HÀr anvÀnde vi inte Tarantool, eftersom det Àr mer praktiskt att leverera objekt frÄn filsystemet, vilket gör det möjligt att göra cache-tiering. Dessutom har vi stora objekt med en maximal storlek pÄ 32 gigabyte, och i Tarantool kan endast smÄ objekt cachas.

Det hÀr var det första systemet vi lanserade, som hade en viss berÀknad kapacitet, som rÀckte för att utforska och förstÄ produkten, samt att den skulle fungera.
FörbÀttringar av stridsystemet: failover och skalning
Systemet var redan i drift, men vi missade nĂ„got i början â vi behövde lĂ€gga till failover och skalning.
VĂ„r S3-demont hade hĂ€mtat metadata via Tarantool-protokollet. Vi ersatte den ursprungliga databasen med Tarantool, som fungerade som en proxy-router för begĂ€rningar om metadata. Ur applikationens perspektiv, som implementerade API:t, förĂ€ndrades inget â den fortsatte att kommunicera med databasen via Tarantool-protokollet, men routern kunde erbjuda aktiv failover. Det betyder att vi kunde kontrollera nodens tillgĂ€nglighet, ha en paus vid omkopplingar och fel, och sĂ„ vidare. SjĂ€lva applikationen modifierade vi inte.

Mer information om hur vi genomförde sharding
NÀsta frÄga som behövde ÄtgÀrdas var sharding. Systemet vÀxte, antalet objekt ökade och vi behövde sÀkerstÀlla möjligheter för vidare tillvÀxt.
LÄt oss ÄtervÀnda till datamodellen: det finns projekt, det finns buckets, krediter och fakturering. Det hÀr Àr objekt som med stor sannolikhet inte kommer vÀxa utanför en enskild instans varken i volym eller i begÀrningar inom överskÄdlig framtid. SÄ det finns ingen anledning att sharda dem, och vi flyttade dem till en separat instans som förblir ofördelad. Det gör det möjligt att hantera projekt och buckets mer konsekvent, eftersom det finns en enhetlig ofördelad punkt.

Det finns ocksĂ„ objekt i schemat som vĂ€xer linjĂ€rt â först var det hundratusentals, nu mĂ€ts deras antal i flera miljarder. SĂ„dana objekt, tillsammans med deras delar, behövde flyttas till en shardad kluster.

Vi delade upp schemat, men objekten behöver arbeta med buckets: varje objekt tillhör alltid en specifik bucket, plus det finns en ACL som körs pÄ bucketen. DÀrför har vi för varje shard med objekt en skugga kopia av varje bucket. Dessutom, under modifiering av objekt och utförande av begÀrningar, mÄste vi rÀkna volymen för fakturering, sÄ pÄ varje shard finns det rÀkneverk för fakturering.
Vi har ocksÄ lagt till ytterligare nÄgra tabeller och komponenter:
- en papperskorg, för att ta bort gamla projekt som tas bort eller fryses;
- kön pÄ bakgrundsuppgifter, det vill sÀga det primÀra lagret kan utföra bakgrundsuppgifter som mÄste göras i klustret;
- livscykelstöd â en mekanism som möjliggör arbete med objekt och hantering av deras livscykel.

Eftersom en del av datan har delats upp pÄ shards behövde vi en sharding-proxy. Man skulle kunna ÄteranvÀnda routern för denna roll, men en separat sharding-proxy, som endast ansvarar för datasharding, gör att routern kan hÀmta data i sin helhet utan att tÀnka pÄ sharding.

Jag kommer att berÀtta varför vi inte valde en fÀrdig lösning utan ville skapa en anpassad sharding-funktion.
LĂ„t oss se hur den Ă€r uppbyggd. Vi har 256 tillgĂ€ngliga shards. För varje bucket tilldelar vi ett intervall med hjĂ€lp av en konsistent funktion. Det Ă€r enkelt â pĂ„ samma sĂ€tt som du med en konsistent funktion bestĂ€mmer tillhörigheten till en shard, sĂ„ bestĂ€mmer du den startande shard och tilldelar intervallet:
f(bucket, shards) = subset
Det vill sÀga, om vi tar en bucket kan vi sÀga att den och dess data alltid kommer att ligga pÄ en specifik delmÀngd av alla shards. Detta minskar pÄverkan mellan olika buckets och förenklar hanteringen av map-reduce-frÄgor, nÀr man till exempel behöver lista objekten i en bucket. För detta mÄste man frÄga alla shards dÀr dessa objekt lagras. Om objekten lÄg pÄ alla shards skulle varje lista pÄverka systemet som helhet, medan den nu endast pÄverkar en specifik delmÀngd.
Vidare â varje objekt tillhör en specifik bucket, sĂ„ nĂ€r vi begĂ€r ett objekt sĂ„ begĂ€r vi det efter namn i en specifik bucket. Det betyder att vi kan definiera en funktion för objektet inte frĂ„n hela det tillgĂ€ngliga intervallet av shards, utan bara frĂ„n delmĂ€ngden av sin bucket:
f(object, subset) = shard
Vi tar ett specifikt objekt och som argument för funktionen skickar vi inte alla shards utan delmĂ€ngden av sin bucket â och fĂ„r en specifik shard.

SĂ„, sharding Ă€r genomfört, vi har en sharding-proxy. Det som Ă„terstĂ„r Ă€r att fĂ„ routern och databasen med metadata att kommunicera med sharding-proxyn. Till exempel för att skapa skuggbilder av objekt â nĂ€r vi skapar en bucket mĂ„ste det primĂ€ra lagret skapa en representant för denna bucket pĂ„ alla shards dĂ€r den ska finnas.

Hur vi genomförde resharding
Det största problemet med shardning Àr reshadering. Det var viktigt för oss att göra detta utan nedtid, eftersom systemet redan var i produktion. Jag kommer att visa hur vi löste problemet med ett exempel frÄn en liknande uppgift med live-migrering av data frÄn ett projekt till ett annat.
Nedan Àr schemat för vÄr klusterkonfiguration som blev resultatet av att vi implementerade shardning. Vi har nginx, S3 API, en router, en primÀr databas med projekt, en sharderande proxy och sjÀlva shardarna.

Jag nĂ€mnde tidigare att det under ett visst stadium av projektet fanns en produktuppgift: "Starta Ă€nnu ett lagringsutrymme, Icebox, â som Hotbox, men för kalla data". I praktiken handlar det om ett liknande lagringsutrymme, men med olika URL:er och utan cache.

Icebox anvÀndes mindre Àn Hotbox, sÄ det klarade sig ganska lÀnge utan nÄgot sharding. Till slut beslutade vi att avveckla det och kombinera Hotbox och Icebox till en tjÀnst genom att helt enkelt dela upp lagringsklasserna.
Basken i lagringarna överlappar inte, de kunde lÀtt slÄs samman och flyttas, men kunderna anvÀnde bÄde det ena och det andra lagringsutrymmet, vilket Äterspeglar behovet av att lösa problemet med att göra detta utan nedtid. Vi kunde inte bara stÀnga av och kopiera. Vi gjorde migreringen i flera steg.
Först synkroniserade vi de primÀra lagringarna. Vi hade Tarantool och vi kunde vid skapandet av ett objekt göra sÄ hÀr:
- en begÀran kommer till databasen för att skapa en bask, till exempel i Hotbox;
- Tarantool kontrollerar i en annan databas (i det hĂ€r fallet â i Icebox) att det inte finns nĂ„gon sĂ„dan bask;
- om basken finns, meddelar databasen att den inte kan skapas, och den synkroniseras som en existerande.

Synkronisering av baskar
I det lagringsutrymme som skulle ta emot all data introducerades en egenskap för projekt och baskar som indikerar var detta objekt lagras. Det kunde lagras lokalt, det vill sĂ€ga i Hotbox, i Icebox â dĂ„ finns det inga data i det nya lagringsutrymmet, eller det kunde vara i migrering.
Om ett projekt eller bask hade egenskapen Migrating, utfördes begÀran under migreringen först i det nya lagringsutrymmet dÀr data bör finnas, och om de inte fanns dÀr, dirigerades begÀrningarna till lagringsutrymmet dÀrefter.
DÀrefter vÀxlade vi trafik. Eftersom API:et kunde hantera bÄde Icebox- och Hotbox-begÀrningar, kunde vi vÀxla trafiken utan nedtid genom att bara flytta vÀrdar och lÀgga till motsvarande poster i Nginx.
Efter att trafiken omdirigerades kunde vi ta bort Nginx och API frÄn Icebox.
DĂ€refter tog vi bort Icebox nginx och S3 API â och allt fungerade:

DĂ€refter startade vi en bakgrundsprocess för migrering som arbetar inuti databasen â den gĂ„r igenom alla projekt och deras buckets element för element, sĂ€tter dem i statusen Migrating, flyttar data och efter att flytten Ă€r klar sĂ€tter den statusen Local.

Efter datamigreringen behöver vi inte lÀngre det gamla lagret, och vi tar bort de kvarvarande delarna av det gamla systemet, samt tar bort stöd för migreringsstatus frÄn koden.

De samma principerna följdes för resharding frÄn det gamla lagret till det shardade:
- Vi markerade alla buckets som
Non-sharded. Alla förfrÄgningar till dem gick till det ursprungliga, icke-shardade lagret. - Nya buckets skapades direkt med statusen
Sharded. - Vi tog en bucket i taget, satte statusen
Migratingoch flyttade data.
FörfrÄgningar betjÀnades enligt principen:
- Vi lÀser i de nya, sedan i de gamla.
- Vi skapar endast i de nya.
- Vi uppdaterar i tvÄ faser: om det inte finns i de nya, flyttar vi frÄn de gamla till de nya, och sedan uppdaterar vi.
Arbete med SSL-certifikat
PÄ frontend anvÀnder vi Nginx. I vÄrt fall Àr det inte vanlig Nginx, utan OpenResty, Nginx med stöd för LuaJIT.
En annan del av systemet â hantering av SSL-certifikat. I S3-lagret kan du stĂ€lla in en egen domĂ€n för att fĂ„ Ă„tkomst till en specifik bucket, bara med hjĂ€lp av CNAME. Men utan HTTPS gĂ„r det inte idag: en egen domĂ€n innebĂ€r en egen SSL-certifikat.
Som jag redan nÀmnt, ansvarar Nginx för lastbalansering och terminering av SSL. I vÄrt fall Àr det inte vanlig Nginx, utan OpenResty, Nginx med stöd för LuaJIT.
Detta gjorde att vi ganska enkelt kunde lÀra vÄrt Nginx att leverera godtyckliga certifikat. Dessutom behövde vi ge ut certifikat dynamiskt (utan att behöva ange dem i konfigurationsfilen). Vi anvÀnde tillÀgget ssl_certificate_by_lua, som gör det möjligt att lÀsa certifikatet frÄn valfri kÀlla direkt under TLS-handshake. Som certifikatslagring anvÀnde vi ocksÄ Tarantool: detta gör att vi kan hantera certifikat utanför och ger en extremt snabb leverans.
Det har ocksĂ„ implementerats en separat demon vars uppgift Ă€r att regelbundet uppdatera de certifikat som utfĂ€rdades med hjĂ€lp av Letâs Encrypt.

Vad jag skulle behÄlla och vad jag skulle göra annorlunda om jag skulle utveckla lagring pÄ nytt
Vad som behövde anvÀndas frÄn början
Sharding direkt. Ganska mÄnga problem orsakades av resharding. Det Àr lÀtt att göra, men om man startar projekt som behöver skalas, Àr det bÀttre att anvÀnda en sharded kluster frÄn början, Àven om det bara har ett minimum av noder. Implementeringen av sharding i början Àr nÀstan gratis i jÀmförelse med att införa sharding i ett fungerande system.
Arbete med Tarantool genom lastbalanserare. Nu kopplar vi alla nya databaser direkt till arbete genom lastbalanserare. Det gör det möjligt att utöka funktionaliteten och uppnÄ högre redundans.
Auto-failover. Jag skulle installera alla verktyg som krÀvs för auto-failover, eftersom de första misslyckandena efter lanseringen var kopplade till avsaknaden av det. Efter erfarenheten med S3 lanserades alla följande produkter med hÀnsyn till detta.
Funktionen S3 "Versionering". Det verkade frÄn början som en funktion som inte var sÀrskilt efterfrÄgad. Att integrera den hÀr möjligheten i arkitekturen av ett fungerande system Àr extremt svÄrt.
Separat fakturering. Det sÀtt pÄ vilket vi integrerade faktureringen i vÄrt system fungerade bra i början, men sÄ smÄningom började det störa, det skulle ha varit bÀttre att ha det som en helt separat tjÀnst.
Vad som var en bra lösning
Datamodell. Historien har visat att i takt med att tjÀnsten utvecklades sÄ överensstÀmde vi ganska vÀl med datamodellen frÄn Amazon, sÄ vi kan implementera de funktioner som finns dÀr.
Schema för sharding. Jag skulle stödja samma typ av intervallsharding för buckets, eftersom det gör det möjligt att fördela förfrÄgningar frÄn olika buckets över ett stort kluster.
AnvÀndning av Tarantool. Tarantool har hjÀlpt mycket vid utvecklingen av tjÀnsten och dess modifiering, vi arbetade enkelt med data, transformerade och shardade lagret utan att behöva gÄ upp pÄ applikationslagret.
Denna presentation hördes först pÄ by Mail.ru Cloud Solutions&Tarantool. Se andra förelÀsningar och följ anmÀlningarna till evenemang pÄ Telegram .
Du kan ocksÄ titta pÄ min gamla presentation om S3 eller lÀsa artikeln frÄn min kollega om blocklagring.
- .
- .
KĂ€lla: habr.com

