Привет, хабр! Меня зовут Евгений Иванов, я разработчик 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 - это стандарт, описывающий методику тестирования производительности без какой-либо официальной реализации. Он был утвержден в 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 не существует. Единственная широко известная реализация 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 складов потребуется около 600 Гбайт уже на запуске. Учитывая, что TPC-C требует как минимум два часа выполнения без разогрева, в итоге соберется значительное количество этих данных. Из-за этого нам не удалось запустить бенчмарк с более чем 3 000 складами на машине с 512 Гбайт оперативной памяти (около 170 Мбайт на склад, что является чрезмерным)! Мы не одни столкнулись с этой проблемой. Согласно документации YugabyteDB: «для 10 000 складов требуется десять клиентских машин типа c5.4xlarge для проведения тестирования». Инстанс типа c5.4xlarge имеет 32 Гбайт оперативной памяти, так что, в соответствии с этой рекомендацией, понадобится 320 Гбайт памяти, или приблизительно 33 Мбайт на один склад, что близко к нашей оценке. Это делает необходимое количество машин для TPC-C сравнимым с количеством машин для работы самой СУБД. Проведение таких тестов в облаке может стать чрезмерно дорогим. Мы решили проблему с памятью с помощью следующих несложных изменений: Не храним полную историю выполнения транзакций, только гистограмму со временем выполнения. Перешли на 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 мы использовали следующие команды: Мы заметили, что при количестве складов свыше 12 000 у CockroachDB часто возникают ошибки: и В то же время мы наблюдали задержки транзакций до 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. Разработчик YDB