воскресенье, 10 июня 2018 г.

TimescaleDB. Комфортное хранение метрик в PostgreSQL

В статье пойдет речь об opensource базе данных TimescaleDB, которая предназначена для хранения временных рядов. Мы поговорим об особенностях хранения временных рядов, рассмотрим, как устроена внутри TimescaleDB, рассмотрим на примере запись и чтение метрик в TimescaleDB.

В одной из предыдущих статей мы рассматривали одну очень интересную базу данных для хранения временных рядов InfluxDB. Мы говорили о том, что InfluxDB очень гибкая позволяет быстро начать работу и, самое главное, InfluxDB заточена под запись хранение и обработку временных рядов. А что это означает: "заточена под запись хранение и обработку временных рядов"? Давайте разберемся.

Разработчики для хранения данных разрабатываемых приложений (будем называть их бизнес данными) в зачастую используют реляционные SQL базы данных. Такие базы строго структурированы и хорошо обеспечивают целостность данных благодаря механизмам внешних ключей и транзакций, позволяют делать сложные запросы к данным и выполняют эти запросы быстро благодаря индексам на колонки, по которым идет фильтрация и сортировка. Бизнес данные - это в большей своей части описание некоторых сущностей реального мира, которые имеют свои параметры, и эти параметры меняются во времени. Когда происходит изменение параметров сущности - происходит обновление определенной строки в определенной таблице в базе данных, при этом предыдущее значение мы не запоминаем, а перезаписываем на новое. А что если мы хотим сохранить все предыдущие состояния наших сущностей? Если мы будем запоминать все изменения объектов - то наши данные превращаются во временные ряды.

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

Так чем же принципиально отличаются данные временных рядов от бизнес данных, которые мы привыкли хранить в реляционных базах данных?

Временные ряды или метрики имеют специфический вид:

Metrics: CPU, free_mem, net_rssi, battery

Tags:    Host=MyServer, Region=West

Data:
2017-01-01 01:02:00    70    500    -40    80
2017-01-01 01:03:00    71    400    -42    80
2017-01-01 01:04:00    72    367    -41    80
2017-01-01 01:05:01    68    750    -54    79

В представленной выше модели данных присутствуют наименования метрик: загрузка процессора (CPU), свободная память (free_mem), характеристики сети (net_rssi), уровень заряда батареи (battery). Также представлен контекст в рамках которых были получены данные: наименование устройства Host=MyServer и местоположение устройства Region=West. И непосредственно массив данных с показаниями метрик. То есть мы видим, что эти данные описывают как система, процесс или поведение меняются с течением времени.

Основные характеристики модели данных временных рядов

Выделим важные характеристики данных, описываемых временными рядами:
  • Ориентированы на время - каждая запись имеет отметку времени.
  • Только добавление - данные только добавляются, но не обновляются.
  • Работа со свежими данными - новые данные как правило приходят с новыми метками времени, мы редко обновляем или восстанавливаем пробелы в старых данных.
Частота данных менее важна. Мы можем собирать данные каждую неделю или каждую секунду. Можем собирать данные регулярно и нерегулярно, например, при возникновении каких-нибудь событий в системе.

Но разве у реляционных баз данных не было временных полей ранее? Ключевое различие между данными временных рядов (и базами данных, которые их поддерживают), по сравнению с другими данными, такими как стандартные реляционные «бизнес-данные», заключается в том, что изменения данных осуществляются вставками, а не перезаписываются.

Хранение данных временных рядов в реляционной БД

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

Здесь видно, что после вставки 100 млн. строк время записи каждой новой строки начинает деградировать. Аналогичную картину мы увидим и в других реляционных базах данных. Из-за этой проблемы привычные нам таблицы в реляционных базах данных не подходят для решения задачи хранения данных временных рядов. Поэтому для хранения временных рядов возникло большое количество специфичных баз данных, таких как InfluxDB, OpenTSDB и др., которые имеют собственную внутреннюю структуру и в которых время вставки новых значений не увеличивается при увеличении количества данных.

TimescaleDB

Относительно недавно появился проект под названием TimescaleDB, который по сути является расширением для PostgreSQL и позволяет решить проблему вставки данных, описанную выше. И, как следствие, позволяет использовать привычный PostgreSQL как для хранения бизнес данных, так и данных временных рядов в одном месте.

Модель данных TimescaleDB

Немного разберемся, как устроена внутри модель хранения временных рядов в TimescaleDB.  TimescaleDB использует модель "широкой таблицы" (wide-table) в отличие от многих других баз данных для хранения временных рядов, которые используют подход "узкой таблицы" (narrow-table) (подробнее о wide narrow data в википедии).

Пример.
Представьте, что вы имеете 1000 устройств, которые собирают данные с датчиков из окружающей среды в определенные моменты времени. Такие данные могут в себя включать:
  • Идентификаторы: device_id, timestamp
  • Метаданные: location_id, dev_type, firmware_version, customer_id
  • Метрики устройств: cpu_1m_avg, free_mem, used_mem, net_rssi, net_loss, battery
  • Данные с датчиков: temperature, humidity, pressure, CO, NO2, PM10
В случае узкой таблицы данные бы выглядели примерно так:
1. {name:  cpu_1m_avg,  device_id: abc123,  location_id: 335,  dev_type: field}
2. {name:  cpu_1m_avg,  device_id: def456,  location_id: 335,  dev_type: roof}
3. {name:  cpu_1m_avg,  device_id: ghi789,  location_id:  77,  dev_type: roof}
4. {name:    free_mem,  device_id: abc123,  location_id: 335,  dev_type: field}
5. {name:    free_mem,  device_id: def456,  location_id: 335,  dev_type: roof}
6. {name:    free_mem,  device_id: ghi789,  location_id:  77,  dev_type: roof}
7. {name: temperature,  device_id: abc123,  location_id: 335,  dev_type: field}
8. {name: temperature,  device_id: def456,  location_id: 335,  dev_type: roof}
9. {name: temperature,  device_id: ghi789,  location_id:  77,  dev_type: roof}

То есть данные представлены набором точек, которые по отдельности в себе содержат пары время-значения плюс набор тэгов, которые идентифицируют в каком контексте была получена точка. Данный подход имеет смысл, когда вы собираете метрики независимо друг от друга. В таком представлении утрачивается структура данных, и становится затруднительно ответить на вопросы такие как:
  • Каково было состояние системы, когда free_mem стала равняться 0?
  • Как cpu_1m_avg коррелирует с free_mem?
  • Какая средняя температура в локации с идентификатором location_id?
При подходе wide-table структура и зависимость между данными сохраняется. И исходные данные сенсоров и устройств выглядят примерно так:
timestampdevice_idcpu_1m_avgfree_memtemperaturelocation_iddev_type
2017-01-01 01:02:00abc12380500MB7242field
2017-01-01 01:02:23def45690400MB6442roof
2017-01-01 01:02:30ghi7891200MB5677roof
2017-01-01 01:03:12abc12380500MB7242field
2017-01-01 01:03:35def45695350MB6442roof
2017-01-01 01:03:42ghi789100100MB5677roof

Здесь каждая строка представляет собой весь срез данных и метаданных в данный момент времени. Это позволяет сохранять отношения внутри данных и задавать более интересные исследовательские запросы, в отличие от narrow-table представления. Также мы можем дополнить метаданные, положив их в связанную таблицу. И при необходимости делать join метаданных к временным данным.

Архитектура TimescaleDB

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

С точки зрения пользователя в TimescaleDB вводится особый тип таблиц, которые называются гипертаблицами (hypertables), которые на самом деле являются абстракцией или виртуальным представлением множества отдельных таблиц, содержащие данные, которые называются чанками (chunks).
Чанки в свою очередь являются разбиением гипертаблицы на одно или несколько измерений. Все гипертаблицы обязательно разбиваются по времени, а также могут быть разбиты по различным ключам разбиения, например по deviceId, userId, location и т.д. То есть данные разбиваются на "пространство и время".

Hypertable

Первичной точкой взаимодействия с данными является гипертаблица (hypertable) - абстракция одной непрерывной таблицы во всех пространственных и временных измерениях. К данной таблице можно получить доступ через стандартный SQL.

Практически все пользовательские взаимодействия с TimescaleDB связаны с гипертаблицами. Создание таблиц и индексов, изменение таблиц, вставка данных, выбор данных и т.д. Могут (и должны) выполняться на гипертаблице.

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

Создание гипертаблицы в TimescaleDB можно выполнить с помощью двух простых команд SQL: CREATE TABLE (со стандартным синтаксисом SQL), за которым следует SELECT create_hypertable().

Индексы по времени и по ключам разбиения автоматически создаются в гипертаблицах, хотя могут быть созданы дополнительные индексы (TimescaleDB поддерживает полный диапазон типов индексов PostgreSQL).

Chunks

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

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

Чанки имеют нужный размер. При этом гарантируется, что все B-деревья индексов будут находиться в памяти при вставке новых значений в гипертаблицу. Избегая черезмерно больших чанков, мы можем избежать дорогостоящих операции vacuum'инга при удалении данных, например, удаляя не определенные строки а сразу целые чанки.

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

Использование TimescaleDB

Давайте попробуем установить TimescaleDB, создать гипертаблицу и записать в нее данные. На машине должен быть установлен PostgreSQL 9.6.  Для начала установим TimescaleDB пакет:
sudo add-apt-repository ppa:timescale/timescaledb-ppa
sudo apt-get update

sudo apt install timescaledb-postgresql-9.6

Далее необходимо отредактировать postgresql.conf, включить расширение timescaledb:
shared_preload_libraries = 'timescaledb'
Сохраняем файл делаем рестарт PostgreSQL
sudo service postgresql restart

Заходим в psql создаем базу данных и подключаемся к ней:
sudo su - postgres
psql

postgres=# create database timescale;
CREATE DATABASE
postgres=# \c timescale
You are now connected to database "timescale" as user "postgres".
timescale=# 

Затем необходимо включить расширение TimescaleDB:
timescale=# CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;
CREATE EXTENSION
timescale=# 

Теперь создадим таблицу, в которую мы будем записывать показания температуры и влажности  с датчиков:
CREATE TABLE conditions (
  time        TIMESTAMPTZ       NOT NULL,
  location    TEXT              NOT NULL,
  temperature DOUBLE PRECISION  NULL,
  humidity    DOUBLE PRECISION  NULL
);

Таблица содержит 4 колонки:
  • time - время получения значения;
  • location - отметка локации, где были получены показания датчика;
  • temperature - значение температуры;
  • humidity - значение влажности;
Превратим таблицу conditions в гипертаблицу с помощью функции create_hypertable. В гипертаблице сделаем разбиение по пространству времени time и по пространству локации location на 4 раздела:
SELECT create_hypertable('conditions', 'time', 'location', 4);

Все. Теперь для записи и чтения значений метрик можно использовать обычный SQL. Давайте добавим несколько значений в нашу таблицу:
INSERT INTO conditions(time, location, temperature, humidity)
  VALUES 
  (NOW(), 'office', 23.0, 33.7),
  (NOW(), 'home', 21.0, 63.5),
  (NOW(), 'courtyard', 19.2, 78.0),
  (NOW(), 'garage', 20.8, 69.9);

Сделаем запрос данных из таблицы:
timescale=# SELECT * FROM conditions ORDER BY time DESC LIMIT 100;
             time              | location  | temperature | humidity 
-------------------------------+-----------+-------------+----------
 2018-06-10 07:16:43.750419+03 | garage    |        20.8 |     69.9
 2018-06-10 07:16:43.750419+03 | office    |          23 |     33.7
 2018-06-10 07:16:43.750419+03 | courtyard |        19.2 |       78
 2018-06-10 07:16:43.750419+03 | home      |          21 |     63.5
(4 rows)

Все возможности работы с гипертаблицами описаны в документации.

Выводы

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

Полезные ссылки