Database Sharding: масштабирование для highload проектов
Когда и как использовать шардирование базы данных
Когда ваше приложение вырастает до миллионов пользователей, вертикальное масштабирование упирается в потолок. Шардирование — это горизонтальное масштабирование базы данных, которое используют Instagram, Twitter, Uber. Что такое Sharding Шардирование — это разделение данных на несколько независимых баз данных (шардов). Каждый шард содержит подмножество данных. Пример: Shard 1: пользователи с ID 0-999,999 Shard 2: пользователи с ID 1,000,000-1,999,999 Shard 3: пользователи с ID 2,000,000-2,999,999 Когда нужен Sharding Признаки, что пора шардировать: База данных > 1 TB Запросы медленнее 100ms даже с индексами CPU/Memory сервера БД на пределе Вертикальное масштабирование слишком дорого Backup/restore занимает часы Альтернативы перед шардированием: Оптимизация запросов — индексы, query tuning Read replicas — разделение read/write нагрузки Caching — Redis, Memcached Вертикальное масштабирование — больше RAM/CPU Партиционирование — разделение таблиц внутри одной БД Стратегии Sharding 1. Hash-based Sharding shard_id = hash(user_id) % number_of_shards Плюсы: равномерное распределение Минусы: сложно добавлять шарды, нет locality 2. Range-based Sharding if user_id Плюсы: просто добавлять шарды, range queries эффективны Минусы: неравномерное распределение (hotspots) 3. Geography-based Sharding if user.country in ["US", "CA", "MX"]: shard = "americas" elif user.country in ["UK", "DE", "FR"]: shard = "europe" Плюсы: низкая latency, соответствие GDPR Минусы: неравномерное распределение 4. Directory-based Sharding Lookup table хранит mapping user_id → shard_id Плюсы: гибкость, легко ребалансировать Минусы: дополнительный lookup, SPOF Проблемы Sharding 1. Cross-shard Queries Запросы, затрагивающие несколько шардов, медленные: SELECT * FROM users WHERE email = 'john@example.com' Решение: денормализация, дублирование данных для поиска 2. Cross-shard Joins JOIN между таблицами на разных шардах невозможен. Решение: application-level joins, денормализация 3. Distributed Transactions ACID транзакции через шарды сложны. Решение: Saga pattern, eventual consistency 4. Rebalancing Добавление/удаление шардов требует миграции данных. Решение: consistent hashing, виртуальные шарды Архитектура Sharded системы Компоненты: Shard Key — поле для определения шарда (user_id) Shard Map — mapping ключей на шарды Query Router — направляет запросы на правильный шард Config Server — хранит метаданные о шардах Пример архитектуры: Application ↓ Query Router (определяет shard) ↓ Shard 1 Shard 2 Shard 3 (users 0-999k) (users 1M-2M) (users 2M-3M) Реализация в PostgreSQL 1. Через Foreign Data Wrappers CREATE EXTENSION postgres_fdw; CREATE SERVER shard1 FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host 'shard1.db.com', dbname 'users'); CREATE FOREIGN TABLE users_shard1 ( id bigint, name text ) SERVER shard1; 2. Через Citus (расширение PostgreSQL) CREATE EXTENSION citus; SELECT create_distributed_table('users', 'user_id'); -- Citus автоматически шардирует данные Реализация в приложении Node.js пример: class ShardManager { constructor(shards) { this.shards = shards; // Array of DB connections } getShardForUser(userId) { const shardIndex = userId % this.shards.length; return this.shards[shardIndex]; } async getUserById(userId) { const shard = this.getShardForUser(userId); return shard.query('SELECT * FROM users WHERE id = $1', [userId]); } async getUsersByIds(userIds) { // Group by shard const shardGroups = {}; userIds.forEach(id => { const shardIndex = id % this.shards.length; if (!shardGroups[shardIndex]) { shardGroups[shardIndex] = []; } shardGroups[shardIndex].push(id); }); // Query each shard in parallel const promises = Object.entries(shardGroups).map(([index, ids]) => { return this.shards[index].query( 'SELECT * FROM users WHERE id = ANY($1)', [ids] ); }); const results = await Promise.all(promises); return results.flat(); } } Consistent Hashing Решает проблему реб алансировки при добавлении шардов: // Виртуальные шарды const VIRTUAL_NODES = 150; class ConsistentHash { constructor(nodes) { this.ring = []; nodes.forEach(node => { for (let i = 0; i a.hash - b.hash); } getNode(key) { const hash = this.hash(key); const index = this.ring.findIndex(node => node.hash >= hash); return this.ring[index === -1 ? 0 : index].node; } } Результат: При добавлении нового шарда перемещается только 1/N данных, а не все. Реальные кейсы Instagram ID generation: timestamp + shard_id + auto_increment Shard key: user_id Тысячи PostgreSQL шардов Миллиарды записей Uber Geography-based sharding Каждый город — отдельный шард Низкая latency для локальных запросов Discord 1 trillion+ сообщений Sharding по guild_id (сервер) Cassandra для хранения Best Practices Выбирайте правильный shard key — равномерное распределение, минимум cross-shard queries Начинайте с нескольких шардов — легче добавить, чем убрать Используйте виртуальные шарды — для easier rebalancing Мониторинг per-shard — отслеживайте hotspots Автоматический failover — replicas для каждого шарда Backup strategy — независимые backup для каждого шарда Альтернативы NoSQL базы с автоматическим шардированием: MongoDB — автоматический sharding Cassandra — распределенная по дизайну DynamoDB — managed sharding от AWS CockroachDB — SQL с автоматическим sharding Метрики успеха После шардирования вы должны увидеть: Latency — снижение на 50-70% Throughput — рост в N раз (N = количество шардов) CPU usage — снижение per-shard Backup time — снижение (параллельный backup) Предупреждения Шардирование необратимо — очень сложно вернуться Complexity растет экспоненциально Требуется сильная DevOps команда Отладка становится сложнее Заключение: Шардирование — это мощный, но сложный инструмент. Не используйте его преждевременно. Исчерпайте все другие опции (индексы, кэширование, replicas) перед тем, как идти на этот шаг. Но когда время пришло, правильное шардирование позволяет масштабироваться до миллиардов пользователей. Планируйте заранее, выбирайте правильный shard key и инвестируйте в tooling.