Стандартная конфигурация кластера с шардированием происходит следующим образом:
- Устанавливается кластер шардов. Представим что это 2 репликасета(мастер реплика).
- Устанавливается прокси роутер на tarantool, на котором так же необходимо дописывать логику для походов в storage, который занимается тем что следит где какой бакет находится и ходить напрямую.
Тем самым мы избавляемся от дополнительной точки отказа, а так же уменьшаем число сетевых походов. Так же это позволяет адаптировать работу с шардами к комьюнити Golang. Увеличить поддержку, удобство отладки и производительность.
Цель и задача библиотеки заключаются в том, чтобы дать пользователям возможность обращаться в репликасеты не используя Tarantool Router инсталяцию, а резолвить бакеты и понимать в какой сторадж ходить прямо из вашего сервиса. При этом не создавая проблем с расхождением интерфейсов lua роутера и go роутера. В случае если термины выше вам незнакомы, мы надеемся что блок теории поможет вам в этом.
Tarantool — платформа in-memory вычислений с гибкой схемой данных для эффективного создания высоконагруженных приложений. Включает в себя базу данных и сервер приложений на Lua. Подробнее ознакомиться можно на официальном сайте Tarantool.
Подробнее про технологию виртуального шардирования а так же про библиотеку vhsard вы можете найти в https://www.youtube.com/watch?v=_9zUB0vmRxM А так же внутри оригинальной библиотеки vshard.
etcd — это быстрая, надёжная и устойчивая к сбоям key-value база данных. В Tarantool etcd является основным источником конфигурации production решений. Подробнее об использовании etcd в качестве источника конфигурации вы можете ознакомиться в библиотеке moonlibs config для версий tarantool ниже 3 (где используется etcd v2) и в официальной документации для Tarantool 3+ (где используется etcd v3).
Для работы с библиотекой вам понадобятся вам необходимо следующее окружение:
- Go: любая из двух последних мажорных версий releases.
- Сетевой доступ до шардированного кластера Tarantool.
- Доступность источника топологии(источником топологии может быть etcd, файл, или другие).
Для локального запуска кластера:
- Tarantool (библиотека проверена для версий 2.8+, документация написана поверх версии 2.11) в качетсве предустановленной программы на вашем ПК, в случае если вы собираетесь запускать кластер локально. Либо сетевое подключение до вашего кластера.
- tt - для установки зависимостей.
В качетсве примера конфигурации и использования рассмотрим расширенный пример на основе README.
package main
import (
"context"
"fmt"
"strconv"
"time"
"github.com/google/uuid"
"github.com/tarantool/go-tarantool/v2"
vshardrouter "github.com/tarantool/go-vshard-router/v2"
"github.com/tarantool/go-vshard-router/v2/providers/static"
)
func main() {
// Создаем фоновый контекст нашего сервиса контекст
ctx := context.Background()
// Создаем объект роутера. Он принимает 2 аргумента - контекст, а так же конфигурацию.
directRouter, err := vshardrouter.NewRouter(ctx, vshardrouter.Config{
// TopologyProvider - обязательный параметр.
// Провайдер топологии является одним из основных атрибутов.
// Существуют стандартные провайдеры топологии, описанные в README.md.
// Однако никто не мешает вам создать свой собственный, соблюдая интерфейс.
// В данном случае используется статический провайдер топологии,
// он принимает топологию в захардкоженном виде.
TopologyProvider: static.NewProvider(map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo{
// Название репликасета является идентификатором поэтому оно обязательно должно присуствовать.
{Name: "replcaset_1", UUID: uuid.New()}: {
// Название инстанса является идентификатором поэтому оно обязательно должно присуствовать.
{Addr: "127.0.0.1:1001", Name: "1_1"},
{Addr: "127.0.0.1:1002", Name: "1_2"},
},
{Name: "replcaset_2", UUID: uuid.New()}: {
{Addr: "127.0.0.1:2001", Name: "2_1"},
{Addr: "127.0.0.1:2002", Name: "2_2"},
},
}),
// TotalBucketCount - обязательный параметр.
// TotalBucketCount - указывает количество бакетов, которое было указано при бутстрапе кластера,
// либо, которое необходимо для дальнейшего бутстрапа кластера.
TotalBucketCount: 128000,
// DiscoveryTimeout - не обязетальный параметр. По дефолту 1 мин.
// Реиндексация расположения бакетов происходит фоновой горутиной.
// Время между работой этой горутины по крону называется DiscoveryTimeout.
DiscoveryTimeout: time.Minute,
// DiscoveryWorkStep - не обязательный параметр. По дефолту 10 мс.
// DiscoveryWorkStep это время интервала ожидания между запросами батча бакетов из репликасетов.
DiscoveryWorkStep: time.Millisecond * 10,
// DiscoveryMode - не обязательный параметр. По дефолту DiscoveryModeOn.
// DiscoveryMode - тип дискаверинга(процесса резолва бакетов).
// Существуют типы DiscoveryModeOn, DiscoveryModeOnce.
// DiscoveryModeOn - работает каждый промежуток DiscoveryTimeout.
// DiscoveryModeOnce - дискаверит бакеты только при старте.
DiscoveryMode: vshardrouter.DiscoveryModeOn,
// PoolOpts - не обязательный параметр. Дефолтные настройки можно найти в go-tarantool.
// PoolOpts - настройки пула подключения к репликасету.
// Они задаются для всех репликасетов одновременно.
PoolOpts: tarantool.Opts{
Timeout: time.Second,
},
// Loggerf - не обязательный параметр. По дефолту используется empty logger, который не пишет логирование.
// Loggerf - интерфейс которым вы можете обернуть свой логгер или предложить готовые обертки логгеров нам в PR.
Loggerf: vshardrouter.StdoutLoggerf{LogLevel: vshardrouter.StdoutLogDebug},
// Metrics - не обязательный параметр. По дефолту используется EmptyMetrics, который не пишет метрик.
// Metrics - интерфейс которым вы можете обернуть свою логику логирования в prometheus/graphite и другие системы.
Metrics: &vshardrouter.EmptyMetrics{},
// User - не обязательный параметр. Дефолтные настройки можно найти в go-tarantool pool.
// User - юзернейм для подключения tarantool pool.
User: "",
// Password - не обязательный параметр. Дефолтные настройки можно найти в go-tarantool pool.
// Password - пароль для подключения tarantool pool.
Password: "",
// BucketGetter - не обязательный параметр. По дефолту nil.
// BucketGetter - функиця логики sugar для удобства использования.
// Если вы не хотите задумываться на тему вычиления бакета при каждом запросе - вы можете написать middleware,
// в которой по определенному ключу в контексте писать bucket id внутрь контекста.
// Данная функция помогает sugar логики роутера понять bucket_id без вашего явного вызова.
// Вы можете не задумываться на тему бакета, а например подготовливать bucket_id из user_id в запросе, а
// в дальнейшем использовать роутер как go-tarantool pool.
BucketGetter: func(ctx context.Context) uint64 {
return 0
},
// RequestTimeout - не обязательный параметр. По дефолту 500ms.
// RequestTimeout - таймаут вызова go-vshard.
// Не стоит путать его с таймаутом вызова пула.
// Таймаут вызова через go-vshard применяется на время переадресации бакета из 1го репликасета в другой
// в случае миграции и других изменениях и проблем карты бакетов.
RequestTimeout: time.Minute,
})
// Обрабатываем ошибку в случае неправильного создания роутера
if err != nil {
panic(err)
}
// В данном случае приведен пример структуры, которую мы будем отправлять в tarantool.
// ID является ключом шардирования.
user := struct{ ID uint64 }{ID: 123}
// Вычисляем bucket_id из id пользователя.
bucketID := vshardrouter.BucketIDStrCRC32(strconv.FormatUint(user.ID, 10), directRouter.RouterBucketCount())
// Вызываем Balanced RO метод на получение некоторой информации.
resp, err := directRouter.CallBRO(
// Контекст необходим для обработки его закрытия и прекращения запроса.
// А также для того чтобы использовать ваш обогащенный логер.
ctx,
// BucketID - идентификатор бакета, который дает роутеру понять куда направлять запрос.
bucketID,
// Наименовение функции, которую мы будем вызывать.
"storage.api.get_user_info",
// Аргументы, которые мы передаем в tarantool.
[]interface{}{&struct {
BucketID uint64 `msgpack:"bucket_id" json:"bucket_id,omitempty"`
Body map[string]interface{} `msgpack:"body"`
}{
BucketID: bucketID,
Body: map[string]interface{}{
"user_id": "123456",
},
}},
// Таймаут обработки вызова go-vshard.
vshardrouter.CallOpts{Timeout: time.Second * 2},
)
// Обрабатываем ошибку, если она произошла.
if err != nil {
panic(err)
}
// Подготавливаем структуру для ответа.
info := &struct {
BirthDay int
}{}
// Распаршиваем данные в нашу структуру.
// Важно чтобы были использованы указатели, поскольку только в таком случае декодер сможет распрасить данные.
err = resp.GetTyped(&[]interface{}{info})
// Обрабатываем ошибку, если она произошла.
if err != nil {
panic(err)
}
// Так же мы можем получить ответ в виде слайса интефрейсов, в котором будет лежать ответ.
interfaceResult, err := resp.Get()
// Обрабатываем ошибку, если она произошла.
if err != nil {
panic(err)
}
// Логирование правильных ответов.
fmt.Printf("interface result: %v", interfaceResult)
fmt.Printf("get typed result: %v", info)
}
Провайдеры топологии - готовые реализации, обеспечивающие чтение состояния топологии вашего кластера из различных источников. Это позовляет вам считать вашу конфигурацию из любого удобного места. Если ни один из источников топологии вам не подходит - вы можете реализовать свой.
В качестве источника топологии tarantool, вам доступны следующие:
- etcd (для конфигурации основанной на moonlibs/config, подходит для tarantool версии ниже 3 и etcd v2)
- static (для случая, когда вам необходимо что-то быстро протестировать и в ручную в коде забить информацию о топологии в вашем кластере. Выше в примере мы рассмавтривали именно его).
- viper (дает широкие возможности использования любого источника топологии, даже тех, которые еще не поддержаны в самом tarantool. Так, любой источник конфигурации, который поддерживает viper вы можете использовать в качестве провайдера)
- etcd v3
- consul
- files
Мы стараемся максимально обеспечить совместимость с интерфейсами go-tarantool там, где это необходимо. В данном разделе мы расмотрим полезные примеры использования модулей go-tarantool, которые позволяет сделать нам совместимость интерфейсов.
Модуль go-tarantool, позволяющий получать доступ к builtin методам Tarantool, не описывая интерфейсов самому.
Проверка состояния кластера.
// Проверим что всем мастера всех репликасетов находятся в состоянии RO=false.
// Проходимся по всем репликасетам внутри кластера.
for _, rs := range router.RouteAll() {
// Поскольку модуль box рабоатет в рамках подключения к 1му инстансу,
// мы можем использовать адаптер, чтобы указать пулу использовать 1 инстанс репликасета в RW.
// В этом поможет нам адаптер go-tarantool.
adapter := pool.NewConnectorAdapter(rs.Pooler(), pool.RW)
// Создадим экземпляр box из go-tarantool.
b := box.New(adapter)
// Получим информацию из нашего инстанса.
info, err := b.Info()
if err != nil {
panic(err)
}
// Проверим, что мастер действительно находится в состоянии RO=false.
if info.RO == false {
panic("master non master 0_0")
}
}