Introducere în microservicii Python cu Nameko
Publicat: 2022-03-11Introducere
Modelul arhitectural pentru microservicii este un stil arhitectural care crește în popularitate, având în vedere flexibilitatea și rezistența sa. Împreună cu tehnologii precum Kubernetes, este din ce în ce mai ușor să porniți o aplicație folosind o arhitectură Microservicii, ca niciodată.
Potrivit unui articol clasic de pe blogul lui Martin Fowler, stilul arhitectural Microservices poate fi rezumat astfel:
Pe scurt, stilul arhitectural de microservicii este o abordare a dezvoltării unei singure aplicații ca o suită de servicii mici, fiecare rulând în propriul proces și comunicând cu mecanisme ușoare, adesea un API de resurse HTTP. Aceste servicii sunt construite în jurul capabilităților de afaceri și pot fi implementate în mod independent de către mașini de implementare complet automatizate.
Cu alte cuvinte, o aplicație care urmează o arhitectură de microservicii este compusă din mai multe servicii independente și dinamice care comunică între ele folosind un protocol de comunicare. Este obișnuit să folosim HTTP (și REST), dar, după cum vom vedea, putem folosi și alte tipuri de protocoale de comunicare, cum ar fi RPC (Remote Procedure Call) peste AMQP (Advanced Message Queuing Protocol).
Modelul de microservicii poate fi gândit ca un caz specific de SOA (architecture oriented service). În SOA, este obișnuit, totuși, să se folosească un ESB (bus de serviciu de întreprindere) pentru a gestiona comunicarea între servicii. ESB-urile sunt de obicei foarte sofisticate și includ funcționalități pentru rutarea mesajelor complexe și aplicarea regulilor de afaceri. În microservicii, este mai obișnuit să se folosească o abordare alternativă: „smart endpoints and dumb pipes”, ceea ce înseamnă că serviciile în sine ar trebui să conțină toată logica și complexitatea afacerii (coeziune ridicată), dar conexiunea dintre servicii ar trebui să fie la fel de simplă ca posibil (decuplare mare), ceea ce înseamnă că un serviciu nu trebuie neapărat să știe ce alte servicii vor comunica cu el. Aceasta este o separare a preocupărilor aplicată la nivel arhitectural.
Un alt aspect al microserviciilor este că nu există nicio punere în aplicare cu privire la tehnologiile care ar trebui utilizate în cadrul fiecărui serviciu. Ar trebui să puteți scrie un serviciu cu orice stivă de software care poate comunica cu celelalte servicii. Fiecare serviciu are și propriul său management al ciclului de viață. Toate acestea înseamnă că într-o companie, este posibil ca echipe să lucreze pe servicii separate, cu tehnologii diferite și chiar cu metodologii de management. Fiecare echipă va fi preocupată de capacitățile de afaceri, ajutând la construirea unei organizații mai agile.
Microservicii Python
Având în vedere aceste concepte, în acest articol ne vom concentra pe construirea unei aplicații de microservicii de dovadă a conceptului folosind Python. Pentru asta, vom folosi Nameko, un cadru de microservicii Python. Are RPC peste AMQP încorporat, permițându-vă să comunicați cu ușurință între serviciile dvs. Are, de asemenea, o interfață simplă pentru interogări HTTP, pe care o vom folosi în acest tutorial. Cu toate acestea, pentru scrierea de microservicii care expun un punct final HTTP, se recomandă să utilizați un alt cadru, cum ar fi Flask. Pentru a apela metode Nameko prin RPC folosind Flask, puteți folosi flask_nameko, un wrapper creat doar pentru interoperarea Flask cu Nameko.
Setarea mediului de bază
Să începem prin a rula cel mai simplu exemplu posibil, extras de pe site-ul web Nameko, și să-l extindem pentru scopurile noastre. În primul rând, veți avea nevoie de Docker instalat. Vom folosi Python 3 în exemplele noastre, așa că asigurați-vă că îl aveți și instalat. Apoi, creați un python virtualenv și rulați $ pip install nameko
.
Pentru a rula Nameko, avem nevoie de brokerul de mesaje RabbitMQ. Acesta va fi responsabil pentru comunicarea dintre serviciile noastre Nameko. Nu vă faceți griji, însă, deoarece nu trebuie să instalați încă o dependență pe mașina dvs. Cu Docker, putem pur și simplu să descărcați o imagine preconfigurată, să o rulăm și, când terminăm, pur și simplu să oprim containerul. Fără demoni, apt-get
sau dnf install
.
Porniți un container RabbitMQ rulând $ docker run -p 5672:5672 --hostname nameko-rabbitmq rabbitmq:3
(s-ar putea să aveți nevoie de sudo pentru a face asta). Aceasta va porni un container Docker folosind cea mai recentă versiune 3 RabbitMQ și o va expune peste portul implicit 5672.
Bună lume, cu microservicii
Continuați și creați un fișier numit hello.py
cu următorul conținut:
from nameko.rpc import rpc class GreetingService: name = "greeting_service" @rpc def hello(self, name): return "Hello, {}!".format(name)
Serviciile Nameko sunt clase. Aceste clase expun puncte de intrare, care sunt implementate ca extensii. Extensiile încorporate includ posibilitatea de a crea puncte de intrare care reprezintă metode RPC, ascultători de evenimente, puncte terminale HTTP sau cronometre. Există, de asemenea, extensii comunitare care pot fi folosite pentru a interacționa cu baza de date PostgreSQL, Redis, etc... Este posibil să vă scrieți propriile extensii.
Să mergem mai departe și să ne dăm exemplul. Dacă aveți RabbitMQ care rulează pe portul implicit, pur și simplu rulați $ nameko run hello
. Acesta va găsi RabbitMQ și se va conecta automat la el. Apoi, pentru a testa serviciul nostru, rulați $ nameko shell
într-un alt terminal. Aceasta va crea un shell interactiv care se va conecta la aceeași instanță RabbitMQ. Lucrul grozav este că, folosind RPC peste AMQP, Nameko implementează descoperirea automată a serviciilor. Când apelează o metodă RPC, nameko va încerca să găsească serviciul de rulare corespunzător.
Când rulați shell-ul Nameko, veți obține un obiect special numit n
adăugat la spațiul de nume. Acest obiect permite trimiterea de evenimente și efectuarea apelurilor RPC. Pentru a efectua un apel RPC către serviciul nostru, rulați:
> >> n.rpc.greetingservice.hello(name='world') 'Hello, world!'
Apeluri simultane
Aceste clase de servicii sunt instanțiate în momentul în care este efectuat un apel și sunt distruse după finalizarea apelului. Prin urmare, ar trebui să fie în mod inerent apatride, ceea ce înseamnă că nu ar trebui să încercați să păstrați nicio stare în obiect sau clasă între apeluri. Aceasta înseamnă că serviciile în sine trebuie să fie apatride. Presupunând că toate serviciile sunt apatride, Nameko este capabil să folosească concurența utilizând greenthread-uri eventlet. Serviciile instanțiate se numesc „lucrători” și poate exista un număr maxim configurat de lucrători care rulează în același timp.
Pentru a verifica concurența Nameko în practică, modificați codul sursă adăugând un sleep la apelul procedurii înainte de a returna răspunsul:
from time import sleep from nameko.rpc import rpc class GreetingService: name = "greeting_service" @rpc def hello(self, name): sleep(5) return "Hello, {}!".format(name)
Folosim sleep
din modulul de time
, care nu este activat pentru asincron. Cu toate acestea, atunci când rulăm serviciile noastre folosind nameko run
, va corecta automat randamentele declanșatorului de la blocarea apelurilor precum sleep(5)
.
Acum este de așteptat ca timpul de răspuns de la un apel de procedură să dureze aproximativ 5 secunde. Cu toate acestea, care va fi comportamentul din următorul fragment, când îl rulăm din shell-ul nameko?
res = [] for i in range(5): hello_res = n.rpc.greeting_service.hello.call_async(name=str(i)) res.append(hello_res) for hello_res in res: print(hello_res.result())
Nameko oferă o metodă call_async
non-blocante pentru fiecare punct de intrare RPC, returnând un obiect de răspuns proxy care poate fi apoi interogat pentru rezultatul său. Metoda result
, atunci când este apelată pe proxy-ul de răspuns, va fi blocată până când răspunsul este returnat.
După cum era de așteptat, acest exemplu rulează în doar aproximativ cinci secunde. Fiecare lucrător va fi blocat în așteptarea încheierii apelului de sleep
, dar acest lucru nu împiedică un alt lucrător să înceapă. Înlocuiți acest apel de sleep
cu un apel util de blocare a bazei de date I/O, de exemplu, și veți obține un serviciu simultan extrem de rapid.
După cum sa explicat mai devreme, Nameko creează lucrători atunci când este apelată o metodă. Numărul maxim de lucrători este configurabil. În mod implicit, acel număr este setat la 10. Puteți testa modificarea range(5)
din fragmentul de mai sus la, de exemplu, intervalul (20). Aceasta va apela metoda hello
de 20 de ori, care ar trebui să dureze acum zece secunde pentru a rula:
> >> res = [] > >> for i in range(20): ... hello_res = n.rpc.greeting_service.hello.call_async(name=str(i)) ... res.append(hello_res) > >> for hellores in res: ... print(hello_res.result()) Hello, 0! Hello, 1! Hello, 2! Hello, 3! Hello, 4! Hello, 5! Hello, 6! Hello, 7! Hello, 8! Hello, 9! Hello, 10! Hello, 11! Hello, 12! Hello, 13! Hello, 14! Hello, 15! Hello, 16! Hello, 17! Hello, 18! Hello, 19!
Acum, să presupunem că ați primit prea mulți (mai mult de 10) utilizatori concurenți care apelează acea metodă hello
. Unii utilizatori vor aștepta mai mult decât cele cinci secunde așteptate pentru răspuns. O soluție a fost creșterea numărului de lucrări prin suprascrierea setărilor implicite folosind, de exemplu, un fișier de configurare. Cu toate acestea, dacă serverul dvs. este deja la limita cu cei zece lucrători, deoarece metoda apelată se bazează pe unele interogări grele de baze de date, creșterea numărului de lucrători ar putea duce la creșterea și mai mult timpului de răspuns.
Extinderea serviciului nostru
O soluție mai bună este să utilizați capabilitățile Nameko Microservices. Până acum, am folosit doar un server (calculatorul dvs.), rulând o instanță de RabbitMQ și o instanță a serviciului. Într-un mediu de producție, veți dori să creșteți în mod arbitrar numărul de noduri care rulează serviciul care primesc prea multe apeluri. De asemenea, puteți construi un cluster RabbitMQ dacă doriți ca brokerul dvs. de mesaje să fie mai de încredere.
Pentru a simula o scalare a unui serviciu, putem pur și simplu să deschidem un alt terminal și să rulăm serviciul ca înainte, folosind $ nameko run hello
. Aceasta va începe o altă instanță de serviciu cu potențialul de a rula încă zece lucrători. Acum, încercați să rulați fragmentul respectiv din nou cu range(20)
. Acum ar trebui să dureze din nou cinci secunde pentru a rula. Când rulează mai mult de o instanță de serviciu, Nameko va combina cererile RPC printre instanțele disponibile.
Nameko este construit pentru a gestiona robust aceste apeluri de metode într-un cluster. Pentru a testa asta, încercați să rulați snipped și înainte de a se termina, mergeți la unul dintre terminalele care rulează serviciul Nameko și apăsați Ctrl+C
de două ori. Acest lucru ar închide gazda fără a aștepta ca lucrătorii să termine. Nameko va realoca apelurile către o altă instanță de serviciu disponibilă.

În practică, veți folosi Docker pentru a vă containeriza serviciile, așa cum vom face mai târziu, și un instrument de orchestrare, cum ar fi Kubernetes, pentru a vă gestiona nodurile care rulează serviciul și alte dependențe, cum ar fi brokerul de mesaje. Dacă este făcut corect, cu Kubernetes, ți-ai transforma efectiv aplicația într-un sistem distribuit robust, imun la vârfuri neașteptate. De asemenea, Kubernetes permite implementări fără timpi de nefuncționare. Prin urmare, implementarea unei noi versiuni a unui serviciu nu va afecta disponibilitatea sistemului dumneavoastră.
Este important să construiți servicii având în vedere o anumită compatibilitate cu versiunea anterioară, deoarece într-un mediu de producție se poate întâmpla ca mai multe versiuni diferite ale aceluiași serviciu să ruleze în același timp, mai ales în timpul implementării. Dacă utilizați Kubernetes, în timpul implementării, va ucide toate containerele versiunii vechi numai atunci când există suficiente containere noi care rulează.
Pentru Nameko, a avea mai multe versiuni diferite ale aceluiași serviciu care rulează în același timp nu este o problemă. Deoarece distribuie apelurile într-un mod round-robin, apelurile pot trece prin versiuni vechi sau noi. Pentru a testa asta, păstrați un terminal cu serviciul nostru care rulează versiunea veche și editați modulul de serviciu astfel încât să arate astfel:
from time import sleep from nameko.rpc import rpc class GreetingService: name = "greeting_service" @rpc def hello(self, name): sleep(5) return "Hello, {}! (version 2)".format(name)
Dacă rulați acel serviciu de pe alt terminal, veți obține cele două versiuni să ruleze în același timp. Acum, rulați din nou fragmentul nostru de testare și veți vedea ambele versiuni afișate:
> >> res = [] > >> for i in range(5): ... hello_res = n.rpc.greeting_service.hello.call_async(name=str(i)) ... res.append(hello_res) > >> for hellores in res: ... print(hello_res.result()) Hello, 0! Hello, 1! (version 2) Hello, 2! Hello, 3! (version 2) Hello, 4!
Lucrul cu mai multe instanțe
Acum știm cum să lucrăm eficient cu Nameko și cum funcționează scalarea. Să facem acum un pas mai departe și să folosim mai multe instrumente din ecosistemul Docker: docker-compose. Acest lucru va funcționa dacă implementați pe un singur server, ceea ce cu siguranță nu este ideal, deoarece nu veți profita de multe dintre avantajele unei arhitecturi de microservicii. Din nou, dacă doriți să aveți o infrastructură mai potrivită, puteți utiliza un instrument de orchestrare, cum ar fi Kubernetes, pentru a gestiona un sistem distribuit de containere. Deci, mergeți mai departe și instalați docker-compose.
Din nou, tot ce trebuie să facem este să implementăm o instanță RabbitMQ, iar Nameko se va ocupa de restul, având în vedere că toate serviciile pot accesa acea instanță RabbitMQ. Codul sursă complet pentru acest exemplu este disponibil în acest depozit GitHub.
Să construim o aplicație simplă de călătorie pentru a testa capabilitățile Nameko. Aplicația respectivă permite înregistrarea aeroporturilor și călătoriilor. Fiecare aeroport este pur și simplu stocat ca numele aeroportului, iar călătoria stochează ID-urile pentru aeroporturile de origine și destinație. Arhitectura sistemului nostru arată astfel:
În mod ideal, fiecare microserviciu ar avea propria instanță de bază de date. Cu toate acestea, pentru simplitate, am creat o singură bază de date Redis pentru a partaja atât microservicii Trips, cât și Airports. Microserviciul Gateway va primi solicitări HTTP printr-un API simplu asemănător REST și va folosi RPC pentru a comunica cu Aeroporturi și călătorii.
Să începem cu microserviciul Gateway. Structura sa este simplă și ar trebui să fie foarte familiară oricui provine dintr-un cadru precum Flask. În principiu definim două puncte finale, fiecare permițând atât metodele GET, cât și POST:
import json from nameko.rpc import RpcProxy from nameko.web.handlers import http class GatewayService: name = 'gateway' airports_rpc = RpcProxy('airports_service') trips_rpc = RpcProxy('trips_service') @http('GET', '/airport/<string:airport_id>') def get_airport(self, request, airport_id): airport = self.airports_rpc.get(airport_id) return json.dumps({'airport': airport}) @http('POST', '/airport') def post_airport(self, request): data = json.loads(request.get_data(as_text=True)) airport_id = self.airports_rpc.create(data['airport']) return airport_id @http('GET', '/trip/<string:trip_id>') def get_trip(self, request, trip_id): trip = self.trips_rpc.get(trip_id) return json.dumps({'trip': trip}) @http('POST', '/trip') def post_trip(self, request): data = json.loads(request.get_data(as_text=True)) trip_id = self.trips_rpc.create(data['airport_from'], data['airport_to']) return trip_id
Să aruncăm o privire la serviciul Aeroporturi acum. După cum era de așteptat, expune două metode RPC. Metoda get
va interoga pur și simplu baza de date Redis și va returna aeroportul pentru id-ul dat. Metoda de create
va genera un id aleatoriu, va stoca informațiile aeroportului și va returna id-ul:
import uuid from nameko.rpc import rpc from nameko_redis import Redis class AirportsService: name = "airports_service" redis = Redis('development') @rpc def get(self, airport_id): airport = self.redis.get(airport_id) return airport @rpc def create(self, airport): airport_id = uuid.uuid4().hex self.redis.set(airport_id, airport) return airport_id
Observați cum folosim extensia nameko_redis
. Aruncă o privire la lista de extensii ale comunității. Extensiile sunt implementate într-un mod care utilizează injecția de dependență. Nameko se ocupă de inițierea obiectului de extensie real pe care îl va folosi fiecare lucrător.
Nu există mare diferență între microservicii Aeroporturi și Călătorii. Iată cum ar arăta microserviciul Trips:
import uuid from nameko.rpc import rpc from nameko_redis import Redis class AirportsService: name = "trips_service" redis = Redis('development') @rpc def get(self, trip_id): trip = self.redis.get(trip_id) return trip @rpc def create(self, airport_from_id, airport_to_id): trip_id = uuid.uuid4().hex self.redis.set(trip_id, { "from": airport_from_id, "to": airport_to_id }) return trip_id
Dockerfile
pentru fiecare microserviciu este, de asemenea, foarte simplu. Singura dependență este nameko
, iar în cazul serviciilor Aeroporturi și călătorii, este nevoie să instalați nameko-redis
. Aceste dependențe sunt date în requirements.txt
din fiecare serviciu. Serviciul Dockerfile pentru Aeroporturi arată astfel:
FROM python:3 RUN apt-get update && apt-get -y install netcat && apt-get clean WORKDIR /app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY config.yml ./ COPY run.sh ./ COPY airports.py ./ RUN chmod +x ./run.sh CMD ["./run.sh"]
Singura diferență între acesta și Dockerfile pentru celelalte servicii este fișierul sursă (în acest caz airports.py
), care ar trebui schimbat în consecință.
Scriptul run.sh
are grijă să aștepte până la RabbitMQ și, în cazul serviciilor Aeroporturi și Călătorii, baza de date Redis este gata. Următorul fragment arată conținutul run.sh
pentru aeroporturi. Din nou, pentru celelalte servicii, schimbați doar de la aiports
la gateway
sau trips
în consecință:
#!/bin/bash until nc -z ${RABBIT_HOST} ${RABBIT_PORT}; do echo "$(date) - waiting for rabbitmq..." sleep 1 done until nc -z ${REDIS_HOST} ${REDIS_PORT}; do echo "$(date) - waiting for redis..." sleep 1 done nameko run --config config.yml airports
Serviciile noastre sunt acum gata de funcționare:
$ docker-compose up
Să ne testăm sistemul. Rulați comanda:
$ curl -i -d "{\"airport\": \"first_airport\"}" localhost:8000/airport HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Content-Length: 32 Date: Sun, 27 May 2018 05:05:53 GMT f2bddf0e506145f6ba0c28c247c54629
Ultima linie este id-ul generat pentru aeroportul nostru. Pentru a testa dacă funcționează, rulați:
$curl localhost:8000/airport/f2bddf0e506145f6ba0c28c247c54629 {"airport": "first_airport"} Great, now let's add another airport: $ curl -i -d "{\"airport\": \"second_airport\"}" localhost:8000/airport HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Content-Length: 32 Date: Sun, 27 May 2018 05:06:00 GMT 565000adcc774cfda8ca3a806baec6b5
Acum avem două aeroporturi, este suficient pentru a forma o călătorie. Să creăm o călătorie acum:
$ curl -i -d "{\"airport_from\": \"f2bddf0e506145f6ba0c28c247c54629\", \"airport_to\": \"565000adcc774cfda8ca3a806baec6b5\"}" localhost:8000/trip HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Content-Length: 32 Date: Sun, 27 May 2018 05:09:10 GMT 34ca60df07bc42e88501178c0b6b95e4
Ca și înainte, ultima linie reprezintă ID-ul călătoriei. Să verificăm dacă a fost introdus corect:
$ curl localhost:8000/trip/34ca60df07bc42e88501178c0b6b95e4 {"trip": "{'from': 'f2bddf0e506145f6ba0c28c247c54629', 'to': '565000adcc774cfda8ca3a806baec6b5'}"}
rezumat
Am văzut cum funcționează Nameko prin crearea unei instanțe locale de rulare a RabbitMQ, conectându-se la aceasta și efectuând mai multe teste. Apoi, am aplicat cunoștințele acumulate pentru a crea un sistem simplu folosind o arhitectură Microservicii.
În ciuda faptului că este extrem de simplu, sistemul nostru este foarte aproape de cum ar arăta o implementare pregătită pentru producție. De preferință, ați folosi un alt cadru pentru a gestiona solicitările HTTP, cum ar fi Falcon sau Flask. Ambele sunt opțiuni excelente și pot fi folosite cu ușurință pentru a crea alte microservicii bazate pe HTTP, în cazul în care doriți să vă întrerupeți serviciul Gateway, de exemplu. Flask are avantajul că are deja un plugin pentru a interacționa cu Nameko, dar poți folosi nameko-proxy direct din orice cadru.
Nameko este, de asemenea, foarte ușor de testat. Nu am acoperit testarea aici pentru simplitate, dar consultați documentația de testare a Nameko.
Cu toate părțile mobile din interiorul unei arhitecturi de microservicii, doriți să vă asigurați că aveți un sistem de înregistrare robust. Pentru a crea unul, consultați Python Logging: Un tutorial aprofundat al unui coleg Toptaler și dezvoltator Python: Son Nguyen Kim.