Когда мы решили написать собственную базу данных временных рядов, у нас определенно были большие ожидания и требования к этому инструменту, включая:

  • быстрый доступ для чтения/записи
  • формат байта на диске для экономии места на диске
  • мониторинг (регистрация и пассивные измерения)
  • Интерфейс REST для операций CRUD над данными
  • нет шардинга
  • Ява

Создание базы данных временных рядов

Во-первых, мы создали интерфейс REST, который был написан с помощью Spark — не путать с проектом Apache Spark — и выглядит почти как современное приложение nodeJS/express. Для массовой обработки данных мы создали несколько массовых запросов, чтобы уменьшить нагрузку на HTTP и сэкономить время в сети.

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

Фрагментация файлов и их отображение в памяти были одной из самых больших проблем при разработке этого инструмента. Если фрагментация слишком высока, у вас будет создано слишком много файлов, что является проблемой для некоторых файловых систем. Мы тестировали другие файловые системы, такие как XFS, но производительность при записи файлов не была линейной, поэтому мы остановились на EXT4. С другой стороны, если фрагментация слишком низкая, файлы будут очень большими, что будет негибко для сценариев резервного копирования и приведет к тому, что заголовок файла будет слишком большим для записи. Мы вернемся к этому заголовку позже…

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

Это будет единственное (беззнаковое) значение, хранящееся в 7 байтах на диске.

Затем вы можете использовать метод putNumber для записи одного значения с заданным «смещением» в сущность. Поэтому вам необходимо рассчитать смещение, которое равно:

offset(int) = (valueTimestampInSeconds — databaseStartTimestampInSeconds) / databaseIntervalInSeconds

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

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

Объединение ключей

Лучшим решением было объединить «пространства ключей» (несколько ключей) в логическом архиве на диске, чтобы все операции записи/чтения в пространстве ключей выполнялись в одном и том же логическом архиве, и таким образом уменьшалось количество файлов, необходимых для одного сценария. от миллионов до одного.

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

Timeseriesx — ключ: например: rtt:aws:eu-central-1:cogent:8.8.0.0/16:median

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

Агрегирование ключей за период времени в один файл выглядит так:

TimeseriesArchive x — ключ: например: rtt:aws:eu-central-1:cogent — время: начало = дата (n), конец = дата (m)

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

В случае измерения латентности для всех сетевых префиксов от заданного транзитного звена (в данном примере когентного) количество создаваемых файлов уменьшается с 650К до 1.

Теперь наш заголовок содержит простую Map‹String,Integer›, которая обозначает смещение в файле, где находится подраздел. Поскольку количество значений на блок задается во время создания (в приведенном выше примере есть одно значение на блок), блок (и сам файл) всегда будут иметь одинаковый размер.

Вернитесь к третьей части этой серии блогов.

Управление временными рядами больших данных — Часть 1