Kuinka rakentaa nopea ja vankka REST-sovellusliittymä Scalan avulla

"Kissan ihoa on enemmän kuin yksi tapa."

Tämä on suosittu sanonta ja vaikka henkinen kuva voi olla häiritsevä, se on universaali totuus, erityisesti tietotekniikan kannalta.

Seuraava on siis tapa rakentaa REST API Scalaan eikä tapa rakentaa sitä.

Oletetaan, että rakennamme käytännössä pari sovellusliittymää Redditin kaltaiselle sovellukselle, jossa käyttäjät voivat käyttää profiiliaan ja lähettää päivityksiä. Jotta voimme rakentaa Reddit-metaforia, kuvittele, että toteutamme (uudelleen) api / v1 / me ja api /

Jotkut perustyöt

Pähkinänkuoressa:

  1. Scala on lambda calculukseen perustuva olio-ohjelmointikieli, joka toimii Java-virtuaalikoneessa ja integroituu saumattomasti Java-ohjelmaan.
  2. AKKA on Scalan huipulle rakennettu kirjasto, joka tarjoaa toimijoita (monisäikeisiä turvallisia esineitä) ja muuta.
  3. Spray.io on HTTP-kirjasto, joka on rakennettu AKKA: n yläpuolelle ja tarjoaa yksinkertaisen, joustavan HTTP-protokollan toteutuksen, jotta voit kääntää oman pilvipalvelun.

Haaste

REST-sovellusliittymän odotetaan tarjoavan:

  1. nopea, turvallinen puhelutason todennus ja lupavalvonta;
  2. nopea liiketoimintalogiikan laskenta ja I / O;
  3. kaikki edellä mainitut suurella samanaikaisuudella;
  4. mainitsinko nopeasti?

Vaihe 1, todennus ja lupa

Todennus tulisi toteuttaa OAUTH- tai OAUTH 2 -käyttöjärjestelmässä tai jonkin verran yksityisen / julkisen avaimen todennusta.

OAUTH2-lähestymistavan etuna on, että saat istunnon tunnuksen (jonka avulla voit etsiä vastaavaa käyttäjätiliä ja istuntoa) ja allekirjoitusmerkin, enemmän siitä hetkessä.

Jatkamme täällä olettaen, että tätä käytämme.

Allekirjoitusmerkki on yleensä salattu tunnus, joka saadaan allekirjoittamalla pyynnön koko hyötykuorma jaetulla salaisella avaimella SHA1: n avulla. Allekirjoitusmerkki tappaa siten kaksi lintua yhdellä kivillä:

  1. se kertoo, tunteeko soittaja oikean jaetun salaisuuden;
  2. se estää tietojen injektoinnin ja ihmisen keskellä hyökkäyksiä;

Yllä olevasta on maksettava pari hintaa: ensin on vedettävä tiedot I / O-kerroksesta ja toiseksi on laskettava suhteellisen kallis salaus (eli SHA1) ennen kuin voit verrata allekirjoitustunnusta soittajalta. ja palvelimen rakentama, jota pidetään oikeana, koska takaosa tietää kaiken (melkein).

I / O: n auttamiseksi voidaan lisätä välimuisti (Memcache? Redis?) Ja poistaa tarve kallista matkaa pysyvään pinoon (Mongo? Postgres?).

AKKA ja Spray.io ovat erittäin tehokkaita käsittelemään edellä mainittua. Spray.io kapseloi vaiheet, joita tarvitaan HTTP-otsikkotietojen ja hyötykuorman purkamiseen. AKKA-toimijat mahdollistavat asynkronisten tehtävien suorittamisen itsenäisesti API-jäsentelystä. Tämä yhdistelmä vähentää pyyntöjen käsittelijän kuormitusta ja se voidaan merkitä vertailupisteellä siten, että useimpien sovellusliittymien käsittelyaika on alle 100 ms. Huomaa: Sanoin, ettei käsittelyaika ole vasteaika, en sisällytä verkon viivettä.

Huomaa: AKKA: n toimijoiden avulla on mahdollista käynnistää kaksi samanaikaista prosessia, yksi luvalle / todennukselle ja toinen liiketoimintalogiikalle. Sitten rekisteröidään takaisinsoittoihinsa ja yhdistetään tulokset. Tämä yhdenmukaistaa sovellusliittymän toteutusta puhelutasolla ottaen huomioon optimistisen lähestymistavan, että todennus onnistuu. Tämä lähestymistapa vaatii minimaalista tietojen toistoa, koska asiakkaan on lähetettävä kaikki liiketoimintalogiikan tarpeet, kuten käyttäjätunnus ja kaikki, mitä normaalisti poimit istunnosta. Kokemukseni mukaan tämän lähestymistavan saavuttaminen vähentää toteutusaikaa noin 10% ja se on kallis sekä suunnittelu- että ajoaikana, koska se käyttää enemmän prosessoria ja enemmän muistia. Voi kuitenkin olla tilanteita, joissa suhteellisen pieni voitto on sidoksissa siihen, että prosessoidaan miljoonia puheluita minuutissa, jolloin säästöt / hyödyt kasvaa. En useimmissa tapauksissa en kuitenkaan suosittelisi sitä.

Kun istunnon tunnus on ratkaistu käyttäjälle, voidaan välimuisti tallentaa käyttöprofiili, joka sisältää käyttöoikeustasot, ja verrata niitä yksinkertaisesti käyttöoikeustasoon, jota tarvitaan API-puhelun suorittamiseen.

API: n luvatason saamiseksi yksi jäsentää URI: n ja purkaa REST-resurssin ja tunnisteen (jos sellainen on) ja käyttää HTTP-otsikkoa tyypin purkamiseen.

Sano esimerkiksi, että haluat sallia rekisteröityneiden käyttäjien saada profiilinsa HTTP GET -sovelluksen kautta

/ API / v1 / minut

niin luvanmääritysasiakirja näyttäisi tällaisessa järjestelmässä:

{
 ”V1 / me”: [{
 “Järjestelmänvalvoja”: [“saada”, “laittaa”, “lähettää”, “poistaa”]
 }, {
 “Rekisteröity”: [“saada”, “laittaa”, “lähettää”, “poistaa”]
 }, {
 “Read_only”: [“get”]
 }, {
 ”Estetty”: []
 }],
 "Lähetä": [{
 “Järjestelmänvalvoja”: [“laita”, “lähetä”, “poistaa”]
 }, {
 “Rekisteröity”: [“lähettää”, “poistaa”]
 }, {
 "Lue ainoastaan": []
 }, {
 ”Estetty”: []
 }]
}

Lukijan tulisi huomata, että tämä on välttämätön, mutta ei riittävä edellytys tietojen käyttöoikeuden luvalle. Toistaiseksi olemme todenneet, että kutsuvalla asiakkaalla on valtuudet soittaa ja että käyttäjällä on lupa käyttää API: ta. Monissa tapauksissa meidän on kuitenkin myös varmistettava, että käyttäjä A ei näe (tai muokkaa) käyttäjän B-tietoja. Joten laajennamme merkintää ”get_owner” tarkoittaa, että todennetuilla käyttäjillä on lupa suorittaa GET vain, jos he omistavat resurssin. Katsotaanpa, miltä kokoonpano näyttäisi silloin:

{
 ”V1 / me”: [{
 “Järjestelmänvalvoja”: [“saada”, “laittaa”, “lähettää”, “poistaa”]
 }, {
 “Rekisteröity”: [“get_owner”, “laita”, “lähettää”, “poistaa”]
 }, {
 ”Read_only”: [“get_owner”]
 }, {
 ”Estetty”: []
 }],
 "Lähetä": [{
 “Järjestelmänvalvoja”: [“laita”, “lähetä”, “poistaa”]
 }, {
 ”Rekisteröity”: [“put_owner”, “post”, “delete”]
 }, {
 "Lue ainoastaan": []
 }, {
 ”Estetty”: []
 }]
}

Nyt rekisteröitynyt käyttäjä voi käyttää omaa profiiliaan, lukea sitä, muokata sitä, mutta kukaan muu ei voi (muu kuin järjestelmänvalvoja). Samoin vain omistaja voi päivittää lähetyksen seuraavilla:

/ API / lähetä / 

Tämän lähestymistavan voima on, että dramaattiset muutokset siihen, mitä käyttäjät voivat ja eivät voi tehdä tiedoilla, voidaan suorittaa yksinkertaisesti muuttamalla lupakonfiguraatiota, koodimuutoksia ei tarvita. Siten tuotteen elinkaaren aikana takaosa voi vastata vaatimuksien muutoksia hetkelliseen ilmoitukseen.

Täytäntöönpano voidaan kapseloida muutamiin toimintoihin, jotka voivat olla agnostiikka sovellusliittymän liiketoimintalogiikan suhteen ja vain toteuttaa ja valvoa todennusta ja lupaa:

def validateSessionToken (sessionToken: String) UserProfile = {
...
}
def checkPermission (
  menetelmä: String,
  resurssi: String,
  Käyttäjä: USERPROFILE
) {
...
// vie poikkeuksen epäonnistumisesta
}

Näitä kutsutaan API-kutsujen Spray.io-käsittelyn alussa:

// HUOMAUTUS: profileReader ja sumbissionWriter jätetään täältä pois, oletetaan, että ne laajentavat AKKA-näyttelijää.
def reitti =
{
pathPrefix ( "API") {
  // ota otsikot ja HTTP-tiedot
  ...
  var käyttäjä: UserProfile = nolla
  yrittää {
    validatedSessionToken (sessionToken)
  } saalis (e: Poikkeus) {
    täydellinen (completeWithError (e.getMessage))
  }
  yrittää {
    checkPermission (menetelmä, resurssi, käyttäjä)
  } saalis (e: Poikkeus) {
    täydellinen (completeWithError (e.getMessage))
  }
  pathPrefix ( "v1") {
    polku ( "me") {
      saada {
        valmis (profileReader? getUserProfile (user.id))
      }
    }
  } ~
  polku ( "lähetä") {
    viesti {
      kokonaisuus (nimellä [String]) {=> jsonstr
        val hyötykuorma = lue [SubmitPayload] (jsonstr)
        täydellinen (SubmitWriter? sumbit (hyötykuorma))
      }
    }
  }
  ...
}

Kuten voimme nähdä, tämä lähestymistapa pitää Spray.io-käsittelijän luettavana ja helposti ylläpidettävänä, koska se erottaa todennuksen / luvan kunkin sovellusliittymän yksilöllisestä liiketoimintalogiikasta. Tietojen omistajuuden valvonta, jota ei ole esitetty tässä, voidaan saavuttaa siirtämällä Boolen arvo I / O-kerrokselle, joka sitten pakottaisi käyttäjän datan omistajuuden pysyvyystasolla.

Vaihe 2, liiketoimintalogiikka

Liiketoimintalogiikka voidaan kapseloida I / O-toimijoihin, kuten yllä olevassa koodinpätkyssä mainittu SubmitWriter. Tämä toimija toteuttaisi asynkronisen I / O-operaation, joka suorittaa kirjoitukset ensin välimuistikerrokselle, esimerkiksi Elasticsearch, ja toiseksi valitulle DB: lle. DB-kirjoitukset voidaan erottaa edelleen tulipalo- ja unohdelogiikka, joka käyttäisi lokipohjaista palautusta, jotta asiakkaan ei tarvitse odottaa näiden kalliiden toimintojen suorittamista.

Huomaa, että tämä on optimistinen lukkiutumaton lähestymistapa ja ainoa tapa, jolla asiakas voi olla varma tietojen kirjoittamisesta, on lukemisen seuranta. Siihen saakka, matkaviestinasiakkaan tulisi toimia olettaen, että vastaava välimuistiversio on likainen.

Tämä on erittäin voimakas suunnittelumalli, mutta lukijalle tulisi varoittaa, että AKKA + Spary.io -sovelluksella et voi mennä yli kolmen tason syvyyteen näyttelijäpuhelupinoon. Esimerkiksi jos nämä ovat järjestelmän toimijoita:

  1. S Spray-reitittimelle.
  2. A API-käsittelijälle.
  3. B I / O-käsittelijälle.

käyttämällä merkintää x? y tarkoittaen, että x kutsuu y soittamaan takaisinsoittoa ja x! y tarkoita, että x sytyttää ja unohtaa y, seuraava toimii:

S? A! B

Nämä eivät kuitenkaan:

S! A! B

S? A! B! B

Näissä kahdessa tapauksessa kaikki B-tapaukset tuhoutuvat heti, kun A on valmis niin tehokkaasti, että sinulla on vain kerran mahdollisuus pakata kaikki tyhjä ladattu tieto tietokoneeseesi tuleen ja unohtaa näyttelijä. Uskon, että tämä on Spray-rajoitus eikä AKKA: ta, ja siihen on ehkä voitu puuttua tämän viestin julkaisemisen yhteydessä.

Viimeiseksi, I / O ja pysyvyys

Kuten yllä on osoitettu, voimme työntää hitaita kirjoitustoimintoja asynkronisiksi säikeiksi pitääksemme API POST / PUT -suorituskyvyn hyväksyttävässä suoritusajassa. Nämä vaihtelevat yleensä kymmenissä sekunnissa tai pieninä sadan millisekunnin ajan riippuen palvelinprofiilista ja kuinka paljon logiikkaa voidaan lykätä käyttämällä tulipalo- ja unohda-lähestymistapaa.

Usein on kuitenkin niin, että lukumäärä kirjoja ylittää yhden tai useamman suuruusluokan. Hyvä välimuistikäsittely on siten kriittinen, jotta saavutetaan korkea kokonaiskäyttö.

Huomaa: päinvastainen pätee IOT-maisemiin, joissa solmut tulevat aistitietokirjoitukset ylittävät lukeman useilla suuruusjärjestyksillä. Tässä tapauksessa maisema voidaan määrittää olemaan palvelinryhmä, joka on konfiguroitu suorittamaan vain kirjoituksia IOT-laitteista, omistaen toisen palvelinryhmän, jolla on erilaiset spesifikaatiot asiakkaiden API-puheluihin (käyttöliittymä). Useimmat, elleivät kaikki koodit, voitaisiin jakaa näiden kahden palvelinluokan kesken, ja ominaisuudet voidaan yksinkertaisesti kytkeä pois päältä kokoonpanon avulla suojausheikkouksien estämiseksi.

Suosittu tapa on käyttää Redis-kaltaista muistia. Redis toimii hyvin, kun sitä käytetään tallentamaan käyttöoikeudet todennusta varten, ts. Tiedot, jotka eivät muutu usein. Yksi Redis-solmu voi tallentaa jopa 250 mailia paria.

Lukuihin, jotka on kysyttävä välimuistista, tarvitsemme erilaisen ratkaisun. Elasticsearch, muistiindeksi, toimii poikkeuksellisen hyvin joko maantieteellisissä tiedoissa tai tiedoissa, jotka voidaan jakaa tyyppeihin. Esimerkiksi hakemisto nimeltä lähetykset tyyppikoirien ja moottoripyörien kanssa voidaan helposti kysyä saadaksesi viimeisimmät ehdotukset (alilukuja?) Tietyistä aiheista.

Esimerkiksi käyttämällä Elasticsearchin http API -merkintää:

curl -XPOST 'localhost: 9200 / lähetykset / koirat / _haku? kaunis' -d '
{
  "kysely": {
    "suodatettu": {
      "query": {"match_all": {}},
      "suodatin": {
        "alue": {
          "luotu": {
            "gte": 1464913588000
          }
        }
      }
    }
  }
}'

palauttaisi kaikki asiakirjat / koirat määritetyn päivämäärän jälkeen. Samoin voimme etsiä kaikkia viestejä / lähetyksiä / moottoripyöriä, joiden asiakirjat sisältävät teoksen “Ducati”.

curl -XPOST 'localhost: 9200 / jättäminen / moottoripyörät / _haku? kaunis' -d '
{
  "query": {"match": {"text": "Ducati"}}
}'
Elastinen haku toimii erittäin hyvin lukemiin, kun hakemisto on suunniteltu ja luotu huolellisesti ennen tietojen syöttämistä. Tämä saattaa lannistaa joitain, koska yksi Elasticsearchin eduista on kyky luoda hakemisto yksinkertaisesti lähettämällä asiakirja ja antaa moottorin selvittää tyypit ja tietorakenteet. Rakenteen määrittelystä saatavat hyödyt kuitenkin ylittävät kustannukset, ja on huomattava, että siirtyminen uuteen hakemistoon on suoraviivaista jopa tuotantoympäristöissä, kun käytetään aliaksia.

Huomaa: Elastisen haun indeksit toteutetaan tasapainoisina puina, joten lisäys ja poisto voivat olla kalliita, kun puu kasvaa. Lisääminen hakemistoon, jossa on kymmeniä miljoonia asiakirjoja, voi viedä jopa kymmeniä sekunteja palvelinmäärityksistä riippuen. Tämä voi tehdä Elasticsearch-kirjoituksistasi yhden hitaimmista pilviprosesseista (tietysti lukuun ottamatta DB-kirjoituksia). Kirjoittaminen tuleen ja unohtaa AKKA-näyttelijä voi kuitenkin parantaa ongelmaa, ellei ratkaise ongelmaa.

johtopäätökset

Scala + AKKA + Spray.io ovat erittäin tehokas tekniikkapino korkean suorituskyvyn REST-sovellusliittymän rakentamiseen naimisissa muistivälimuistiin ja / tai muistin indeksointiin.

Työskentelin toteutuksen suhteen kaukana tässä kuvatuista käsitteistä, joissa 2000 osumaa minuutissa solmua kohti tuskin siirrettiin prosessorin kuormaa yli 1%.

Bonuskierros: koneoppiminen ja paljon muuta

Elasticsearchin lisääminen pinoon avaa oven sekä rivin että linjan koneoppimiselle, kun Elasticsearch integroituu Apache Sparkin kanssa. Koneoppimismoduulit voivat käyttää uudelleen samaa sovellusrajapinnan palvelemiseen käytettävää pysyvyyskerrosta, mikä vähentää koodausta, ylläpitokustannuksia ja pinon monimutkaisuutta. Viimeiseksi, Scala antaa meille mahdollisuuden käyttää mitä tahansa Scala- tai Java-kirjastoa, joka avaa oven kehittyneemmälle tietojenkäsittelylle hyödyntämällä muun muassa Stanfordin Core NLP: tä, OpenCV: tä, Spark Mlib: ää ja paljon muuta.

Linkit tässä viestissä mainittuihin tekniikoihin

  1. http://www.scala-lang.org
  2. http://spray.io
  3. ja (2) on järkevää katsoa sivua http://akka.io