Как высокопроизводительная платформа сетевых приложений, Netty реализует высокопроизводительный механизм управления памятью.
Изучая принципы реализации, алгоритмы и дизайн параллелизма, нам полезно писать более элегантный и высокопроизводительный код; при использовании Netty мы можем более эффективно обнаруживать и устранять неполадки, когда сталкиваемся с проблемами памяти.
В этой статье представлен механизм управления памятью на основе Netty4.1.43.Final.
Классификация ByteBuf
Netty использует объекты ByteBuf в качестве контейнеров данных для операций чтения и записи ввода-вывода.Управление памятью Netty также вращается вокруг эффективного выделения и освобождения объектов ByteBuf.
При обсуждении управления объектами ByteBuf оно в основном классифицируется по следующим аспектам:
- Объединенные и не объединенные
Unpooled, память без пула напрямую вызывает системный API каждый раз, когда она выделяется для применения к операционной системе того же размера памяти, который требуется ByteBuf, и освобождает ее через системные вызовы после ее использования.Pooled, память в пуле выделяется на основе целого блока предварительно выделенной большой памяти, а часть ее инкапсулируется в ByteBuf для использования и перерабатывается в пул памяти после использования.
tips:Netty4 по умолчанию использует метод Pooled, который можно установить параметром -Dio.netty.allocator.type=unpooled или pooled
- Куча и прямой Heap, относится к памяти, выделенной в куче JVM, связанной с ByteBuf, а выделенная память управляется сборщиком мусора.Direct, что означает, что память, связанная с ByteBuf, выделяется вне кучи JVM. Выделенная память не управляется сборщиком мусора и должна применяться и освобождаться с помощью системных вызовов. Базовый объект DirectByteBuffer основан на Java NIO.
Заметка:Преимущество использования памяти вне кучи заключается в том, что когда Java выполняет операции ввода-вывода, ему необходимо передать начальный адрес и длину буфера, в котором находятся данные. куча имеет тенденцию перемещаться, вызывая изменение адреса объекта Ошибка в системном вызове. Чтобы избежать этой ситуации, при выполнении системных вызовов ввода-вывода на основе кучи память необходимо копировать из кучи, а если операции ввода-вывода выполняются непосредственно на основе памяти вне кучи, стоимость копирования может быть спасен.
Управление объединенными объектами
Объектам без пула нужно только вызвать реализацию базового интерфейса для использования и освобождения объектов, в то время как реализация объектов из пула намного сложнее и может быть изучена с помощью следующих вопросов:
- Как алгоритм управления пулом памяти обеспечивает эффективное выделение и освобождение памяти, а также уменьшает фрагментацию памяти
- Как добиться эластичного масштабирования, когда пул памяти постоянно применяется/освобождается под высокой нагрузкой
- Пул памяти как глобальные данные, как уменьшить конкуренцию замков в многопоточной среде
1 Схема алгоритма
1.1 Общий принцип
Сначала Netty обращается к системе за целым блоком непрерывной памяти, называемым чанком, с размером по умолчанию chunkSize = 16Mb, который упаковывается объектом PoolChunk. Для более точного управления Netty дополнительно разбивает фрагменты на страницы, и каждый фрагмент по умолчанию содержит 2048 страниц (pageSize = 8 КБ).
Стратегии выделения объединенных в пул объектов памяти разных размеров различны.Следующее сначала представляет принцип выделения объединенных объектов, размер памяти которых находится в диапазоне **(pageSize/2, chunkSize]**, и принципы выделения других больших объекты и мелкие объекты. Представьте еще раз. В том же блоке Netty управляет страницами в нескольких слоях в соответствии с разной степенью детализации:
- Уровень 1, размер группы = 1*pageSize, всего 2048 групп.
- Уровень 2, размер группы = 2 * размер страницы, всего 1024 группы.
- Уровень 3, размер группы = 4*pageSize, всего 512 групп ...
При запросе на выделение памяти количество выделенной по запросу памяти берется до ближайшего размера группы, а свободная группа находится слева направо в соответствующем уровне размера группы. Например, запрос на выделение объекта памяти составляет 1,5 * pageSize, а значение увеличивается до 2 * pageSize размера группы, и в этой группе слоев находится полностью свободная группа памяти для выделения, как показано на следующем фигура:
Когда выделяется память размером группы 2 * pageSize, чтобы облегчить следующее выделение памяти, группа помечается каквсе использовано(отмечено красным на рисунке), более грубая группировка памяти вверху отмечена какчастично используется(Желтая метка на картинке)
1.2 Структура алгоритма
Netty реализует упомянутое выше многоуровневое групповое управление различной степенью детализации на основе сбалансированных деревьев.
Когда необходимо создать ByteBuf заданного размера, алгоритм должен найти первое место, которое может вместить выделенную память в памяти с размером chunkSize в PoolChunk.
Чтобы быстро найти место в чанке, которое может вместить запрошенную память, алгоритм строит полностью сбалансированное дерево на основе хранилища байтового массива (memoryMap).Множественные уровни сбалансированного дерева такие же, как и упомянутое выше многоуровневое дерево. группировка слоев фрагментов в соответствии с разной степенью детализации. :
Глубина дерева рассчитывается от 0, количества узлов в каждом слое и размера памяти, соответствующего каждому узлу, выглядит следующим образом:
depth = 0, 1 node,nodeSize = chunkSize
depth = 1, 2 nodes,nodeSize = chunkSize/2
...
depth = d, 2^d nodes, nodeSize = chunkSize/(2^d)
...
depth = maxOrder, 2^maxOrder nodes, nodeSize = chunkSize/2^{maxOrder} = pageSize
Максимальная глубина дерева — maxOrder (максимальный порядок, значение по умолчанию — 11), через это дерево поиск алгоритма в чанке можно преобразовать в:
При запросе на выделение памяти chunkSize/2^k искать первый свободный узел слева направо на уровне сбалансированного дерева высотой k
Поле использования массива начинается с индекса = 1, и сбалансированное дерево хранится в массиве в иерархическом порядке, первый узел с глубиной = n хранится в memoryMap[2^n], а второй узел хранится в memoryMap [2^ n+1] и т. д. (на рисунке ниже представлен выделенный chunkSize/2)
Использование узла можно получить по значению memoryMap[id].Чем больше значение memoryMap[id], тем меньше доступной памяти остается.
- memoryMap[id] = depth_of_id:узел идентификатора простаивает, начальное состояние, значение depth_of_id представляет глубину узла id в дереве
- memoryMap[id] = maxOrder + 1:Используются все узлы id, память узла полностью выделена, и ни один из дочерних узлов не свободен
- depth_of_id Часть идентификатора узла используется, значение x memoryMap[id], представляющееСреди дочерних узлов id первый свободный узел находится на глубине x, и нет ни одного свободного узла в диапазоне глубин [depth_of_id, x)
1.3 Выделить/освободить память
При запросе на выделение памяти он сначала нормализует запрошенный размер выделенной памяти (значение вверх) и использует метод PoolArena#normalizeCapacity() для получения значения ближайшей степени 2, например, 8000 байт нормализуется до 8192 байт ( chunkSize/ 2^11 ), 8193 байта нормализуются до 16384 байт (chunkSize/2^10)
Алгоритм обработки запросов памяти В методе PoolChunk#allocateRun при выделении нормализованной памяти с chunkSize/2^d, то есть первую свободную память нужно найти на уровне depth = d.Переход от корневого узла(глубина корневого узла = 0, id = 1), конкретные шаги следующие:
-
шаг 1Определите, является ли значение текущего узла memoryMap[id] > d или depth_of_id > dЕсли это так, из этого фрагмента нельзя выделить память, поиск завершается.
-
Шаг 2Определить, является ли значение узла memoryMap[id] == d и depth_of_id Если это так, текущий узел является свободной памятью с глубиной = d, поиск завершен, обновите значение текущего узла до memoryMap[id] = max_order + 1, что означает, что узел был использован, и пройдите все узлы-предки текущий узел, обновите значение узла до соответствующих Минимальное значение значений левого и правого дочерних узлов; если нет, перейдите к шагу 3
-
Шаг 3Определить, является ли значение текущего узла memoryMap[id] Если это так, незанятый узел находится в дочерних узлах текущего узла, затем сначала оценивается левый дочерний узел memoryMap[2 * id]
Обратитесь к следующему рисунку для справочного примера, примените память, выделенную с помощью chunkSize/2.
note: Хотя на рисунке дочерний узел memoryMap[id] = depth_of_id индекса = 2, на самом деле выделяется память узла, потому что алгоритм начинает обход сверху вниз, поэтому в фактической обработке после того, как узел выделяет память, только узлы-предки обновляются.значение и не обновляет значение дочернего узла
При освобождении памяти обновите memoryMap[id] до depth_of_id в соответствии с идентификатором, возвращенным при подаче заявки на память, и установите значение узла-предка узла идентификатора на минимальное значение левого и правого узлов.
1.4 Управление памятью гигантских объектов
Для огромных объектов (огромных), размер выделения которых превышает chunkSize, Netty применяет стратегию управления без пула и создает специальный объект PoolChunk без пула для управления при выделении памяти для каждого запроса. освобождается Когда вся память Chunk освобождается, соответствующая логика приложения памяти находится в методе PoolArena#allocateHuge(), а логика освобождения — в методе PoolArena#destroyChunk()
1.5 Управление памятью малых объектов
Когда размер объекта запроса reqCapacity
Выделение страницы непосредственно этим небольшим объектам приведет к потерям, а разметка дерева баланса на странице займет больше места Поэтому реализация Netty такова: сначала подайте заявку на свободную страницу в PoolChunk, и та же самая страница будет разделена на небольшую память того же размера и спецификации для хранения
Эти страницы инкапсулированы объектами PoolSubpage. PoolSubpage записывает размер памяти (elemSize), объем доступной памяти (numAvail) и использование каждой маленькой памяти. Соответствующее битовое значение растрового изображения типа long[] равно 0 или 1 для записи память. Была ли она использована
note: Некоторые читатели должны были заметить, что нормализованное значение приложения Netty для объединенной памяти больше.Например, 1025 байт будут нормализованы до 2048 байт, а 8193 байта будут нормализованы до 16384 байт.Это приводит к некоторым потерям? Это можно понимать как компромисс.Посредством нормализации размер выделяемой памяти в пуле нормализуется, что значительно облегчает применение памяти и память и повторное использование памяти, а также повышает эффективность.
2 Эластичное масштабирование
В предыдущем разделе, посвященном принципам алгоритма, показано, как Netty реализует применение и освобождение блоков памяти. Емкость одного фрагмента ограничена. Как управлять несколькими фрагментами и создавать пул памяти, который может эластично масштабироваться?
2.1 Управление фрагментами пула
Чтобы решить проблему ограниченной емкости одного PoolChunk, Netty управляет несколькими PoolChunk вместе как связанным списком, а затем использует объект PoolChunkList для хранения заголовка связанного списка.
Если все PoolChunks формируются в связанный список, эффективность обхода и управления поиском низка, поэтому Netty разработала объект PoolArena (арена — сцена и место на китайском языке), чтобы реализовать управление несколькими PoolChunkList и PoolSubpage, контроль безопасности потоков, выделение внешней памяти, освобожденная служба
PoolArena содержит 6 PoolChunkList, и диапазон использования PoolChunk, хранящихся в каждом PoolChunkList, различен:
// 容纳使用率 (0,25%) 的PoolChunk
private final PoolChunkList<T> qInit;
// [1%,50%)
private final PoolChunkList<T> q000;
// [25%, 75%)
private final PoolChunkList<T> q025;
// [50%, 100%)
private final PoolChunkList<T> q050;
// [75%, 100%)
private final PoolChunkList<T> q075;
// 100%
private final PoolChunkList<T> q100;
Шесть объектов PoolChunkList образуют двусвязный список. Когда память PoolChunk выделяется и освобождается, что приводит к изменению использования, необходимо определить, превышает ли PoolChunk ограниченный диапазон использования PoolChunkList, в котором он расположен. PoolChunkList становится новым head; аналогично, когда создается новый PoolChunk и выделяется память, PoolChunk также необходимо поместить в соответствующий PoolChunkList в соответствии с приведенной выше логикой.
Выделите нормализованную память normCapacity (диапазон размеров [pageSize, chunkSize]). Конкретная обработка выглядит следующим образом:
- Получите последовательный доступ к q050, q025, q000, qInit, q075 и просмотрите список PoolChunk в PoolChunkList, чтобы определить, может ли какой-либо PoolChunk выделять память.
- Если какой-либо из вышеперечисленных 5 PoolChunkList имеет успешное выделение памяти PoolChunk, а использование PoolChunk изменилось, перепроверьте и поместите его в соответствующий PoolChunkList и завершите
- В противном случае создайте новый PoolChunk, выделите память и поместите его в соответствующий PoolChunkList (расширение PoolChunkList).
**примечание:** видно, что выделение памяти имеет приоритет в PoolChunkList q050 -> q025 -> q000 -> qInit -> q075. Преимущество этого заключается в том, что использование памяти в каждом интервале после выделения больше в диапазоне [75, 100), улучшить использование памяти PoolChunk с учетом эффективности, уменьшив обход PoolChunk в PoolChunkList
Когда память PoolChunk освобождается и скорость использования PoolChunk изменяется, перепроверьте и поместите ее в соответствующий PoolChunkList.Если скорость использования памяти PoolChunk равна 0 после освобождения, удалите ее из PoolChunkList и освободите эту часть пространства, чтобы избежать пиковое время. Запрошенная память была кэширована в пуле (PoolChunkList сжимается)
Номинальный диапазон использования PoolChunkList перекрывается.Это разработано, потому что, если основываться на критическом значении, когда использование памяти после выпуска приложения памяти PoolChunk колеблется вокруг критического значения, это заставит PoolChunkList перемещаться вперед и назад в связанный список.
2.2 Управление подстраницей пула
PoolArena внутренне содержит два массива PoolSubpage, в которых хранятся крошечные и маленькие подстраницы PoolSubpage соответственно:
// 数组长度32,实际使用域从index = 1开始,对应31种tiny规格PoolSubpage
private final PoolSubpage<T>[] tinySubpagePools;
// 数组长度4,对应4种small规格PoolSubpage
private final PoolSubpage<T>[] smallSubpagePools;
PoolSubpages одного размера (elemSize) образуют связанный список, а заголовки связанных списков PoolSubpage с разными спецификациями хранятся в массивах tinySubpagePools или smallSubpagePools, как показано на следующем рисунке:
Когда необходимо выделить небольшой объект памяти для PoolSubpage, в соответствии с нормализованным размером, вычислить нижний индекс связанного списка PoolSubpage, который будет доступен в массивах tinySubpagePools и smallSubpagePools, и получить доступ к PoolSubpage в связанном списке, чтобы применить для памяти выделение, если доступ к PoolSubpage Если количество узлов связанного списка равно 0, создайте новую PoolSubpage для выделения памяти и добавьте ее в связанный список
PoolSubpage, хранящаяся в связанном списке PoolSubpage, представляет собой всю выделенную память. Когда вся память выделена или память освобождена, PoolSubpage будет удалена из связанного списка, чтобы уменьшить ненужные узлы связанного списка; когда вся память PoolSubpage выделена, а затем часть памяти освобождается, джоин-лист
Масштабирование эластичного пула памяти poolarean можно резюмировать на следующем рисунке:
3 Параллельное проектирование
Выделение и освобождение памяти неизбежно столкнется с многопоточными сценариями параллелизма. Будь то тег сбалансированного дерева в PoolChunk или тег растрового изображения в PoolSubpage, он небезопасен для многопоточности. Как максимально улучшить производительность параллелизма в предпосылке потокобезопасности?
Прежде всего, чтобы уменьшить конкуренцию между потоками, Netty заранее создаст несколько PoolArenas (число поколений по умолчанию = 2 * количество ядер ЦП).Когда поток впервые запрашивает выделение памяти в пуле, он находит PoolArena удерживается наименьшим потоком и сохраняет поток. В локальной переменной PoolThreadCache реализована связанная привязка между потоками и PoolArena (метод PoolThreadLocalCache#initialValue())
**примечание: **собственный Java ThreadLocal реализует принцип локальных переменных потока: на основе переменной-члена типа ThreadLocalMap потока, ключом карты в этой переменной является ThreadLocal, а значением является значение локальной переменной потока, которая необходимо настроить. Когда вызывается метод ThreadLocal#get(), значение в ThreadLocalMap потока, к которому обращается текущий поток, получается с помощью Thread.currentThread().
Netty разработала более производительный класс замены для ThreadLocal: FastThreadLocal, который необходимо использовать вместе с классом FastThreadLocalThread, который наследует Thread.Основной принцип заключается в расширении исходного хранилища локальных переменных Thead на основе ThreadLocalMap до массива, к которому можно получить доступ. более быстро (Object[] indexedVariables), каждый FastThreadLocal поддерживает глобальный атомарно увеличивающийся индекс массива типа int
Кроме того, Netty также разработала механизм кэширования для повышения производительности параллелизма: когда память объекта запроса освобождается, PoolArena не освобождает ее немедленно, а сначала пытается сохранить такую информацию, как положение смещения (переменная обработчика) в PoolChunk. и фрагмент, связанный с памятью, в PoolThreadLocalCache в очереди кэша фиксированного размера (если очередь кэша заполнена, память будет немедленно освобождена); При запросе выделения памяти PoolArena отдает приоритет доступу к очереди кеша PoolThreadLocalCache, чтобы узнать, есть ли доступная кеш-память.Если да, она будет выделена напрямую для повышения эффективности выделения.
Суммировать
Дизайн управления объединенной памятью Netty основан на jemalloc Facebook, а также имеет сходство с алгоритмом распределения памяти Linux Buddy алгоритмом и алгоритмом Slab.Дизайн многих распределенных систем и фреймворков можно найти в дизайне операционных систем. основополагающие принципы ценны
В следующей статье рассказывается об устранении неполадок, связанных с утечками памяти Netty вне кучи.
Ссылаться на
"масштабируемое выделение памяти с помощью jemalloc - Facebook"Engineering.convenient.com/core-data/ это…
«Введение и практика Netty: имитация системы обмена мгновенными сообщениями WeChat IM»Наггетс Талант/книга/684473…
Более интересно, добро пожаловать, чтобы обратить внимание на общедоступную архитектуру распределенной системы