Подготовленные операторы и лавина блокировок на секционированной таблице, часть 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
The author does not allow comments to this entry
Comments
Display comments as Linear | Threaded