Creeu una API REST ràpida i robusta amb Scala

"Hi ha més d'una manera de pelar un gat".

Aquesta és una dita popular i, tot i que la imatge mental pot ser inquietant, és una veritat universal, especialment per a la informàtica.

Per tant, el que segueix és una manera de construir l’API REST a Scala, no una manera de construir-la.

Per comoditat, suposem que estem creant algunes API per a una aplicació similar a Reddit que permetrà als usuaris accedir al seu perfil i enviar actualitzacions. Per aprofitar la metàfora de Reddit, imaginem que implementem api / v1 / me i api / submit (nou)

Alguns treballs de terra

En poques paraules:

  1. Scala és un llenguatge de programació orientat a objectes que es basa en el càlcul de Lambda, s'executa en una màquina virtual de Java i s'integra perfectament amb Java.
  2. AKKA és una biblioteca basada en Scala que ofereix actors (objectes segurs amb múltiples fils) i molt més.
  3. Spray.io és una biblioteca HTTP que es basa en AKKA i ofereix una implementació senzilla i flexible del protocol HTTP perquè pugueu implementar el vostre propi servei al núvol.

El repte

S'espera que l'API REST proporcioni:

  1. Autenticació i control de permisos a nivell de trucada ràpid i segur;
  2. càlcul lògic de negoci ràpid i E / S;
  3. tot això amb un alt grau de simultaneïtat;
  4. he esmentat ràpidament?

Pas 1, autenticació i permís

L'autenticació s'ha d'implementar a OAUTH o OAUTH 2 o una variant de l'autenticació amb clau privada / pública.

L’avantatge d’un enfocament OAUTH2 és que obteniu un testimoni de sessió (que podeu utilitzar per obtenir el compte d’usuari i la sessió adequats) i un testimoni de signatura en un instant, més sobre això.

Continuarem aquí assumint que això és el que estem utilitzant.

El testimoni de signatura sol ser un testimoni xifrat que s’obté signant tota la càrrega útil de la sol·licitud amb una clau secreta compartida mitjançant SHA1. La fitxa signatura mata dos ocells d'una sola pedra:

  1. Aquí podeu esbrinar si la persona que truca coneix el secret compartit correcte.
  2. impedeix la injecció de dades i els atacs home-al-mig;

Hi ha alguns costos a pagar per això: primer, heu de treure les dades de la capa d'E / S i, en segon lloc, heu de calcular un xifratge relativament car (és a dir, SHA1) abans de poder comparar el testimoni de signatura de la persona que truca i el del servidor que es considera la correcta, ja que la part posterior (quasi) les coneix totes.

Es pot afegir una memòria cau (Memcache? Redis?) Per donar suport a E / S i ja no cal un viatge car a la pila persistent (Mongo? Postgres?).

AKKA i Spray.io són molt eficaços per resoldre els problemes anteriors. Spray.io encapsula els passos necessaris per extreure informació de capçalera HTTP i càrrega útil. Els actors AKKA permeten realitzar tasques asíncrones independentment de l'anàlisi de l'API. Aquesta combinació redueix la càrrega del controlador de sol·licituds i es pot comparar de manera que la majoria de les API tinguin un temps de processament inferior a 100 ms. Nota: He indicat que el temps de processament no és un temps de resposta. La latència de la xarxa no es té en compte.

Nota: Amb els actors AKKA, es poden activar dos processos que s'executen alhora, un per a l'autorització / autenticació i un per a la lògica empresarial. A continuació, us registrareu per obtenir les devolucions de trucades i agregareu els resultats. Això paral·lela la implementació de l'API a nivell de trucada, adoptant l'enfocament optimista perquè l'autenticació tingui èxit. Aquest enfocament requereix una mínima iteració de dades, ja que el client ha d'enviar tot el que necessita la lògica empresarial, com ara: Com ara l’identificador d’usuari i qualsevol cosa que normalment extret de la sessió. Segons la meva experiència, obtenir aquest enfocament redueix aproximadament un 10% el temps d'execució i és car tant en temps de disseny com en temps d'execució, ja que utilitza més CPU i més memòria. No obstant això, pot haver-hi escenaris en què el benefici relativament baix estigui relacionat amb el processament de milions de trucades per minut, augmentant l’estalvi / beneficis. En la majoria dels casos, però, no ho recomano.

Tan bon punt s’ha resolt el testimoni de sessió per a un usuari, el perfil d’usuari amb els nivells d’autorització es pot guardar a la memòria cau i es pot comparar fàcilment amb el nivell d’autorització requerit per a la trucada API.

Per obtenir el nivell d'autorització d'una API, s'analitza l'URI i s'extreu el recurs REST i l'identificador (si escau) i s'utilitza la capçalera HTTP per extreure el tipus.

Suposem que voleu permetre als usuaris registrats obtenir el seu perfil mitjançant un HTTP GET

/ api / v1 / me

llavors és com seria un document de configuració de permisos en aquest sistema:

{"V1 / me": [{"Admin": ["get", "put", "post", "delete"]}, {"Registered": ["get", "put", "post", "delete"]}, {"read_only": ["get"]}, {"blocked": []}], "submit": [{"Admin": ["put", "post", "delete" ]}, {"Registered": ["Post", "Delete"]}, {"Only-read": []}, {"Clogged": []}]}

El lector ha de tenir en compte que es tracta d’una condició necessària però no suficient per al permís d’accés a les dades. Fins ara hem determinat que el client que truca està autoritzat a fer la trucada i que l'usuari té autorització per accedir a l'API. Tot i això, en molts casos també hem de garantir que l’usuari A no pugui veure (ni editar) les dades de l’usuari B. Per tant, ampliem la notació amb "get_owner", el que significa que els usuaris autenticats només tenen l'autorització per executar un GET si són el propietari del recurs. Vegem com seria la configuració:

{"V1 / me": [{"Admin": ["get", "put", "post", "delete"]}, {"Registered": ["get_owner", "put", "post", "delete"]}, {"Read_only": ["get_owner"]}, {"clogged": []}], "submit": [{"Admin": ["put", "post", "delete" ]}, {"Registered": ["put_owner", "post", "delete"]}, {"write-protected": []}, {"blocked": []}]}

Ara un usuari registrat pot accedir, llegir i canviar el seu propi perfil, però ningú més ho pot fer (excepte un administrador). De la mateixa manera, només el propietari pot actualitzar una publicació amb:

/ api / enviar /

La força d’aquest enfocament és que simplement canviar la configuració dels permisos pot fer canvis dramàtics en el que poden fer els usuaris amb les dades. No cal fer cap canvi de codi. D’aquesta manera, el back-end pot adaptar instantàniament els canvis als requisits durant el cicle de vida del producte.

L'aplicació es pot encapsular en diverses funcions que no estan relacionades amb la lògica empresarial de l'API i només implementen i fan complir l'autenticació i l'autorització:

def validateSessionToken (sessionToken: String) UserProfile = {...} def checkPermission (Mètode: String, Recurs: string de caràcters, User: UserProfile) {... // Llança una excepció en cas d'error
}

Aquestes es diuen al començament de la gestió de Spray.io de les trucades API:

// NOTA: profileReader i sumbissionWriter s’omet aquí si amplien un actor AKKA.
ruta def = = pathPrefix ("api") {
// extreu les capçaleres i la informació HTTP
... var usuari: UserProfile = null Proveu {validatedSessionToken (sessionToken)} catch (e: Exception) {complete (completeWithError (e.getMessage))}
Proveu {checkPermission (mètode, recurs, usuari)} catch (e: Exception) {complete (completeWithError (e.getMessage))}
pathPrefix ("v1") {path ("I") {get {complete (profileReader? getUserProfile (user.id))}
}
} ~ camí ("enviar") {publicar {
entitat (com a [Cadena]) {=> jsonstr val payload = read [SubmitPayload] (jsonstr)
complet (submissióWriter? sumbit (càrrega útil))
}}
} ...
}

Com podem veure, d’aquesta manera el controlador Spray.io continua llegint i és fàcil de mantenir, ja que l’autenticació / autorització se separa de la lògica empresarial individual de cada API. L'aplicació de la propietat de dades, que no es mostra aquí, es pot aconseguir passant un valor booleà a la capa d'E / S, que després imposa la propietat de les dades dels usuaris al nivell de persistència.

Pas 2, lògica empresarial

La lògica empresarial es pot encapsular en actors d'E / S com el SubmissionWriter esmentat al fragment de codi anterior. Aquest actor implementaria una operació d'E / S asíncrona que escriu primer a una capa de memòria cau, com ara Elasticsearch, i després a una base de dades que trieu. Les escriptures de base de dades es poden desvincular encara més en foc i oblidar la lògica que utilitza la recuperació basada en registres de manera que el client no hagi d’esperar a que finalitzin aquestes costoses operacions.

Tingueu en compte que es tracta d’un enfocament optimista i sense bloqueig i que l’única manera que el client pot assegurar que s’han escrit les dades és si es realitza una lectura. Fins aleshores, un client mòbil hauria de treballar suposant que les dades de la memòria cau respectives estiguessin brutes.

Aquest és un paradigma de disseny molt potent, però, s’hauria de comunicar al lector que AKKA + Spary.io no us permet entrar a la pila de trucades d’actors de més de tres nivells. Per exemple, si aquests són els actors del sistema:

  1. S per a l’encaminador per aspersió.
  2. A per al controlador d'API.
  3. B per al controlador d'E / S.

Quan obtingueu la x? Utilitzeu la notació y per indicar que x cridarà a y i sol·licitarà una devolució de trucada, i x! y significa x dispara i y oblida, això és el que funciona:

S? UN! B.

Tot i això, no:

S! UN! B.

S? UN! B! B.

En tots dos casos, una vegada que A es completi de manera tan eficaç que només tingueu l'oportunitat única d'incendiar tot el càlcul descarregat i oblidar-vos de l'actor, totes les instàncies de B es destruiran. Crec que es tracta d’una limitació d’aspersió, no AKKA, i que podria haver-se solucionat en el moment d’aquesta publicació.

Per últim, E / S i persistència

Com es mostra més amunt, les escriptures lentes es poden enviar a fils asíncrons per mantenir el rendiment de l'API POST / PUT dins d'un temps d'execució acceptable. Normalment, es troben en un interval d’unes quantes desenes de segons o menys de cent mil·lisegons, depenent del perfil del servidor i de la quantitat de lògica que es pot diferir mitjançant l’enfocament ignició i oblit.

Tot i així, sovint es dóna el cas que el nombre d’escriptures llegides supera un o més ordres de magnitud. Per tant, un bon enfocament de la memòria cau és fonamental per aconseguir un rendiment global elevat.

Nota: el contrari és cert per als paisatges IOT, on les dades sensorials que provenen dels nodes són diversos ordres de magnitud per sobre del valor de lectura. En aquest cas, el paisatge es pot configurar per configurar un grup de servidors perquè només facin escriptures des de dispositius IoT, assignant així un altre grup de servidors amb especificacions diferents a les trucades API dels clients (front-end) . La majoria de les bases de codi, si no totes, es poden compartir entre aquestes dues classes de servidors, i les funcions es poden desactivar simplement mitjançant la configuració per evitar fallades de seguretat.

Un enfocament popular és utilitzar una memòria cau de memòria com Redis. Redis té un bon rendiment en emmagatzemar els drets d’usuari per a l’autenticació; H. Dates que no canvien sovint. Un sol node Redis pot emmagatzemar fins a 250 mi parells.

Per a les lectures que necessiten consultar la memòria cau, necessitem una solució diferent. Elasticsearch, un índex a la memòria, és ideal per a dades geogràfiques o per a dades que es poden dividir en tipus. Per exemple, es pot consultar fàcilment un índex anomenat Enviaments amb els tipus Gossos i Motocicletes per obtenir els darrers subredits per a temes específics.

Per exemple, utilitzeu la notació de l'API http d'Elasticsearch:

curl -XPOST 'localhost: 9200 / submissions / dogs / _suche? pretty '-d' {"query": {"filtered": {"query": {"match_all": {}}, "filter": {"offer": {"created": {"gte": 1464913588000} }}}}} '

retornaria tots els documents després de la data especificada a / dogs. També podríem cercar tots els enviaments a / Enviaments / Motocicletes els documents dels quals contenen l’obra “Ducati”.

curl -XPOST 'localhost: 9200 / submit / motos / _search? pretty '-d' {"query": {"match": {"text": "Ducati"}}} '
Elasticsearch és molt bo per llegir si l’índex s’ha dissenyat i construït amb cura abans de l’entrada de dades. Això en pot dissuadir alguns, ja que un dels avantatges d’Elasticsearch és la possibilitat de crear un índex simplement publicant un document i deixant al motor descobrir tipus i estructures de dades. Tanmateix, els avantatges de definir l’estructura superen els costos i cal tenir en compte que la migració a un nou índex és fàcil en entorns de producció aliats.

Nota: els índexs Elasticsearch s’implementen com a arbres equilibrats. La inserció i la supressió d’índexs pot resultar costosa en arbres grans. La inserció en un índex de desenes de milions de documents pot trigar fins a deu segons, segons l'especificació del servidor. Això pot fer que les escriptures d'Elasticsearch siguin un dels processos més lents del núvol (a part de les escriptures de la base de dades, és clar). Tanmateix, si escriviu l'escriptura i us oblideu de l'actor AKKA, el problema pot millorar-se si no ho solucioneu.

Conclusions

Scala + AKKA + Spray.io és una pila de tecnologia molt eficaç per crear API REST d’alt rendiment si esteu casat amb la memòria cau de memòria i / o la indexació de memòria.

He estat treballant en una implementació que no s’allunya massa dels conceptes descrits aquí, on 2000 visites per minut per node amb prou feines van moure la càrrega de la CPU per sobre de l’1%.

Ronda de bonificació: aprenentatge automàtic i molt més

Si afegiu Elasticsearch al lot s’obre la porta a l’aprenentatge automàtic en línia i fora de línia ja que Elasticsearch s’incorpora a Apache Spark. La mateixa capa de persistència que s’utilitza per a l’API pot ser reutilitzada pels mòduls d’aprenentatge automàtic, reduint la codificació, els costos de manteniment i la complexitat de l’apilament. Finalment, amb Scala, podem utilitzar qualsevol biblioteca Scala o Java que obri les portes a una informàtica més sofisticada utilitzant, per exemple, el NLP bàsic de Stanford, OpenCV, Spark Mlib i molt més.

Enllaços a les tecnologies esmentades en aquest post

  1. http://www.scala-lang.org
  2. http://spray.io
  3. i amb això (2) que té sentit, mireu http://akka.io