У традиционной архитектуры PostgreSQL есть фундаментальные ограничения: вертикальное масштабирование рано или поздно упирается в возможности одного сервера, а попытки масштабировать систему горизонтально приводят к репликации, шардированию и связанным с ними тупикам и компромиссам. Сколько ни улучшай PostgreSQL, сколько ни отчитывайся о реальных примерах внедрения, архитектурный потолок от этого никуда не исчезает: есть класс задач, где дальнейшее развитие в рамках традиционной архитектуры объективно затрудняется. 

Именно здесь появляется Tantor Polar — распределенная СУБД с разделением вычислений и хранения и 100% совместимостью с PostgreSQL. На ее основе построена машина баз данных Tantor XData Gen3, объединяющая мощные вычислительные узлы, высокопроизводительное общее хранилище и высокоскоростную сеть c RDMA в единую систему. В этой статье мы покажем, что эта архитектура дает на практике — рассмотрим устройство синергично работающих программных и аппаратных модулей позволяющих достичь высоких показателей в популярных нагрузочных фреймворках, таких как TPC‑B (250 тыс. TPS) и TPC‑C (2,6 млн NOPM). 

Архитектура Tantor Polar 

В основе архитектуры распределенной СУБД — несколько вычислительных узлов, подключенных к общему хранилищу данных (shared storage). Это позволяет избежать проблем, характерных для shared memory и shared nothing архитектур, а также масштабировать необходимые типы ресурсов независимо друг от друга. Если необходимо больше процессорных ядер и/или оперативной памяти, можно добавлять вычислительные узлы. Если столкнулись с ограничениями пропускной способности системы ввода‑вывода или необходимо хранить больший объем данных, можно масштабировать хранилище.

Основной узел записывает на общее хранилище WAL и страницы данных. Реплики читают данные с того же общего хранилища. Добавить новую реплику легко, копирование данных не требуется. В качестве хранилища может быть использовано любое блочное устройство. Рекомендованный способ организации общего хранилища — Linux MD Cluster поверх NVMe‑oF с поддержкой RDMA.

Tantor Polar использует, дополняет и развивает различные механизмы оптимизации и повышения производительности PolarDB, такие как LogIndex, WAL pipelining, CSN и др.Общее хранилище снимает необходимость в классической репликации. Однако данные находящегося в оперативной памяти buffer pool на репликах устаревают после того, как основной узел производит какие‑либо изменения. Для поддержания реплик в актуальном состоянии достаточно передать на них метаданные WAL с информацией о том, какие страницы были обновлены. Метаданные имеют гораздо меньший объем, чем сами данные, а их передача осуществляется по RDMA, и это позволяет достичь высокой скорости репликации. Кроме того, реплики не проигрывают весь WAL в последовательном режиме, как это происходит в обычном PostgreSQL. Вместо этого Tantor Polar использует специальную структуру LogIndex — отображение идентификатора страницы памяти на список LSN (Log Sequence Number) ее изменений, для того, чтобы проигрывать WAL по требованию (во время чтения) и в многопоточном режиме. В результате лаг репликации составляет несколько миллисекунд.

Tantor Polar оптимизирует запись в WAL за счет конвейерной обработки. В стандартном PostgreSQL каждый процесс, фиксирующий транзакцию, выполняет запись и сброс WAL на запоминающее устройство, конкурируя за блокировки и систему ввода‑вывода. При высокой транзакционной нагрузке это становится узким местом. Конвейерная обработка позволяет нескольким потокам помещать LSN своих транзакций в очередь, после чего специальный обработчик сохраняет все содержимое очереди в пакетном режиме. Это существенно ускоряет запись WAL.

Tantor Polar использует механизм CSN для того, чтобы упростить и ускорить определение видимости при многоверсионном управлении доступом к данным (MVCC — Multi Version Currency Control). В традиционной реализации PostgreSQL для определения видимости изменений транзакции требуется получить ее статус в CLOG (Commit Log) и сравнить ее идентификатор (XID) со списком транзакций активных на момент создания снимка БД (асимптотическая сложность O(n)). При использовании механизма CSN проверка видимости сводится к простому сравнению: изменения транзакции видимы, если ее CSN больше CSN снапшота (асимптотическая сложность O(1)).

Файловая система

Основой общей системы хранения Tantor Polar является высокопроизводительная распределенная файловая система Tantor PolarFS — доработанная версия Alibaba PolarFS. PolarFS предоставляет Tantor Polar доступ к блочному устройству на уровне файловой системы. Она оптимизирована для работы с NVMe‑oF поверх RDMA. Для реализации ввода‑вывода используется асинхронная библиотека libaio и, соответственно, режим небуферизованного доступа к файлам O_DIRECT. Это позволяет исключить страничный кеш ОС и использовать до 75% RAM под buffer pool. Tantor PolarFS содержит собственную реализацию fsync, позволяющую эффективно производить надежную запись данных на произвольное блочное устройство. Существенно оптимизирован и переработан механизм межпроцессного взаимодействия (по сравнению с исходной реализацией). Группировка poll‑запросов реплик для проверки изменения метаданных позволяет минимизировать расходы на чтение данных из специального файла. Используя анонимные файлы в памяти (memfd), Tantor PolarFS получает непосредственный (zero‑copy) доступ ко всем резидентным буферам разделяемой памяти PostgreSQL — WAL buffers, buffer pool, COPY buffer, SLRU pages и так далее

Перечисленные оптимизации позволили на порядок увеличить скорость чтения и записи в Tantor PolarFS по сравнению с исходной реализацией.

Балансировка и параллелизация

Для балансировки нагрузки и пулинга соединений Tantor Polar использует Tantor Polar Proxy — прокси‑сервер, основанный на ProxySQL. Прокси‑сервер позволяет равномерно распределять читающие запросы между узлами. Также в нем реализован пул соединений.

Однако для тяжелых аналитических запросов простого распределения на уровне балансировщика может быть недостаточно. Для таких запросов архитектура Tantor Polar позволяет задействовать массивно‑параллельную обработку, благодаря которой один запрос может выполняться сразу на нескольких узлах (независимо от того, на какой узел его направил балансировщик). При этом в рамках одного узла запрос также может исполняться параллельно в несколько потоков. MPP реализована с помощью механизма эластичного распараллеливания запросов PX/ePQ (Parallel Executor / elastic Parallel Query), использующего доработанный планировщик GPORCA. Механизм называется эластичным, так как каждый узел имеет доступ ко всем данным, что дает возможность легко добавлять вычислительные узлы «эластично» увеличивая степень параллелизации выполнения запроса (DOP — degree of parallelism).

Архитектура МБД Tantor XData Gen3

Машина баз данных Tantor XData Gen3 на аппаратном уровне обеспечивает поддержку реализации архитектурных возможностей СУБД Tantor Polar, таких как разделение и независимое масштабирование подсистем вычислений и хранения.

В основе XData Gen3:

  • мощные вычислительные серверы на многоядерных процессорах AMD для запуска экземпляров Tantor Polar (основного узла и реплик);

  • высокопроизводительные (NVMe) запоминающие устройства, подключенные к серверам системы хранения;

  • сетевые коммутаторы Infiniband/RoCE v2 от 100 Гбит и выше для высокоростного внутреннего взаимодействия между узлами через RDMA.

На каждом узле хранения несколько локальных NVMe‑устройств объединяются в RAID 0 с помощью mdadm. Том RAID 0 каждого узла доступен на других узлах через выделенную многоканальную сеть NVMe‑oF с использованием RDMA. Таким образом, каждый узел хранения видит все тома других узлов (fullу connected mesh). На каждом узле хранения с помощью Linux MD Cluster формируется кластерное устройство RAID 1, состоящее локального и удаленных томов. Локальный RAID 0 поддерживает запись со скоростью 30 Гбит/с. При наличии трех узлов хранения зеркалирование генерирует трафик 60 Гбит/с. Для изолированной передачи этого трафика используется выделенный канал 100 ГБит/сек.

Подробнее об устройстве и отличительных особенностях МБД Tantor XData Gen3 можно прочесть в этой статье

Как все работает на практике

Рассмотрим, каких практических результатов позволяют достичь описанные выше архитектурные особенности МБД Tantor XData Gen3 и СУБД Tantor Polar. Для генерации нагрузки использовались тесты TPC: TPC‑B, TPC‑C и TPC‑E. Эти тесты позволяют проверить работу СУБД под OLTP‑нагрузкой в виде большого количества одновременных SQL‑запросов.

Все тесты выполнялись многократно, приведенные ниже результаты являются усредненными.

Конфигурация тестового стенда

Менеджмент (1 сервер)

Процессоры: 2 x AMD 192 ядер, 3.2 ГГц

Оперативная память: 1 Тб, DDR5

Диск: SSD SAS/SATA 30 Тб

Сеть: 2 x Infiniband ConnectX-6 100 Гбит

Вычислительные узлы (3 сервера)

Процессоры: 2 x AMD 192 ядер, 3.2 ГГц

Оперативная память: 1 Тб, DDR5

Диск: SSD SAS/SATA 560 Тб

Сеть: 2 x Infiniband ConnectX-6 100 Гбит

Хранилище (3 сервера)

Процессоры: 2 x AMD 32 ядра, 3.65 ГГц

Оперативная память: 256 Гб, DDR5

Диск: SSD NVMe 91 Тб

Сеть: 2 x Infiniband ConnectX-6 100 Гбит

Коммутаторы

Данные: 2 x Infiniband 100 Гбит

Управление: 48 x 1 Гбит RJ45, 4 × 10/25 Гбит, 2 x 40 Гбит

Основные настройки

Репликация

асинхронная

shared buffers

350 Гб

huge pages

206 000 страниц по 2 Мб

CSN

включен

Тест TPC‑B

Тест TPC‑B предназначен для измерения транзакционной нагрузки (OLTP), которую способна обработать СУБД. Тест многократно выполняет транзакцию, состоящую из трех UPDATE, одного SELECT и одного INSERT операторов. Результаты теста измеряются в транзакциях в секунду (TPS — Transactions Per Second). БД состоит из четырех таблиц. Для выполнения теста использовалась утилита pgbench, входящая в стандартную инсталляцию PostgreSQL.

Т.к. под shared buffers отведено 350 Гб, тест будем проводить на БД размером ~1 Тб, чтобы исключить возможность ее полного кеширования в оперативной памяти. Для ее генерации необходимо задать коэффициент масштаба 100 000 (ключ ‑scale). Базу данных можно создать следующим образом:

PGPASSWORD=postgres pgbench \
  --initialize \
  --scale=100000 \
  --host=localhost \
  --port=5432 \
  --username=postgres \
  my_tpcb_db

Запуск теста

PGPASSWORD=postgres pgbench \
  --protocol=prepared \
  --time=300 \
  --host=localhost \
  --port=5432 \
  --progress=1 \
  --client=1000 \
  --jobs=256 \
  --username=postgres \
  my_tpcb_db

Здесь:

--protocol=prepared

использовать extended protocol и prepared statements

--client=1000

эмулировать параллельную работу тысячи клиентов

--jobs=256

использовать 256 потоков для отправки запросов

--progress=1

выводить сообщения о ходе теста раз в секунду

Результат выполнения теста — в среднем около 250 тыс. TPS.

Пример запуска:

Скрытый текст
pgbench (PostgreSQL 15.18 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 11.5.0 20240719 (Red Hat 11.5.0-14), 64-bit)
starting vacuum...end.
progress: 1.1 s, 0.0 tps, lat 0.000 ms stddev 0.000, 0 failed
progress: 2.0 s, 217013.3 tps, lat 4.606 ms stddev 19.863, 0 failed
progress: 3.0 s, 284939.5 tps, lat 3.507 ms stddev 3.297, 0 failed
progress: 4.0 s, 288413.8 tps, lat 3.434 ms stddev 3.016, 0 failed
progress: 5.0 s, 286961.1 tps, lat 3.519 ms stddev 3.697, 0 failed
progress: 6.0 s, 286891.6 tps, lat 3.485 ms stddev 3.105, 0 failed
progress: 7.0 s, 278812.3 tps, lat 3.586 ms stddev 3.339, 0 failed
progress: 8.0 s, 266805.4 tps, lat 3.741 ms stddev 4.375, 0 failed
progress: 9.0 s, 283594.2 tps, lat 3.525 ms stddev 3.314, 0 failed
progress: 10.0 s, 270787.6 tps, lat 3.600 ms stddev 3.840, 0 failed
progress: 11.0 s, 266464.5 tps, lat 3.846 ms stddev 4.477, 0 failed
progress: 12.0 s, 281500.2 tps, lat 3.524 ms stddev 3.980, 0 failed
progress: 13.0 s, 274039.1 tps, lat 3.588 ms stddev 3.664, 0 failed
progress: 14.0 s, 272739.7 tps, lat 3.762 ms stddev 4.659, 0 failed
progress: 15.0 s, 270624.8 tps, lat 3.683 ms stddev 3.815, 0 failed
progress: 16.0 s, 281179.8 tps, lat 3.563 ms stddev 3.675, 0 failed
progress: 17.0 s, 273686.6 tps, lat 3.651 ms stddev 4.096, 0 failed
progress: 18.0 s, 260651.1 tps, lat 3.835 ms stddev 4.325, 0 failed
progress: 19.0 s, 279534.8 tps, lat 3.578 ms stddev 3.688, 0 failed
progress: 20.0 s, 280565.7 tps, lat 3.557 ms stddev 3.574, 0 failed
progress: 21.0 s, 277141.9 tps, lat 3.609 ms stddev 3.966, 0 failed
progress: 22.0 s, 278798.8 tps, lat 3.586 ms stddev 4.184, 0 failed
progress: 23.0 s, 274611.7 tps, lat 3.635 ms stddev 3.853, 0 failed
progress: 24.0 s, 258635.1 tps, lat 3.823 ms stddev 4.598, 0 failed
progress: 25.0 s, 268608.7 tps, lat 3.679 ms stddev 4.221, 0 failed
progress: 26.0 s, 268584.2 tps, lat 3.812 ms stddev 4.895, 0 failed
progress: 27.0 s, 271344.6 tps, lat 3.688 ms stddev 4.155, 0 failed
progress: 28.0 s, 275260.7 tps, lat 3.632 ms stddev 3.477, 0 failed
progress: 29.0 s, 260328.4 tps, lat 3.839 ms stddev 4.276, 0 failed
progress: 30.0 s, 277137.3 tps, lat 3.608 ms stddev 3.990, 0 failed
progress: 31.0 s, 278381.8 tps, lat 3.586 ms stddev 3.514, 0 failed
progress: 32.0 s, 270456.7 tps, lat 3.699 ms stddev 4.771, 0 failed
progress: 33.0 s, 264088.1 tps, lat 3.770 ms stddev 4.328, 0 failed
progress: 34.0 s, 266508.6 tps, lat 3.770 ms stddev 4.371, 0 failed
progress: 35.0 s, 277826.4 tps, lat 3.598 ms stddev 4.268, 0 failed
progress: 36.0 s, 262102.9 tps, lat 3.814 ms stddev 5.686, 0 failed
progress: 37.0 s, 254985.3 tps, lat 3.916 ms stddev 4.777, 0 failed
progress: 38.0 s, 282909.6 tps, lat 3.535 ms stddev 3.874, 0 failed
progress: 39.0 s, 269163.8 tps, lat 3.716 ms stddev 4.527, 0 failed
progress: 40.0 s, 268108.2 tps, lat 3.719 ms stddev 4.677, 0 failed
progress: 41.0 s, 274462.2 tps, lat 3.645 ms stddev 3.798, 0 failed
progress: 42.0 s, 271685.6 tps, lat 3.686 ms stddev 3.417, 0 failed
progress: 43.0 s, 277169.8 tps, lat 3.606 ms stddev 3.589, 0 failed
progress: 44.0 s, 275183.8 tps, lat 3.629 ms stddev 3.868, 0 failed
progress: 45.0 s, 270375.2 tps, lat 3.698 ms stddev 4.011, 0 failed
progress: 46.0 s, 271277.7 tps, lat 3.684 ms stddev 3.595, 0 failed
progress: 47.0 s, 268475.7 tps, lat 3.724 ms stddev 2.865, 0 failed
progress: 48.0 s, 251619.5 tps, lat 3.970 ms stddev 4.475, 0 failed
progress: 49.0 s, 268261.2 tps, lat 3.707 ms stddev 4.110, 0 failed
progress: 50.0 s, 270641.3 tps, lat 3.719 ms stddev 3.654, 0 failed
progress: 51.0 s, 275049.5 tps, lat 3.636 ms stddev 4.121, 0 failed
progress: 52.0 s, 264599.8 tps, lat 3.775 ms stddev 3.874, 0 failed
progress: 53.0 s, 268659.3 tps, lat 3.723 ms stddev 4.293, 0 failed
progress: 54.0 s, 273813.9 tps, lat 3.651 ms stddev 4.045, 0 failed
progress: 55.0 s, 270361.2 tps, lat 3.697 ms stddev 4.200, 0 failed
progress: 56.0 s, 266858.1 tps, lat 3.745 ms stddev 3.665, 0 failed
progress: 57.0 s, 259759.5 tps, lat 3.850 ms stddev 4.425, 0 failed
progress: 58.0 s, 277792.6 tps, lat 3.595 ms stddev 4.119, 0 failed
progress: 59.0 s, 261231.3 tps, lat 3.831 ms stddev 3.779, 0 failed
progress: 60.0 s, 246105.5 tps, lat 4.055 ms stddev 4.821, 0 failed
progress: 61.0 s, 273969.0 tps, lat 3.654 ms stddev 3.347, 0 failed
progress: 62.0 s, 264187.7 tps, lat 3.779 ms stddev 3.984, 0 failed
progress: 63.0 s, 244876.8 tps, lat 4.082 ms stddev 3.861, 0 failed
progress: 64.0 s, 266836.1 tps, lat 3.752 ms stddev 4.030, 0 failed
progress: 65.0 s, 265177.0 tps, lat 3.757 ms stddev 3.597, 0 failed
progress: 66.0 s, 269024.7 tps, lat 3.723 ms stddev 3.545, 0 failed
progress: 67.0 s, 246314.2 tps, lat 4.056 ms stddev 3.796, 0 failed
progress: 68.0 s, 261654.5 tps, lat 3.821 ms stddev 3.817, 0 failed
progress: 69.0 s, 261046.6 tps, lat 3.835 ms stddev 3.818, 0 failed
progress: 70.0 s, 234287.9 tps, lat 4.267 ms stddev 5.708, 0 failed
progress: 71.0 s, 191669.4 tps, lat 4.385 ms stddev 8.077, 0 failed
progress: 72.0 s, 235132.4 tps, lat 4.924 ms stddev 14.576, 0 failed
progress: 73.0 s, 176064.2 tps, lat 5.686 ms stddev 18.263, 0 failed
progress: 74.0 s, 179057.2 tps, lat 5.584 ms stddev 19.776, 0 failed
progress: 75.0 s, 224393.9 tps, lat 4.461 ms stddev 9.783, 0 failed
progress: 76.0 s, 224639.1 tps, lat 4.434 ms stddev 8.549, 0 failed
progress: 77.0 s, 238755.0 tps, lat 4.194 ms stddev 5.369, 0 failed
progress: 78.0 s, 241211.8 tps, lat 4.149 ms stddev 7.398, 0 failed
progress: 79.0 s, 228743.9 tps, lat 4.277 ms stddev 6.648, 0 failed
progress: 80.0 s, 221481.4 tps, lat 4.610 ms stddev 7.678, 0 failed
progress: 81.0 s, 250906.7 tps, lat 3.985 ms stddev 5.667, 0 failed
progress: 82.0 s, 243364.6 tps, lat 4.109 ms stddev 5.834, 0 failed
progress: 83.0 s, 235682.7 tps, lat 4.238 ms stddev 5.729, 0 failed
progress: 84.0 s, 233014.4 tps, lat 4.293 ms stddev 6.414, 0 failed
progress: 85.0 s, 248087.8 tps, lat 4.025 ms stddev 5.274, 0 failed
progress: 86.0 s, 231095.2 tps, lat 4.322 ms stddev 6.101, 0 failed
progress: 87.0 s, 227428.4 tps, lat 4.406 ms stddev 5.752, 0 failed
progress: 88.0 s, 255638.5 tps, lat 3.891 ms stddev 5.334, 0 failed
progress: 89.0 s, 244763.4 tps, lat 4.104 ms stddev 5.289, 0 failed
progress: 90.0 s, 229885.2 tps, lat 4.347 ms stddev 5.189, 0 failed
progress: 91.0 s, 218177.2 tps, lat 4.184 ms stddev 6.915, 0 failed
progress: 92.0 s, 260283.8 tps, lat 4.176 ms stddev 7.161, 0 failed
progress: 93.0 s, 239232.2 tps, lat 4.152 ms stddev 6.047, 0 failed
progress: 94.0 s, 234495.8 tps, lat 4.287 ms stddev 6.850, 0 failed
progress: 95.0 s, 259313.1 tps, lat 3.852 ms stddev 4.029, 0 failed
progress: 96.0 s, 241072.1 tps, lat 4.154 ms stddev 4.595, 0 failed
progress: 97.0 s, 239695.0 tps, lat 4.105 ms stddev 6.522, 0 failed
progress: 98.0 s, 245813.6 tps, lat 4.124 ms stddev 6.016, 0 failed
progress: 99.0 s, 245729.2 tps, lat 4.076 ms stddev 6.668, 0 failed
progress: 100.0 s, 239022.2 tps, lat 4.181 ms stddev 4.849, 0 failed
progress: 101.0 s, 252464.6 tps, lat 3.956 ms stddev 4.701, 0 failed
progress: 102.0 s, 262157.0 tps, lat 3.800 ms stddev 4.208, 0 failed
progress: 103.0 s, 246570.6 tps, lat 4.070 ms stddev 4.583, 0 failed
progress: 104.0 s, 234252.2 tps, lat 4.266 ms stddev 7.021, 0 failed
progress: 105.0 s, 217362.4 tps, lat 4.578 ms stddev 8.443, 0 failed
progress: 106.0 s, 261793.6 tps, lat 3.841 ms stddev 4.618, 0 failed
progress: 107.0 s, 246110.6 tps, lat 4.056 ms stddev 4.768, 0 failed
progress: 108.0 s, 233725.9 tps, lat 4.278 ms stddev 5.542, 0 failed
progress: 109.0 s, 261310.3 tps, lat 3.822 ms stddev 4.290, 0 failed
progress: 110.0 s, 255688.2 tps, lat 3.871 ms stddev 4.512, 0 failed
progress: 111.0 s, 249768.7 tps, lat 4.050 ms stddev 5.068, 0 failed
progress: 112.0 s, 247486.8 tps, lat 4.039 ms stddev 4.528, 0 failed
progress: 113.0 s, 259301.0 tps, lat 3.851 ms stddev 3.713, 0 failed
progress: 114.0 s, 245150.8 tps, lat 4.079 ms stddev 4.110, 0 failed
progress: 115.0 s, 235689.9 tps, lat 4.245 ms stddev 5.324, 0 failed
progress: 116.0 s, 259283.0 tps, lat 3.858 ms stddev 4.991, 0 failed
progress: 117.0 s, 258534.1 tps, lat 3.868 ms stddev 5.381, 0 failed
progress: 118.0 s, 247524.4 tps, lat 4.035 ms stddev 4.083, 0 failed
progress: 119.0 s, 252071.5 tps, lat 3.963 ms stddev 4.800, 0 failed
progress: 120.0 s, 250938.7 tps, lat 3.922 ms stddev 4.663, 0 failed
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 65535
query mode: prepared
number of clients: 1000
number of threads: 256
maximum number of tries: 1
duration: 120 s
number of transactions actually processed: 30507016
number of failed transactions: 0 (0.000%)
latency average = 3.897 ms
latency stddev = 5.569 ms
initial connection time = 1073.452 ms
tps = 256435.810898 (without initial connection time)

Тест TPC‑C

Тест TPC‑C также предназначен для измерения транзакционной нагрузки. Считается, что он более приближен к реальным условиям, чем TPC‑B. В TPC‑C параллельно выполняются пять транзакций различной сложности. БД состоит из девяти таблиц с колонками различных типов и со значительно различающимся количеством записей. TPC‑C условно имитирует работу оптового поставщика. Результаты теста измеряются в количестве созданных заказов в минуту (NOPM — New Orders Per Minute). NOPM для TPC‑C можно получить из TPS по формуле:

n = 0,43 * t * 60,

где n — NOPM, t — TPS.

Стандартная утилита pgbench не позволяет запускать тест TPC‑C (имеется возможность запуска произвольных пользовательских SQL‑скриптов, но встроенная возможность непосредственного запуска TPC‑C и создания БД для него отсутствует). Для запуска теста была выбрана утилита HammerDB, в которой реализован удобный консольный интерфейс. Для проведения теста вновь создадим БД размером около 1 ТБ (10000 складов). В качестве способа реализации скриптов выберем хранимые процедуры, так как считаем, что этот вариант ближе к устройству реальных системы.

Создание базы данных

Команда

Пояснение

dbset db pg

установить тип СУБД PostgreSQL

dbset bm tpc-c

установить тип теста TPC‑C

diset connection pg_host localhost

diset connection pg_port 5432

diset tpcc pg_superuser postgres

diset tpcc pg_superuserpass postgres

diset tpcc pg_user postgres

diset tpcc pg_pass postgres

diset tpcc pg_dbase my_tpcc_db

параметры подключения

diset tpcc pg_count_ware 10000

количество складов (от этого зависит размер БД)

diset tpcc pg_num_vu 10

количество потоков создания БД

diset tpcc pg_storedprocs true

использовать хранимые процедуры

diset tpcc pg_raiseerror true

не скрывать текст ошибок при их возникновении

buildschema

запустить создание БД

Запуск теста

Команда

Пояснение

diset tpcc pg_driver timed

ограничить выполнение теста по времени

diset tpcc pg_rampup 5

время «прогрева» БД в минутах

diset tpcc pg_duration 15

общее время теста БД в минутах

vudestroy

закрыть подключения к БД от предыдущего теста (если он был)

vuset vu 1000

задать количество одновременных клиентских подключений

vuсreate

создать подключения к БД

vurun

запустить тест

Тест запускался для 10, 50, 100, 200, 300, 400, 500, 1000 и 5000 клиентов (параметр vu, см. выше). Большинство результатов запусков дает результат в промежутке от 1 до 1,6 млн NOPM на 50–1000 клиентах, чуть меньше 1 млн (800–900 тыс) NOPM на 5000 клиентах и 0,5 млн NOPM на 10 клиентах. Максимальное значение, полученное на 1000 клиентах, — 2,6 млн NOMP. Запуск на 5000 клиентах производился через Polar Proxy с размером пула 1000 соединений.

Полученные результаты значительно превышают показатели, полученные на альтернативных платформах, включая Oracle Exadata X8M/X9M:

Нагрузка и используемые во время теста ресурсы (главный вычислительный узел):

Количество транзакций
Количество транзакций
Процессор
Процессор
Оперативная память
Оперативная память
Сетевой трафик
Сетевой трафик
Операций в секунду на запоминающем устройстве
Операций в секунду на запоминающем устройстве
Пропускная способность запоминающего устройства
Пропускная способность запоминающего устройства

Tantor Polar Proxy Transaction Splitting

Одна из наиболее мощных функциональных возможностей Tantor Polar Proxy — выполнение транзакции на нескольких узлах (transaction splitting). Т.е. прокси сервер может выполнять на репликах SELECT‑запросы (производить offloading) встречающиеся внутри транзакций. Даже если это пишущая транзакция. Причем offloading выполняется не только для SELECT‑запросов идущих вначале транзакции, но и для тех SELECT‑запросов, которые идут после DML‑операций.

Транзакция рассмотренного выше теста TPC‑B содержит тривиальные запросы, большинство из которых — UPDATE/INSERT. Это делает его не самым удачным кандидатом для проверки работы механизма transaction splitting. Однако, если транзакцию слегка модифицировать, уровня количество записей и чтений, то offloading можно наблюдать и на этом тесте.

Стандартная реализация TPC‑B содержит следующие запросы:

\set aid random(1, 100000 * :scale)
\set bid random(1, 1 * :scale)
\set tid random(1, 10 * :scale)
\set delta random(-5000, 5000)
BEGIN;
UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid;
SELECT abalance FROM pgbench_accounts WHERE aid = :aid;
UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid = :tid;
UPDATE pgbench_branches SET bbalance = bbalance + :delta WHERE bid = :bid;
INSERT INTO pgbench_history (tid, bid, aid, delta, mtime)
VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP);
END;

Модифицированный TPC‑B-like тест для проверки работы transaction splitting может выглядеть следующим образом:

\set aid random(1, 100000 * :scale)
\set bid random(1, 1 * :scale)
\set tid random(1, 10 * :scale)
\set delta random(-5000, 5000)
BEGIN;
UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid;
SELECT abalance FROM pgbench_accounts WHERE aid = :aid;
SELECT abalance FROM pgbench_accounts WHERE aid = :aid + 1;
SELECT abalance FROM pgbench_accounts WHERE aid = :aid + 2;
UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid = :tid;
--UPDATE pgbench_branches SET bbalance = bbalance + :delta WHERE bid = :bid;
INSERT INTO pgbench_history (tid, bid, aid, delta, mtime)
VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP);
END;

Для запуска pgbench с измененным SQL‑скриптом используется ключ ‑file. Также в этом случае необходимо явно задать коэффициент масштаба (ключ ‑scale). Для корректной работы transaction splitting необходимо использовать протокол simple (ключ ‑protocol). В остальном запуск pgbench через прокси ничем не отличается от обычного запуска (за исключением того, что надо указать адрес, порт и учетную запись Tantor Polar Proxy). Команда может выглядеть так:

PGPASSWORD=postgres pgbench \
  --file=tpcb-like.sql \
  --scale=100000
  --protocol=simple \
  --time=120 \
  --host=localhost \
  --port=6133 \
  --progress=1 \
  --client=1000 \
  --jobs=256 \
  --username=postgres \
  my_db

Выполнив следующий запрос в административной консоли Polar Proxy, можно увидеть распределение запросов по кластеру:

SELECT 
  hostgroup,
  srv host,
  Queries,
  Latency_us
FROM stats_pgsql_connection_pool;
 hostgroup |   srv_host   |  Queries  | Latency_us
-----------+--------------+-----------+------------
 10        | 192.168.6.10 | 122437440 | 170
 20        | 192.168.6.11 | 22208236  | 208
 20        | 192.168.6.12 | 22340367  | 178
(3 rows)

Здесь 192.168.6.10 — мастер и 192.168.6.11, 192.168.6.12 — реплики. Распределение не очень равномерное, так как на данном типе нагрузки не для каждого запроса успевает отрабатывать WAL flush. Тем не менее можно видеть, что на реплики уходит довольно большое количество запросов и механизм transaction splitting работает даже для случая, когда читающие запросы идут после модифицирующих.

Тест TPC‑E

TPC‑E — еще один тест для измерения транзакционой нагрузки. Это самый сложный и приближенный к реальности из рассматриваемых тестов. В TPC‑E выполняются двенадцать параллельных транзакций разных типов и сложности. База данных состоит из тридцати трех таблиц с колонками различных типов данных и кардинальности и независимыми коэффициентами масштабирования. TPC‑E условно имитирует работу брокерской организации на бирже. Результаты измеряются в количестве транзакций выполненных за тест.

Этот тест гораздо лучше подходит для демонстрации работы Tantor Polar Proxy и Transaction Splitting, в частности, потому, что он содержит порядка 75% читающей нагрузки. Тест проводился с помощью утилиты DBT-5 (документацию можно найти здесь). Для возможности использования Tantor Polar Proxy утилита была доработана, в нее была добавлена возможность использования simple протокола.

База данных для проведения тестов была создана следующим образом:

dbt5 build --tpcetools=egen -t 7000 -f 500 -w 300 pgsql my_tpce_db

Здесь:

--tpcetools=egen

путь к генератору данных

-t 7000

количество клиентов биржи

-f 500

количество клиентов биржи на результат сделки

-w 300

период торговли в днях

pgsql

тип СУБД (PostgreSQL)

My_tpce_db

наименование базы данных

Запуск теста:

DURATION=1200 USERS=1000 PGUSER=postgres PGPASSWORD=postgres \
dbt5 run
    --client-side \
    --dbaas \
    --tpcetools=egen \
    -h localhost \
    -p 6133 \
    -n my_tpce_db \
    -u ${USERS} \
    -d ${DURATION} \
    -z "Users: ${USERS}, duration: ${DURATION}" \
    pgsql \
    result

Здесь:

--client-side

не используем хранимые процедуры (для проверки Transaction Splitting)

--dbaas

не запускать сервис СУБД (он уже запущен)

--tpcetools=egen

путь к генератору данных

-h localhost

хост подключения

-p 6133

порт подключения (указываем порт Tantor Polar Proxy)

-n my_tpce_db

наименование базы данных

-u 1000

количество одновременных клиентских подключений

-d 12000

длительность теста в секундах

-z "Users: 1000, duration: 1200"

сообщение‑комментарий

pgsql

тип СУБД (PostgreSQL)

result

каталог для сохранения результата

После выполнения теста можно увидеть, что распределение запросов по узлам значительно улучшилось:

 hostgroup |   srv_host   |  Queries  | Latency_us
-----------+--------------+-----------+------------
 10        | 192.168.6.10 | 249571149 | 107
 20        | 192.168.6.11 | 365686660 | 147
 20        | 192.168.6.12 | 365400134 | 118
(3 rows)

Тест запускался для 10, 50, 100, 200, 300, 400, 500, 1000 и 5000 одновременных клиентских подключений. Результаты теста через прокси в сравнении с прямым подключением к главному узлу:

Как видно из гистограммы, с ростом количества подключений растет и эффект от применения Transaction Splitting.

Эффективный Transaction Splitting

Более явного эффекта от применения Transaction Splitting можно достичь используя для теста пользовательские скрипты.

Создание базы данных:

DROP TABLE IF EXISTS accounts;
CREATE TABLE accounts
(
    account_id bigint PRIMARY KEY,
    region_id  integer NOT NULL,
    balance    numeric(18,2) NOT NULL,
    status     integer NOT NULL DEFAULT 0,
    payload    text NOT NULL
);

DROP TABLE IF EXISTS operations;
CREATE TABLE operations
(
    operation_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    account_id   bigint NOT NULL REFERENCES accounts(account_id),
    amount       numeric(18,2) NOT NULL,
    operation_ts timestamp NOT NULL,
    operation_type integer NOT NULL,
    payload      text NOT NULL
);

CREATE INDEX accounts_region_idx
    ON accounts(region_id);

CREATE INDEX operations_account_idx
    ON operations(account_id);

CREATE INDEX operations_ts_idx
    ON operations(operation_ts);

CREATE INDEX operations_account_ts_idx
    ON operations(account_id, operation_ts);

Заполнение базы данных:

INSERT INTO accounts
SELECT
    i,
    (i % 100) + 1,
    (10000 + random() * 100000)::numeric(18,2),
    0,
    repeat(md5(i::text), 4)
FROM generate_series(1, 1000000) AS g(i);

INSERT INTO operations
    (account_id, amount, operation_ts, operation_type, payload)
SELECT
    (random() * 999999)::bigint + 1,
    (random() * 10000)::numeric(18,2),
    now() - random() * interval '365 days',
    (random() * 4)::integer,
    repeat(md5(i::text), 2)
FROM generate_series(1, 20000000) AS g(i);

VACUUM (ANALYZE) accounts;
VACUUM (ANALYZE) operations;

Бизнес логика (banking.sql):

BEGIN;

SELECT
    a.region_id,
    o.operation_type,
    count(*) AS operation_count,
    sum(o.amount) AS total_amount,
    avg(o.amount) AS avg_amount
FROM operations o JOIN accounts a ON a.account_id = o.account_id
WHERE o.operation_ts >= now() - interval '30 days'
    AND a.region_id = (
        SELECT region_id
        FROM accounts
        WHERE account_id = :account_id)
GROUP BY
    a.region_id,
    o.operation_type;

SELECT
    count(*) AS operation_count,
    coalesce(sum(amount), 0) AS total_amount,
    coalesce(avg(amount), 0) AS avg_amount,
    coalesce(max(amount), 0) AS max_amount
FROM operations
WHERE account_id = :account_id
    AND operation_ts >= now() - interval '180 days';

SELECT
    account_id,
    region_id,
    balance,
    status,
    length(payload)
FROM accounts
WHERE account_id = :account_id;

UPDATE account SET
    balance = balance + 1.00,
    status = status + 1
WHERE account_id = :account_id;

SELECT
    account_id,
    balance,
    status
FROM accounts
WHERE account_id = :account_id;

SELECT
    a.region_id,
    count(*) AS operation_count,
    sum(o.amount) AS total_amount,
    avg(o.amount) AS avg_amount,
    min(o.amount) AS min_amount,
    max(o.amount) AS max_amount
FROM operations o JOIN accounts a ON a.account_id = o.account_id
WHERE o.operation_ts >= now() - interval '30 days'
    AND a.region_id = (
        SELECT region_id
        FROM accounts
        WHERE account_id = :account_id)
GROUP BY a.region_id;

COMMIT;

Запуск теста без прокси:

PGPASSWORD=postgres pgbench
  --file=banking.sql
  --host=localhost
  --protocol=prepared
  --progress=1
  --time=300
  --client=1000
  --jobs=128
  --username=postgres
  --port=5432
  my_db

Результат одного из запусков:

transaction type: routing.sql
scaling factor: 1
query mode: prepared
number of clients: 1000
number of threads: 128
maximum number of tries: 1
duration: 300 s
number of transactions actually processed: 71573
number of failed transactions: 0 (0.000%)
latency average = 3925.378 ms
latency stddev = 9731.581 ms
initial connection time = 1050.291 ms
tps = 235.925803 (without initial connection time)
statement latencies in milliseconds and failures:
         0.048           0  \set account_id random(1, 1000000)
      2964.291           0  BEGIN;
       489.044           0  SELECT
         0.860           0  SELECT
         0.504           0  SELECT
         0.799           0  UPDATE accounts
         0.330           0  SELECT
       467.913           0  SELECT
         1.696           0  COMMIT;

Запуск теста через прокси:

PGPASSWORD=postgres pgbench
  --file=banking.sql
  --host=localhost
  --protocol=simple
  --progress=1
  --time=300
  --client=1000
  --jobs=128
  --username=postgres
  --port=6133
  my_db

Результат одного из запусков:

transaction type: routing.sql
scaling factor: 1
query mode: simple
number of clients: 1000
number of threads: 128
maximum number of tries: 1
duration: 300 s
number of transactions actually processed: 169934
number of failed transactions: 0 (0.000%)
latency average = 1761.360 ms
latency stddev = 1774.200 ms
initial connection time = 1046.546 ms
tps = 566.430824 (without initial connection time)
statement latencies in milliseconds and failures:
         0.095           0  \set account_id random(1, 1000000)
        12.761           0  BEGIN;
       146.804           0  SELECT
         1.453           0  SELECT
         1.040           0  SELECT
         5.141           0  UPDATE accounts
         1.957           0  SELECT
      1582.483           0  SELECT
         9.650           0  COMMIT;

Реплики забирают на себя тяжелые аналитические запросы, поэтому на этом тесте наблюдаем кратный рост нагрузки при использовании Tantor Polar Proxy (более чем в два раза):

Дальше — больше!

Машина баз данных Tantor XData Gen 3 на основе СУБД Tantor Polar — прорывное решение для российского рынка. Мощная современная аппаратная архитектура, существенные доработки и экстремальные оптимизации известных программных продуктов, максимально согласованная работа всех используемых модулей в рамках единого ПАК и 100% совместимость с PostgreSQL, приложениями и расширениями позволяют получить невероятно эффективное и универсальное решение для промышленной обработки больших объемов данных «под ключ».

В статье нарочно не раскрыта тема тестирования производительности системы под OLAP‑ и HTAP‑ нагрузками. Эту часть мы рассмотрим 10 сентября в нашем докладе на Tantor JAM 2026. Регистрируйтесь, приходите, будет интересно!