Toteutettu järjestelmä käsittelee satoja tuhansia viestejä päivässä. Yksittäisten viestien koko vaihtelee kilotavuista kymmeniin megatavuihin. Viestit tulevat kymmenistä eri järjestelmistä ja päätyvät kymmeniin eri järjestelmiin. Järjestelmän vanhetessa siihen liitetään vielä satoja uusia järjestelmiä, joista tulevia viestejä käsitellään ja toimitetaan eteenpäin. Järjestelmä ei saa muokata viestin tietosisältöä, vaikka sitä muunnetaan muodosta toiseen. Viestejä ei saa myöskään hukata tai monistaa. Tällaiset vaatimukset ovat hankalia toteuttaa millä tahansa arkkitehtuurilla, mutta microservices-arkkitehtuuri näyttäisi toimivan kuitenkin paremmin kuin vastaavan (molemmat ratkaisumallit käytännössä nähneenä) järjestelmän toteuttaminen eräpohjaisesti tai ESB:n pohjalle rakennetulla SOA-ratkaisulla, joissa molemmissa viestien käsittely tapahtuu yhdessä ja samassa monoliittisessa prosessissa.
Hyödyt
Skaalautuvuus vaatii yksittäisiltä palveluilta tilattomuutta ja toiminnallista riippumattomuutta toisista palveluista. Tilatonta yksinkertaista palvelua on helppo monistaa, jolloin sitä on myös helppo skaalata. Vaikka palvelua itseään on helppo monistaa, niin lisäksi pitää ratkaista miten palvelun eri instanssit löytyvät. Mielestäni palvelun instanssien löytämisen ratkaiseminen on hankalampaa kuin useiden palveluinstanssien luominen.
Tilaton palvelu on helppo hajauttaa, koska sen instanssit eivät riipu toisistaan. Näin palvelu voi toimia sijainnista riippumatta useassa eri paikassa yhtä aikaa.
Mikäli palveluiden välinen kommunikaatio on toteutettu jollain laajasti tuetulla protokollalla, kuten HTTP:llä, ei palvelun sisäisellä toteutuksella tai toteutustekniikalla ole merkitystä. Tästä seuraa toteutuksen teknologiariippumattomuus.
Uudelleenkäytettävyys ja yksinkertaisen palvelutoteutuksen ymmärrettävyys ovat myös merkittäviä etuja järjestelmän koko elinkaarta ajatellen. Mikäli yksi toteutus pystyy tarjoamaan saman palvelun usealle eri käyttäjälle, ei toteutusta tarvitse toistaa uudestaan ja uudestaan eri paikoissa. Palvelutasolla toteutuksen ja palvelun tarkoituksen ymmärtäminen on helppoa, mikäli palvelu tekee vain yhtä asiaa. Käytännössä yhden asian tekeminen tarkoittaa yksinkertaista toteutusta. Uudelleenkäytettävyys ja ymmärrettävyys tukevat siis molemmat toisiaan.
Ylläpidettävyys ja järjestelmän elinkaari
Yksinkertaisista palveluista koostuva järjestelmä on helpompi ylläpitää palvelutasolla kuin monoliittinen järjestelmä. Näin ollen toteuttajien vaihtuvuus ei haittaa palvelun ylläpitoa ja jatkokehitystä niin merkittävästi kuin monoliittisissa järjestelmissä, joissa yksittäisen toiminnallisuuden muuttaminen edellyttää usein koko järjestelmän tuntemista. On kuitenkin huomattava, että vaikka yksittäisten palveluiden ylläpitäminen ja jatkokehitys on helppoa, voi järjestelmän kokonaiskuva jäädä kehittäjille vieraaksi. Järjestelmätason ymmärrettävyysongelmiin microservices-arkkitehtuuri ei mielestäni juuri tuo helpotusta.
Isoissa järjestelmissä palvelun elinkaari on usein hyvin pitkä, usein vuosia tai jopa kymmeniä vuosia. Näiden vuosien aikana parhaina pidetyt toteutusteknologiat vaihtuvat ja järjestelmän sujuva jatkokehitys edellyttää usein uusien teknologioiden liittämisen vanhaan järjestelmään. Mikäli palveluiden välisessä viestinnässä käytetty protokolla on kuitenkin vielä käyttökelpoinen, voi uuden microservice-palvelun toteuttaa parhaalla mahdollisella teknologialla aiemmasta toteutuksesta välittämättä. Monoliittista suurta järjestelmää ei voi yleensä muuttaa käyttämään aiemmasta poikkeavia teknologioita, koska vanhaa ja uutta toteutusta ei voi liittää helposti yhteen.
Koska parhaatkin arvaukset tarvittavista ominaisuuksista menevät usein metsään, on hyvä, mikäli järjestelmän osia voi vaihtaa pikkuhiljaa uusiin ja tarpeita vastaaviin. Yksinkertainen palvelu on helppo kirjoittaa kokonaan uusiksi eikä muutaman palvelun uudelleenkirjoittaminen vie paljoa aikaa. Näin missä tahansa järjestelmän ylläpito- ja jatkokehitysvaiheessa voidaan tarvittaessa pienin kustanuksin uusia järjestelmää pala kerrallaan eikä kallista sekä riskialtista kertahyppäystä versiosta toiseen tarvita.
Järjestelmän elinkaaren kannalta mielestäni tärkein päätös on palveluiden välisen kommunikaatiotavan valitseminen: mitä protokollaa palvelut puhuvat toisilleen? Miten mahdollinen autentikaatio ja autorisaatio tehdään? Entä miten palvelut löytävät toisensa?
Käytännön rajoitteet ja ongelmat
Microservice-arkkitehtuuri vaatii huolellista palveluiden hallinnoinnin etukäteissuunnittelua. Kun puhutaan kymmenistä erilaisista palveluista eli käytännössä kymmenistä eri prosesseista, täytyy palveluiden konfiguraatioiden hallinnan ja palveluiden käyttöönoton (deployment) olla hyvin hallussa.
Lukuisten palveluiden asentaminen ja ylläpitäminen käsipelillä on vaikeata. Kun palveluita asennetaan usealle koneelle ja mahdollisesti monistetaan dynaamisesti, on käsityö mahdotonta. Siksi järjestelmän asennusten automatisointi on välttämättömyys jo hyvin vaatimattomassa microservice-pohjaisessa järjestelmässä. Automaattinen asennus on kuitenkin hyvästä jo muutenkin: http://pedanttinen.blogspot.fi/2012/04/automatisoidun-asennuksen-autuus.html
Monesta pienestä palvelusta (usein prosessista) koostuvassa järjestelmässä yhtenäisen tiedon kerääminen ja tilannekuvan ylläpitäminen on haasteellista. Järjestelmän käsittelemän yksittäisen kokonaisuuden prosessointi kulkee monen eri palvelun kautta mahdollisesti monella eri koneella, jolloin esimerkiksi virhetilanteessa voi olla vaikeata löytää missä kohtaa järjestelmää virhe tapahtui. Järjestelmän tilannekuvan luominen eli kunkin ajanhetken toimintakyvyn selvittäminen voi vaatia monimutkaista päättelyä. Esimerkiksi yhden palveluinstanssin toimimattomuus ei välttämättä merkitse järjestelmätasolla mitään, sillä palveluinstanssi ei välttämättä ole käytössä tai saman palvelun toinen instassi saattaa järjestelmätasolla peittää vian.
Usein ison monoliittisen järjestelmän käynnistäminen kestää hyvin kauan, mikä on kehitystyön kannalta todella ikävää. Microservice-arkkitehtuurissa yksittäisen palvelun käynnistäminen puolestaan on hyvin nopeata, mikä tukee hyvin nopeata kehityssykliä. Kolikon kääntöpuolena on se, että kokonaisen microservice-pohjaisen järjestelmän käynnistäminen on helposti vielä hitaampaa kuin monoliittisen järjestelmän eikä kehittäjän ole helppo laittaa koko järjestelmää omalle kehityskoneelleen pyörimään. Joissain ongelmaselvitystilanteissa tämä voi olla erittäin ikävää.
Palvelun uudelleenkäytettävyys vaatii onnistumista rajapintasuunnittelussa, eikä microservices-arkkitehtuuri tätä vaatimusta muuta. Päin vastoin, rajapintasuunnittelu on entistä hankalampaa, koska palvelun ei pitäisi välittää tai tietää varsinaisista kutsujista mitään. Palvelun rajapinnan suunnittelussa pitää keskittyä erityisesti varsinaisen ongelman ratkaisuun eikä miettiä miten palvelua olisi esimerkiksi helppo kutsua palvelusta x tai mikä olisi minimiratkaisu palvelun y ongelmaan.
Elävän elämän esimerkki
Esimerkkijärjestelmä käsittelee satoja tuhansia yksittäisiä viestejä päivässä, jotka tulevat kymmenistä eri ulkoisista järjestelmistä ja päätyvät kymmeniin eri järjestelmiin. Käsittely on pääosin viestin muuntamista muodosta toiseen sekä reitittämistä ulkoisten järjestelmien välillä. Ehdottomina vaatimuksina on keskeisen viestisisällön säilyttäminen muunnoksista huolimatta muuttumattomana, viestin 100% toimitusvarmuus ja se, että viesti toimitetaan eteenpäin täsmälleen kerran. Järjestelmä ei voi luottaa sisääntulevien viestin sisältöön millään tavalla ja virheellisistä viesteistä pitää pitää kirjaa ja antaa palaute. Nämä vaatimukset ovat haastavia mille tahansa systeemiarkkitehtuurille.
Yksittäiset palvelut on toteutettu järjestelmässä Javalla ja näiden välinen viestintä tapahtuu HTTP/REST-rajapinnoilla JSON-muotoisilla viesteillä. Palvelut jakaantuvat karkeasti ottaen kolmeen ryhmään: 1) Väyläpalveluihin, 2) apupalveluihin sekä 3) ajastettuihin palveluihin. Palveluita järjestelmässä on yli neljäkymmentä ja lisää on tekeillä.
Järjestelmän perustehtävä on siis hakea viesti yhdestä kanavasta, käsitellä viesti ja toimittaa toiseen kanavaan. Koska erilaisia viestinkäsittelytapoja on vähintään niin paljon kuin sisääntulevia kanavia, pitää käsittelyreitin olla hyvin joustava. Käsittelyreitin muodostaa palvelu, joka liikuttaa viestiä toisten palveluiden välillä (voidaan ajatella viestiväylänä) ja ajoaikaisesti konfiguroitava joukko palveluita, joille viesti kuljetetaan. Käsittelyreitti tarkoittaa siis joukkoa palveluita ja niiden välistä käsittelyjärjestystä.
Esimerkkireitti voisi lukea XML:ää järjestelmään ja toimittaa siitä muodostettuja sähköposteja ulos. Yksinkertaisimmillaan reitti voisi sisältää täsmälleen yhden palvelun, joka ottaa viesti-XML:n sisään ja lähettää sen sähköpostiksi muunnettuna maailmalle. Käytännössä järkevämpää on pilkkoa reitti pienemmiksi palveluiksi. Palvelut voisivat olla seuraavat: 1) XML:n validointi, 2) XML:n parsinta eli olennaisten tietojen hakeminen, 3) tiedon muuntaminen sähköpostiksi ja 4) sähköpostin lähettäminen. Hienommaksi jauhetusta palvelujoukosta huomaa heti, että jokainen palvelu voisi olla helposti uudelleenkäytettävissä toisellakin reitillä!
Reiteillä olevat palvelut käyttävät apupalveluita, jotka tekevät usein toistuvia asioita tai esimerkiksi toimivat jaettuina tietovarastoina. Viestien käsittelyssä useimmiten tarvittava palvelu on XSL-muunnos - joskus hyvin raskas sellainen - mitä ei kannata toteuttaa jokaiseen palveluun erikseen. XSL-muunnospalvelusta on siis tehty apupalvelu, jota muut palvelut käyttävät muunnosten tekemiseen.
Reiteillä olevat palvelut toimivat, kun reitille saapuu käsiteltävä viesti. Ne vaativat siis ulkoisen herätteen. Koska järjestelmä tarvitsee myös kellonaikaan sidottuja toiminnallisuuksia, tarvitaan väylä- ja apupalveluiden lisäksi vielä kolmas palvelutyyppi eli ajastatetut palvelut. Nämä palvelut valvovat esimerkiksi järjestelmän tilaa ja lähettävät ajastettuja raportteja.
Reiteillä sijaitsevat palvelut eivät tiedä mitä palveluita niitä ennen on kutsuttu tai mitä palveluita niiden jälkeen tullaan kutsumaan. Palvelut kuitenkin kommunikoivat toistensa kanssa asettamalla viesteihin lisätietoja. Esimerkiksi ulosmenoreitin määrittävä palvelu saattaa asettaa viestille tiedon, että ulosmenoreitti on sähköposti. Näin sähköpostisisällön luova palvelu ymmärtää luoda viestille sähköpostin ja sähköpostia lähettävä palvelu huomaa sen lähettää maailmalle. Huomattakoon kuitenkin, että mikään edellä mainittu palvelu ei tiennyt mistä sen käyttämät tiedot ilmestyivät ja miksi.
Kun palvelu käynnistyy, se rekisteröityy konfiguraatiota hallitsevaan apupalveluun. Palvelu kysyy konfiguraatiopalvelulta oman konfiguraationsa ja tarvittessa selvittää missä sen tarvitsemat muut palvelut sijaitsevat. Palveluihin on siis kovakoodattu asennusvaiheessa sen käyttämän konfiguraatiopalvelun sijainti, mutta kaikki muut tiedot voidaan selvittää ajoaikaisesti.
Konfiguraatiopalvelu sisältää järjestelmän kaiken ajoaikaisesti muutettavissa olevan konfiguraation. Käytännössä ajoaikainen konfiguraatiomuutos saa konfiguraatiopalvelun kutsumaan muita palveluita, joita konfiguraatiomuutos koskee ja päivittämään näiden palveluiden konfiguraation. Konfiguraatiopalvelun kautta hallitaan myös käsittelyreittejä eli uusia viestien käsittelyreittejä hallitaan ajoaikaisesti.
Koska yksittäisen viestin käsittelyä on hyvin hankala seurata pelkästään esimerkiksi lokitiedostojen sisältöä seuraamalla, on järjestelmään rakennettu tapahtumia keräävä palvelu. Kaikki yksittäisen viestin käsittelyyn liittyvät merkittävät tapahtumat lähetetään tapahtumapalveluun säilöön. Näin minkä tahansa viestin käsittelyhistoria on saatavilla ja vikatilanteissa on helppo selvittää mitä viestille tapahtui. Lisäksi lokitietoa kerätään keskitetysti, jolloin akuutit virhetilanteet on helpompi selvittää.
Järjestelmän tilaa valvotaan pääasiassa palvelutasolla. Mikäli yksittäinen palvelu menee yllättäen alas, järjestelmä hälyttää. Lisäksi järjestelmän läpi ajetaan jatkuvasti testiviestejä, joilla järjestelmän toimintaa voidaan valvoa järjestelmätasolla. On nimittäin mahdollista, että mikään palvelu ei ole alhaalla, mutta järjestelmä ei silti kokonaisuutena toimi.
Järjestelmän hajautus perustuu jonojen hajautettuun lukemiseen. Jokainen käsittelyreitti muodostuu osareiteistä, jotka päättyvät aina jonoon. Yhden käsittelyreitin sisällä mennään siis useamman kerran jonoon. Jonosta lähtiessään viesti saattaa jatkaa käsittelyä samalla palvelimella kuin aiemmin tai sitten jossain muualla. Näin pienet kokonaisuudet saadaan käsiteltyä samalla palvelimella (tai oikeammin samassa palveluryhmässä) ja kuitenkin tarvittaessa käsittely skaalautuu useammalle koneelle (tai palveluryhmälle).
Yksinkertaisessa esimerkkikuvassa alla esitetään sisääntulevat viestit tiedostoina, jotka haetaan kuvan yläreunassa olevasta laatikosta. Sisääntuleva viesti käsitellään kolmessa eri palvelussa ennen kuin siitä muodostetaan tulostiedosto. Käsittelyjärjestys on ajoaikaisen konfiguraation määräämä. Palvelu, joka viestejä liikuttaa käsittelypalveluiden välillä ei näy kuvassa. Jokaisesta kolmesta käsittelypalvelusta on olemassa kaksi instanssia, jotka on hajautettu eri ajoympäristöihin. Kunkin viestin käsittely saman palvelun eri instansseissa määräytyy jonosta lukemisjärjestyksessä. Tyypillisesti palvelut lukevat jonosta niin paljon viestejä kuin pystyvät, jolloin yleensä vähiten kuormitettu instanssi käsittelee viestin, koska se ehtii jonosta eniten uusia viestejä hakemaan. Kuten kuvasta näkyy, järjestelmän hajautus perustuu jonototeutuksen hajautukseen, mikä siirtää yhden hankalan ongelman valmiskomponentin ratkaistavaksi. Laadukkaita hajautettuja jonototeutuksia on tarjolla useita ja käytetty toteutus on helppo vaihtaa (muutoksia vain reitityksestä huolehtivaan palveluun), joten valmiskomponentin käyttö soveltuu järjestelmään enemmän kuin hyvin. Käytännössä kuvatunlaisen arkkitehtuurin skaalautuvuutta rajoittavat jonototeutuksen rajoitukset, viestejä sisääntuovan järjestelmän rajoitukset sekä tallennusta hoitavien järjestelmien rajoitukset.
Omat kokemukseni microservice-arkkitehtuurista ovat äärimmäisen positiiviset. En kuitenkaan lähtisi näin monimutkaisella tavalla ratkaisemaan kaikkia mahdollisia ongelmia. Monoliittiratkaisun microservices kuitenkin voittaa varmasti lähes aina, mikäli järjestelmä on yhtään laajempi.