One-cloud - operativsystem på datacenternivå i Odnoklassniki

One-cloud - operativsystem på datacenternivå i Odnoklassniki

Aloha, människor! Jag heter Oleg Anastasyev, jag arbetar på Odnoklassniki i plattformsteamet. Och förutom mig är det mycket hårdvara som fungerar i Odnoklassniki. Vi har fyra datacenter med cirka 500 rack med mer än 8 tusen servrar. Vid en viss tidpunkt insåg vi att införandet av ett nytt ledningssystem skulle göra det möjligt för oss att ladda utrustning mer effektivt, underlätta åtkomsthantering, automatisera (om)distributionen av datorresurser, påskynda lanseringen av nya tjänster och snabba upp svaren. till storskaliga olyckor.

Vad kom ut av det?

Förutom mig och ett gäng hårdvara finns det också människor som arbetar med den här hårdvaran: ingenjörer som finns direkt i datacenter; nätverkare som installerar nätverksprogramvara; administratörer, eller SRE:er, som tillhandahåller motståndskraft mot infrastruktur; och utvecklingsteam, var och en av dem ansvarar för en del av portalens funktioner. Mjukvaran de skapar fungerar ungefär så här:

One-cloud - operativsystem på datacenternivå i Odnoklassniki

Användarförfrågningar tas emot båda på framsidan av huvudportalen www.ok.ru, och på andra, till exempel på musik-API-fronterna. För att bearbeta affärslogiken anropar de applikationsservern, som vid bearbetning av begäran anropar de nödvändiga specialiserade mikrotjänsterna - en graf (graf över sociala anslutningar), användarcache (cache för användarprofiler), etc.

Var och en av dessa tjänster är utplacerade på många maskiner, och var och en av dem har ansvariga utvecklare som ansvarar för modulernas funktion, deras drift och tekniska utveckling. Alla dessa tjänster körs på hårdvaruservrar, och tills nyligen lanserade vi exakt en uppgift per server, det vill säga den var specialiserad för en specifik uppgift.

Varför är det så? Detta tillvägagångssätt hade flera fördelar:

  • Lättad masshantering. Låt oss säga att en uppgift kräver vissa bibliotek, vissa inställningar. Och sedan tilldelas servern till exakt en specifik grupp, cfengine-policyn för denna grupp beskrivs (eller har redan beskrivits), och denna konfiguration rullas ut centralt och automatiskt till alla servrar i denna grupp.
  • Förenklat diagnostik. Låt oss säga att du tittar på den ökade belastningen på den centrala processorn och inser att denna belastning endast kan genereras av uppgiften som körs på den här hårdvaruprocessorn. Sökandet efter någon att skylla på slutar mycket snabbt.
  • Förenklat övervakning av. Om något är fel på servern rapporterar monitorn det, och du vet exakt vem som är skyldig.

En tjänst som består av flera repliker tilldelas flera servrar - en för varje. Sedan allokeras datorresursen för tjänsten väldigt enkelt: antalet servrar tjänsten har, den maximala mängd resurser den kan förbruka. "Lätt" betyder här inte att det är lätt att använda, utan i den meningen att resursallokering sker manuellt.

Detta tillvägagångssätt gjorde det också möjligt för oss att göra specialiserade järnkonfigurationer för en uppgift som körs på den här servern. Om uppgiften lagrar stora mängder data använder vi en 4U-server med ett chassi med 38 diskar. Om uppgiften är rent beräkningsmässig kan vi köpa en billigare 1U-server. Detta är beräkningseffektivt. Detta tillvägagångssätt tillåter oss bland annat att använda fyra gånger färre maskiner med en belastning som är jämförbar med ett vänligt socialt nätverk.

Sådan effektivitet i användningen av datorresurser bör också säkerställa ekonomisk effektivitet, om vi utgår från premissen att det dyraste är servrar. Länge var hårdvara den dyraste, och vi lade mycket ansträngning på att sänka priset på hårdvara och kom fram till feltoleransalgoritmer för att minska hårdvarans tillförlitlighetskrav. Och idag har vi nått det stadium då priset på servern har upphört att vara avgörande. Om du inte överväger de senaste exotiska, spelar den specifika konfigurationen av servrarna i racket ingen roll. Nu har vi ett annat problem - priset på utrymmet som servern tar upp i datacentret, det vill säga utrymmet i racket.

När vi insåg att så var fallet bestämde vi oss för att beräkna hur effektivt vi använde ställen.
Vi tog priset på den mest kraftfulla servern från de ekonomiskt försvarbara, beräknade hur många sådana servrar vi kunde placera i rack, hur många uppgifter vi skulle köra på dem baserat på den gamla modellen "en server = en uppgift" och hur mycket sådana uppgifter skulle kunna utnyttja utrustningen. De räknade och fällde tårar. Det visade sig att vår effektivitet i att använda rack är cirka 11%. Slutsatsen är uppenbar: vi måste öka effektiviteten i att använda datacenter. Det verkar som att lösningen är uppenbar: du måste köra flera uppgifter på en server samtidigt. Men det är här som svårigheterna börjar.

Masskonfigurationen blir dramatiskt mer komplicerad - det är nu omöjligt att tilldela en grupp till en server. När allt kommer omkring, nu kan flera uppgifter med olika kommandon startas på en server. Dessutom kan konfigurationen vara motstridig för olika applikationer. Diagnosen blir också mer komplicerad: om du ser ökad CPU- eller diskförbrukning på en server vet du inte vilken uppgift som orsakar problem.

Men huvudsaken är att det inte finns någon isolering mellan uppgifter som körs på samma maskin. Här är till exempel en graf över den genomsnittliga svarstiden för en serveruppgift före och efter att en annan beräkningsapplikation lanserades på samma server, inte på något sätt relaterad till den första - svarstiden för huvuduppgiften har ökat avsevärt.

One-cloud - operativsystem på datacenternivå i Odnoklassniki

Uppenbarligen måste du köra uppgifter antingen i behållare eller i virtuella maskiner. Eftersom nästan alla våra uppgifter körs under ett OS (Linux) eller är anpassade för det behöver vi inte stödja många olika operativsystem. Följaktligen behövs inte virtualisering; på grund av den extra omkostnaden blir den mindre effektiv än containerisering.

Som en implementering av behållare för att köra uppgifter direkt på servrar är Docker en bra kandidat: filsystembilder löser problem med motstridiga konfigurationer bra. Det faktum att bilder kan vara sammansatta av flera lager tillåter oss att avsevärt minska mängden data som krävs för att distribuera dem på infrastrukturen, och separera gemensamma delar i separata baslager. Då kommer de grundläggande (och mest voluminösa) lagren att cachelagras ganska snabbt genom hela infrastrukturen, och för att leverera många olika typer av applikationer och versioner behöver endast små lager överföras.

Dessutom ger ett färdigt register och bildtaggning i Docker oss färdiga primitiver för versionering och leverans av kod till produktion.

Docker, precis som alla andra liknande tekniker, ger oss en viss nivå av containerisolering ur lådan. Till exempel minnesisolering - varje behållare ges en gräns för användningen av maskinminne, utöver vilken den inte kommer att konsumera. Du kan också isolera behållare baserat på CPU-användning. För oss räckte dock inte standardisolering. Men mer om det nedan.

Att köra behållare direkt på servrar är bara en del av problemet. Den andra delen är relaterad till att hosta behållare på servrar. Du måste förstå vilken behållare som kan placeras på vilken server. Detta är inte en så lätt uppgift, eftersom containrar måste placeras på servrar så tätt som möjligt utan att minska deras hastighet. Sådan placering kan också vara svår ur feltoleranssynpunkt. Ofta vill vi placera repliker av samma tjänst i olika rack eller till och med i olika rum i datacentret, så att om ett rack eller rum går sönder, förlorar vi inte omedelbart alla servicerepliker.

Att distribuera behållare manuellt är inte ett alternativ när du har 8 tusen servrar och 8-16 tusen behållare.

Dessutom ville vi ge utvecklare mer självständighet i resursallokering så att de själva kunde hosta sina tjänster i produktionen, utan hjälp av en administratör. Samtidigt ville vi behålla kontrollen så att någon mindre tjänst inte skulle förbruka alla våra datacenters resurser.

Självklart behöver vi ett kontrolllager som skulle göra detta automatiskt.

Så vi kom fram till en enkel och begriplig bild som alla arkitekter avgudar: tre rutor.

One-cloud - operativsystem på datacenternivå i Odnoklassniki

one-cloud masters är ett failover-kluster som ansvarar för molnorkestrering. Utvecklaren skickar ett manifest till mastern, som innehåller all information som behövs för att vara värd för tjänsten. Baserat på det ger befälhavaren kommandon till utvalda minions (maskiner designade för att köra containrar). Underhållarna har vår agent, som tar emot kommandot, utfärdar sina kommandon till Docker och Docker konfigurerar linuxkärnan för att starta motsvarande behållare. Förutom att utföra kommandon, rapporterar agenten kontinuerligt till befälhavaren om förändringar i tillståndet för både minionmaskinen och behållarna som körs på den.

Resursfördelning

Låt oss nu titta på problemet med mer komplex resursallokering för många minions.

En datorresurs i ett moln är:

  • Mängden processorkraft som förbrukas av en specifik uppgift.
  • Mängden tillgängligt minne för uppgiften.
  • Nätverkstrafik. Var och en av minionsna har ett specifikt nätverksgränssnitt med begränsad bandbredd, så det är omöjligt att fördela uppgifter utan att ta hänsyn till mängden data de överför över nätverket.
  • Diskar. Utöver det, uppenbarligen, till utrymmet för dessa uppgifter, allokerar vi också typen av disk: HDD eller SSD. Diskar kan betjäna ett ändligt antal förfrågningar per sekund - IOPS. Därför, för uppgifter som genererar mer IOPS än en enskild disk kan hantera, allokerar vi också "spindlar" - det vill säga diskenheter som måste vara exklusivt reserverade för uppgiften.

Sedan för någon tjänst, till exempel för användarcache, kan vi registrera de förbrukade resurserna på detta sätt: 400 processorkärnor, 2,5 TB minne, 50 Gbit/s trafik i båda riktningarna, 6 TB hårddiskutrymme placerat på 100 spindlar. Eller i en mer bekant form så här:

alloc:
    cpu: 400
    mem: 2500
    lan_in: 50g
    lan_out: 50g
    hdd:100x6T

Användarcachetjänstresurser förbrukar endast en del av alla tillgängliga resurser i produktionsinfrastrukturen. Därför vill jag försäkra mig om att användarens cache plötsligt, på grund av ett operatörsfel eller inte, förbrukar mer resurser än vad som tilldelats den. Det vill säga att vi måste begränsa resurserna. Men vad skulle vi kunna binda kvoten till?

Låt oss återgå till vårt mycket förenklade diagram över komponenternas interaktion och rita om det med mer detaljer - så här:

One-cloud - operativsystem på datacenternivå i Odnoklassniki

Vad fångar blickarna:

  • Webbgränssnittet och musiken använder isolerade kluster av samma applikationsserver.
  • Vi kan urskilja de logiska lager som dessa kluster tillhör: fronter, cachar, datalagring och hanteringslager.
  • Frontänden är heterogen, den består av olika funktionella delsystem.
  • Cachar kan också vara utspridda över det delsystem vars data de cachelagrar.

Låt oss rita om bilden igen:

One-cloud - operativsystem på datacenternivå i Odnoklassniki

Bah! Ja, vi ser en hierarki! Detta innebär att du kan fördela resurser i större bitar: tilldela en ansvarig utvecklare till en nod i denna hierarki som motsvarar det funktionella undersystemet (som "musik" på bilden), och koppla en kvot till samma nivå i hierarkin. Denna hierarki tillåter oss också att organisera tjänsterna mer flexibelt för att underlätta hanteringen. Till exempel delar vi upp hela webben, eftersom detta är en mycket stor gruppering av servrar, i flera mindre grupper, som på bilden visas som grupp1, grupp2.

Genom att ta bort de extra raderna kan vi skriva varje nod i vår bild i en plattare form: group1.web.front, api.music.front, user-cache.cache.

Det är så vi kommer till begreppet "hierarkisk kö". Den har ett namn som "group1.web.front". En kvot för resurser och användarrättigheter tilldelas den. Vi kommer att ge personen från DevOps rättigheterna att skicka en tjänst till kön, och en sådan anställd kan starta något i kön, och personen från OpsDev kommer att ha administratörsrättigheter, och nu kan han hantera kön, tilldela personer dit, ge dessa personer rättigheter etc. Tjänster som körs på denna kö kommer att köras inom köns kvot. Om köns beräkningskvot inte räcker för att exekvera alla tjänster på en gång, kommer de att exekveras sekventiellt, vilket bildar själva kön.

Låt oss ta en närmare titt på tjänsterna. En tjänst har ett fullständigt kvalificerat namn, som alltid inkluderar namnet på kön. Då kommer den främre webbtjänsten att ha namnet ok-web.grupp1.web.front. Och applikationsservertjänsten den kommer åt kommer att anropas ok-app.grupp1.web.front. Varje tjänst har ett manifest som specificerar all nödvändig information för placering på specifika maskiner: hur många resurser denna uppgift förbrukar, vilken konfiguration som behövs för den, hur många repliker det ska finnas, egenskaper för att hantera fel i denna tjänst. Och efter att tjänsten placerats direkt på maskinerna visas dess instanser. De namnges också entydigt - som instansnummer och tjänstnamn: 1.ok-web.group1.web.front, 2.ok-web.group1.web.front, …

Detta är mycket bekvämt: genom att bara titta på namnet på den löpande behållaren kan vi omedelbart ta reda på mycket.

Låt oss nu titta närmare på vad dessa instanser faktiskt utför: uppgifter.

Klasser för uppgiftsisolering

Alla uppgifter i OK (och förmodligen överallt) kan delas in i grupper:

  • Korta latensuppgifter - prod. För sådana uppgifter och tjänster är svarsfördröjningen (latens) mycket viktig, hur snabbt var och en av förfrågningarna kommer att behandlas av systemet. Exempel på uppgifter: webbfronter, cachar, applikationsservrar, OLTP-lagring, etc.
  • Beräkningsproblem - batch. Här är bearbetningshastigheten för varje specifik begäran inte viktig. För dem är det viktigt hur många beräkningar denna uppgift kommer att göra under en viss (lång) tidsperiod (genomströmning). Dessa kommer att vara alla uppgifter för MapReduce, Hadoop, maskininlärning, statistik.
  • Bakgrundsuppgifter - inaktiv. För sådana uppgifter är varken latens eller genomströmning särskilt viktiga. Detta inkluderar olika tester, migreringar, omräkningar och konvertering av data från ett format till ett annat. Å ena sidan liknar de beräknade, å andra sidan spelar det ingen roll för oss hur snabbt de blir klara.

Låt oss se hur sådana uppgifter förbrukar resurser, till exempel den centrala processorn.

Korta förseningsuppgifter. En sådan uppgift kommer att ha ett CPU-förbrukningsmönster som liknar detta:

One-cloud - operativsystem på datacenternivå i Odnoklassniki

En begäran från användaren tas emot för bearbetning, uppgiften börjar använda alla tillgängliga CPU-kärnor, bearbetar den, returnerar ett svar, väntar på nästa begäran och stoppar. Nästa förfrågan kom - återigen valde vi allt som fanns där, beräknade det och väntar på nästa.

För att garantera minsta fördröjning för en sådan uppgift måste vi ta de maximala resurserna den förbrukar och reservera det erforderliga antalet kärnor på minion (maskinen som kommer att utföra uppgiften). Då blir bokningsformeln för vårt problem följande:

alloc: cpu = 4 (max)

och om vi har en minionmaskin med 16 kärnor, så kan exakt fyra sådana uppgifter placeras på den. Vi noterar särskilt att den genomsnittliga processorförbrukningen för sådana uppgifter ofta är mycket låg - vilket är uppenbart, eftersom en betydande del av tiden väntar uppgiften på en förfrågan och inte gör någonting.

Beräkningsuppgifter. Deras mönster kommer att vara något annorlunda:

One-cloud - operativsystem på datacenternivå i Odnoklassniki

Den genomsnittliga CPU-resursförbrukningen för sådana uppgifter är ganska hög. Ofta vill vi att en beräkningsuppgift ska slutföras inom en viss tid, så vi måste reservera det minsta antal processorer den behöver så att hela beräkningen slutförs inom en acceptabel tid. Dess reservationsformel kommer att se ut så här:

alloc: cpu = [1,*)

"Snälla placera den på en minion där det finns minst en fri kärna, och sedan så många som det finns kommer den att sluka allt."

Här är effektiviteten i användningen redan mycket bättre än på uppgifter med kort fördröjning. Men vinsten blir mycket större om du kombinerar båda typerna av uppgifter på en minionmaskin och fördelar dess resurser på språng. När en uppgift med kort fördröjning kräver en processor tar den emot den omedelbart, och när resurserna inte längre behövs överförs de till beräkningsuppgiften, det vill säga ungefär så här:

One-cloud - operativsystem på datacenternivå i Odnoklassniki

Men hur gör man det?

Låt oss först titta på prod och dess allok: cpu = 4. Vi måste reservera fyra kärnor. I Docker-körning kan detta göras på två sätt:

  • Använder alternativet --cpuset=1-4, d.v.s. allokera fyra specifika kärnor på maskinen till uppgiften.
  • Använd --cpuquota=400_000 --cpuperiod=100_000, tilldela en kvot för processortid, d.v.s. indikera att varje 100:e ms realtid förbrukar uppgiften inte mer än 400 ms processortid. Samma fyra kärnor erhålls.

Men vilken av dessa metoder är lämplig?

cpuset ser ganska attraktivt ut. Uppgiften har fyra dedikerade kärnor, vilket gör att processorcacher kommer att fungera så effektivt som möjligt. Detta har också en nackdel: vi skulle behöva ta på oss uppgiften att distribuera beräkningar över maskinens olastade kärnor istället för operativsystemet, och detta är en ganska icke-trivial uppgift, särskilt om vi försöker placera batchuppgifter på en sådan maskin. Tester har visat att alternativet med en kvot lämpar sig bättre här: på så sätt har operativsystemet större frihet att välja kärnan för att utföra uppgiften i det aktuella ögonblicket och processortiden fördelas mer effektivt.

Låt oss ta reda på hur man gör reservationer i Docker baserat på det minsta antalet kärnor. Kvoten för batchuppgifter är inte längre tillämplig, eftersom det inte finns något behov av att begränsa maximinivån, det räcker med att bara garantera minimum. Och här passar alternativet bra docker run --cpushares.

Vi kom överens om att om en batch kräver en garanti för minst en kärna, så anger vi --cpushares=1024, och om det finns minst två kärnor, anger vi --cpushares=2048. CPU-andelar stör inte på något sätt fördelningen av processortid så länge det finns tillräckligt med den. Således, om prod för närvarande inte använder alla sina fyra kärnor, finns det inget som begränsar batchuppgifter, och de kan använda ytterligare processortid. Men i en situation där det råder brist på processorer, om prod har förbrukat alla sina fyra kärnor och har nått sin kvot, kommer den återstående processortiden att delas proportionellt till cpushares, dvs i en situation med tre fria kärnor, kommer en att vara ges till en uppgift med 1024 cpushares, och de återstående två kommer att ges till en uppgift med 2048 cpushares.

Men att använda kvot och aktier räcker inte. Vi måste se till att en uppgift med kort fördröjning får prioritet framför en batchuppgift vid allokering av processortid. Utan sådan prioritering kommer batchuppgiften att ta upp all processortid i det ögonblick då den behövs av prod. Det finns inga alternativ för behållarprioritering i Docker-körning, men Linux CPU-schemaläggningspolicyer kommer väl till pass. Du kan läsa om dem i detalj här, och inom ramen för denna artikel kommer vi att gå igenom dem kort:

  • SCHED_OTHER
    Som standard tar alla normala användarprocesser på en Linux-maskin emot.
  • SCHED_BATCH
    Designad för resurskrävande processer. När en uppgift placeras på en processor, införs en så kallad aktiveringsstraff: en sådan uppgift är mindre sannolikt att ta emot processorresurser om den för närvarande används av en uppgift med SCHED_OTHER
  • SCHED_IDLE
    En bakgrundsprocess med mycket låg prioritet, till och med lägre än nice -19. Vi använder vårt bibliotek med öppen källkod en-nio, för att ställa in nödvändig policy när du startar behållaren genom att anropa

one.nio.os.Proc.sched_setscheduler( pid, Proc.SCHED_IDLE )

Men även om du inte programmerar i Java, kan samma sak göras med kommandot chrt:

chrt -i 0 $pid

Låt oss sammanfatta alla våra isoleringsnivåer i en tabell för tydlighetens skull:

Isoleringsklass
Alloc Exempel
Docker-körningsalternativ
sched_setscheduler chrt*

Prod
cpu = 4
--cpuquota=400000 --cpuperiod=100000
SCHED_OTHER

Sats
CPU = [1, *)
--cpushares=1024
SCHED_BATCH

Idle
Cpu= [2, *)
--cpushares=2048
SCHED_IDLE

*Om du gör chrt inifrån en behållare, kan du behöva sys_nice-förmågan, eftersom Docker som standard tar bort denna funktion när behållaren startas.

Men uppgifter förbrukar inte bara processorn utan också trafik, vilket påverkar latensen för en nätverksuppgift ännu mer än den felaktiga allokeringen av processorresurser. Därför vill vi naturligtvis få exakt samma bild för trafiken. Det vill säga när en prod-uppgift skickar några paket till nätverket, begränsar vi den maximala hastigheten (formel alloc: lan=[*,500mbps) ), med vilken prod kan göra detta. Och för batch garanterar vi endast den minsta genomströmningen, men begränsar inte den maximala (formel alloc: lan=[10Mbps,*) ) I det här fallet bör prod-trafik få prioritet framför batchuppgifter.
Här har Docker inga primitiver som vi kan använda. Men det kommer till vår hjälp Linux trafikkontroll. Vi kunde uppnå önskat resultat med hjälp av disciplin Hierarkisk rättvis servicekurva. Med dess hjälp skiljer vi två klasser av trafik: högprioriterad prod och lågprioriterad batch/tomgång. Som ett resultat är konfigurationen för utgående trafik så här:

One-cloud - operativsystem på datacenternivå i Odnoklassniki

här är 1:0 "root qdisc" till hsfc-disciplinen; 1:1 - hsfc barnklass med en total bandbreddsgräns på 8 Gbit/s, under vilken barnklasserna för alla containrar är placerade; 1:2 - hsfc-underklassen är gemensam för alla batch- och inaktiva uppgifter med en "dynamisk" gräns, som diskuteras nedan. De återstående hsfc-underklasserna är dedikerade klasser för för närvarande körande prodcontainrar med gränser som motsvarar deras manifest - 450 och 400 Mbit/s. Varje hsfc-klass tilldelas en qdisc-kö fq eller fq_codel, beroende på Linux-kärnversionen, för att undvika paketförlust under trafikskurar.

Vanligtvis tjänar tc-discipliner till att endast prioritera utgående trafik. Men vi vill prioritera inkommande trafik också - trots allt kan någon batchuppgift enkelt välja hela den inkommande kanalen, ta emot till exempel en stor batch indata för map&reduce. För detta använder vi modulen ifb, som skapar ett virtuellt ifbX-gränssnitt för varje nätverksgränssnitt och omdirigerar inkommande trafik från gränssnittet till utgående trafik på ifbX. Vidare, för ifbX, arbetar alla samma discipliner för att styra utgående trafik, för vilken hsfc-konfigurationen kommer att vara mycket lika:

One-cloud - operativsystem på datacenternivå i Odnoklassniki

Under experimenten fick vi reda på att hsfc visar bäst resultat när klassen 1:2 av icke-prioriterad batch/tomgångstrafik är begränsad på minionmaskiner till högst ett visst fritt körfält. Annars har icke-prioriterad trafik för stor inverkan på fördröjningen av prod-uppgifter. miniond bestämmer den aktuella mängden ledig bandbredd varje sekund, och mäter den genomsnittliga trafikförbrukningen för alla prod-uppgifter för en given minion One-cloud - operativsystem på datacenternivå i Odnoklassniki och subtrahera den från nätverksgränssnittets bandbredd One-cloud - operativsystem på datacenternivå i Odnoklassniki med liten marginal, d.v.s.

One-cloud - operativsystem på datacenternivå i Odnoklassniki

Band definieras oberoende för inkommande och utgående trafik. Och enligt de nya värdena konfigurerar miniond om den icke-prioriterade klassgränsen 1:2.

Således implementerade vi alla tre isoleringsklasserna: prod, batch och inaktiv. Dessa klasser påverkar i hög grad prestationsegenskaperna för uppgifter. Därför bestämde vi oss för att placera detta attribut högst upp i hierarkin, så att när man tittar på namnet på den hierarkiska kön skulle det omedelbart framgå vad vi har att göra med:

One-cloud - operativsystem på datacenternivå i Odnoklassniki

Alla våra vänner webb и musik fronterna placeras sedan i hierarkin under prod. Till exempel, under batch, låt oss placera tjänsten musikkatalog, som med jämna mellanrum sammanställer en katalog med spår från en uppsättning mp3-filer som laddats upp till Odnoklassniki. Ett exempel på en tjänst under inaktiv skulle vara musik transformator, som normaliserar musikvolymen.

Med de extra raderna borttagna igen kan vi skriva våra tjänstnamn plattare genom att lägga till uppgiftsisoleringsklassen i slutet av det fullständiga tjänstnamnet: web.front.prod, catalog.music.batch, transformator.musik.tom.

Och nu, när vi tittar på namnet på tjänsten, förstår vi inte bara vilken funktion den utför, utan också dess isoleringsklass, vilket betyder dess kritikalitet, etc.

Allt är bra, men det finns en bitter sanning. Det är omöjligt att helt isolera uppgifter som körs på en maskin.

Vad vi lyckades uppnå: om batch konsumerar intensivt endast CPU-resurser, då gör den inbyggda Linux CPU-schemaläggaren sitt jobb mycket bra, och det finns praktiskt taget ingen inverkan på prod-uppgiften. Men om den här batchuppgiften börjar arbeta aktivt med minne, uppträder redan den ömsesidiga påverkan. Detta händer eftersom prod-uppgiften "tvättas ut" ur processorns minnescache - som ett resultat ökar cachemissarna och processorn bearbetar prod-uppgiften långsammare. En sådan batchuppgift kan öka latensen för vår typiska produktbehållare med 10 %.

Att isolera trafik är ännu svårare på grund av att moderna nätverkskort har en intern kö av paket. Om paketet från batchuppgiften kommer dit först, kommer det att vara det första som överförs över kabeln, och ingenting kan göras åt det.

Dessutom har vi hittills bara lyckats lösa problemet med att prioritera TCP-trafik: hsfc-metoden fungerar inte för UDP. Och även i fallet med TCP-trafik, om batchuppgiften genererar mycket trafik, ger detta också cirka 10 % ökning av prod-uppgiftens fördröjning.

feltolerans

Ett av målen när man utvecklade ett moln var att förbättra Odnoklassnikis feltolerans. Därför skulle jag härnäst vilja överväga mer i detalj möjliga scenarier av fel och olyckor. Låt oss börja med ett enkelt scenario - ett containerfel.

Själva behållaren kan misslyckas på flera sätt. Detta kan vara något slags experiment, bugg eller fel i manifestet, på grund av vilket prod-uppgiften börjar konsumera mer resurser än vad som anges i manifestet. Vi hade ett fall: en utvecklare implementerade en komplex algoritm, omarbetade den många gånger, övertänkte sig själv och blev så förvirrad att problemet till slut fick en loop på ett mycket icke-trivialt sätt. Och eftersom prod-uppgiften har högre prioritet än alla andra på samma minions, började den konsumera alla tillgängliga processorresurser. I det här läget räddade isolering, eller snarare CPU-tidskvoten, dagen. Om en uppgift tilldelas en kvot kommer uppgiften inte att förbruka mer. Därför märkte inte batch- och andra prod-uppgifter som kördes på samma maskin något.

Det andra möjliga problemet är att behållaren faller. Och här räddar omstartspolicyer oss, alla känner till dem, Docker själv gör ett bra jobb. Nästan alla prod-uppgifter har en alltid omstartspolicy. Ibland använder vi on_failure för batchuppgifter eller för att felsöka prodcontainrar.

Vad kan du göra om en hel minion är otillgänglig?

Kör självklart behållaren på en annan maskin. Det intressanta här är vad som händer med IP-adresserna som tilldelats behållaren.

Vi kan tilldela containrar samma IP-adresser som minionmaskinerna som dessa containrar körs på. Sedan, när behållaren startas på en annan dator, ändras dess IP-adress, och alla klienter måste förstå att behållaren har flyttats, och nu måste de gå till en annan adress, vilket kräver en separat Service Discovery-tjänst.

Service Discovery är bekvämt. Det finns många lösningar på marknaden med olika grader av feltolerans för att organisera ett serviceregister. Ofta implementerar sådana lösningar lastbalanseringslogik, lagrar ytterligare konfiguration i form av KV-lagring, etc.
Vi vill dock undvika behovet av att implementera ett separat register, eftersom det skulle innebära att man inför ett kritiskt system som används av alla tjänster i produktionen. Detta innebär att detta är en potentiell punkt av misslyckande, och du måste välja eller utveckla en mycket feltålig lösning, som uppenbarligen är mycket svår, tidskrävande och dyr.

Och ytterligare en stor nackdel: för att vår gamla infrastruktur skulle fungera med den nya skulle vi behöva skriva om absolut alla uppgifter för att använda något slags Service Discovery-system. Det finns MYCKET arbete, och på vissa ställen är det nästan omöjligt när det kommer till lågnivåenheter som fungerar på OS-kärnnivå eller direkt med hårdvaran. Implementering av denna funktionalitet med hjälp av etablerade lösningsmönster, som t.ex sidvagn skulle på vissa ställen innebära en extra belastning, på andra - en komplikation av driften och ytterligare felscenarier. Vi ville inte komplicera saker och ting, så vi bestämde oss för att göra användningen av Service Discovery valfri.

I ett moln följer IP:n behållaren, det vill säga varje uppgiftsinstans har sin egen IP-adress. Den här adressen är "statisk": den tilldelas varje instans när tjänsten först skickas till molnet. Om en tjänst hade ett annat antal instanser under sin livstid kommer den i slutändan att tilldelas lika många IP-adresser som det fanns maximalt antal instanser.

Därefter ändras inte dessa adresser: de tilldelas en gång och fortsätter att existera under hela tjänstens livslängd i produktion. IP-adresser följer behållare över nätverket. Om behållaren överförs till en annan minion, kommer adressen att följa den.

Således ändras mappningen av ett tjänstnamn till dess lista över IP-adresser mycket sällan. Om du tittar igen på namnen på tjänsteinstanserna som vi nämnde i början av artikeln (1.ok-web.group1.web.front.prod, 2.ok-web.group1.web.front.prod, …), kommer vi att märka att de liknar de FQDN som används i DNS. Det stämmer, för att mappa namnen på tjänsteinstanser till deras IP-adresser använder vi DNS-protokollet. Dessutom returnerar denna DNS alla reserverade IP-adresser för alla behållare - både pågående och stoppade (låt oss säga att tre repliker används, och vi har fem adresser reserverade där - alla fem kommer att returneras). Klienter, som har fått denna information, kommer att försöka upprätta en koppling till alla fem replikerna - och på så sätt fastställa vilka som fungerar. Detta alternativ för att fastställa tillgänglighet är mycket mer tillförlitligt, det involverar varken DNS eller Service Discovery, vilket innebär att det inte finns några svåra problem att lösa för att säkerställa relevansen av information och feltolerans för dessa system. Dessutom, i kritiska tjänster som driften av hela portalen beror på, kan vi inte använda DNS alls, utan helt enkelt ange IP-adresser i konfigurationen.

Att implementera sådan IP-överföring bakom behållare kan vara otrivialt - och vi ska titta på hur det fungerar med följande exempel:

One-cloud - operativsystem på datacenternivå i Odnoklassniki

Låt oss säga att one-cloud master ger kommandot till minion M1 att köra 1.ok-web.group1.web.front.prod med adress 1.1.1.1. Fungerar på en minion FÅGEL, som annonserar denna adress till speciella servrar ruttreflektor. De senare har en BGP-session med nätverkshårdvaran, till vilken adressvägen 1.1.1.1 på M1 översätts. M1 dirigerar paket inuti behållaren med Linux. Det finns tre ruttreflektorservrar, eftersom detta är en mycket kritisk del av en-moln-infrastrukturen - utan dem kommer nätverket i ett-moln inte att fungera. Vi placerar dem i olika rack, om möjligt placerade i olika rum i datacentret, för att minska sannolikheten för att alla tre misslyckas samtidigt.

Låt oss nu anta att kopplingen mellan one-cloud master och M1 minion är förlorad. One-cloud master kommer nu att agera utifrån antagandet att M1 har misslyckats helt. Det vill säga, det kommer att ge kommandot till M2 minion att starta web.group1.web.front.prod med samma adress 1.1.1.1. Nu har vi två motstridiga rutter på nätverket för 1.1.1.1: på M1 och på M2. För att lösa sådana konflikter använder vi Multi Exit Discriminator, som specificeras i BGP-meddelandet. Detta är en siffra som visar vikten av den annonserade rutten. Bland de motstridiga rutterna kommer rutten med det lägre MED-värdet att väljas. One-cloud-mastern stöder MED som en integrerad del av IP-adresser för behållare. För första gången skrivs adressen med ett tillräckligt stort MED = 1 000 000. I situationen med en sådan nödcontaineröverföring minskar befälhavaren MED, och M2 kommer redan att få kommandot att annonsera adressen 1.1.1.1 med MED = 999 999. Den instans som körs på M1 kommer att finnas kvar i det här fallet finns det ingen koppling, och hans vidare öde intresserar oss lite tills förbindelsen med befälhavaren återställs, då han kommer att stoppas som en gammal take.

olyckor

Alla datacenterhanteringssystem hanterar alltid mindre fel på ett acceptabelt sätt. Containerspill är normen nästan överallt.

Låt oss titta på hur vi hanterar en nödsituation, till exempel ett strömavbrott i ett eller flera rum i ett datacenter.

Vad betyder en olycka för ett datacenterhanteringssystem? Först och främst är detta ett massivt engångsfel för många maskiner, och kontrollsystemet behöver migrera många behållare samtidigt. Men om katastrofen är mycket stor, kan det hända att alla uppgifter inte kan omfördelas till andra minions, eftersom resurskapaciteten för datacentret sjunker under 100% av belastningen.

Ofta åtföljs olyckor av fel på kontrollskiktet. Detta kan hända på grund av fel på dess utrustning, men oftare på grund av att olyckor inte testas, och själva kontrollskiktet faller på grund av den ökade belastningen.

Vad kan du göra åt allt detta?

Massmigreringar innebär att det förekommer ett stort antal aktiviteter, migrationer och distributioner i infrastrukturen. Var och en av migreringarna kan ta lite tid som krävs för att leverera och packa upp behållarbilder till hantlangare, starta och initiera behållare etc. Därför är det önskvärt att viktigare uppgifter startas före mindre viktiga.

Låt oss återigen titta på hierarkin av tjänster vi är bekanta med och försöka bestämma vilka uppgifter vi vill köra först.

One-cloud - operativsystem på datacenternivå i Odnoklassniki

Naturligtvis är det de processer som är direkt involverade i att behandla användarförfrågningar, d.v.s. prod. Vi indikerar detta med placeringsprioritet — ett nummer som kan tilldelas kön. Om en kö har högre prioritet placeras dess tjänster först.

På prod ger vi högre prioritet, 0; på batch - lite lägre, 100; på tomgång - ännu lägre, 200. Prioriteringar tillämpas hierarkiskt. Alla uppgifter lägre i hierarkin kommer att ha motsvarande prioritet. Om vi ​​vill att cacher inuti prod ska lanseras före frontends, så tilldelar vi prioriteringar till cache = 0 och till främre subköer = 1. Om vi ​​till exempel vill att huvudportalen ska lanseras från fronterna först, och endast musikfronten då kan vi tilldela en lägre prioritet till den senare - 10.

Nästa problem är bristen på resurser. Så en stor mängd utrustning, hela hallar i datacentret, misslyckades, och vi återlanserade så många tjänster att det nu inte finns tillräckligt med resurser för alla. Du måste bestämma vilka uppgifter du ska offra för att hålla de viktigaste kritiska tjänsterna igång.

One-cloud - operativsystem på datacenternivå i Odnoklassniki

Till skillnad från placeringsprioritet kan vi inte urskillningslöst offra alla batchuppgifter, några av dem är viktiga för driften av portalen. Därför har vi markerat separat företrädesrätt uppgifter. När den placeras kan en uppgift med högre prioritet föregripa, d.v.s. stoppa, en uppgift med lägre prioritet om det inte finns fler lediga minions. I det här fallet kommer en uppgift med låg prioritet troligen att förbli oplacerad, d.v.s. det kommer inte längre att finnas en lämplig minion för det med tillräckligt med lediga resurser.

I vår hierarki är det mycket enkelt att ange en preemption-prioritet så att prod- och batch-uppgifter föregriper eller stoppar lediga uppgifter, men inte varandra, genom att ange en prioritet för inaktiv lika med 200. Precis som i fallet med placeringsprioritet, vi kan använda vår hierarki för att beskriva mer komplexa regler. Låt oss till exempel indikera att vi offrar musikfunktionen om vi inte har tillräckligt med resurser för huvudwebbportalen, och sätter prioriteten för motsvarande noder lägre: 10.

Hela DC-olyckor

Varför kan hela datacentret misslyckas? Element. Var ett bra inlägg orkanen påverkade arbetet i datacentret. Elementen kan betraktas som hemlösa som en gång brände optiken i grenröret, och datacentret tappade helt kontakten med andra sajter. Orsaken till misslyckandet kan också vara en mänsklig faktor: operatören kommer att utfärda ett sådant kommando att hela datacentret faller. Detta kan hända på grund av en stor bugg. I allmänhet är det inte ovanligt att datacenter kollapsar. Detta händer oss en gång varannan månad.

Och det här är vad vi gör för att hindra någon från att twittra #alive.

Den första strategin är isolering. Varje enmolninstans är isolerad och kan hantera maskiner i endast ett datacenter. Det vill säga att förlusten av ett moln på grund av buggar eller felaktiga operatörskommandon är förlusten av endast ett datacenter. Vi är redo för detta: vi har en redundanspolicy där repliker av applikationen och data finns i alla datacenter. Vi använder feltoleranta databaser och testar regelbundet för fel.
Sedan idag har vi fyra datacenter, det betyder fyra separata, helt isolerade instanser av ett moln.

Detta tillvägagångssätt skyddar inte bara mot fysiska fel, utan kan också skydda mot operatörsfel.

Vad mer kan man göra med den mänskliga faktorn? När en operatör ger molnet något konstigt eller potentiellt farligt kommando kan han plötsligt bli ombedd att lösa ett litet problem för att se hur väl han tänkte. Till exempel, om detta är någon form av massstopp av många repliker eller bara ett konstigt kommando - att minska antalet repliker eller ändra namnet på bilden, och inte bara versionsnumret i det nya manifestet.

One-cloud - operativsystem på datacenternivå i Odnoklassniki

Resultat av

Utmärkande egenskaper för ett moln:

  • Hierarkiskt och visuellt namnschema för tjänster och containrar, vilket gör att du mycket snabbt kan ta reda på vad uppgiften är, vad den avser och hur den fungerar och vem som ansvarar för den.
  • Vi tillämpar vår teknik för att kombinera produkt- och batch-uppgifter på minions för att förbättra effektiviteten i maskindelning. Istället för cpuset använder vi CPU-kvoter, andelar, CPU-schemaläggare och Linux QoS.
  • Det var inte möjligt att helt isolera behållare som körde på samma maskin, men deras ömsesidiga inflytande förblir inom 20 %.
  • Att organisera tjänster i en hierarki hjälper till med automatisk katastrofåterställning med hjälp av placerings- och företrädesprioriteringar.

FAQ

Varför tog vi inte en färdig lösning?

  • Olika klasser av uppgiftsisolering kräver olika logik när de placeras på minions. Om prod-uppgifter kan placeras genom att helt enkelt reservera resurser, måste batch- och lediga uppgifter placeras för att spåra det faktiska utnyttjandet av resurser på minion-maskiner.
  • Behovet av att ta hänsyn till resurser som förbrukas av uppgifter, såsom:
    • nätverksbandbredd;
    • typer och "spindlar" av skivor.
  • Behovet av att ange prioriteringar för tjänster under nödsituationer, rättigheter och kvoter för kommandon för resurser, vilket löses med hierarkiska köer i ett moln.
  • Behovet av att ha mänskliga namn på containrar för att minska responstiden på olyckor och tillbud
  • Omöjligheten av en engångsimplementering av Service Discovery; behovet av att samexistera under lång tid med uppgifter på hårdvaruvärdar - något som löses genom att "statiska" IP-adresser följer efter behållare, och som en konsekvens av detta behovet av unik integration med en stor nätverksinfrastruktur.

Alla dessa funktioner skulle kräva betydande modifieringar av befintliga lösningar för att passa oss, och efter att ha bedömt mängden arbete insåg vi att vi kunde utveckla vår egen lösning med ungefär samma arbetskostnader. Men din lösning blir mycket lättare att driva och utveckla – den innehåller inga onödiga abstraktioner som stödjer funktionalitet som vi inte behöver.

Till er som läser de sista raderna, tack för ert tålamod och uppmärksamhet!

Källa: will.com

Lägg en kommentar