← Назад к вопросам

Какие знаешь способы масштабирования БД?

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 для асинхронного обновления

Вывод: выбор способа масштабирования зависит от характера данных, объёма и требований к консистентности.

Какие знаешь способы масштабирования БД? | PrepBro