Без модуля bson

В моей последней статье Как хранить документы размером более 16 МБ в MongoDB я предложил подход к хранению большого документа в связанных документах. Для этого нам нужно вычислить размер документа во время выполнения, чтобы знать, когда его разделить и сохранить в MongoDB. Я использовал очень наивный подход для расчета размера на лету.

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

Документы хранятся в MongoDB в формате BSON. Вы можете догадаться, что BSON - это двоичная нотация объектов JavaScript или для краткости двоичный JSON. Он возник в MongoDB - вот его спецификация.

Мы будем использовать модуль bson для изучения формата BSON. Давайте сначала установим его с npm install bson. Затем мы требуем ìt с const BSON = require('bson');. Мы в порядке.

Давайте начнем изучать, как документы кодируются с помощью BSON.

Кодировка пустого объекта

Начнем с самого простого документа - пустого объекта. Сериализуйте его и войдите в консоль:

Мы видим, что есть буфер на 5 байтов. Это самый маленький действующий документ BSON.

Давайте посмотрим, как кодируется пустой объект: 05 00 00 00 00

  1. 05 00 00 00: Первые четыре байта на самом деле являются числом int32. Это размер (5 байтов) документа BSON, включая сам документ и признак конца документа.
  2. 00: последний байт обозначает терминатор документа.

Подробности реализации: байты записываются с прямым порядком байтов, где младший байт идет первым.

Кодировка чисел

Затем давайте посмотрим на действительный документ MongoDB, который можно сохранить в базе данных. В нем только одна пара "ключ-значение".

Как кодируется пара "ключ-значение"

0e 00 00 00 10 5f 69 64 00 01 00 00 00 00

  1. 0e 00 00 00 размер документа составляет 14 байт.
  2. 10 5f 69 64 00 01 00 00 00 пара "ключ-значение".
  3. 00 последний байт обозначает терминатор документа.

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

Давайте проанализируем пару "ключ-значение" сверху

10 5f 69 64 00 01 00 00 00

  1. 10 Первый байт является типом значения - в данном случае int32 (вы можете посмотреть ссылку на спецификацию выше).
  2. 5f 69 64 любое количество следующих байтов является ключом.
  3. 00 Ключевой терминатор.
  4. 01 00 00 00 Последние четыре байта - это значение - мы знаем, что оно занимает четыре байта, потому что это int32.

У нас есть тип значения, но где же тип ключа?

Детали реализации: ключ всегда является строкой. В результате нам не нужно добавлять в BSON тип ключа - мы знаем, что это строка.

Тип строки

Давайте посмотрим, как кодируются пары "ключ-значение" со строками в качестве значений.

Как кодируется строковая пара "ключ-значение"

02 74 79 70 65 00 07 00 00 00 73 74 72 69 6e 67 00

  1. 02 Первый байт - это тип значения, в данном случае строка.
  2. 74 79 70 65 ключ type.
  3. 00 Ключевой терминатор.
  4. 07 00 00 00 Для строкового типа длина строкового значения записывается в int32. Длина включает один байт для ограничителя строкового значения.
  5. 73 74 72 69 6e 67 наша закодированная строка.
  6. 00 признак конца строкового значения.

Детали реализации: строка кодируется в UTF-8, что означает, что для кодирования каждого символа требуется от одного до четырех байтов. Для кодирования латинского алфавита, чисел и некоторых других вещей требуется один байт. Для кодирования некоторых неанглийских символов требуется два байта. Китайские иероглифы занимают три байта. Смайлы занимают четыре байта. Чтобы вычислить размер закодированной строки, мы можем использовать метод буфера nodejs: Buffer.from(str).size.

Тип массива

Пойдем дальше и посмотрим, как кодируются массивы.

Как кодируется часть массива "ключ-значение"

04 76 61 6c 75 65 73 00 12 00 00 00 02 30 00 06 00 00 00 66 69 72 73 74 00 00

  1. 04: Тип документа массива.
  2. 76 61 6c 75 65 73 00: ключ values с терминатором 00.
  3. 12 00 00 00 02 30 00 06 00 00 00 66 69 72 73 74 00 00: закодированный массив. Это полноценный документ BSON.

Давайте посмотрим, как кодируется массив ['first']

12 00 00 00 02 30 00 06 00 00 00 66 69 72 73 74 00 00

  1. 12 00 00 00: длина документа составляет 12 байт.
  2. 02 30 00 06 00 00 00 66 69 72 73 74 00: закодированная пара "ключ-значение".
  3. 00 терминатор документа.

Но подождите, зачем нужна пара "ключ-значение", если у нас был только один элемент в массиве ['first’]?

Подробности реализации: массивы хранятся как объекты в BSON, поэтому ['first'] фактически сохраняется как { '0': 'first' }.

Нулевой тип

В массиве Column.values у меня могут быть строки и нули, поэтому мне нужно знать, как хранятся нули.

Как кодируется пустая часть "ключ-значение":
0a 30 00

  1. 0a код нулевого типа.
  2. 30 строковый ключ 0, за которым следует
  3. 00 ключевой терминатор.

Как видите, очень эффективно хранить null в формате BSON. Нам не нужно указывать значение, это очевидно по его типу.

Теперь у нас достаточно информации, чтобы создать функцию, вычисляющую размер BSON для интерфейса theColumn. MongoDB ограничивает размер документа до 16Mb, что составляет 16777216 байта.

Расчет функции размера BSON

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

Функция, которую мы используем в производстве, фактически использует модуль bson для вычисления размера объекта с пустым массивом. Затем, добавляя новые элементы, мы отслеживаем размер документа.

Заключение

При вычислении размера theBSON во время выполнения мы можем эффективно разбить столбец на части.

Я буду писать о других вариантах уменьшения размера документов MongoDB в будущих статьях, так что следите за обновлениями.