Перейти к содержимому
Последнее обновление

Стратегия управления памятью

Doris Allocator служит единой точкой входа для выделения крупных блоков памяти в системе. Он своевременно вмешивается в процесс выделения памяти, ограничивая его и обеспечивая эффективное и контролируемое использование памяти.

Doris MemoryArbitrator — это арбитр памяти, который в реальном времени отслеживает потребление памяти процессом Doris BE, регулярно обновляет сведения о состоянии памяти и собирает снимки статистики, связанной с памятью.

Doris MemoryReclamation — это механизм освобождения памяти, который при недостатке доступной памяти запускает GC для освобождения её части, обеспечивая стабильное выполнение большинства задач в кластере.

Doris Allocator​

Обзор управления памятью

Allocator запрашивает память у системы и использует MemTracker для отслеживания объёмов выделяемой и освобождаемой памяти. Крупными блоками памяти, необходимыми для пакетного выполнения операторов, управляют различные структуры данных.

В процессе выполнения запроса выделением крупных блоков памяти в основном управляют структуры данных Arena, HashTable и PODArray. Allocator служит единым интерфейсом работы с памятью для Arena, PODArray и HashTable, обеспечивая единое управление памятью и её локальное повторное использование.

Выделение памяти

Allocator использует универсальный аллокатор для запроса памяти. При выборе между Jemalloc и TCMalloc учитывался предыдущий опыт Doris: в тестах с большим числом одновременных запросов на Spin Lock структуры CentralFreeList в TCMalloc приходилось 40% общего времени выполнения запросов. Хотя отключение агрессивного decommit памяти позволяет повысить производительность, оно приводит к значительному перерасходу памяти. Поэтому для периодического освобождения кеша TCMalloc приходится использовать отдельный поток. Jemalloc превосходит TCMalloc при большом числе одновременных запросов и является зрелым и стабильным решением. В Doris 1.2.2 был выполнен переход на Jemalloc. После настройки его производительность в большинстве сценариев сопоставима с TCMalloc, а потребление памяти ниже. Производительность при большом числе одновременных запросов также значительно улучшилась.

Arena​

Arena — это пул памяти, который хранит список блоков памяти и выделяет из них память в ответ на запросы alloc. Это сокращает число обращений к системе за памятью и повышает производительность. Блок памяти называется Chunk. Он существует на протяжении всего жизненного цикла пула памяти и освобождается вместе с остальными блоками при уничтожении пула. Обычно жизненный цикл пула совпадает с жизненным циклом запроса. Arena также поддерживает выравнивание памяти и в основном используется для хранения сериализованных/десериализованных данных в процессе Shuffle, сериализованных Key в HashTable и т. д.

Начальный размер Chunk составляет 4096 байт. Для отслеживания позиции выделенной памяти внутри него используется курсор. Если оставшейся памяти в текущем Chunk недостаточно для выполнения текущего запроса, запрашивается новый Chunk и добавляется в список. Чтобы сократить число обращений к системе за памятью, пока размер текущего Chunk меньше 128M, размер каждого нового Chunk удваивается. Когда размер текущего Chunk превышает 128M, размер нового Chunk выбирается в пределах 128M при условии удовлетворения текущего запроса памяти, чтобы избежать избыточного расхода памяти. По умолчанию предыдущий Chunk больше не участвует в последующих операциях alloc.

HashTable​

HashTable в Doris в основном используется в Hash Join, агрегации, операциях над множествами и оконных функциях. Основная используемая реализация PartitionedHashTable содержит до 16 вложенных HashTable и поддерживает параллельное слияние двух HashTable. Каждый вложенный Hash Join расширяется независимо, что должно уменьшить суммарное потребление памяти и распределить задержки при расширении.

Когда размер HashTable меньше 8M, она расширяется в 4 раза. Когда размер HashTable превышает 8M, она расширяется в 2 раза. Пока размер HashTable меньше 2G, коэффициент расширения составляет 50%, то есть расширение запускается при заполнении HashTable на 50%. После превышения размера 2G коэффициент расширения меняется на 75%. Чтобы избежать избыточного расхода памяти, HashTable обычно заранее расширяют с учётом объёма данных перед её построением. Кроме того, в Doris разработаны разные HashTable для разных сценариев. Например, PHmap используется для оптимизации производительности при параллельной работе в сценариях агрегации.

PODArray​

PODArray — это динамический массив элементов типа POD. В отличие от std::vector, он не инициализирует элементы, поддерживает часть интерфейсов std::vector, выравнивание памяти и расширение в 2 раза. При уничтожении PODArray деструкторы отдельных элементов не вызываются, а сразу освобождается весь блок памяти. Он в основном используется для хранения данных столбцов, например String. Кроме того, PODArray широко применяется при вычислении функций и фильтрации по выражениям.

Повторное использование памяти​

В слое выполнения Doris активно применяется повторное использование памяти, и большинство заметных узких мест, связанных с памятью, уже устранено. Например, блоки данных Block повторно используются на протяжении всего выполнения запроса. На стороне Sender в Shuffle всегда сохраняется один Block для приёма данных, а ещё один Block попеременно используется при передаче по RPC. При чтении Tablet слой хранения повторно использует столбец предиката для чтения, фильтрации и копирования данных в Block верхнего слоя, а затем выполняет Clear. При загрузке таблицы Aggregate Key, когда MemTable с кешированными данными достигает определённого размера, выполняется предварительная агрегация, после которой MemTable уменьшается и запись продолжается, и т. д.

Кроме того, перед началом Scan данных Doris заранее выделяет набор Free Blocks с учётом числа Scanners и потоков. Каждый раз при запуске Scanner планировщиком из этого набора берётся Block и передаётся слою хранения для чтения данных. После завершения чтения Block помещается в очередь производителя, откуда оператор верхнего слоя забирает его для дальнейших вычислений. После копирования данных оператор верхнего слоя возвращает Block в Free Block для следующего запуска Scanner планировщиком, обеспечивая повторное использование памяти. После завершения Scan данных все Free Block освобождаются в тех же потоках, где они были предварительно выделены. Это позволяет избежать дополнительных накладных расходов, возникающих при выделении и освобождении памяти в разных потоках. Количество Free Blocks также в определённой степени определяет параллелизм Scan данных.

GC памяти​

Doris BE периодически получает от системы сведения об объёме физической памяти, используемой процессом, и текущем объёме доступной системной памяти, а также собирает снимки MemTracker всех задач запросов, загрузки и compaction. Когда потребление памяти процессом BE превышает лимит или системе не хватает доступной памяти, Doris освобождает кеш и завершает некоторые запросы или загрузки, чтобы высвободить память. Этот процесс регулярно выполняется отдельным потоком GC.

GC памяти

Minor GC запускается, когда потребление памяти процессом Doris BE превышает SoftMemLimit (по умолчанию 81% общего объёма системной памяти) или оставшийся объём доступной системной памяти опускается ниже порогового уровня Warning (обычно не более 3.2GB). В этот момент выполнение запроса приостанавливается при выделении памяти через Allocator, данные из кеша принудительно загружаются, а также освобождаются часть Data Page Cache и устаревшие записи Segment Cache. Если освобождено менее 10% памяти процесса и включён перерасход памяти запросами, запросы с большой долей перерасхода памяти отменяются, пока не будет освобождено 10% памяти процесса или не останется запросов, которые можно отменить. Затем уменьшаются интервал получения сведений о состоянии системной памяти и интервал GC. Остальные запросы продолжают выполняться, когда обнаруживается доступная память.

Если потребление памяти процессом BE превышает MemLimit (по умолчанию 90% общего объёма системной памяти) или оставшийся объём доступной системной памяти опускается ниже порогового уровня Low (обычно не более 1.6GB), запускается Full GC. В дополнение к перечисленным действиям загрузка также приостанавливается при принудительном сбросе данных кеша, а весь Data Page Cache и большая часть остальных кешей освобождаются. Если освобождено менее 20% памяти, начинается поиск в списке MemTracker всех запросов и загрузок по определённой стратегии. Сначала отменяются запросы с высоким потреблением памяти, затем загрузки с большой долей перерасхода памяти и загрузки с высоким потреблением памяти, пока не будет освобождено 20% памяти процесса. Затем увеличиваются интервал получения сведений о состоянии системной памяти и интервал GC, а остальные запросы и загрузки продолжают выполняться. Обычно GC занимает от сотен микросекунд до десятков миллисекунд.

Метод расчёта лимитов памяти и пороговых уровней​

  • Лимит памяти процесса MemLimit = be.conf/mem_limit * PhysicalMemory, по умолчанию составляет 90% общего объёма системной памяти, подробнее см. .

  • Мягкий лимит памяти процесса SoftMemLimit = be.conf/mem_limit * PhysicalMemory * be.conf/soft_mem_limit_frac, по умолчанию составляет 81% общего объёма системной памяти.

  • Нижний порог оставшейся доступной системной памяти LowWaterMark = be.conf/max_sys_mem_available_low_water_mark_bytes, значение по умолчанию -1. В этом случае LowWaterMark = min(PhysicalMemory - MemLimit, PhysicalMemory * 0.05). На машине с 64G памяти значение LowWaterMark немного меньше 3.2 GB (поскольку фактическое значение PhysicalMemory часто меньше 64G).

  • Предупреждающий порог оставшейся доступной системной памяти WarningWaterMark = 2 * LowWaterMark. На машине с 64G памяти значение WarningWaterMark по умолчанию немного меньше 6.4 GB.

Расчёт оставшейся доступной памяти в системе​

Если объём доступной памяти, указанный в сообщении об ошибке, ниже нижнего порога, это также считается превышением лимита памяти процесса. Значение доступной системной памяти берётся из MemAvailable в /proc/meminfo. Если MemAvailable недостаточно, последующие запросы на выделение памяти могут вернуть std::bad_alloc или привести к OOM процесса BE. Поскольку обновление статистики памяти процесса и GC памяти BE выполняются с некоторой задержкой, небольшой запас памяти резервируется в виде нижнего порога, чтобы по возможности избежать OOM.

Здесь MemAvailable — это общий объём памяти, который можно предоставить пользовательским процессам, по возможности не задействуя swap. Операционная система определяет его на основе текущего объёма свободной памяти, буферов, кеша, фрагментации памяти и других факторов. Упрощённая формула расчёта: MemAvailable = MemFree - LowWaterMark + (PageCache - min(PageCache / 2, LowWaterMark)). Это значение совпадает со значением available, которое показывает команда free. Подробнее см.:

Почему MemAvailable значительно меньше суммы MemFree, buffers и cached

MemAvailable в Linux

Нижний порог по умолчанию составляет 3.2G (1.6G до 2.1.5). Он рассчитывается на основе MemTotal, vm/min_free_kbytes, confg::mem_limit, config::max_sys_mem_available_low_water_mark_bytes и позволяет избежать избыточного расхода памяти. Здесь MemTotal — это общий объём системной памяти, значение которого также берётся из /proc/meminfo; vm/min_free_kbytes задаёт буфер, резервируемый операционной системой для процесса GC памяти, и его значение обычно составляет от 0.4% до 5%. На некоторых облачных серверах значение vm/min_free_kbytes может составлять 5%, из-за чего отображаемый объём доступной системной памяти оказывается меньше фактического. Увеличение config::max_sys_mem_available_low_water_mark_bytes позволит зарезервировать больший запас памяти для Full GC на машинах с объёмом памяти более 64G, а уменьшение этого значения позволит использовать память максимально полно.