Skip to main content

29 Tekniikkaa

29Tekniikka logo
API-integraatio näyttää usein valmiilta työltä silloin, kun ensimmäinen testi palauttaa odotetun vastauksen.
Sovellus lähettää pyynnön. Rajapinta vastaa. Tiedot menevät perille. Kehittäjä voi siirtyä seuraavaan tehtävään.
Tuotannossa tilanne on harvoin näin siisti.
Kun pilvisovellus alkaa keskustella useiden ulkoisten palveluiden, tietokantojen, maksupalveluiden, ERP-järjestelmien tai muiden API-rajapintojen kanssa, pienet oletukset alkavat näkyä. Yksi palvelu vastaa hitaasti. Toinen palauttaa 429-virheen. Kolmas katkaisee yhteyden juuri silloin, kun sovellus yrittää käsitellä tilausta. Joskus integraatio toimii testissä moitteettomasti, mutta tuotannossa sama kutsu alkaa epäonnistua muutaman tunnin välein.
Silloin ongelma ei välttämättä ole itse API:ssa.
Usein vaikeampi kysymys on se, miten oma sovellus käyttäytyy, kun toinen järjestelmä ei toimi odotetulla tavalla.

Integraatio ei ole vain HTTP-kutsu

Yksinkertainen API-kutsu voi näyttää koodissa tältä:
  • lähetä pyyntö
  • odota vastausta
  • käsittele data
Todellisessa palvelussa ketju on pidempi.
Pyyntö voi kulkea sovelluksen, kuormantasaajan, palomuurin, DNS:n, NAT:n ja ulkoisen palvelun kautta. Vastauksen jälkeen sovellus voi vielä päivittää tietokantaa, julkaista viestin jonoon tai kutsua seuraavaa rajapintaa.
Jos jokin näistä vaiheista hidastuu, käyttäjä näkee lopputuloksen: toiminto kestää liian kauan tai epäonnistuu.
Tämän vuoksi API-integraation suorituskykyä ei kannata arvioida pelkästään sen perusteella, kuinka nopeasti ulkoinen rajapinta vastaa.
Esimerkiksi 150 millisekunnin API-vastaus ei auta paljon, jos sama pyyntö tekee kymmenen tällaista kutsua peräkkäin. Vielä huonommaksi tilanne muuttuu, jos jokaisella kutsulla avataan uusi yhteys tai jokainen virhe käynnistää automaattisen uudelleenyrityksen.
Siinä vaiheessa ongelma on jo sovelluksen toimintatavassa.

Ensimmäinen vaikea kohta on riippuvuus toisen palvelun toiminnasta

Kun oma järjestelmä käyttää ulkoista API:a, osa toiminnasta ei enää ole omissa käsissä.
Toinen palvelu voi olla hetkellisesti hidas. Sen liikennekiintiö voi tulla vastaan. Huoltoikkuna voi osua huonoon aikaan. Rajapinnan vastausrakenne voi muuttua. Yhteys voi katketa kesken pyynnön.
Näitä tilanteita ei pitäisi käsitellä poikkeuksina, joita tapahtuu kerran vuodessa.
Tuotantokoodissa niiden pitäisi olla normaali osa suunnittelua.
Yksi tavallinen virhe on tehdä API-kutsusta liian vahvasti synkroninen.
Käyttäjä esimerkiksi painaa painiketta, jonka jälkeen oma palvelu kutsuu ensin maksupalvelua, sitten varastonhallintaa ja lopuksi toimitusjärjestelmää. Jos kaikki kolme vastaavat nopeasti, kaikki näyttää hyvältä.
Mutta mitä tapahtuu, jos varastonhallinta kestääkin kahdeksan sekuntia?
Käyttäjä odottaa.
Jos HTTP-yhteyden timeout on kymmenen sekuntia, palvelin pitää resurssia varattuna lähes koko ajan. Kun samaan aikaan tulee lisää pyyntöjä, hitaus alkaa levitä muihin toimintoihin.
Tässä kohtaa API-integraatio muuttuu kapasiteettiongelmaksi.

Timeout ei ole pelkkä numero asetustiedostossa

➠ Timeout-arvoja käsitellään joskus liian yksinkertaisesti.
➠ “Laitetaan timeout viiteen sekuntiin.”
➠ Mutta mikä timeout oikeastaan on kyseessä?
➠ Yhteyden muodostamisella voi olla oma aikarajansa. Vastauksen odottamisella toinen. Kokonaispyynnöllä voi olla kolmas raja.
➠ Näiden erolla on merkitystä.
➠ Jos DNS-haku, TCP-yhteys, TLS-neuvottelu ja palvelun käsittely kaikki kuuluvat samaan kokonaisaikaan, viiden sekunnin timeout ei kerro vielä, missä aika kului.
➠ Tuotannossa tästä pitäisi saada havaintoja myös lokiin tai metriikoihin.
➠ Esimerkiksi:
  • API: customer-service
  • status: 200
  • duration: 842 ms
  • timeout: 3000 ms
  • retry: 0
➠ Yksittäinen lokirivi ei ratkaise ongelmaa. Kun vastaavia rivejä kertyy tuhansia, niistä alkaa kuitenkin näkyä, onko kyse satunnaisesta viiveestä vai jatkuvasta ongelmasta.

Uudelleenyritys voi korjata virheen ja samalla pahentaa ongelmaa

Retry on hyödyllinen silloin, kun virhe on hetkellinen.
Mutta automaattinen uudelleenyritys ei ole yleinen ratkaisu kaikkiin API-virheisiin.
Jos palvelu palauttaa 429 Too Many Requests -vastauksen, kolme välitöntä uutta yritystä eivät välttämättä auta. Ne voivat päinvastoin lisätä toisen palvelun kuormaa.
Sama koskee 500-virheitä tilanteessa, jossa vastapuolen palvelu on jo ylikuormittunut.
Uudelleenyrityksissä tarvitaan yleensä ainakin:
  • rajattu määrä yrityksiä
  • viive yritysten välillä
  • kasvava viive tarvittaessa
  • selkeä käsittely eri HTTP-virheille
  • tieto siitä, voiko sama toiminto turvallisesti suorittaa uudelleen
Viimeinen kohta on erityisen tärkeä.
Jos sovellus lähettää maksupyynnön ja saa yhteyskatkon juuri ennen vastausta, kehittäjä ei välttämättä tiedä, tapahtuiko maksu vai ei.
Uuden pyynnön lähettäminen suoraan voi silloin johtaa kaksinkertaiseen tapahtumaan.
Tässä tullaan idempotenssiin.

Idempotenssi ratkaisee yhden ikävän tuotanto-ongelman

Idempotentti toiminto voidaan suorittaa uudelleen ilman, että sama liiketoimintatapahtuma syntyy vahingossa useita kertoja.
Maksuissa, tilausten luonnissa ja muissa tärkeissä tapahtumissa tämä voidaan toteuttaa esimerkiksi idempotency key -tunnisteella.
Sovellus lähettää pyynnön mukana yksilöllisen tunnisteen:
Idempotency-Key: order-84721-payment
Jos sama pyyntö lähetetään uudelleen yhteysongelman jälkeen, vastaanottava palvelu voi tunnistaa sen samaksi tapahtumaksi.
Tämä on paljon turvallisempi lähtökohta kuin oletus, että “jos ensimmäinen kutsu epäonnistui, mitään ei varmasti tapahtunut”.
Tuotannossa juuri tällaiset rajatapaukset ovat usein niitä, jotka maksavat eniten selvitystyötä.

API:n vastaus ei aina ole sellainen kuin kehittäjä odottaa

➠ Integraatio voi hajota myös ilman varsinaista käyttökatkoa.
➠ Palvelu vastaa edelleen HTTP 200 -statuksella, mutta datan rakenne on muuttunut.
➠ Kenttä, joka ennen oli aina mukana, onkin nyt null. Päivämäärän muoto muuttuu. Uusi enum-arvo ilmestyy vastaukseen. Desimaaliluku palautetaan eri muodossa kuin testidatassa.
➠ Sovellus voi jatkaa toimintaansa jonkin aikaa ennen kuin virhe huomataan.
➠ Siksi ulkoisen API:n datan käsittelyssä kannattaa erottaa toisistaan kaksi asiaa: teknisesti onnistunut HTTP-pyyntö ja liiketoiminnallisesti onnistunut operaatio.
➠ 200 OK ei vielä tarkoita, että kaikki meni oikein.
➠ Esimerkiksi:
{
“status”: “accepted”,
“customer”: null
}
➠ voi olla teknisesti täysin kelvollinen JSON-vastaus. Sovelluksen näkökulmasta se voi kuitenkin olla vakava ongelma.

Liian suuri riippuvuus ulkoisesta API:sta näkyy nopeasti

Kaikkea tietoa ei tarvitse hakea reaaliajassa.
Tämä on yksi käytännön asioista, joka kannattaa päättää jo arkkitehtuurivaiheessa.
Jos käyttäjän näkymä tarvitsee tietoa, jonka ei tarvitse olla sekunnilleen ajan tasalla, sitä voidaan joissakin tapauksissa säilyttää oman järjestelmän välimuistissa tai tietokannassa.
Tällöin käyttäjän pyyntö ei ole riippuvainen ulkoisen palvelun tämänhetkisestä vasteajasta.
Ratkaisu ei kuitenkaan ole automaattisesti cache.
Välimuistissa pitää tietää, kuinka vanhaa tietoa voidaan hyväksyä. Jos kyse on tuotteen hinnasta, varastosaldosta tai maksun tilasta, vanhentunut tieto voi olla liiketoiminnallisesti ongelmallista.
Siksi kysymys ei ole vain siitä, “voidaanko tämä laittaa Redis-välimuistiin”.
Parempi kysymys on: kuinka tuoretta tiedon täytyy olla?

Pilviympäristössä verkko tuo omat ongelmansa

API-integraation kehittäjä voi testata palvelua omalta työasemaltaan ja saada vastauksen heti.
Pilvessä sama pyyntö kulkee eri reittiä.
Palvelu voi olla yksityisessä verkossa. Ulkoinen API voi olla saavutettavissa vain NAT-yhdyskäytävän kautta. DNS-asetukset voivat poiketa kehitysympäristöstä. Palomuuri tai security group voi estää yhteyden. TLS-varmenteen tarkistus voi epäonnistua ympäristössä, jossa sitä ei ole koskaan testattu kunnolla.
Siksi “API toimii selaimella” ei kerro juuri mitään siitä, toimiiko palvelinten välinen yhteys.
Kun integraatio epäonnistuu pilvessä, ensimmäisiä tarkistuksia ovat esimerkiksi:
  • ratkeaako palvelun DNS-nimi
  • muodostuuko TCP-yhteys
  • onnistuuko TLS-kättely
  • pääseekö liikenne oikeaan kohteeseen
  • palauttaako vastapuoli HTTP-vastauksen
  • katkeaako yhteys ennen vastausta
Näitä asioita kannattaa tutkia erikseen eikä niputtaa kaikkea yhdeksi “API ei toimi” -ongelmaksi.

Kun yksi API kutsuu toista API:a, vian jäljittäminen vaikeutuu

➠ Modernissa pilvisovelluksessa käyttäjän pyyntö voi kulkea usean palvelun läpi.

➠ Esimerkiksi:

Selain

↓

API Gateway

↓

Order Service

↓

Payment Service

↓

External Payment API

➠ Jos käyttäjä saa virheen, mikä palvelu epäonnistui?
➠ Ilman kunnollista jäljitettävyyttä vastausta voi olla vaikea saada.
➠ Distributed tracing auttaa yhdistämään saman pyynnön eri palveluissa syntyvät tapahtumat. Trace ID:n avulla voidaan nähdä, kuinka paljon aikaa kului missäkin vaiheessa.
➠ Yksittäisen palvelun loki kertoo yhden näkökulman. Hajautetussa järjestelmässä tarvitaan usein koko pyynnön ketju.
➠ Tämä korostuu erityisesti silloin, kun ongelma esiintyy vain ajoittain.

API-integraation suorituskykyä ei kannata katsoa vain keskiarvosta

Keskiarvo voi näyttää hyvältä ja käyttäjäkokemus silti olla huono.
Jos API vastaa yleensä 100 millisekunnissa mutta osa pyynnöistä kestää neljä sekuntia, keskiarvo voi peittää ongelman.
Siksi tuotannossa on hyödyllistä seurata esimerkiksi p50-, p95- ja p99-arvoja.
Jos p50 on 120 ms ja p95 jo 1,8 sekuntia, kaikki käyttäjät eivät koe samaa palvelua.
Tällaisessa tilanteessa kannattaa selvittää juuri hitaiden pyyntöjen yhteinen tekijä.
Onko kyse tietystä asiakasryhmästä? Tietystä endpointista? Tietyistä payload-kokoluokista? Tietystä kellonajasta? Vai vastapuolen kuormasta?
Tämäntyyppinen tutkiminen johtaa yleensä nopeammin oikeaan ongelmaan kuin koodin satunnainen optimointi.

Kun API-integraatio epäonnistuu, aloita yhdestä oikeasta tapauksesta

Integraatio-ongelmaa voi yrittää tutkia lukemalla koko koodipohjaa.
Usein nopeampi tapa on ottaa yksi epäonnistunut pyyntö ja seurata sitä alusta loppuun.
Mitä käyttäjä teki?
Mikä endpoint käynnistyi?
Mitä ulkoisia kutsuja tehtiin?
Kuinka kauan kukin kesti?
Mikä HTTP-status palautui?
Tuliko retry?
Mitä tietokannassa tapahtui?
Mikä lopulta lähetettiin käyttäjälle?
Kun yksi tapaus tunnetaan tarkasti, saman ongelman etsiminen muista pyynnöistä helpottuu.
Tässä vaiheessa lokit, metriikat ja trace-tiedot ovat paljon hyödyllisempiä kuin pelkkä lähdekoodin lukeminen.

API-integraatio kannattaa suunnitella myös epäonnistumista varten

Hyvä integraatio ei ole sellainen, joka toimii täydellisesti silloin, kun kaikki muut palvelut toimivat.
Parempi testi on kysyä, mitä tapahtuu silloin, kun toinen järjestelmä:
  • vastaa hitaasti
  • palauttaa 429-virheen
  • palauttaa 500-virheen
  • katkaisee yhteyden
  • palauttaa puutteellista dataa
  • ei vastaa lainkaan
  • hyväksyy pyynnön mutta vastaus katoaa matkalla
Näitä tilanteita voidaan testata tarkoituksella.
Jos järjestelmä kestää ne hallitusti, tuotantoon tulee vähemmän yllätyksiä.
Jos taas koko palvelu pysähtyy yhden ulkoisen API:n vuoksi, riippuvuus on liian tiukka.

Mitä korjataan ensin?

➠ API-integraatioissa korjausjärjestys kannattaa pitää käytännöllisenä.
➠ Jos yhteys ei muodostu, ei ole järkeä aloittaa JSON-serialisoinnin optimoinnista.
➠ Jos vastapuoli palauttaa 429-virheitä, kannattaa ensin selvittää liikenne ja käyttörajat ennen kuin lisätään uusia retry-kierroksia.
➠ Jos ongelma näkyy vain hitaissa pyynnöissä, kannattaa löytää juuri nämä pyynnöt ja verrata niitä normaaleihin tapauksin.
➠ Jos taas sama ulkoinen palvelu hidastaa koko omaa sovellusta, voidaan joutua muuttamaan arkkitehtuuria: käyttää jonoa, siirtää työ taustalle, ottaa käyttöön välimuisti tai erottaa kriittinen toiminto vähemmän tärkeästä.
➠ Kaikkea ei tarvitse ratkaista samalla tavalla.

Lopuksi

Pilvisovelluksen API-integraatio näyttää koodissa usein pieneltä osalta järjestelmää. Tuotannossa sen vaikutus voi olla paljon suurempi.
Yksi hidas riippuvuus voi hidastaa käyttäjälle näkyvää toimintoa. Huonosti suunniteltu retry voi kasvattaa kuormaa. Puutteellinen timeout voi pitää palvelimen resursseja varattuina liian pitkään. Puuttuva idempotenssi voi puolestaan aiheuttaa tilanteen, jossa sama liiketoimintatapahtuma käsitellään kahdesti.
Siksi API-integraatiota ei kannata suunnitella vain onnistuneen vastauksen ympärille.
Kiinnostavampi kysymys on, mitä oma järjestelmä tekee silloin, kun vastapuoli ei toimi täydellisesti.
Kun tämä suunnitellaan etukäteen, integraatiosta tulee paljon helpompi ylläpitää myös silloin, kun liikenne kasvaa ja järjestelmien määrä lisääntyy.

Usein kysyttyä

Miksi API toimii kehitysympäristössä mutta ei tuotannossa?

Ympäristöjen verkkoasetukset, DNS, palomuurit, käyttöoikeudet, TLS-asetukset ja liikenteen reititys voivat olla erilaisia. Siksi integraatio kannattaa testata samankaltaisesta verkkoympäristöstä kuin missä palvelu oikeasti ajetaan.

Milloin retry kannattaa ottaa käyttöön?

Retry sopii lähinnä tilanteisiin, joissa virheen oletetaan olevan hetkellinen. Yritysten määrän, viiveen ja uusintayritysten ehdot pitää määritellä. Kaikkia virheitä ei pidä yrittää uudelleen.

Miksi timeoutit ovat niin tärkeitä?

Ilman sopivaa timeoutia yksi hidas ulkoinen palvelu voi pitää sovelluksen resursseja varattuina pitkään. Kuorman kasvaessa tämä voi näkyä koko palvelun hidastumisena.

Tarvitaanko API-integraatioissa distributed tracingia?

Kaikissa pienissä sovelluksissa ei välttämättä. Kun palveluja on useita ja yksi käyttäjän pyyntö kulkee niiden läpi, tracing helpottaa huomattavasti sen selvittämistä, missä viive tai virhe syntyi.

Voiko kaikki API-ongelmat ratkaista välimuistilla?

Ei. Välimuisti voi vähentää tarpeettomia API-kutsuja, mutta samalla tieto voi vanhentua. Ratkaisu riippuu siitä, kuinka tuoretta tiedon pitää olla ja mitä tapahtuu, jos käyttäjä näkee hetkellisesti vanhan arvon.