microservices example
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# Используем official Python runtime как базовый образ
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Устанавливаем рабочую директорию в контейнере
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем файл зависимостей
|
||||
COPY requirements.txt .
|
||||
|
||||
# Устанавливаем зависимости
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Копируем код приложения
|
||||
COPY service-1-main.py main.py
|
||||
|
||||
# Экспонируем порт
|
||||
EXPOSE 8001
|
||||
|
||||
# Команда для запуска приложения
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"]
|
||||
@@ -0,0 +1,20 @@
|
||||
# Используем official Python runtime как базовый образ
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Устанавливаем рабочую директорию в контейнере
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем файл зависимостей
|
||||
COPY requirements.txt .
|
||||
|
||||
# Устанавливаем зависимости
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Копируем код приложения
|
||||
COPY service-2-main.py main.py
|
||||
|
||||
# Экспонируем порт
|
||||
EXPOSE 8002
|
||||
|
||||
# Команда для запуска приложения
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8002"]
|
||||
@@ -1,3 +1,313 @@
|
||||
# ApiGatewayTest
|
||||
# Примеры тестирования микросервисов через API Gateway
|
||||
|
||||
Минимально жизнеспособный пример работы api gateway
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
.
|
||||
├── service-1-main.py # User Service
|
||||
├── service-2-main.py # Order Service
|
||||
├── requirements.txt # Зависимости
|
||||
├── Dockerfile.service1 # Docker образ для Service 1
|
||||
├── Dockerfile.service2 # Docker образ для Service 2
|
||||
└── docker-compose.yaml # Конфигурация Docker Compose
|
||||
```
|
||||
|
||||
## Запуск контейнеров
|
||||
|
||||
```bash
|
||||
# Построить образы и запустить контейнеры
|
||||
docker-compose up --build
|
||||
|
||||
# Запустить в фоновом режиме
|
||||
docker-compose up -d --build
|
||||
|
||||
# Просмотреть логи
|
||||
docker-compose logs -f
|
||||
|
||||
# Остановить контейнеры
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## Service 1 - User Service (порт 8001)
|
||||
|
||||
### Health Check
|
||||
```bash
|
||||
curl http://localhost:8001/
|
||||
curl http://localhost:8001/health
|
||||
```
|
||||
|
||||
### Получить список пользователей
|
||||
```bash
|
||||
curl http://localhost:8001/users
|
||||
```
|
||||
|
||||
### Получить пользователя по ID
|
||||
```bash
|
||||
curl http://localhost:8001/users/1
|
||||
```
|
||||
|
||||
### Создать нового пользователя
|
||||
```bash
|
||||
curl -X POST http://localhost:8001/users \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Петр Иванов",
|
||||
"email": "petr@example.com",
|
||||
"age": 30
|
||||
}'
|
||||
```
|
||||
|
||||
### Обновить пользователя
|
||||
```bash
|
||||
curl -X PUT http://localhost:8001/users/1 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Иван Петров (обновлено)",
|
||||
"email": "ivan.updated@example.com",
|
||||
"age": 29
|
||||
}'
|
||||
```
|
||||
|
||||
### Удалить пользователя
|
||||
```bash
|
||||
curl -X DELETE http://localhost:8001/users/1
|
||||
```
|
||||
|
||||
### Получить статистику
|
||||
```bash
|
||||
curl http://localhost:8001/stats
|
||||
```
|
||||
|
||||
## Service 2 - Order Service (порт 8002)
|
||||
|
||||
### Health Check
|
||||
```bash
|
||||
curl http://localhost:8002/
|
||||
curl http://localhost:8002/health
|
||||
```
|
||||
|
||||
### Получить список продуктов
|
||||
```bash
|
||||
curl http://localhost:8002/products
|
||||
```
|
||||
|
||||
### Получить продукт по ID
|
||||
```bash
|
||||
curl http://localhost:8002/products/1
|
||||
```
|
||||
|
||||
### Создать новый продукт
|
||||
```bash
|
||||
curl -X POST http://localhost:8002/products \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Мышка беспроводная",
|
||||
"price": 1500.00,
|
||||
"quantity": 20
|
||||
}'
|
||||
```
|
||||
|
||||
### Обновить продукт
|
||||
```bash
|
||||
curl -X PUT http://localhost:8002/products/1 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Ноутбук (обновлено)",
|
||||
"price": 55000.00,
|
||||
"quantity": 3
|
||||
}'
|
||||
```
|
||||
|
||||
### Удалить продукт
|
||||
```bash
|
||||
curl -X DELETE http://localhost:8002/products/1
|
||||
```
|
||||
|
||||
### Получить список заказов
|
||||
```bash
|
||||
curl http://localhost:8002/orders
|
||||
```
|
||||
|
||||
### Получить заказ по ID
|
||||
```bash
|
||||
curl http://localhost:8002/orders/1
|
||||
```
|
||||
|
||||
### Создать новый заказ
|
||||
```bash
|
||||
curl -X POST http://localhost:8002/orders \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"user_id": 2,
|
||||
"product_id": 2,
|
||||
"quantity": 2
|
||||
}'
|
||||
```
|
||||
|
||||
### Получить статистику
|
||||
```bash
|
||||
curl http://localhost:8002/stats
|
||||
```
|
||||
|
||||
## Тестирование через Python requests
|
||||
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
|
||||
BASE_URL_SERVICE1 = "http://localhost:8001"
|
||||
BASE_URL_SERVICE2 = "http://localhost:8002"
|
||||
|
||||
# Проверка здоровья Service 1
|
||||
response = requests.get(f"{BASE_URL_SERVICE1}/health")
|
||||
print(f"Service 1 Health: {response.json()}")
|
||||
|
||||
# Получение всех пользователей
|
||||
response = requests.get(f"{BASE_URL_SERVICE1}/users")
|
||||
print(f"Users: {response.json()}")
|
||||
|
||||
# Создание пользователя
|
||||
user_data = {
|
||||
"name": "Тест Пользователь",
|
||||
"email": "test@example.com",
|
||||
"age": 25
|
||||
}
|
||||
response = requests.post(f"{BASE_URL_SERVICE1}/users", json=user_data)
|
||||
print(f"Created User: {response.json()}")
|
||||
|
||||
# Проверка здоровья Service 2
|
||||
response = requests.get(f"{BASE_URL_SERVICE2}/health")
|
||||
print(f"Service 2 Health: {response.json()}")
|
||||
|
||||
# Получение всех продуктов
|
||||
response = requests.get(f"{BASE_URL_SERVICE2}/products")
|
||||
print(f"Products: {response.json()}")
|
||||
|
||||
# Создание заказа
|
||||
order_data = {
|
||||
"user_id": 1,
|
||||
"product_id": 3,
|
||||
"quantity": 5
|
||||
}
|
||||
response = requests.post(f"{BASE_URL_SERVICE2}/orders", json=order_data)
|
||||
print(f"Created Order: {response.json()}")
|
||||
```
|
||||
|
||||
## Конфигурация API Gateway
|
||||
|
||||
Для проверки работы API Gateway, вы можете использовать:
|
||||
|
||||
1. **Kong** - популярный API Gateway
|
||||
2. **NGINX** - простой и быстрый
|
||||
3. **Traefik** - автоматическое обнаружение сервисов
|
||||
4. **AWS API Gateway** - облачное решение
|
||||
|
||||
### Пример конфигурации NGINX
|
||||
|
||||
```nginx
|
||||
upstream service1 {
|
||||
server service-1:8001;
|
||||
}
|
||||
|
||||
upstream service2 {
|
||||
server service-2:8002;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8000;
|
||||
server_name localhost;
|
||||
|
||||
location /api/users {
|
||||
proxy_pass http://service1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location /api/products {
|
||||
proxy_pass http://service2;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location /api/orders {
|
||||
proxy_pass http://service2;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Основные характеристики микросервисов
|
||||
|
||||
### Service 1 - User Service
|
||||
- **Порт**: 8001
|
||||
- **Функционал**: Управление пользователями (CRUD)
|
||||
- **Endpoints**:
|
||||
- `GET /` - Проверка статуса
|
||||
- `GET /health` - Health check
|
||||
- `GET /users` - Список пользователей
|
||||
- `GET /users/{id}` - Получить пользователя
|
||||
- `POST /users` - Создать пользователя
|
||||
- `PUT /users/{id}` - Обновить пользователя
|
||||
- `DELETE /users/{id}` - Удалить пользователя
|
||||
- `GET /stats` - Статистика
|
||||
|
||||
### Service 2 - Order Service
|
||||
- **Порт**: 8002
|
||||
- **Функционал**: Управление продуктами и заказами (CRUD)
|
||||
- **Endpoints**:
|
||||
- `GET /` - Проверка статуса
|
||||
- `GET /health` - Health check
|
||||
- `GET /products` - Список продуктов
|
||||
- `GET /products/{id}` - Получить продукт
|
||||
- `POST /products` - Создать продукт
|
||||
- `PUT /products/{id}` - Обновить продукт
|
||||
- `DELETE /products/{id}` - Удалить продукт
|
||||
- `GET /orders` - Список заказов
|
||||
- `GET /orders/{id}` - Получить заказ
|
||||
- `POST /orders` - Создать заказ
|
||||
- `GET /stats` - Статистика
|
||||
|
||||
## Логирование и мониторинг
|
||||
|
||||
Оба сервиса включают:
|
||||
- Встроенное логирование (INFO уровень)
|
||||
- Health check endpoints
|
||||
- Метрики статистики
|
||||
- Документацию Swagger (автоматическая генерация)
|
||||
|
||||
### Доступ к Swagger документации
|
||||
|
||||
- Service 1: `http://localhost:8001/docs`
|
||||
- Service 2: `http://localhost:8002/docs`
|
||||
|
||||
## Отладка
|
||||
|
||||
```bash
|
||||
# Проверить статус контейнеров
|
||||
docker-compose ps
|
||||
|
||||
# Просмотреть логи конкретного сервиса
|
||||
docker-compose logs service-1
|
||||
docker-compose logs service-2
|
||||
|
||||
# Подключиться к контейнеру
|
||||
docker-compose exec service-1 /bin/bash
|
||||
|
||||
# Проверить сетевое подключение между сервисами
|
||||
docker-compose exec service-1 curl http://service-2:8002/health
|
||||
```
|
||||
|
||||
## Требования
|
||||
|
||||
- Docker 20.10+
|
||||
- Docker Compose 2.0+
|
||||
- Python 3.11+ (для локального запуска без Docker)
|
||||
- curl или PostMan для тестирования API
|
||||
|
||||
## Лицензия
|
||||
|
||||
MIT
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Service 1 - User Service (порт 8001)
|
||||
service-1:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.service1
|
||||
container_name: user-service
|
||||
ports:
|
||||
- "8001:8001"
|
||||
environment:
|
||||
- SERVICE_NAME=user-service
|
||||
- SERVICE_PORT=8001
|
||||
networks:
|
||||
- microservices-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8001/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
labels:
|
||||
service.type: "microservice"
|
||||
service.name: "user-service"
|
||||
service.port: "8001"
|
||||
|
||||
# Service 2 - Order Service (порт 8002)
|
||||
service-2:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.service2
|
||||
container_name: order-service
|
||||
ports:
|
||||
- "8002:8002"
|
||||
environment:
|
||||
- SERVICE_NAME=order-service
|
||||
- SERVICE_PORT=8002
|
||||
networks:
|
||||
- microservices-network
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8002/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
labels:
|
||||
service.type: "microservice"
|
||||
service.name: "order-service"
|
||||
service.port: "8002"
|
||||
|
||||
networks:
|
||||
microservices-network:
|
||||
driver: bridge
|
||||
name: microservices-network
|
||||
@@ -0,0 +1,4 @@
|
||||
fastapi==0.109.1
|
||||
uvicorn[standard]==0.27.0
|
||||
pydantic==2.5.3
|
||||
python-multipart==0.0.6
|
||||
@@ -0,0 +1,206 @@
|
||||
"""
|
||||
Второй микросервис - Service 2
|
||||
Предоставляет API для управления заказами и продуктами
|
||||
"""
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
app = FastAPI(
|
||||
title="Service 2 - Order Service",
|
||||
description="Микросервис для управления заказами и продуктами",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# Логирование
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Модели данных
|
||||
class Product(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
price: float
|
||||
quantity: int
|
||||
|
||||
class ProductCreate(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
quantity: int
|
||||
|
||||
class Order(BaseModel):
|
||||
id: int
|
||||
user_id: int
|
||||
product_id: int
|
||||
quantity: int
|
||||
total_price: float
|
||||
created_at: str
|
||||
|
||||
class OrderCreate(BaseModel):
|
||||
user_id: int
|
||||
product_id: int
|
||||
quantity: int
|
||||
|
||||
# Имитация базы данных
|
||||
products_db = {
|
||||
1: {"id": 1, "name": "Ноутбук", "price": 50000.00, "quantity": 5},
|
||||
2: {"id": 2, "name": "Монитор", "price": 15000.00, "quantity": 10},
|
||||
3: {"id": 3, "name": "Клавиатура", "price": 3000.00, "quantity": 25},
|
||||
}
|
||||
|
||||
orders_db = {
|
||||
1: {
|
||||
"id": 1,
|
||||
"user_id": 1,
|
||||
"product_id": 1,
|
||||
"quantity": 1,
|
||||
"total_price": 50000.00,
|
||||
"created_at": "2025-01-01T10:00:00"
|
||||
},
|
||||
}
|
||||
|
||||
next_product_id = 4
|
||||
next_order_id = 2
|
||||
|
||||
# Маршруты
|
||||
@app.get("/", tags=["Health"])
|
||||
async def root():
|
||||
"""Проверка работоспособности Service 2"""
|
||||
logger.info("Health check для Service 2")
|
||||
return {
|
||||
"service": "Service 2 - Order Service",
|
||||
"status": "operational",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
||||
@app.get("/health", tags=["Health"])
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
return {"status": "healthy", "service": "order-service"}
|
||||
|
||||
# Endpoints для продуктов
|
||||
@app.get("/products", response_model=List[Product], tags=["Products"])
|
||||
async def list_products(skip: int = 0, limit: int = 10):
|
||||
"""Получить список всех продуктов"""
|
||||
logger.info(f"Получение списка продуктов (skip={skip}, limit={limit})")
|
||||
products_list = list(products_db.values())
|
||||
return products_list[skip:skip + limit]
|
||||
|
||||
@app.get("/products/{product_id}", response_model=Product, tags=["Products"])
|
||||
async def get_product(product_id: int):
|
||||
"""Получить продукт по ID"""
|
||||
logger.info(f"Получение продукта с ID: {product_id}")
|
||||
if product_id not in products_db:
|
||||
logger.warning(f"Продукт с ID {product_id} не найден")
|
||||
raise HTTPException(status_code=404, detail="Продукт не найден")
|
||||
return products_db[product_id]
|
||||
|
||||
@app.post("/products", response_model=Product, tags=["Products"])
|
||||
async def create_product(product: ProductCreate):
|
||||
"""Создать новый продукт"""
|
||||
global next_product_id
|
||||
logger.info(f"Создание нового продукта: {product.name}")
|
||||
|
||||
new_product = {
|
||||
"id": next_product_id,
|
||||
"name": product.name,
|
||||
"price": product.price,
|
||||
"quantity": product.quantity
|
||||
}
|
||||
products_db[next_product_id] = new_product
|
||||
next_product_id += 1
|
||||
|
||||
logger.info(f"Продукт создан с ID: {new_product['id']}")
|
||||
return new_product
|
||||
|
||||
@app.put("/products/{product_id}", response_model=Product, tags=["Products"])
|
||||
async def update_product(product_id: int, product: ProductCreate):
|
||||
"""Обновить продукт"""
|
||||
logger.info(f"Обновление продукта с ID: {product_id}")
|
||||
if product_id not in products_db:
|
||||
raise HTTPException(status_code=404, detail="Продукт не найден")
|
||||
|
||||
updated_product = {
|
||||
"id": product_id,
|
||||
"name": product.name,
|
||||
"price": product.price,
|
||||
"quantity": product.quantity
|
||||
}
|
||||
products_db[product_id] = updated_product
|
||||
return updated_product
|
||||
|
||||
@app.delete("/products/{product_id}", tags=["Products"])
|
||||
async def delete_product(product_id: int):
|
||||
"""Удалить продукт"""
|
||||
logger.info(f"Удаление продукта с ID: {product_id}")
|
||||
if product_id not in products_db:
|
||||
raise HTTPException(status_code=404, detail="Продукт не найден")
|
||||
|
||||
del products_db[product_id]
|
||||
return {"message": f"Продукт {product_id} удален"}
|
||||
|
||||
# Endpoints для заказов
|
||||
@app.get("/orders", response_model=List[Order], tags=["Orders"])
|
||||
async def list_orders(skip: int = 0, limit: int = 10):
|
||||
"""Получить список всех заказов"""
|
||||
logger.info(f"Получение списка заказов (skip={skip}, limit={limit})")
|
||||
orders_list = list(orders_db.values())
|
||||
return orders_list[skip:skip + limit]
|
||||
|
||||
@app.get("/orders/{order_id}", response_model=Order, tags=["Orders"])
|
||||
async def get_order(order_id: int):
|
||||
"""Получить заказ по ID"""
|
||||
logger.info(f"Получение заказа с ID: {order_id}")
|
||||
if order_id not in orders_db:
|
||||
logger.warning(f"Заказ с ID {order_id} не найден")
|
||||
raise HTTPException(status_code=404, detail="Заказ не найден")
|
||||
return orders_db[order_id]
|
||||
|
||||
@app.post("/orders", response_model=Order, tags=["Orders"])
|
||||
async def create_order(order: OrderCreate):
|
||||
"""Создать новый заказ"""
|
||||
global next_order_id
|
||||
logger.info(f"Создание нового заказа для пользователя {order.user_id}")
|
||||
|
||||
if order.product_id not in products_db:
|
||||
raise HTTPException(status_code=404, detail="Продукт не найден")
|
||||
|
||||
product = products_db[order.product_id]
|
||||
if product["quantity"] < order.quantity:
|
||||
raise HTTPException(status_code=400, detail="Недостаточно товара на складе")
|
||||
|
||||
total_price = product["price"] * order.quantity
|
||||
|
||||
new_order = {
|
||||
"id": next_order_id,
|
||||
"user_id": order.user_id,
|
||||
"product_id": order.product_id,
|
||||
"quantity": order.quantity,
|
||||
"total_price": total_price,
|
||||
"created_at": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
orders_db[next_order_id] = new_order
|
||||
product["quantity"] -= order.quantity
|
||||
next_order_id += 1
|
||||
|
||||
logger.info(f"Заказ создан с ID: {new_order['id']}")
|
||||
return new_order
|
||||
|
||||
@app.get("/stats", tags=["Stats"])
|
||||
async def get_stats():
|
||||
"""Получить статистику по заказам"""
|
||||
logger.info("Получение статистики")
|
||||
total_revenue = sum(order["total_price"] for order in orders_db.values())
|
||||
return {
|
||||
"total_orders": len(orders_db),
|
||||
"total_products": len(products_db),
|
||||
"total_revenue": total_revenue,
|
||||
"service": "order-service"
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8002)
|
||||
Reference in New Issue
Block a user