Learning Platform
Глоссарий Troubleshooting
Урок 14.01 · 22 мин
Средний
buffer-managermemoryinternalsout-of-core

Buffer manager: как DuckDB управляет памятью

DuckDB умеет обрабатывать датасеты больше оперативной памяти. Не «иногда» и не «с оговорками» — это штатный режим: на ноутбуке с 16 ГБ RAM DuckDB прогоняет запросы по 100+ ГБ данных. Чтобы понять, как это вообще возможно, нужно начать с компонента, который этим управляет, — buffer manager (буферный менеджер).

Этот урок — фундамент всего модуля. Buffer manager решает, что держать в памяти, а что — на диске; он же первым реагирует на нехватку памяти. Все механизмы out-of-core исполнения — внешняя агрегация, внешний join, внешняя сортировка — стоят на нём. Без понимания буферного менеджера остальной модуль будет набором не связанных приёмов.


Зачем нужен буферный менеджер

Наивная модель работы с данными — «загрузить всё в RAM и работать». Она ломается на двух классах данных:

  • Данные больше RAM: датасет на 100 ГБ просто не поместится в 16 ГБ памяти.
  • Промежуточные результаты больше RAM: даже если исходная таблица помещается, hash-таблица join или состояние агрегации в процессе запроса могут раздуться сверх памяти.

Buffer manager — это слой между «логическими данными, которые нужны движку» и «физической памятью, которой ограниченно».

Trino: модель памяти — user, revocable и heap headroom

DataFusion: управление памятью — MemoryPool и spill Его задача — создать иллюзию, что памяти достаточно, перемещая данные между RAM и диском так, чтобы движок мог работать, не зная деталей.

Аналогия — виртуальная память операционной системы: процесс адресует больше памяти, чем есть физически, а ОС подкачивает страницы с диска. Buffer manager DuckDB делает то же самое, но на уровне СУБД и для своих структур данных — и потому делает это умнее, чем мог бы делать general-purpose своп ОС, потому что знает семантику данных.

Buffer manager как слой между движком и хранилищем
Движок исполненияОператоры запроса запрашивают блоки данных, не зная, в RAM они или на диске
запрос блока / pin
Buffer managerРешает, держать блок в памяти или вытеснить на диск; следит за бюджетом памяти
загрузка / вытеснение
RAMБыстрая память; вмещает ограниченное число блоков в рамках memory_limit
ДискФайл базы и temp_directory; сюда вытесняются блоки, не помещающиеся в RAM

Что считается «памятью» для buffer manager

Buffer manager управляет не одной сущностью, а несколькими типами потребителей памяти:

  • Блоки данных таблиц. Когда движок читает таблицу, её колоночные блоки (по умолчанию 256 КиБ каждый) загружаются в память через buffer manager.
  • Буферы под векторы и DataChunk-и. Векторизованное исполнение оперирует чанками по ~2048 значений; память под них тоже учитывается.
  • Состояние операторов с накоплением. Hash-таблица join, hash-таблица агрегации, буфер сортировки — это структуры, которые растут по ходу запроса и могут занять много памяти.
  • Кэш блоков. Уже прочитанные блоки буферный менеджер старается удерживать в памяти, чтобы не перечитывать их с диска.

Все эти потребители делят один общий бюджет — memory_limit. Buffer manager — арбитр, который этот бюджет распределяет и следит, чтобы суммарное потребление его не превысило.


Pin и unpin: как блок удерживается в памяти

Базовая операция буферного менеджера — pin / unpin. Когда оператору нужен блок данных, он просит buffer manager «закрепить» (pin) блок: тот гарантирует, что блок находится в памяти и не будет вытеснен, пока он закреплён. Закончив работу с блоком, оператор его «открепляет» (unpin) — теперь buffer manager волен вытеснить блок, если памяти не хватает.

Жизненный цикл блока в buffer manager
На дискеБлок не нужен прямо сейчас, хранится в файле
pin: загрузить в RAM
Закреплён в RAMОператор работает с блоком; buffer manager не может его вытеснить
unpin: освободить
В RAM, можно вытеснитьБлок ещё в памяти как кэш, но при нехватке памяти будет вытеснен

Закреплённый блок вытеснить нельзя — он используется прямо сейчас. Открепленный блок остаётся в памяти как кэш: если он понадобится снова, не придётся читать с диска. Но именно открепленные блоки — кандидаты на вытеснение, когда buffer manager упирается в memory_limit.

Тонкость: блоки таблицы, прочитанные с диска и не изменённые, при вытеснении просто отбрасываются — их копия и так лежит в файле базы, перечитать можно в любой момент. А вот промежуточные данные запроса (содержимое hash-таблицы, буфера сортировки), которых на диске ещё нет, при вытеснении нужно сначала записать в temp-файл. Это и есть «спилл» (spill), о котором подробно — в следующих уроках.


Вытеснение при нехватке памяти

Когда buffer manager обнаруживает, что суммарное потребление приближается к memory_limit, он начинает освобождать память — вытесняет открепленные блоки. Стратегия выбора жертвы основана на принципе «давно не использованный блок вытесняем первым» (классическая идея LRU — least recently used). Логика: блок, к которому давно не обращались, скорее всего не понадобится в ближайшее время.

Процесс:

  1. Потребление памяти подошло к memory_limit.
  2. Buffer manager ищет открепленные блоки — кандидаты на вытеснение.
  3. Выбирает наименее недавно использованные.
  4. Если блок «чистый» (есть копия на диске) — просто отбрасывает его из памяти. Если «грязный» (промежуточные данные без копии на диске) — пишет в temp-файл, затем освобождает память.
  5. Освобождённая память выдаётся тому, кто её запросил.
-- посмотреть текущий лимит и потребление
SELECT current_setting('memory_limit');
-- 12.8 GiB   (по умолчанию ~80% физической RAM)

-- наблюдать использование памяти движком
SELECT * FROM duckdb_memory();
-- покажет потребление по категориям: hash table, sort, ...

Функция duckdb_memory() показывает разбивку текущего потребления по типам потребителей — это окно в то, что сейчас держит buffer manager.

NOTE

По умолчанию memory_limit равен примерно 80% физической оперативной памяти машины. Оставшиеся 20% — запас для самой ОС, других процессов и накладных расходов. Этот лимит можно менять командой SET memory_limit, и в следующем уроке мы увидим, зачем его иногда специально занижают.


Почему buffer manager умнее свопа ОС

Возникает резонный вопрос: зачем DuckDB свой buffer manager, если у ОС уже есть виртуальная память и своп — пусть ОС и подкачивает страницы? Ответ — в семантике.

Своп ОС работает со страницами памяти вслепую: он не знает, что в странице, что важнее, что скоро понадобится. Он реагирует постфактум, когда память уже кончилась, и часто принимает плохие решения — выгружает то, что вот-вот понадобится.

Buffer manager DuckDB знает контекст:

  • Он знает, что именно лежит в каждом блоке — данные таблицы, hash-таблица, буфер сортировки.
  • Он знает, что блок чистый (можно отбросить) или грязный (надо записать).
  • Он управляет вытеснением проактивно, до того как память кончилась, выбирая жертвы осмысленно.
  • Он координируется с операторами: например, внешняя агрегация специально устроена так, чтобы её состояние можно было спилить аккуратными партициями, а не случайными страницами.

Поэтому полагаться на своп ОС — плохая идея: запрос будет работать, но в разы медленнее и непредсказуемо. Правильный подход — дать DuckDB честный memory_limit и temp_directory, и пусть buffer manager управляет спиллом сам. На многих системах своп вообще стоит отключать для процесса с DuckDB, чтобы под нагрузкой не словить деградацию из-за слепой подкачки ОС.

Своп ОСBuffer manager DuckDB
Знает содержимое страницы/блоканетда
Различает чистые и грязные данныенетда
Момент реакциипостфактум, память кончиласьпроактивно, до исчерпания
Координация с операторами запросанетда
Результат под нагрузкойнепредсказуемая деградацияконтролируемый спилл

Buffer manager и параллелизм

DuckDB исполняет запрос на многих потоках. Все они делят один buffer manager и один общий memory_limit. Это значит, что бюджет памяти — общий ресурс между потоками: если восемь потоков параллельно строят части hash-таблицы, суммарно они должны уложиться в memory_limit, а не каждый в него.

Отсюда практическое следствие, к которому мы вернёмся в уроке про бюджет памяти на поток: чем больше потоков, тем меньше памяти приходится на каждый, и тем выше шанс, что операторам придётся спилить. Buffer manager и настройка threads связаны: число потоков влияет на то, как делится память.


Блоки и почему именно 256 КиБ

Buffer manager оперирует не отдельными байтами и не строками, а блоками фиксированного размера — по умолчанию 256 КиБ. Данные таблицы на диске хранятся блоками, и в память они поднимаются тоже блоками. Это типичное для СУБД решение, и размер блока — компромисс.

Слишком мелкий блок означал бы много отдельных операций ввода-вывода и раздутую структуру учёта (на каждый блок — запись в таблицах буферного менеджера). Слишком крупный блок означал бы грубую гранулярность: чтобы поднять одну нужную строку, пришлось бы загрузить большой блок целиком, и память тратилась бы на лишнее. 256 КиБ — баланс: блок достаточно велик, чтобы амортизировать накладные расходы на одну операцию I/O, и достаточно мал, чтобы вытеснение и загрузка были гибкими.

Для buffer manager фиксированный размер блока ещё и упрощает управление: все «слоты» в памяти одинаковы, освобождённое место под один блок всегда годится под любой другой. Это убирает фрагментацию, которая мучила бы менеджер при блоках переменного размера.

NOTE

Блок (256 КиБ) — единица, которой buffer manager перемещает данные между RAM и диском. Это не то же, что vector (~2048 значений, единица векторизованного исполнения) и не row group (~122 880 строк, единица хранилища). У DuckDB несколько уровней гранулярности под разные задачи: vector — для исполнения, row group — для организации хранения и пропуска по статистике, блок — для управления памятью. Не путайте их.


Попробуй сам

  1. Запусти DuckDB и посмотри лимит по умолчанию: SELECT current_setting('memory_limit'); Сопоставь с объёмом RAM твоей машины — близко ли к 80%?
  2. Выполни SELECT * FROM duckdb_memory(); на «холодной» сессии — что показывает потребление, пока запросов не было?
  3. Создай большую таблицу: CREATE TABLE big AS SELECT range AS id, range % 1000 AS k FROM range(20000000); Снова посмотри duckdb_memory() — изменилось ли потребление?
  4. Выполни тяжёлый запрос с группировкой: SELECT k, count(*) FROM big GROUP BY k; и сразу после — SELECT * FROM duckdb_memory(); Видно ли категорию памяти под hash-таблицу агрегации?
  5. Поставь маленький лимит: SET memory_limit = '200MB'; и повтори запрос. Он всё ещё отрабатывает? Подумай, что должен был сделать buffer manager, чтобы запрос завершился при таком лимите.

ClickHouse: memory tracker и управление памятью на запрос
Проверка знанийKnowledge check
Почему DuckDB реализует собственный buffer manager вместо того, чтобы полагаться на своп операционной системы для обработки данных больше RAM?
ОтветAnswer
Своп ОС работает со страницами памяти вслепую: он не знает, что находится в странице, насколько эти данные важны и понадобятся ли скоро. Он реагирует постфактум, когда память уже исчерпана, и часто выгружает именно то, что вот-вот понадобится. Buffer manager DuckDB знает семантику данных: он знает, что лежит в каждом блоке (данные таблицы, hash-таблица join, буфер сортировки), различает чистые блоки (есть копия на диске, можно просто отбросить) и грязные (промежуточные данные, которые надо записать в temp-файл перед вытеснением), управляет вытеснением проактивно — до исчерпания памяти — и выбирает жертвы осмысленно по принципу LRU. Кроме того, он координируется с операторами: например, внешняя агрегация специально устроена так, чтобы её состояние спиливалось аккуратными партициями, а не случайными страницами. В результате при нехватке памяти DuckDB даёт контролируемый предсказуемый спилл, тогда как своп ОС привёл бы к непредсказуемой деградации в разы. Поэтому правильный подход — задать честный memory_limit и temp_directory, а своп для процесса с DuckDB часто стоит отключать.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какова основная роль buffer manager в DuckDB?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6