
Привет, Хабр! На связи Андрей Бородин, в Yandex Cloud я руковожу направлением разработки СУБД с открытым исходным кодом — и я попал в Яндекс через опенсорс. Я уже немного рассказывал, что и зачем мы делаем в опенсорсных БД с точки зрения облачных сервисов, где мы развиваем PostgreSQL, Greenplum, Cloudberry, Valkey и другие решения.
Но из этих историй часто ускользает человеческая сторона: мы занимаемся опенсорсом не только для того, чтобы сделать решения с открытым кодом более облачными, не только потому, что это модно, но и потому, что это приносит пользу не только продукту, но и самим разработчикам‑контрибьюторам.
На масштабах Яндекса возникают нетривиальные задачи, которые интересно решать. А когда мы делимся решениями с сообществом, то можем получить от них новый взгляд на проблему, и продолжить совместную разработку новой фичи в удобном формате: с кем‑то на условиях независимого сотрудничества, а кого‑то можем позвать в команду (как это было и со мной).
В общем, если придерживаться опенсорс‑философии, может возникнуть ситуация win‑win. Сегодня с коллегами Леонидом Борчуком @leborchuk и Дмитрием Сарафанниковым расскажу пару историй про то, как это бывает с опенсорсными СУБД.
История про кеш и проблему с подтранзакциями
Под капотом у PostgreSQL есть много интересных деталей. Например, подтранзакции, довольно удобные с точки зрения программиста. Вам о них обычно не требуется знать, пока в один прекрасный момент всё не начнет неожиданно сильно тормозить. На эту тему была статья от GitLab, в которой коллеги рассказывают, как и почему они устранили все подтранзакции у себя. Вот несколько графиков из статьи, которые наглядно показывают, как развиваются инциденты, связанные с подтранзакциями. Отдельные запросы тормозят настолько, что полностью перестают выполняться:

Эта проблема выражалась и с другой стороны — в мультитранзакциях. Такое бывает в случае, когда один tuple заблокирован несколькими транзакциями или апдейтами, вот пример:
\set aid random_zipfian(1, 100000 * :scale, 1.5)
SELECT abalance FROM pgbench_accounts WHERE aid = :aid;
620 000 TPS, 0 MB\sec write
\set aid random_zipfian(1, 100000 * :scale, 5.0)
SELECT abalance FROM pgbench_accounts
WHERE aid = :aid FOR SHARE;
На эту тему также был хороший доклад Александра Короткова, где он обратил внимание, что select‑only‑нагрузка может порождать 15 МБ/с записи на диск только для того, чтобы расставить блокировки на tuple. Это делается через подсистему SLRU, не самую производительную в PostgreSQL, потому что она рассчитана на небольшие серверы. Проблема наблюдается, если эксплуатировать машину с 64 ядрами или больше, а на 32-ядерном процессоре воспроизвести её крайне сложно. Хотя и такое тоже бывает при определённом стечении нагрузки, когда есть микс из быстрых и спектра медленных транзакций.
Однажды это случилось и в Яндексе. 17 апреля 2020 года на меня завели тикет, в котором коллеги предлагали: «Давайте чинить SLRU». У нас произошёл инцидент, когда важная база была недоступна 20 минут, пока подсистема SLRU была занята чтением.

Подсистема оказалась однопоточной. Мы подстелили соломы, чтобы быстро купировать инцидент: расширили кеш, сделали SLRU больше, но увеличивать бесконечно было нельзя. Хотелось системного решения, которое устраняет проблему и защищает базу от внезапной деградации производительности.
Четыре года мы работали над этой проблемой. В апреле 2024 года в PostgreSQL 17 закоммитился патч, который состоит из моей доработки в кеш транзакций, подтранзакций и всего, что находится в SLRU.

Идея заключается в том, что я заимствовал технологию из процессорных кешей, разделив кеш на банки, и перенёс её в кеш транзакций.
Чуть позже Дилип Кумар из EnterpriseDB дописал распил локов на банки, и эта технология стала ещё лучше с точки зрения Lock contention. В таком виде оно было принято до этапа feature freeze.
После этого мы ещё немного дооптимизировали это решение: выяснилось, что такая же ситуация с проблемными транзакциями затрагивает все реплики, поскольку там было лишнее ожидание — это мы тоже починили и закоммитили в августе.
Сейчас сообщество уже уверено в надёжности этой технологии. Коллеги из EnterpriseDB даже научились воспроизводить исходную проблему и наглядно показывать её влияние на производительность. Вот графики из их доклада:

Мы видим, что без патча при желании можно положить базу в ноль. С наложением патча мы всё ещё видим деградацию — длинные транзакции по-прежнему вредят, патч здесь не панацея, но от даунтайма он спасает.
История про маленький, но полезный коммит в PostgreSQL
Не всегда нужны годы, для того, чтобы создать и закоммитить новый патч. Иногда мы обнаруживаем возможность для быстрых улучшений в пару строк, но которые было бы сложно найти, если бы не наши специфические задачи. Историей поделился Дмитрий Сарафанников.
Наверняка большинство знают, как выглядит наукастинг в Яндекс Погоде краткосрочный прогноз с шагом в 10 минут, который предсказывает погодные явления с коротким жизненным циклом. Самый распространённый вариант — это карты осадков или температуры.

Карты осадков на тот момент хранились в PostgreSQL в виде полигонов и времени, на которое они актуальны. Поиск по полигонам производился средствами PostGIS. Каждые 10 минут туда заливались новые прогнозы и удалялись старые. При этом запрос, ищущий нужные полигоны использовал JOIN двух таблиц.
В PostgreSQL реализовано три варианта, как выполнить JOIN: NestedLoop, HashJoin и MergeJoin. Планировщик при составлении плана запроса пытается оценить возможность выполнения и стоимость каждого способа соединения таблиц. MergeJoin возможен только по сортированным входным данным. Собственно, в таблице была колонка с временной меткой (timestamp) полигона, и был построен btree‑индекс по ней, который хранит данные в отсортированном виде и гарантирует порядок сортировке на входе MergeJoin. Для расчёта стоимости MergeJoin планировщик должен оценить диапазон значений колонки, для этого он пытается открыть индекс и прочитать первое и последнее значения колонки.
Проблема заключалась в bloat. VACUUM не удаляет bloat из индексов. А при удалении полигонов мы всё время удаляли самые старые по timestamp данные — те строки с начала индекса. При удалении старых полигонов строки, соответствующие минимальным значениям timestamp в индексе, становятся невидимыми. И для поиска минимального значения приходилось двигаться по индексу вправо, пока не найдём первую видимую строку.
Таким образом, время планирования запроса увеличилось с долей мс до десятков мс. Плюс к этому на планирование этого запроса полностью съедался весь CPU на реплике, хотя исполнение самого запроса занимало единицы мс. Всё это продолжалось, пока не придёт VACUUM и не почистит удаленное.
Идея фикса была в том, чтобы добавить snapshot нового типа: который видит только что удалённые строки, а фильтрует только строки, которые уже точно никому не видны, — и сканировать индекс, используя его. План выполнения получался таким же, а CPU на его выбор стало тратиться гораздо меньше.
Итого, в процессе разбора этой ситуации мы раскопали интересный эффект: текущая на тот момент инсталляция сканировала индекс с помощью SnapshotDirty, принимала незакоммиченные строки и отклоняла мёртвые строки. Получился вот такой коммит.
Нужно ли быть матёрым сеньором, чтобы начать что-то делать в опенсорсе?
Нет. Все прошлые истории выглядят сложно, но чтобы начать что‑то делать не обязательно 30 лет прокачивать свои скилы. Вселенная опенсорсных СУБД огромна, место найдется всем. Меня зовут Леонид Борчук @leborchuk и я занимаюсь Greenplum — MPP аналитической БД на основе PostgreSQL.
В мае 2024 года Greenplum стал closed source (молча, без каких‑либо официальных комментариев). Сообщество в ответ самоорганизовалось и продолжило развивать БД под названием Сloudberry. Первое время это был проект разработчиков из HashData (Пекин) и Яндекса (Москва), но спустя какое‑то время мы стали проектом Apache, и география участников проекта существенно расширилась.
Cloudberry был форкнут от greenplum в феврале 2022 года, ещё на этапе beta‑версии Greenplum 7. Было проделано много работы: в частности, что важно для дальнейшего повествования, ядро обновлено на PostgreSQL 14.4. Однако основной проект Greenplum продолжал развиваться и к моменту ухода в closed source команда разработки (спасибо им большое за это) выпустила две стабильные версии.
Когда мы начали стабилизировать Сloudberry, то обнаружили, что большинство проблем уже было исправлено за прошедшие два года с момента форка. Решили, что нужно превентивно бороться с проблемами: не ждать, когда столкнемся с багом, а черри‑пикнуть уже сделанные изменения. К сожалению, из‑за изменений ядра PostgreSQL просто так забрать изменения уже было нельзя. Нужно было понять, что делает каждое из изменений, и переписать его на новой кодовой базе.
Для оценки объема работы собрали список всех коммитов‑кандидатов на cherry‑pick. Получилось 2994 штуки в нашей комьюнити‑таблице. Далее мы прошлись по всем изменениям и разметили их: стоит ли брать это изменение или нет. Осталось 2195 коммита. На момент написания статьи более 1600 коммитов мы уже перенесли: это изменения в основные компоненты — executor и оптимизатор, а также критичные багфиксы. Остаётся еще много работы, и большей частью это фиксы в инфраструктуру вокруг БД и малозначимые изменения.
Работа над черри‑пиками уже сделанных изменений — отличный способ начать что‑то разрабатывать в проекте. У вас есть код, есть Cloudberry, который работает. И у вас есть уже поревьювленное изменение старой командой Greenplum. Дальше нужно понять, что делает это изменение, переписать его на новую кодовую базу, прогнать тесты, сделать пул‑реквест, и вот вы уже участвуете в проекте. Переписать готовое решение гораздо проще, чем с нуля разбираться с не самым простым кодом. Постепенно узнаёшь, как проект разбит на модули, какие есть либы и какие структуры используются в коде, за что они отвечают, учишься вносить изменения, правильно их тестировать и общаться с сообществом.
Приходите в это обсуждение, будем рады. Специалисты по PostgreSQL и Greenplum сейчас очень ценны.