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

Что такое шардирование в БД?

1.7 Middle🔥 151 комментариев
#Базы данных#Производительность и оптимизация

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

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

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

Шардирование в БД

Шардирование (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
}

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

Что такое шардирование в БД? | PrepBro