Micro services: Det er ikke (kun) størrelsen der er vigtigt, det er (også) hvordan du bruger dem – Del 2

Flattr this!

Del 1 – Microservices: Det er ikke (kun) størrelsen der er vigtigt, det er (også) hvordan du bruger dem
Del 3 – Microservices: Det er ikke (kun) størrelsen der er vigtigt, det er (også) hvordan du bruger dem
Del 4 – Microservices: Det er ikke (kun) størrelsen der er vigtigt, det er (også) hvordan du bruger dem

English version: http://www.tigerteam.dk/2014/micro-services-its-not-only-the-size-that-matters-its-also-how-you-use-them-part-2/

Micro services: Det er ikke (kun) størrelsen der er vigtigt, det er (også) hvordan du bruger dem – Del 1 diskuterede vi at anvendelsen af antal linier kode er en meget dårligmålestok for om en Service har den rette størrelse og helt ubrugelig til at vurdere om en service har det rigtige ansvar.

Vi diskuterede også problemerne ved anvendelse af 2 vejs (synkron) kommunikation mellem vores services medfører hård kobling og andre ubehageligheder:

  • Det medfører kommunikations mæssig kobling (data og logik ligger ikke altid i samme service)
    • Dette medfører også kontraktuel, data og funktions mæssig kobling samt høj latens tid (pga. netværks kommunikation)
  • Lagdelt kobling (persistens ligger ikke altid i samme service)
  • Temporal kobling (vores service kan ikke fungere hvis den ikke kan kommunikere med de services den afhænger af)
  • Det at vores service afhænger af andre services underminerer dens autonomi og gør den mere skrøbelig
  • Alt dette medfører også behovet kompleks kompensations logik pga. manglen på reliable messaging og transaktioner.
Genbrugelige services, 2 vejs (synkron) kommunikation og kopling

Genbrugelige services, 2 vejs (synkron) kommunikation og kopling

Hvis vi kombinerer (synkron) 2 vejs kommunikation med små/mikro services, f.eks. efter tesen 1 klasse = 1 service, er vi reelt sendt tilbage til 1990’erne med Corba og J2EE og distribuerede objekter. Desværre ser det ud til at nye generationer af udviklere, der ikke oplevede distribuerede objekter og derfor heller ikke var med til at indse hvor dårlig ideen var, er igang med at gentage historien. Denne gang blot med nye teknologier, f.eks. HTTP i stedet for RMI eller IIOP.
Jay Kreps opsummerede den nuværende Microservice tilgang, med anvendelse af 2 vejs kommunikation, meget rammende:

Jay Kreps - Microservice == distributed objects for hipsters (what could possibly go wrong?)

Jay Kreps – Microservice == distributed objects for hipsters (what could possibly go wrong?)

Blot fordi at Microservices benytter HTTP, JSON og REST får ikke ulemperne ved remote kommunikation til at forsvinde. Ulemperne, som nybegyndere indenfor distribueret nemt overser, er opsummeret i de 8 fejlslutninger om distribueret computing (8 fallacies of distributed computing):

De tror:

  1. Netværket er pålideligt
    Men, en hver der har prøvet miste forbindelsen til en server eller til internettet  fordi netværks routere, switche, Wifi forbindelser, etc. er mere eller mindre upålidelige. Selv i et perfekt setup, vil man kunne opleve nedbrud i netværks udstyr fra tid til anden – se blot Netværksfejl hos IBM skyld i Dankort-kaosFejlen fundet: Firewalls lagde Post Danmarks netværk ned i 11 timer eller Nedbrud i CSC’s netværk lammer Skats tastselv-service
  2. Latenstid er nul
    Hvad man nemt overser er at det er meget dyrere at lave et netværkskald end at lave et tilsvarende in-process kald. Båndbredden er også mere begrænset og latenstiden måles i milli-sekunder istedet for nano-sekunder over netværket. Jo flere kald der skal udføres sekventielt desto værre bliver den samlede latenstid
  3. Båndbredden er uendelig
    I virkeligheden er netværks båndbredden, selv på et 10 GBit netværk, er meget lavere end hvis samme kald blev foretaget in-memory/in-process. Desto flere data og kald der bliver foretaget og jo mindre vores services bliver, desto flere kald vil det medføre, desto større indflydelse har det på den ledige båndbredde
  4. Netværket er sikkert
    Jeg behøver vel blot at sige NSA? 😉
  5. Topologi ændres ikke
    Virkeligheden er anderledes. Services der er deployet til produktion vil opleve at miljøet løbende ændrer sig. Gamle servere bliver opgraderet eller flyttet (skifter evt. IP adresse), netværks udstyr bliver skiftet eller rekonfigureret, firewalls ændrer konfiguration, etc.
  6. Der er én administrator
    I enhver større installation vil der være flere administratorer: Netværks administratorer, Windows admin, Unix admin, DB admin, etc.
  7. Transport omkostningerne er nul
    Et simpelt eksempel på at dette er en fejlslutning er eksempelvis omkostningerne ved at serialisere/deserialisere fra den interne repræsentation til/fra JSON/XML/…
  8. Netværket er homogen
    De fleste netværk består af forskellige fabrikater af netværks udstyr, understøtter forskellige protokoller, kommunikerer med computere med forskellige operativ systemer, etc.

Gennemgangen af de 8 fejlslutninger om distribueret computing er langt fra fuldstændig. Hvis du er nysgerrig har Arnon Rotem-Gal-Oz lavet en grundig gennemgang (PDF format).

Hvad er alternativet til 2 vejs (synkron) kommunikation mellem services?

Svaret kan bla. findes i Pat Hellands “Life Beyond Distributed Transactions – An Apostate’s Opinion” (PDF format).
I sin artikel fortæller diskutterer Pat at “voksne” ikke benytter Distribuerede transaktioner til koordinering af opdateringer på tværs af transaktions skel (f.eks. på tværs af databaser, services, applikationer, etc.). Der er mange gode grunde til ikke at benytte distribuerede transaktioner, her i blandt:

  • Transaktioner låser ressourcer, mens de er aktive
    Services er autonome, så hvis en anden service, gennem Distribuerede transaktioner, får lov at låse resourcer i din service er det en klar overtrædelse af autonomien
  • En service kan IKKE forventes at afslutte sin processering inden for et bestemt tidsinterval – det ligger så at sige i autonomien, servicen bestemmer selv. Dvs. at det svageste led (Service) i en kæde af opdateringer bestemmer styrken af kæden.
  • Låsning holder andre transaktioner fra at fuldføre deres job
  • Låsning skalere ikke (hvis en given transaktion tager 200 ms og f.eks. holder en tabel lås, så kan servicen max skaleres til 5 samtidige transaktioner i sekundet – og det hjælper ikke at sætte flere maskiner op da de ikke får låsen til at vare kortere tid)
  • 2 phase/3 phase/X phase commit distribuerede transaktioner er skrøbelig per design.
    Så selv om de på bekostning af performance (X phase er en dyr protokol) løser problemet med opdateringer på tværs af transaktions skel, så er der rigtig mange scenarier hvor en X phase transaktion bliver efterladt i en ukendt tilstand (f.eks. hvis en 2 phase commit bliver afbrudt under commit fasen, således at alle har prepared, nogen har committet mens andre endnu ikke har. Hvis en af servicerne fejler eller er utilgængelig i forbindelse med commit fasen, så efterlades du på dybt vand uden er båd – se nedenstående tegning for forløbet af en 2 phase commit)
2 fase commit protokol flow

2 fase commit protokol flow

Hvis løsningen ikke er distribuerede transaktioner, hvad er så løsningen?

Løsningen skal findes i tre dele:

  1. Den ene del er hvordan vi opsplitter vores data/services 
  2. Hvordan vi identificerer vores data/services
  3. Samt hvordan vi kommunikerer mellem vores data/services

Hvordan opsplitter vi vores data/services og identificerer dem?

I følge Pat Helland skal data samles i klumper kaldet entiteter. Disse entiteter skal være afgrænset i størrelse, således at de efter en transaktion er konsistente.
Dette kræver, at en entitet ikke er større end at den kan være på een maskine (for på tværs af maskiner bliver vi ellers nød til at benytte distribuerede transaktioner for at sikre konsistens, og det er jo det vi gerne ville undgå i første omgang). Det kræver også at entiteten ikke er for lille, i forhold til de usecases der benytter entiteten. Ellers vil den samlede usecase kræve interaktion med flere entiteter og så vil opdateringen på tværs af entiteter ikke være konsistent efter en transaktion, med mindre man igen brugte distribuerede transaktioner.
Tommelfinger reglen er: en transaktion involverer kun een entitet.

Lad os tage et eksempel fra den virkelige verden:
På et tidligere projekt blev jeg stillet overfor et skole eksempel på hvordan misforståede genbrugs idealer og mikro opsplitning er ødelæggende for en service stabilitet, transaktionalitet, lav kobling samt latens tiden.

Kundens tanke var sikre maksimal genbrug for to domæne koncepter, hhv. juridiske enheder (legal entity i diagrammet) og adresser, hvor adresser principielt var alt der kunne bruges til at adresse en juridisk enhed (og alt andet på jorden), så som hjemme adresse, arbejds adresse, email, telefon nummer, mobil nummer, GPS lokation, etc.
For at sikre genbrugeligheden og koordineringen af opdateringer og læsninger, var man nød til at introducere en task service, her kaldet “Legal Entity Task Service”, der skulle koordinere arbejdet mellem data servicerne “Legal Entity Micro Service” og “Address Micro Service”. Man kunne også have valgt at lade “Legal Entity Micro Servicen” overtage rollen af Task service, men det løser ikke transaktionalitets problemet som vi skal diskuttere nu.

For at oprette en jurdisk enhed, f.eks. en person eller et firma, skal der først oprettes en juridisk enhed i “Legal Entity Micro Service” og en til flere Adresser i “Address Micro Service” (afhængigt af hvor mange der var defineret i de data der blev givet til CreateLegalEntity() metoden i “Legal Entity Task Service”). For hver Adresse der bliver oprettet skal den AddressId, som bliver returneret fra CreateAddress() metoden i “Address Micro Service”, associeres med den LegalEntityId der blev returneret fra CreateLegalEntity() metoden i “Legal Entity Micro Service” ved at kalde AssociateLegalEntityWithAddress() i “Legal Entity Micro Service”.

Dårlige microservices - Oprettelses scenario

Dårlige microservices – Oprettelses scenario

Fra sekvens diagrammet ovenfor er det tydeligt at der er tale om en høj grad af kobling (på alle niveauer). Hvis “Address Micro service” ikke svarer kan man ikke oprette nogen Juridiske enheder. Latenstiden i en sådan løsning er også høj pga. de mange remote kald. Noget af latenstiden kan minimeres ved at udføre flere af kaldene parallelt, men det er igen suboptimering af en grundlæggende forkert løsning og vores transaktions problem er stadig det samme:

Så længe blot en enkel af CreateAddress() eller AssociateLegalEntityWithAddress() kaldene fejler står vi med et grimt problem. Vi har oprettet en Jurdisk enhed og nu er en af CreateAddress() kaldene gået galt. Vi står med et inkonsistent system. Ikke alle data er blevet oprettet og associeret.
Det kan også være at vi har fået oprettet vores JuridiskeEnhed og alle Adresserne, men har ikke fået associeret alle Adresserne med den juridiske enhed. Igen har vi et inkonsistent system.

Denne form for orkestrering placerer en stor byrde på CreateLegalEntity() metoden i “Legal Entity Task Service”. Den skal nu være ansvarlig for at gen-forsøge fejlede kald eller rydde op (også kendt som kompensation). Måske fejler en af oprydnings kaldende og hvad gør man så? Hvad nu hvis CreateLegalEntity() metoden i “Legal Entity Task Service” er igang med at gen-forsøge et af kaldene eller igang med at rydde op og den fysiske server den kører på bliver slukket? Har udvikleren husket at implementere CreateLegalEntity() metoden (i “Legal Entity Task Service”) så den kan huske hvor langt den var og derfor kan genoptage sit arbejde når serveren bliver startet. Har udvikleren af CreateAddress()  eller AssociateLegalEntityWithAddress() metoderne sørget for at de implementeret idempotent, således at metoderne kan gen-forsøges flere gange uden at risikere dobbelt oprettelse eller dobbelt association?

Transaktionalitets problemet kan løses ved at kigge på usecasen og reevaluere genanvendelses tesen

Designet af den LegalEntity (Juridiske Enhed) og Adresse servicen opstod efter at et team arkitekter havde designet en logisk kanonisk model og ud fra den havde afgjort hvad der var genbrugeligt og dermed services. Problemet er at en kanonisk data model slet ikke tager hensyn til hvordan dataene bliver brugt, altså de usecases der involverer disse data. Grunden til at det er et problem, er at den måde data’ene bliver ændret/oprettet direkte bestemmer vores transaktions grænser, også kendt som vores konsistens grænser. Data der bliver ændret sammen i en transaktion/usecase hører som udgangspunkt også sammen data mæssigt og ejerskabs mæssigt.
Vores tommelfinger reglen udvides derfor til: 1 usecase = 1 transaktion = 1 entitet.

Den anden fejl de begik var at se den Juridisk enheds adresse som en generel adresse og bagefter ophøje adresse til en service. Man kan sige at deres genanvendeligheds fokus gjorde, at alt der lugtede af adresse med vold og magt skulle placeres i Adresse servicen. Tesen var at hvis alle bruge denne Adresse service og hvis nu f.eks. by navnet eller postkoden for en by/by-del ændrede sig, så kunne man rette det, dette ene sted. Det sidste var måske en valid grund til at centralisere, men det havde store omkostninger for alt andet.

Data modellen så nogen lunde sådan ud i løsningen (mange detaljer er udeladt):

Dårlig micro service - data model

Dårlig micro service – data model

Som det fremgår af modellen er associationen mellem LegalEntity og Address en Shared directed association, hvilket indikerer at to LegalEntities kan dele en Address. Sådan forholdt det sig aldrig, så associationen var reelt en komposition (composite) association, hvilket indikerer et parent-child forhold; der er f.eks. ingen grund til at gemme en addresse for en LegalEntity efter at denne er slettet (igen en composite association). Parent-child forholdet viser at LegalEntity og Address hører tæt sammen, de oprettes sammen, de ændres sammen og de bruges sammen.
Det betyder at der ikke er tale om to entiteter, men der i mod een entitet LegalEntity (vores omdrejningpunkt) med Addresse objekter tæt knyttet. Det er her Pat Hellands Entitets navngivning med fordel kan udvides/beriges af Domain Driven Designs (DDD) mere rige sprog, der inkluderer:

  • Entity – der beskriver et objekt der er defineret ud fra dens identitet (og ikke dens data) – f.eks. en Juridisk enhed (en Person har et CPR nummer, et firma har et CVR nummer)
  • Value Object – der beskriver et objekt der er defineret ud fra dens data og ikke dens identitet, f.eks. en Adresse, Navn, Email adresse. Et value objekt har som nævnt ikke nogen identitet, to Value objekter af samme type med samme værdier er ens (equal). Et Value objekt eksisterer aldrig alene, den indgår altid i en sammenhæng med en Entitet, som Value objektet så at sige beriger gennem dens data.
  • Aggregate – der beskriver en samling/klynge af sammenhængende (coherent) objekter med komplekse associationer. En Aggregate benyttes til at sikre invarianter og garantere konsistens for sammenhængen af objekter. En Aggregate benyttes bla. til overordnet låsning og transaktionel konsistens i forbindelse med distribuerede systemer.
    • En Aggregate vælger en Entity til at være rod (root) og den kontrollerer adgangen til objekter inden i Aggregaten. Denne kaldes en Aggregate Root.
    • En Aggregate er unik identificerbar gennem en ID
    • Andre Aggregater referer til hinanden via deres ID – der anvendes ALDRIG memory pointers eller Join tabeller (dette vender vi tilbage til i næste blog post)

Ud fra denne beskrivelse kan vi afgøre at det Pat Helland kalder en Entitet i DDD jargon hedder en Aggregate. DDD’s sprog er mere rigt, så derfor vil jeg fremover benytte DDD’s navngivning. Hvis du er mere interesseret i Aggregates kan jeg anbefale Gojko Adzic’s artikel.

Ud fra en vores usecase analyse (LegalEntity og Adresse oprettes og ændres sammen) og med anvendelsen af DDD’s jargon (LegalEntity er en AggregateRoot og en Entitet, samt Address er et value objekt) kan vi nu redesigne data modellen (også kendt som domæne modellen):

LegalEntity Microservice - bedre model

LegalEntity Microservice – bedre model

Med ovenstående design er AddressId’en forsvundet fra Address, da den ikke har behov for det eftersom at der er tale om et Value objekt.
Vores LegalEntity har stadig sin LegalEntityId og det er den vi refererer til når vi kommunikerer med LegalEntity microservicen.
Efter model redesignet har vi gjort Adresse servicen overflødig og tilbage har vi kun Legal Entity Micro servicen:

Bedre LegalEntity Microservice

Bedre LegalEntity Microservice

Med dette design er vores transaktionalitets problem helt forsvundet, da der kun er een service at tale med i dette eksempel.
Der er meget der stadig kan forbedres her og vi har heller ikke dækket hvordan man kommunikerer mellem services så vi sikre koordinering og konsistens mellem services når vi har processer/usecases der går på tværs af aggregates/services.

Denne blog post er allerede ved at være for lang, så jeg vil vente med at dække dette til næste blog post.

Indtil da, som altid er jeg interesseret i feedback og tanker 🙂

Share Button
The following two tabs change content below.
Profile photo of Jeppe Cramon

Jeppe Cramon

Partner/Arkitekt/Udvikler af TigerTeam ApS
Partner i TigerTeam. Erfaren software arkitekt/udvikler.

2 kommentarer for “Micro services: Det er ikke (kun) størrelsen der er vigtigt, det er (også) hvordan du bruger dem – Del 2

Skriv et svar

Din e-mailadresse vil ikke blive publiceret. Krævede felter er markeret med *