golang

Как мы тестировали Tarantool Database на 640 инстансов

  • пятница, 26 июня 2026 г. в 00:00:14
https://habr.com/ru/companies/vktech/articles/1051316/

Привет, Хабр! Меня зовут Андрей Орлов, я QA‑инженер в команде Tarantool Database, VK Tech. Я занимаюсь функциональным тестированием: проверяю новые фичи и изменения, поддерживаю и развиваю автотесты, разбираю инциденты, анализирую логи и метрики. Нагрузочное тестирование и стресс‑тестирование тоже входит в мои задачи — в том числе для проверки поведения Tarantool Database на больших конфигурациях. В этой статье я расскажу, как мы организовали и провели тестирование Tarantool Database на 640 инстансах, какие подходы и инструменты использовали и какие выводы сделали.

Введение

Когда речь заходит о горизонтальном масштабировании баз данных, чаще всего обсуждают кластеры на 10–50 инстансов. В Tarantool, конечно, встречаются и существенно более крупные установки — и на этапах pre-sale мы регулярно проверяем подобные конфигурации. Но в этот раз нам нужно было системно протестировать кластер на сотнях инстансов, собрать цифры и зафиксировать результаты.

Нам нужен был кластер на 640 инстансов — 128 роутеров и 128 репликасетов с фактором репликации 4. Это не учебный пример и не эксперимент ради эксперимента: реальный проект требовал хранения десятков терабайт данных с низким latency.

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

Статья будет полезна тем, кто планирует масштабные развертывания Tarantool Database, интересуется нагрузочным тестированием или хочет узнать, как ведет себя база данных на нестандартных конфигурациях.

Статья написана по результатам тестирования в августе 2025 года. Опыт может различаться в зависимости от версии, конфигурации и условий эксплуатации.

1. Зачем нам такой масштаб

Мы сосредоточились на трех ключевых вопросах:

  1. Как поведет себя кластер? Вдруг что-то сломается на таком количестве инстансов? Discovery, шардирование, репликация — обычно для тестов мы используем меньшее количество инстансов.

  2. Какой максимум RPS можно выдавить? Нужно было понять предельные возможности такой конфигурации: где потолок, подтвердить расчеты сайзингов.

  3. Найти баги и узкие места Tarantool Database. Где узкое место: в самом Tarantool, в конфигурации, в сети? Какие ошибки всплывут при таком масштабировании?

Это была разведка боем — понять, с чем мы имеем дело, прежде чем идти в продакшен.

2. Конфигурация кластера

Схема развертывания

Классическая архитектура TDB: роутеры принимают запросы от приложения, стораджи хранят данные.

Роутеры — stateless-узлы. Каждый знает топологию кластера и маршрутизирует запросы к нужному сторадж-узлу на основе bucket_id и информации о лидере репликационной группы. Роутеров 128 — чтобы обеспечить достаточную пропускную способность и отказоустойчивость; потеря нескольких роутеров не должна влиять на доступность.

Стораджи — хранят данные. 128 репликасетов, каждый из 4 инстансов:

  • 1 master — принимает записи;

  • 3 replica — обслуживают чтение, готовы стать мастером при падении.

Бакетов — 30 000. Это единицы шардирования в vshard (библиотеке автоматического шардирования Tarantool Database): каждый bucket принадлежит ровно одному репликасету. При 128 репликасетах каждый хранит ~234 бакета. Принцип выбора: слишком мало бакетов — сложно перераспределять при добавлении новых нод, слишком много — накладные расходы на управление.

Схема данных

Для тестирования использовали два спейса с одинаковой минималистичной схемой, приближенной к key-value нагрузке, типичной для Tarantool Database. Спейсы test_async и test_sync соответственно для асинхронной и синхронной репликации.

local space = box.schema.space.create('test_async', { if_not_exists = true })

space:format({
{ name = 'id',    type = 'unsigned'               },
{ name = 'bucket_id', type = 'unsigned', is_nullable = true },
{ name = 'value', type = 'number'                 },
})

space:create_index('primary', {
parts = {'id'},
if_not_exists = true },
})

space:create_index('bucket_id', {
parts = {'bucket_id'} },
unique  = false,
if_not_exists = true,
})

tt_helpers.register_sharding_key(space.name, {'id'})

Поле bucket_id — обязательное требование crud-модуля (высокоуровневый API для работы с шардированными данными в Tarantool Database): он использует его для маршрутизации внутри репликасета. При вставке можно передать nil — тогда crud вычислит bucket_id автоматически из ключа.

Параметр

Значение

Спейсы

test_async, test_sync

Индексы

Первичный по id, вторичный по bucket_id

Ключ (id)

unsigned, 8 байт

Значение (value)

number ~1–9 байт

Размер одной записи

~20 байт (включая заголовки tuple)

Данные на пике

~463 GB (90% memtx — in-memory движка Tarantool Database)

Железо

Ресурсы планировали по документации Tarantool Database: на каждый инстанс — 2 ядра CPU. При 640 инстансах получалось 1280 ядер, распределенных на 32 виртуальные машины. На каждый компонент инфраструктуры — отдельная машина.

Серверы TDB (инстансы кластера) — 32 VM:

  • CPU: Intel Ice Lake (Intel Xeon Gen3) 40 vCPU;

  • RAM: 79 GB (80920 MB);

  • Disk: 100 GB SSD;

  • OS: Astra Linux 1.8.x.

Итого: 32 × 40 = 1280 vCPU. Ровно по 2 ядра на каждый из 640 инстансов. На каждой VM размещалось ~20 инстансов Tarantool Database.

Серверы нагрузки — 2 VM:

  • CPU: Intel Ice Lake (Intel Xeon Gen3) 64 vCPU;

  • RAM: 128 GB;

  • Disk: 100 GB SSD;

  • OS: Ubuntu 22.04.

Инфраструктура:

Компонент

CPU

RAM

Disk

OS

TCM (Tarantool Cluster Manager)

8 vCPU (Cascade Lake)

8 GB

20 GB SSD

Astra Linux 1.8.x

Grafana + Prometheus + Loki

8 vCPU (Ice Lake)

8 GB

20 GB SSD

Astra Linux 1.8.x

ATE (Ansible Tarantool Enterprise)

8 vCPU (Ice Lake)

8 GB

20 GB SSD

Astra Linux 1.8.x

etcd

2 vCPU (Cascade Lake)

4 GB

10 GB SSD

Astra Linux 1.8.x

Отдельно стоит отметить скромные ресурсы для etcd — всего 2 vCPU и 4 GB RAM. Как выяснится позже, этого оказалось недостаточно.

Сетевые параметры и потребление ресурсов

Облачная инфраструктура не дает выделенных сетевых интерфейсов, поэтому все 32 VM кластера делят общую сетевую фабрику облака. Для всех машин кластера подключили MultiQueue, чтобы снизить накладные расходы на обработку сетевого трафика на хостах и уменьшить влияние сетевого стека на результаты.

Параметр

Значение

Сеть между VM

Виртуальная, латентность ~0,2–0,5 мс между VM одного пула

Инстансов на VM

~20 (на одной сетевой карте)

Полоса на VM

До 10 Гбит/с (burst)

Как разворачивали

Развертывание 640 инстансов — типичная задача для инсталлятора Ansible Tarantool Enterprise. ATE генерирует inventory на лету из описания топологии кластера. Вместо статического файла с 640 хостами — декларативное описание (примерный вид инвентаря):

# hosts.yaml

plugin: tarantool.enterprise.generator
cluster_name: tarantool
product: tarantool
 
constants:
  tarantool_config_etcd_endpoints:
	- http://etcd-1:2379
	- http://etcd-2:2379
	- http://etcd-3:2379
 
servers:
  - name: vm_1
	host: 10.0.0.11
	advertise_host: 10.0.0.11
	port: 22
	user: ubuntu
	port_starts:
  	iproto: 3301
  	http: 8081
 
  - name: vm_2
	host: 10.0.0.12
	advertise_host: 10.0.0.12
	port: 22
	user: ubuntu
	port_starts:
  	iproto: 3401
  	http: 8082
 
  - name: vm_3
	host: 10.0.0.13
	advertise_host: 10.0.0.13
	port: 22
	user: ubuntu
	port_starts:
  	iproto: 3501
  	http: 8083
 
components:
  - name: storage
	replicasets: 1
	replicas: 2
	config:
  	group:
    	app:
      	module: storage
    	sharding:
      	roles: [storage]
 
 - name: router
	replicasets: 1
	replicas: 1
	config:
  	group:
    	app:
      	module: router
    	sharding:
      	roles: [router]

ATE превращает это в полный inventory для Ansible: каждый инстанс получает уникальный URI, http-порт, bind-адрес. Порты и адреса вычисляются автоматически — нумерация по порядку, смещение от базового порта. Запуск процесса развертывания:

docker run --network host -it --rm \
	-v ${PATH_TO_PRIVATE_KEY}:/ansible/.ssh/id_private_key:Z \
	-v ${PATH_TO_INVENTORY}:/ansible/inventories/hosts.yml:Z \
	-v ${PATH_TO_PACKAGE}/${PACKAGE_NAME}:/ansible/packages/${PACKAGE_NAME}:Z \
	-e SUPER_USER_NAME=${SUPER_USER_NAME} \
	-e PACKAGE_NAME=${PACKAGE_NAME} \
	ansible-tarantool-enterprise:${DEPLOY_TOOL_VERSION_TAG} \
	ansible-playbook -i /ansible/inventories/hosts.yml \
	--extra-vars '{
    	"cartridge_package_path":"/ansible/packages/'${PACKAGE_NAME}'",
    	"ansible_ssh_private_key_file":"/ansible/.ssh/id_private_key",
    	"super_user":"'${SUPER_USER_NAME}'"
	}' \
	playbooks/install_3_0.yml

Процесс bootstrap

После запуска всех 640 инстансов кластер нужно проинициализировать.

Bootstrap в Tarantool Database — это процесс инициализации нового экземпляра: создания системных пространств (_vspace, index, user и др.) и заполнения их начальными данными. Для репликасета bootstrap означает выбор лидера начальной загрузки (bootstrap leader), который инициализирует схему кластера, — остальные инстансы присоединяются, получая снимок от него.

Управляли процессом через TCM (Tarantool Cluster Manager) — веб-интерфейс для управления кластером. TCM сам ничего не делает, он отправляет команды инстансам.

На пустом кластере bootstrap свелся к одному действию в UI TCM:

  1. Навести на три точки в интерфейсе.

  2. Нажать «Bootstrap vshard».

Процесс прошел за несколько секунд — TCM отправил команды на распределение 30 000 бакетов по репликасетам, роутеры получили карту маршрутизации. Кластер готов к работе.

Настройка мониторинга

Без мониторинга такой кластер — черный ящик. Prometheus собирал метрики со всех 640 инстансов каждые 15 секунд:

# prometheus/prometheus.yml

global:
  scrape_interval: 15s
 
scrape_configs:
  - job_name: 'tarantool'
	file_sd_configs:
  	- files:
      	- '/etc/prometheus/targets/*.json'
    	refresh_interval: 30s

Target'ы генерировались динамически из inventory — при таком количестве инстансов статический список был бы кошмаром.

Grafana — дашборд Tarantool Database, дашборд Node Exporter и кастомный дашборд K6-tarantool (bencher) с ключевыми метриками:

  • Cluster health — сколько инстансов живы, есть ли мастера во всех репликасетах;

  • RPS — запросы в секунду, разбивка по типам операций;

  • Latency — p50, p95, p99 по операциям;

  • Количество ошибок при выполнении запросов;

  • Resources — CPU, memory, network, disk I/O.

Мониторинг развернули параллельно с кластером. К моменту первых тестов уже видели всю картину.

3. Проверка работоспособности

После bootstrap — обязательная проверка, что все работает. Три этапа: визуальный контроль через TCM, метрики в Grafana, тестовая нагрузка через K6-tarantool.

Через TCM

Tarantool Cluster Manager отображает топологию кластера в виде интерактивной схемы: каждый репликасет, каждый инстанс внутри, статус и роль.

Что проверяли:

  • Все ли инстансы видны — 640 из 640.

  • Статус каждого инстанса — «зеленый», здоров.

  • Лидеры в репликасетах — во всех 128 репликасетах должен быть лидер.

  • Buckets — все 30 000 активны.

Топология кластера в TCM: 128 репликасетов, все 640 инстансов в статусе «Здоров»:

Через мониторинг в Grafana

Cluster Overview Dashboard подтвердил картину из TCM: все 640 инстансов на запущены и здоровы. Дополнительно проверили потребление CPU/Memory в idle и сетевой трафик — аномалий нет.

Дашборд Grafana Cluster Overview: кластер в idle, все ключевые метрики в норме:

Тестовая нагрузка через K6-tarantool

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

export const options = {
scenarios: {
    constant_load: {
        executor: 'constant-vus', 
            vus: VUS_COUNT, 
            duration: DURATION, 
        } 
    }, 
    thresholds: {
    checks: [{threshold: 'rate==1', abortOnFail: true }], 
    }, 
    summaryTrendStats: ['min', 'med', 'avg', 'p(95)', 'p(99)', 'p(99.99)', 'max', 'count'], 
}

export default function () { 
    const tuple = [ 
        exec.scenario.iterationInTest, 
        null, 
        hlp.random(100000000), 
    ]; 

    let response = hlp.tarantoolCallSimple('crud.replace', [NAME_SPACE, tuple], { noreturn: true, noconvert: false }); 
    check(response, {'replace requests - status was OK': (r) => r.err === '' }); 
}

Параметры теста:

  • VUs: 100, Duration: 30 сек.;

  • Операция: replace.

Результат: Все операции: success 100%.

Данные записываются. Ошибок нет. Кластер готов к нагрузочным тестам.

4. Первые бенчмарки: разочарование

Раунд 1: одна нагрузочная машина

Инструмент: K6 с расширением x-tarantool (K6-tarantool).

Сценарий: тестировали только операцию replace — простую запись с перезаписью по ключу.

Нагрузочная машина: CPU: 64 vCPU, RAM: 128 GB

Методология

Запускали K6-tarantool, подбирая оптимальное количество VUs. Ожидали, что больше VUs = больше RPS, но реальность оказалась сложнее.

Результаты

Начали с 1500 VUs:

VUs

RPS

Примечание

1500

~120 000

Стартовая точка

2000

~95 000

RPS упал!

1300

~133 000

Начали снижать VUs

1200

~156 000

1000

163–176k

900

~176 000

700

183–186k

500

~190 000

400–450

меньше

Перестало расти

550

~193 000

Максимум

Возник парадокс: меньше VUs — больше RPS. После 1000 VUs отдача падала, latency росла. Оптимум — около 550 VUs.

Поиск бутылочного горлышка

CPU на инстансах Tarantool Database

  • Роутеры: 40–50%;

  • Стораджи: 50–70%.

CPU на машине-нагружателе (K6-tarantool): CPU: 70–90%

Вывод: Клиентское железо стало узким местом — кластер недогружен, надо добавить еще одну машину-нагружатор.

Раунд 2: две машины, разделение роутеров

Добавили вторую нагрузочную машину с теми же характеристиками (64 vCPU, 128 GB). Каждая коннектится к своим 64 роутерам без пересечений. Машина 1 → роутеры 1–64, машина 2 → роутеры 65–128.

CPU на инстансах Tarantool Database:

  • Роутеры: 60–70%;

  • Стораджи: 80–90%.

Результат: максимум ~300k RPS. Увеличение числа потоков результата не дало. Стораджи до сих пор недозагружены примерно наполовину, так как в их распоряжении 2 ядра — максимум это 200%.

Проблема с InfluxDB

Использовали кастомный дашборд K6-tarantool с InfluxDB для визуализации в реальном времени. На большом потоке метрик InfluxDB начал захлебываться. Экспериментировали с K6_INFLUXDB_PUSH_INTERVAL и K6_INFLUXDB_CONCURRENT_WRITES — в целом помогло добиться стабильности без потери данных.

Раунд 3: убираем connection_pool

Попробовали убрать connection_pool из нагружатора K6-tarantool и подключаться к роутерам напрямую. Распределение сохранили: каждая машина грузит свои 64 роутера.

Результаты:

Операция

VUs (сумма)

RPS

replace

700

~350 000

get

700

~500 000

Кардинально ничего не поменялось:

  • CPU роутеры: 60%;

  • CPU стораджи (мастера): 90%;

  • CPU нагружаторы: ~60%.

Больше 350k RPS на запись выдавить не удалось — это мало для 640 инстансов.

Промежуточный итог

K6-tarantool упирается в ограничения архитектуры. На запись получили ~350k RPS, на чтение ~500k RPS. Кластер загружен не полностью. Нужен собственный бенчер.

5. Собственный бенчер на Go

Мы выбрали Go по нескольким причинам:

  • Горутины — легковесные потоки, можно создавать десятки тысяч без значительных накладных расходов.

  • Эффективный runtime — GC оптимизирован для высокого concurrency.

  • go-tarantool — нативный клиент с поддержкой пулов соединений и crud-модуля.

Архитектура бенчера

Бенчер состоит из трех частей: пула соединений ко всем роутерам, N горутин-воркеров и сборщика метрик.

Запросы идут не через raw tarantool-операции (box.space:replace), а через Tarantool crud-модуль. Это принципиально для шардированного кластера: crud сам вычисляет bucket_id из ключа и маршрутизирует запрос на нужный репликасер-роутер прозрачно. В go-tarantool для этого есть отдельный пакет crud.

1. Сборка пула соединений

instances := make([]pool.Instance, 0, len(cfg.Endpoints))
for _, addr := range cfg.Endpoints {
instances = append(instances, pool.Instance{
    Name: addr,
    Dialer: tarantool.NetDialer{
        Address:  addr,
        User: cfg.User,
        Password: cfg.Pass,
    },
})
}
conn, err := pool.Connect(ctx, instances)

pool.Connect поднимает соединение к каждому из 128 роутеров. При вызове conn.Do(req, pool.ANY) запрос уходит на любой доступный роутер — балансировка на стороне клиента.

2. Горутины-воркеры

for i := 0; i < cfg.Vus; i++ {
wg.Add(1)
go func() {
    defer wg.Done()
    reqMaker, _ := NewRequestMaker(cfg.Method, cfg.SpaceName, opts)
    for {
        select {
        case <-ctx.Done():
            return
        default:
            req  := reqMaker.Make()       // crud.MakeReplaceRequest / crud.MakeGetRequest / ...
            st   := time.Now()
            res, err := conn.Do(req, pool.ANY).Get()
            metrics.Observe(cfg.Method, st, err != nil)
        }
    }
}()
}
wg.Wait()

reqMaker.Make() возвращает нужный crud-запрос в зависимости от метода теста. Например, для replace:

req = crud.MakeReplaceRequest(spaceName).
	Tuple(tuple).       	// [id, nil, value] — nil означает "вычисли bucket_id сам"
	Opts(crud.ReplaceOpts{
    	Noreturn: crud.MakeOptBool(true),   // не возвращать tuple — экономим трафик
    	Timeout:  crud.MakeOptFloat64(10),
	})

3. Шаблоны данных

Кортежи генерируются по шаблону, заданному в конфиге:

iter,bucket_id,randString(128)
  • iter — инкрементный счетчик (поле id)

  • bucket_id — передается как nil, crud вычисляет из id автоматически

  • randString(128) — случайная строка 128 байт (поле value)

4. Метрики

Бенчер экспонирует Prometheus-метрики на :2112 в реальном времени — RPS, error rate, latency-гистограмма. Для точных перцентилей использует HdrHistogram (не обычный histogram): p50, p95, p99, p99.9 без потери точности на хвостах.

Конфиг для нашего теста:

vus: 18000
duration: "60s"
endpoints: "router-01:3301,router-02:3301,...,router-128:3301"
space: "test_async"
method: replace
tuple: "iter,bucket_id,randString(128)"
max_rps: 700000
metricsAddr: ":2112"
get:
  mode: write
  balance: true

Пример вывода после теста:

Results:
duration: 60.12 seconds
Requests:
RPS: 633142.00
total requests: 38064234
errors count: 0
Latency:
min:0.196 ms
p50:3.800 ms
p95:18.000 ms
p99:28.000 ms
p99.9:  80.000 ms
max:201.000 ms
mean:   28.280 ms

Результаты

С одной нагрузочной машины: CPU нагружателя: ~30–40%

Нагрузка на кластер: CPU стораджи (мастера): ~160–170%

Наконец-то кластер работает практически на пределе.

Подбор оптимального количества VUs

VUs

RPS

Latency p99

Примечание

10 000

580 000

8 мс

15 000

620 000

12 мс

18 000

~650 000

15 мс

Оптимум

20 000

650 000

25 мс

RPS не растет, latency↑

25 000

648 000

40 мс

Только хуже

18 000 VUs — точка, после которой RPS не увеличивается, а только растет latency. Классический признак перегрузки: запросы встают в очередь, но не обрабатываются быстрее.

Grafana: RPS ~650 000 на replace, загрузка CPU стораджей превышает 160%:

Финальные результаты

Replace (запись):

Режим репликации

RPS (пик)

RPS (среднее)

Асинхронный

650 000

633 000

Синхронный

490 000

461 000

Синхронный режим репликации ждет подтверждения кворума реплик, поэтому он надежнее. Цена этого — производительность: в наших замерах он оказался примерно на 30% медленнее.

Get (чтение):

Режим репликации

RPS

Асинхронный

~1 000 000

Синхронный

~1 000 000

На чтении разницы между режимами почти нет — роутеры и стораджи успевают обрабатывать запросы без очередей.

Grafana: RPS ~1 000 000 на get-операциях:

Итог: собственный бенчер на Go позволил выжать из кластера ~1 млн RPS на чтение и ~650k RPS на запись — в 2 раза больше, чем с K6-tarantool.

Сводная таблица производительности

Пояснение параметров

Для get-запросов TDB позволяет управлять маршрутизацией:

mode=write — запрос идет на master (как при записи). mode=read — запрос идет на ближайшую реплику. mode=read дает чуть меньший RPS (~913k против ~996k), но снижает медианную latency (7,49 мс против 11,03 мс) — реплики менее загружены, чем мастер.

balance=true — равномерно распределяет запросы по репликам. balance=false — всегда идет на один узел. Для get-запросов разница минимальна, так как узкое место — роутер, а не сторадж.

Финальные результаты тестирования на максимальный RPS для всех операций

Главные цифры: get-операции уверенно держат ~1 млн RPS независимо от режима — ключевой показатель для read-доминантных нагрузок. На запись replace async дает 633k RPS — практический потолок кластера в текущей конфигурации. Обратите внимание на разлет между median и average latency: чем больше разрыв, тем сильнее влияние редких, но медленных запросов (подробнее об этом — после таблицы).

Метод

RPS

Latency med, мс

Latency avg, мс

get async (mode=write, balance=false)

984 558

11,22

18,23

get async (mode=write, balance=true)

996 447

11,03

18,00

get async (mode=read, balance=true)

913 078

7,49

19,66

get sync (mode=write, balance=true)

999 851

11,23

17,96

get sync (mode=write, balance=false)

994 999

11,76

18,03

get sync (mode=read, balance=true)

922 145

8,60

19,46

replace async

633 610

3,80

28,28

replace sync

461 855

4,62

38,90

insert async

584 537

3,55

30,72

insert sync

498 821

5,52

30,02

upsert async

367 228

1,29

48,90

upsert sync

420 303

4,22

35,61

update async

338 164

1,18

44,22

update sync

378 137

3,68

39,60

Примечание по upsert и update: для этих операций sync показал RPS выше, чем async, — картина, противоположная replace и insert. Это следствие облачной вариативности: тесты upsert/update проводились в другие временные окна, и колебания производительности виртуальных машин повлияли на соотношение sync/async. Основной фокус был на get и replace, повторных прогонов для upsert/update не делали — здесь важен порядок величины, а не точное соотношение sync/async.

Разница между median и average latency — не баг, а особенность распределенных систем. Значительный разрыв (например, у upsert async: 1,29 мс медиана против 48,90 мс среднее) означает, что основная масса запросов отрабатывает быстро, но есть редкие выбросы. В облачной среде это нормально, но при планировании SLA ориентироваться стоит на median (p50) и перцентили (p95, p99), а не на среднее.

Наш стенд не был идеальным, как и облачное окружение, и бенчер, который мы написали в сжатые сроки. При повторении одних и тех же тестов в разные моменты времени мы получали заметный разброс результатов, поэтому абсолютные цифры стоит воспринимать как ориентир. Чтобы расширить картину, можно заглянуть в ещё один кейс: наш коллега Олег Жуковец публиковал статью про то, как выжать 5 млн RPS с одного инстанса Tarantool на локальной машине.

6. Исследование: роутеры vs стораджи

После достижения 1 млн RPS возник вопрос: оптимальна ли конфигурация? 128 роутеров и 128 репликасетов — сбалансировано ли это?

Методология

Тестировали асинхронную репликацию на этом же кластере, отправляя  get-запросы с фиксированным bucket_id, тем самым направляя весь поток на один конкретный storage (один репликасет):

  1. Варьировали количество роутеров на один сторадж.

  2. Нашли оптимальное соотношение.

  3. Увеличивали количество стораджей при найденном соотношении.

Результаты

Соотношение роутеров к стораджам:

Конфигурация

RPS

7 роутеров : 1 сторадж

70 000

128 роутеров : 1 сторадж

75 000

Соотношение 7:1 практически достигает максимума. Дальнейшее увеличение роутеров на один сторадж не дает значимого прироста — сторадж уже работает на пределе, роутеры не являются узким местом.

Масштабирование стораджей (при соотношении 7:1):

Количество стораджей

RPS

Примечание

1

70 000

Базовая точка

4

280 000

Линейный рост

8

560 000

Линейный рост

12

753 000

Линейный рост

18+

не тестировали

Рост идет практически линейно вплоть до 12 стораджей. Далее рост замедляется. Предварительная гипотеза — по причине роста сетевых накладных расходов, но этот вопрос требует дополнительного исследования.

Выводы по топологии

  1. Роутер — узкое место. Один роутер не может нагрузить сторадж на полную мощность. Требуется 7 роутеров на 1 сторадж для максимальной производительности.

  2. Линейное масштабирование до 12+ стораджей. Можно планировать ресурсы с достаточной точностью.

  3. Конфигурация 128:128 — соотношение 1:1, что ниже найденного оптимума 7:1. При равномерном распределении нагрузки каждый мастер в репликасете работал практически на пределе, что может указывать на рост накладных расходов при увеличении числа стораджей и при большом факторе репликации — в первую очередь за счёт межузлового трафика и репликационных операций, что требует дополнительных исследований.

7. Выводы и рекомендации

Что показали тесты

1.     TDB работает на 640 инстансах. Кластер был развернут и стабильно работал. Discovery, шардирование, репликация — все функционирует. Нам было важно испытать Tarantool Database в таком масштабе.

2.     Производительность ~1 млн RPS на чтение, ~650k RPS на запись при асинхронной репликации. Синхронная репликация в нашем случае снижает производительность записи на ~27% (до 461k RPS).

3.     Линейное масштабирование. Производительность растет линейно при добавлении стораджей (до определенного предела).

4.     TDB — это комплексный продукт. Мы тестировали не только саму базу данных, но и TCM — встроенный инструмент управления и наблюдаемости, который входит в состав TDB. При реальной нагрузке оба компонента проявили свои особенности на большом масштабе, и каждый потребовал отдельной настройки.

5.     Параметры по умолчанию потребовали подстройки. iproto.net_msg_max (лимит сообщений внутреннего бинарного протокола Tarantool — iproto), readahead и таймауты TCM рассчитаны на меньший масштаб. Прежде чем идти в продакшен с кластером такого размера, нужен отдельный этап тюнинга конфигурации.

Узкие места и как их обойти

1. Роутер — слабое звено

Проблема: один роутер не может нагрузить сторадж на полную мощность. Требуется 7 роутеров на 1 сторадж.

Решение: планировать топологию с запасом по роутерам, оптимальное соотношение — 7:1.

2. K6-tarantool ограничен

Проблема: connection pool в k6-tarantool работает на 15% хуже прямого подключения, максимальная производительность ~350k RPS на запись.

Решение: для больших кластеров использовать более производительную утилиту или собственный бенчер на Go.

3. Конфигурация кластера «из коробки»

Проблема: дефолтные настройки TDB рассчитаны на меньшие масштабы. Наблюдаются «моргания» в TCM, таймауты.

Решение: необходимы подстройки:

  • Увеличить iproto.net_msg_max и readahead на инстансах до 1536 и 32640 соответственно.

  • Увеличьте config.etcd.http.request.timeout, чтобы снизить количество повторных запросов (и «спам») в etcd. Значение таймаута подберите под реальное время ответа вашего кластера etcd под нагрузкой.

  • Настроить таймауты опроса инстансов в TCM:

    • cluster.refresh-state-period: 30

    • cluster.refresh-state-timeout: 10

    • storage.etcd.dial-timeout: 0.5s

    • storage.etcd.dial-keep-alive-time: 2s

    • storage.etcd.dial-keep-alive-timeout: 1s

4. etcd под нагрузкой

Проблема: при изменении конфигурации все 640 инстансов одновременно обращаются к etcd — возникает лавина запросов, трафик превышает 4 Гбит/с. За этим стоял баг, специфичный для большого числа инстансов, — передан команде платформы Tarantool и оперативно исправлен. Даже с исправлением etcd в такой конфигурации требует выделенного мощного железа.

Решение:

  • Использовать более мощный сервер для etcd.

  • Увеличить тайм-ауты.

5. Облачная инфраструктура

Проблема: существенные колебания результатов тестов из-за «шума соседей» и вариативности облачной среды.

Решение: использовать железные серверы, в которых исключаются посторонние влияния.

Best practices для масштабирования TDB

При планировании топологии:

  • Рассчитывать не менее 2 ядра CPU на инстанс.

  • Отношение роутеров к стораджам — 7:1 для максимальной производительности.

При развертывании:

  • Использовать ATE для разворота.

  • Пользоваться динамическим инвентарем при большом количестве инстансов.

При настройке:

  • Увеличить iproto.net_msg_max и readahead.

  • Настроить таймауты TCM.

  • Увеличить таймауты etcd.

При нагрузочном тестировании:

  • Подбирать оптимальное количество VUs эмпирически.

  • Менять нагружатор, если результаты явно занижены.

8. Заключение

Мы протестировали Tarantool DB на конфигурации, для которой ранее не публиковали результаты с подробными выводами, — 640 инстансов. Результаты нас удовлетворили:

  • Кластер работает. 128 роутеров, 128 репликасетов, 30 000 бакетов — все стабильно. Главный результат: технология выдерживает масштаб.

  • Производительность. ~1 млн RPS на чтение, ~650k RPS на запись при асинхронной репликации.

  • Масштабирование линейно. Можно планировать ресурсы, зная, что получишь.

  • TDB — это комплекс. Тестировался не только движок базы, но и TCM как встроенный инструмент управления. Оба компонента потребовали настройки под нестандартный масштаб.

  • Масштаб выявил баг. При одновременном обращении 640 нод к etcd кластер уходил в аномальную загрузку. Баг передан команде Tarantool и исправлен — именно такие вещи находятся только при реальном масштабировании.

Не обошлось без работы руками: подкручивали параметры iproto и таймауты TCM, писали собственный бенчер, боролись с облачной нестабильностью. Но все проблемы решаемы, и главное — они теперь известны заранее.

Что не вошло в статью

За несколько дней тестирования мы проверили базовые сценарии, но некоторые вещи остались за скобками:

  • Падение стораджей под нагрузкой. Проверили потерю нескольких виртуальных машин кластера при использовании стороннего балансировщика, но не тестировали переключение мастера в репликасете во время пиковой нагрузки.

  • Скорость подъема при заполнении памяти на 90%. Проверили, как быстро кластер поднимется, если память memtx заполнена на 90%, общий объем получился 463 Гб, и поднялся весь кластер с нуля за 4 минуты.

Что дальше

  1. Оптимизация роутеров. Роутер — слабое звено. Нужно искать способы улучшить его производительность: тюнинг, другая архитектура.

  2. Тестирование отказоустойчивости в полном объеме. Падение стораджей, сетевые проблемы, split-brain сценарии — все это важно для продакшена.

  3. Нагрузочное тестирование с реальными данными. При более «боевой» нагрузке — сложных запросах, нескольких индексах и транзакциях — профиль производительности может быть другим.

Ссылки