← Назад к вопросам
Что нужно учитывать при разработке, чтобы приложение могло быть горизонтально масштабируемым?
2.3 Middle🔥 121 комментариев
#DevOps и инфраструктура#Django
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Горизонтальное масштабирование: как проектировать приложение
Горизонтальное масштабирование означает добавление большего количества серверов, а не увеличение мощности одного сервера (вертикальное масштабирование). После 10+ лет опыта я разработал набор принципов, которые обеспечивают возможность масштабирования.
1. Stateless дизайн приложения
Это самое критичное правило.
# ❌ ПЛОХО: Stateful приложение
class AuthService:
def __init__(self):
self.logged_in_users = {} # Состояние в памяти!
def login(self, user_id):
self.logged_in_users[user_id] = datetime.now()
def is_logged_in(self, user_id):
return user_id in self.logged_in_users
# Проблема: если у нас 3 сервера, они не знают о пользователях друг друга
# Пользователь логинится на сервер 1, запрос идёт на сервер 2 → не залогинен
# ✅ ХОРОШО: Stateless приложение
class AuthService:
def __init__(self, redis_client):
self.redis = redis_client
def login(self, user_id):
self.redis.set(f"user:{user_id}:logged_in", "true", ex=3600)
def is_logged_in(self, user_id):
return self.redis.exists(f"user:{user_id}:logged_in")
# Все серверы смотрят в один Redis → согласованность
Правило: Все состояние должно быть во внешних хранилищах (БД, Redis, S3).
2. Отсутствие локального кэша и сессий
# ❌ ПЛОХО: Кэш в памяти приложения
from functools import lru_cache
@lru_cache(maxsize=1000)
def get_user(user_id):
return db.query(User).filter(User.id == user_id).first()
# Проблемы:
# - Каждый сервер имеет свой кэш → разные данные
# - Обновление на сервере 1 не видно на сервере 2
# - Память растёт неконтролируемо
# ✅ ХОРОШО: Кэш во Redis
import redis
redis_client = redis.Redis(host='localhost', port=6379)
def get_user(user_id):
# Проверяем кэш
cached = redis_client.get(f"user:{user_id}")
if cached:
return json.loads(cached)
# Берём из БД
user = db.query(User).filter(User.id == user_id).first()
# Кэшируем
redis_client.set(
f"user:{user_id}",
json.dumps(user.to_dict()),
ex=3600 # 1 час TTL
)
return user
# Все серверы видят один кэш
3. Сессии в распределённом хранилище
# ❌ ПЛОХО: Сессии на сервере
# app.py (Flask)
from flask import session
@app.route('/login', methods=['POST'])
def login():
user = authenticate(username, password)
session['user_id'] = user.id # Сохраняется на сервере
return "Logged in"
# Проблема: load balancer отправил запрос на другой сервер
# Сессия там не существует → logout
# ✅ ХОРОШО: Сессии в Redis
from flask_session import Session
from flask_session.backends.redis import RedisSessionInterface
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = redis.from_url('redis://localhost:6379')
Session(app)
@app.route('/login', methods=['POST'])
def login():
user = authenticate(username, password)
session['user_id'] = user.id # Сохраняется в Redis
return "Logged in"
# Все серверы читают из Redis → всё работает
4. Асинхронная обработка длительных задач
# ❌ ПЛОХО: Длительная операция в request-response цикле
@app.post("/send-email")
def send_email(email: str):
# Это займёт 5 секунд
smtp = smtplib.SMTP('smtp.gmail.com', 587)
smtp.send_message(create_email(email))
smtp.quit()
return {"status": "sent"}
# Проблемы:
# - HTTP запрос ждёт 5 секунд
# - Если ошибка → пользователь видит ошибку
# - При масштабировании: 100 запросов одновременно
# = нужно 100 воркеров на 500 секунд = дорого
# ✅ ХОРОШО: Асинхронная задача через очередь
from celery import Celery
from celery.result import AsyncResult
app = FastAPI()
celery_app = Celery('tasks')
@celery_app.task
async def send_email_async(email: str):
smtp = smtplib.SMTP('smtp.gmail.com', 587)
smtp.send_message(create_email(email))
smtp.quit()
@app.post("/send-email")
def send_email(email: str):
# Добавляем в очередь и возвращаем immediately
task = send_email_async.delay(email)
return {"task_id": task.id, "status": "queued"}
# Преимущества:
# - HTTP возвращает мгновенно (< 100ms)
# - Celery workers обрабатывают асинхронно
# - 100 запросов = 100 задач в очереди
# - 10 workers обрабатывают их параллельно
5. Правильное использование БД
# ❌ ПЛОХО: N+1 запросы
users = db.query(User).all()
for user in users:
posts = db.query(Post).filter(Post.user_id == user.id).all() # 1000 запросов!
print(f"{user.name}: {len(posts)} posts")
# При масштабировании: 1000 пользователей = 1001 запрос в БД
# На 3 серверах = 3003 запроса одновременно
# БД может не выдержать
# ✅ ХОРОШО: Eager loading
users = db.query(User).options(
joinedload(User.posts) # Один запрос вместо 1001!
).all()
for user in users:
print(f"{user.name}: {len(user.posts)} posts")
# Итого: 1 запрос вместо 1001 = 1000x быстрее
6. Кэширование на всех уровнях
# На разных уровнях:
# 1. HTTP кэширование (браузер)
@app.get("/api/products")
def get_products():
return {
"products": products_list,
"cache-control": "public, max-age=3600"
}
# 2. Redis кэш (приложение)
redis_client.set("products:list", json.dumps(products), ex=3600)
# 3. Database indices
db.execute("""
CREATE INDEX idx_products_category
ON products(category)
""")
# 4. Query результаты
results = db.query(Product).filter(...).all()
redis_client.set("query:products:electronics", results, ex=1800)
# 5. CDN для статики
static_url = "https://cdn.example.com/images/product-1.jpg"
7. Database sharding (разбиение данных)
# На 10 млн пользователей: одна БД не может
# Решение: sharding — разбить данные по серверам
def get_shard_number(user_id: int) -> int:
"""Определяет, на каком shard'е находится пользователь"""
return user_id % NUM_SHARDS
def get_user(user_id: int):
shard_num = get_shard_number(user_id)
db_connection = get_db_connection(f"shard_{shard_num}")
user = db_connection.query(User).filter(
User.id == user_id
).first()
return user
# Результат:
# - 10 млн пользователей распределены на 5 БД
# - Каждая БД имеет 2 млн записей
# - Запросы параллельны на разных серверах
8. Connection pooling
# ❌ ПЛОХО: Новое подключение для каждого запроса
for i in range(100):
conn = psycopg2.connect(dbname="mydb", user="user", password="pass")
# Execute query
conn.close()
# Проблема: подключение к БД требует 100-500ms
# 100 запросов = 10-50 секунд только на подключение!
# ✅ ХОРОШО: Connection pool
from sqlalchemy import create_engine
engine = create_engine(
"postgresql://user:password@localhost/mydb",
pool_size=20, # 20 постоянных подключений
max_overflow=10, # ещё до 10 временных
pool_recycle=3600 # переподключаться каждый час
)
session = Session(engine)
for i in range(100):
# Берёт подключение из пула (мгновенно)
user = session.query(User).first()
# Результат: мгновенно, без задержек
9. Логирование структурировано
# ❌ ПЛОХО: Обычное логирование
import logging
logger = logging.getLogger()
logger.info(f"User {user_id} logged in")
# Проблемы:
# - Логи в разных форматах
# - Нельзя искать по полям
# - При масштабировании: логи на 5 серверах
# как их все смотреть?
# ✅ ХОРОШО: Структурированное логирование
import structlog
logger = structlog.get_logger()
logger.info(
"user_login",
user_id=user_id,
ip_address=request.remote_addr,
timestamp=datetime.now(),
server=os.getenv("HOSTNAME")
)
# В ELK Stack (Elasticsearch):
# POST /logs/_doc
# {
# "event": "user_login",
# "user_id": 123,
# "ip_address": "192.168.1.1",
# "server": "app-server-1"
# }
# Теперь можно: Kibana → поиск всех логинов на 2025-03-23
10. Health checks и graceful shutdown
# ✅ Kubernetes/Docker должны знать когда сервер здоров
@app.get("/health")
def health_check():
# Проверяем зависимости
if not redis_client.ping():
return {"status": "unhealthy"}, 503
if not db.ping():
return {"status": "unhealthy"}, 503
return {"status": "healthy", "uptime": time.time() - start_time}
# Graceful shutdown: закончить текущие запросы, потом выключиться
import signal
def signal_handler(signum, frame):
logger.info("Shutting down gracefully...")
# Не принимаем новые запросы
# Ждём текущие завершиться (timeout=30s)
# Закрываем БД и Redis
exit(0)
signal.signal(signal.SIGTERM, signal_handler)
Чек-лист для горизонтального масштабирования
✅ Приложение stateless (всё состояние во внешних хранилищах)
✅ Нет локального кэша (используется Redis)
✅ Сессии в распределённом хранилище
✅ Длительные задачи асинхронны (Celery, RabbitMQ)
✅ Database N+1 решены (eager loading, indices)
✅ Connection pooling настроен
✅ Логирование структурированное и централизованное
✅ Health checks работают
✅ Graceful shutdown реализован
✅ Запросы идемпотентны (повторный запрос = тот же результат)
✅ Нет file uploads на локальный диск (используется S3)
✅ Версионирование API (легче обновлять)
Архитектура с 1000+ одновременными пользователями
Load Balancer (nginx)
↓
├─ App Server 1
├─ App Server 2
├─ App Server 3
└─ App Server N
↓
├─ PostgreSQL (master-slave replication)
├─ Redis Cluster (кэш и сессии)
└─ RabbitMQ (очередь задач)
↓
├─ Celery Worker 1
├─ Celery Worker 2
└─ Celery Worker N
Заключение
Горизонтальное масштабирование — это не добавление серверов в конце. Это архитектурные решения, которые нужно принять в самом начале проекта. Главное правило: приложение должно быть stateless, и все её зависимости (БД, кэш, сессии, логи) должны быть распределённые и масштабируемые.