Для чего нужно шардирование?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Шардирование в распределённых системах
Шардирование (sharding) — это техника горизонтального масштабирования базы данных, при которой данные разделяются между несколькими независимыми экземплярами БД на основе ключа шарда.
Суть проблемы, которую решает шардирование
Когда один сервер БД больше не может вместить растущий объём данных или обработать количество запросов, обычное решение — вертикальное масштабирование (более мощный сервер) — достигает своего предела. Шардирование позволяет масштабироваться горизонтально, распределяя нагрузку между несколькими серверами.
Основные причины использования
1. Преодоление ограничений одного сервера
# Без шардирования — один сервер обрабатывает всё
# 10 миллиардов записей в таблице users — слишком медленно
# С шардированием — данные разделены
# Сервер 1: user_id % 4 == 0
# Сервер 2: user_id % 4 == 1
# Сервер 3: user_id % 4 == 2
# Сервер 4: user_id % 4 == 3
Каждый сервер хранит только 2.5 млрд записей вместо 10 млрд. Запросы становятся намного быстрее.
2. Уменьшение нагрузки на I/O и процессор
Если один сервер обрабатывает 100,000 QPS (запросов в секунду), а нам нужно 400,000 QPS, мы можем разделить нагрузку на 4 шарда по 100,000 QPS каждый.
def get_shard_id(user_id: int, num_shards: int = 4) -> int:
"""Определить, на каком шарде лежит юзер"""
return user_id % num_shards
def get_user_database(user_id: int):
"""Подключиться к нужной БД на основе шарда"""
shard_id = get_shard_id(user_id)
return DATABASES[f"shard_{shard_id}"]
# Использование
db = get_user_database(user_id=12345)
user = db.query(User).filter(User.id == 12345).first()
3. Улучшение производительности кеширования
Когда данные разделены на шарды, кеш-линия каждого сервера становится эффективнее, так как он работает с меньшим подмножеством данных.
Стратегии шардирования
Range-based шардирование
Данные разделяются по диапазонам значения ключа:
def get_shard_by_range(user_id: int):
if 1 <= user_id <= 1000000:
return "shard_1" # Database 1
elif 1000001 <= user_id <= 2000000:
return "shard_2" # Database 2
elif 2000001 <= user_id <= 3000000:
return "shard_3" # Database 3
Проблема: дисбалансировка. Если пользователей 1-1000000 намного больше, чем 2000001-3000000, нагрузка будет неравномерной.
Hash-based шардирование
Применить хеш-функцию и взять остаток от деления:
def get_shard_hash(user_id: int, num_shards: int = 4):
return hash(user_id) % num_shards
Преимущество: равномерное распределение. Недостаток: при добавлении нового шарда почти все данные нужно переместить.
Consistent Hashing
Решает проблему дорогостоящего перемещения данных при добавлении новых шардов.
from hashlib import md5
class ConsistentHash:
def __init__(self, nodes=None, replicas=3):
self.replicas = replicas
self.ring = {}
self.sorted_keys = []
if nodes:
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(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(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]]
# Использование
ch = ConsistentHash(nodes=["shard_1", "shard_2", "shard_3", "shard_4"])
print(ch.get_node("user_12345"))
Практические примеры из реальных систем
MongoDB Sharding:
# Пример конфигурации шардирования MongoDB
db.adminCommand({
shardCollection: "database.users",
key: { user_id: 1 }
})
PostgreSQL с pg_partman:
-- Партиционирование таблицы по диапазонам
CREATE TABLE users (
id BIGINT,
name VARCHAR(255),
email VARCHAR(255)
) PARTITION BY RANGE (id);
CREATE TABLE users_partition_1 PARTITION OF users
FOR VALUES FROM (1) TO (1000000);
CREATE TABLE users_partition_2 PARTITION OF users
FOR VALUES FROM (1000000) TO (2000000);
Проблемы и сложности шардирования
1. Расширение неизбежно
Когда один шард вырастает слишком большой, его нужно ресшардировать (пересчитать и переместить данные). Это сложная операция.
2. Запросы, охватывающие несколько шардов (scatter-gather)
async def get_all_users_by_email(email: str):
results = []
for shard_id in range(num_shards):
db = get_shard_connection(shard_id)
result = await db.query(
"SELECT * FROM users WHERE email = %s",
(email,)
)
results.extend(result)
return results
Такие запросы медленнее и сложнее в отладке.
3. Транзакции между шардами
Дистрибьютивные транзакции очень сложны и дорогие. Обычно используют паттерн Saga или просто избегают кросс-шардовых транзакций на уровне приложения.
4. Балансировка нагрузки
Если ключ шарда выбран плохо (например, по географии, и 80% юзеров в одной стране), то один шард будет перегружен.
Когда использовать шардирование
- Когда одна БД больше не может хранить данные (десятки ТБ, миллиарды записей)
- Когда QPS превышает возможности одного сервера (очень высокая нагрузка)
- Когда нужна географическая распределённость (шарды в разных регионах)
Когда НЕ использовать
- Если объём данных умещается на одном сервере
- Если нагрузка не максимальна и есть резерв
- Если много кросс-шардовых запросов (выбрать другую архитектуру)
Шардирование — мощный инструмент, но сложный в реализации. Лучше отложить его на потом, пока это не станет абсолютно необходимо.