Как нужно работать с шардированной базой данных?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как нужно работать с шардированной базой данных?
Работа с шардированной БД — это совсем другой уровень сложности. Нужно понимать специфику и избежать распространённых ошибок.
Основной принцип шардирования
Шардирование — горизонтальное разбиение данных на несколько независимых БД по ключу шарда.
Одна большая БД (1 млн пользователей):
users (id 1-1,000,000)
↓
Шардирование по user_id
↓
Три независимых БД:
Shard 1: users (id 1-333,333)
Shard 2: users (id 333,334-666,666)
Shard 3: users (id 666,667-1,000,000)
Как выбрать ключ шарда (Shard Key)
Ключ шарда — идентификатор, по которому определяется, в какой шард поместить запись.
Хорошие кандидаты:
- user_id — максимально частая фильтрация
- tenant_id — в SaaS приложениях
- customer_id — для e-commerce
- organization_id — в B2B системах
Плохие кандидаты:
- country — очень неравномерное распределение
- email — может измениться
- subscription_plan — часто меняется
Правильный выбор:
- Уникален — каждая запись имеет один ключ шарда
- Стабилен — не меняется со временем
- Распределяется равномерно — нет "горячих" шардов
- Используется в большинстве запросов — по нему фильтруем
Определение шарда (Shard Resolution)
Формула распределения:
shard_id = hash(shard_key) % number_of_shards
Пример:
def get_shard(user_id, num_shards=3):
return hash(user_id) % num_shards
user_id = 42
shard_id = get_shard(42, 3)
# hash(42) = 8765
# 8765 % 3 = 2
# Данные пользователя 42 в Shard 2
Важно: Никогда не меняй функцию хеша или количество шардов без перебалансировки!
Когда можешь запрашивать без знания шарда
1. Если знаешь shard_key в запросе
# Хорошо: знаем user_id
user_id = 42
shard_id = get_shard(user_id)
query = f"SELECT * FROM users WHERE id = {user_id}"
result = execute_on_shard(shard_id, query)
Время: ~5ms (одна БД)
2. Если нужны все шарды (VERY EXPENSIVE!)
# Плохо: нужно запросить все 3 шарда
query = "SELECT * FROM users WHERE status = 'active'"
results = []
for shard_id in range(num_shards):
result = execute_on_shard(shard_id, query)
results.extend(result)
# Объединить результаты
final_result = aggregate_results(results)
Время: 3 × 100ms = ~300ms (потому что 3 параллельные БД)
Проблемы:
- Нужно запросить ВСЕ шарды
- Сложнее обработать ошибки (одна шард может упасть)
- Нужно объединить результаты в памяти
- Может вернуться 1 млн строк
Чего НЕЛЬЗЯ делать в шардированной БД
1. JOIN между шардами
-- НЕЛЬЗЯ: orders в Shard 1, products в Shard 2
SELECT o.*, p.name
FROM orders o
JOIN products p ON o.product_id = p.id;
-- Почему: data of orders и products в разных БД!
Решение:
# Загрузить в приложение
orders = get_orders(shard_1, user_id)
product_ids = [o['product_id'] for o in orders]
products = get_products(shard_2, product_ids) # Может быть другой sharding key!
# JOIN в памяти приложения
result = join_in_memory(orders, products)
2. Фильтрация без shard_key
-- ОЧЕНЬ ПЛОХО: фильтр по email
SELECT * FROM users WHERE email = 'ivan@example.com';
-- Почему: нужно запросить ВСЕ 3 шарда, потому что не знаешь какой user_id
Решение:
# Вспомогательная таблица (lookup table)
# В шарде 0 (или отдельной БД) хранить маппинг:
email -> user_id
email = 'ivan@example.com'
user_id = lookup_table.get(email) # Получить user_id
shard_id = get_shard(user_id)
user = execute_on_shard(shard_id, f"SELECT * FROM users WHERE id = {user_id}")
3. Распределённые транзакции
-- НЕЛЬЗЯ: транзакция охватывает 2 шарда
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE shard_key = 1;
UPDATE accounts SET balance = balance + 100 WHERE shard_key = 2;
COMMIT;
-- Если упадёт между UPDATE-ами, данные несогласованные!
Решение: Используй компенсирующие транзакции или Saga паттерн.
Архитектура работы с шардами
Вариант 1: Прямое подключение (Simple)
def get_shard_connection(shard_id):
shards = {
0: "db1.example.com:5432",
1: "db2.example.com:5432",
2: "db3.example.com:5432",
}
return connect(shards[shard_id])
def get_user(user_id):
shard_id = hash(user_id) % 3
conn = get_shard_connection(shard_id)
return conn.execute(f"SELECT * FROM users WHERE id = {user_id}")
Плюсы: Просто Минусы: Клиент знает логику шардирования
Вариант 2: Middleware (Proxy)
Приложение -> Shard Proxy -> Shard Resolver -> Shard 1, 2, 3
(один вход)
Примеры: Vitess (для MySQL), Citus (для PostgreSQL).
# Приложение не знает о шардировании
result = proxy.execute(
"SELECT * FROM users WHERE user_id = 42 AND ..."
)
# Proxy сам определит shard_id и маршрутизирует запрос
Плюсы: Приложение не знает о шардах, проще разработка Минусы: Дополнительный слой, задержка
Типичные операции с шардами
1. Создание записи (CREATE)
def create_order(user_id, product_id, amount):
shard_id = get_shard(user_id)
query = """
INSERT INTO orders (id, user_id, product_id, amount)
VALUES (uuid_generate_v4(), %s, %s, %s)
"""
execute_on_shard(shard_id, query, [user_id, product_id, amount])
Правило: Всегда включай shard_key в запрос!
2. Чтение записи (READ)
def get_user_orders(user_id):
shard_id = get_shard(user_id)
query = "SELECT * FROM orders WHERE user_id = %s"
return execute_on_shard(shard_id, query, [user_id])
Оптимизация: Добавить индекс на user_id внутри каждого шарда.
3. Обновление (UPDATE)
def update_order_status(user_id, order_id, new_status):
shard_id = get_shard(user_id)
query = """
UPDATE orders
SET status = %s
WHERE user_id = %s AND id = %s
"""
execute_on_shard(shard_id, query, [new_status, user_id, order_id])
Правило: WHERE включает shard_key!
4. Скан по диапазону (RANGE)
def get_all_orders_by_date(start_date, end_date):
# БЕЗ user_id — нужны ВСЕ шарды!
results = []
for shard_id in range(num_shards):
query = """
SELECT * FROM orders
WHERE created_at >= %s AND created_at <= %s
"""
result = execute_on_shard(shard_id, query, [start_date, end_date])
results.extend(result)
return results
Проблема: O(N × M) где N = количество шардов, M = записей в каждом.
Перебалансировка (Resharding)
Проблема: Со временем нужно добавить новые шарды.
Использование растёт, 3 шарда уже не хватает.
Нужно добавить 4-й шард.
Что произойдёт:
user_id = 42: hash(42) % 3 = 2 -> Shard 2
user_id = 42: hash(42) % 4 = 2 -> Shard 2 (OK, совпадает!)
user_id = 45: hash(45) % 3 = 0 -> Shard 0
user_id = 45: hash(45) % 4 = 1 -> Shard 1 (ПРОБЛЕМА! Перемещён!)
Решение: Использовать consistent hashing:
from hashring import HashRing
ring = HashRing(nodes=["shard0", "shard1", "shard2", "shard3"])
shard = ring.get_node(user_id)
# При добавлении нового шарда: минимум данных перемещается
Когда использовать шардирование
Используй шардирование если:
- Данные > 500 GB
- TPS (транзакций в сек) > 10,000
- Одна БД физически не может справиться
- Геораспределённые данные
НЕ используй шардирование если:
- Данные < 100 GB
- Нужны частые JOIN-ы между таблицами
- Много операций без shard_key
- Новая система (слишком рано усложнять)
Совет System Analyst
- Выбери правильный shard key — это 80% успеха
- Каждый запрос ДОЛЖЕН включать shard_key в WHERE
- JOIN между шардами = смерть приложения — избегай
- Используй lookup tables для альтернативных фильтров
- Компенсирующие транзакции вместо распределённых
- Мониторь распределение данных — избегай горячих шардов
- Документируй стратегию шардирования — команда должна знать
Шардирование — это точка невозврата. После её пересечения разработка становится сложнее в 10 раз. Используй только когда действительно нужно.