Контейнеризация, Kubernetes, логирование, метрики, алертинг.
Деплой — это не конец, а начало. В этой теме научимся контейнеризировать, развёртывать и мониторить MCP серверы в продакшене.
# Dockerfile
FROM python:3.11-slim
# Рабочая директория
WORKDIR /app
# Установка зависимостей
RUN pip install --no-cache-dir poetry==1.7.0
COPY pyproject.toml poetry.lock* ./
RUN poetry config virtualenvs.create false && \
poetry install --only main --no-interaction
# Копирование кода
COPY src/ ./src/
# Переменные окружения
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# Порт для SSE/WebSocket
EXPOSE 8000
# Запуск сервера
CMD ["python", "-m", "uvicorn", "src.server:app", "--host", "0.0.0.0", "--port", "8000"]# Dockerfile
# Stage 1: Build
FROM python:3.11-slim as builder
WORKDIR /app
RUN pip install --no-cache-dir poetry==1.7.0
COPY pyproject.toml poetry.lock* ./
RUN poetry config virtualenvs.create false && \
poetry install --no-interaction
COPY src/ ./src/
# Stage 2: Runtime
FROM python:3.11-slim as runtime
WORKDIR /app
# Создание не-root пользователя
RUN useradd -m -u 1000 mcpuser
# Копирование из builder
COPY /usr/local/lib/python3.11/site-packages/ /usr/local/lib/python3.11/site-packages/
COPY /app/src/ ./src/
# Переключение на не-root пользователя
USER mcpuser
# Переменные окружения
ENV PYTHONUNBUFFERED=1
EXPOSE 8000
# Health check
HEALTHCHECK \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
CMD ["python", "-m", "uvicorn", "src.server:app", "--host", "0.0.0.0", "--port", "8000"]# docker-compose.yml
version: '3.8'
services:
mcp-server:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/mcp
- REDIS_URL=redis://redis:6379
- LOG_LEVEL=INFO
depends_on:
- db
- redis
volumes:
- ./src:/app/src # Hot reload для разработки
restart: unless-stopped
db:
image: postgres:15-alpine
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=mcp
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres_data:# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-server
labels:
app: mcp-server
spec:
replicas: 3
selector:
matchLabels:
app: mcp-server
template:
metadata:
labels:
app: mcp-server
spec:
containers:
- name: mcp-server
image: myregistry/mcp-server:1.0.0
ports:
- containerPort: 8000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: mcp-secrets
key: database-url
- name: LOG_LEVEL
value: "INFO"
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8000
initialDelaySeconds: 5
periodSeconds: 5# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: mcp-server
spec:
selector:
app: mcp-server
ports:
- protocol: TCP
port: 80
targetPort: 8000
type: ClusterIP# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mcp-server
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
tls:
- hosts:
- mcp.example.com
secretName: mcp-tls
rules:
- host: mcp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: mcp-server
port:
number: 80# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: mcp-server-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: mcp-server
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80import logging
import json
import sys
from pythonjsonlogger import jsonlogger
class CustomJsonFormatter(jsonlogger.JsonFormatter):
"""Кастомный JSON форматтер для логов."""
def add_fields(self, log_record, record, message_dict):
super().add_fields(log_record, record, message_dict)
log_record['level'] = record.levelname
log_record['logger'] = record.name
log_record['timestamp'] = record.created
def setup_logging(level: str = "INFO"):
"""Настройка структурированного логирования."""
logger = logging.getLogger()
logger.setLevel(getattr(logging, level.upper()))
handler = logging.StreamHandler(sys.stdout)
formatter = CustomJsonFormatter(
'%(timestamp)s %(level)s %(name)s %(message)s'
)
handler.setFormatter(formatter)
logger.addHandler(handler)
return logger
logger = setup_logging()
# Использование
logger.info(
"Tool called",
extra={
"tool_name": "search",
"user_id": "123",
"duration_ms": 45
}
)import logging
from contextvars import ContextVar
import uuid
# Контекстная переменная для trace_id
trace_id_var: ContextVar[str] = ContextVar('trace_id', default='')
class TraceFilter(logging.Filter):
"""Фильтр для добавления trace_id в логи."""
def filter(self, record):
record.trace_id = trace_id_var.get()
return True
def setup_trace_logging():
"""Настройка логирования с trace_id."""
handler = logging.StreamHandler()
handler.addFilter(TraceFilter())
formatter = logging.Formatter(
'%(asctime)s [%(trace_id)s] %(levelname)s %(name)s: %(message)s'
)
handler.setFormatter(formatter)
logger = logging.getLogger()
logger.addHandler(handler)
logger.setLevel(logging.INFO)
def generate_trace_id() -> str:
"""Генерация trace_id для запроса."""
return str(uuid.uuid4())
# Использование в MCP инструменте
@mcp.tool()
async def traced_tool(param: str):
trace_id = generate_trace_id()
token = trace_id_var.set(trace_id)
try:
logger.info("Tool started", extra={"param": param})
result = await process(param)
logger.info("Tool completed")
return result
finally:
trace_id_var.reset(token)from prometheus_client import Counter, Histogram, Gauge, start_http_server
import time
# Метрики
REQUEST_COUNT = Counter(
'mcp_requests_total',
'Total MCP requests',
['tool', 'status']
)
REQUEST_DURATION = Histogram(
'mcp_request_duration_seconds',
'Request duration',
['tool'],
buckets=(0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0)
)
ACTIVE_CONNECTIONS = Gauge(
'mcp_active_connections',
'Active connections'
)
ERROR_COUNT = Counter(
'mcp_errors_total',
'Total errors',
['tool', 'error_type']
)
def metrics_middleware(func):
"""Декоратор для сбора метрик."""
@wraps(func)
async def wrapper(*args, **kwargs):
tool_name = func.__name__
start_time = time.time()
ACTIVE_CONNECTIONS.inc()
try:
result = await func(*args, **kwargs)
REQUEST_COUNT.labels(tool=tool_name, status='success').inc()
return result
except Exception as e:
REQUEST_COUNT.labels(tool=tool_name, status='error').inc()
ERROR_COUNT.labels(tool=tool_name, error_type=type(e).__name__).inc()
raise
finally:
duration = time.time() - start_time
REQUEST_DURATION.labels(tool=tool_name).observe(duration)
ACTIVE_CONNECTIONS.dec()
return wrapper
# Запуск endpoint метрик
start_http_server(9090) # /metrics endpoint{
"dashboard": {
"title": "MCP Server",
"panels": [
{
"title": "Request Rate",
"targets": [{
"expr": "rate(mcp_requests_total[5m])"
}]
},
{
"title": "Error Rate",
"targets": [{
"expr": "rate(mcp_errors_total[5m])"
}]
},
{
"title": "P95 Latency",
"targets": [{
"expr": "histogram_quantile(0.95, rate(mcp_request_duration_seconds_bucket[5m]))"
}]
}
]
}
}from fastapi import FastAPI, HTTPException
from datetime import datetime
import asyncio
app = FastAPI()
@app.get("/health")
async def health_check():
"""Базовая проверка здоровья."""
return {
"status": "healthy",
"timestamp": datetime.utcnow().isoformat(),
"version": "1.0.0"
}
@app.get("/ready")
async def readiness_check():
"""Проверка готовности обрабатывать запросы."""
checks = {}
# Проверка БД
try:
async with db_pool.acquire() as conn:
await conn.fetchval("SELECT 1")
checks["database"] = "ok"
except Exception as e:
checks["database"] = f"error: {e}"
# Проверка Redis
try:
await redis.ping()
checks["redis"] = "ok"
except Exception as e:
checks["redis"] = f"error: {e}"
# Проверка внешнего API
try:
async with aiohttp.ClientSession() as session:
async with session.get("https://api.example.com/health", timeout=5) as resp:
resp.raise_for_status()
checks["external_api"] = "ok"
except Exception as e:
checks["external_api"] = f"error: {e}"
all_healthy = all(v == "ok" for v in checks.values())
if not all_healthy:
raise HTTPException(status_code=503, detail=checks)
return {"status": "ready", "checks": checks}
@app.get("/live")
async def liveness_check():
"""Проверка живости (не заблокирован ли процесс)."""
return {"status": "alive"}# alertmanager/rules.yml
groups:
- name: mcp_server
rules:
- alert: HighErrorRate
expr: rate(mcp_errors_total[5m]) > 0.1
for: 5m
labels:
severity: warning
annotations:
summary: "High error rate on {{ $labels.instance }}"
description: "Error rate is {{ $value }} errors/sec"
- alert: HighLatency
expr: histogram_quantile(0.95, rate(mcp_request_duration_seconds_bucket[5m])) > 1
for: 10m
labels:
severity: warning
annotations:
summary: "High latency on {{ $labels.instance }}"
description: "P95 latency is {{ $value }}s"
- alert: PodCrashLooping
expr: rate(kube_pod_container_status_restarts_total[15m]) > 0
for: 5m
labels:
severity: critical
annotations:
summary: "Pod {{ $labels.pod }} is crash looping"# .gitlab-ci.yml
stages:
- test
- build
- deploy
variables:
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
test:
stage: test
image: python:3.11
script:
- pip install poetry
- poetry install
- poetry run pytest tests/ --cov=src --cov-report=xml
- poetry run coverage report --fail-under=80
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
build:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $DOCKER_IMAGE .
- docker push $DOCKER_IMAGE
only:
- main
deploy-staging:
stage: deploy
image: bitnami/kubectl
script:
- kubectl set image deployment/mcp-server mcp-server=$DOCKER_IMAGE -n staging
environment:
name: staging
only:
- main
deploy-production:
stage: deploy
image: bitnami/kubectl
script:
- kubectl set image deployment/mcp-server mcp-server=$DOCKER_IMAGE -n production
environment:
name: production
when: manual
only:
- main| Компонент | Назначение |
|---|---|
| Docker | Контейнеризация для переносимости |
| Kubernetes | Оркестрация, масштабирование, self-healing |
| Логирование | Структурированные логи с trace_id |
| Метрики | Prometheus для мониторинга производительности |
| Health checks | /health, /ready, /live endpoints |
| Алертинг | Уведомления о проблемах |
Следующая тема: Интеграция с системами — legacy, микросервисы, event-driven архитектура.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.