Эта история началась с шутки на офисной кухне 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. Вдруг вам захочется повторить наш прекрасный эксперимент.