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

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

3.0 Senior🔥 131 комментариев
#Архитектура и паттерны#Базы данных (SQL)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Какие знаю способы шардирования БД?

Шардирование - это горизонтальное масштабирование базы данных, при котором данные распределяются между несколькими узлами. Это критичный паттерн для высоконагруженных систем, которые не влезают в один сервер.

1. Range-based Sharding (Диапазонное шардирование)

Данные распределяются по диапазону значений ключа шардирования.

from typing import List
import hashlib

class RangeBasedSharding:
    def __init__(self, num_shards: int):
        self.num_shards = num_shards
    
    def get_shard(self, user_id: int) -> int:
        """
        Примеры:
        user_id: 0-1000 -> shard 0
        user_id: 1001-2000 -> shard 1
        user_id: 2001-3000 -> shard 2
        """
        shard_size = 1000
        return (user_id - 1) // shard_size
    
    def get_shard_range(self, shard_id: int) -> tuple:
        shard_size = 1000
        start = shard_id * shard_size + 1
        end = (shard_id + 1) * shard_size
        return start, end

sharding = RangeBasedSharding(4)
print(sharding.get_shard(1500))  # Shard 1
print(sharding.get_shard(2500))  # Shard 2
print(sharding.get_shard_range(1))  # (1001, 2000)

# Плюсы:
# - Простая реализация
# - Easy range queries

# Минусы:
# - Hot spots (неравномерное распределение нагрузки)
# - Если user_id 0-1000 очень активны, shard 0 перегружен
# - Сложное переш
# ardирование при добавлении новых shards

2. Hash-based Sharding (Хеш-шардирование)

Используется хеш-функция для распределения данных.

import hashlib
from datetime import datetime

class HashBasedSharding:
    def __init__(self, num_shards: int):
        self.num_shards = num_shards
    
    def get_shard(self, key: str) -> int:
        """
        Использует хеш-функцию для распределения
        """
        hash_value = int(hashlib.md5(str(key).encode()).hexdigest(), 16)
        return hash_value % self.num_shards
    
    def distribute_users(self, users: List[str]) -> dict:
        distribution = {i: [] for i in range(self.num_shards)}
        for user in users:
            shard_id = self.get_shard(user)
            distribution[shard_id].append(user)
        return distribution

sharding = HashBasedSharding(num_shards=4)
users = ['alice', 'bob', 'charlie', 'diana', 'eve', 'frank']
result = sharding.distribute_users(users)
for shard_id, users_in_shard in result.items():
    print(f'Shard {shard_id}: {users_in_shard}')

# Плюсы:
# - Хорошее распределение нагрузки (равномерное)
# - Простая масштабируемость

# Минусы:
# - Range queries становятся очень дорогими (нужно скэннировать все shards)
# - При добавлении nового shard большинство ключей перехэшируются
# - Требует consistent hashing для минимизации переразработки

3. Consistent Hashing (Консистентное хеширование)

Улучшенная версия хеш-шардирования, которая минимизирует переразработку при добавлении узлов.

import hashlib
from bisect import bisect_right

class ConsistentHashing:
    def __init__(self, num_virtual_nodes: int = 160):
        self.num_virtual_nodes = num_virtual_nodes
        self.ring = {}  # hash -> node
        self.sorted_keys = []  # Отсортированные хеши
        self.nodes = set()
    
    def _hash(self, key: str) -> int:
        return int(hashlib.md5(str(key).encode()).hexdigest(), 16)
    
    def add_node(self, node_id: str):
        self.nodes.add(node_id)
        for i in range(self.num_virtual_nodes):
            virtual_key = f'{node_id}:{i}'
            hash_value = self._hash(virtual_key)
            self.ring[hash_value] = node_id
        self.sorted_keys = sorted(self.ring.keys())
    
    def remove_node(self, node_id: str):
        self.nodes.discard(node_id)
        for i in range(self.num_virtual_nodes):
            virtual_key = f'{node_id}:{i}'
            hash_value = self._hash(virtual_key)
            del self.ring[hash_value]
        self.sorted_keys = sorted(self.ring.keys())
    
    def get_node(self, key: str) -> str:
        if not self.ring:
            raise ValueError('No nodes in the ring')
        
        hash_value = self._hash(key)
        idx = bisect_right(self.sorted_keys, hash_value)
        if idx == len(self.sorted_keys):
            idx = 0
        return self.ring[self.sorted_keys[idx]]

ch = ConsistentHashing()
ch.add_node('node1')
ch.add_node('node2')
ch.add_node('node3')

# При добавлении нового узла только часть данных переразрабатывается
for user in ['alice', 'bob', 'charlie']:
    print(f'{user} -> {ch.get_node(user)}')

print('\nAdding node4...')
ch.add_node('node4')

for user in ['alice', 'bob', 'charlie']:
    print(f'{user} -> {ch.get_node(user)}')

# Плюсы:
# - Минимальное переразрабатывание при добавлении узлов
# - Хорошее распределение нагрузки
# - Используется в Redis Cluster, Cassandra, DynamoDB

# Минусы:
# - Сложнее в реализации
# - Все еще не оптимизирована для range queries

4. Directory-based Sharding (Справочное шардирование)

Используется таблица-справочник для хранения информации о том, на каком shard находятся данные.

from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

Base = declarative_base()

class ShardDirectory(Base):
    __tablename__ = 'shard_directory'
    
    user_id = Column(Integer, primary_key=True)
    shard_id = Column(Integer, nullable=False)

class ShardingService:
    def __init__(self, num_shards: int):
        self.num_shards = num_shards
        # Подключение к master БД со справочником
        self.engine = create_engine('postgresql://master')
        Session = sessionmaker(bind=self.engine)
        self.session = Session()
    
    def assign_shard(self, user_id: int) -> int:
        """
        Простое распределение: новый пользователь -> случайный shard
        """
        import random
        shard_id = random.randint(0, self.num_shards - 1)
        
        # Записываем в справочник
        directory = ShardDirectory(user_id=user_id, shard_id=shard_id)
        self.session.add(directory)
        self.session.commit()
        
        return shard_id
    
    def get_shard(self, user_id: int) -> int:
        directory = self.session.query(ShardDirectory).filter_by(user_id=user_id).first()
        if not directory:
            return self.assign_shard(user_id)
        return directory.shard_id

# Плюсы:
# - Полная гибкость в распределении данных
# - Легко переразрабатывать данные
# - Поддерживает сложную логику распределения

# Минусы:
# - Требует дополнительного запроса к справочнику (latency)
# - Справочник становится bottleneck (нужно масштабировать)
# - Однозначный point of failure

5. Geographic Sharding (Географическое шардирование)

Данные распределяются по географическим регионам.

class GeographicSharding:
    REGIONS = {
        'us-east': {'shard_ids': [0, 1], 'datacenter': 'Virginia'},
        'eu-west': {'shard_ids': [2, 3], 'datacenter': 'Ireland'},
        'ap-southeast': {'shard_ids': [4, 5], 'datacenter': 'Singapore'},
    }
    
    def get_shard(self, user_id: int, region: str) -> int:
        import random
        if region not in self.REGIONS:
            raise ValueError(f'Unknown region: {region}')
        
        shard_ids = self.REGIONS[region]['shard_ids']
        return random.choice(shard_ids)
    
    def get_user_shard(self, user_id: int, user_region: str) -> str:
        shard_id = self.get_shard(user_id, user_region)
        return f'db-shard-{shard_id}'

sharding = GeographicSharding()
print(sharding.get_user_shard(123, 'us-east'))  # db-shard-0 or db-shard-1
print(sharding.get_user_shard(456, 'eu-west'))  # db-shard-2 or db-shard-3

# Плюсы:
# - Низкие latencies для пользователей
# - Data residency compliance (GDPR)
# - Disaster recovery по регионам

# Минусы:
# - Скомплицированная логика репликации между регионами
# - Нужно синхронизировать между разными DC

6. Time-based Sharding (Временное шардирование)

Данные распределяются по времени создания.

from datetime import datetime

class TimeBasedSharding:
    def __init__(self, months_per_shard: int = 1):
        self.months_per_shard = months_per_shard
        self.start_date = datetime(2020, 1, 1)
    
    def get_shard(self, timestamp: datetime) -> int:
        months_passed = (timestamp.year - self.start_date.year) * 12
        months_passed += (timestamp.month - self.start_date.month)
        return months_passed // self.months_per_shard
    
    def get_shard_range(self, shard_id: int) -> tuple:
        from datetime import timedelta
        start = self.start_date + timedelta(days=30 * self.months_per_shard * shard_id)
        end = start + timedelta(days=30 * self.months_per_shard)
        return start, end

sharding = TimeBasedSharding(months_per_shard=3)  # Квартальное шардирование
print(sharding.get_shard(datetime(2023, 6, 15)))  # Shard 14

# Плюсы:
# - Отлично для логов и временных рядов (time-series)
# - Легко удалять старые данные (drop shard)
# - Хорошая локальность данных

# Минусы:
# - Неравномерное распределение нагрузки
# - Все запросы за текущий месяц идут в один shard
# - Сложный routing в большие периоды времени

Сравнительная таблица

МетодРаспределениеRange QueryПереразработкаСложность
RangeПлохоеОтличноеПлохоеНизкая
HashОтличноеПлохоеПлохоеНизкая
Consistent HashОтличноеПлохоеХорошееСредняя
DirectoryГибкоеОтличноеОтличноеВысокая
GeographicХорошееЗависитСреднееСредняя
Time-basedПлохоеОтличноеХорошееНизкая

Проблемы при шардировании

# Problem 1: Hot shard
class HotShardMitigation:
    def __init__(self):
        self.user_traffic = {}  # user_id -> traffic
    
    def detect_hot_shard(self):
        # Если один shard получает 80% трафика - разбить его
        pass
    
    def split_shard(self, shard_id):
        # Перемещаем часть данных в новый shard
        pass

# Problem 2: Joins across shards
# Решение: денормализация или приложение-уровень joins

# Problem 3: Transactions across shards
# Решение: two-phase commit, Saga pattern, или избегаем

# Problem 4: Resharding
class ReshardingStrategy:
    def double_shards(self):
        # Когда шарды переполнены, удваиваем их количество
        # Процесс:
        # 1. Создаем новые шарды
        # 2. Копируем данные (с миграцией)
        # 3. Переключаемся на новую схему
        # 4. Удаляем старые шарды
        pass

Когда использовать шардирование

Шардирование нужно только когда:

  1. Данные не влезают в один сервер
  2. Нагрузка на чтение слишком высокая для репликации
  3. Вертикальное масштабирование исчерпано

До этого используй:

  • Индексы
  • Репликацию
  • Кеширование
  • Read replicas

Шардирование - это последний шаг масштабирования, потому что оно существенно усложняет архитектуру и разработку.

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