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

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

На дворе было 10 декабря, руководство поставило задачу сдать отчёт 20 января, до нового года оставалось меньше месяца, а в руках появился знакомый всем инженерам зуд.

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

Железная сторона эксперимента

Итак, раз мы хотим узнать есть ли жизнь в PostgreSQL под гнётом петабайтов, первая задача, которую предстоит решить, это где найти тот самый петабайт свободного места. Цифра значительная, дисков столько в подсобке явно не валяется, поэтому смотрим на рынок и видим что, предположим, относительно быстро и доступно можно прикупить дисков по 15 терабайт, да по 24 диска на два юнита и так три раза, и что-то получается долго ждать поставки и дороговато для разового эксперимента. И мы же помним про наступающий Новый год? Там надо в снежки играть и на санках кататься, а не pgbench гонять.

Но мы люди продвинутые и как известно, для всяких MVP и сезонных нагрузок идеально подходят облака. Нас слишком долго этому учили маркетологи сразу всех облачных провайдеров. Значит, открываем гугл, формируем список облачных провайдеров и отправляем каждому примерно такое ТЗ:

  • Гарантированный петабайт места в виде единого стораджа. Без заигрываний с thin provision, а сразу и честно.

  • Диски должны быть шустрые, поэтому никаких HDD.

  • N машин имеющих к нему доступ. Точное количество не принципиально, но не меньше трёх.

  • От 4 до 16 vCPU, потому что, а как иначе? Не надо ничего супер крутого, это эксперимент, поэтому самые простые Intel Silver и их аналоги нас устроят более чем.

  • Не меньше 64 GiB RAM в каждой машине, а лучше 128 или даже 256, чтобы точно не упираться в память. Благо на фоне всего остального стоит она незначительных денег и на общую цену сильно не влияет.

  • Для каких целей нам это великолепие? Сгенерировать базу, пострелять в неё запросами, замерить всякое, нарисовать графики и пойти над ними думать, чтобы сделать много далекоидущих выводов.

И тут нас ждало удивление номер один. Абсолютно все облачники окуклились на первой же строчке. Ну вот нет у них столько свободного места, даже если начать его специально под нас разгребать. Можно собрать по кускам из разных регионов, но качество результатов такого тестирования, когда у тебя нода в Москве, нода в Саратове, а третья нода на Крайнем  Севере — сами понимаете.

Поэтому облака были отвергнуты и в дело вступила классическая аренда железа. И тут нас ждало точно такое же разочарование: ни в одном ЦОДе не оказалось столько места на быстрых дисках, вот чтобы просто вынь да положь. С одной стороны мы их понимаем, потому что железо должно зарабатывать, а не греть воздух. С другой стороны, а как же иметь горячий запас? Но это всё вопросы философские, которые разбились о суровую реальность. И когда мы уже были на грани того, чтобы забросить нашу прекрасную идею, из одного хорошего ЦОДа нам ответили, что у них есть семь примерно похожих на наш запрос сервера, они готовы набить их дисками и занедорого дать нам попользоваться. Распределённая СУБД Shardman на семь машин звучит интересно, поэтому было принято управленческое решение согласиться.

А что такое Shardman?

Это распределённая СУБД, которую в Postgres Professional разрабатывают уже лет пять. Она основана на идее, что можно взять табличку, партиционировать и раскидать группы партиций по разным серверам, так чтобы было share-nothing. И поверх всего этого единый SQL интерфейс. При этом итоговая система удовлетворяет всем принципам ACID и предназначена для OLTP-нагрузки. А главный интерес для пользователей это возможность использовать любой сервер как точку доступа к данным.

А между тем, пока мы искали и нашли, до нового года осталось всего две недели.

Бенчмарк

Ну хорошо, с железом мы разобрались. А что с линейкой которой проводить измерения? Времени мало, задача построить космический звездолёт у нас не стоит, поэтому давайте возьмём готовый популярный бенчмарк, зальём с его помощью данные, погоняем запросики и пойдём резать салаты к новогоднему столу. 

Из готового и заслуженного под руки попался YCSB (Yahoo! Cloud Serving Benchmark), а если точнее, то его реализацию на go от pingcap. Если вы такого не знаете, то вот что просила передать бригада. Это написанный в 2010 году NoSQL бенчмарк ребятами из Yahoo. В нём покрыт самый минимум простейших операций, а то что он NoSQL нас не смущает ибо там реализована классическая key-value модель. Всё просто отлично: никаких заумных джойнов, распределённых транзакций и сложных запросов. 

Бенчмарк создает и наполняет данными всего одну табличку userdata, а также поддерживает партиционирование. То есть мы можем взять одну табличку, нарезать на партиции по ключу и просто залить её в шардман без предварительной адаптации. И самое главное, ключ там очень простой и понятный: ycsb_key = ‘user’ + hash(seq) , плюс десять колонок типа varchar(100) со случайными данными. Всего четыре буквы и какой-то хэш от числа. Остальные колонки — абсолютно случайны, то есть нам не надо заморачиваться на бизнес-логику, валидации и так далее. Реальность нас ещё щёлкнет по носу за такое вольнодумство, но это чуть позже.

Состав полей самой таблички:

 Partitioned table "public.usertable"
  Column  |      	Type      	  | Collation | Nullable  | Default
----------+------------------------+-----------+----------+---------
 ycsb_key | character varying(64)  |       	  | not null |
 field0   | character varying(100) |       	  |      	 |
 field1   | character varying(100) |       	  |      	 | 
 field2   | character varying(100) |       	  |      	 |
 field3   | character varying(100) |       	  |      	 |
 field4   | character varying(100) |       	  |      	 |
 field5   | character varying(100) |       	  |      	 |
 field6   | character varying(100) |       	  |      	 |
 field7   | character varying(100) |       	  |      	 |
 field8   | character varying(100) |       	  |      	 |
 field9   | character varying(100) |       	  |      	 |
Partition key: HASH (ycsb_key)
Indexes:
	"usertable_pkey" PRIMARY KEY, btree (ycsb_key)

А пока немного математики. Одна такая строчка в таблице это примерно 1100 байт. Значит таких строчек нам надо залить в базу примерно триллион(это когда в числе 13 цифр). Времени у нас две недели, значит надо выдавать около гигабайта в секунду и тогда даже небольшой запас останется. Выглядит как абсолютно реальная задача и современные диски вполне так умеют. 

Теперь от теории переходим к практике. Помимо замеров с помощью бенчмарка go-ycsb было решено написать собственные тесты для нашего инструмента pg_microbench, который использует стандартные java-библиотеки для работы с многозадачностью и для взаимодействия с базами данных через JDBC. 

Мы разработали два теста: YahooSimpleSelect и YahooSimpleUpdate. Оба выполнены так, что перед их запуском можно указать к какому шарду следует подключаться и из какого шарда читать, для того чтобы точно быть уверенными кто и откуда читает. Для каждого сгенерированного ycsb_key вычисляется к какому шарду он принадлежит, чтобы выполнять запросы только для него.

Алгоритм тестирования мы заложили довольно стандартный: 

  • Собираем статистику для сегментированных и глобальных таблиц с помощью функции shardman.global_analyze().

  • Запускаем warm-up тест длительностью 10 минут для выбранного профиля нагрузки 

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

Всё, план-капкан готов, начинаем буквально вот уже сейчас и где там уже наши салатики.

10 декабря 2024. Суровая реальность

Пока нам собирали сервера, пробежала светлая мысль: «А давайте мы пока в своей лаборатории запустим этот бенчмарк и хоть посмотрим что он там выдаёт на самом деле!» Собрали десяток виртуалочек, выдали им по 8 ядер, запустили всё и ушли на обед. Спустя полчаса посмотрели на результат и холодный пот пробежал по спине:50 GB за тридцать минут суммарно на всех нодах это прям совсем не то что мы ожидали увидеть. Где наши много-много гигабайт в секунду? А самое интересное всплыло, когда оказалось, что генератор вообще перестал записывать новые данные. Пришлось открывать его код и править таким образом, чтобы он заливал данные сразу в нужный нам шард, заранее определяя нужный узел по ключу шардирования. Но почему всё так медленно?

Итак, что там было внутри? Имеем классический цикл идущий от нуля до бесконечности

for (long seq = 0; seq < X; seq++) {
    ycbs_key = ‘user’ + fnv1a(seq)
    ...
}

Кажется всё правильно. Генерится ключ, по нему вставляются данные, но где тогда результат? Выяснилось что ключик не простой, а натурально золотой. Как видно выше, он добавляет к юзеру некий fnv1a хэш от некоторого сиквенса. Что это за чудо такое никто не знал, поэтому продолжили разбираться.

Оказалось, что мы словили самую банальную из возможных проблем — коллизию хэшей. То есть, алгоритм генерил ключи, которые со временем стали повторяться и генерация начала делать пустые циклы. Поэтому, в какой-то момент коллизии пошли настолько большими пачками, что генерация фактически остановилась.

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

На дворе было 20 декабря. До дедлайна 31 день.

Всем очевидно, что в январе хочется заниматься совершенно не тестированием PostgreSQL, поэтому поступаем радикально: выкидываем хэш-функцию и генерируем по рабоче-крестьянски последовательно. Взяли в руки Java, сделали батчинг 100 строк за 1 SQL-транзакцию, быструю node-wise заливку на нужные ноды и избавились от лишней генерации рандома, который нам был абсолютно не нужен. Просто генерируем миллион строчек и забираем в батч произвольные. Сбоку сделали ещё мониторинг, потому что в оригинальном бенчмарке не показывался прогресс генерации. И ради чувства прекрасного добавили логирование процесса загрузки в отдельную табличку. В неё скидывали данные каждые пять минут. 

Запустили всё на тех же вируталочках, сходили ещё раз на обед — успех! Генератор выдавал примерно 150Гб за 5 минут, что на этих машинках было полным успехом, поэтому давайте уже переходить на упражнения с железом.

27 декабря. Нам дали сервера. До дедлайна 24 дня.

Время шинковать салатики, а мы пошли накатывать Debian 12 и собирать RAID 0 из доступных дисков. Просто кладём всё в кучу: 10 дисков по 15 терабайт, итого 140 Тб полезного места на сервер, умножаем на 7, получаем ну почти целый петабайт. Годится.

Накатываем на ОСь, Shardman, ставим мониторинг, добавляем всякого полезного обвеса(pgpro-otel-collector, pgpro_stats, pgpro_pwr), обмазываем всё метриками. Из интересного можно отметить параметр num_parts, отвечающий за количество секций, на которые будут разделены распределённые таблицы. Мы его установили на 70. Таким образом, на каждом из узлов shardman хранилось одинаковое число секций (10), а также мы избегали риска превысить максимально возможный размер секции равный 32TB. PGDATA размещаем прямо в точке монтирования рейда, во избежание всякого. Как выяснится чуть позже, всякое всё же случилось, так что размещать PGDATA в точке монтирования может немного подпортить малину.

Поскольку shardman распределённая база данных, то как многие базы такого типа чувствителен к расхождениям времени, на ноды кластера был установлен демон chrony для автоматической синхронизации с серверами NTP. Вспоминаем добрым словом Омниссию и запускаем генератор на каждом узле кластера. 

Перед отходом из офиса окинули взглядом что получается. Скорость загрузки составила ~150 ГБ за 10 минут на каждой из нод. Правда оказалось, что шестой сервер прям какой-то чемпион и сгенерил данных чуть-чуть больше остальных. Но больше это же не меньше, значит хорошо. Поэтому офис был закрыт на ключ, все разъехались по домам с целью забыться счастливым мандариновым сном. Казалось бы, что может пойти не так, когда у нас такой многообещающий старт.

  • planck-1: 629 GB

  • planck-2: 632 GB

  • planck-3: 620 GB

  • planck-4: 632 GB

  • planck-5: 631 GB

  • planck-6: 975 GB

  • planck-7: 626 GB

28 декабря 2024. Не деплойте в пятницу. До дедлайна 23 дня.

А вот утром субботы нас ждал сюрприз, потому что картина стала прямо противоположной: две ноды здорово отстали от остальных. Как видим, на каждой ноде сгенерировалось за ночь по 11 теров, а на третьей и шестой всего лишь по 4.

  • planck-1: 11T

  • planck-2: 11T

  • planck-3: 3.5T

  • planck-4: 11T

  • planck-5: 11T

  • planck-6: 4.1T

  • planck-7: 11T

Беда-беда-огорчение, но времени грустить нет, поэтому срочно лезем под капот и с помощью iostat видим повышенную утилизацию программного рейда и что диски начинают крайне медленно выполнять операции ввода-вывода. По iostat-у время записи на nvme диски сильно отличалось от других серверов: 5мс (на серверах 3 и 6) против 0.15мс (на других серверах).

А на дворе оказывается суббота. Хочется чего угодно, но только не этого. А там ещё и корпоративы и всё такое. А тут тебе по 10 миллисекунд на ответ диска. Выглядит очень странно, но делать нечего, бежим в техподдержку, которая, хвала ей, буквально через десять минут активно включилась в решение проблемы. SMART сказал что всё хорошо. Прошивки на всех дисках одинаковые. Перезагрузили, сравнили версии и настройки биосов — всё сходится. В качестве экстренной меры меняются кабеля подключения NVME-дисков — помогает(10 мс превращаются в 1 мс), но ненадолго. Да и 1мс это не 0,1 мс как на других машинах.

Далее было долгое обсуждение, которое так ни к чему и не привело, кроме версии, что проблему с массивом вызывает один сбойный диск. Так как рейд у нас софтовый, скорость его работы равна скорости работы самого медленного члена рейда. Но менять все диски и начинать заново — не вариант, а делать что-то надо, поэтому применяем элегантнейший ход конём — разбираем рейды на проблемных машинах обратно на отдельные диски. На каждом создаём табличное пространство и будем писать сразу туда. Вполне хорошее решение. Как показалось тогда в моменте.

Но беда пришла откуда не ждали: оказывается в Shardman нельзя создавать локальные table space. Если ты его делаешь, то будь добр повторить это на всех серверах. Так-то звучит оно логично ибо это же распределённая СУБД, но у нас тут уже свой сетап и переделывать его с нуля ой как не хочется.

postgres=# CREATE TABLESPACE u02_01 LOCATION '/u02_01';
ERROR: local tablespaces are not supported
HINT: use "global" option to create global tablespace

Поэтому применяется любимый трюк наших родителей: «Сейчас ты узнаешь что так можно, но сам никогда так не делай» Мы отключили синхронизацию изменений схемы.

postgres=# set shardman.sync_schema = off;
SET

postgres=# show shardman.sync_schema;
 shardman.sync_schema 
----------------------
 off
(1 row)

postgres=# CREATE TABLESPACE u02_01 LOCATION '/u02_01';
ERROR: tablespace location template doesn't specify all necessary substituions
HINT: expected to see rgid word in location template

Таким образом наш Shardman становится локальным и всё должно заработать, но и тут засада: в названии тейбл спейса нет номера сервера, поэтому ничего я вам делать не буду. Но, как известно, на каждую хитрую резьбу можно найти свой болт, поэтому мы просто создали папочку с номером, который был воспринят как номер сервера.

mkdir /u02_01/3
mkdir /u02_02/3
postgres=# CREATE TABLESPACE u02_02 LOCATION '/u02_02/{rgid}';
CREATE TABLESPACE
postgres=# CREATE TABLESPACE u02_03 LOCATION '/u02_03/{rgid}';
CREATE TABLESPACE

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

ALTER TABLE usertable_2 SET TABLESPACE u02_01;
ALTER TABLE usertable_9 SET TABLESPACE u02_02;
ALTER TABLE usertable_16 SET TABLESPACE u02_03;

Все выдохнули, на дворе уже было 30 декабря. Мы пошли отмечать, настроив всё так, чтобы генерация шла оставив везде по полтерабайта. Зачем? Если диски кончатся, то тесты мы запустить не сможем и всё насмарку. Вот на такой прекрасной ноте все разошлись ближе к семьям и салатикам.

3 января 2025. Никогда не торопись. До дедлайна 17 дней.

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

alter index usertable_12_pkey set tablespace u02_01;
alter index usertable_19_pkey set tablespace u02_02;

Поменяли tablespace’ы индексов везде, где требовалось, и продолжили генерацию.

9 января. Так и что там? А то до дедлайна 11 дней.

После ударного празднования Нового года, часть команды слегла с вирусами, часть уехала кататься в горы, часть погрузилась в другие проекты, а самые странные люди просто отдыхали от работы. Однако 9 числа один очень нетерпеливый всё же решил заглянуть в мониторинг, а там кругом сплошная авария: ras-демон ругается на битые планки памяти. Абсолютно все планки на всех серверах, кроме седьмого, показывались как требующие срочной замены.

2025-01-09T16:58:52.754082+03:00 planck-3 kernel: [1021273.775923] mce: [Hardware Error]: Machine check events logged
2025-01-09T16:58:52.754511+03:00 planck-3 kernel: [1021273.776582] EDAC skx MC5: HANDLING MCE MEMORY ERROR
2025-01-09T16:58:52.754515+03:00 planck-3 kernel: [1021273.776585] EDAC skx MC5: CPU 16: Machine Check Event: 0x0 Bank 17: 0x8c00008200800090
2025-01-09T16:58:52.754517+03:00 planck-3 kernel: [1021273.776591] EDAC skx MC5: TSC 0x8b09ed5a153c5
2025-01-09T16:58:52.754520+03:00 planck-3 kernel: [1021273.776594] EDAC skx MC5: ADDR 0x306533e800
2025-01-09T16:58:52.754523+03:00 planck-3 kernel: [1021273.776597] EDAC skx MC5: MISC 0x9018c07f2898486
2025-01-09T16:58:52.754527+03:00 planck-3 kernel: [1021273.776600] EDAC skx MC5: PROCESSOR 0:0x606a6 TIME 1736431132 SOCKET 1 APIC 0x40
2025-01-09T16:58:52.754530+03:00 planck-3 kernel: [1021273.776617] EDAC MC5: 2 CE memory read error on CPU_SrcID#1_MC#1_Chan#0_DIMM#0 (channel:0 slot:0 page:0x306533e offset:0x800 grain:32 syndrome:0x0 —  err_code:0x0080:0x0090 SystemAddress:0x306533e800 ProcessorSocketId:0x1 MemoryControllerId:0x1 ChannelAddress:0x3f94cf800 ChannelId:0x0 RankAddress:0x1fca67800 PhysicalRankId:0x1 DimmSlotId:0x0 Row:0xfe51 Column:0x308 Bank:0x3 BankGroup:0x0 ChipSelect:0x1 ChipId:0x0)

Снова бежим в техподдержку, те включаются, всё быстро проверяют, что-то там меняют и улучшают. Мы ждём — и через несколько часов оно снова работает. Железо, конечно, так себе нам досталось, но оперативность ребят из поддержки моё почтение.

10 января 2025. Тесты и до дедлайна 10 дней.

В этот момент было решено, что давайте погоняем хоть какие-то тесты и перейдём от теории к практике. Помимо замеров с помощью бенчмарка go-ycsb мы всё также были настроены использовать наш pg_microbench, ради его многозадачности и умения читать данные с конкретного сервера через заданный шард.

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

Результатом наших замеров стал тест получивший название YahooSimpleSelect и вот такая матрица, содержащая достигнутый TPS при одинаковом количестве воркеров (20):

          | planck-1 | planck-2 | planck-3 | planck-4 | planck-5 | planck-6 | planck-7
---------------------------------------------------------------------------------------
planck-1  | 15805    | 710      | 764      | 649      | 751      | 734      | 242
planck-2  | 741      | 10987    | 725      | 629      | 677      | 697      | 229
planck-3  | 984      | 905      | 23673    | 825      | 914      | 931      | 333
planck-4  | 779      | 634      | 670      | 10220    | 623      | 633      | 214
planck-5  | 771      | 712      | 764      | 628      | 7830     | 695      | —
planck-6  | 810      | 751      | 802      | 654      | 693      | 37807    | 219
planck-7  | 223      | 236      | 219      | 206      | 210      | 330      | 1663

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

Очевидно, что по диагонали значения получились самые лучшие, тут никаких новостей. Но зато серверы 3 и 6, на которые мы жаловались больше всего, оказались очень даже быстрыми относительно остальных. Седьмой сервер, где не было алёртов на оперативную память, показал аномально низкие результаты. Причина оказалась в неправильно выставленном параметре stripe, о котором и поговорим прямо сейчас.

Вот эта «пила» утилизации CPU на седьмой машине во время теста крайне нас насторожила. По всем понятиям тут должна быть ровненькая линия, потому что нагрузку мы подаём ровную. Начали разбираться и по классике запустили джентльменский набор — Perf и FlameGraph. Правда, сразу проблема обнаружена не была, как мы надеялись, поэтому пришлось копать глубже.

perf record -F 99 -a -g --call-graph=dwarf sleep 30
perf script --header --fields comm,pid,tid,time,event,ip,sym,dso 

Если его несколько увеличить, то вылезает проблема в одной из ext4 функций — ext4_mb_good_group. Лезем в гугл и выясняем, что надо проверить параметр stripe, который скорее всего отличается от дефолтного нолика. То есть это полноценный баг, который был пофикшен. Убеждаемся что рейды наши создались со stripe=1280 и хотя установка его в 0 лишь временный костыль, это лучше чем ничего. Так что раз говорят надо «поставить нолик», то ставим нолик, делаем remount, перезапускаем тесты — и вуаля!

Тесты летают, утилизация близка к 100%. Давайте теперь снова запустим генератор, ещё немного отдохнём и добьём петабайт.

15 января. Продолжаем делать тесты. До дедлайна 5 дней.

Эта дата известна как день рождения одного хорошего человека и дата окончания генерации на всех серверах, чтобы мы успели провести все запланированные тесты. Скажу сразу — достичь заветного петабайта так и не получилось. Мы остановились на цифре 863 терабайта, что эквивалентно где-то 800 миллиардам строк. Но если кажется что маловато будет, недожали и надо было просто ещё пару дней подождать, то не забывайте, что реально занятое на дисках место будет всегда больше, нежели полезный объём данных. То есть диски уже были забиты практически под завязку.

Узел

\l+ postgres

Кол-во загруженных строк по данным monitoring_insert

planck-1

126 TB

799447789415

planck-2

107 TB

799506350001

planck-3

107 TB

799998897115

planck-4

126 TB

799586514368

planck-5

126 TB

799559145747

planck-6

126 TB

799857655389

planck-7

126 TB

799346369872

Итого: 863 TB

max: 799998897115

И что же мы обнаружили на серверах? А обнаружили мы там любимый всеми DBA автовакуум. Все кто использует PostgreSQL, прекрасно знают что после заливки большого объёма данных просыпается мафия автовакуум и пытается вычитать все свалившиеся на него терабайты данных.

  pid    | duration            | wait_event             | mode       | database | table          | phase         | table_size | total_size | scanned | scanned_pct | vacuumed | vacuumed_pct 
---------+---------------------+------------------------+------------+----------+----------------+---------------+------------+-------------+---------+--------------+-----------+---------------
1957485 | 1 day 01:41:41.759642 | Timeout.VacuumDelay   | wraparound | postgres | usertable_0    | scanning heap | 9309 GB    | 13 TB       | 3210 GB | 34.5         | 0 bytes   | 0.0
1957487 | 1 day 01:41:40.691321 | LWLock.WALWrite       | wraparound | postgres | usertable_7    | scanning heap | 9266 GB    | 13 TB       | 3230 GB | 34.9         | 0 bytes   | 0.0
1957491 | 1 day 01:41:39.662693 | IO.WALWrite           | wraparound | postgres | usertable_56   | scanning heap | 9266 GB    | 13 TB       | 3553 GB | 38.3         | 0 bytes   | 0.0
1957504 | 1 day 01:41:38.557464 | LWLock.WALWrite       | wraparound | postgres | usertable_63   | scanning heap | 9266 GB    | 13 TB       | 3562 GB | 38.4         | 0 bytes   | 0.0
2207587 | 10:57:22.773513       | Timeout.VacuumDelay   | regular    | postgres | usertable_21   | scanning heap | 11 TB      | 13 TB       | 9856 GB | 88.6         | 0 bytes   | 0.0
2207588 | 10:57:21.801343       | LWLock.WALWrite       | regular    | postgres | usertable_35   | scanning heap | 11 TB      | 13 TB       | 9855 GB | 88.6         | 0 bytes   | 0.0
2207593 | 10:57:20.791863       | Timeout.VacuumDelay   | regular    | postgres | usertable_14   | scanning heap | 11 TB      | 13 TB       | 9856 GB | 88.6         | 0 bytes   | 0.0
2207606 | 10:57:19.790292       | Timeout.VacuumDelay   | regular    | postgres | usertable_28   | scanning heap | 11 TB      | 13 TB       | 9856 GB | 88.6         | 0 bytes   | 0.0

За сутки этот красавец смог осилить по 4 терабайта из 14 на каждой партиции, а всё остальное висит со статусом VacuumDelay. Затея, конечно, отличная, но нам сервера сдавать скоро, да и начальство уже ждёт отчёта о потраченных деньгах. Поэтому выключаем VacuumDelay и видим что побежало быстрее, но не так быстро чтобы нам всё успеть.

 pid     | duration       | wait_event         | mode       | database | table         | phase         | table_size | total_size | scanned | scanned_pct | vacuumed | vacuumed_pct 
---------+----------------+--------------------+------------+----------+---------------+---------------+------------+-------------+---------+--------------+-----------+---------------
2429208 | 00:42:25.247109 | LWLock.WALWrite    | wraparound | postgres | usertable_5   | scanning heap | 12 TB      | 13 TB       | 452 GB  | 3.7          | 0 bytes   | 0.0
2429213 | 00:42:24.36491  | LWLock.WALWrite    | wraparound | postgres | usertable_12  | scanning heap | 12 TB      | 13 TB       | 778 GB  | 6.3          | 0 bytes   | 0.0
2429216 | 00:42:23.352458 | LWLock.WALWrite    | wraparound | postgres | usertable_19  | scanning heap | 12 TB      | 13 TB       | 451 GB  | 3.7          | 0 bytes   | 0.0
2429231 | 00:42:22.34729  | LWLock.WALWrite    | wraparound | postgres | usertable_33  | scanning heap | 12 TB      | 13 TB       | 452 GB  | 3.7          | 0 bytes   | 0.0
2429233 | 00:42:21.352138 | LWLock.WALWrite    | wraparound | postgres | usertable_26  | scanning heap | 12 TB      | 13 TB       | 451 GB  | 3.7          | 0 bytes   | 0.0
2429238 | 00:42:20.35141  | LWLock.WALWrite    | wraparound | postgres | usertable_40  | scanning heap | 12 TB      | 13 TB       | 451 GB  | 3.6          | 0 bytes   | 0.0
2429240 | 00:42:19.350775 | IO.WALSync         | wraparound | postgres | usertable_47  | scanning heap | 12 TB      | 13 TB       | 450 GB  | 3.6          | 0 bytes   | 0.0
2429242 | 00:42:18.349118 | LWLock.WALWrite    | wraparound | postgres | usertable_54  | scanning heap | 12 TB      | 13 TB       | 450 GB  | 3.6          | 0 bytes   | 0.0
2429246 | 00:42:17.336848 | LWLock.WALWrite    | wraparound | postgres | usertable_61  | scanning heap | 12 TB      | 13 TB       | 78 GB   | 0.6          | 0 bytes   | 0.0
2429259 | 00:42:16.347052 | LWLock.WALWrite    | wraparound | postgres | usertable_68  | scanning heap | 12 TB      | 13 TB       | 78 GB   | 0.6          | 0 bytes   | 0.0

(10 rows)

Смотрим почему так произошло, и понимаем что теперь упёрлось в WAL. Он у нас получился большой, красивый и на каждой ноде свой WAL. Экспериментировать некогда, время поджимает, поэтому выносим WAL на tmpfs на время работы автовакуума.

pid     | duration        | wait_event          | mode            | database | table        | phase         | table_size | total_size | scanned | scanned_pct | vacuumed | vacuumed_pct
--------|-----------------|---------------------|-----------------|----------|--------------|---------------|------------|------------|---------|-------------|----------|------------
2500393 | 00:02:33.715813 | LWLock:WALWrite     | wraparound      | postgres | usertable_5  | scanning heap | 12 TB      | 13 TB      | 816 GB  | 6.6         | 0 bytes  | 0.0
2500397 | 00:02:32.790738 | IO:DataFileWrite    | wraparound      | postgres | usertable_12 | scanning heap | 12 TB      | 13 TB      | 1141 GB | 9.2         | 0 bytes  | 0.0
2500398 | 00:02:31.788569 | LWLock:WALWrite     | wraparound      | postgres | usertable_19 | scanning heap | 12 TB      | 13 TB      | 813 GB  | 6.6         | 0 bytes  | 0.0
2500407 | 00:02:30.788247 |                     | wraparound      | postgres | usertable_33 | scanning heap | 12 TB      | 13 TB      | 814 GB  | 6.6         | 0 bytes  | 0.0
2500408 | 00:02:29.788889 | LWLock:WALWrite     | wraparound      | postgres | usertable_26 | scanning heap | 12 TB      | 13 TB      | 813 GB  | 6.6         | 0 bytes  | 0.0
2500422 | 00:02:28.786883 |                     | wraparound      | postgres | usertable_40 | scanning heap | 12 TB      | 13 TB      | 812 GB  | 6.5         | 0 bytes  | 0.0
2500426 | 00:02:27.785951 | IO:WALWrite         | wraparound      | postgres | usertable_47 | scanning heap | 12 TB      | 13 TB      | 811 GB  | 6.6         | 0 bytes  | 0.0
2500427 | 00:02:26.785828 |                     | wraparound      | postgres | usertable_54 | scanning heap | 12 TB      | 13 TB      | 811 GB  | 6.6         | 0 bytes  | 0.0
2500432 | 00:02:25.785364 | IO:DataFileRead     | wraparound      | postgres | usertable_61 | scanning heap | 12 TB      | 13 TB      | 440 GB  | 3.6         | 0 bytes  | 0.0
2500438 | 00:02:24.732249 |                     | wraparound      | postgres | usertable_68 | scanning heap | 12 TB      | 13 TB      | 438 GB  | 3.6         | 0 bytes  | 0.0
(10 rows)

Это помогло и за 20 часов случилось всё, что должно было случится. Ваккум провакуумил, фризы профризились, данные показали готовность к тестам и настроение наше улучшилось.

17 января. Рисуем графики оставшиеся три дня до дедлайна.

Графики штука хорошая. Они могут быть абсолютно бессмысленными, если их не сравнивать между собой, но всегда остаются очень красивыми.

Сначала мы решили перепроверить наш тест YahooSimpleSelect, в котором имеем возможность указать, к какому шарду нужно подключаться и из какого шарда вычитывать данные, на этот раз решив сделать тесты под разным количеством потоков и ограничившись только проверкой «диагоналей»: когда для подключения и чтения использовалась одна и та же нода.

Выводы тут каждый может сделать какие угодно, но то что третий сервер, на который мы больше всего ругались, тут показывает самый большой TPS, — непреложный факт и абсолютная загадка. Мы действительно не поняли, как так вышло, но он был самым быстрым на чтение, а шестой на запись. И можно отметить, что после 80 воркеров производительность заметно не менялась, а утилизация процессора не превышала 60%.

Аналогичный тест YahooSimpleUpdate: запись на planck-6 неожиданно показала вдвое больший TPS, чем на остальные ноды. Странно всё это, и будем думать что просто какая-то аномалия. Хотя по-честному купив сервера увидеть на них такой разбег совершенно не хотелось. Всё же от одинакового железа ждёшь одинаковых показателей.

Переходим к самому интересному — к тестам самого go-ycsb. Первым делом запустили банальный read only тест и опять увидели разбег по однородности нагрузки.

Диаграмма
Диаграмма

Но на других тестах, по нашим замерам, получилось, что масштабируется такое решение всё же достаточно линейно. Мы увидели рост TPS до полутора тысяч воркеров и это на самых простеньких Intel Silver на 16 ядер.

Мы проверяли различные схемы работы и везде было плюс-минус одинаково.

Кроме одной схемы, которую мы теперь активно изучаем — при схеме 50/50 происходит очевидная деградация.

Но с этим нам ещё только предстоит разобраться.

Итоги, они же достижения.

 Все любят цифры, поэтому давайте сразу с них:

  • 1200 сообщений во внутреннем чатике этого проекта;

  • 23 часа овертайма в пред и новогодние праздники. В обычное время мы такое решительно осуждаем, но тут был азарт и адреналин, поэтому всё сугубо добровольно;

  • много сбойных планок памяти. Конкретное число утеряно, но памяти пожгли от души;

  • один сгоревший вентилятор в сервере. В статье он не упоминался, но честно гонял воздух пока не испустил дух;

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

Чтобы мы сделали, если бы вписались в эту авантюру ещё раз? В первую очередь взяли бы запас. Запас по времени и дискам. Мы даже просили добавить нам места, но оказалось что использовалась одноюнитовая платформа на 10 дисков и ещё один вставлять просто некуда.

Следом бенчмарк. Абсолютное их большинство заточено под быстрые замеры на маленьком объёме данных. Если хочется тестировать петабайты, нужен бенчмарк, логика которого учитывает такие объёмы.

Аномалии с железом. Что происходило с третьим и шестым серверами так никто и не понял. Можно во всём винить Шрёдингера, квантовые аномалии и космическое излучение, но в такие моменты очень не хватает человека глубоко погружённого в тематику.

Из нового узнали две важные вещи. Во-первых, RAS-демон это действительно круто и без него теперь никуда. И следим за взаимопониманием между ext4 и RAID. Далее мог бы идти большой список всего про Shardman, но это будет не интересно читателю, не погружённому в его разработку.

Окупилось-ли всё это? Мы считаем что да. Знания бесценны, а стоил этот эксперимент вполне конечную сумму денег. В сухом остатке мы стали знать гораздо больше, научились делать новые классные штуки и знаем как подступиться к новому классу задач.

А ещё на github мы выложили нашу утилиту для загрузки данных в Shardman. Вдруг вам захочется повторить наш прекрасный эксперимент.

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


  1. onets
    14.05.2025 14:15

    Мда, 3 и 6 сервера отожгли)


    1. vanxant
      14.05.2025 14:15

      причём чтение ещё можно объяснить например настройками/таймингами памяти (её ж меняли), но запись-то чисто в диски упирается...


      1. BeLord
        14.05.2025 14:15

        Навскидку не только в диски упирается-) В случае софтрейда контроллер шины PCI-е+контроллер дисков+ контроллер самого диска, а также их взаимодействие друг с другом, в случае хард рейда еще добавляется контроллер хардрейда. Это если рассматривать цепочку с середины цикла записи/чтения в рамках одного сервера. SMART для анализа не сильно поможет, он покажет только битый/не битый диск, если грубо, поведение контроллера конкретного диска при больших нагрузках он не покажет. Что происходит на уровне шины PCI-e тоже науке не известно. Это очень грубо на уровне железа, считаем что софт идеальный и багов в нем нет. Как показывает практика с серверами Supermicro 10 одинаковых серверов, вот реально все потроха одной ревизии, отличаются только неделями выпуска, ведут себя по разному при больших нагрузках.


  1. JumboH
    14.05.2025 14:15

    Большое спасибо за статью,с интересом прочитали с коллегами.

    Не могли бы прокомментировать этот момент:

    >> и вот такая матрица, содержащая достигнутый TPS при одинаковом количестве воркеров (20):

    Производительность кросс-серверных обращений очевидным образом на полтора порядка ниже, но не является ли это следствием малого количества воркеров?

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

    Шардман теоретически рассматривали как "поставить и не думать о масштабировании / реализации своего шардирования", эта табличка намекает, что "не думать" не получится...


    1. mizhka Автор
      14.05.2025 14:15

      Вот это ещё одна загадка, до которой мы не успели докапаться на том оборудовании. В целом на других тестах мы не наблюдали такой большой разницы.

      Более того, в случаи с межсерверным обращением, утилизация процессора увеличивается на промежуточном сервере на 50%, что тоже неожиданно. По диску разницы не вижу, там одинаковый набор читался. И явно по сети не вижу проблем.

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


  1. FlashHaos
    14.05.2025 14:15

    Я почему-то oracle supercluster и exadata вспомнил, аж вспотел. Они, конечно работали, особенно если zdlra поставить. Цена сопоставима с годовым бюджетом некрупной страны.

    Очень интересно, какой у этого кошмара промышленный юзкейс? Наверное, какая-то аналитика, ни для чего другого в здравом уме петабайт в одну базу лить никто не будет. А логически разбить на базы никак нельзя? Ведь в проде это не будет работать все равно - это невозможно забекапить, здесь никогда не будут работать стандартные процедуры работы. Это уникальное решение не вполне понятно для чего.


    1. Vytian
      14.05.2025 14:15

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

      Ну или любой современный радиотелескоп 0.5-1 TB/s генерит, тупо мультиспектральные картинки 2^15 на 2^16 размером, но очень , очень много штук. Проект Aeneas по созданию общей базы данных звездного неба в радио диапазоне был рассчитан на 8.5 экзабайт на 15 лет в постоянном хранении, уж не знаю, сколько они там собрали в итоге, но это вполне рутинная задача.


      1. FlashHaos
        14.05.2025 14:15

        Я понимаю, откуда эти данные могут взяться. Сам бекапил по 4-6 петабайт каждый день, когда в сбере работал. Но это же не все в одной базе лежит, нет никакого смысла это класть в одну базу, даже если данные однородные.

        Я уверен, что тот же церн не в базу сырые данные пишет по петабайту в день.


        1. Vytian
          14.05.2025 14:15

          В несколько баз поменьше. И часть сращу в Будапешт, что ли стримит, там у них второй кластер для обработки (вроде на общей T1 магистрали сидят). А в радиотелескопах в том и соль, что иногда надо интегральную картинку собрать со в всех источников. но не сразу за полгода, конечно, и не регулярно. Но вроде проект на 3 миллиона был как раз про seamless on-demand access. Ну чтоб любой тупой студент мог вбить временное окошко, частотный диапазон, и через 10 секунд на рабочем столе картинка.


          1. FlashHaos
            14.05.2025 14:15

            Вопрос про применение петабайтной реляционной базы остается открытым.


    1. mizhka Автор
      14.05.2025 14:15

      Такие OLTP базы есть, в основном это высоконагруженные системы, где данные прилетают массово. К примеру платежи, посылки, realtime rating и charging и прочие. Во многих таких системах важно хранить данные за долгие годы.

      По некоторой инфе есть в РФ инстансы на GreenPlum-е с 6 петабайтами данных (не знаю что за природа базы, но это даже не self-hosted, а на арендованных серверах).

      Можно конечно пилить на несколько баз, но зачем - это только усложняет доступы. Надо менеджить уже не одну единицу, а несколько.

      Бекапы делаются инкрементальные, конечно не надо каждый час и день делать полную копию. Раз в месяц сделать полный бекап за сутки возможно, а дальше только собирать инкременты. Это рабочая стратегия.

      Суть простая - такие базы существуют, их мало, но они есть.


  1. danolivo
    14.05.2025 14:15

    Судя по флеймграфу, 15% занимает GetCachedPlan. Что-то многовато: может таки generic-планы стоило использовать?


    1. mizhka Автор
      14.05.2025 14:15

      Тогда fine tuning нам даже и не пришло в голову делать - задача не стояла так. Надо бы конечно покрутить планами, со стороны утилиты мы точно используем расширенный протокол. Скорее всего generic слишком дорогой, вот он и постоянно кастомы строит. Можно попробовать конечно force_generic. Думаешь стоит?


      1. danolivo
        14.05.2025 14:15

        Полагаю, что можно смотреть в pg_stat_statement - там где generic дает сравнимый execution time и видно, что кастомные планы не имеют больших разбросов по времени выполнения - то я бы фиксил дженерик.


        1. mizhka Автор
          14.05.2025 14:15

          О, а pg_hint_plan так умеет делать? Было бы прикольно


          1. danolivo
            14.05.2025 14:15

            Он умеет хинты проставлять - в том числе настройки гуков. Но, полагаю, тебе нужно написать plpgsql-рутину, которая будет ходить в статистику, prepared statements, и решать, что форсить.


  1. ZaMaZaN4iK
    14.05.2025 14:15

    Пробовали оптимизировать работу PostgreSQL (ванильного или Shardman) при помощи пересборки с Profile-Guided Optimization (PGO)? Если да, то интересно было бы посмотреть на ваши результаты. По имеющимся бенчмаркам выглядит достаточно интересно: тык


    1. mizhka Автор
      14.05.2025 14:15

      Хотели поисследовать это как и LTO, и знаем что на простых тестах это даёт некоторый процент выйгрыша. Но вот в базах данных зачастую производительность задаётся не только оптимильным набором инструкций, но ещё сложность алгоритмов обработки информации и пределами масштабирования на горячих данных (блокировках). Поэтому PGO/LTO - вещи хорошие, со своими минусами и багами (PGO насколько помню вносит требование что расширения тоже должны быть собраны таким же или особым способом), но лучше boost производительности дают именно улучшения кода с точки зрения алгоритмов и lock-free вещей.


  1. vagon333
    14.05.2025 14:15

    Правильно-ли предположить, что вы измеряли производительность базы не 1ПБ, а 1/7?
    Ведь задача делилась между 7 серверами.
    Если горизонтально масштабировать до 15 серверов, то на 2ПБ производительность должна быть похожей.
    Или я что-то упускаю?


    1. mizhka Автор
      14.05.2025 14:15

      Для конечного пользователя это всё таки одна база данных, даже одна таблица, размером в петабайт. Но и верно что ресурсно это разбросано на 7 серверов, как с точки зрения дисков, так и с точки зрения процессоров.

      14 серверов и 2ПБ vs 7 серверов и 1ПБ мы конечно не сравнивали, в теории конечно можно конечно оптимистично говорить что будет похожее, но уверен что там тоже будут интересные погружения в rabbit holes. Как минимум чем больше нод, тем больше интерконнектов на каждой ноде и т.п.