Введение в микросервисы
Микросервис — это подход к разбиению большого монолитного приложения на отдельные приложения, специализирующиеся на конкретной услуге/функции. Этот подход часто называют сервис-ориентированной архитектурой или SOA.В монолитной архитектуре каждая бизнес-логика находится в одном приложении. Службы приложений, такие как управление пользователями, аутентификация и другие функции, используют одну и ту же базу данных.
В микросервисной архитектуре приложение разбивается на несколько отдельных служб, которые выполняются в отдельных процессах. Существует другая база данных для разных функций приложения, и службы взаимодействуют друг с другом с использованием HTTP, AMQP или двоичного протокола, такого как TCP, в зависимости от характера каждой службы. Межсервисное взаимодействие также может осуществляться с использованием очередей сообщений, таких как RabbitMQ , Kafka или Redis .
Преимущества микросервиса
Микросервисная архитектура имеет множество преимуществ. Некоторые из этих преимуществ:- Слабосвязанное приложение означает, что различные сервисы могут быть созданы с использованием технологий, которые им подходят лучше всего. Таким образом, команда разработчиков не ограничена выбором, сделанным при запуске проекта.
- Так как сервисы отвечают за конкретный функционал, что упрощает понимание и контроль над приложением.
- Масштабирование приложений также становится проще, поскольку если один из сервисов требует высокой загрузки графического процессора, то только сервер, на котором этот сервис должен иметь высокий графический процессор, а другие могут работать на обычном сервере.
Недостатки микросервиса
Микросервисная архитектура — это не панацея, которая решит все ваши проблемы. У нее есть и свои недостатки. Некоторые из этих недостатков:- Поскольку разные службы используют разные базы данных, транзакции, включающие более одной службы, должны использовать итоговую согласованность.
- Идеального разделения услуг очень сложно добиться с первой попытки, и это необходимо повторить, прежде чем добиться наилучшего разделения услуг.
- Поскольку службы взаимодействуют друг с другом посредством сетевого взаимодействия, это замедляет работу приложения из-за задержки в сети и медленного обслуживания.
Почему микросервис в Python
Python — идеальный инструмент для создания микросервисов, поскольку у него отличное сообщество, простота обучения и множество библиотек.Введение в FastAPI
FastAPI — это современная высокопроизводительная веб-инфраструктура, которая обладает множеством интересных функций, таких как автоматическое документирование на основе OpenAPI и встроенная библиотека сериализации и проверки. Здесь вы найдете список всех интересных функций FastAPI.Почему ФастAPI
Некоторые из причин, по которым я считаю FastAPI отличным выбором для создания микросервисов на Python, заключаются в следующем:- Авто документация
- Поддержка асинхронности/ожидания
- Встроенная проверка и сериализация
- 100% тип аннотирован, поэтому автодополнение работает отлично.
Установка ФастAPI
Перед установкой FastAPI создайте новый каталог movie_serviceи создайте новую виртуальную среду внутри вновь созданного каталога, используя virtualenv .Если вы еще не установили virtualenv:
pip install virtualenv
Теперь создайте новую виртуальную среду.
virtualenv env
Если вы используете Mac/Linux, вы можете активировать виртуальную среду с помощью команды:
source ./env/bin/activate
Пользователи Windows могут вместо этого запустить эту команду:
.\env\Scripts\activate
Наконец, вы готовы установить FastAPI, выполните следующую команду:
pip install fastapi
Поскольку FastAPI не имеет встроенного сервиса, uvicornдля его запуска вам необходимо установить его. uvicorn— это сервер ASGI , который позволяет нам использовать функции async/await.
Установить uvicornс помощью команды
pip install uvicorn
Создание простого REST API с использованием FastAPI
Прежде чем приступить к созданию микросервиса с использованием FastAPI, давайте изучим основы FastAPI. Создайте новый каталог appи новый файл main.pyвнутри вновь созданного каталога.Добавьте следующий код в main.py.
#~/movie_service/app/main.py
from fastapi import FastAPI
app = FastAPI()
@app.get('/')
async def index():
return {"Real": "Python"}
Здесь вы сначала импортируете и создаете экземпляр FastAPI, а затем регистрируете корневую конечную точку /, которая затем возвращает файл JSON.
Вы можете запустить сервер приложений, используя uvicorn app.main:app --reload. Здесь app.mainуказывается, что вы используете main.pyфайл внутри appкаталога, и :appуказывается имя нашего FastAPIэкземпляра.
Вы можете получить доступ к приложению по адресу http://127.0.0.1:8000 . Чтобы получить доступ к интересной автоматической документации, перейдите по адресу http://127.0.0.1:8000/docs . Вы можете экспериментировать и взаимодействовать со своим API из самого браузера.
Давайте добавим в наше приложение некоторые функции CRUD.
Обновите свой файл main.py, чтобы он выглядел следующим образом:
#~/movie_service/app/main.py
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
app = FastAPI()
fake_movie_db = [
{
'name': 'Star Wars: Episode IX - The Rise of Skywalker',
'plot': 'The surviving members of the resistance face the First Order once again.',
'genres': ['Action', 'Adventure', 'Fantasy'],
'casts': ['Daisy Ridley', 'Adam Driver']
}
]
class Movie(BaseModel):
name: str
plot: str
genres: List[str]
casts: List[str]
@app.get('/', response_model=List[Movie])
async def index():
return fake_movie_db
Как видите, вы создали новый класс Movie, который является продолжением BaseModelpydantic.
Модель Movieсодержит название, фото, жанры и актерский состав. В состав Pydantic встроен FastAPI, что упрощает создание моделей и проверку запросов.
Если вы перейдете на сайт документации, вы увидите, что поля нашей модели Movies уже упоминались в разделе примера ответа. Это возможно, потому что вы указали response_modelв нашем определении маршрута.
Теперь давайте добавим конечную точку, чтобы добавить фильм в наш список фильмов.
Добавьте новое определение конечной точки для обработки POSTзапроса.
@app.post('/', status_code=201)
async def add_movie(payload: Movie):
movie = payload.dict()
fake_movie_db.append(movie)
return {'id': len(fake_movie_db) - 1}
Теперь зайдите в браузер и протестируйте новый API. Попробуйте добавить фильм с недопустимым полем или без обязательных полей и убедитесь, что проверка автоматически выполняется FastAPI.
Давайте добавим новую конечную точку для обновления фильма.
@app.put('/{id}')
async def update_movie(id: int, payload: Movie):
movie = payload.dict()
movies_length = len(fake_movie_db)
if 0 <= id <= movies_length:
fake_movie_db[id] = movie
return None
raise HTTPException(status_code=404, detail="Movie with given id not found")
Вот idиндекс нашего fake_movie_dbсписка.
Примечание. Не забудьте импортировать HTTPExceptionизfastapi
Теперь вы также можете добавить конечную точку для удаления фильма.
@app.delete('/{id}')
async def delete_movie(id: int):
movies_length = len(fake_movie_db)
if 0 <= id <= movies_length:
del fake_movie_db[id]
return None
raise HTTPException(status_code=404, detail="Movie with given id not found")
Прежде чем двигаться дальше, давайте лучше структурируем наше приложение. apiСоздайте внутри новую папку appи создайте новый файл movies.pyвнутри недавно созданной папки. Переместите все коды, связанные с маршрутами, из main.pyв movies.py. Итак, movies.pyдолжно выглядеть следующим образом:
#~/movie-service/app/api/movies.py
from typing import List
from fastapi import Header, APIRouter
from app.api.models import Movie
fake_movie_db = [
{
'name': 'Star Wars: Episode IX - The Rise of Skywalker',
'plot': 'The surviving members of the resistance face the First Order once again.',
'genres': ['Action', 'Adventure', 'Fantasy'],
'casts': ['Daisy Ridley', 'Adam Driver']
}
]
movies = APIRouter()
@movies.get('/', response_model=List[Movie])
async def index():
return fake_movie_db
@movies.post('/', status_code=201)
async def add_movie(payload: Movie):
movie = payload.dict()
fake_movie_db.append(movie)
return {'id': len(fake_movie_db) - 1}
@movies.put('/{id}')
async def update_movie(id: int, payload: Movie):
movie = payload.dict()
movies_length = len(fake_movie_db)
if 0 <= id <= movies_length:
fake_movie_db[id] = movie
return None
raise HTTPException(status_code=404, detail="Movie with given id not found")
@movies.delete('/{id}')
async def delete_movie(id: int):
movies_length = len(fake_movie_db)
if 0 <= id <= movies_length:
del fake_movie_db[id]
return None
raise HTTPException(status_code=404, detail="Movie with given id not found")
Здесь вы зарегистрировали новый маршрут API, используя APIRouter из FastAPI.
Кроме того, создайте новый файл , models.pyв apiкотором вы будете хранить наши модели Pydantic.
#~/movie-service/api/models.py
from typing import List
from pydantic import BaseModel
class Movie(BaseModel):
name: str
plot: str
genres: List[str]
casts: List[str]
Теперь зарегистрируйте этот новый файл маршрутов вmain.py
#~/movie-service/app/main.py
from fastapi import FastAPI
from app.api.movies import movies
app = FastAPI()
app.include_router(movies)
Наконец, структура каталогов нашего приложения выглядит следующим образом:
movie-service
├── app
│ ├── api
│ │ ├── models.py
│ │ ├── movies.py
│ |── main.py
└── env
Прежде чем двигаться дальше, убедитесь, что ваше приложение работает правильно.
Использование базы данных PostgreSQL с FastAPI
Раньше вы использовали поддельный список Python для добавления фильмов, но теперь вы, наконец, готовы использовать для этой цели реальную базу данных. Для этой цели вы собираетесь использовать PostgreSQL . Установите PostgreSQL, если вы еще этого не сделали. После установки PostgreSQl создайте новую базу данных, я назову свою movie_db.Вы собираетесь использовать кодировку/базы данных для подключения к базе данных asyncи awaitее поддержки. Узнайте больше о async/awaitPython здесь
Установите необходимую библиотеку, используя:
pip install 'databases[postgresql]'
при этом будут установлены sqlalchemyи asyncpgнеобходимые для работы с PostgreSQL.
Создайте внутри новый файл apiи назовите его db.py. Этот файл будет содержать фактическую модель базы данных для нашего REST API.
#~/movie-service/app/api/db.py
from sqlalchemy import (Column, Integer, MetaData, String, Table,
create_engine, ARRAY)
from databases import Database
DATABASE_URL = 'postgresql://movie_user:movie_password@localhost/movie_db'
engine = create_engine(DATABASE_URL)
metadata = MetaData()
movies = Table(
'movies',
metadata,
Column('id', Integer, primary_key=True),
Column('name', String(50)),
Column('plot', String(250)),
Column('genres', ARRAY(String)),
Column('casts', ARRAY(String))
)
database = Database(DATABASE_URL)
Вот DATABASE_URIURL-адрес, используемый для подключения к базе данных PostgreSQL. Здесь movie_userуказано имя пользователя базы данных, movie_passwordпароль пользователя базы данных и movie_dbимя базы данных.
Точно так же, как в SQLAlchemy, вы создали таблицу для базы данных фильмов.
Обновите main.pyдля подключения к базе данных. main.pyдолжно выглядеть следующим образом:
#~/movie-service/app/main.py
from fastapi import FastAPI
from app.api.movies import movies
from app.api.db import metadata, database, engine
metadata.create_all(engine)
app = FastAPI()
@app.on_event("startup")
async def startup():
await database.connect()
@app.on_event("shutdown")
async def shutdown():
await database.disconnect()
app.include_router(movies)
FastAPI предоставляет некоторые обработчики событий, которые вы можете использовать для подключения к нашей базе данных при запуске приложения и отключения при его завершении.
Обновите movies.py, чтобы он использовал базу данных вместо поддельного списка Python.
#~/movie-service/app/api/movies.py
from typing import List
from fastapi import Header, APIRouter
from app.api.models import MovieIn, MovieOut
from app.api import db_manager
movies = APIRouter()
@movies.get('/', response_model=List[MovieOut])
async def index():
return await db_manager.get_all_movies()
@movies.post('/', status_code=201)
async def add_movie(payload: MovieIn):
movie_id = await db_manager.add_movie(payload)
response = {
'id': movie_id,
**payload.dict()
}
return response
@movies.put('/{id}')
async def update_movie(id: int, payload: MovieIn):
movie = payload.dict()
fake_movie_db[id] = movie
return None
@movies.put('/{id}')
async def update_movie(id: int, payload: MovieIn):
movie = await db_manager.get_movie(id)
if not movie:
raise HTTPException(status_code=404, detail="Movie not found")
update_data = payload.dict(exclude_unset=True)
movie_in_db = MovieIn(**movie)
updated_movie = movie_in_db.copy(update=update_data)
return await db_manager.update_movie(id, updated_movie)
@movies.delete('/{id}')
async def delete_movie(id: int):
movie = await db_manager.get_movie(id)
if not movie:
raise HTTPException(status_code=404, detail="Movie not found")
return await db_manager.delete_movie(id)
Давайте добавим db_manager.pyвозможность манипулировать нашей базой данных.
#~/movie-service/app/api/db_manager.py
from app.api.models import MovieIn, MovieOut, MovieUpdate
from app.api.db import movies, database
async def add_movie(payload: MovieIn):
query = movies.insert().values(**payload.dict())
return await database.execute(query=query)
async def get_all_movies():
query = movies.select()
return await database.fetch_all(query=query)
async def get_movie(id):
query = movies.select(movies.c.id==id)
return await database.fetch_one(query=query)
async def delete_movie(id: int):
query = movies.delete().where(movies.c.id==id)
return await database.execute(query=query)
async def update_movie(id: int, payload: MovieIn):
query = (
movies
.update()
.where(movies.c.id == id)
.values(**payload.dict())
)
return await database.execute(query=query)
Давайте обновим нашу систему models.py, чтобы вы могли использовать модель Pydantic с таблицей sqlalchemy.
#~/movie-service/app/api/models.py
from pydantic import BaseModel
from typing import List, Optional
class MovieIn(BaseModel):
name: str
plot: str
genres: List[str]
casts: List[str]
class MovieOut(MovieIn):
id: int
class MovieUpdate(MovieIn):
name: Optional[str] = None
plot: Optional[str] = None
genres: Optional[List[str]] = None
casts: Optional[List[str]] = None
Вот MovieInбазовая модель, которую вы используете для добавления фильма в базу данных. Вам нужно добавить idк этой модели, получая ее из базы данных, следовательно, и модель MovieOut. MovieUpdateМодель позволяет нам сделать значения в модели необязательными, чтобы при обновлении фильма можно было отправлять только то поле, которое необходимо обновить.
Теперь перейдите на сайт документации браузера и начните экспериментировать с API.
Шаблоны управления данными микросервисов
Управление данными в микросервисе — один из наиболее сложных аспектов создания микросервиса. Поскольку разные функции приложения выполняются разными службами, использование базы данных может оказаться затруднительным.Вот несколько шаблонов, которые можно использовать для управления потоком данных в приложении.
База данных на услугу
Использование базы данных для каждого сервиса отлично подходит, если вы хотите, чтобы ваши микросервисы были как можно более слабо связанными. Наличие отдельной базы данных для каждого сервиса позволяет нам независимо масштабировать разные сервисы. Транзакция с участием нескольких баз данных выполняется через четко определенные API. Это имеет свой недостаток, поскольку реализация бизнес-транзакций, включающих несколько сервисов, не является простой задачей. Кроме того, дополнительные сетевые издержки делают его менее эффективным в использовании.Общая база данных
Если есть много транзакций с участием нескольких сервисов, лучше использовать общую базу данных. Это дает преимущества высокосогласованного приложения, но лишает большинства преимуществ микросервисной архитектуры. Разработчикам, работающим над одним сервисом, необходимо координировать изменения схемы в других сервисах.Состав API
В транзакциях с участием нескольких баз данных композитор API действует как шлюз API и выполняет вызовы API к другим микросервисам в необходимом порядке. Наконец, результаты каждого микросервиса возвращаются клиентской службе после выполнения соединения в памяти. Недостатком этого подхода является неэффективное объединение больших наборов данных в памяти.Создание микросервиса Python в Docker
Трудности развертывания микросервиса можно значительно уменьшить, используя Docker. Docker помогает инкапсулировать каждую службу и масштабировать ее независимо.Установка Docker и Docker Compose
Если вы еще не установили docker в свою систему. Убедитесь, что докер установлен, выполнив команду docker. После завершения установки Docker установите Docker Compose . Docker Compose используется для определения и запуска нескольких контейнеров Docker. Это также помогает облегчить взаимодействие между ними.Создание службы фильмов
Поскольку большая часть работы по созданию сервиса фильмов уже проделана при начале работы с FastAPI, вам придется повторно использовать уже написанный код. Создайте новую папку, я назову свою python-microservices. Переместите код, который вы написали ранее и который я назвал movie-service.Итак, структура папок будет выглядеть так:
python-microservices/
└── movie-service/
├── app/
└── env/
Прежде всего, давайте создадим requirements.txtфайл, в котором вы будете хранить все зависимости, которые вы собираетесь использовать в нашем movie-service. Создайте внутри
новый файл и добавьте в него следующее:requirements.txtmovie-service
asyncpg==0.20.1
databases[postgresql]==0.2.6
fastapi==0.48.0
SQLAlchemy==1.3.13
uvicorn==0.11.2
httpx==0.11.1
Вы использовали все упомянутые там библиотеки, кроме httpx , который вы собираетесь использовать при выполнении вызова API между службами.
Создайте Dockerfileвнутреннюю часть movie-serviceсо следующим содержимым:
FROM python:3.8-slim
WORKDIR /app
COPY ./requirements.txt /app/requirements.txt
RUN apt-get update \
&& apt-get install gcc -y \
&& apt-get clean
RUN pip install -r /app/requirements.txt \
&& rm -rf /root/.cache/pip
COPY . /app/
Здесь сначала вы определяете, какую версию Python вы хотите использовать. Затем установите WORKDIRпапку appвнутри контейнера Docker. После этого gccустанавливается то, что требуется библиотекам, которые вы используете в приложении.
Наконец, установите все зависимости requirements.txtи скопируйте все файлы внутри movie-service/app.
Обновить db.pyи заменить
DATABASE_URI = 'postgresql://movie_user:movie_password@localhost/movie_db'
с
DATABASE_URI = os.getenv('DATABASE_URI')
Примечание. Не забудьте импортировать osв начало файла.
Вам нужно сделать это, чтобы вы могли позже предоставить его DATABASE_URIв качестве переменной среды.
Также обновите main.pyи замените
app.include_router(movies)
с
app.include_router(movies, prefix='/api/v1/movies', tags=['movies'])
Здесь вы добавили prefix /api/v1/moviesтак, что управление разными версиями API становится проще. Кроме того, теги упрощают поиск API-интерфейсов moviesв документации FastAPI.
Кроме того, вам необходимо обновить наши модели, чтобы в них castsсохранялся идентификатор актера, а не фактическое имя. Итак, обновите файл, models.pyчтобы он выглядел так:
#~/python-microservices/movie-service/app/api/models.py
from pydantic import BaseModel
from typing import List, Optional
class MovieIn(BaseModel):
name: str
plot: str
genres: List[str]
casts_id: List[int]
class MovieOut(MovieIn):
id: int
class MovieUpdate(MovieIn):
name: Optional[str] = None
plot: Optional[str] = None
genres: Optional[List[str]] = None
casts_id: Optional[List[int]] = None
Аналогично нужно обновить таблицы базы данных, давайте обновим db.py:
#~/python-microservices/movie-service/app/api/db.py
import os
from sqlalchemy import (Column, DateTime, Integer, MetaData, String, Table,
create_engine, ARRAY)
from databases import Database
DATABASE_URL = os.getenv('DATABASE_URL')
engine = create_engine(DATABASE_URL)
metadata = MetaData()
movies = Table(
'movies',
metadata,
Column('id', Integer, primary_key=True),
Column('name', String(50)),
Column('plot', String(250)),
Column('genres', ARRAY(String)),
Column('casts_id', ARRAY(Integer))
)
database = Database(DATABASE_URL)
Теперь обновите, movies.pyчтобы проверить, присутствует ли актерский состав с данным идентификатором в службе кастинга, прежде чем добавлять новый фильм или обновлять фильм.
#~/python-microservices/movie-service/app/api/movies.py
from typing import List
from fastapi import APIRouter, HTTPException
from app.api.models import MovieOut, MovieIn, MovieUpdate
from app.api import db_manager
from app.api.service import is_cast_present
movies = APIRouter()
@movies.post('/', response_model=MovieOut, status_code=201)
async def create_movie(payload: MovieIn):
for cast_id in payload.casts_id:
if not is_cast_present(cast_id):
raise HTTPException(status_code=404, detail=f"Cast with id:{cast_id} not found")
movie_id = await db_manager.add_movie(payload)
response = {
'id': movie_id,
**payload.dict()
}
return response
@movies.get('/', response_model=List[MovieOut])
async def get_movies():
return await db_manager.get_all_movies()
@movies.get('/{id}/', response_model=MovieOut)
async def get_movie(id: int):
movie = await db_manager.get_movie(id)
if not movie:
raise HTTPException(status_code=404, detail="Movie not found")
return movie
@movies.put('/{id}/', response_model=MovieOut)
async def update_movie(id: int, payload: MovieUpdate):
movie = await db_manager.get_movie(id)
if not movie:
raise HTTPException(status_code=404, detail="Movie not found")
update_data = payload.dict(exclude_unset=True)
if 'casts_id' in update_data:
for cast_id in payload.casts_id:
if not is_cast_present(cast_id):
raise HTTPException(status_code=404, detail=f"Cast with given id:{cast_id} not found")
movie_in_db = MovieIn(**movie)
updated_movie = movie_in_db.copy(update=update_data)
return await db_manager.update_movie(id, updated_movie)
@movies.delete('/{id}', response_model=None)
async def delete_movie(id: int):
movie = await db_manager.get_movie(id)
if not movie:
raise HTTPException(status_code=404, detail="Movie not found")
return await db_manager.delete_movie(id)
Давайте добавим сервис для вызова API для службы трансляции:
#~/python-microservices/movie-service/app/api/service.py
import os
import httpx
CAST_SERVICE_HOST_URL = 'http://localhost:8002/api/v1/casts/'
url = os.environ.get('CAST_SERVICE_HOST_URL') or CAST_SERVICE_HOST_URL
def is_cast_present(cast_id: int):
r = httpx.get(f'{url}{cast_id}')
return True if r.status_code == 200 else False
Вы делаете вызов API, чтобы получить приведение с заданным идентификатором, и возвращаете true, если приведение существует, и false в противном случае.
Создание службы Casts
Как и в случае с файлом movie-service, для создания casts-serviceвы будете использовать базу данных FastAPI и PostgreSQL.Создайте структуру папок, подобную следующей:
python-microservices/
.
├── cast_service/
│ ├── app/
│ │ ├── api/
│ │ │ ├── casts.py
│ │ │ ├── db_manager.py
│ │ │ ├── db.py
│ │ │ ├── models.py
│ │ ├── main.py
│ ├── Dockerfile
│ └── requirements.txt
├── movie_service/
...
Добавьте следующее в requirements.txt:
asyncpg==0.20.1
databases[postgresql]==0.2.6
fastapi==0.48.0
SQLAlchemy==1.3.13
uvicorn==0.11.2
Dockerfile:
FROM python:3.8-slim
WORKDIR /app
COPY ./requirements.txt /app/requirements.txt
RUN apt-get update \
&& apt-get install gcc -y \
&& apt-get clean
RUN pip install -r /app/requirements.txt \
&& rm -rf /root/.cache/pip
COPY . /app/
main.py
#~/python-microservices/cast-service/app/main.py
from fastapi import FastAPI
from app.api.casts import casts
from app.api.db import metadata, database, engine
metadata.create_all(engine)
app = FastAPI()
@app.on_event("startup")
async def startup():
await database.connect()
@app.on_event("shutdown")
async def shutdown():
await database.disconnect()
app.include_router(casts, prefix='/api/v1/casts', tags=['casts'])
Вы добавили префикс, /api/v1/castsчтобы управление API стало проще. Кроме того, добавление упрощает tagsпоиск документов, связанных с castsдокументами FastAPI.
casts.py
#~/python-microservices/cast-service/app/api/casts.py
from fastapi import APIRouter, HTTPException
from typing import List
from app.api.models import CastOut, CastIn, CastUpdate
from app.api import db_manager
casts = APIRouter()
@casts.post('/', response_model=CastOut, status_code=201)
async def create_cast(payload: CastIn):
cast_id = await db_manager.add_cast(payload)
response = {
'id': cast_id,
**payload.dict()
}
return response
@casts.get('/{id}/', response_model=CastOut)
async def get_cast(id: int):
cast = await db_manager.get_cast(id)
if not cast:
raise HTTPException(status_code=404, detail="Cast not found")
return cast
db_manager.py
#~/python-microservices/cast-service/app/api/db_manager.py
from app.api.models import CastIn, CastOut, CastUpdate
from app.api.db import casts, database
async def add_cast(payload: CastIn):
query = casts.insert().values(**payload.dict())
return await database.execute(query=query)
async def get_cast(id):
query = casts.select(casts.c.id==id)
return await database.fetch_one(query=query)
db.py
#~/python-microservices/cast-service/app/api/db.py
import os
from sqlalchemy import (Column, Integer, MetaData, String, Table,
create_engine, ARRAY)
from databases import Database
DATABASE_URI = os.getenv('DATABASE_URI')
engine = create_engine(DATABASE_URI)
metadata = MetaData()
casts = Table(
'casts',
metadata,
Column('id', Integer, primary_key=True),
Column('name', String(50)),
Column('nationality', String(20)),
)
database = Database(DATABASE_URI)
models.py
#~/python-microservices/cast-service/app/api/models.py
from pydantic import BaseModel
from typing import List, Optional
class CastIn(BaseModel):
name: str
nationality: Optional[str] = None
class CastOut(CastIn):
id: int
class CastUpdate(CastIn):
name: Optional[str] = None
Запуск микросервиса с помощью Docker Compose
Чтобы запустить микросервисы, создайте docker-compose.ymlфайл и добавьте в него следующее:version: '3.7'
services:
movie_service:
build: ./movie-service
command: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
volumes:
- ./movie-service/:/app/
ports:
- 8001:8000
environment:
- DATABASE_URI=postgresql://movie_db_username:movie_db_password@movie_db/movie_db_dev
- CAST_SERVICE_HOST_URL=http://cast_service:8000/api/v1/casts/
movie_db:
image: postgres:12.1-alpine
volumes:
- postgres_data_movie:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=movie_db_username
- POSTGRES_PASSWORD=movie_db_password
- POSTGRES_DB=movie_db_dev
cast_service:
build: ./cast-service
command: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
volumes:
- ./cast-service/:/app/
ports:
- 8002:8000
environment:
- DATABASE_URI=postgresql://cast_db_username:cast_db_password@cast_db/cast_db_dev
cast_db:
image: postgres:12.1-alpine
volumes:
- postgres_data_cast:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=cast_db_username
- POSTGRES_PASSWORD=cast_db_password
- POSTGRES_DB=cast_db_dev
volumes:
postgres_data_movie:
postgres_data_cast:
Здесь у вас есть 4 разных сервиса: movie_service, база данных для Movie_service, cast_service и база данных для сервиса Cast. Вы открыли movie_serviceпорт 8001аналогично cast_serviceпорту 8002.
Для базы данных вы использовали тома, чтобы данные не уничтожались при выключении Docker-контейнера.
Запустите docker-compose с помощью команды:
docker-compose up -d
Это создает образ докера, если он еще не существует, и запускает его.
Перейдите по адресу http://localhost:8002/docs , чтобы добавить приведение в службе приведения. Аналогично, http://localhost:8001/docs , чтобы добавить фильм в службу фильмов.
Использование Nginx для доступа к обеим службам с использованием одного адреса хоста
Вы развернули микросервисы с помощью Docker Compose, но есть одна небольшая проблема. Доступ к каждому из микросервисов должен осуществляться через отдельный порт. Вы можете решить эту проблему, используя обратный прокси-сервер Nginx. Используя Nginx, вы можете направить запрос, добавив промежуточное программное обеспечение, которое направляет наши запросы к различным службам на основе URL-адреса API.nginx_config.confДобавьте внутрь новый файл python-microservicesсо следующим содержимым.
server {
listen 8080;
location /api/v1/movies {
proxy_pass http://movie_service:8000/api/v1/movies;
}
location /api/v1/casts {
proxy_pass http://cast_service:8000/api/v1/casts;
}
}
Здесь вы запускаете Nginx на порту 8080и направляете запросы к сервису фильмов, если конечная точка начинается с /api/v1/movies, и аналогично службе трансляции, если конечная точка начинается с/api/v1/casts
Теперь вам нужно добавить службу nginx в наш docker-compose-yml. Добавьте следующую услугу после cast_dbслужбы:
nginx:
image: nginx:latest
ports:
- "8080:8080"
volumes:
- ./nginx_config.conf:/etc/nginx/conf.d/default.conf
depends_on:
- cast_service
- movie_service
Теперь закройте контейнеры командой:
docker-compose down
И запустите его снова с помощью:
docker-compose up -d
Теперь вы можете получить доступ как к сервису фильмов, так и к сервису трансляции через порт 8080.
Перейдите по адресу http://localhost:8080/api/v1/movies/, чтобы получить список фильмов.
Теперь вам может быть интересно, как получить доступ к документации служб. Для этого обновите main.pyсервис фильмов и замените
app = FastAPI()
с
app = FastAPI(openapi_url="/api/v1/movies/openapi.json", docs_url="/api/v1/movies/docs")
Аналогично, для службы приведения замените его на
app = FastAPI(openapi_url="/api/v1/casts/openapi.json", docs_url="/api/v1/casts/docs")
openapi.jsonЗдесь вы изменили конечную точку и откуда обслуживаются документы .
Теперь вы можете получить доступ к документам
http://localhost:8080/api/v1/movies/docs
http://localhost:8080/api/v1/casts/docs
Микросервис на Python+ FastAPI
Введение в микросервисы Микросервис — это подход к разбиению большого монолитного приложения на отдельные приложения, специализирующиеся на конкретной услуге/функции. Этот подход часто...
habr.com