← Назад к вопросам
Какие знаешь способы масштабирования БД?
2.3 Middle🔥 221 комментариев
#Архитектура и паттерны#Базы данных (SQL)
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Способы масштабирования БД
Масштабирование БД — один из самых сложных аспектов системного дизайна. Существует два основных подхода и множество стратегий.
1. Вертикальное масштабирование (Scale-Up)
Увеличение ресурсов на одной машине:
Проблема: SELECT * FROM users WHERE active = true -- 5 млн. пользователей
Решение 1: Повысить RAM (8GB → 64GB)
Решение 2: Использовать лучший процессор (4 cores → 32 cores)
Решение 3: Быстрее SSD (SATA → NVMe)
Преимущества:
- Просто внедрить
- Нет архитектурных изменений
- ACID транзакции остаются простыми
Недостатки:
- Дорого — каждый раз удваивание цены
- Верхний предел — физические ограничения сервера
- Downtime при обновлении
- Единая точка отказа
2. Горизонтальное масштабирование (Scale-Out)
Распределение данных между несколькими машинами:
Один сервер PostgreSQL:
┌──────────────────┐
│ PostgreSQL │
│ 100GB данных │
└──────────────────┘
Три сервера с шардированием:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Shard 1 │ │ Shard 2 │ │ Shard 3 │
│ Users ID │ │ Users ID │ │ Users ID │
│ 1-33M │ │ 33M-66M │ │ 66M-100M │
│ ~33GB │ │ ~33GB │ │ ~34GB │
└──────────────┘ └──────────────┘ └──────────────┘
3. Шардирование (Sharding)
Разделение данных по горизонтали:
Шардирование по Range
def get_shard_id(user_id: int, num_shards: int = 3) -> int:
# Shard 0: user_id 0-999
# Shard 1: user_id 1000-1999
# Shard 2: user_id 2000-2999
return (user_id // 1000) % num_shards
# Проблемы: неравномерное распределение при росте user_id
Шардирование по Hash
import hashlib
def get_shard_id(user_id: int, num_shards: int = 3) -> int:
hash_value = int(hashlib.md5(str(user_id).encode()).hexdigest(), 16)
return hash_value % num_shards
# Лучше: распределение равномерно
# Проблемы: при добавлении/удалении шардов нужна перебалансировка
# Использование
user_id = 12345
shard = get_shard_id(user_id) # Shard 0
db_connection = connections[f'shard_{shard}']
result = db_connection.query(f'SELECT * FROM users WHERE id = {user_id}')
Consistent Hashing
class ConsistentHash:
def __init__(self, nodes: list, replicas: int = 3):
self.replicas = replicas
self.ring = {}
self.sorted_keys = []
for node in nodes:
self.add_node(node)
def add_node(self, node):
for i in range(self.replicas):
virtual_key = f'{node}:{i}'
hash_value = int(hashlib.md5(virtual_key.encode()).hexdigest(), 16)
self.ring[hash_value] = node
self.sorted_keys = sorted(self.ring.keys())
def get_node(self, key):
if not self.ring:
return None
hash_value = int(hashlib.md5(key.encode()).hexdigest(), 16)
for ring_key in self.sorted_keys:
if hash_value <= ring_key:
return self.ring[ring_key]
return self.ring[self.sorted_keys[0]]
# При добавлении шарда только ~1/n данных нужно переместить
ch = ConsistentHash(['shard_1', 'shard_2', 'shard_3'])
shard = ch.get_node('user_12345')
4. Репликация
Копирование данных для надёжности и читаемости:
# Primary-Replica архитектура
┌─────────────────┐
│ Primary │ -- Записи
│ Master │
└────────┬────────┘
│ Replication
│
┌────┴────┬─────────────┐
│ │ │
┌───▼──┐ ┌───▼──┐ ┌───▼──┐
│Replica1 │Replica2 │Replica3│ -- Только чтение
└────────┘└────────┘└────────┘
Read Replicas в SQLAlchemy
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# Master для записи
master_engine = create_engine('postgresql://localhost/db')
# Replicas для чтения
replica_engines = [
create_engine('postgresql://replica1/db'),
create_engine('postgresql://replica2/db'),
create_engine('postgresql://replica3/db'),
]
import random
def get_read_connection():
return random.choice(replica_engines)
def write_user(name: str):
session = sessionmaker(bind=master_engine)()
user = User(name=name)
session.add(user)
session.commit()
return user
def read_user(user_id: int):
session = sessionmaker(bind=get_read_connection())()
user = session.query(User).filter(User.id == user_id).first()
return user
5. Денормализация и кэширование
# Вместо JOIN которые медленные
# SELECT u.name, COUNT(p.id)
# FROM users u LEFT JOIN posts p ON u.id = p.user_id
# WHERE u.id = ?
# Сохранять count в denormalized колонке
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
posts_count = Column(Integer, default=0) # Денормализованное поле
# На каждое создание post обновляем:
def create_post(user_id, content):
post = Post(user_id=user_id, content=content)
user = db.query(User).filter(User.id == user_id).first()
user.posts_count += 1
db.commit()
6. Кэширование (Redis/Memcached)
import redis
from functools import wraps
import json
cache = redis.Redis(host='localhost', port=6379)
def cached(ttl=3600):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
cache_key = f'{func.__name__}:{args}:{kwargs}'
# Попытка получить из кэша
cached_result = cache.get(cache_key)
if cached_result:
return json.loads(cached_result)
# Вычислить и кэшировать
result = func(*args, **kwargs)
cache.setex(cache_key, ttl, json.dumps(result))
return result
return wrapper
return decorator
@cached(ttl=3600)
def get_user_by_id(user_id: int):
return db.query(User).filter(User.id == user_id).first()
# Использование
user = get_user_by_id(123) # Запрос в БД
user = get_user_by_id(123) # Из кэша Redis
7. Партиционирование таблиц
-- Партиционирование по диапазону дат
CREATE TABLE events (
id SERIAL,
user_id INT,
created_at TIMESTAMP,
data JSONB
) PARTITION BY RANGE (EXTRACT(YEAR FROM created_at));
CREATE TABLE events_2023 PARTITION OF events
FOR VALUES FROM (2023) TO (2024);
CREATE TABLE events_2024 PARTITION OF events
FOR VALUES FROM (2024) TO (2025);
-- Запросы автоматически идут в нужное разбиение
SELECT * FROM events WHERE created_at > '2024-01-01'
8. Connection Pooling
from sqlalchemy.pool import QueuePool
engine = create_engine(
'postgresql://localhost/db',
poolclass=QueuePool,
pool_size=20, # Количество постоянных соединений
max_overflow=40, # Максимум временных соединений
pool_recycle=3600, # Переустанавливать соединение каждый час
echo=False
)
9. Асинхронное выполнение
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
engine = create_async_engine(
'postgresql+asyncpg://localhost/db',
echo=False,
pool_size=20,
max_overflow=40
)
async def get_user(user_id: int):
async with AsyncSession(engine) as session:
user = await session.execute(
select(User).where(User.id == user_id)
)
return user.scalar_one_or_none()
10. CQRS (Command Query Responsibility Segregation)
# Separate write и read модели
class UserWrite: # Для записи — нормализованная
id: int
name: str
email: str
class UserReadModel: # Для чтения — денормализованная
id: int
name: str
email: str
posts_count: int
followers_count: int
last_activity: datetime
# Писать в primary, читать из read replicas/elastic/cache
Сравнительная таблица
Метод | Сложность | Стоимость | Надёжность | Консистентность
---|---|---|---|---
Вертикальное | Низкая | Высокая | Средняя | Сильная (ACID)
Горизонтальное | Высокая | Средняя | Высокая | Слабая (BASE)
Шардирование | Высокая | Средняя | Высокая | Средняя
Репликация | Средняя | Средняя | Высокая | Слабая (лаг)
Кэширование | Низкая | Низкая | Низкая | Очень слабая
Партиционирование | Средняя | Низкая | Средняя | Сильная
Практический пример: YouTube-like система
1M записей на секунду — нужна горизонтальная масштабируемость
Слой 1: Шардирование по user_id
├─ Shard 1: Users 0-1M
├─ Shard 2: Users 1M-2M
└─ Shard 3: Users 2M-3M
Слой 2: Репликация каждого шарда
├─ Primary (write)
└─ 3 Replicas (read)
Слой 3: Кэширование
├─ Redis для горячих данных (trending videos)
└─ CDN для видео файлов
Слой 4: Денормализация
├─ Сохраняем view_count в видео
├─ Сохраняем subscriber_count в channel
└─ Используем queue для асинхронного обновления
Вывод: выбор способа масштабирования зависит от характера данных, объёма и требований к консистентности.