Введение в микросервисы

Микросервис — это подход к разбиению большого монолитного приложения на отдельные приложения, специализирующиеся на конкретной услуге/функции. Этот подход часто называют сервис-ориентированной архитектурой или 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к этой модели, получая ее из базы данных, следовательно, и модель MovieOutMovieUpdateМодель позволяет нам сделать значения в модели необязательными, чтобы при обновлении фильма можно было отправлять только то поле, которое необходимо обновить.

Теперь перейдите на сайт документации браузера и начните экспериментировать с 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 

Комментарии (5)


  1. twistfire92
    06.04.2024 21:28
    +7

    Тут разве не принято добавлять ссылку на оригинал и помечать статью переводом? Отдельные моменты уже устарели, а перевод в некоторых местах некорректный.
    Например

    В состав Pydantic встроен FastAPI

    оригинал

    Pydantic comes built-in with FastAPI

    что означает ровно обратное.

    Кто называет ендпоинты конечными точками? Дословно так и переводится, но вот в русскоязычной среде такое название вообще не на слуху (может где-то и встречается)

    @app.on_event("startup")
    @app.on_event("shutdown")
    

    Это уже устаревшая история, документация (в том же разделе выше) говорит об этом (не удивительно, оригинальная статья 20-го года)

    Подобных моментов, думаю, можно найти больше (коммент решил оставить после найденых нестыковок, которые перечислены выше)

    Подытожим. Статья сама по себе полезная, расписано достаточно хорошо, но является переводом, что желательно указать и проверять актуальность.


    1. Ryav
      06.04.2024 21:28

      Статья подойдёт для демонстрации, но если кто будет именно так писать сервисы — ужас-ужас.


  1. DonAlPAtino
    06.04.2024 21:28

    Понятно что статья старая и претензия не к переводящему, но не хватает следующим шагом описания публикации всего этого добра в кубере...


  1. bascomo
    06.04.2024 21:28

    Ужасный перевод


  1. ASP
    06.04.2024 21:28

    ФастAPI - это нормально так переводить?