Это первая статья о том, как идея проходит путь от прототипа до полноценного продукта — с участием архитектуры на каждом шагу. Формат — ADR (Architecture Decision Records): каждое решение зафиксировано по дням, чтобы показать реальную эволюцию проекта. Продукт вымышленный, проблемы — настоящие. Те самые, с которыми сталкиваются архитекторы и команды. Документация и код — в открытом доступе на GitHub.
Контекст
В процессе разработки находится прототип на Python, состоящий из:
-
Трех gRPC-микросервисов: заказы, резервирование и лояльность
-
API-шлюза (оркестратора) с последовательным вызовом методов: резервирование товаров → применение лояльности → сохранение заказа
Текущая архитектура:
-
Микросервисы запускаются как отдельные процессы через asyncio
-
Все компоненты упакованы в один контейнер
Выявленные проблемы: Несмотря на независимость микросервисов, ошибка в одном сервисе может вызвать каскадный сбой в других. Это создает риски для:
-
Масштабируемости
-
Отказоустойчивости
-
Мониторинга
-
Безопасности
Далее описан процесс запуска и исследования прототипа микросервисной архитектуры на базе Python с использованием gRPC и Docker. Прототип включает в себя три микросервиса (заказы, резервирование и лояльность) и API-шлюз (оркестратор) для координации запросов.
Запуск прототипа
Подготовка окружения
Чтобы запустить прототип, необходимо в первую очередь собрать образ контейнера. Для этого выполните следующие действия:
Шаг 1. Клонирование репозитория
Склонируйте репозиторий проекта на локальную машину.
$ git clone git@github.com:arootcom/grpc-fastapi.git
$ cd ./grpc-fastapi
Шаг 2. Переключение на рабочую ветку
Переключитесь на ветку day1.
$ git remote update
$ git checkout -b day1 origin/day1
$ git pull
Если всё выполнено верно, при просмотре списка локальных веток вы увидите следующий результат, где рабочая ветка будет помечена знаком *.
$ git branch
* day1
main
Шаг 3. Сборка образа
Соберите образ контейнера python3-grpc.
$ docker build -t python3-grpc .
Список доступных локально собранных образов должен обязательно содержать две записи.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
python3-grpc latest 1c1383197816 45 hours ago 1.25GB
python 3 4db51a7249e1 2 weeks ago 1.12GB
Шаг 4. Запуск сервиса
Теперь всё готово для запуска.
$ docker-compose -f docker-compose-signl.yaml up
main | 2025-12-19 09:26:34.346 | INFO | servers.server:__init__:42 - Enable reflation server
main | 2025-12-19 09:26:34.354 | INFO | servers.server:register:48 - Register: Order Service
main | 2025-12-19 09:26:34.354 | INFO | servers.server:register:50 - Register: Reserve Service
main | 2025-12-19 09:26:34.354 | INFO | servers.server:register:52 - Register: Loyalty Service
main | 2025-12-19 09:26:34.354 | INFO | servers.server:run:58 - *** Сервис gRPC запущен: 0.0.0.0:50091 ***
Конфигурация и запуск
Сервис main запускается вызовом команды python main.py.
Для управления контейнерами в приложении используется Docker Compose. Параметры окружения описаны в файле docker-compose-single.yaml.
version: '3.8'
name: "grpc-fastapi-singl"
services:
main:
container_name: main
image: python3-grpc
volumes:
- "./app:/usr/src/app"
environment:
- GRPC_HOST_ORDERS=0.0.0.0
- GRPC_HOST_LOYALTIES=0.0.0.0
- GRPC_HOST_RESERVE=0.0.0.0
- GRPC_HOST_LOCAL=0.0.0.0
- GRPC_PORT=50091
- SERVICE_PORT=1111
- SERVICE_HOST_LOCAL=0.0.0.0
ports:
- "8080:1111"
- "8787:50091"
command: "python main.py"
Инициализация
Enable Reflection Server — это сервис, который позволяет клиентам динамически обнаруживать сервисы и их методы на сервере gRPC во время выполнения.
Принцип работы:
Работает по протоколу gRPC Server Reflection Protocol для обнаружения сервисов.
Реализация:
Инициализация реализована в singleton-классе servers.server, который гарантирует, что у класса будет только один экземпляр. После инициализации выполняется регистрация доступных сервисов в данном экземпляре сервера и его запуск.
from loguru import logger
from grpc import aio
from grpc_reflection.v1alpha import reflection
[skip]
def __init__(self) -> None:
if not hasattr(self, 'initialized'):
self.SERVER_ADDRESS = f'{os.environ["GRPC_HOST_LOCAL"]}:{os.environ["GRPC_PORT"]}'
self.server = aio.server(ThreadPoolExecutor(max_workers=10))
self.server.add_insecure_port(self.SERVER_ADDRESS)
SERVICE_NAMES = (
order_pb2.DESCRIPTOR.services_by_name["OrderService"].full_name,
reserve_pb2.DESCRIPTOR.services_by_name["ReserveService"].full_name,
loyalty_pb2.DESCRIPTOR.services_by_name["LoyaltyService"].full_name,
reflection.SERVICE_NAME,
)
reflection.enable_server_reflection(SERVICE_NAMES, self.server)
logger.info(f'Enable reflation server')
self.initialized = True
[skip]
def register(self) -> None:
order_pb2_grpc.add_OrderServiceServicer_to_server(OrderService(), self.server)
logger.info(f'Register: Order Service')
reserve_pb2_grpc.add_ReserveServiceServicer_to_server(ReserveService(), self.server)
logger.info(f'Register: Reserve Service')
loyalty_pb2_grpc.add_LoyaltyServiceServicer_to_server(LoyaltyService(), self.server)
logger.info(f'Register: Loyalty Service')
async def run(self) -> None:
self.register()
await self.server.start()
logger.info(f'*** Сервис gRPC запущен: {self.SERVER_ADDRESS} ***')
await self.server.wait_for_termination()
[skip]
Исследование прототипа
Шаг 1. Просмотр запущенных контейнеров
При просмотре запущенных контейнеров вы увидите один контейнер с именем main (колонка NAMES).
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0064b53278c2 python3-grpc "python main.py" 4 days ago Up 3 days 0.0.0.0:8080->1111/tcp, [::]:8080->1111/tcp, 0.0.0.0:8787->50091/tcp, [::]:8787->50091/tcp main
Конфигурация портов:
Два порта, определённые в контейнере, проброшены на порты localhost
Шаг 2. REST API: создание заказа
Используем для запроса утилиту командной строки curl.
$ curl -X 'POST' 'http://localhost:8080/order?name=test&completed=false' -H 'accept: application/json'
Результат обработки запроса:
{
"notificationType": "ORDER_NOTIFICATION_TYPE_ENUM_OK",
"order": {
"uuid": "9841d6f7-69df-4986-b837-65c386acb41d"
}
}
В консоли, где запущен docker-compose, вы увидите логи — последовательные записи о вызовах и результатах работы связанных бизнес-логикой микросервисов:
-
Получение входящего запроса и запрос на резервирование
main | INFO: 172.22.0.1:56680 - "POST /order?name=test&completed=false HTTP/1.1" 200 OK
main | 2025-12-22 09:26:56.083 | INFO | servers.services.reserve:ReserveItem:16 - Received request is for reserve item: uuid='5f23a419-3a68-4c6a-87a9-d14f9f021a64' quantity=10
main | 2025-12-22 09:26:56.083 | SUCCESS | servers.handlers.reserve:reserve_item:12 - Reserved item: {'uuid': '5f23a419-3a68-4c6a-87a9-d14f9f021a64', 'name': 'asdf', 'instock': 3, 'reserve': 10}
main | 2025-12-22 09:26:56.084 | DEBUG | servers.services.reserve:ReserveItem:20 - Result reserve item: notificationType='RESERVE_NOTIFICATION_TYPE_ENUM_OK' item=Item(uuid='5f23a419-3a68-4c6a-87a9-d14f9f021a64', name='asdf', instock=3, reserve=10)
-
Запрос информации о лояльности
main | 2025-12-22 09:26:56.086 | INFO | servers.services.loyalty:LoyaltyInfo:16 - Received request is for loyalty info: uuid='5f23a419-3a68-4c6a-87a9-d14f9f021a64' quantity=10
main | 2025-12-22 09:26:56.086 | SUCCESS | servers.handlers.loyalty:loyalty_info:10 - Loyalty info: {'uuid': '5f23a419-3a68-4c6a-87a9-d14f9f021a64', 'persent': 10}
main | 2025-12-22 09:26:56.086 | DEBUG | servers.services.loyalty:LoyaltyInfo:20 - Result loyalty info: notificationType='LOYALTY_NOTIFICATION_TYPE_ENUM_OK' loyalty=Loyalty(uuid='5f23a419-3a68-4c6a-87a9-d14f9f021a64', persent=10)
-
Создание заказа
main | 2025-12-22 09:26:56.089 | INFO | servers.services.order:CreateOrder:25 - Received request is for create order: uuid='9841d6f7-69df-4986-b837-65c386acb41d' name='test' completed=False date='2025-12-19 09:26:34.311752Z'
main | 2025-12-22 09:26:56.104 | SUCCESS | servers.handlers.order:create_order:8 - Created order: [{'uuid': '9841d6f7-69df-4986-b837-65c386acb41d'}]
main | 2025-12-22 09:26:56.104 | DEBUG | servers.services.order:CreateOrder:29 - Result created order: notificationType='ORDER_NOTIFICATION_TYPE_ENUM_OK' order=OrderResponse(uuid='9841d6f7-69df-4986-b837-65c386acb41d', name=None, completed=False, date=None)
Шаг 3. Исследование gRPC-сервисов
Используем утилиту командной строки grpcurl для анализа:
Доступные операции:
-
Просмотр обслуживаемых микросервисов
$ grpcurl -plaintext localhost:8787 list
grpc.reflection.v1alpha.ServerReflection
protos.loyalties.LoyaltyService
protos.order.OrderService
protos.warehouse.ReserveService
-
Просмотр методов микросервиса
$ grpcurl -plaintext localhost:8787 list protos.order.OrderService
protos.order.OrderService.CreateOrder
-
Просмотр модели запроса
$ grpcurl -plaintext localhost:8787 describe protos.order.CreateOrderRequest
protos.order.CreateOrderRequest is a message:
message CreateOrderRequest {
string name = 1;
bool completed = 2;
string date = 3;
}
-
Создание заказа
$ grpcurl -plaintext -d '{"name":"first"}' localhost:8787 protos.order.OrderService/CreateOrder
{
"notificationType": "ORDER_NOTIFICATION_TYPE_ENUM_OK",
"order": {
"uuid": "0eca9a0f-a52a-4d76-b9f8-375d3387c143"
}
}
Важные особенности и ограничения
Прямой вызов gRPC
По логам видно, что при прямом вызове gRPC происходит прямое резервирование без учёта бизнес-логики проверки остатков и применения лояльности.
Это означает, что при прямом доступе нет гарантии атомарности операции — завершения всех шагов либо ни одного.
Рекомендации
Для обеспечения целостности данных и соблюдения бизнес-логики рекомендуется использовать запросы через API-шлюз (оркестратор), который обеспечивает корректную последовательность вызовов и применение всех необходимых проверок.
Заключение
Данный прототип демонстрирует базовые принципы работы микросервисной архитектуры с использованием gRPC и Docker. Следуя описанным шагам, вы сможете запустить и исследовать работу системы, понять особенности взаимодействия микросервисов и выявить потенциальные точки роста архитектуры.
Прямой доступ к gRPC-сервисам создаёт риск частичного выполнения бизнес-процесса, что может привести к:
-
Некорректным данным
-
Потере информации о применённых скидках
-
Резервированию недоступных товаров
Архитектурные улучшения
Для обеспечения целостности системы рекомендуется:
-
Использовать только API-шлюз (оркестратор) для бизнес-операций
-
Ограничить прямой доступ к gRPC-сервисам на уровне сети
-
Внедрить транзакционную модель (например, Saga Pattern)
-
Добавить мониторинг для отслеживания частичных сбоев
Следующие шаги эволюции:
-
Разделение контейнеров для независимого масштабирования
Решение
Разделение микросервисов по контейнерам — это архитектурное решение, обеспечивающее изоляцию сервисов, независимость в развертывании и масштабировании.
Ключевые преимущества подхода:
-
Минимизация размера контейнера. Каждый контейнер содержит только необходимые зависимости и код, что упрощает управление ресурсами и ускоряет развертывание.
-
Эффективное масштабирование. Каждый микросервис масштабируется независимо от других, обеспечивая оптимальное распределение нагрузки и рациональное использование инфраструктуры.
-
Упрощённое обновление и управление. Изменения в одном микросервисе не требуют пересоздания всех контейнеров — обновляется только затронутый сервис.
-
Гибкое управление зависимостями и версиями. Различные версии микросервисов могут существовать параллельно, не влияя на работу других компонентов системы.
Внесённые изменения в архитектуру:
-
Прямой вызов gRPC к микросервисам заказов, резервирования и лояльности заблокирован
-
Все вызовы осуществляются только через API Gateway по REST API
-
Контейнер main декомпозирован на четыре независимых контейнера: -- gw — микросервис-оркестратор, содержащий бизнес-логику создания заказа -- orders service — микросервис управления заказами -- reserve service — микросервис управления резервированием данных заказа -- loyalty service — микросервис управления данными о лояльности
Запуск измененного прототипа
Предварительные условия:
-- Репозиторий проекта склонирован
-- Выбрана ветка day1
-- Образ контейнера python3-grpc собран
Шаг 1. Запуск проекта
$ docker-compose -f ./docker-compose-multi.yaml up
[+] Running 4/0
⠿ Container gw Created
⠿ Container loyalties Created
⠿ Container orders Created
⠿ Container reserve Created
Attaching to gw, loyalties, orders, reserve
reserve | 2025-12-22 12:23:20.786 | INFO | servers.reserve:__init__:31 - Enable reflation server
reserve | 2025-12-22 12:23:20.786 | INFO | servers.reserve:register:37 - Register: Reserve Service
reserve | 2025-12-22 12:23:20.786 | INFO | servers.reserve:run:42 - *** Сервис Reserve gRPC запущен: reserve:50091 ***
loyalties | 2025-12-22 12:23:20.833 | INFO | servers.loyalties:__init__:31 - Enable reflation server
loyalties | 2025-12-22 12:23:20.834 | INFO | servers.loyalties:register:37 - Register: Loyalty Service
loyalties | 2025-12-22 12:23:20.834 | INFO | servers.loyalties:run:42 - *** Сервис Loyalties gRPC запущен: loyalties:50091 ***
orders | 2025-12-22 12:23:20.952 | INFO | servers.orders:__init__:32 - Enable reflation server
orders | 2025-12-22 12:23:20.959 | INFO | servers.orders:register:38 - Register: Order Service
orders | 2025-12-22 12:23:20.960 | INFO | servers.orders:run:44 - *** Сервис Orders gRPC запущен: orders:50091 ***
gw | INFO: Will watch for changes in these directories: ['/usr/src/app']
gw | INFO: Uvicorn running on http://0.0.0.0:1111 (Press CTRL+C to quit)
gw | INFO: Started reloader process [1] using StatReload
gw | INFO: Started server process [8]
gw | INFO: Waiting for application startup.
gw | INFO: Application startup complete.
По логам видно, что запустилось четыре контейнера, описанных в файле docker-compose-multi.yaml.
Скрипты для запуска контейнеров размещены в директории проекта app/ и соответствуют именам контейнеров:
Исследование измененного прототипа
Шаг 1. Просмотр запущенных контейнеров
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bde2a383c5f7 python3-grpc "python orders.py" 5 days ago Up About an hour orders
18a6bd24b3a4 python3-grpc "python loyalties.py" 5 days ago Up About an hour loyalties
0e5972f9928e python3-grpc "python reserve.py" 5 days ago Up About an hour reserve
5dc86b298da5 python3-grpc "python gw.py" 5 days ago Up About an hour 0.0.0.0:8080->1111/tcp, [::]:8080->1111/tcp gw
Видим четыре запущенных контейнера. Порт, определённый в контейнере gw, проброшен на порт localhost, как показано на схеме выше. Порт 8080 принимает запросы по HTTP.
Шаг 2. REST API: создание заказа
Используем утилиту командной строки curl. Сам запрос не изменился.
$ curl -X 'POST' 'http://localhost:8080/order?name=test&completed=false' -H 'accept: application/json'
Результат обработки запроса:
{
"notificationType": "ORDER_NOTIFICATION_TYPE_ENUM_OK",
"order": {
"uuid": "9841d6f7-69df-4986-b837-65c386acb41d"
}
}
Теперь логи и адреса вызовов изменились — каждый запрос идёт в отдельный контейнер:
Получение входящего запроса:
gw | INFO: 172.20.0.1:57392 - "POST /order?name=test&completed=false HTTP/1.1" 200 OK
Запрос на резервирование:
reserve | 2025-12-22 13:54:52.486 | INFO | servers.services.reserve:ReserveItem:16 - Received request is for reserve item: uuid='5f23a419-3a68-4c6a-87a9-d14f9f021a64' quantity=10
reserve | 2025-12-22 13:54:52.486 | SUCCESS | servers.handlers.reserve:reserve_item:12 - Reserved item: {'uuid': '5f23a419-3a68-4c6a-87a9-d14f9f021a64', 'name': 'asdf', 'instock': 3, 'reserve': 10}
reserve | 2025-12-22 13:54:52.486 | DEBUG | servers.services.reserve:ReserveItem:20 - Result reserve item: notificationType='RESERVE_NOTIFICATION_TYPE_ENUM_OK' item=Item(uuid='5f23a419-3a68-4c6a-87a9-d14f9f021a64', name='asdf', instock=3, reserve=10)
Запрос информации о лояльности:
loyalties | 2025-12-22 13:54:52.491 | INFO | servers.services.loyalty:LoyaltyInfo:16 - Received request is for loyalty info: uuid='5f23a419-3a68-4c6a-87a9-d14f9f021a64' quantity=10
loyalties | 2025-12-22 13:54:52.492 | SUCCESS | servers.handlers.loyalty:loyalty_info:10 - Loyalty info: {'uuid': '5f23a419-3a68-4c6a-87a9-d14f9f021a64', 'persent': 10}
loyalties | 2025-12-22 13:54:52.492 | DEBUG | servers.services.loyalty:LoyaltyInfo:20 - Result loyalty info: notificationType='LOYALTY_NOTIFICATION_TYPE_ENUM_OK' loyalty=Loyalty(uuid='5f23a419-3a68-4c6a-87a9-d14f9f021a64', persent=10)
Создание заказа:
orders | 2025-12-22 13:54:52.497 | INFO | servers.services.order:CreateOrder:25 - Received request is for create order: uuid='3b2b8bb5-f488-483d-a1f1-b2771fd84149' name='test' completed=False date='2025-12-22 12:23:22.447949Z'
orders | 2025-12-22 13:54:52.512 | SUCCESS | servers.handlers.order:create_order:8 - Created order: [{'uuid': '3b2b8bb5-f488-483d-a1f1-b2771fd84149'}]
orders | 2025-12-22 13:54:52.512 | DEBUG | servers.services.order:CreateOrder:29 - Result created order: notificationType='ORDER_NOTIFICATION_TYPE_ENUM_OK' order=OrderResponse(uuid='3b2b8bb5-f488-483d-a1f1-b2771fd84149', name=None, completed=False, date=None)
Обоснование
Принцип «Один микросервис — один контейнер» в микросервисной архитектуре обоснован необходимостью изоляции и переносимости микросервисов.
Этот подход позволяет:
-
Разрабатывать, тестировать и масштабировать компоненты по отдельности
-
Обеспечивать согласованность между средой разработки, тестовой средой и production
Ключевые преимущества:
-
Изоляция. Контейнеры работают независимо и не влияют друг на друга. Сбой в одном сервисе не приводит к падению всей системы.
-
Переносимость. Контейнеризированное приложение можно запускать в различных типах инфраструктуры без необходимости рефакторинга для каждой среды.
-- На «голом железе»
-- В виртуальных машинах
-- В облаке -
Экономия ресурсов. Контейнеры используют ядро хостовой операционной системы без полной виртуализации оборудования, что делает их легковесными и обеспечивает экономию ресурсов по сравнению с традиционными виртуальными машинами.
Последствия
Когда микросервисов становится много, управлять контейнерами вручную становится затруднительно. Здесь на помощь приходят оркестраторы — системы автоматизации развёртывания, масштабирования, балансировки и мониторинга контейнеров.
Например, Kubernetes обеспечивает:
-
Запуск контейнеров
-
Мониторинг состояния
-
Автоматический перезапуск после сбоев
-
Масштабирование сервисов в зависимости от нагрузки
Преимущества
-
Простота поддержки и обновления. Команды могут работать над разными сервисами параллельно, не мешая друг другу.
-
Избирательное масштабирование. Если один компонент испытывает высокую нагрузку, можно увеличить ресурсы только для него, а не для всего приложения.
-
Лёгкость модернизации. Старые сервисы можно постепенно заменять новыми без полного переписывания кода.
Недостатки и риски
-
Сложность тестирования. Необходимо тестировать:
-- Каждый микросервис отдельно
-- Взаимодействие между сервисами -
Ограничение внешних вызовов. Вызовы от внешних клиентов возможны только по REST API через API Gateway.
-
Отсутствие гарантии атомарности транзакций. Нет встроенного механизма, гарантирующего, что либо завершатся все шаги транзакции, либо ни одного.
Предложения:
-
Внедрить автоматические интеграционные и сервисные тесты.
-
Рассмотреть возможность добавления вызовов к gw по gRPC или альтернативного варианта коммуникации.
-
Внедрить паттерны обеспечения консистентности в распределённых системах:
-- Saga Pattern (паттерн саги)
-- Transactional Outbox (подход «сохрани, затем опубликуй»)
-- Idempotent Operations (идемпотентные операции)
Автор: rootcom
