Статьи

Исключение избыточности в Neo4j

вступление

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

Отход от старой модели: новый способ хранения вещей

До настоящего времени свойства хранились на диске в двусвязном списке, где каждый из его узлов содержал некоторые необходимые административные / структурные накладные расходы и фактические данные свойств. Более конкретно, макет был:

byte  0     : 4 high bits of previous pointer, inUse flag
byte  1     : unused<
byte  2     : 4 high bits of next pointer
bytes 3-4   : property type
bytes 5-8   : property index
bytes 9-12  : previous pointer 32 low bits
bytes 13-16 : next pointer 32 low bits
bytes 17-24 : property data

Последние 8 байтов, где хранится значение, достаточно для размещения всех значений примитивов, короткой строки или указателя на динамическое хранилище, где цепочка динамических записей будет хранить длинную строку, массив примитивов или String [].

Здесь есть некоторая трата, отчасти потому, что полные 8 байтов используются в (редких) случаях хранения значений типа double и long или для коротких строк, но в основном потому, что эти указатели повторяются для каждого свойства, что создает влияние структурных накладных расходов. С другой стороны, оптимизация Short String имела большой успех, доказав ценность встраивания большего количества типов свойств. Поэтому мы решили выделить хорошие части и выделить плохие, получив в итоге структуру PropertyRecord, которая больше не эквивалентна одному свойству, а действует как контейнер для переменного числа свойств переменной длины. Текущий макет:

byte  0    : 4 high bits of previous, 4 high bits of next pointers
bytes 1-4  : previous property record
bytes 5-8  : next property record
bytes 9-40 : payload

Да, это правильно, нет флага inUse, объясняемого структурой полезной нагрузки.

Во-первых, давайте назовем 4 8-байтовых блока в блоках полезной нагрузки, чтобы иметь простое имя для них. Каждый из этих блоков используется по-разному, в зависимости от типа данных свойства. Начиная с каждого свойства необходимо иметь индекс свойства и тип свойства. Они являются общими и всегда присутствуют, причем индекс свойства занимает первые 3 байта блока, а тип занимает 4 старших бита 4-го байта. Теперь, после этого приходит значение свойства. Если это примитив, который умещается в 4 байта, то 4 младших бита 4-го байта пропускаются, а оставшиеся 4 байта блока используются для хранения значения, и мы закончили. При сохранении указателя в хранилище DynamicStore для не коротких строк и массивов, необходимые 36 битов находят вторую половину 4-го байта и младшие 4 байта.Это означает, что каждый PropertyRecord может хранить до 4 таких свойств — огромная экономия места.
Для длинных и двойных чисел, для которых требуется 8 байтов, завершающие байты 4 1/2 пропускаются, и вместо этого следующий блок используется целиком для хранения значения. Это приводит к некоторым потерям, но это все еще более эффективно, чем предыдущий метод, и это относительно редкий случай использования.

Осталось только ShortStrings и новый ShortArray . Поскольку мы сохранили все это пространство и вызовы ввода / вывода с помощью ShortString, почему бы не расширить эту идею? Теперь у нас есть LongerShortString, который похож на ShortString, но на трещину. Он работает по тому же принципу — он сканирует строку, видит, попадает ли она в кодировку, кодирует ее и сохраняет заголовок с длиной и идентификатором таблицы кодирования, а затем фактические данные, закодированные в long, которые занимают блоки сразу после информация о недвижимости Если он не помещается в максимум 3 1/2 блока записи свойства, он вместо этого кодируется как UTF8 и сохраняется в DynamicStringStore. Аналогичная идея применяется к массивам. При передаче примитивного массива мы сначала определяем минимальное количество битов, необходимое для хранения его значений, эффективно сбрасывая все левые нули, которые мы можем, сохраняя при этом все элементы массива одинакового размера. Это означает, что если нас попросят сохранить новый int [] {1,2,3,4,5} , записи будут занимать не 32, а 3 бита каждая. логическое []например стоит 1 бит на запись. Очевидно, что смешивание даже одного отрицательного значения сразу дает максимальное количество битов на запись. Итак, чтобы сохранить массив, мы сначала определяем это число, а затем заголовок становится:

   4 бита, значение enum, идентифицирующее тип
   6 битов примитива , длина массива
   6 бит, количество бит на элемент

а затем следуйте «битой» записи массива. Тот же алгоритм используется и для динамических массивов, но длина фактически сохраняется в поле длины динамической записи (как обычно), а не в заголовке ShortArray, и мы просто сохраняем, сколько битов последнего байта используется. Этого вместе с количеством битов на номер записи достаточно для восстановления значения. Конечно, и в этом случае, если массив не помещается в PropertyRecord даже после этого «сжатия», он сохраняется в DynamicArrayStore как обычно, хотя теперь в его побритой форме как byte [], что означает меньше DynamicRecords используются так меньше отходов. Это достигается ценой восстановления массива при его считывании, но уменьшенный ввод-вывод более чем компенсирует это. Более точное описание новой ShortString,включая все классы ShortString и ограничения размераКак и новый ShortArray , доступен в руководстве .

Как насчет тайны пропавшего флага inUse? Ну, это сочетание двух вещей. Во-первых, блоки помечаются индивидуально как используемые или нет, поскольку API позволяет удалять свойство, и теперь свойство больше не является записью, а представляет собой набор блоков. Таким образом, мы свернули это в тип свойства, где 0 означает, что он не используется. Во-вторых, блоки записываются дефрагментированными на диск, что означает, что если из 3-х свойств в записи мы удаляем среднее (устанавливаем тип на удаленный), то будут записаны только два оставшихся. Это приводит к простому метку пометки «больше нет свойств в этой записи» путем записи 0 для 4-го байта первого неиспользуемого блока (реализация просто записывает целую строку).Следствием этого является то, что запись свойства, которая имеет 4-й байт первого блока 0, фактически не используется.

Код прохождение

Я собирался изложить изменения / дополнения на уровне исходного кода здесь, но этот пост становится слишком длинным. Кроме того, из вышесказанного код становится простым для подражания. Если у вас есть какие-либо вопросы, предложения или вы хотите немного поговорить о реализации, загляните в наш список рассылки.

Здесь просто заметка — логика того, когда и как происходит распределение блоков, и стратегия дефрагментации содержится в WriteTransaction. Продолжайте и экспериментируйте с тем, что лучше всего подходит для вашего варианта использования — отзывы об этих путях кода будут встречены с большой радостью!

Долой старое, новое: перенести магазин

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

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

Весь код миграции находится в исходном коде ядра в пакете org.neo4j.kernel.impl.storemigration и может быть запущен как в качестве отдельного инструмента, так и в качестве части обычного запуска — поэтому независимо от того, используете ли вы серверные сценарии или просто ядро библиотека, просто установите параметр конфигурации «allow_store_upgrade» = «true» и вы готовы идти.

Вперед и вверх

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

Спасибо, что сделали Neo4j таким, какой он есть.