maanantai 30. maaliskuuta 2015

Käytännön kokemuksia Microservices-arkkitehtuurista

Olen työskennellyt pitkään palvelun parissa, jonka uudelleenkirjoitettu versio päädyttiin toteuttamaan microservices-arkkitehtuurilla puolivahingossa. Alkuvaiheessa uudelleenkirjoitusprojektia tarkoitus ei ollut tehdä toteutusta microservices-arkkitehtuurilla, mutta alkuvaiheen jälkeen huomasimme, että sellainen siitä tuli tehtyä.

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


Microservices-arkkitehtuurien väitetään olevan skaalautuvia, hajautettavia, uudelleenkäytettäviä, teknologiariippumattomia ja helppoja ymmärtää, koska järjestelmä rakentuu yksinkertaisista palveluista. Kokemukseni mukaan microservice-arkkitehtuuri mahdollistaa juuri nämä ominaisuudet, mutta automaattisesti hyötyjä ei saa.

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.

sunnuntai 15. maaliskuuta 2015

Installing Ubuntu 15.04 on Surface Pro 3

UPDATE: see my experiences with release version from http://pedanttinen.blogspot.fi/2015/08/installing-dual-boot-ubuntu-1504-on.html

Here's my experiences on installing Ubuntu 15.04 from daily build. This is not the official release version try out!

By following these instructions, you get Ubuntu 15.04 installed on Surface Pro 3, but at least at the moment everything does not work right. The older instructions based on 14.10 do not help to overcome the issues.

I'm using Surface Pro 3 i5/256GB/8GB version. I wanted to keep pre-installed Windows 8.1 as secondary operating system, because Ubuntu still has many known issues. So, I'm creating a dual boot setup.

Before installingn Linux, you have to make some space for it. I used Windows tools to reduce the partition size. I left 50GB for Windows partition, which means that there's about 20GB of free space before anything else than default software has been installed.

Reducing Windows partition size

  1. Start diskmgmt.msc tool
  2. Try to reduce the size of the Windows partition
  3. In my case, I had to remove Bitlocker from the Windows partition before I was able to reduce the size

Booting from USB stick

  1. Remove secure boot. Shutdown Surface and restart it by first holding down volume up button and then the power button.
  2. Disable secure boot.
  3. Shutdown Surface
  4. Start Surface by holding volume down button and then power button

Install Ubuntu 15.04

  1. To speed up installation process, do not let installer to access internet
  2. During installation, choose manual partitioning
  3. I created the following partitions 
    1. Primary 30GB for root (the / mount point)
    2. Primary (actually not sure whether you can choose between logical and primary for swap) 8GB swap
    3. Primary partition for /home with rest of the space
  4. I installed Linux boot loader on /dev/sda. Many posts in internet say that it would be better to install it on the Windows EFI partition i.e. /dev/sda2, but I got it working this way, so I cannot tell whether it's a good idea or not
  5. After installation is ready, boot Surface and you will end up back in Windows. Ubuntu is nowhere to find
  6. To fix the problem, you have to use boot-repair tool
    1. To run boot-repair, boot Linux from USB stick and run the following commands in shell (these instructions are based on the official guide: https://help.ubuntu.com/community/Boot-Repair)
      1. sudo add-apt-repository ppa:yannubuntu/boot-repair
      2. sudo apt-get update
      3. sudo apt-get install -y boot-repair && boot-repair
      4. Accept default recommended repair
      5. Remember to note down the link boot-repair is giving you
      6. Check the last lines generated by the tool, because they are needed later
    2. Most likely because my Ubuntu 15.04 is from daily build, I wasn't able to execute the boot-repair tool from Ubuntu 15.04 USB stick, so I had to use 14.04 Ubuntu to do this. Since Ubuntu 14.04 does not support Surface type cover keyboard out-of.the-box, you have to use the Ubuntu's default virtual keyboard Onboard, which you can access from the application menu.
  7. After reboot, you still end up in Windows automatically. To make GRUB 2 working, you have to go to Windows CMD as administrator and then execute the command boot-repair tool gave you in the end of its generated output. Most likely it's the one below
    1. bcdedit /set {bootmgr} path \EFI\ubuntu\shimx64.efi
  8. Shutdown the system and boot by pressing volume up + power button. Enable secure boot (if you don't enable it, Surface will keep on booting to Windows)
  9. Now after reboot, GRUB 2 should show you the menus for selecting Windows or Ubuntu dual boot.
  10. At this point, I made Windows my default boot option
    1. Boot to Linux
    2. sudo vi /etc/default/grub
    3. Change line with GRUB_DEFAULT=0 to GRUB_DEFAULT=2 (you can calculate the correct number by checking which line in boot menu has Windows. Indexing starts from 0). Save changes.
    4. sudo update-grub
    5. Next time Windows will be selected automatically, if user does not select anything else in the boot menu
  11. It's worth to notice that if you don't have type cover attached to Surface during boot, you can still use on-screen keyboard to select the right operating system from boot menu. Nice!

What works and what doesn't

Working out-of-the-box
  • Type cover
  • Surface pen as a mouse / pointing device (buttons do not work however)
  • Touch screen (sometimes stops working for a while and has some strange behaviour at times)
  • WLAN
  • Bluetooth
Not working out-of-the-box
  • Cameras
  • Touch pad
  • Ubuntu does not recognize that it's running on laptop with battery. At least the desktop software cannot show the state of the batter
The biggest problems in running Ubuntu 15.04 on Surface at the moment is the lack of touch pad support. It would be nice also to get the touch screen working without random problems. I will keep on using Windows as long as I'm not able to solve the touch pad issue. Otherwise the system seems to run ok.