Ответ
Шардирование (или шардинг) — это техника горизонтального масштабирования баз данных или других хранилищ данных. Суть заключается в разделении одного большого набора данных на множество мелких частей, называемых шардами, и распределении этих шардов по разным серверам.
Основная цель — распределить нагрузку (как на чтение/запись, так и по объёму хранения) и повысить производительность и отказоустойчивость системы.
Основные компоненты и принципы:
- Ключ шардирования (Shard Key): Это один или несколько атрибутов данных, на основе которых решается, в какой шард попадут данные. Выбор ключа критически важен для равномерного распределения.
- Функция распределения: Алгоритм, который по ключу шардирования определяет номер или адрес нужного шарда. Чаще всего используется хеш-функция.
- Маршрутизатор запросов (Router): Компонент, который принимает запрос от приложения, определяет по ключу нужный шард и перенаправляет запрос на него.
Пример реализации: In-memory sharded map на Go
Этот пример показывает, как можно реализовать потокобезопасную map
с использованием шардирования для снижения конкуренции за мьютекс.
package main
import (
"hash/fnv"
"sync"
)
// Shard представляет один шард: мапу с собственным мьютексом.
type Shard struct {
sync.RWMutex
data map[string]interface{}
}
// ShardedMap - это массив шардов.
type ShardedMap struct {
shards []*Shard
}
// NewShardedMap создает новую шардированную мапу.
func NewShardedMap(shardCount int) *ShardedMap {
shards := make([]*Shard, shardCount)
for i := 0; i < shardCount; i++ {
shards[i] = &Shard{data: make(map[string]interface{})}
}
return &ShardedMap{shards: shards}
}
// getShardIndex определяет, в каком шарде должен находиться ключ.
func (sm *ShardedMap) getShardIndex(key string) int {
hasher := fnv.New32a()
hasher.Write([]byte(key))
// Используем оператор % для определения индекса шарда
return int(hasher.Sum32()) % len(sm.shards)
}
// Set устанавливает значение по ключу.
func (sm *ShardedMap) Set(key string, value interface{}) {
shardIndex := sm.getShardIndex(key)
shard := sm.shards[shardIndex]
shard.Lock() // Блокируем только один шард
defer shard.Unlock()
shard.data[key] = value
}
// Get получает значение по ключу.
func (sm *ShardedMap) Get(key string) (interface{}, bool) {
shardIndex := sm.getShardIndex(key)
shard := sm.shards[shardIndex]
shard.RLock() // Блокируем на чтение только один шард
defer shard.RUnlock()
val, ok := shard.data[key]
return val, ok
}
Важные моменты:
- Реальный мир: В реальных системах шардами выступают не структуры в памяти, а отдельные серверы баз данных (PostgreSQL, MongoDB, Redis и т.д.).
- Сложности: Шардирование вносит дополнительную сложность:
- Ребалансировка: При добавлении новых серверов нужно перераспределять данные.
- Транзакции: Сложно обеспечить ACID-транзакции, затрагивающие несколько шардов.
- Запросы без ключа: Запросы, которые не содержат ключ шардирования, должны быть отправлены на все шарды, что неэффективно.