Что такое шардирование в БД?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Шардирование в БД
Шардирование (sharding) — это техника горизонтального масштабирования базы данных, при которой данные распределяются между несколькими узлами (шардами) на основе некоторого ключа. Это позволяет системе обрабатывать большие объёмы данных и трафика.
Основной принцип
Вместо того чтобы хранить всю базу данных на одном сервере, данные разбиваются на подмножества и распределяются среди нескольких серверов.
┌──────────────────────────────────────────────────┐
│ Одноузловая БД │
│ User 1, User 2, User 3, ... User 1,000,000 │
└──────────────────────────────────────────────────┘
↓ (проблема: не масштабируется)
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Shard 1 │ │ Shard 2 │ │ Shard 3 │
│ User 1-333k │ │ User 333k- │ │ User 666k- │
│ │ │ 666k │ │ 1M │
└─────────────┘ └─────────────┘ └─────────────┘
Стратегии шардирования
1. Range-based Sharding Данные делятся по диапазонам значений.
func getShardByID(userID int64, numShards int) int {
rangeSize := 1000000 / numShards
return int(userID / int64(rangeSize))
}
func main() {
// User ID 500,000 → Shard 0
fmt.Println(getShardByID(500000, 3)) // 1
// User ID 800,000 → Shard 2
fmt.Println(getShardByID(800000, 3)) // 2
}
Проблема: неравномерное распределение (Shard 1-333k может быть переполнена, если пользователи приходят неравномерно)
2. Hash-based Sharding Хеш ключа определяет шард.
import "hash/fnv"
func getShardByHash(userEmail string, numShards int) int {
h := fnv.New32a()
h.Write([]byte(userEmail))
return int(h.Sum32() % uint32(numShards))
}
func main() {
shard1 := getShardByHash("user1@example.com", 3)
shard2 := getShardByHash("user2@example.com", 3)
fmt.Println(shard1, shard2) // Равномерное распределение
}
Преимущества: хорошее распределение, простое вычисление Недостатки: при добавлении шардов нужна переалокация данных
3. Directory-based Sharding Таблица-справочник хранит маппинг ключа к шарду.
type ShardDirectory struct {
mu sync.RWMutex
mapping map[string]int // userID → shardID
}
func (sd *ShardDirectory) GetShard(userID string) int {
sd.mu.RLock()
defer sd.mu.RUnlock()
if shard, ok := sd.mapping[userID]; ok {
return shard
}
return -1 // Не найден
}
func (sd *ShardDirectory) SetShard(userID string, shardID int) {
sd.mu.Lock()
defer sd.mu.Unlock()
sd.mapping[userID] = shardID
}
Преимущества: гибкость, легко переносить данные Недостатки: требует доступа к справочнику (может быть bottleneck)
Ключевые проблемы шардирования
1. Распределённые транзакции Если данные для транзакции находятся на разных шардах, это усложняет ACID гарантии.
// Проблема: товар на Shard 1, заказ на Shard 2
func CreateOrderWithItem(userID string, itemID string) error {
shardUser := getShardByHash(userID, 3)
shardItem := getShardByHash(itemID, 3)
if shardUser != shardItem {
// Нужна распределённая транзакция или компенсирующая транзакция
// Очень сложно!
}
// Решение: денормализовать или использовать two-phase commit
return nil
}
2. Hot shards (горячие шарды) Если распределение неравномерное, некоторые шарды будут перегружены.
Пример: все сторонние пользователи на одном шарде
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ Shard 1 │ │ Shard 2 │ │ Shard 3 │
│ 100k QPS │ │ 100k QPS │ │ 1M QPS ⚠️ │
└──────────┘ └──────────┘ └──────────────┘
3. Перешардирование (resharding) При добавлении нового шарда нужно перераспределить данные.
// Старое распределение: hash % 3
// Новое распределение: hash % 4
// Нужно перепереместить ~25% данных между шардами
// Это дорого и сложно!
func reshardDataWithMinimalMovement() {
// Используем Consistent Hashing для минимизации перемещений
// или используем Directory-based для контроля над маппингом
}
Когда использовать шардирование?
✅ Используй, если:
- База данных больше 1TB
- QPS превышает возможности одного сервера
- Данные естественно делятся (по user_id, tenant_id)
- Можешь терпеть дополнительную сложность
❌ Не используй, если:
- Данные умещаются на одном сервере (реплики лучше)
- Часто нужны cross-shard запросы
- Транзакции охватывают несколько шардов
Примеры в Go
type ShardedDB struct {
shards []*sql.DB
}
func (s *ShardedDB) GetUser(userID string) (*User, error) {
shardID := getShardByHash(userID, len(s.shards))
db := s.shards[shardID]
var user User
err := db.QueryRow(
"SELECT id, name FROM users WHERE id = ?",
userID,
).Scan(&user.ID, &user.Name)
return &user, err
}
func (s *ShardedDB) InsertUser(user *User) error {
shardID := getShardByHash(user.ID, len(s.shards))
db := s.shards[shardID]
_, err := db.Exec(
"INSERT INTO users (id, name) VALUES (?, ?)",
user.ID, user.Name,
)
return err
}
Шардирование — это мощный инструмент для масштабирования, но требует тщательного проектирования и планирования. Выбирай стратегию в зависимости от особенностей данных и нагрузки.