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

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

.envScriptsactivate 

Наконец, вы готовы установить 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