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