Kuidas luua kiire ja vastupidav REST API Scala abil

"Kassi naha saamiseks on rohkem kui üks viis."

See on populaarne ütlus ja ehkki vaimne pilt võib olla häiriv, on see universaalne tõde, eriti informaatika alal.

See, mis järgneb, on seega viis REST API ehitamiseks Scalasse, mitte aga viis nende üles ehitamiseks.

Mõelgem praktilistel eesmärkidel, et ehitame Redditi-laadse rakenduse jaoks paar API-liidest, kus kasutajad saavad oma profiilile juurde pääseda ja värskendusi esitada. Redditi metafoorile tuginedes kujutleme, et rakendame (uuesti) api / v1 / mind ja api / esitame

Mõned maatööd

Lühidalt:

  1. Scala on lambda calculusel põhinev objektorienteeritud programmeerimiskeel, mis töötab Java virtuaalmasinas ja integreerub sujuvalt Java-ga.
  2. AKKA on Scala tippu ehitatud raamatukogu, mis pakub näitlejaid (mitme keermega ohutud objektid) ja palju muud.
  3. Spray.io on HTTP-teek, mis on üles ehitatud AKKA-le, pakkudes lihtsat ja paindlikku HTTP-protokolli juurutamist, et saaksite oma pilveteenust kasutada.

Väljakutse

Eeldatakse, et REST API pakub:

  1. kiire, turvaline kõnetaseme autentimine ja lubade kontroll;
  2. kiire äriloogika arvutamine ja I / O;
  3. kõik eelnimetatu üheaegselt;
  4. kas ma mainisin kiiret?

1. samm, autentimine ja luba

Autentimine tuleks rakendada OAUTH või OAUTH 2 või mõne isikliku / avaliku võtme autentimise maitsega.

OAUTH2 lähenemisviisi eeliseks on see, et saate seansimärgi (mille abil saate otsida vastavat kasutajakontot ja seanssi) ning allkirja žetooni.

Jätkame siin eeldusega, et just seda me kasutame.

Allkirja tunnus on tavaliselt krüptitud luba, mis saadakse allkirjastades kogu päringu koormus jagatud salajase võtmega, kasutades SHA1. Seega tapab allkirja märk ühe kiviga kaks lindu:

  1. see annab teada, kas helistaja teab õiget jagatud saladust;
  2. see hoiab ära andmesüstimise ja keskrünnakutes viibimise;

Ülaltoodu eest tuleb maksta paar hinda: esmalt peate andmed oma I / O kihist välja tõmbama ja teiseks peate enne helistaja allkirja toki võrdlemist arvutama suhteliselt kalli krüptimise (st SHA1). ja seda, mille server ehitab, peetakse õigeks, kuna tagakülg teab kõiki (peaaegu).

I / O abistamiseks saab lisada vahemälu (Memcache? Redis?) Ja eemaldada vajadus kuluka reisi järele püsivasse virna (Mongo? Postgres?).

AKKA ja Spray.io on ülalnimetatute käsitlemisel väga tõhusad. Spray.io sisaldab kapsleid, mis on vajalikud HTTP päise teabe ja kasuliku koormuse ekstraheerimiseks. AKKA näitlejad võimaldavad asünkroonseid toiminguid täita sõltumatult API parsimisest. See kombinatsioon vähendab päringukäsitleja koormust ja selle saab tähistada nii, et enamiku API töötlemisaeg on alla 100ms. Märkus. Ma ütlesin, et töötlemisaeg, mitte reageerimise aeg, ma ei arva võrgu latentsusaega.

Märkus: AKKA osalejaid kasutades on võimalik käivitada kaks samaaegset protsessi, üks loa / autentimise ja teine ​​äriloogika jaoks. Seejärel registreeritakse nende tagasikutsumisele ja liidetakse tulemused. See paralleelselt võimaldab API rakendamist kõne tasemel, kasutades optimistlikku lähenemisviisi, et autentimine õnnestub. See lähenemisviis nõuab andmete minimaalset kordamist, kuna klient peab saatma kõik äriloogika jaoks vajalikud andmed, näiteks kasutajatunnused ja kõik, mida tavaliselt seansist eraldate. Minu kogemuse kohaselt vähendab selle lähenemisviisi kasutamine täiteaega umbes 10% ja see on kallis nii projekteerimis- kui käitamisajal, kuna see kasutab rohkem protsessorit ja rohkem mälu. Võib siiski esineda stsenaariume, kus suhteliselt väike kasum on seotud miljonite kõnede töötlemise lõpptulemusega, suurendades kokkuhoidu / kasu. Enamikul juhtudel ei soovitaks ma seda.

Kui seansi tunnus on kasutajale lahendatud, saab vahemällu salvestada kasutajaprofiili, mis sisaldab õigustaset, ja võrrelda neid lihtsalt API-kõne jaoks vajaliku õigustasandiga.

API loa taseme saamiseks parsib URI ja ekstraheerib REST ressursi ja identifikaatori (kui see on olemas) ning üks kasutab HTTP päist tüübi ekstraheerimiseks.

Ütle näiteks, et soovite lubada registreeritud kasutajatel saada oma profiili HTTP GET-i kaudu

/ api / v1 / mina

siis näeks sellises süsteemis välja selline loa konfiguratsioonidokument:

{
 „V1 / mina”: [{
 „Admin”: [„saada”, „panna”, „postitada”, „kustutada”]
 }, {
 “Registreeritud”: [“saada”, “panna”, “postitada”, “kustutada”]
 }, {
 “Read_only”: [“get”]
 }, {
 „Blokeeritud”: []
 }],
 "Esita": [{
 „Admin”: [„pane”, „postita”, „kustuta”]
 }, {
 “Registreeritud”: [“postita”, “kustuta”]
 }, {
 "Loe ainult": []
 }, {
 „Blokeeritud”: []
 }]
}

Lugeja peaks arvestama, et see on vajalik, kuid mitte piisav tingimus andmetele juurdepääsu saamiseks. Siiani oleme tuvastanud, et helistajal kliendil on luba helistada ja kasutajal on API-le juurdepääsu luba. Kuid paljudel juhtudel peame ka tagama, et kasutaja A ei näe (või redigeeri) kasutaja B andmeid. Laiendame märkust sõnaga „get_owner”, mis tähendab, et autentitud kasutajatel on luba GET-i käivitada ainult siis, kui see ressurss kuulub. Vaatame siis, kuidas see konfiguratsioon välja näeks:

{
 „V1 / mina”: [{
 „Admin”: [„saada”, „panna”, „postitada”, „kustutada”]
 }, {
 “Registreeritud”: [“saada_omanik”, “panna”, “postitada”, “kustutada”]
 }, {
 “Read_only”: [“get_owner”]
 }, {
 „Blokeeritud”: []
 }],
 "Esita": [{
 „Admin”: [„pane”, „postita”, „kustuta”]
 }, {
 „Registreeritud”: [„pane_omanik”, „postita”, „kustuta”]
 }, {
 "Loe ainult": []
 }, {
 „Blokeeritud”: []
 }]
}

Nüüd pääseb registreeritud kasutaja juurde oma profiilile, saab seda lugeda, seda muuta, kuid keegi teine ​​ei saa (peale administraatori). Samamoodi saab ainult omanik värskendada esitust järgmistel viisidel:

/ api / Submit / 

Selle lähenemisviisi tugevuseks on see, et dramaatilisi muudatusi selles, mida kasutajad saavad andmetega teha ja mida mitte, saab teha lihtsalt loa konfiguratsiooni muutmisega, koodimuudatusi pole vaja. Seega saab toote elutsükli vältel sobitada muudatused nõuetes hetkega.

Täitmise võib kapseldada mitmeks funktsiooniks, mis võivad olla API äriloogikast agnostilised ning rakendavad ja jõustavad lihtsalt autentimise ja loa:

def valideSessionToken (sessionToken: String) UserProfile = {
...
}
def checkPermission (
  meetod: keelpill,
  ressurss: keelpill,
  kasutaja: UserProfile
) {
...
// viskab ebaõnnestumise osas erandi
}

Neid kutsutakse API-kõnede töötlemise Spray.io alguses:

// MÄRKUS: profileReader ja sumbissionWriter jäetakse siit välja, eeldades, et nad laiendavad AKKA näitlejat.
def marsruut =
{
pathPrefix ("api") {
  // ekstraktige päised ja HTTP-teave
  ...
  var kasutaja: UserProfile = null
  proovige {
    valideSessionToken (sessionToken)
  } saak (e: erand) {
    täielik (completeWithError (e.getMessage))
  }
  proovige {
    checkPermission (meetod, ressurss, kasutaja)
  } saak (e: erand) {
    täielik (completeWithError (e.getMessage))
  }
  pathPrefix ("v1") {
    tee ("mina") {
      saada {
        täielik (profileReader? getUserProfile (user.id))
      }
    }
  } ~
  tee ("esitama") {
    postita {
      entiteet (kui [String]) {=> jsonstr
        val payload = loe [SubmitPayload] (jsonstr)
        täielik (IesutamineWriter? sumbit (kasulik koormus))
      }
    }
  }
  ...
}

Nagu näeme, hoiab see lähenemisviis Spray.io käitlejat loetava ja hõlpsalt hooldatavana, kuna see eraldab autentimise / loa iga API individuaalsest äriloogikast. Andmete omandiõiguse jõustamist, mida siin pole näidatud, saab saavutada Booleani viimisega I / O kihti, mis seejärel jõustab kasutajaandmete omandiõiguse püsivuse tasemel.

2. samm, äriloogika

Äriloogika saab kapseldada I / O-osalistesse, nagu ülaltoodud koodilõikes mainitud SubmitWriter. See osaleja rakendaks asünkroonset sisend / väljundoperatsiooni, mis teostab kirjutamist vahemälu kihti, näiteks Elasticsearch, ja teiseks valitud DB-sse. DB kirjutab saab täiendavalt lahti siduda tulekahju ja unustada loogika, mis kasutaks logipõhist taastamist, nii et klient ei peaks nende kallite toimingute lõpetamist ootama.

Pange tähele, et see on optimistlik mitteblokeeruv lähenemisviis ja ainus viis, kuidas klient saab kindel olla, et andmed on kirjutatud, on loetud järelmeetmed. Kuni selle ajani peaks mobiilklient tegutsema eeldusel, et vastavad vahemällu salvestatud andmed on räpased.

See on väga võimas kujundusparadigma, kuid lugejat tuleks hoiatada, et rakendusega AKKA + Spary.io ei saa te näitlejakõnes üle kolme taseme minna. Näiteks kui need on süsteemis osalejad:

  1. S pihustusruuteri jaoks.
  2. A API-käitlejale.
  3. B I / O-käitlejale.

kasutades märget x? y tähendamaks, et x kutsub y tagasihelistamist ja x! y tähendab seda, et x süttib ja unustab y, toimib järgmine:

S? A! B

Need aga ei:

S! A! B

S? A! B! B

Neil kahel juhul hävitatakse kõik B juhtumid niipea, kui A on nii edukalt lõpule jõudnud, on teil vaid üks kord võimalus kogu oma mahalaaditud arvutus tulle pakatada ja näitleja unustada. Ma usun, et see on Spray ja mitte AKKA piirang ning see võis selle postituse avaldamise ajaks sellega tegeleda.

Viimaseks: I / O ja püsivus

Nagu ülal näidatud, võime lükata aeglased kirjutamistoimingud asünkroonsetesse lõimedesse, et hoida API POST / PUT jõudlus vastuvõetava täitmisaja jooksul. Need ulatuvad tavaliselt kümnete sekunditeni või saja millisekundini madalaks, sõltuvalt serveri profiilist ja sellest, kui palju loogikat saab tule ja unusta lähenemise abil edasi lükata.

Kuid sageli on nii, et loeb arvulisi kirju ühe või mitme suurusjärgu võrra. Hea vahemälul põhinev lähenemisviis on seega ülitäpse läbilaskevõime saavutamiseks kriitilise tähtsusega.

Märkus: IOT-maastike puhul, kus sõlmedest pärit sensoorsed andmekirjutused ületavad lugemise mitme suurusjärgu võrra, on vastupidine. Sel juhul saab maastiku konfigureerida nii, et serverirühm on konfigureeritud teostama ainult IOT-seadmetest kirjutatavaid servereid, pühendades teise serverite rühma, millel on erinevad spetsifikatsioonid, kliendi API-kõnedele (kasutajaliides). Enamiku kui mitte kogu koodialust saaks nende kahe serveriklassi vahel jagada ning turvahaavatavuste vältimiseks saaks funktsioonid konfiguratsiooni kaudu lihtsalt välja lülitada.

Populaarne lähenemisviis on kasutada mälupuhverit nagu Redis. Redis toimib hästi, kui seda kasutatakse autentimiseks kasutajalubade salvestamiseks, st andmete jaoks, mis ei muutu sageli. Üks Redise sõlme mahutab kuni 250 mi paari.

Lugemiste jaoks, mis vajavad vahemälust päringut, vajame teistsugust lahendust. Elasticsearch ehk mäluindeks töötab erakordselt hästi nii geograafiliste andmete kui ka tüüpide kaupa jaotatavate andmete puhul. Näiteks saab hõlpsalt küsida indeksit, mis kannab nime tüüpi koerad ja mootorrattad, et saada teatud teemadel uusim esitamine (alamkrediidid?).

Näiteks kasutades Elasticsearchi http API märget:

curl -XPOST 'localhost: 9200 / esildised / koerad / _otsing? ilus' -d '
{
  "päring": {
    "filtreeritud": {
      "query": {"match_all": {}},
      "filter": {
        "vahemik": {
          "loodud": {
            "gte": 1464913588000
          }
        }
      }
    }
  }
} '

tagastaks pärast dokumendi / koeri kõik dokumendid pärast määratud kuupäeva. Samamoodi võiksime otsida kõiki postitusi kataloogidest / esildistest / mootorratastest, mille dokumendid sisaldavad teost “Ducati”.

curl -XPOST 'localhost: 9200 / esitamine / mootorrattad / _otsing? päris' -d '
{
  "päring": {"vaste": {"tekst": "Ducati"}}
} '
Elasticsearch toimib väga hästi lugemiste jaoks, kui register on enne andmete sisestamist hoolikalt kujundatud ja loodud. See võib mõnda heidutada, kuna üks Elasticsearchi eeliseid on võime luua indeks lihtsalt dokumendi postitamise teel ja lasta mootoril välja mõelda tüübid ja andmestruktuurid. Kuid struktuuri määratlemisest tulenev kasu kaalub üles kulud ja tuleb märkida, et varjunimede kasutamisel on uuele indeksile üleminek lihtne isegi tootmiskeskkonnas.

Märkus: elastse otsingu indekseid rakendatakse tasakaalustatud puudena, seega võib puu lisamine ja kustutamine olla kallis, kui puu suureks saab. Kümnete miljonite dokumentidega indeksisse sisestamine võib sõltuvalt serveri spetsifikatsioonist võtta kuni kümneid sekundeid. See võib muuta teie Elasticsearchi kirjutamise teie pilves üheks aeglasemaks töötavaks protsessiks (muidugi peale DB kirjutab). Kui aga kirjutada tulekahju ja unustada AKKA näitleja, saab seda leevendada, kui see probleemi ei lahenda.

Järeldused

Scala + AKKA + Spray.io on väga tõhus tehnoloogiapakk suure jõudlusega REST API loomiseks, kui ta on abielus mälu vahemällu salvestamise ja / või mälu indekseerimisega.

Töötasin siin kirjeldatud kontseptsioonidest mitte liiga kaugel, kus 2000 tabamust minutis sõlme kohta viis CPU koormus vaevalt üle 1%.

Boonusvoor: masinõpe ja palju muud

Elasticsearchi lisamine virna avab ukse nii reaalajas kui ka väljaspool seda töötava masina õppimiseks, kuna Elasticsearch integreerub Apache Sparkiga. Sama püsivuskihti, mida kasutatakse API teenindamiseks, saab masinõppe moodulites uuesti kasutada, vähendades kodeerimist, hoolduskulusid ja virna keerukust. Ja lõpuks, Scala võimaldab meil kasutada mis tahes Scala või Java teeki, mis avab ukse keerukamale andmetöötlusele, võimendades selliseid asju nagu Stanfordi Core NLP, OpenCV, Spark Mlib ja palju muud.

Lingid selles postituses nimetatud tehnoloogiatele

  1. http://www.scala-lang.org
  2. http://spray.io
  3. ja et (2) oleks mõistlik, siis vaadake lehte http://akka.io