Skip to content

Подготовленные операторы и лавина блокировок на секционированной таблице, часть 2

Автор: Николай Самохвалов #PostgresMarathon 2-010: Prepared statements and partitioned table lock explosion, part 2


В первой части мы сосредоточились на поведении Lock Manager при работе с подготовленными выражениями и секционированными таблицами.


В простом синтетическом примере мы увидели взрыв числа блокировок: 8 с custom‑планами в первых пяти вызовах, до 52 с generic‑планом в шестом, и до 13 с использование кэшированного generic‑плана в седьмом и последующих вызовах. Остаются вопросы:



  • почему именно в 6‑м вызове происходит этот скачок до 52 блокировок, и можно ли его избежать?

  • почему мы блокируем все 12 секций, хотя во время выполнения 11 из них отсекаются?

Разберёмся подробнее.



6‑й вызов: почему 52 блокировки?


В статье "LWLock:LockManager и подготовленные операторы мы изучили механику блокирования для не секционированных таблиц. Здесь действует тот же шаблон, но с принципиальным отличием: хотя в первых пяти вызовах у нас эффективно срабатывало отсечение секций на этапе планирования, при построении generic‑плана в 6‑м вызове оно не применяется.


Проследим выполнение (по исходникам PG18; // комментарии мои).



Шаг 1: получаем блокировки планировщика

GetCachedPlan() начинает с блокировки дерева запроса через AcquirePlannerLocks():



AcquirePlannerLocks(plansource->query_list, true);


В дереве запроса (результат парсера) фигурирует только родительская таблица. Итог — получено 4 блокировки (родительская таблица + 3 родительских индекса). Блокировки секций будут получены позже, на этапе планирования.



Шаг 2: выбор типа плана

Функция choose_custom_plan() решает, использовать custom или generic‑план:



if (plansource->num_custom_plans < 5)
return true; // NOT taken (num_custom_plans = 5)
if (plansource->generic_cost < avg_custom_cost)
return false; // TAKEN (generic_cost = -1, meaning "not yet calculated")


Итог: строим generic‑план.



Шаг 3: строим generic‑план

Так как кэшированного плана ещё нет (CheckCachedPlan() вернул false), строим новый:



plan = BuildCachedPlan(plansource, qlist, NULL, queryEnv);
// ^^^^ NULL = нет связанных параметров


Внутри шага 3: отсечение секций не срабатывает

На этапе планирования prune_append_rel_partitions() пытается отсечь секции. Она вызывает get_matching_partitions(), которая в итоге упирается в критическую проверку:



paramids = pull_exec_paramids(expr);
if (!bms_is_empty(paramids)) // TRUE - $1 распознан как PARAM_EXEC
{
context->has_exec_param = true;
if (context->target != PARTTARGET_EXEC) // TRUE - мы в планировщике
return PARTCLAUSE_UNSUPPORTED; // ОТСЕЧЕНИЕ НЕВОЗМОЖНО
}


Почему отсечение не сработало:



  • в запросе есть WHERE event_time = $1;

  • при построении generic‑плана нет boundParams;

  • параметр распознан как PARAM_EXEC;

  • контекст — PARTTARGET_PLANNER (а не исполнитель);

  • возвращается PARTCLAUSE_UNSUPPORTED.



Итог — get_matching_partitions() возвращает все секции:



/ Если ничего пригодного нет, вернуть все секции /
if (pruning_steps == NIL)
return bms_add_range(NULL, 0, rel->nparts - 1); // ВСЕ 12 СЕКЦИЙ


Всё ещё внутри шага 3: разворачиваем все секции

Поскольку отсечения не произошло, планировщик открывает каждую секцию через try_table_open():



childrel = try_table_open(childOID, lockmode);


Все 12 секций и их 36 индексов открываются и добавляются в rtable объекта PlannedStmt (range table — список всех таблиц и индексов, фигурирующих в плане).



Возврат на верхний уровень: кэшируем план

После возврата из BuildCachedPlan() план кэшируется:



plansource->gplan = plan;  // Кэшируем план со ВСЕМИ секциями


Теперь в кэше хранится generic‑план, в чьей range table перечислены все 52 отношения. Эти 52 блокировки планировщика удерживаются до конца транзакции.



Резюмируя механику 6‑го вызова:



GetCachedPlan()
├─ AcquirePlannerLocks() → 4 блокировки
├─ choose_custom_plan() → решаем строить generic
└─ BuildCachedPlan()
└─ pg_plan_queries()
└─ standard_planner()
└─ build_simple_rel() / expand_inherited_rtentry()
├─ prune_append_rel_partitions() → отсечение не сработало
├─ try_table_open() для каждой секции → +12 блокировок (таблицы)
└─ get_relation_info() для индексов → +36 блокировок (индексы)
[возвращаем план с 52 отношениями]
└─ Кэшируем → plansource->gplan = plan


Итак, на 6‑м выполнении мы строим generic‑план, но без значений параметров планировщик не может отсечь секции на этапе планирования. Он обязан учитывать все секции, блокируя все 52 отношения.

Trackbacks

No Trackbacks

Comments

Display comments as Linear | Threaded

No comments

The author does not allow comments to this entry

Add Comment

Enclosing asterisks marks text as bold (*word*), underscore are made via _word_.
Standard emoticons like :-) and ;-) are converted to images.

To prevent automated Bots from commentspamming, please enter the string you see in the image below in the appropriate input box. Your comment will only be submitted if the strings match. Please ensure that your browser supports and accepts cookies, or your comment cannot be verified correctly.
CAPTCHA

Form options

Submitted comments will be subject to moderation before being displayed.