Linux что такое slab

Обновлено: 06.07.2024

Слабовый распределитель 5

Текущее состояние слабового распределителя можем рассмотреть в файловой системе /proc (что даёт достаточно много для понимания самого принципа слабового распределения):

В версии 2.6.18 и практически во всей литературе этот вызов описан так:

name — строка имени кэша;

size — размер элементов кэша (единый и общий для всех элементов);

offset — смещение первого элемента от начала кэша (для обеспечения соответствующего выравнивания по границам страниц, достаточно указать 0, что означает выравнивание по умолчанию);

flags — опциональные параметры (может быть 0);

ctor, dtor — конструктор и деструктор, соответственно, вызываются при размещении-освобождении каждого элемента, но с некоторыми ограничениями . например, деструктор будет вызываться (финализация), но не гарантируется, что это будет поисходить сразу непосредственно после удаления объекта.

К версии 2.6.24 [5, 6] он становится другим (деструктор исчезает из описания):

Наконец, в 2.6.32, 2.6.35 и 2.6.35 можем наблюдать следующую фазу изменений (меняется прототип конструктора):

Это значит, что то, что компилировалось для одного ядра, перестанет компилироваться для следующего. Вообще то, это достаточно обычная практика для ядра, но к этому нужно быть готовым, а при использовании таких достаточно глубинных механизмов, руководствоваться не навыками, а изучением заголовочных файлов текущего ядра.

Из флагов создания, поскольку они также находятся в постоянном изменении, и большая часть из них относится к отладочным опциям, стоит назвать:

SLAB_HWCACHE_ALIGN — расположение каждого элемента в слабе должно выравниваться по строкам процессорного кэша, это может существенно поднять производительность, но непродуктивно расходуется память;

SLAB_POISON — начально заполняет слаб предопределённым значением (A5A5A5A5) для обнаружения выборки неинициализированных значений;

Если не нужны какие-то особые изыски, то нулевое значение будет вполне уместно для параметра flags .

Как для любой операции выделения, ей сопутствует обратная операция по уничтожению слаба:

Операция уничтожения может быть успешна (здесь достаточно редкий случай, когда функция уничтожения возвращает значение результата), только если уже все объекты, полученные из кэша, были возвращены в него. Таким образом, модуль должен проверить статус, возвращённый kmem_cache_destroy() ; ошибка указывает на какой-то вид утечки памяти в модуле (так как некоторые объекты не были возвращены).

После того, как кэш объектов создан, вы можете выделять объекты из него, вызывая:

Здесь flags - те же, что передаются kmalloc() .

Полученный объект должен быть возвращён когда в нём отпадёт необходимость :

Несмотря на изменчивость API слаб алокатора, вы можете охватить даже диапазон версий ядра, пользуясь директивами условной трансляции препроцессора; модуль использующий такой алокатор может выглядеть подобно следующему (архив slab.tgz ):

А вот как выглядит выполнение этого размещения (картина весьма поучительная, поэтому остановимся на ней подробнее):

$ sudo insmod ./slab.ko

$ dmesg | tail -n300 | grep -v audit

$ cat /proc/slabinfo | grep my_

$ sudo rmmod slab

Итого: объекты размером 7 байт благополучно разместились в новом слабе с именем my_cache , отображаемом в /proc/slabinfo , организованным с размером элементов 16 байт (эффект выравнивания?), конструктор при размещении 31 таких объектов вызывался 257 раз. Обратим внимание на чрезвычайно важное обстоятельство: при создании слаба никаким образом не указывается реальный или максимальный объём памяти, находящейся под управлением этого слаба: это динамическая структура, «добирающая» столько страниц памяти, сколько нужно для поддержания размещения требуемого числа элементов данных (с учётом их размера). Увеличенное число вызовов конструктора можно отнести: а). на необходимость переразмещения существующих элементов при последующих запросах, б). эффекты SMP (2 ядра) и перераспределения данных между процессорами. Проверим тот же тест на однопроцессорном Celeron и более старой версии ядра:

$ sudo /sbin/insmod ./slab.ko

$ /sbin/lsmod | grep slab

$ cat /proc/slabinfo | grep my_

$ sudo /sbin/rmmod slab

Число вызовов конструктора не уменьшилось, а даже возросло, а вот размер объектов, под который создан слаб, изменился с 16 на 8.

Примечание: Если рассмотреть 3 первых поля вывода /proc/slabinfo , то и в первом и во втором случае видно, что под слаб размечено некоторое фиксированное количество фиксированных объекто-мест (339 в последнем примере), которые укладываются в некоторый начальный объём слаба меньше или порядка 1-й страницы физической памяти.

А вот тот же тест при больших размерах объектов и их числе:

$ sudo insmod ./slab.ko size=1111 number=300

$ sudo rmmod slab

$ sudo insmod ./slab.ko size=1111 number=3000

$ sudo rmmod slab

Примечание: Последний рассматриваемый пример любопытен в своём поведении. Вообще то «завалить» операционную систему Linux — ничего не стоит, когда вы пишете модули ядра. В противовес тому, что за несколько лет плотной (почти ежедневной) работы с микроядерной операционной системой QNX мне так и не удалось её «завалить» ни разу (хотя попытки и предпринимались). Это, попутно, к цитировавшемуся ранее эпиграфом высказыванию Линуса Торвальдса относительно его оценок микроядерности. Но сейчас мы не о том. Если погонять показанный тест с весьма большим размером блока и числом блоков для размещения (заметно больше показанных выше значений), то можно наблюдать прелюбопытную ситуацию: нет, система не виснет, но распределитель памяти настолько активно отбирает память у системы, что постепенно угасают все графические приложения, потом и вся подсистема X11 . но остаются в живых чёрные текстовые консоли, в которых даже живут мыши. Интереснейший получается эффект 6 .

Ещё одна вариация на тему распределителя памяти, в том числе и слаб-алокатора — механизм пула памяти:

Пул памяти сам по себе вообще не является алокатором, а всего лишь является интерфейсом к алокатору (к тому же кэшу, например). Само наименование «пул» (имеющее схожий смысл в разных контекстах и разных операционных системах) предполагает, что такой механизм будет всегда поддерживать «в горячем резерве» некоторое количество объектов для распределения. Аргумент вызова min_nr является тем минимальным числом выделенных объектов, которые пул должен всегда поддерживать в наличии. Фактическое выделение и освобождение объектов по запросам обслуживают alloc_fn() и free_fn() , которые предлагается написать пользователю, и которые имеют такие прототипы:

Последний параметр mempool_create() - pool_data передаётся последним параметром в вызовы alloc_fn() и free_fn() .

Но обычно просто дают обработчику-распределителю ядра выполнить за нас задачу — объявлено ( <linux/mempool.h> ) несколько групп API для разных распределителей памяти. Так, например, существуют две функции, например, ( mempool_alloc_slab() и mempool_free_slab() ), ориентированный на рассмотренный уже слаб алокатор, которые выполняют соответствующие согласования между прототипами выделения пула памяти и kmem_cache_alloc() и kmem_cache_free() . Таким образом, код, который инициализирует пул памяти, который будет использовать слаб алокатор для управления памятью, часто выглядит следующим образом:

После того, как пул был создан, объекты могут быть выделены и освобождены с помощью:

После создания пула памяти функция выделения будет вызвана достаточное число раз для создания пула предопределённых объектов. После этого вызовы mempool_alloc_slab() пытаются получить новые объекты от функции выделения - возвращается один из предопределённых объектов (если таковые сохранились). Когда объект освобождён mempool_free_slab() , он сохраняется в пуле если количество предопределённых объектов в настоящее время ниже минимального, в противном случае он будет возвращён в систему.

Примечание: Такие же группы API есть для использования в качестве распределителя памяти для пула kmalloc() ( mempool_kmalloc() ) и страничного распределителя памяти ( mempool_alloc_pages() ).

Размер пула памяти может быть динамически изменён:

- в случае успеха этот вызов изменяет размеры пула так, чтобы иметь по крайней мере new_min_nr объектов.

Когда пул памяти больше не нужен он возвращается системе:

5) В литературе (публикациях) мне встречалось русскоязычное наименование такого распределителя как: «слабовый», «слябовый», «слэбовый». Поскольку термин нужно как-то именовать, а ни одна из транскрипций не лучше других, то я буду пользоваться именно первым произношением из перечисленных.

6) Что напомнило высказывание классика отечественного юмора М. Жванецкого: «А вы не пробовали слабительное со снотворным? Удивительный получается эффект!».

Прежде чем говорить о распределителе SLAB, позвольте мне рассказать о двух понятиях: внутренняя фрагментация и внешняя фрагментация.

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


Если процессу теперь необходимо подать заявку на пространство памяти 32 КБ с непрерывными адресами из операционной системы, обратите внимание, что адреса являются непрерывными.На самом деле в настоящее время в системе имеется 10 КБ + 23 КБ = 33 КБ свободной памяти, но эта свободная память не является непрерывной, поэтому они не могут удовлетворить запрос процесса. Это называется внешней фрагментацией. Главная причина внешней фрагментации заключается в том, что процессы или системы часто применяют и освобождают набор последовательных фреймов страниц разных размеров. Чтобы максимально избежать внешней фрагментации в операционной системе Linux, используется алгоритм Buddy System.

Внутренние фрагментыЭто пространство памяти, которое было выделено (которое может четко указывать, какому процессу принадлежит), но не может быть использовано, внутренняя фрагментация - это блок хранения внутри области или внутри страницы. Процесс, занимающий эти области или страницы, не использует этот блок хранения. Когда процесс занимает этот блок хранения, система не может его использовать. Система не может использовать этот блок хранения, пока процесс не освободит его или процесс не завершится. Простой пример показан ниже:


Процесс, примененный для пространства памяти 3 КБ из системы.Система может выделить пространство памяти 4 КБ (стандартная страница) для процесса с помощью алгоритма партнерской системы, в результате чего оставшееся пространство памяти 1 КБ, которое не может быть использовано системой, приводит к трате. Это связано с несоответствием между объемом памяти, запрашиваемым процессом, и объемом памяти, выделенным ему системой. Поскольку алгоритм приятеля использует Page Frame в качестве основной области памяти, он подходит для больших запросов памяти. Во многих случаях объем памяти, запрашиваемый процессом или системой, составляет 4 КБ (стандартная страница), и использование алгоритма партнера неизбежно приведет к значительному расходу системной памяти. Чтобы удовлетворить запрос процесса или системы на небольшой фрагмент памяти, создается распределитель SLAB с меньшей степенью детализации управления памятью. (Примечание: алгоритм SLAB в Linux фактически заимствован из модели SLAB в операционной системе Sun Solaris)

2. Краткое знакомство с дистрибьютором SLAB

Распределитель SLAB фактически построен на алгоритме партнерской системы. Пространство памяти, используемое распределителем SLAB, выделяется с помощью алгоритма партнера, но SLAB реализует собственный алгоритм для этих пространств памяти, а затем управляет небольшим блоком памяти.

Прежде чем объяснить принципы SLAB, мы рассмотрим следующий сценарий: если приложение часто использует определенный тип объекта или часто создает и повторно использует определенный тип объекта, то можем ли мы попытаться сохранить такие объекты отдельно В результате, когда процесс не используется, мы не будем его перерабатывать в настоящее время. При повторном использовании приложения мы вынимаем объекты, которые должны быть переработаны ранее, но просто восстанавливаем объекты, чтобы уменьшить количество одного выпуска и применить к памяти Он работал.

Примечание. Кэш, описанный ниже, не относится к реальному кешу. Реальный кеш относится к аппаратному кешу, который мы обычно называем кешем L1, кешем L2 и кешем L3. Аппаратный кеш предназначен для решения проблемы быстрого ЦП и скорости. Проблема несоответствия скорости между медленной памятью: процессор обращается к кешу быстрее, чем память; если вы помещаете часто используемые данные в аппаратный кеш, процессор напрямую обращается к кешу, не обращаясь к памяти при использовании, тем самым увеличивая скорость системы. В следующем кеше фактически используется программное обеспечение, чтобы заранее открыть пространство в памяти, которое напрямую берется из этого пространства при его использовании и создается распределителем SLAB для облегчения управления небольшими блоками памяти.

3. Принцип работы дистрибьютора SLAB

3.1 Инструкции, связанные с SLAB

(1) SLAB и алгоритм Бадди

Вы можете участвовать в других блогах о внедрении партнерской системы. В системе собеседников алгоритм системы собеседников распределяет пользователю 2 фрейма страницы заказа в соответствии с запросами пользователя, а размер заказа составляет от 0 до 11. Выше упоминается, что дистрибьютор SLAB построен на партнерской системе. Проще говоря, пользовательский процесс или системный процесс применены к SLAB для пространства памяти, выделенного для хранения объектов определенного типа, но в это время в SLAB недостаточно места для конкретного хранения таких объектов, поэтому SLAB походит на систему-партнер, претендующую на степень 2 Второй последовательный физический фрейм страницы. После того, как партнерская система удовлетворяет приложение SLAB, SLAB управляет этим фрагментом памяти для хранения нескольких объектов определенного типа, упомянутых выше.

(2) SLAB и объекты

Объекты на самом деле относятся к определенному типу данных. SLAB предназначается только для одного типа данных (объекта). Чтобы повысить эффективность доступа к объектам, SLAB может выравнивать объекты.

(3) SLAB и кэш на процессор

Для повышения эффективности SLAB-распределитель предоставляет каждую структуру данных CPU struct array_cache для каждого CPU, которая указывает на освобожденный объект. Когда ЦП должен обратиться к пространству памяти объекта, он сначала проверит наличие свободных объектов в массиве array_cache и будет использовать его напрямую, если он есть. Если нет свободных объектов, как дистрибьютор SLAB для применения.

3.2 SLAB-связанная структура данных

Обратите внимание, что код SLAB, описанный в этом разделе, основан на версии ядра Linux v4.7,URL тега。

SLAB Distributor помещает объекты в кеш. Каждый кеш является своего рода «резервом» для объектов одного типа. Область основной памяти, содержащая кэш, разделена на несколько SLAB, и каждый SLAB состоит из одного или нескольких последовательных фреймов страниц, которые содержат как выделенные объекты, так и страницы, содержащие свободные объекты.

В ядре Linux кеш SLAB представлен структурой kmem_cache,Исходный URLЕго определение таково:

В структуре struct kmem_cache более важными являются два атрибута struct array_cache __percpu * cpu_cache и struct kmem_cache_node * node [MAX_NUMNODES]. Аргумент array_cache для кеша SLAB каждого ЦП, kmem_cache_node для кеша SLAB каждого узла NUMA. Среди них структура struct kmem_cache_node определяется следующим образом:Исходный URL

В структуре struct kmem_cache_node есть еще три важных атрибута: slabs_partial, slabs_full, slabs_free, где slabs_partial указывает на связанный список дескрипторов slab, где используются только некоторые объекты, и slabs_full указывает на связанный список дескрипторов slab, где используются все объекты , Slabs_free указывает на связанный список дескрипторов плиты, где не все объекты используются.

Определение дескриптора плиты находится на странице структуры, и соответствующий код выглядит следующим образом:Исходный URL

В структуре struct page есть еще два важных атрибута, связанных с SLAB: s_mem и freelist, где s_mem указывает на адрес первого объекта в slab (или был выделен или свободен). freelist указывает на свободный список связанных объектов.

В структуре struct kmem_cache есть атрибут cpu_cache. Cpu_cache - указатель на массив. Каждый элемент массива соответствует процессору в системе. Каждый элемент массива содержит указатель на struct array_cache, который определяется следующим образом:Исходный URL

Сказав так много, вы, возможно, не знаете точно, о чем все вышеупомянутые структуры. Чтобы лучше понять взаимосвязь между вышеперечисленными структурами, см. Следующую картину:


3.3 SLAB и разделитель рамки страницы

Сначала представим распределитель разделенных фреймов страниц, распределитель разделенных фреймов страниц используется для обработки запросов на выделение памяти для последовательных групп кадров страниц. Существуют некоторые функции и макросы, запрашивающие фреймы страниц. Как правило, все они возвращают линейный адрес первой выделенной страницы. В случае сбоя выделения возвращается NULL. Краткое введение этих функций заключается в следующем:

Когда распределитель SLAB создает новый SLAB, он должен полагаться на распределитель разделенных фреймов страниц, чтобы получить непрерывный набор свободных фреймов страниц. Для этого вам нужно вызвать функцию kmem_getpages (), которая определяется следующим образом:Исходный URL

Среди них параметр cachep указывает на дескриптор кэша кэша, для которого требуется дополнительный фрейм страницы (количество фреймов страницы запроса сохраняется в порядке, определенном в поле cache-> gfporder), флаги указывают, как запрашивать фреймы страницы, а nodeid указывает, с какого узла NUMA Фрейм страницы в памяти. Противоположностью функции kmem_getpages () является kmem_freepages (). Функция kmem_freepages () может освобождать фреймы страниц, выделенные для slab. Функция kmem_freepages () определяется следующим образом:Исходный URL

Чтобы создать новый кэш slab, вызовите функцию kmem_cache_create (). Функция определяется следующим образом:Исходный URL

Функция kmem_cache_create () возвращает указатель на созданный кеш при успешном вызове или NULL в случае сбоя.

Соответствующей функции kmem_cache_create () является функция для уничтожения кэша kmem_cache_destroy (), которая определяется следующим образом:Исходный URL

Следует отметить, что кэш, уничтоженный функцией kmem_cache_destroy (), должен содержать только неиспользуемые объекты. Если кэш содержит используемые объекты, вызов функции kmem_cache_destroy () завершится неудачно. Мы можем легко увидеть.

Давайте посмотрим на пример использования этих двух функций:Исходный URL

Приведенный выше код очень прост, он предназначен для создания и уничтожения трех типов данных: rmap_item, mm_slot, stable_node cache.

3.5 Объекты SLAB и SLAB

После создания кеша определенного типа данных или объекта мы можем выделить и освободить объекты из кеша объекта. Среди них функция kmem_cache_alloc () используется для получения объектов из определенного кэша, функция определяется следующим образом:Исходный URL

Из имени функции, параметров, возвращаемого значения и комментариев легко узнать, что функция kmem_cache_alloc () получает указатель на свободный объект из данного кеша слябов. Фактически, при получении свободных объектов, он сначала найдет свободные объекты из кэша каждого процессора, то есть, array_cache, если нет, он получит бесплатные объекты из kmem_cache_node, если нет, вам нужно использовать алгоритм партнера для выделения новых последовательных страниц. Box, а затем получить бесплатные объекты из нового окна страницы. От kmem_cache_alloc () до общей цепочки вызовов выглядит следующим образом:

kmem_cache_alloc () ——> slab_alloc () ——> __ do_cache_alloc () ——> ____ cache_alloc () ——> cpu_cache_get () (это фактически получение свободных объектов из array_cache) ——> cache_alloc_refill () Выполняется, когда нет свободных объектов в ———— cpu_cache_get () (после выполнения cache_alloc_refill () в основном гарантирует, что в array_cache есть свободные объекты) ——> Возвращает доступные свободные объекты

Шаги выполнения функции cache_alloc_refill () разбиты следующим образом:

cache_alloc_refill () ——> Попытка получить свободные объекты из буфера, общего для всех процессоров узла NUMA (комментарий исходного кода гласит: посмотрим, сможем ли мы повторно заполнить из общего массива), если затем будут возвращены доступные объекты, пополнение заканчивается - -> Получить свободные объекты из плиты в kmem_cache_node, вернуть, если есть, выполнить следующий шаг, если нет—> kmem_getpages ()

Функция kmem_cache_free () используется для освобождения объектов из определенного кэша. Определение функции выглядит следующим образом:Исходный URL

Процесс освобождения объекта аналогичен выделению объекта и не будет описан снова.

3.6 SLAB окраска

3.7 Общий и выделенный кеш

Кэши (не относящиеся к аппаратным кешам) в SLAB делятся на два типа: обычный и выделенный. Нормальный кеш используется только распределителем SLAB для своих собственных целей, в то время как выделенный кеш используется остальной частью ядра. Выделенный кеш создается функцией kmem_cache_create () и отзывается функцией kmem_cache_destroy (), которая используется для хранения объектов (или определенных типов данных). Обычный кеш создается путем вызова kem_cache_init () во время инициализации системы. Функции kmalloc () и kfree () используются для выделения и освобождения места в обычном кэше. Ниже приведен исходный код функции kmem_cache_init ():Исходный URL

Приведенный выше код в сочетании со следующим рисунком облегчит понимание:


Вы можете получить вышеуказанную информацию, используя sudo / proc / slabinfo в Linux. Значение каждого столбца следующее:


Ниже мы сосредоточимся на функции kmalloc (), исходный код которой выглядит следующим образом:Исходный URL

Размер в параметре функции указывает количество байтов, запрошенных для выделения, следует отметить, что функция kmalloc () выделяет непрерывную физическую память.

Противоположностью функции kmalloc () является функция kfree (), которая определяется следующим образом:Исходный URL

Функция kfree () используется для освобождения пространства непрерывной памяти, выделенного функцией kmalloc (). kmalloc () выделяет память ядра, но его выделенный размер ограничен.

3.8 Идеи по улучшению, воплощенные в дистрибьюторе SLAB:

(1) Распределитель SLAB рассматривает область памяти как объект

(2) Распределитель SLAB помещает группу объектов в кеш

(3) Каждый SLAB - это один и тот же тип объекта памяти

Если вышеуказанное содержание неуместно, пожалуйста, раскритиковайте указатель, спасибо!

Рост популярности контейнеров и их использование в совокупности с контрольными группами выявили серьезную проблему масштабируемости, которая приводит к значительному падению производительности на больших машинах. Проблема в том, что время обхода SLAB-кэшей зависит квадратично от количества контейнеров, а активное потребление больших объемов памяти за короткий период может стать причиной ухода системы в busy loop, потребляющий 100% процессорного времени. Сегодня мне хотелось бы рассказать, как мы решили эту проблему, изменив алгоритм учета использования контрольной группой memcg объектов SLAB-кэшей и оптимизировав функцию shrink_slab().

Очистка памяти



Почему вообще встал вопрос оптимизации процессов в ядре? Все началось с того, что один из наших заказчиков, активно использующий контейнеры и контрольные группы памяти (memcg), обратил внимание на странные пики потребления ресурсов процессора, происходящие время от времени. Обычная загрузка системы была порядка 50%, а в пиковые моменты было занято 100% процессорного времени, причем практически все оно потреблялось ядром (sys time).
Сама нода была многопользовательской, и на ней было запущено порядка 200 OpenVZ контейнеров. Анализ показал, что большое количество пользователей создавали вложенные Docker контейнеры и многоуровневые иерархии контрольных групп памяти. Каждый пользовательский контейнер верхнего уровня содержал порядка 20 точек монтирования и 20 контрольных групп памяти (memcg), созданных systemd. Кроме этого были точки монтирования и контрольные группы, созданные упомянутым выше Docker. Проще говоря, нода была сильно загружена, и нагрузка на нее была намного сильнее, чем среднестатистически у всех остальных наших заказчиков. Нам было интересно найти причину появления этих пиков, поскольку такая же проблема могла проявляться и на менее загруженных машинах, где была малозаметной (например, давать пики по +5% sys time, которые ухудшают производительность).

Путем манипуляций с perf, удалось поймать пик и снять трейс. Выяснилось, что большая часть процессорного времени расходуется на очистку кэшей SLAB, а именно, кэшей суперблока:

Здесь стоит сделать пояснение и остановиться подробнее на этом вопросе. Всем известно, что ядро на некоторое время кэширует неиспользуемые данные перед тем как окончательно освободить память. Ядро широко использует этот принцип. Например, кэш страниц содержит в себе страницы данных, относящихся к файлу, что существенно ускоряет повторный доступ к ним при чтении (потому что не требуется заново обращаться к диску). В нашем же случае проблема возникла с кэшем метаданных суперблока, содержащихся в двух списках LRU: s_dentry_lru и s_inode_lru.

LRU (Least Recently Used)

struct lru_list указывает на массив связных списков, и каждой активной memcg соответствует один элемент (list_lru_one) в этом массиве. Когда некий объект SLAB перестает использоваться ядром, ядро добавляет его в один из связных списков массива (в зависимости от того, к какой memcg объект относится, или, грубо говоря, к какой memcg относился процесс, когда он создавал этот объект). Сам массив описан следующим образом (lru_list::node::memcg_lrus):


lru[0] указывает список объектов, относящихся к memcg с ID 0;
lru[1] указывает список объектов, относящихся к memcg с ID 1;

lru[n] указывает список объектов, относящихся к memcg с ID n;

В нашей проблеме фигурируют LRU списки s_dentry_lru и s_inode_lru, и как нетрудно догадаться из названия, они содержат неиспользуемые объекты dentry и inode файловой системы.
В дальнейшем, при нехватке памяти в системе или конкретной memcg, часть из элементов списка окончательно освобождается, и занимается этим специальный механизм, называющийся shrinker.

Shrinker

Когда ядру требуется выделить страницы памяти, а свободной памяти на NUMA-узле или в системе нет, запускается механизм по ее очистке. Он пытается выбросить или сбросить на диск некоторое количество: 1)страниц содержимого файлов из page cache; 2)страниц, относящихся к анонимной памяти в своп, и 3)кэшированных объектов SLAB (с ними и связана проблема, с которой мы столкнулись).

Выкидывание части кэшированных объектов SLAB влияет на освобождение страниц не напрямую: их размер, как правило, существенно меньше размера страницы, и в одной странице содержатся сотни объектов. При освобождении части объектов, в страницах SLAB возникают свободные промежутки памяти, которые могут быть использованы для создания других объектов SLAB. Этот алгоритм принят в ядре намерено: он простой и довольно эффективный. Интересующийся читатель может посмотреть формулу выбора части объектов для очистки в функции do_shrink_slab().

Эта функция выполняет собственно очистку части объектов, руководствуясь описанием, переданным ей в struct shrinker:


Применительно к shrinker суперблока, эти функции реализованы следующим образом. Каждый суперблок поддерживает свои собственные s_dentry_lru и s_inode_lru списки неиспользуемых объектов, относящихся к нему:

Метод .count_objects возвращает количество объектов:

Метод .scan_objects собственно, освобождает объекты:


Количество объектов, которые нужно освободить, передается в параметре sc. Также, там указана memcg, объекты которой должны быть выкинуты из LRU:


Таким образом, prune_dcache_sb() выбирает связный список из массива struct list_lru_memcg::lru[] и работает с ним. Аналогично поступает prune_icache_sb().

Старый алгоритм обхода shrinker’ов

При стандартном подходе “выкидывание” объектов из SLAB при нехватке памяти в
sc->target_mem_cgroup происходит следующим образом:


Проходим по всем дочерним memcg и вызываем shrink_slab() для каждой из них. Далее, в функции shrink_slab() мы проходим по всем shrinker’ам и для каждого из них вызываем do_shrink_slab():


Вспомним, что для каждого суперблока добавляется свой shrinker в этот список. Посчитаем сколько раз будет вызван do_shrink_slab() для случая с 200 контейнерами по 20 memcg и 20 точек монтирования в каждом. Всего мы имеем 200*20 точек монтирования и 200 * 20 контрольных групп. При нехватке памяти в самой верхней memcg, мы будем вынуждены обойти все ее дочерние memcg (т.е., вообще все), и для каждой из них вызвать каждый из shrinker из списка shrinker_list. Таким образом, ядро сделает 200 * 20 * 200 * 20 = 16000000 вызовов функции do_shrink_slab().

При этом, подавляющее число вызовов этой функции будет бесполезно: контейнеры обычно изолированы между собой, и вероятность того, что контейнер CT1 будет использовать super_block2, созданный в CT2, в общем случае невысока. Или, что то же самое, если memcg1 — контрольная группа из CT1, то соответствующий ей элемент массива super_block2->s_dentry_lru->node->memcg_lrus->lru[memcg1_id] будет пустым списком, и нет смысла вызывать do_shrink_slab() для него.

Эту проблему можно смоделировать с помощью простого bash-скрипта (здесь используются данные из патчсета, который был в дальнейшем передан в ядро):

Посмотрим что будет, если 5 раз подряд вызывать процедуру сброса кэшей:

Первая итерация продолжается 14 секунд, потому что кэшированные объекты действительно есть в памяти: 0.00 user 13.78 system 0:13.78 elapsed 99% CPU.
Вторая итерация занимает 5 секунд, хотя объектов уже нет: 0.00user 5.59system 0:05.60elapsed 99%CPU.
Третья итерация занимает 5 секунд: 0.00user 5.48system 0:05.48elapsed 99%CPU
Четвертая итерация занимает 8 секунд: 0.00user 8.35system 0:08.35elapsed 99%CPU
Пятая итерация занимает 8 секунд: 0.00user 8.34system 0:08.35elapsed 99%CPU

Стало очевидно, что используемый ванильным ядром алгоритм обхода shrinker’ов не оптимален, и нужно менять его в лучшую с точки зрения масштабируемости сторону.

Новый алгоритм обхода shrinker’ов

От нового алгоритма хотелось добиться следующего:

  1. освободить его от недостатков старого и
  2. не добавлять новых блокировок. Вызывать do_shrink_slab() только тогда, когда в этом есть смысл (то есть не пуст соответствующий связный список из массива s_dentry_lru или из массива s_inode_lru), но при этом напрямую не обращаться к памяти связных списков.

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

Теперь цикл в shrink_slab() может быть оптимизирован для обхода только необходимых shrinker’ов:


(Также реализована чистка бита, когда shrinker переходит в состояние “нет смысла вызывать do_shrink_slab(). См. подробнее в коммите на Github.

Если повторить тест сброса кэшей, то с использованием нового алгоритма он показывает существенно лучшие результаты:

Первая итерация: 0.00user 1.10system 0:01.10elapsed 99%CPU
Вторая итерация: 0.00user 0.00system 0:00.01elapsed 64%CPU
Третья итерация: 0.00user 0.01system 0:00.01elapsed 82%CPU
Четвертая итерация: 0.00user 0.00system 0:00.01elapsed 64%CPU
Пятая итерация: 0.00user 0.01system 0:00.01elapsed 82%CPU
Длительность итераций со второй по пятую — 0.01 секунды, в 548 раз быстрее, чем было раньше.

Поскольку аналогичные действия по сбросу кэшей происходят при каждой нехватке памяти на машине, то эта оптимизация существенно улучшает работу машин с большим количеством контейнеров и контрольных групп памяти. Набор патчей (17 штук) был принят в ванильное ядро, и вы можете найти его там, начиная с версии 4.19.

В процессе ревью патчей появился сотрудник Google, и выяснилось, что они столкнулись с такой же проблемой. Поэтому патчи были дополнительно протестированы на другом типе нагрузки.
В результате патчсет был принят с 9-й итерации; а его вхождение в ванильное ядро заняло около 4-х месяцев. Также на сегодня патчсет включен в наше собственное ядро Virtuozzo 7, начиная с версии vz7.71.9

Распределение slab – это механизм управления памятью, предназначенный для более эффективного распределения памяти и устранения значительной фрагментации. Основой этого алгоритма является сохранение выделенной памяти, содержащей объект определенного типа, и повторное использование этой памяти при следующем выделении для объекта того же типа. Этот метод был впервые введен в SunOS Джефом Бонвиком и сейчас широко используется во многих операционных системах Unix, включая FreeBSD и Linux.

Содержание

Основа

Фундаментальная идея способа распределения slab основывается на результатах наблюдений, показывающих, что некоторые объекты данных ядра часто создаются и уничтожаются после того, как перестают быть нужными. Таким образом, при каждом выделении памяти для объектов такого типа затрачивается некоторое время для нахождения наиболее подходящего места для этого объекта. Кроме того, освобождения памяти после уничтожения объекта способствует фрагментации памяти, которая создает дополнительную нагрузку на ядро для реорганизации памяти.

В случае же распределением slab, при использовании программистом определенных системных вызовов, участки памяти, подходящие для размещения объектов данных определенного типа и размера, заранее предопределены. Распределитель slab хранит информацию о размещении этих участков, известных также как кэши. Таким образом, если поступает запрос на выделение памяти для объекта данных определенного размера, он может мгновенно удовлетворить запрос уже выделенным слотом. Однако, уничтожение объектов не освобождает память, а только открывает слот, который помещается в список свободных слотов распределителем slab. Следующий вызов для выделения памяти того же размера вернет слот памяти, неиспользуемый в данный момент. Этот процесс устраняет необходимость в поиске подходящего участка памяти и значительно снижает фрагментацию памяти. В этом контексте slab – это одна или более смежных страниц в памяти, содержащих заранее выделенные участки памяти.

Реализация

Понимание распределения slab требует определения следующих терминов:

  1. Кэш: кэш представляет собой небольшой объём очень быстрой памяти. Здесь мы используем кэш как память для хранения таких объектов, как семафоры, дескрипторы процессов, объекты файлов и т. д. Каждый кэш способен хранить только один тип объектов.
  2. Slab: slab представляет собой непрерывный участок памяти, обычно составленный из нескольких физических смежных страниц. Кэш состоит из одного или более slab’ов.

Когда программа создает кэш, она выделяет ряд объектов в него. Их количество зависит от размера связанных slab’ов. Slab может находиться в одном из следующих состояний:

  1. пустой – все объекты в slab’e помечены как свободные
  2. частично занятый – slab содержит как используемые, так и пустые объекты
  3. заполненный – все объекты в slab’е помечены как используемые

Изначально система помечает каждый slab как «пустой». Когда процесс обращается за новым объектом ядра, система делает попытку найти свободное место для этого объекта в частично занятом slab’е в кэше для этого типа объектов. Если такого места не находится, система выделяет новый slab из смежных физических страниц и передает их в кэш. Новый объект размещается в этом slab’е, а это местоположение помещается как «частично занятое». Основное преимущество алгоритма slab заключается в том, что память выделяется точно в том объёме, в котором требуется. Таким образом, отсутствует внутренняя фрагментация памяти. Распределение происходит быстро, поскольку система создает объекты заранее и легко выделяет их из slab’а.

Slab – объём памяти, за счет которого кэш может увеличиваться или уменьшаться. Он представляет собой распределение памяти в кэш, а его размер обычно кратен размеру страницы памяти. Slab должен содержать список свободных буферов, а также список буферов, которые были выделены (в случае большого размера slab’а).

Большие slab’ы

Предназначены для кэшей, хранящих объекты, размер которых не менее 1/8 размера страницы памяти данной машины. Причина, по которой большие slab’ы имеют структуру, отличную от небольших slab’ов, заключается в том, что так они могут лучше умещаться на страницах памяти, таким образом избегая фрагментации. Slab содержит список буферов, которые являются контроллерами для каждого буфера, который может быть выделен (буфер – это память, которую пользователь распределителя slab будет использовать).

Малые slab’ы

Содержат объекты, которые не превышают 1/8 размера страницы памяти данной машины. Они должны быть оптимизированы отдельно от логической структуры, избегая использования буферов (которые были бы такими же большими, как и данные в них, вызывая тем самым намного больший расход памяти). Небольшие slab’ы занимают точно одну страницу, и имеют определенную структуру, позволяющую им избежать буферизации. Последняя часть страницы содержит «заголовок slab», представляющий собой информацию, необходимую для хранения slab’а. Начиная с первого адреса страницы, есть столько буферов, сколько может быть выделено без использования заголовка slab в конце страницы.

Вместо обычного использования буферов, используется буфер для хранения списка свободных ссылок. Это позволяет обходиться без использования буферов в небольших slab’ах.

Читайте также: