Ах, горизонтальное масштабирование — это кулинарное искусство архитектуры баз данных! Подобно нарезке гигантской салями на управляемые кусочки (но с меньшим количеством чеснока), сегментирование помогает нам обслуживать данные быстрее, чем нью-йоркский пиццерийщик. Давайте наденем наши поварские колпаки и приготовим устойчивую реализацию сегментирования в Go!

Сегментированный шведский стол: выберите вкус раздела

Прежде чем мы запустим кодовую печь, давайте рассмотрим основные варианты подачи: Горизонтальное или вертикальное сегментирование

graph LR A[Монолитная база данных] --> B{Тип сегментирования} B --> C[Вертикальное: таблицы в виде слоёв] B --> D[Горизонтальное: строки в виде срезов] C --> E[Специализированные серверы баз данных] D --> F[Распределённые узлы данных]

В этом рецепте мы сосредоточимся на горизонтальной нарезке — потому что кому не нравятся равномерно распределённые кусочки данных?

Приготовление блюда с помощью Go: пошаговый рецепт

1. Выбор правильного ключа сегментации (секретный соус)

Ваш ключ сегментации подобен выбору идеального ножа — выбирайте неправильно, и вы получите беспорядочный хэш. Для нашей пользовательской базы данных мы будем использовать составной ключ UserID_CreateTime:

func ShardKey(userID string, createTime time.Time) string {
    return fmt.Sprintf("%s_%d", userID, createTime.UnixNano())
}

Это даёт нам временное распределение при сохранении локальности пользовательских данных.

2. Маршрутизатор сегментов (наш регулировщик трафика данных)

Давайте реализуем взвешенный маршрутизатор с согласованным хешированием, который обрабатывает добавление узлов плавнее, чем джазовый саксофонист:

type ShardRouter struct {
    sync.RWMutex
    ring       *consistent.Consistent
    nodeWeights map[string]int
}
func NewShardRouter(nodes []string) *ShardRouter {
    cr := consistent.New()
    for _, node := range nodes {
        cr.Add(node)
    }
    return &ShardRouter{
        ring:       cr,
        nodeWeights: make(map[string]int),
    }
}
func (sr *ShardRouter) GetShard(key string) (string, error) {
    sr.RLock()
    defer sr.RUnlock()
    return sr.ring.Get(key)
}

Этот малыш обрабатывает 50 тыс. поисков в секунду на ноутбуке моей бабушки 2008 года выпуска (проверено во время ужина в честь Дня благодарения).

3. Транзакции между сегментами (майонез дьявола)

Обработка транзакций между сегментами подобна управлению кошками — возможно, если дать им достаточно лакомств:

func DistributedTransaction(shards []string, execFunc func(ShardConn) error) error {
    var wg sync.WaitGroup
    errChan := make(chan error, len(shards))
    for _, s := range shards {
        wg.Add(1)
        go func(shard string) {
            defer wg.Done()
            conn, _ := GetShardConnection(shard)
            if err := execFunc(conn); err != nil {
                errChan <- err
            }
        }(s)
    }
    go func() {
        wg.Wait()
        close(errChan)
    }()
    return <-errChan
}

Совет от профессионала: добавьте логику повторных попыток, если вам не нравится играть в «ударь крота» с базой данных.

Тёмная сторона сегментирования: действуйте осторожно!

Раннее сегментирование подобно предложению руки и сердца на первом свидании — заманчиво, но часто приводит к катастрофе. Вот когда стоит подумать об этом:

СценарийНужно ли сегментировать?Лучшая альтернатива
100 RPSОптимизация индекса
10k RPS🤔Реплики чтения
Более 1 млн RPSИспользуйте сегментирование!

Мастерство миграции: перемещение данных без слёз

Наша поэтапная стратегия миграции (проверенная во время лунного затмения):

  1. Двойная запись в старые и новые сегменты.
  2. Постепенно переместите трафик чтения.
  3. Проверьте с помощью теневой записи.
  4. Удалите старое хранилище (с похоронами в стиле викингов).
func MigrateShard(oldShard, newShard string) error {
    // Пакетная обработка 1000 записей за раз
    return BatchProcess(oldShard, 1000, func(record Record) error {
        if err := newShard.Write(record); err != nil {
            return fmt.Errorf("миграция не удалась: %w", err)
        }
        oldShard.FlagAsMigrated(record.ID)
        return nil
    })
}

Грандиозный финал: сегментирование как профессионал

Помните, дети: сегментирование — это как соль. Слишком мало, и ваша система будет пресной, слишком много — и вы испортите блюдо. Начните с простого, всё измеряйте и масштабируйте по мере необходимости. А теперь вперёд и разбивайте эти базы данных, как лесоруб с бензопилой на фабрике зубочисток! Только не забудьте убрать журналы транзакций…