Данни от времеви серии: Защо (и как) да се използва релационна база данни вместо NoSQL

В наши дни приложенията за данни от времеви серии (например, център за данни / сървър / микросервиз / мониторинг на контейнери, анализ на сензори / IoT, анализ на финансови данни и др.) Се разпространяват.

В резултат на това базите от времеви серии са на мода (тук са 33 от тях). Повечето от тях се отказват от атрибутите на традиционна релационна база данни и приемат това, което обикновено е известно като NoSQL модел. Моделите на използване са сходни: скорошно проучване показа, че разработчиците предпочитат NoSQL пред релационни бази данни за данни от времеви серии с над 2: 1.

Релационните бази данни включват: MySQL, MariaDB Server, PostgreSQL. Базите данни на NoSQL включват: Elastic, InfluxDB, MongoDB, Cassandra, Couchbase, Graphite, Prometheus, ClickHouse, OpenTSDB, DalmatinerDB, KairosDB, RiakTS. Източник: https://www.percona.com/blog/2017/02/10/percona-blog-poll-database-engine-using-store-time-series-data/

Обикновено причината за приемане на бази данни от времеви серии от NoSQL се свежда до мащаб. Въпреки че релационните бази данни имат много полезни функции, които повечето бази данни NoSQL нямат (стабилна поддръжка на вторичен индекс; сложни предикати; богат език за заявки; JOINs и т.н.), те са трудни за мащабиране.

И тъй като данните от времеви серии се натрупват много бързо, много разработчици смятат, че релационните бази данни не са подходящи за това.

Ние заемаме различна, донякъде еретична позиция: релационните бази данни могат да бъдат доста мощни за данни от времеви серии. Човек просто трябва да реши проблема с мащабирането. Това правим в TimescaleDB.

Когато обявихме TimescaleDB преди две седмици, получихме много положителни отзиви от общността. Но също така чухме от скептици, на които беше трудно да повярват, че човек трябва (или би могъл) да изгради мащабируема база данни от времеви серии на релационна база данни (в нашия случай PostgreSQL).

Има два отделни начина да мислите за мащабирането: мащабиране, така че една машина може да съхранява повече данни и мащабиране, така че данните да могат да се съхраняват на множество машини.

Защо и двете са важни? Най-често срещаният подход за мащабиране през клъстер от N сървъри е да се раздели или раздели набор от данни на N дялове. Ако всеки сървър е ограничен в своята производителност или производителност (т.е. не е в състояние да увеличи мащаба си), тогава общата пропускливост на клъстера е значително намалена.

Тази публикация обсъжда мащабирането. (Публикуваща публикация ще бъде публикувана на по-късна дата.)

По-специално тази публикация обяснява:

  • Защо релационните бази данни обикновено не се увеличават добре
  • Как LSM дърветата (обикновено се използват в бази данни NoSQL) не решават адекватно нуждите на много приложения от времеви серии
  • Данните от времеви серии са уникални, как човек може да използва тези различия, за да преодолее проблема с мащабирането и някои резултати от производителността

Нашите мотивации са двойни: за всеки, който се сблъсква с подобни проблеми, да сподели наученото от нас; и за тези, които обмислят да използват TimescaleDB за данни от времеви серии (включително скептиците!), за да обясним някои от нашите дизайнерски решения.

Защо базите обикновено не се увеличават добре: подмяната на / в паметта е скъпа

Често срещан проблем с мащабирането на производителността на базата данни на една машина е значителното компромиси между цена и производителност между памет и диск. Докато паметта е по-бърза от диска, тя е много по-скъпа: около 20 пъти по-скъпа от твърдо състояние като Flash, 100x по-скъпа от твърдите дискове. В крайна сметка целият ни набор от данни няма да се побере в паметта, поради което ще трябва да запишем данните и индексите си на диск.

Това е стар, често срещан проблем за релационните бази данни. В повечето релационни бази данни, таблицата се съхранява като колекция от страници с фиксиран размер на данни (например, 8KB страници в PostgreSQL), отгоре на която системата изгражда структури от данни (като например B-дървета), за да индексира данните. С индекс, заявката може бързо да намери ред с посочен идентификационен номер (например номер на банкова сметка), без да сканира цялата таблица или да „разхожда“ таблицата в някакъв сортиран ред.

Сега, ако работният набор от данни и индекси е малък, можем да го запазим в паметта.

Но ако данните са достатъчно големи, че не можем да поберем всички (с подобен размер фиксиран размер) страници от нашето B-дърво в паметта, тогава актуализирането на произволна част от дървото може да включва значителни дискови I / O, докато четем страници от диска в паметта, модифицирайте в паметта и след това изпишете обратно на диска (когато бъде изгонен, за да направите място за други страници на B-tree). И релационна база данни като PostgreSQL поддържа B-дърво (или друга структура от данни) за всеки индекс на таблицата, за да може стойностите в този индекс да бъдат намерени ефективно. И така, проблемните съединения, когато индексирате повече колони.

В действителност, тъй като базата данни има достъп само до диска в граници с размер на страницата, дори привидно малките актуализации могат да причинят появата на тези суапове: За да промените една клетка, базата данни може да се наложи да смени съществуваща страница от 8KB и да я запише обратно на диска, след това прочетете в новата страница, преди да я модифицирате.

Но защо да не използвате страници с по-малки или променливи размери? Има две добри причини: минимизиране на фрагментацията на диска и (в случай на въртящ се твърд диск), минимизиране на режийните разходи на времето за търсене (обикновено 5–10 ms), необходимо за физическото преместване на дисковата глава на ново място.

Какво ще кажете за твърдотелни устройства (SSD)? Докато решения като NAND Flash дискове елиминират всяко физическо време за „търсене“, те могат да бъдат прочетени от или записани само на ниво подробност на страницата (днес, обикновено 8KB). И така, дори за да актуализирате един байт, фърмуерът на SSD трябва да прочете 8KB страница от диска до буферния си кеш, да модифицира страницата, след което да запише обновената страница от 8KB обратно в нов блок на диска.

Цената за смяна на паметта и извън нея може да се види в тази графика на производителността от PostgreSQL, където вмъкнете пропускателна способност с размер на таблицата и се увеличава в зависимост (в зависимост от това дали заявките попадат в паметта или изискват (потенциално множество) извличания от диска).

Вмъкнете пропускателната способност като функция от размера на таблицата за PostgreSQL 9.6.2, работи с 10 работници на машина Azure стандартен DS4 v2 (8 ядро) със SSD (premium LRS) съхранение. Клиентите вмъкват отделни редове в базата данни (всяка от тях има 12 колони: времева марка, индексиран произволно избран основен идентификатор и 10 допълнителни цифрови показатели). Скоростта на PostgreSQL започва с над 15K вмъквания / секунда, но след това започва значително да намалява след 50M редове и започва да изпитва много висока дисперсия (включително периоди от само 100s вмъквания / сек).

Въведете NoSQL бази данни с структурирани от дървета обединени дървета (и нови проблеми)

Преди около десетилетие започнахме да виждаме редица системи за съхранение „NoSQL“ да се справят с този проблем чрез дървесни структурирани сливащи (LSM) дървета, които намаляват разходите за създаване на малки записи, като извършват само по-големи записи, които се добавят само на диск.

Вместо да изпълнява записи „на място“ (когато малка промяна на съществуваща страница изисква четене / запис на цялата страница от / на диск), LSM дърветата поставят на опашка няколко нови актуализации (включително изтривания!) На страници и ги записват като една партида на диск. По-специално, всички записи в LSM дърво се изпълняват на сортирана таблица, поддържана в паметта, която след това се прехвърля на диск като неизменна партида, когато е с достатъчен размер (като „сортирана таблица от низове“ или SSTable). Това намалява разходите за създаване на малки записи.

В LSM дърво всички актуализации се записват първо в сортирана таблица в паметта и след това се прехвърлят на диск като неизменна партида, съхранявана като SSTable, която често се индексира в паметта.
(Източник: https://www.igvita.com/2012/02/06/sstable-and-log-structured-storage-leveldb/)

Тази архитектура - която беше възприета от много бази данни „NoSQL“ като LevelDB, Google BigTable, Cassandra, MongoDB (WiredTiger) и InfluxDB - може да изглежда страхотна в началото. И все пак той въвежда други компромиси: по-високи изисквания към паметта и лоша поддръжка на вторичен индекс.

Изисквания за по-висока памет: За разлика от B-дърво, в LSM дървото няма единично подреждане: няма глобален индекс, който да ни подреди сортиран ред за всички клавиши. Следователно търсенето на стойност за ключ става по-сложно: първо проверете таблицата с памет за най-новата версия на ключа; в противен случай потърсете (потенциално много) таблици на диска, за да намерите най-новата стойност, свързана с този ключ. За да се избегне прекомерен дисков I / O (и ако самите стойности са големи, например съдържанието на уеб страниците, съхранявано в BigTable на Google), индексите за всички SSTables може да се запазят изцяло в паметта, което от своя страна увеличава изискванията за памет.

Лоша поддръжка на вторичен индекс: Като се има предвид, че те нямат глобален подреден ред, LSM дърветата естествено не поддържат вторични индекси. Различни системи са добавили допълнителна поддръжка, като например дублиране на данните в различен ред. Или те подражават на поддръжка за по-богати предикати, като изграждат основния си ключ като обединяване на множество стойности. И все пак този подход идва с цената на изискването за по-голямо сканиране сред тези клавиши по време на заявката, като по този начин поддържа само елементи с ограничена кардиналност (например дискретни стойности, а не цифрови).

Има по-добър подход към този проблем. Да започнем с по-доброто разбиране на данните от времеви серии.

Данните от времеви серии са различни

Нека да направим крачка назад и да разгледаме първоначалния проблем, който релационните бази данни са проектирани да решат. Започвайки от семенната система R на IBM в средата на 70-те години на миналия век, релационни бази данни бяха използвани за това, което стана известно като онлайн обработка на транзакции (OLTP).

В OLTP операциите често са транзакционни актуализации на различни редове в база данни. Например, помислете за банков превод: потребител дебитира пари от една сметка и кредитира друга. Това съответства на актуализации на два реда (или дори само две клетки) на таблица на база данни. Тъй като банкови преводи могат да се извършват между всеки два акаунта, двата реда, които са променени, са донякъде произволно разпределени в таблицата.

Данните от времеви серии възникват от много различни настройки: промишлени машини; транспорт и логистика; DevOps, център за обработка на данни и мониторинг на сървъри; и финансови приложения.

Сега нека разгледаме няколко примера за натоварвания от времеви серии:

  • DevOps / мониторинг на сървър / контейнер Обикновено системата събира показатели за различни сървъри или контейнери: използване на процесора, свободна / използвана памет, мрежова tx / rx, дискова IOPS и др. Всеки набор от показатели е свързан с времева марка, уникално име / идентификатор на сървър и набор от тагове които описват атрибут на това, което се събира.
  • Данни от IoT сензора. Всяко IoT устройство може да отчита множество показания на сензора за всеки период от време. Като пример за мониторинг на качеството на околната среда и въздуха това може да включва: температура, влажност, барометрично налягане, нива на звука, измервания на азотен диоксид, въглероден окис, прахови частици и др. Всеки набор от показания е свързан с марка на време и уникален идентификатор на устройството и може да съдържа други метаданни.
  • Финансови данни. Данните за финансовите тикове могат да включват потоци с времева марка, името на ценната книга и текущата цена и / или промяна на цената. Друг вид финансови данни са платежните транзакции, които ще включват уникален идентификационен номер на акаунта, времева марка, сума на транзакцията, както и всякакви други метаданни. (Обърнете внимание, че тези данни са различни от примера на OLTP по-горе: тук записваме всяка транзакция, докато OLTP системата отразява само текущото състояние на системата.)
  • Управление на флота / активите. Данните могат да включват идентификатор на превозно средство / актив, времева марка, GPS координати по това време и всякакви метаданни.

Във всички тези примери, наборите от данни са поток от измервания, които включват вмъкване на „нови данни“ в базата данни, обикновено до последния интервал от време. Въпреки че е възможно данните да пристигат много по-късно, отколкото когато са били генерирани / задействани по време, или поради забавяне на мрежата / системата или поради корекции за актуализиране на съществуващи данни, това обикновено е изключение, а не норма.

С други думи, тези две натоварвания имат много различни характеристики:

OLTP Пише

  • Основно актуализира
  • Случайно разпределени (върху множеството първични ключове)
  • Често транзакции в множество първични ключове

Времеви серии пише

  • Основно ВЪВЕЖДАЙТЕ
  • Предимно на скорошен интервал от време
  • Основно свързан както с времева марка, така и с отделен първичен ключ (например идентификатор на сървъра, идентификатор на устройството, идентификатор на защита / акаунт, идентификатор на превозно средство / актив и т.н.)

Защо това има значение? Както ще видим, човек може да се възползва от тези характеристики, за да реши проблема с мащабирането на релационна база данни.

Нов начин: Адаптивно време / пространство

Когато предишните подходи се опитваха да избегнат малки записи на диск, те се опитваха да адресират по-широкия OLTP проблем на UPDATE на произволни места. Но както току-що установихме, натоварванията от времеви серии са различни: записите са предимно INSERTS (а не UPDATES), до скорошен интервал от време (а не случайно местоположение). С други думи, натоварванията от времеви серии са добавени само.

Това е интересно: това означава, че ако данните са сортирани по време, ние винаги ще пишем към „края“ на нашата база данни. Организирането на данни по време също би ни позволило да запазим действително работния набор от страници от бази данни доста малки и да ги поддържаме в паметта. И четенията, за които сме прекарали по-малко време в обсъждане, също биха могли да се възползват: ако много заявки за четене са до последните интервали (например за табла в реално време), тези данни вече ще бъдат кеширани в паметта.

На пръв поглед може да изглежда, че индексирането на времето би ни предоставило ефективни записи и четене безплатно. Но след като искаме всякакви други индекси (напр. Друг основен ключ като идентификатор на сървър / устройство или някакви вторични индекси), тогава този наивен подход ще ни върне обратно към правенето на произволни вложки в нашето B-дърво за този индекс.

Има и друг начин, който наричаме „адаптиране на време / пространство“. Това използваме в TimescaleDB.

TimescaleDB съхранява всеки фрагмент във вътрешна таблица на базата данни, така че индексите растат само с размера на всеки парче, а не на целия хипертабилен. Тъй като вложките са до голяма степен в по-новия интервал, това остава в паметта, като се избягват скъпи размени на диск.

Вместо само да индексира по време, TimescaleDB изгражда отделни таблици, като разделя данни според две измерения: интервал от време и първичен ключ (например, идентификатор на сървър / устройство / актив). Ние ги наричаме парчета, за да ги разграничим от дялове, които обикновено се дефинират чрез разделяне на пространството на първичния ключ. Тъй като всеки от тези парчета се съхранява като самата таблица на базата данни и планиращият заявките е наясно с диапазоните (във времето и пространството на клавишите), планиращият запитване може веднага да каже на кой фрагмент (и) принадлежат данните на операцията. (Това се отнася както за вмъкване на редове, така и за подрязване на набора откъси, които трябва да се докоснат при изпълнение на заявки.)

Основното предимство на този подход е, че сега всички наши индекси са изградени само в тези много по-малки парчета (таблици), а не в една таблица, представляваща целия набор от данни. Така че ако правилно оразмерим тези парчета, можем да поберем най-новите таблици (и техните B-дървета) напълно в паметта и да избегнем този проблем за размяна на диск, като същевременно поддържаме поддръжка за множество индекси.

Подходи към прилагането на парчета

Двата интуитивни подхода за проектиране на този период от време / пространство имат значителни ограничения:

Подход №1: Интервали с фиксирана продължителност

При този подход всички парчета могат да имат фиксирани, еднакви интервали от време, например 1 ден. Това работи добре, ако обемът на данните, събрани на интервал, не се промени. Въпреки това, тъй като услугите стават популярни, тяхната инфраструктура съответно се разширява, което води до повече сървъри и повече данни за наблюдение. По подобен начин успешните продукти на IoT ще разполагат с все по-голям брой устройства. И след като започнем да пишем твърде много данни на всеки парче, редовно се разменяме на диск (и ще се озовем отново в квадрат). От друга страна, изборът на твърде малки интервали, за да започнете, води до други недостатъци на производителността, например, трябва да докосвате много таблици по време на заявка.

Всеки парче има фиксирана продължителност във времето. Но ако обемът на данните се увеличава, в крайна сметка размерът на парче става твърде голям, за да се впише в паметта.

Подход №2: Парчета с фиксиран размер

При този подход всички парчета имат фиксирани целеви размери, например 1 GB. Писа се парче, докато достигне максималния си размер, в този момент става „затворен“ и ограниченията му във времевия интервал стават фиксирани. По-късните данни, попадащи в интервала на „затворения“, ще продължат да се записват в парчето, за да се запази точността на времевите ограничения на парчето.

Ключово предизвикателство е, че времевият интервал на парчето зависи от реда на данните. Помислете дали данните (дори една точка за данни) пристигат „рано“ по часове или дори дни, потенциално поради несинхронизиран часовник или поради различни закъснения в системи с прекъсната свързаност. Тази ранна точка от данни ще разшири интервала от време на „отворения“ фрагмент, докато последващите данни за време могат да задвижат парчето над целевия му размер. Логиката на вмъкване за този подход също е по-сложна и скъпа, намалявайки пропускателната способност за големи партидни записи (като например големи операции COPY), тъй като базата данни трябва да се увери, че тя въвежда данни във времева последователност, за да определи кога трябва да се създаде нов фрагмент (дори в средата на операция). Други проблеми съществуват и за парчета с фиксиран или максимален размер, включително времеви интервали, които може да не съвпадат добре с политиките за запазване на данни („изтрийте данните след 30 дни“).

Времевият интервал на всеки парче се фиксира само след достигане на максималния му размер. Но ако данните пристигнат рано, това създава голям интервал за парчето и в крайна сметка парчето става твърде голямо, за да се впише в паметта.

TimescaleDB използва трети подход, който съчетава силните страни и на двата подхода.

Подход №3: Адаптивни интервали (сегашният ни дизайн)

Парчетата се създават с фиксиран интервал, но интервалът се приспособява от парче до парче въз основа на промените в обемите на данни, за да се постигнат максимални целеви размери.

Избягвайки отворени интервали, този подход гарантира, че пристигащите данни рано не създават твърде дълги интервали от време, които впоследствие ще доведат до прекалено големи парчета. Освен това, подобно на статичните интервали, той по-естествено поддържа политики за задържане, посочени във времето, например „изтриване на данни след 30 дни“. Предвид откъсването от време, основано на TimescaleDB, такива политики се прилагат чрез просто изпускане на парчета (таблици) в базата данни. Това означава, че отделни файлове в основната файлова система могат просто да бъдат изтрити, вместо да се налага да изтривате отделни редове, което изисква изтриване / обезценяване на части от базовия файл. Ето защо такъв подход избягва фрагментацията в основните файлове на базата данни, което от своя страна избягва необходимостта от вакуумиране. И това вакуумиране може да бъде изключително скъпо в много големи маси.

Все пак този подход гарантира, че парчетата са оразмерени по подходящ начин, така че най-новите могат да се поддържат в паметта, дори и да се променят обемите на данни.

След това разпределението чрез първичен ключ отнема всеки интервал от време и по-нататък го разделя на няколко по-малки парчета, които всички споделят един и същ интервал от време, но са разединени по отношение на тяхното основно клавишно пространство. Това дава възможност за по-добра паралелизация както на сървъри с множество дискове - както за вмъквания и запитвания - - така и на множество сървъри. Повече по тези въпроси в по-късен пост.

Ако обемът на данните се увеличава, интервалът на парчетата намалява, за да поддържа парчета с десен размер.
Ако данните пристигнат рано, тогава данните се съхраняват в „бъдеща“ част, за да се поддържат парчета с десен размер.

Резултат: 15x подобрение на скоростта на вмъкване

Поддържането на парчета в правилния размер е как постигаме нашите INSERT резултати, които надминават ванилията PostgreSQL, което Ajay вече показа в предишния си пост.

Вмъкнете пропускателна способност на TimescaleDB срещу PostgreSQL, като използвате същата натовареност, както е описана по-рано. За разлика от vanilla PostgreSQL, TimescaleDB поддържа постоянна скорост на вмъкване (от около 14,4K вмъквания / секунда или 144K показатели / секунда, с много ниска дисперсия), независимо от размера на набора от данни.

Тази последователна пропускателна способност се запазва и при записване на големи партиди от редове в единични операции в TimescaleDB (вместо ред по ред). Такива партидни вложки са обичайна практика за бази данни, използвани в по-мащабна производствена среда, например при поглъщане на данни от разпределена опашка като Kafka. В такива сценарии един сървър на Timecale може да поема 130K редове (или 1.3M показатели) в секунда, приблизително 15x този на ванилия PostgreSQL, след като таблицата достигне няколко 100M реда.

Вмъкнете пропускателна способност на TimescaleDB срещу PostgreSQL, когато изпълнявате INSERT на партиди от 10 000 реда.

резюме

Релационната база данни може да бъде доста мощна за данни от времеви серии. И все пак разходите за смяна на паметта / изхода от паметта значително се отразяват на тяхната ефективност. Но подходите на NoSQL, които прилагат Log Structured Merge Trees, само изместиха проблема, въвеждайки по-високи изисквания към паметта и лоша поддръжка на вторичен индекс.

Разпознавайки, че данните от времеви серии са различни, ние можем да организираме данните по нов начин: адаптивни времена / пространство. Това свежда до минимум смяната на диск, като запазва работните данни, достатъчно малки, за да се поберат в паметта, като същевременно ни позволява да поддържаме стабилна поддръжка на първичен и вторичен индекс (и пълния набор от функции на PostgreSQL). В резултат на това ние сме в състояние да мащабираме значително PostgreSQL, което води до 15x подобрение на скоростта на вмъкване.

Но какво да кажем за сравненията на производителността с базите данни на NoSQL? Този пост идва скоро.

Междувременно можете да изтеглите последната версия на TimescaleDB, издадена под разрешителния лиценз Apache 2, на GitHub.

Харесва ли ви този пост? Интересувате се да научите повече?

Следвайте ни тук на Medium, разгледайте нашия GitHub, присъединете се към нашата Slack общност и се запишете за списъка с пощенски съобщения на общността по-долу. Ние също се наемаме!