Python a mikroserwisy – przydatne wskazówki oparte na doświadczeniu
Zaktualizowaliśmy ten tekst dla Ciebie!
Data aktualizacji: 31.12.2024
Autor aktualizacji: Damian Krupa
Nie sposób dziś nie usłyszeć o mikroserwisach. To modne słowo. Wszyscy o nim mówią, piszą i myślą – zarówno deweloperzy, jak i managerowie. W tym wpisie chciałbym skupić się na kilku przydatnych przypadkach dotyczących Pythona i mikroserwisów.
Na początek warto powiedzieć, dlaczego. Python dobrze radzi sobie z mikroserwisami.
• łatwo zacząć, szybkie prototypowanie zapewnia działające API szybko i łatwo
• świetne mikroframeworki gotowe do użycia, takie jak Flask
• asynchroniczne serwisy z Tornado lub Twisted
• wiele przydatnych pakietów: requesty, uritemplate, rfc3339, flask-restless
• klienci popularnych usług, takich jak RabbitMQ, Redis, MongoDB
• zalety Pythona, takie jak łatwa konwersja JSON (najpopularniejszy nośnik danych REST) na słowniki Pythona.
Synchroniczny WSGI – pomóż sobie asynchronicznością Gevent lub asyncio
Standard WSGI (Web Server Gateway Interface) to implementacja PEP3333, która została zainspirowana przez CGI (Common Gateway Interface). Warto wspomnieć o kilku kluczowych funkcjonalnościach, jakie posiada WSGI i które świetnie sprawdzają się w tworzeniu mikroserwisów. Zaczynając od elastyczności, która pozwala nam zmieniać serwer backend bez zmiany kodu aplikacji (np.: przełączanie się z Nginx na Apache), poprzez skalowalność obsługiwaną przez sam serwer WSGI (co pozwala nam dodawać instancje, jeśli obciążenie żądaniami jest ogromne). Innymi, nie mniej ważnymi, są wielokrotnego użytku komponenty middleware do obsługi pamięci podręcznych, sesji, autoryzacji itp.
Mikroserwisy polegają na wysyłaniu żądania i otrzymywaniu odpowiedzi. Czas niezbędny do otrzymania danych może czasami stanowić wąskie gardło dla synchronicznych frameworków.
Dlatego, jak wspomniano we wstępie, Tornado i Twisted zyskują na popularności, ponieważ w oparciu o „callbacki” całkiem dobrze radzą sobie z asynchronicznością. Nie twierdzę, że callbacki są lekarstwem. Ważne jest, aby podkreślić, że w niektórych przypadkach mogą pomóc. Niemniej jednak nie ma nic złego w implementacji mikroserwisów opartych na standardzie WSGI, o ile aplikacja działa w sposób: 1 żądanie = 1 wątek. Jeśli nadal serwis synchroniczny jest deal-breakerem, istnieje sztuczka, aby przyspieszyć naszą aplikację, używając gevent, lub (o czym przypomnimy na końcu tego rozdziału) asyncio może być odpowiedzią.
Zanim przejdę do przykładów kodu, chciałbym poinformować wszystkich (jeszcze raz), że kod współbieżny nie zawsze jest odpowiedzią. Postawiłbym na śmiałe stwierdzenie, że należy go używać, jeśli inne rozwiązania zawiodły. Dlaczego? Ponieważ komplikuje kod, utrudnia debugowanie. Nie mówiąc już o synchronizacji pomiędzy współbieżnymi fragmentami kodu korzystającymi ze wspólnych danych. Tak więc nie jest to darmowe (a propos, czy kiedykolwiek słyszałeś termin „callback hell” w odniesieniu do JavaScript?) i powinno się go używać tylko wtedy, gdy są ku temu przesłanki (dużo czasu spędzonego na oczekiwaniu na odpowiedź, podczas gdy użycie procesora jest niskie).
Gevent to biblioteka współbieżności, która zapewnia API dla wielu zadań związanych ze współbieżnością i siecią. Opiera się na greenletach (moduł coroutine napisany jako rozszerzenie C). Może skrócić czas potrzebny do obsługi wielu wywołań do naszych endpointów.
Utwórzmy dwa pliki python example_without_gevent.py i example_with_gevent.py, a następnie zmierzmy ich czas. Przekonaj się sam.
# example_without_gevent.py
import requests
def run():
urls = [
'http://www.google.com',
'http://www.python.org',
'http://www.wikipedia.org',
'http://www.github.com',
]
responses = [requests.get(url) for url in urls]
$ python -mtimeit -n 3 'import example_without_gevent' 'example_without_gevent.run()'
3 loops, best of 3: 1.52 sec per loop
# example_with_gevent.py
import gevent
from gevent import monkey
monkey.patch_all()
import requests
def run():
urls = [
'http://www.google.com',
'http://www.python.org',
'http://www.wikipedia.org',
'http://www.github.com',
]
jobs = [gevent.spawn(lambda url: requests.get(url), each) for each in urls]
gevent.joinall(jobs, timeout=5)
responses = [requests.get(url) for url in urls]
$ python -mtimeit -n 3 'import example_with_gevent' 'example_with_gevent.run()'
3 loops, best of 3: 647 msec per loop
Nie wszystko jest takie różowe. Aby gevent lib działał prawidłowo, cały kod, który go używa, musi być zgodny z jego wersją. To jest powód, dla którego niektóre pakiety rozwijane przez społeczność czasami blokują się nawzajem (szczególnie rozszerzenia C). Tak czy inaczej, w większości przypadków nie zmierzysz się z tym sam.
Najładniejszym i najnowocześniejszym sposobem jest asyncio. Od Pythona 3.4, kiedy został wprowadzony, asyncio pozwala na pisanie współbieżnego kodu poprzez dostarczanie API wysokiego poziomu (coroutines, synchronizacja współbieżnego kodu, kontrola podprocesów) i niskiego poziomu (pętle zdarzeń) oraz nowych słów kluczowych (await/async). Jeśli twój projekt pozwala ci używać najnowszych wydań Pythona, to prawdopodobnie jest to najlepszy sposób radzenia sobie ze współbieżnością. Pozwolę sobie podać odnośnik do oficjalnej strony internetowej, na której opublikowano bardziej szczegółowe informacje: https://docs.python.org/3/library/asyncio.html
Analiza luk w zabezpieczeniach przy użyciu Bandit
Społeczność OpenStack zaprojektowała i stworzyła narzędzie o nazwie Bandit, aby znaleźć słabe punkty bezpieczeństwa (np. SQL injection). W wyniku testów luk użytkownik otrzymuje przejrzysty output konsoli ze wskazanymi przypadkami, które zawiodły podczas testu.
Utwórzmy przykładowy kod z celowo występującymi problemami bezpieczeństwa (linie z problemami oznaczone komentarzami).
# 1st issue related to subprocess
import subprocess
import yaml
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
def read_config(file_name):
with open(file_name) as config:
# 2nd issue – unsafe yaml load
data = yaml.load(config.read())
def run_command(cmd):
# 3rd issue – shell=True
return subprocess.check_call(cmd, shell=True)
db = create_engine('sqlite://localhost')
Session = sessionmaker(bind=db)
def get_product(id):
session = Session()
# 4th issue – SQL injection
query = "select * from products where id='%s'" % id
return session.execute(query)
Uruchom następujące polecenie, aby wykonać testy bandit na pliku:
$ bandit my_file.py
Oto rezultat:
Test results:
>> Issue: [B404:blacklist] Consider possible security implications associated with subprocess module.
Severity: Low Confidence: High
Location: example.py:1
More Info: https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html#b404-import-subprocess 1 import subprocess
2 import yaml
3 from sqlalchemy import create_engine
--------------------------------------------------
>> Issue: [B506:yaml_load] Use of unsafe yaml load. Allows instantiation of arbitrary objects. Consider yaml.safe_load().
Severity: Medium Confidence: High
Location: example.py:9
More Info: https://bandit.readthedocs.io/en/latest/plugins/b506_yaml_load.html
8 with open(file_name) as config:
9 data = yaml.load(config.read())
10
--------------------------------------------------
>> Issue: [B602:subprocess_popen_with_shell_equals_true] subprocess call with shell=True identified, security issue.
Severity: High Confidence: High
Location: example.py:13
More Info: https://bandit.readthedocs.io/en/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html12 def run_command(cmd):
13 return subprocess.check_call(cmd, shell=True)
14
--------------------------------------------------
>> Issue: [B608:hardcoded_sql_expressions] Possible SQL injection vector through string-based query construction.
Severity: Medium Confidence: Low
Location: example.py:22
More Info: https://bandit.readthedocs.io/en/latest/plugins/b608_hardcoded_sql_expressions.html
21 session = Session()
22 query = "select * from products where id='%s'" % id
23 return session.execute(query)
--------------------------------------------------
Code scanned:
Total lines of code: 15
Total lines skipped (#nosec): 0
Run metrics:
Total issues (by severity):
Undefined: 0.0
Low: 1.0
Medium: 2.0
High: 1.0
Total issues (by confidence):
Undefined: 0.0
Low: 1.0
Medium: 0.0
High: 3.0
Bandit uwzględnia dziesiątki testów. W kontekście platformy Flask, warto wspomnieć o jednym teście: sprawdzenie, czy debugowanie jest ustawione na True, co jest w porządku w instancjach deweloperskich, ale nie w produkcyjnych. Ponieważ stworzenie aplikacji Flask wymaga mniej kodu niż pojedyncze System.out.println() w Javie, nie waham się umieścić poniżej podstawowego przykładu:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Nothing is here'
if __name__ == '__main__':
app.run(debug=True)
Uruchomienie testu bandit na tym kodzie daje następujący wynik:
Test results:
>> Issue: [B201:flask_debug_true] A Flask app appears to be run with debug=True, which exposes the Werkzeug debugger and allows the execution of arbitrary code.
Severity: High Confidence: Medium
Location: app.py:13
More Info: https://bandit.readthedocs.io/en/latest/plugins/b201_flask_debug_true.html
12 if __name__ == '__main__':
13 app.run(debug=True)
Przeprowadzenie takiego testu na gałęzi produkcyjnej przed deploymentem do środowiska produkcyjnego jest zdecydowanie dobrą praktyką. Zauważ, że zautomatyzowane narzędzia takie jak to nie powinny być traktowane jak wyrocznia. Należy ich używać w połączeniu z poważnymi testami (wszelkiego rodzaju). Ponadto, ponieważ jest to biblioteka strony trzeciej, chodzi również o zaufanie do autorów, prawda?
Bandit można skonfigurować poprzez plik .ini:
[bandit]
skips: B201
Exclude: tests
Zarządzaj API za pomocą flask-restless
Flask-restless zasadniczo zapewnia mapowanie między bazą danych a modelem, upraszczając generowanie API dla modelu bez pisania routes, ponieważ dostęp do tabel bazy danych jest w zasadzie taki sam dla wszystkich encji. W wyniku żądania GET dla określonego modelu flask-restless zwraca JSON.
Będziemy używać przykładowej bazy danych SQLite dostępnej pod adresem http://www.sqlitetutorial.net/sqlite-sample-database/ jako zasobu lokalnego. Plik nazywa się chinook.db. Oto wynik drzewa mojego katalogu roboczego (na wypadek potrzeby sprawdzenia):
flask_restless_with_sqlite_example/
config.cfg
database
chinook.db
requirements.txt
src
my_app.py
venv
Przydatne jest umieszczenie stringu połączenia z bazą danych w pliku config.cfg, jak pokazano poniżej:
SQLALCHEMY_DATABASE_URI = 'sqlite:///../database/chinook.db'
DEBUG = True
chinook.db zawiera więcej niż wystarczającą liczbę tabel, chociaż na potrzeby tego bloga skupimy się na tabelach Albumy i Artyści.
my_app.py wygląda następująco:
from pathlib import Path
import sqlalchemy as db
from flask import Flask
from flask_restless import APIManager
from sqlalchemy.ext.declarative import declarative_base
app = Flask(__name__)
app.config.from_pyfile(Path(Path(__file__).parent, '..', 'config.cfg'))
engine = db.create_engine(app.config['SQLALCHEMY_DATABASE_URI'])
session = db.orm.sessionmaker(bind=engine)()
Base = declarative_base()
class Albums(Base):
__tablename__ = 'Albums'
album_id = db.Column('AlbumId', db.Integer, primary_key=True)
title = db.Column(db.String(160))
artist_id = db.Column(
'ArtistId', db.Integer, db.ForeignKey('Artists.ArtistId')
)
class Artists(Base):
__tablename__ = 'Artists'
artist_id = db.Column('ArtistId', db.Integer, primary_key=True)
name = db.Column(db.String(160))
albums = db.orm.relationship('Albums', backref='Artists')
manager = APIManager(app, session=session)
manager.create_api(Albums)
manager.create_api(Artists)
if __name__ == '__main__':
app.run()
Aplikacja domyślnie obsługuje dane pod adresem http://localhost:5000/. Dostęp do naszego API uzyskuje się, wysyłając żądanie GET do http://localhost:5000/api/Albums. W rezultacie otrzymujemy JSON (pokazany poniżej) i to wszystko. Podajemy dane z naszej bazy danych
{
"num_results": 347,
"objects": [
{
"Artists": {
"artist_id": 1,
"name": "AC/DC"
},
"album_id": 1,
"artist_id": 1,
"title": "For Those About To Rock We Salute You"
},
{
"Artists": {
"artist_id": 2,
"name": "Accept"
},
"album_id": 2,
"artist_id": 2,
"title": "Balls to the Wall"
},
(…most of this JSON has been cut...)
{
"Artists": {
"artist_id": 8,
"name": "Audioslave"
},
"album_id": 10,
"artist_id": 8,
"title": "Audioslave"
}
],
"page": 1,
"total_pages": 35
}
Wnioski
Obecnie panuje ogromny szum wokół mikroserwisów. Zderzmy go z popularnością języka programowania Python (według statystyk Stackoverflow, Python jest językiem na temat którego zadaje się najwięcej pytań w ich serwisie), a otrzymamy ładny duet, który jest w stanie bardzo dobrze obsługiwać mikroserwisy (z bibliotekami gotowymi do użycia). Moim zdaniem to fantastyczny czas, aby dowiedzieć się o chwytliwych mikroserwisach w tak istotnym języku, jakim jest Python. Przyjrzyjmy się największym graczom na rynku, oni już dostrzegli zalety Pythona + mikroserwisów!
Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!
Skontaktuj się z nami


