Чтобы объяснить, что есть Postgres Pre-Commitfest party и зачем мы в это ввязались, для начала нужно объяснить, как идёт разработка ванильного постгреса. Процесс принятия новых фичей и патчей в код разделён на так называемые коммифтесты (сокращённо CF), расписание которых всегда можно посмотреть на сайте https://commitfest.postgresql.org/. Когда разработчик предлагает свой код (неважно, будь это новая фича или багофикс), для того чтобы он был рассмотрен в рамках CF, он предварительно должен пройти процедуру ревью и быть одобрен для рассмотрения. Конечно, это совершенно не гарантирует, что код будет принят на этом CFили даже следующем, но сейчас не про это.
Найти ревьюера для своего кода зачастую задача более сложная, чем написать сам код. Человека надо погрузить в решаемую проблему, объяснить, что именно ты предлагаешь изменить, почему так, а не иначе и так далее. Прямо сейчас, в статусе Needs review, находится 29 патчей. Со свойственным нам желанием помогать, мы решили кинуть клич среди коммитеров и собрать всех желающих под одной крышей, чтобы они могли представить свои патчи, обсудить их и, возможно, найти желающего провести ревью.
Так получилось, что собрались мы в рамках проходившего Highload++ в Санкт-Петербурге. Поскольку Open Source — дело добровольное, мы просто разослали всем контактам, которые у нас были, предложение примерно следующего содержания: зовём всех неравнодушных постгресистов собраться в шатре, рассказать про свои патчи, обсудить их и хорошо провести время.
А ниже выжимка и расшифровка того, что у нас из этого вышло. Для тех, кому лень читать — видео с докладами и видео с записью последующего обсуждения из нашего шатра.
Приветственное слово
Как ни крути, но лучше кандидата, чем Олег Бартунов из Postgres Professional было не найти, поэтому ему слово:
У нас впервые происходит такой эксперимент, и это замечательное событие. Я очень рад, ведь когда мы организовывали нашу компанию, как раз рассчитывали на то, что появится много других компаний, которые будут работать с Postgres, появится отрасль СУБД-строения, и вместе мы будем заниматься красивым, хорошим Open Source делом.
Сейчас впервые собрались пять компаний в одном месте. Мы все друг с другом конкурируем, но так устроено Postgres-сообщество. Меня многие спрашивают, каким образом люди кричат за Open Source, но при этом зарабатывают деньги. Как устроено сообщество Postgres? Дело в том, что это сообщество развилось из Open Source. Например, родились Postgres-компании, которые оказывают коммерческие услуги большим энтерпрайзам. При этом нет одного ключевого игрока, все компании более-менее равноправные и были организованы выходцами из сообщества. Они являются его членами, т.е. принимают участие во всех мероприятиях, организовывают их, а сотрудники занимаются в сообществе разработкой.
При этом компании конкурируют, в первую очередь, с ванилой, потому что ванильный Postgres очень хороший. В то же время каждая компания является проксей между сообществом и энтерпрайзом, поскольку первое развивается по своим законам, а второе требует какие-то фичи прямо сейчас.
Практически все компании делают свой форк, но каждой компании выгодно поделиться кодом с сообществом, потому что оно берет на себя ответственность за его поддержание. Таким образом, ванильный Postgres начинает расти, а компании снова приходится с ним конкурировать, изобретать какие-то новые фичи, обратно делиться. Ванила опять растет, ты идешь дальше — процесс взаимовыгодный.
При этом происходит следующее — ты сделал какую-то фичу и делишься не просто с кем-то, а сразу со всеми, даже со своими конкурентами. Например, если мы сейчас продвигаем 64-битные транзакции, которые очень важны в России, поскольку у нас Postgres используется в очень больших системах, то отдаем всем, включая наших конкурентов. Я знаю, что они этот патч используют так или иначе. Конечно, мы на R&D потратили очень много сил, но, тем не менее, это как раз тот оверхед, который компания должна нести, чтобы Postgres-сообщество и Open Source развивался.
Казалось бы, очень легко сделать какую-то фичу, зажать себе, сказать, что это у меня преимущество и зарабатывать деньги. Но тогда сообщество не будет развиваться, а вы не будете его настоящим членом. Поэтому наше Postgres-сообщество является чисто Open Source.
Так работает наше уникальное, очень хорошо сбалансированное Open Source сообщество. Еще раз напомню, что в Postgres нет владельца. Нельзя взять и заархивировать проект а GitHub.
И еще я радуюсь тому, что ключевая особенность Open Source не в том, что фичи можно смотреть, использовать, модифицировать, и т.д. В первую очередь, Open Source — это коллаборация. Сегодняшнее событие — это первый шаг к тому, чтобы мы работали совместно.
Да, мы здесь соревнуемся друг с другом. Но у нас есть сообщество, в котором можем коллаборировать – продвигать свои патчи, помогать ревьюить и т.д.
И с небольшим приветствием выступил известный и уважаемый член комьюнити Postgres — Андрей Бородин из «Яндекса».
Зачем подобные события нужны участникам? Вряд ли можно найти что-то сложнее разработки СУБД, операционных систем или компиляторов. Это интересно. Кроме того, вы к нам пришли, значит, Postgres ваш, вы его разрабатываете, и первый шаг в сообщество — это ревью чужого кода, а не написание своего, взгляд на код другого человека, который тоже в сообществе. Поэтому давайте слушать людей и обсуждать их идеи.
Ну а теперь переходим к самому главному — презентации патчей. Первым выступал руководитель группы производительности Михаил Жилин из Postgres Professional.
Fix wait_event of pg_stat_activity in case many backends
Речь шла про патч, который должен исправить колонку wait_event в представлении pg_stat_activity, когда у базы очень много подключений. Но что такое много? В данном случае это не десятки, не сотни, а тысячи и десятки тысяч подключений.
Возьмем тестовую базу и начнем на нее гнать простую нагрузку. Например, очень короткие select сгенерированные всем известным pg_bench. И выполним запрос, который выбирает из представления pg_stat_activity данные, фильтрует их по клиентским соединениям и группирует по колонке wait_event.
select wait_event, state, count(1)
from pg_stat_activity
where backend_type = 'client backend'
and state = 'active'
group by wait_event, state;
Мы смотрим только на активные подключения, фильтруя побочные idle, idle in transaction и т.д. Т.е. те подключения, которые сейчас выполняют какой-то конкретный запрос. И получаем такой ответ:
select wait_event, state, count(1)
from pg_stat_activity
where backend_type = 'client backend'
and state = 'active'
group by wait_event, state;
wait_event | state | count
------------+--------+-------
ClientRead | active | 5
<null> | active | 1
Выберется первая строчка, где состояние ожидания — это ClientRead, а состояние нашего бэкенда — активный. И это, вообще-то, ошибка. ClientRead говорит нам о том, что мы сейчас никакого запроса не выполняем. Процесс ждет какое-то новое сообщение от клиента, но не может быть активными в этот момент. Мы с этой проблемой столкнулись и начали разбираться. Как заведено, сразу посмотрели в алгоритм. Выглядит он довольно-таки просто:
Nested Loop (t1.pid = t2.pid)
BackendStatusArray (1, copy)
state, query, IP address, timestamps
row-consistency
ProcArray (2)
wait_event_info (wait_event)
Здесь есть некоторый Loop, который объединяет две внутренних структуры данных. Первая структура данных называется BackendStatusArray. Из нее выбирается информация о состоянии бэкенда: активный он или нет, какой запрос сейчас выполняется, с какого IP-адреса, какие там старты новых транзакций и т.п. Самое главное, что она заполняется копированием данных, но при этом не создавая блокировку на чтение, а сразу просто берёт и копирует. Но при этом проверяется, что в рамках одной строчки скопировалось всё консистентно, т.е. должно соблюдаться row-consistency.
Для чего это сделано? Pg_stat_activity — это инструмент мониторинга, он не должен вызывать блокировки. Во всяком случае, таково было мнение разработчика. Далее он итерирует по копии созданного BackendStatusArray и выбирает из другой структуры данных нашу колонку wait_event. Но это происходит с задержкой, конечно. Т.е. сначала он скопировал одно, а потом заходит в другой массив и, очевидно, это становится проблемой.
У нас получается дельта времени между тем, что мы прочитали из BackendStatusArray и реальной колонкой wait_event. Т.е. для одного и того же бэкенда мы читаем данные в колонках, которые относятся к совершенно разным моментам времени. И чем ниже строка, тем больше разбег времени, и тем хуже у нас получается выборка. Ошибка начинает появляться все чаще и чаще.
Что было предложено в качестве решения: давайте не будем разделять эти два события, а объединим в один цикл и будем собирать ProcArray.wait_event_info при копировании BackendStatusArray. Открыли дискуссию в hackers почти два года назад https://www.PostgresQL.org/message-id/ab1c0a7d-e789-5ef5-1180-42708ac6fe2d%40Postgrespro.ru, которая вызвала положительный отклик в сообществе, потому что не мы одни столкнулись с этой проблемой. Но всё закончилась сообщением от Роберта Хааса: «I don't agree that this is a bug». И самое лучшее действие, по его мнению, это оставить всё как есть.
Мы, конечно, расстроились, но решили фикс не бросать и в течение года продолжали его проверять у себя. Проверили, покрыли TAP-тестами, а год назад выпустили данный фикс в рамках своего форка PgPro. Что важно отметить — за этот год нам несколько раз действительно очень сильно помогло это исправление. Т.е, когда мы сравнивали поведение в нашем форке, все было хорошо, а когда смотрели ванилу — снова слезы, печаль и грусть.
И эта ситуация подводит нас к главной цели мероприятия — здесь собрались лучшие постгресисты, мнение которых хотелось бы услышать. Почему Роберт Хасс сказал, что это не баг? Да потому что он боится ставить блокировки. С одной стороны — стремление к перфекционизму, с другой — наше желание минимизировать дельту во времени на чтение. А может надо всю информацию держать в одной структуре и т.д. То есть вариантов много, проблема понятна и надо искать как из неё выходить.
И небольшой комментарий от Федора Сигаева касательно того, почему возникают разночтения в таких, казалось бы, очевидных ситуациях:
Ситуация сейчас в мире складывается такая, что средний инстанс в России в разы больше, как по объему данных, так и по мощности машины. В результате часто сообществу приходится доказывать, что это проблема. Потому что если вы делаете небольшие запросы раз в 5 секунд, у вас 10 коннектов, машина на 16 ядер, а данных там 10GB, то столкнуться с проблемой, о которой рассказал Миша, практически невозможно. Сообщество сейчас в среднем работает с машинами по 16 ядер, а 64 ядра — это уже большая машина. У наших клиентов 64 — это стартовый уровень. Поэтому нам нужно кооперироваться и рассказывать, что у Postgres есть проблемы на больших машинах. И это только одна из них.
Про следующий патч, New [relation] options engine, рассказал Николай Шаплов из Postgres Professional
Это пример патча с очень долгой историей, которая длится уже девять лет. Всё началось со студенческой задачки приделать опции к операторам класса со словами: «Ну, тут на две недели максимум. Даже не надо думать, что это такое, просто есть оператор класса, и к нему можно и нужно приделать опции». На деле оказалось, что вообще-то опции таблиц (и не только) в Postgres уже есть, и надо просто каким-то образом использовать один и тот же код. Но выяснилось, что этот код сильно прибит гвоздями.
#Таблицы
CREATE TABLE test(i INT) WITH (FILLFACTOR=30);
#Индексы
CREATE INDEX ON test2 using GIST(p) WITH
(BUFFERING = off)
#Операторы класса
CREATE INDEX brinidx_bloom ON brintest_bloom
USING brin (byteacol bytea_bloom_ops
(n_distinct_per_range = -1.1));
В Postgres действительно есть опции таблиц — на картинке они выделены красным. Наверняка, каждый делал что-то такое, когда табличке при создании присваивались какие-то дополнительные атрибуты: заполнять ее сильно/не сильно, автовакуум запускать/не запускать и так далее. Такая же история есть и в индексах, и в операторах класса. Но проблема заключается в том, что этот код очень-очень древний. Он возник еще в тот момент, когда Postgres был не модульным, а монолитным.
#src/backend/access/common/reloptions.c
static relopt_int intRelOpts[] =
{
{
{
"fillfactor",
"Packs table pages only to this percentage",
RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock /* since it applies only to later
* inserts */
},
HEAP_DEFAULT_FILLFACTOR, HEAP_MIN_FILLFACTOR, 100
},
{
{
"fillfactor",
"Packs btree index pages only to this percentage",
RELOPT_KIND_BTREE,
ShareUpdateExclusiveLock /* since it applies only to later
* inserts */
},
BTREE_DEFAULT_FILLFACTOR, BTREE_MIN_FILLFACTOR, 100
Изрядное количество опций определены как статический массив, и мы не можем динамически их менять. А в тот момент, когда захотелось выносить индексы в отдельные расширения, придумали еще один механизм.
#src/backend/access/common/reloptions.c
static int num_custom_options = 0;
static relopt_gen **custom_options = NULL;
static bool need_initialization = true;
#src/include/access/reloptions.h
extern void add_bool_reloption(bits32 kinds, const char *name, const char *desc,
bool default_val, LOCKMODE lockmode);
extern void add_int_reloption(bits32 kinds, const char *name, const char *desc,
int default_val, int min_val, int max_val,
LOCKMODE lockmode);
extern void add_real_reloption(bits32 kinds, const char *name, const char *desc,
double default_val, double min_val, double max_val,
LOCKMODE lockmode);
extern void add_enum_reloption(bits32 kinds, const char *name, const char *desc,
relopt_enum_elt_def *members, int default_val,
const char *detailmsg, LOCKMODE lockmode);
Помимо статического объявления опций, есть еще динамическое т.е. отдельный API, который заполняет специальный массив, где рассказано, как эти опции устроены.
Соответственно, после того как добавили опции оператора класса, разработчики ванилы завели еще один API, абсолютно симметричный и повторяющий то же самое, но только работающий локально. Короче говоря, нагромоздили костыль на костыле, и, если честно, очень сильно хотелось сделать какой-то универсальный API. Такой, что работает с опциями в принципе, не привязываясь к тому, является ли это таблицей, оператором класса или чем угодно, поскольку где-то еще могут понадобиться опции. Хочется сделать абстрактный механизм по работе с опциями и после этого уже прикрутить его и к relation, и к opclass, и к чему еще захочется. То есть фактически понадобилось всё разрушить и построить заново.
Что было сделано? А было заведено две концепции:
-
Структура, описывающая одну конкретную опцию с полным описанием того, как она должна парситься и валидироваться.
Option Spec: option_spec_basic
-
Структура, описывающая набор всех доступных опций, которые могут быть присвоены данному объекту (например, таблице).
Option Spec Set: options_spec_set
Вот что получилось:
#src/include/access/options.h
/* generic structure to store Option Spec information */
typedef struct option_spec_basic
{
const char *name; /* must be first (used as list termination marker) */
const char *desc;
LOCKMODE lockmode;
option_type type;
int struct_offset; /* offset of the value in Bytea representation */
option_value postvalidate postvalidate_fn;
} option_spec_basic;
/* reloptions records for specific variable types */
typedef struct option_spec_bool
{
option_spec_basic base;
bool default_val;
} option_spec_bool;
typedef struct option_spec_int
{
option_spec_bool base;
int default_val;
} option_spec_int;
typedef struct options_spec_set
{
option_spec_basic **definitions;
int num; // * Number of spec_set items in use */
int num_allocated; // * Number of spec_set items allocated */
bool assert_on_realloc; // * If number of items of the spec_set were
// * strictly set to certain value assert on
// * adding more items */
bool is_local; // * If true specset is in local memory context */
Size struct_size; // * Size of a structure for options in binary
// * representation */
postprocess_byte options_function postprocess_run; // * This function is
// * called after options were converted in
// * Dytea representation.
// * Can be used for extra
// * validation etc. */
char *namespace; // * Spec Set is used for options from this
// * namespace */
} options_spec_set;
После чего можно добавлять в access метод (или любое другое нужное место) наши новые универсальные опции.
#src/backend/access/brin/brin.c
static options_spec_set *brin_relopt_specset = NULL;
void *
brinreloptspecset(void)
{
if (brin_relopt_specset)
return brin_relopt_specset;
brin_relopt_specset = allocatedOptionsSpecSet(NULL,
sizeof(BrinOptions), false, 2);
optionsSpecSetAddInt(brin_relopt_specset, "pages_per_range",
"Number of pages that each page range covers in a BRIN index",
NoLock, /* since ALTER is not allowed no lock needed */);
offsetof(BrinOptions, pagesPerRange), NULL,
BRIN_DEFAULT_PAGES_PER_RANGE,
BRIN_MIN_PAGES_PER_RANGE,
BRIN_MAX_PAGES_PER_RANGE);
optionsSpecSetAddBool(brin_relopt_specset, "autosummarize",
"Enables automatic summarization on this BRIN index",
AccessExclusiveLock,
offsetof(BrinOptions, autosummarize), NULL,
false);
return brin_relopt_specset;
}
И используем
@@ -294,6 +292,7 @@ brinhandler(PG_FUNCTION_ARGS)
amroutine->amestimateparallelscan = NULL;
amroutine->aminitparallelscan = NULL;
amroutine->amparallelrescan = NULL;
+amroutine->amreloptspecset = bringetreloptspecset;
PG_RETURN_POINTER(amroutine);
Единственная проблема, которая нас отделяет от этого счастливого будущего, заключается в том, что патч очень большой, а комьюнити предпочитает такие вещи внедрять постепенно. В этом случае, как его резать на куски непонятно. Классическая проблема, когда исправляешь в одном месте, а оно тянет за собой другое. А из-за старости кода, никто уже не понимает как это устроено. С патчем можно ознакомиться здесь.
Комментарий от Федора Сигаева:
Во-первых, Коля затронул одну вещь, что нужно по возможности резать большие патчи на какие-то более мелкие. В реальности это достаточно тяжело, если у вас фича крупная, типа 64-битных XID’ов или JSON, как было.
Мы с Олегом разработали три индекса в Postgres и половину индексов сильно переписали. Думаю, что два или три десятка операторов класса в результате нарисовали. Опций сильно не хватает. Когда я это все разглядывал, Коле говорил: “А давай JSON попробуем. Ну чего, там вечно нужно какие-то особые конструкции выдумывать”. Комьюнити ответило, что никакого JSON в каталоге. Теперь Коля мучается, пытается сделать гибкую вещь без JSON. Надо помочь.
Следующим выступил Илья Евдокимов из Tantor Labs с патчем pg_stat_advisor extension
Он написал новое расширение, которое называется pg_stat_advisor. Если сказать одним предложением — оно дает подсказки, которые нужны, чтобы что-то сделать со статистиками таблицы.
Какова история написания этого расширения? 4 года назад Константин Книжник пытался написать патч, который может улучшить estimated rows в join-командах как раз с помощью расширенной статистики. Там был создан бинарный GUC-параметр, который может делать следующее — подсказывать, что надо выполнить CREATE STATISTICS в такой-то таблице по таким-то колонкам.
Отсюда возникла идея — почему бы не создать отдельное расширение, которое может именно эту расширенную статистику рекомендовать по различным критериям. Илья это решение написал, и на сегодняшний день оно может делать две вещи:
1. Был создан GUC-параметр со значением double. Он вычисляет все таплы, которые добавились, удалились и апгрейдились, но еще не были обработаны командой ANALYZE и общее количество всех таплов, которые есть в таблице.
SET pg_stat_advisor.suggest_statistics_threshold = 0.01;
>>>
SET
EXPLAIN ANALYZE SELECT * FROM my_tbl
WHERE fld_1 = 500 AND fld_2 = 100;
>>>
NOTICE: pg_stat_advisor suggestion: CREATE STATISTICS
my_tbl.fld_1 fld_2 ON fld_1, fld_2 FROM my_tbl
Gather (cost=1000.00..24311.11 rows=1 width=12)
(actual time=104.973..113.246 rows=100 loops=1)
Workers Planned: 2
Workers Launched: 2
-------------------------------------
Соответственно, если этот коэффициент будет меньше, чем выделенное на скрине отношение, то при вызове какого-то запроса (на текущий момент пока что по командам SELECT), по команде EXPLAIN ANALYZE он нам подскажет, что надо сделать ANALYZE, потому что уже накопилось слишком много таплов, которые командой ANALYZE не учлись.
SET pg_stat_advisor.analyze_scale_factor = 0.01;
>>>
SET
EXPLAIN ANALYZE SELECT * FROM my_tbl
WHERE fld_1 = 500 AND fld_2 = 100;
>>>
NOTICE: pg_stat_advisor suggestion: 'ANALYZE my_tbl'
Gather (cost=1000.00..24309.94 rows=1 width=12)
(actual time=101.366..108.701 rows=100 loops=1)
Workers Planned: 2
Workers Launched: 2
2. Второй GUC-параметр связан с расширенной статистикой. Если вдруг отношение estimated rows к общему количеству rows будет превышать заданный GUC-параметр, то по команде EXPLAIN ANALYZE он будет рекомендовать сделать расширенную статистику по таким-то столбцам.
Возможно, пока критерии слишком примитивные и тривиальные, но в будущем хочется реализовать дополнительную фильтрацию с WHERE, поскольку различные типы расширенной статистики могут применяться только при определенных фильтрах
Традиционный комментарий от Федора Сигаева: .
На самом деле вещь, на мой взгляд, очень полезная, потому что все-таки тренд развития компьютеров сегодня лежит туда, чтобы человек как можно меньше участвовал. Это шаг в том направлении.
Хочу обратить внимание — если люди не проявляют интерес к фишке, то комьюнити тоже на это может не посмотреть. Обращаюсь прежде всего к DBA, инженерам — если видите полезную фишку, не поленитесь написать, что я воспользовался, мне она зашла и так далее.
На сцену вышел Павел Селезнёв из команды Pangolin «Сбертеха», чтобы представить Pgcopydb refactoring
Как известно, Postgres — это не только ядро, но и различные компоненты. Идей, что исправить, много, но начать было решено именно с pgcopydb — компонента, который помогает мигрировать базы данных. Это более быстрый способ переноса, чем связка pg_dump/pg_restore. Собственно, самое интересное исправленное место на скриншоте, но патч большой и там есть, что обсудить.
Например, наиболее интересное срабатывае (memory leak):
PQExpBuffer uri = createPQExpBuffer();
appendPQExpBufferStr(uri, "*pgur");
...
else
{
log_error("Failed to percent-escape URI parameter [%s] ", ...
return false;
}
Иван Кушнаренко из Tantor Labs рассказал про автономные транзакции
Судьба у них интересная, поэтому цель доклада была в том, чтобы слушатели узнали про них, нашли для себя их применение и отписали в Hackers, какое они видят применение автономных транзакций для себя. Потому что если 7 лет назад все сообщество было «за», то сейчас уже сообщество думает — нужны или нет.
Автономная транзакция — это транзакция, которая будет зафиксирована, даже если основная транзакция откатилась. Но их нет в SQL-стандарте.
Однако сейчас они нужны для:
Конвертации кода из Oracle, поскольку там они реализованы;
Логирования, аудита, трекинга таблиц
Интеграции с внешними системами
Дебага
Выполнения не транзакционных команд в функциях, таких как VACUUM и т.д.
Автономная транзакция создается с помощью функции PRAGMA AUTONOMOUS_TRANSACTION. При вызове этой функции создается автономная сессия, и транзакция будет выполняться автономно.
CREATE FUNCTION foo() RETURNS void AS $$
BEGIN
INSERT INTO tbl VALUES (1);
END;
$$ LANGUAGE plpgsql;
BEGIN;
INSERT INTO tbl VALUES (0);
SELECT foo();
ROLLBACK;
SELECT * FROM tdbl;
>>>
1;
В примере создается основная транзакция, где вставляется 0, вызывается функция, вставляется 1, rollback на нашу транзакцию, и мы получаем в таблице единичку, после чего она сохраняется.
Автономные транзакции используются во многих СУБД. Отечественные вендора тоже внедряют их, включая решения, основанные не на Postgres. Обычно автономные транзакции реализуются с помощью расширений. Самые известные — это dblink, pg_background и plpython3u. Но у них есть существенные недостатки. В некоторых не создается пул сессий, соответственно на каждый вызов SQL-команды создается новая сессия, что затратно. Или, например, логин/пароль в открытом виде хранятся в SQL-коде. Поэтому хочется уже сделать единственное и качественное решение.
Первые запросы по автономкам появились в 2008 году в Hackers, и тренд сохранился до 2011 года.
2011 https://www.PostgresQL.org/message-id/flat/1303399444.9126.8.camel%40vanquo.pezone.net
2011 https://wiki.PostgresQL.org/wiki/Autonomous_subtransactions
В 2014 году появилась первая реализация, потом был вариант от 2016 года, а в 2023 году Иван предложил свой вариант.
2014, Rajeev Rastogi
https://www.PostgresQL.org/message-id/flat/BF2827DCCE55594C8D7A8F7FFD3AB7713DDDEF59%40SZXEML508-MBX.china.huawei.com https://www.PostgresQL.org/message-id/flat/BF2827DCCE55594C8D7A8F7FFD3AB7715990499A%40szxeml521-mbs.china.huawei.com2016, Peter Eisentraut
https://www.PostgresQL.org/message-id/flat/659a2fce-b6ee-06de-05c0-c8ed6a01979e%402ndquadrant.com2023, Ivan Kush
https://www.PostgresQL.org/message-id/flat/f7470d5a-3cf1-4919-8404-5c4d91341a9f%40tantorlabs.com
Этот патч реализован на основе патча Peter Eisentraut. Из него взяли некоторые идеи, сделали ребейз, обложили тестами и добавили PRAGMA AUTONOMOUS. Автономная сессия реализована с помощью background воркеров. Общение между основной сессией и автономной идет по протоколу постгреса, общение синхронное. И одно из главных преимуществ — появился пул автономных сессий.
Немного о том, как это работает. Создаются статичные структуры типа UserData key, потом создаются две очереди, по которым происходит общение между основной сессией и автономной сессией.
И есть некоторые планы на будущее, которые хочется реализовать:
Доделать оставшиеся стейтменты для PL/pgSQL
Реализовать поддержку PL/Python
Сделать общий пул для всех бекэндов
Асинхронные транзакции
Комментарий от Федора Сигаева, как без него:
Даже не заглядывая в комьюнити, сразу могу назадавать вопросов, на которые требуются ответы. Первое: “Автономные транзакции должны видеть результаты родительской транзакции или нет?”. В вашей схеме, конечно, вы не сможете увидеть. Но, может быть это и правильно.
Следующий вопрос: “Как оно себя поведет, если воркеры исчерпались? Воркеры все заняты, а в Postgres их лимитированное количество, вы не можете их много сделать. Если они все заняты, то родительская транзакция будет ждать пока освободятся?Дальше. У нас рекорд 100 тысяч баз в одном инстансе. Вопрос: “Как себя поведет эта штука?”.
Таких вопросов на самом деле достаточно много. Я не требую ответов прямо сейчас, это просто повод подумать. Поскольку вы совершенно правильно заметили, что в стандарте SQL этого нет, то на них нужно отвечать.
Максим Орлов, ещё один разработчик из Postgres Professional, рассказал о продвижении 64-битных транзакций в Postgres
Немного окунемся в тематику. Тут придется, конечно, от Адама и Евы начинать, но не будем тратить ваше время и просто отметим самые важные вещи про счётчик транзакций. Хотя если хочется вникнуть глубоко, то подробнее о транзакциях можно прочитать в статье Максима.
А теперь быстрая версия. Счётчик транзакций в поздней версии в Postgres — это целочисленный 32-битный счётчик, то есть 4 294 967 295. Этого хватит на 4 294 967 295 / 86 400 = 49710 транзакций в секунду. Это просто число, которое постоянно бежит вперед. Кажется, какие проблемы? Но при достаточно высокой нагрузке оборот счётчика происходит за сутки. И это уже обыденная реальность, в которой мы живём. Если у вас небольшая нагрузка, например, какой-то сайтик, и вы кладете в базу простые запросы, вы никогда не наткнетесь на проблемы с транзакциями и не исчерпаете этот счетчик. Разработчики Postgres подумали о вас и внедрили механизмы, которые рассчитаны на то, чтобы вы это не увидели. Это всем известные wraparound и vacuum. Но если у вас высокая нагрузка, то вы должны понимать, что при таком режиме вся работа Postgres критически (прям выделить и подчеркнуть) завязана на работу vacuum. Поэтому он обязательно должен быть очень хорошо настроен, и за ним надо пристально следить. Или всё сломается.
А что нам дает в этом случае переход на 64-битные транзакции? Дело в том, что при переходе на 64-битный счётчик транзакций проблема его оборота становится гипотетической! При текущих нагрузках чисто математически счетчик сможет переполниться, но это произойдет через тысячу лет.
Не будем погружаться в детали реализации, почему мы так сделали, или не так сделали, какие форматы страниц выбирались и т.д. Это все уже обсуждалось в сообществе не один раз. Главное — сейчас там нет людей, которые могли бы сказать, что этот патч не нужен. Все понимают, что развитие идёт в эту сторону, и количество транзакций будет только увеличиваться.
Но проблема в том, что это очень большой патч, на целых полтора мегабайта с огромным количеством изменений в разных модулях. И никто не знает, как его закоммитить. Хочется скаламбурить — давайте слона есть по частям. Поэтому вопрос в том, как разбить его на части.
Можете удивиться, что здесь статус Returned with feedback. Обычно это означает, что предлагаемый патч не нашёл отклика и был отклонён. Но тут исключение. На самом деле было решено, что мы вернемся к этой теме и переоткроем ее, как только будем готовы, потому что, полтора мегабайта закоммитить за один раз невозможно.
Сейчас наша ближайшая задача — перевести офсеты мультиксидов на 64 бита. Не будем углубляться в проблематику, нам главное, что этот патч небольшой. Возможно, вы не сталкивались с этой проблемой, но если сталкивались, то обязательно заходите в тред и говорите — да, нам это нужно, без этого жить невозможно, ну и так далее. Всё обсуждение идёт здесь. Так что работа ведётся, и мы надеемся такими небольшими шажками реализовать xid 64.
Федор Сигаев:
Это еще раз подтверждает то, что нагрузки на Postgres в России заметно выше, чем в среднем по миру. Когда мы начинали эту работу, у нашего клиента wraparound был раз в две недели. Потихоньку это уменьшилось до недели, до трех дней и сейчас есть клиент, который пришел к нам с тем, что у него раз в день wraparound на ваниле. Сейчас у него все хорошо, но беда в том, что, когда мы пришли приблизительно год назад с очередным своим патчем: “Давайте коммитить”, ответ был: “Слушай, раз в два месяца vacuum можно и потерпеть”. А у нас уже раз в день.
Следующим выступающим был Антон Мельник, коллега по Postgres Professional, и его Limit backend memory
Этот патч был создан в первую очередь по просьбе наших инженеров. Его задача — позволять ограничивать память как отдельных бэкендов, так и всех бэкендов вместе. Например, можно выставить ограничение для каждого бэкенда потреблять не больше 200 мегабайт памяти, а для всех вместе 2 гигабайт.
Для чего это делается? Очевидно, данный фокус позволит спокойно запускать на сервере другие процессы, не вызывая гонку за ресурсами, использование swap и защищая от спецэффектов вроде вызова OOM killer с последующим крахом и восстановлением всего сервера.
Что сейчас происходит вокруг этого патча? В сообществе опубликованы патч от Реда Томпсона, который умеет ограничивать общий объем используемой памяти. Две независимые проверки в ревью, от Томаса Вондры и наша, показали, что текущий патч от Реда Томпсона практически не влияет на скорость выполнения запросов.
Соответственно, мы продолжили разработку и представили патч, который уже умеет ограничивать память отдельных бэкендов, но все вместе — нет.
Что ещё надо доработать. В первую очередь, необходимо решить проблему с ошибкой выделения памяти. Сейчас ошибка приходит в тот бекенд, который был последним, а не в самый большой. Затем объединить наш патч с патчам Реда Томсона и пройти ревью.
Традиционный уже вопрос от Федора Сигаева:
У меня вопрос. Если вы сталкивались с OOM killer в Postgres, какой процесс он убивал? Обычно убивается не тот, который сожрал больше памяти, а тот, кто больше потрогал. В современных условиях больше всего обычно потрогал checkpointer, поэтому его OOM killer и пришибает. После этого Postgres разводит руками и говорит: “Я не буду работать в таких условиях”. Поэтому очень важно иметь защиту от прихода OOM killer.
Следующей выступала Алёна Рыбакина из Postgres Professional, а называется он Replace OR clauses with ANY expression
Это совместная разработка Алёны и Андрея Лепихова. Патч был в мастере совсем недолгое время, буквально неделю, но мы активно продолжаем над ним работу.
Началось всё с клиента, у которого сформировалось очень много BitmapScan с OR-выражениями, потому что его фреймворк банально не мог обработать IN выражения. У него сформировалось 50000 BitmapScan, для выполнения этого запроса потребовалось 1,5 GB памяти, а время выполнения составило 55 с.
SELECT FORMAT(prepare x %%s AS SELECT * FROM pgbench_accounts a
WHERE %s, '(' || string_agg(int, ',') || ')'::string_agg(FORMAT('aid = $%s', g.id), ' OR ') AS cmd
FROM generate_series(1, 50000) AS g(id) igexec
------------------------------------------------------------
Bitmap Heap Scan on pgbench_accounts a (cost=44.35..83.96 rows=10 width=97)
Recheck Cond: ((aid = 1) OR (aid = 2) OR (aid = 3) OR (aid = 4) OR (aid = 5) OR (aid = 6) OR (aid = 7) OR ...)
-> BitmapOr (cost=44.35..44.35 rows=10 width=0)
-> Bitmap Index Scan on pgbench_accounts_pkey (cost=0.00..4.43 rows=1 width=0)
Index Cond: (aid = 1)
-> Bitmap Index Scan on pgbench_accounts_pkey (cost=0.00..4.43 rows=1 width=0)
Index Cond: (aid = 2)
...
... It took 50000 pas...
Execution time: 55756,053 ms (00:55,756)
MemoryComsuption: 1.27GB
У нас родилась идея: а давайте упакуем их в одно ANY([..])-выражение! Наверное, должно сработать.
SELECT FORMAT('prepare x %%s AS
SELECT * FROM pgbench_accounts a
WHERE %%s, '' || string_agg(int, ',') || '' || ',
string_agg(FORMAT('aid = $%s', g.id), '' or '')) AS cmd
FROM generate_series(1, 50000) AS g(id) 'gexec
------------------------------------------------------------
Index Scan using pgbench_accounts_pkey on pgbench_accounts a (cost=0.42:48.43 rows=10 width=97)
Index Cond: (aid = ANY ('{1,2,3,4,5,6,7,8,9,10,...}'::integer[]))
(2 rows)
Execution time: 339.764 ms
Memory/Consumption: очень много)
В результате время выполнения сократилось до 300 мс. Выглядит всё прекрасно. Т.е. у нас вместо BitmapScan использовался Index Scan и одно большое выражение с массивом. Да, это corner case, но он часто встречается, если у вас есть фреймворки формирующие автогенерированные запросы.
А что ещё даёт нам этот патч, кроме ускорения в данном кейсе?
Во-первых, за счет более короткого списка выражений (у нас практически все упаковывается в одно выражение) уходит меньше времени на формирование планов. Оптимизатор, к сожалению, любит много раз сканировать OR выражения. Вот примерный список случаев, где он много раз это делает:
Удаление дубликатов (find_duplicate_ors)
Создание индексов (build_index_path) (не один раз)
Оценка селективности для выражений (clauselist_selectivity) (не один раз)
Попытка удаления join соединений
Также ускоряется выполнение SeqScan, поскольку попытка найти значение в hashed array происходит намного быстрее, чем циклический поиск по длинному выражению для каждого входящего кортежа. Если раньше нам приходилось каждый тупл сравнивать с каждым выражением (тупл равен единице, равен двум и т.д.), то здесь мы просто формируем хэш-таблицу и проверяем тупл на вхождение. Но это ещё не всё, т.к. у нас идёт влияние на выбор наиболее оптимального плана.
У нас получается преимущество перед Index Scan — все туплы уже отсортированы. Нам не нужна дополнительная операция сортировки, чтобы отсортировать туплы и, например, применить Group By операцию. Также патч иногда влияет на улучшение оценки кардинальности. С тредом можно ознакомиться тут.
Федор Сигаев:
Этот патч возвращает нас к проблеме OR, которые генерят не задумываясь, как умеют, и порождают такие случаи. В моем личном опыте первый раз я с этим столкнулся в версии Postgres 6.3. Тогда он просто падал на больше, чем 9 OR. Теперь он молодец, выдерживает 50 тысяч OR, но при этом сжирает всю доступную память, то есть улучшение есть, но надо продолжать.
Предпоследний патч PITR improvements представил Юрий Соколов из Postgres Professional
Патч небольшой, но важный для всех, кто уже делает бекапы. Пришел клиент, говорит: «Мы сделали вашим pg_probackup бэкапы, у них есть recovery time, пытаемся на это время восстановиться, получаем recovery ended before configured target was created. Почему?».
Потому что pg_probackup, чтобы узнать, на какой момент он делает бэкап, добавляет Recovery Point с помощью SQL-функции pg_create_restore_point. А клиент тестирует или на тестовом окружении, или залитые в базу данные не дополняются, т.е. в базе нет активности. Но при восстановлении на точку во времени в Postgres сейчас анализируют только записи COMMIT и ABORT, а время в XLOG_RESTORE_POINT есть, но при восстановлении на точку во времени не используется.
Но так было не всегда. Примерно 10 лет назад, в 2014 году, это место рефакторили и случайно потеряли реакцию на RestorePoint.
From c945af80cfdaf72adb91d6688fb3a4c4f17c0757
Date: Thu, 9 Jan 2014 14:00:39 +0200
Subject: [PATCH] Refactor checking whether we've reached the recovery target.
Makes the replay loop slightly more readable, by ...
Собственно предлагается два патча:
Вернуть реакцию на таймстемп в XLOG_RESTORE_POINT
Ещё лучше добавить таймстемп в XLOG_BACKUP_END
Вот такой вот баг, который надо чинить.
И завершил всё Андрей Бородин из Yandex Cloud с GiST amcheck
Андрей — известный любитель проверять индексы на повреждения, на физическое разрушение данных и его патч довольно давно лежит.
Самое главное, что, если вы вдруг хотите разобраться в разных индексах: B-деревьях, GiST, GIN, или хотите написать для хэша, приходите разобраться. Андрей с радостью погрузит вас в детали.
Вместо выводов
Как всё прошло? На наш взгляд отлично.
Будем-ли делать ещё? Кажется, да.
Поэтому, когда в следующий раз услышите анонс Pre-Commitfest пати и вам есть что сказать - смело приходите. Если хочется послушать, поспорить или просто высказать своё мнение - тоже приходите.
eee
И сколько пулл реквестов смержили в итоге?