Containers, mikrotjänster och servicenät

På internet heap artiklar о servicenät (servicenät), och här är en till. Hurra! Men varför? Sedan vill jag uttrycka min åsikt att det hade varit bättre om servicenät dök upp för 10 år sedan, innan tillkomsten av containerplattformar som Docker och Kubernetes. Jag säger inte att min synvinkel är bättre eller sämre än andra, men eftersom servicenät är ganska komplexa djur, kommer flera synpunkter att hjälpa till att bättre förstå dem.

Jag ska prata om dotCloud-plattformen, som byggdes på över hundra mikrotjänster och stödde tusentals containeriserade applikationer. Jag kommer att förklara de utmaningar vi stod inför när vi utvecklade och lanserade det, och hur servicenätverk kunde (eller inte kunde) hjälpa.

Historia om dotCloud

Jag har skrivit om historien om dotCloud och arkitekturvalen för den här plattformen, men jag har inte pratat så mycket om nätverkslagret. Om du inte vill dyka in i läsning sista artikeln om dotCloud, här är kärnan i ett nötskal: det är en PaaS-plattform-som-en-tjänst som tillåter kunder att köra ett brett utbud av applikationer (Java, PHP, Python...), med stöd för ett brett utbud av data tjänster (MongoDB, MySQL, Redis...) och ett arbetsflöde som Heroku: Du laddar upp din kod till plattformen, den bygger behållarbilder och distribuerar dem.

Jag kommer att berätta hur trafiken dirigerades till dotCloud-plattformen. Inte för att det var speciellt coolt (även om systemet fungerade bra för sin tid!), utan främst för att med moderna verktyg en sådan design lätt kan implementeras på kort tid av ett blygsamt team om de behöver ett sätt att dirigera trafik mellan ett gäng av mikrotjänster eller en massa applikationer. På så sätt kan du jämföra alternativen: vad händer om du utvecklar allt själv eller använder ett befintligt servicenät. Standardvalet är att göra det själv eller köpa det.

Trafikdirigering för värdbaserade applikationer

Applikationer på dotCloud kan exponera HTTP- och TCP-slutpunkter.

HTTP-slutpunkter dynamiskt läggs till i lastbalanseringsklusterkonfigurationen Hipache. Detta liknar vad resurser gör idag Ingress i Kubernetes och en lastbalanserare som Traefik.

Klienter ansluter till HTTP-slutpunkter via lämpliga domäner, förutsatt att domännamnet pekar på dotCloud-belastningsutjämnare. Inget speciellt.

TCP-slutpunkter associerat med ett portnummer, som sedan skickas till alla behållare i den stacken via miljövariabler.

Klienter kan ansluta till TCP-slutpunkter med lämpligt värdnamn (något som gateway-X.dotcloud.com) och portnummer.

Detta värdnamn löser sig till "nats"-serverklustret (ej relaterat till NATS), som kommer att dirigera inkommande TCP-anslutningar till rätt behållare (eller, i fallet med lastbalanserade tjänster, till rätt behållare).

Om du är bekant med Kubernetes kommer detta förmodligen att påminna dig om tjänster Nodport.

Det fanns inga motsvarande tjänster på dotCloud-plattformen ClusterIP: För enkelhetens skull nåddes tjänsterna på samma sätt både från insidan och utanför plattformen.

Allt var helt enkelt organiserat: de initiala implementeringarna av HTTP- och TCP-routningsnätverk var förmodligen bara några hundra rader Python vardera. Enkla (jag skulle säga naiva) algoritmer som förfinades i takt med att plattformen växte och ytterligare krav dök upp.

Omfattande omfaktorisering av befintlig kod krävdes inte. Särskilt, 12 faktor appar kan direkt använda adressen som erhålls genom miljövariabler.

Hur skiljer sig detta från ett modernt servicenät?

Begränsad synlighet. Vi hade inga mätvärden för TCP-routingnätet alls. När det kommer till HTTP-routing introducerade senare versioner detaljerade HTTP-mått med felkoder och svarstider, men moderna servicenät går ännu längre, vilket ger integration med statistikinsamlingssystem som Prometheus, till exempel.

Synlighet är viktig inte bara ur ett operativt perspektiv (för att hjälpa till att felsöka problem), utan också när nya funktioner släpps. Det handlar om säkert blågrön utbyggnad и kanariefågel utplacering.

Routing effektivitet är också begränsad. I dotCloud-ruttnätet måste all trafik gå genom ett kluster av dedikerade routingnoder. Detta innebar potentiellt att korsa flera AZ-gränser (Availability Zone) och öka latensen avsevärt. Jag minns felsökningskoden som gjorde över hundra SQL-frågor per sida och öppnade en ny anslutning till SQL-servern för varje fråga. När den körs lokalt laddas sidan direkt, men i dotCloud tar det några sekunder att ladda eftersom varje TCP-anslutning (och efterföljande SQL-fråga) tar tiotals millisekunder. I det här specifika fallet löste ihållande anslutningar problemet.

Moderna servicenät är bättre på att hantera sådana problem. Först och främst kontrollerar de att anslutningar är dirigerade i källan. Det logiska flödet är detsamma: клиент → меш → сервис, men nu fungerar mesh lokalt och inte på fjärrnoder, så anslutningen клиент → меш är lokal och mycket snabb (mikrosekunder istället för millisekunder).

Moderna servicenät implementerar också smartare lastbalanseringsalgoritmer. Genom att övervaka tillståndet hos backends kan de skicka mer trafik till snabbare backends, vilket resulterar i förbättrad övergripande prestanda.

Безопасность bättre också. DotCloud routing mesh körde helt på EC2 Classic och krypterade inte trafik (baserat på antagandet att om någon lyckades sätta en sniffer på EC2 nätverkstrafik så hade du redan stora problem). Moderna tjänstenät skyddar transparent all vår trafik, till exempel med ömsesidig TLS-autentisering och efterföljande kryptering.

Dirigera trafik för plattformstjänster

Okej, vi har diskuterat trafik mellan applikationer, men hur är det med själva dotCloud-plattformen?

Själva plattformen bestod av ett hundratal mikrotjänster med ansvar för olika funktioner. Några accepterade förfrågningar från andra, och några var bakgrundsarbetare som kopplade till andra tjänster men som inte accepterade anslutningar själva. I vilket fall som helst måste varje tjänst känna till ändpunkterna för de adresser den behöver ansluta till.

Många tjänster på hög nivå kan använda det routingnät som beskrivs ovan. Faktum är att många av dotClouds mer än hundra mikrotjänster har distribuerats som vanliga applikationer på själva dotCloud-plattformen. Men ett litet antal lågnivåtjänster (särskilt de som implementerar detta routingnät) behövde något enklare, med färre beroenden (eftersom de inte kunde lita på sig själva för att fungera - det gamla goda problemet med kyckling och ägg).

Dessa verksamhetskritiska tjänster på låg nivå distribuerades genom att köra containrar direkt på några nyckelnoder. I det här fallet användes inte standardplattformstjänster: länkare, schemaläggare och löpare. Vill man jämföra med moderna containerplattformar är det som att köra ett kontrollplan med docker run direkt på noderna, istället för att delegera uppgiften till Kubernetes. Det är ganska likt i konceptet statiska moduler (pods), som den använder kubeadm eller bootkube när du startar ett fristående kluster.

Dessa tjänster exponerades på ett enkelt och grovt sätt: en YAML-fil listade deras namn och adresser; och varje klient var tvungen att ta en kopia av denna YAML-fil för distribution.

Å ena sidan är den extremt tillförlitlig eftersom den inte kräver stöd av en extern nyckel/värdebutik som Zookeeper (kom ihåg att etcd eller Consul inte fanns vid den tiden). Å andra sidan gjorde det det svårt att flytta tjänster. Varje gång en flytt gjordes skulle alla klienter få en uppdaterad YAML-fil (och eventuellt starta om). Inte särskilt bekvämt!

Därefter började vi implementera ett nytt schema, där varje klient kopplade till en lokal proxyserver. Istället för en adress och port behöver den bara känna till tjänstens portnummer och ansluta via localhost. Den lokala proxyn hanterar denna anslutning och vidarebefordrar den till den faktiska servern. Nu, när du flyttar backend till en annan maskin eller skalar, istället för att uppdatera alla klienter, behöver du bara uppdatera alla dessa lokala proxyservrar; och en omstart krävs inte längre.

(Det var också planerat att kapsla in trafiken i TLS-anslutningar och sätta ytterligare en proxyserver på den mottagande sidan, samt verifiera TLS-certifikat utan deltagande av den mottagande tjänsten, som är konfigurerad att acceptera anslutningar endast på localhost. Mer om detta senare).

Detta är väldigt likt SmartStack från Airbnb, men den betydande skillnaden är att SmartStack implementeras och distribueras till produktion, medan dotClouds interna routingsystem lades på hyllan när dotCloud blev Docker.

Jag anser personligen att SmartStack är en av föregångarna till system som Istio, Linkerd och Consul Connect eftersom de alla följer samma mönster:

  • Kör en proxy på varje nod.
  • Klienter ansluter till proxyn.
  • Kontrollplanet uppdaterar proxykonfigurationen när backends ändras.
  • ... Vinst!

Modern implementering av ett servicenät

Om vi ​​behövde implementera ett liknande rutnät idag skulle vi kunna använda liknande principer. Konfigurera till exempel en intern DNS-zon genom att mappa tjänstnamn till adresser i utrymmet 127.0.0.0/8. Kör sedan HAProxy på varje nod i klustret, acceptera anslutningar vid varje tjänstadress (i det subnätet 127.0.0.0/8) och omdirigera/balansera belastningen till lämpliga backends. HAProxy-konfiguration kan kontrolleras confd, så att du kan lagra backend-information i etcd eller Consul och automatiskt skicka uppdaterad konfiguration till HAProxy när det behövs.

Det är ungefär så Istio fungerar! Men med några skillnader:

  • Används Envoy Proxy istället för HAProxy.
  • Lagrar backend-konfiguration via Kubernetes API istället för etcd eller Consul.
  • Tjänster tilldelas adresser på det interna subnätet (Kubernetes ClusterIP-adresser) istället för 127.0.0.0/8.
  • Har en extra komponent (Citadel) för att lägga till ömsesidig TLS-autentisering mellan klienten och servrarna.
  • Stöder nya funktioner som kretsbrytning, distribuerad spårning, kanariefågel, etc.

Låt oss ta en snabb titt på några av skillnaderna.

Envoy Proxy

Envoy Proxy skrevs av Lyft [Ubers konkurrent på taximarknaden - ca. körfält]. Det liknar på många sätt andra proxyservrar (t.ex. HAProxy, Nginx, Traefik...), men Lyft skrev sina för att de behövde funktioner som andra proxyer saknade, och det verkade smartare att göra en ny snarare än att utöka den befintliga.

Envoy kan användas på egen hand. Om jag har en specifik tjänst som behöver ansluta till andra tjänster kan jag konfigurera den för att ansluta till Envoy, och sedan dynamiskt konfigurera och konfigurera om Envoy med platsen för andra tjänster, samtidigt som jag får en hel del fantastiska ytterligare funktioner, som synlighet. Istället för ett anpassat klientbibliotek eller att injicera samtalsspår i koden skickar vi trafik till Envoy och den samlar in mätvärden åt oss.

Men Envoy kan också arbeta som dataplan (dataplan) för servicenätet. Det betyder att Envoy nu är konfigurerad för detta servicenät kontrollplan (kontrollplan).

Kontrollplan

För kontrollplanet förlitar sig Istio på Kubernetes API. Detta skiljer sig inte mycket från att använda confd, som förlitar sig på etcd eller Consul för att se uppsättningen nycklar i datalagret. Istio använder Kubernetes API för att visa en uppsättning Kubernetes-resurser.

Mellan detta och då: Jag tyckte personligen att det här var användbart Kubernetes API-beskrivningsom lyder:

Kubernetes API Server är en "dum server" som erbjuder lagring, versionshantering, validering, uppdatering och semantik för API-resurser.

Istio är designad för att fungera med Kubernetes; och om du vill använda den utanför Kubernetes, måste du köra en instans av Kubernetes API-server (och etcd-hjälptjänsten).

Serviceadresser

Istio förlitar sig på ClusterIP-adresser som Kubernetes tilldelar, så Istio-tjänster får en intern adress (inte i intervallet 127.0.0.0/8).

Trafik till ClusterIP-adressen för en specifik tjänst i ett Kubernetes-kluster utan Istio fångas upp av kube-proxy och skickas till den proxyns backend. Om du är intresserad av de tekniska detaljerna, ställer kube-proxy upp iptables-regler (eller IPVS-lastbalanserare, beroende på hur det är konfigurerat) för att skriva om destinations-IP-adresserna för anslutningar som går till ClusterIP-adressen.

När Istio väl har installerats på ett Kubernetes-kluster ändras ingenting förrän det uttryckligen aktiveras för en given konsument, eller till och med hela namnområdet, genom att introducera en container sidecar i anpassade kapslar. Den här behållaren kommer att snurra upp en instans av Envoy och ställa in en uppsättning iptables-regler för att fånga upp trafik som går till andra tjänster och omdirigera den trafiken till Envoy.

När den är integrerad med Kubernetes DNS betyder det att vår kod kan ansluta med tjänstens namn och allt "bara fungerar." Med andra ord, vår kod utfärdar frågor som http://api/v1/users/4242sedan api lösa begäran om 10.97.105.48, kommer iptables-reglerna att fånga upp anslutningar från 10.97.105.48 och vidarebefordra dem till den lokala Envoy-proxyn, och den lokala proxyn kommer att vidarebefordra begäran till det faktiska backend-API:et. Puh!

Ytterligare krusiduller

Istio tillhandahåller även end-to-end-kryptering och autentisering via mTLS (ömsesidig TLS). En komponent som kallas Citadel.

Det finns också en komponent Mixer, som envoy kan begära för av varje begäran om att fatta ett speciellt beslut om den begäran beroende på olika faktorer såsom rubriker, backend-belastning, etc... (oroa dig inte: det finns många sätt att hålla Mixer igång, och även om den kraschar kommer Envoy att fortsätta att arbeta böter som ombud).

Och, naturligtvis, nämnde vi synlighet: Envoy samlar in en enorm mängd mätvärden samtidigt som de tillhandahåller distribuerad spårning. I en mikrotjänstarkitektur, om en enskild API-begäran måste passera genom mikrotjänsterna A, B, C och D, kommer distribuerad spårning vid inloggning att lägga till en unik identifierare till begäran och lagra denna identifierare genom underförfrågningar till alla dessa mikrotjänster, vilket tillåter alla relaterade samtal som ska fångas upp, förseningar osv.

Utveckla eller köp

Istio har rykte om sig att vara komplex. Däremot är det relativt enkelt att bygga routingnätet som jag beskrev i början av det här inlägget med hjälp av befintliga verktyg. Så, är det vettigt att skapa ditt eget servicenät istället?

Om vi ​​har blygsamma behov (vi behöver inte synlighet, en strömbrytare och andra finesser), kommer tankarna på att utveckla vårt eget verktyg. Men om vi använder Kubernetes kanske det inte ens behövs eftersom Kubernetes redan tillhandahåller grundläggande verktyg för tjänsteupptäckt och lastbalansering.

Men om vi har avancerade krav, så verkar "köpa" ett servicenät vara ett mycket bättre alternativ. (Detta är inte alltid ett "köp" eftersom Istio är öppen källkod, men vi måste fortfarande investera tid för att förstå, distribuera och hantera det.)

Ska jag välja Istio, Linkerd eller Consul Connect?

Hittills har vi bara pratat om Istio, men detta är inte det enda servicenätet. Populärt alternativ - Linkerd, och det finns mer Konsul Connect.

Vad ska man välja?

Ärligt talat, jag vet inte. För närvarande anser jag mig inte vara kompetent nog att svara på denna fråga. Det finns några intressant artiklar med en jämförelse av dessa verktyg och till och med riktmärken.

Ett lovande tillvägagångssätt är att använda ett verktyg som SuperGloo. Den implementerar ett abstraktionslager för att förenkla och förena API:erna som exponeras av tjänstnät. Istället för att lära oss de specifika (och, enligt min mening, relativt komplexa) API:er för olika tjänstenät, kan vi använda SuperGloos enklare konstruktioner - och enkelt byta från en till en annan, som om vi hade ett mellanliggande konfigurationsformat som beskriver HTTP-gränssnitt och backends som kan att generera den faktiska konfigurationen för Nginx, HAProxy, Traefik, Apache...

Jag har pysslat lite med Istio och SuperGloo, och i nästa artikel vill jag visa hur man lägger till Istio eller Linkerd till ett befintligt kluster med hjälp av SuperGloo, och hur det senare får jobbet gjort, det vill säga låter dig byta från ett tjänstnät till ett annat utan att skriva över konfigurationer.

Källa: will.com

Lägg en kommentar