Привет, хабр! Меня зовут Евгений Иванов, я разработчик YDB. И я хочу снова поговорить о моей любимой теме - производительности распределенных СУБД. В этом посте Вашему вниманию представлен перевод свежей статьи "YDB meets TPC-C: distributed transactions performance now revealed".

В нашем предыдущем посте о производительности YDB, посвященном Yahoo! Cloud Serving Benchmark (YCSB), мы упоминали, что готовим к публикации результаты других бенчмарков. Мы придерживаемся плана и сегодня рады представить вашему вниманию наши первые результаты бенчмарка TPC-C*, который является индустриальным стандартом оценки производительности онлайн транзакций (OLTP). Согласно этим результатам есть сценарии, в которых YDB немного превосходит CockroachDB, другую хорошо известную распределенную SQL СУБД.

Мы начнём с краткого обзора бенчмарка TPC-C. Затем обсудим интересные детали реализации TPC-C для YDB и проведем экскурс в проект Benchbase известной исследовательской группы университета Carnegie Mellon, работающей под руководством профессора Энди Павло. Отдельно остановимся на форке Benchbase от наших коллег из YugabyteDB. Мы предлагаем ряд значительных улучшений этих реализаций, которые существенно сокращают потребление CPU/RAM и время, необходимое для импорта начальных данных TPC-C.

В конце мы представим результаты TPC-C YDB и CockroachDB. Они будут особенно интересны тем, кто интересуется распределенными системами, так как и YDB, и CockroachDB - распределенные СУБД, а TPC-C активно использует распределенные транзакции. Кроме того, эти две СУБД основаны на разных парадигмах. Команда YDB первоначально была вдохновлена идеями Calvin, но с тех пор система значительно эволюционировала, а рассказ об этом заслуживает отдельного поста. Тем временем CockroachDB достигает консенсуса через протокол Raft, подход, который также выбран YugabyteDB, ещё одним наследником Google Spanner. У каждого подхода есть свои сторонники и противники. Так, здесь (часть1, часть2) профессор Даниэль Дж. Абади, один из ключевых авторов протокола Calvin, фокусируется на недостатках Spanner и преимуществах Calvin. YugabyteDB в ответ утверждают, что СУБД на основе Calvin страдают от ограничений транзакционной модели (только NoSQL), имеют высокие задержки (latency) и низкую пропускную способность (throughput). Тем не менее, YDB — это распределенная SQL реляционная база данных. И теперь мы дополним этот преимущественно теоретический спор реальными данными о производительности, полученными на основе наших прогонов TPC-C.

Обзор TPC-C

TPC-C - это стандарт, описывающий методику тестирования производительности без какой-либо официальной реализации. Он был утвержден в 1992 году сразу после исчезновения динозавров и первого релиза Linux, но до появления iPhone, Android, Tesla, SpaceX и даже Windows 95. При этом он по-прежнему чрезвычайно актуален и активно используется. Без сомнения, TPC-C - это «единственная объективная методика оценки производительности OLTP» (цитата CockroachDB).

TPC-C имитирует торговую компанию с настраиваемым числом складов. На каждый склад приходится 10 районов. Один район обслуживает 3 000 клиентов. Клиенты создают заказы, состоящие из нескольких товаров. Компания, конечно же, отслеживает платежи, доставку и историю заказов. Кроме того, проводится инвентаризация запасов товаров.

База компании состоит из следующих таблиц:

  • Warehouse (склад)

  • District (район)

  • Customer (клиент)

  • Order (заказ)

  • New-Order (новый заказ)

  • History (история)

  • Item (товар)

  • Stock (складской запас)

  • Order-Line (строка заказа)

Связи между таблицами и их пропорции показаны на диаграмме ниже, взятой из стандарта.

В каждом районе есть свой терминал, и все транзакции выполняются через эти терминалы. В TPC-C нагрузка состоит из следующих транзакций:

  • NewOrder (новый заказ)

  • Payment (платеж)

  • Order-Status (статус заказа)

  • Delivery (доставка)

  • Stock-level (инвентаризация)

Терминал может выполнять одновременно только одну транзакцию. Транзакция выбирается согласно заданной вероятности: 0,45 для NewOrder, 0,43 для Payment и 0,04 для каждой из Order Status, Delivery и Stock Level. Для каждого типа транзакции заданы свои времена ввода и «осмысления», что ограничивает максимальную пропускную способность терминала.

Согласно стандарту:

«Метрика производительности, представленная в TPC-C, - это "бизнес-производительность", измеряющая количество обработанных заказов в минуту. Используется несколько транзакций для имитации бизнес-активности обработки заказа, и каждая транзакция должна укладываться в заданное время ответа. Метрика производительности выражается в транзакциях-в-минуту-C (tpmC)».

Обратите внимание, что, несмотря на измерение только числа транзакций NewOrder, tpmC является интегральной метрикой, характеризующей выполнение всех транзакций.

Очень разумной идеей в дизайне TPC-C является то, что ограничено максимальное количество транзакций на терминал. Таким образом, чтобы увеличить нагрузку, необходимо увеличить количество складов, что в свою очередь увеличивает количество терминалов. Однако, увеличение количества складов приводит к увеличению размера хранимых данных.

На каждый склад требуется около 85 Мбайт памяти. Максимальное значение tpmC на один склад равно 12.86 tpmC. Реализации TPC-C обычно выводят tpmC и эффективность, рассчитанную на основе полученного и максимально возможного tpmC.

Реализация TPC-C в YDB

Как мы уже отметили выше, официальной реализации TPC-C не существует. Единственная широко известная реализация TPC-C является частью проекта Benchbase. Этот проект поддерживает большое количество бенчмарков и СУБД и спроектирован так, чтобы его можно было легко расширять. Некоторые вендоры добавили свою поддержку в оригинальный Benchbase, другие, такие как YugabyteDB, клонировали его и адаптировали под свои нужды. Изначально мы хотели добавить поддержку YDB в Benchbase, но ниже описываем причины, из-за которых нам тоже пришлось его клонировать и адаптировать.

Benchbase написан на Java 17. Каждый терминал - отдельный поток. Транзакции выполняются как синхронные запросы к базе данных. Большую часть времени терминалы неактивны из-за требований TPC-C. Benchbase эмулирует это требование, усыпляя потоки. Много спящих потоков не проблема, пока их не становится слишком много. К тому же, каждый поток требует стек.

Стандартный размер стека для потоков Java (точнее, ThreadStackSize) составляет 1 Мбайт на x86_64 и 2 Мбайт на aarch64. Напомним, что каждый склад имеет 10 терминалов. Таким образом, если у вас 15 000 складов, вам понадобится 150 000 потоков и приблизительно 150 Гбайт виртуальной памяти. Конечно, размер реального потребления памяти зависит от фактического использования стека и управления страницами памяти. Так, в случае использования hugepages потребление памяти будет огромным. И даже если предположить, что на поток уходит 64 Кбайт, суммарно это целых 10 Гбайт.

Кроме того, Benchbase хранит в памяти информацию о каждой выполненной транзакции, в частности тип транзакции, время начала, время выполнения, идентификатор терминала и идентификатор фазы бенчмарка. На первый взгляд, это всего лишь 9 байт на транзакцию без учета выравнивания данных в памяти. Однако они заранее выделяют 500 000 элементов для каждого потока. Это составляет приблизительно 4 Мбайта на каждый терминал и 40 Мбайт на склад. Для 15 000 складов потребуется около 60 Гбайт уже на запуске. Учитывая, что TPC-C требует как минимум два часа выполнения без разогрева, в итоге соберется значительное количество этих данных.

Из-за этого нам не удалось запустить бенчмарк с более чем 3 000 складами на машине с 512 Гбайт оперативной памяти (около 170 Мбайт на склад, что является чрезмерным)! Мы не одни столкнулись с этой проблемой. Согласно документации YugabyteDB: «для 10 000 складов требуется десять клиентских машин типа c5.4xlarge для проведения тестирования». Инстанс типа c5.4xlarge имеет 32 Гбайт оперативной памяти, так что, в соответствии с этой рекомендацией, понадобится 320 Гбайт памяти, или приблизительно 33 Мбайт на один склад, что близко к нашей оценке. Это делает необходимое количество машин для TPC-C сравнимым с количеством машин для работы самой СУБД. Проведение таких тестов в облаке может стать чрезмерно дорогим.

Мы решили проблему с памятью с помощью следующих несложных изменений:

  1. Не храним полную историю выполнения транзакций, только гистограмму со временем выплнения.

  2. Перешли на Java 20, чтобы использовать виртуальные потоки. Эта фича окончательно реализована в Java 21.

Наши изменения уменьшили требование к памяти до 6 Мбайт на склад. Это заметное улучшение, хотя явно есть место для дальнейшей оптимизации.

Еще одной проблемой, с которой мы столкнулись, было высокое потребление CPU во время импорта начальных данных. Мы выяснили, что причиной этой проблемы был util.RandomGenerator, используемый всеми потоками загрузки. Его замена на ThreadLocalRandom сократила потребление CPU с сотен ядер до всего нескольких. Вот наш pull request в Benchbase.

Несмотря на это улучшение, первоначальный импорт данных занимал много времени. YDB предлагает три метода добавления данных: INSERT, UPSERT и BULK UPSERT. Последний значительно быстрее и предназначен для импорта пользовательских данных. К сожалению, у Benchbase не хватило гибкости для использования BULK UPSERT без изменения его общего кода.

Мы также обнаружили ошибку в реализации транзакции Delivery, дефект, присутствующий как в оригинальном Benchbase (issue), так и в YugabyteDB (issue). После исправления этой ошибки в реализации YDB TPC-C мы заметили снижение tpmC на 4%.

Тестовый стенд

Наш тестовый стенд производительности — это кластер из трех физических машин со следующей конфигурацией каждой:

  • 128 логических ядер: два 32-ядерных процессора Intel Xeon Gold 6338 с частотой 2,00 ГГц с включённым гипертредингом;

  • 4xNVMe Intel-SSDPE2KE032T80;

  • 512 ГБ ОЗУ;

  • скорость передачи данных в сети 50 Гбит/с;

  • включены transparent huge pages;

  • Ubuntu 20.04.3 LTS.

Согласно нашим тестам, Intel-SSDPE2KE032T80 NVMe обладает следующими характеристиками производительности:

Операция

Пропускная способность, МБайт/с

IOPS (в тыс.)

Задержка, мкс

Случайное чтение

2350

588

211

Случайная запись

907

227

556

Чтение

2910

23

2795

Запись

2721

21

2908

Мы тестируем версии и конфигурации СУБД, перечисленные ниже.

YDB 23-3 (1aea989). На одной машине работает 1 узел хранения данных (с привязкой к 32 ядрам CPU) и 6 вычислительных узлов (каждый привязан к своим 16 ядрам). Полную конфигурацию можно найти здесь, а также скрипт установки.

CockroachDB 23.1.10. Для каждого диска мы запускаем один экземпляр CockroachDB в привязке к 32 ядрам, 37,5 Гбайт кэша и 37,5 Гбайт оперативной памяти для SQL. Мы применяем рекомендуемые настройки базы. Здесь представлена конфигурация, используемая нашими установочными скриптами. Отметим, что мы не используем partitioning, который является коммерческой фичей CockroachDB. Мы избегаем этого, так как существуют сценарии, когда partitioning данных, как в TPC-C, невозможен.

К сожалению, из-за этой проблемы в этот раз мы не смогли включить YugabyteDB в наши тесты. Мы надеемся, что они скоро ее устранят, и мы сможем снова провести сравнительное тестирование с их участием.

Прогон TPC-C выполнялся в течение 12 часов на отдельной машине, на которой также было 128 логических ядер и 512 Гбайт оперативной памяти. Для YDB мы использовали наши собственные скрипты benchhelpers. Для CockroachDB мы использовали следующие команды:

./cockroach workload fixtures import tpcc --warehouses=12000 'postgres://root@host:26257?sslmode=disable'

sleep 30m

./cockroach workload run tpcc --warehouses=12000 --ramp=30m --duration=12h `cat ../cockroach_addr`

Результаты

Мы заметили, что при количестве складов свыше 12 000 у CockroachDB часто возникают ошибки:

_elapsed___errors__ops/sec(inst)___ops/sec(cum)__p50(ms)__p95(ms)__p99(ms)_pMax(ms)
 1261.0s        0           36.2           95.2     92.3    125.8   7516.2   7516.2 delivery
 1261.0s        0            3.0          939.2   4563.4   9126.8   9126.8   9126.8 newOrder
 1261.0s        0           43.2           95.5     12.1     26.2     29.4     29.4 orderStatus
 1261.0s        0          314.3          956.6     26.2     35.7    159.4   1140.9 payment
 1261.0s        0           33.1           95.5     28.3     88.1    167.8    167.8 stockLevel
Error: error in delivery: ERROR: inbox communication error: rpc error: code = Canceled desc = context canceled (SQLSTATE 58C01)

и

  108.8s        0            1.0          250.5   4563.4   4563.4   4563.4   4563.4 delivery
  108.8s        0           17.9         2478.8  10200.5  10737.4  10737.4  10737.4 newOrder
  108.8s        0            4.0          254.1   5368.7   5905.6   5905.6   5905.6 orderStatus
  108.8s        0            5.0         2509.0   5368.7  10737.4  10737.4  10737.4 payment
  108.8s        0            3.0          258.1   4563.4   5637.1   5637.1   5637.1 stockLevel
Error: error in delivery: ERROR: rpc error: code = Unavailable desc = error reading from server: read tcp [2a02:6b8:c34:14:0:1354:eb1f:2ca6]:37234->[2a02:6b8:c34:14:0:1354:eb1f:2962]:26258: use of closed network connection (SQLSTATE XXUUU)

В то же время мы наблюдали задержки транзакций до 10 секунд как в выходных данных бенчмарка, так и в метриках. Мы проконсультировались с командой CockroachDB в Slack, и они подтвердили, что эти задержки были вызваны перегрузкой кластера. В итоге, максимальное количество складов, на которых нам удалось запустить бенчмарк с хорошо воспроизводимыми результатами, составило 12 000 (с эффективностью 97,8%).

В случае с YDB мы заметили снижение производительности при увеличении числа складов свыше 13 000. Поэтому результаты, представленные в этом посте, базируются на конфигурации с 13 000 складами (с эффективностью 99,4%).

Вот получившиеся значения tpmC (больше - лучше). YDB опережает CockroachDB на 5,6%.

Ниже представлены перцентили задержки транзакций, измеренные в миллисекундах (чем меньше, тем лучше). Существует разница в точности измерений между реализациями TPC-C в YDB и CockroachDB, но в целом задержки выглядят похожими.

YDB

Транзакция

50%, мс

95%, мс

99%, мс

NewOrder

100

100

100

Payment

50

100

100

Order-Status

10

50

50

Delivery

500

500

1000

Stock-level

10

50

50

CockroachDB

Транзакция

50%, мс

95%, мс

99%, мс

NewOrder

36

105

193

Payment

19

65

117

Order-Status

9

26

50

Delivery

65

184

302

Stock-level

19

44

75

Заключение

В этом посте мы рассмотрели бенчмарк TPC-C, который считается ключевым для оценки производительности онлайн транзакций (OLTP). Мы рассказали о реализации TPC-C для YDB и обсудили некоторые ограничения, найденные в других существующих реализациях, таких как Benchbase.

Согласно нашим результатам нам удалось обойти наших коллег из CockroachDB в данном окружении из трёх «железных» серверов. Однако, это только начало. Существует большое количество других конфигураций, а также регулярно выходят обновления СУБД, которые могут повлиять на результаты. Так что оставайтесь с нами, если хотите быть в курсе следующих серий глубокого анализа и сравнения СУБД «лицом к лицу».

* Результаты не являются официально принятыми TPC результатами и несопоставимы с другими результатами теста TPC-C, опубликованными на сайте TPC.

Комментарии (2)


  1. vlad4kr7
    28.09.2023 15:13

    В то же время мы наблюдали задержки транзакций до 10 секунд

    как часто такое случалось (в час)?

    Стенд у вас конечно мощный! А можно посчитать среднюю tpmС на teraflop кластера?


    1. eivanov Автор
      28.09.2023 15:13

      как часто такое случалось (в час)?

      У CockroachDB во время выполнения TPC-C такое случается всего раз за запуск, после чего бенчмарк завершается ошибкой. Я предполагаю, что если бы бенчмарк не завершился, то получилась бы полочка с задержкой в 10 секунд.

      Стенд у вас конечно мощный! А можно посчитать среднюю tpmС на teraflop кластера?

      У нас процессор Ice lake. Согласно википедии он делает 32 FP64/cycle. Частота, когда включается turbo boost, равна 3.2 GHz. В сервере 2 чипа с 32 физическим ядрами, что дает 192 ядра на кластер. Перемножив, получаем 614.4 GFlops или 0.6144 TFlop.