Data časových řad: Proč (a jak) použít relační databázi namísto NoSQL

V těchto dnech se rozšiřují datové aplikace časových řad (např. Monitorování datových center / serverů / mikroskopických služeb / kontejnerů, analytika senzorů / IoT, analýza finančních dat atd.).

Výsledkem je, že databáze časových řad jsou v módě (zde jich je 33). Většina z nich se vzdává úchytů tradiční relační databáze a přijímá to, co je obecně známé jako model NoSQL. Vzory použití jsou podobné: nedávný průzkum ukázal, že vývojáři preferovali NoSQL před relačními databázemi pro data časových řad o více než 2: 1.

Relační databáze zahrnují: MySQL, MariaDB Server, PostgreSQL. Databáze NoSQL zahrnují: Elastic, InfluxDB, MongoDB, Cassandra, Couchbase, Graphite, Prometheus, ClickHouse, OpenTSDB, DalmatinerDB, KairosDB, RiakTS. Zdroj: https://www.percona.com/blog/2017/02/10/percona-blog-poll-database-engine-using-store-time-series-data/

Důvod pro přijetí databází časových řad NoSQL se obvykle zmenšuje. Přestože relační databáze mají mnoho užitečných funkcí, které většina databází NoSQL nemá (robustní podpora sekundárních indexů; komplexní predikáty; bohatý dotazovací jazyk; JOIN atd.), Je obtížné je škálovat.

A protože se hromadí data časových řad velmi rychle, mnoho vývojářů se domnívá, že relační databáze pro ni nejsou vhodné.

Vezmeme jiný, poněkud kacířský postoj: relační databáze mohou být pro data časových řad docela silné. Jeden musí vyřešit problém měřítka. To děláme v TimescaleDB.

Když jsme před dvěma týdny oznámili TimescaleDB, dostali jsme od komunity hodně pozitivní zpětné vazby. Slyšeli jsme však také od skeptiků, kterým bylo těžké uvěřit, že člověk by měl (nebo mohl) vybudovat škálovatelnou databázi časových řad na relační databázi (v našem případě PostgreSQL).

Existují dva samostatné způsoby, jak přemýšlet o změně měřítka: zvětšení tak, aby jeden stroj mohl ukládat více dat, a škálování tak, aby data mohla být uložena na více počítačích.

Proč jsou oba důležité? Nejběžnějším přístupem k škálování v klastru serverů N je rozdělení nebo shard datové sady na oddíly N. Pokud je každý server omezen svou propustností nebo výkonem (tj. Nemůže zvětšit měřítko), pak je celková propustnost clusteru výrazně snížena.

Tento příspěvek pojednává o rozšiřování. (Příspěvek na změnu měřítka bude zveřejněn později.)

Tento příspěvek vysvětluje zejména:

  • Proč se relační databáze normálně nezvyšují
  • Jak stromy LSM (obvykle používané v databázích NoSQL) dostatečně nevyřeší potřeby mnoha aplikací časových řad
  • Jak jedinečná jsou data časových řad, jak lze tyto rozdíly využít k překonání problému škálování a některých výsledků výkonu

Naše motivace je dvojí: pro každého, kdo čelí podobným problémům, sdílet to, co jsme se naučili; a pro ty, kteří zvažují použití TimescaleDB pro data časových řad (včetně skeptiků!), vysvětlit některá z našich návrhových rozhodnutí.

Proč se databáze normálně nevylepšují: Vyměňování paměti z / do paměti je drahé

Běžným problémem škálování výkonu databáze na jednom počítači je výrazný kompromis mezi cenou a výkonem mezi pamětí a diskem. Zatímco paměť je rychlejší než disk, je mnohem dražší: asi 20x nákladnější než polovodičové úložiště jako Flash, 100x dražší než pevné disky. Celý náš datový soubor se nakonec nevejde do paměti, a proto musíme zapsat naše data a indexy na disk.

Toto je starý, běžný problém pro relační databáze. Ve většině relačních databází je tabulka uložena jako soubor stránek s pevnou velikostí (např. 8 kB stránky v PostgreSQL), na jehož vrcholu systém vytváří datové struktury (jako jsou například B-stromy) pro indexování dat. S indexem může dotaz rychle najít řádek se zadaným ID (např. Číslo bankovního účtu) bez skenování celé tabulky nebo „procházení“ tabulky v nějakém seřazeném pořadí.

Pokud je pracovní sada dat a indexů malá, můžeme je uchovat v paměti.

Ale pokud jsou data dostatečně velká, že se nevejdou všechny stránky (podobně pevné velikosti) našeho B-stromu do paměti, pak aktualizace náhodné části stromu může zahrnovat významné I / O na disku, když čteme stránky z disku do paměti, upravovat v paměti a poté zapisovat zpět na disk (když je vystěhován, aby se uvolnil prostor pro další stránky B-stromu). A relační databáze, jako je PostgreSQL, udržuje B-strom (nebo jinou datovou strukturu) pro každý index tabulky, aby bylo možné hodnoty v tomto indexu nalézt efektivně. Takže problém se spojuje, když indexujete více sloupců.

Ve skutečnosti, protože databáze přistupuje k disku pouze v mezích velikosti stránky, mohou tyto swapy způsobit i zdánlivě malé aktualizace: Ke změně jedné buňky může databáze potřebovat vyměnit existující stránku 8 kB a zapsat ji zpět na disk, před změnou si ji přečtěte na nové stránce.

Ale proč nepoužívat stránky menší nebo proměnné velikosti? Existují dva dobré důvody: minimalizace fragmentace disku a (v případě rotujícího pevného disku) minimalizace režie „doby hledání“ (obvykle 5–10 ms), která je nutná pro fyzický přesun hlavy disku do nového umístění.

A co disky SSD? Ačkoli řešení, jako jsou disky NAND Flash, eliminují jakýkoli fyzický čas „vyhledávání“, lze je číst nebo zapisovat pouze na úrovni podrobnosti na stránce (dnes, obvykle 8 kB). Takže i pro aktualizaci jednoho bajtu musí firmware SSD číst stránku 8KB z disku do vyrovnávací paměti, upravit stránku a poté aktualizovanou stránku 8KB zapsat zpět do nového diskového bloku.

Náklady na swapování a odkládání paměti lze vidět v tomto grafu výkonu z PostgreSQL, kde se propustnost vložení vrhá s velikostí tabulky a zvyšuje se rozptyl (v závislosti na tom, zda požadavky zasáhly paměť nebo vyžadují (potenciálně více) načtení z disku).

Vložte propustnost jako funkci velikosti tabulky pro PostgreSQL 9.6.2, běžící s 10 zaměstnanci na standardním stroji Azure DS4 v2 (8 jádrových) s úložištěm založeným na SSD (prvotřídní LRS). Klienti vkládají do databáze jednotlivé řádky (každý z nich má 12 sloupců: časové razítko, indexované náhodně vybrané primární ID a 10 dalších číselných metrik). Sazba PostgreSQL začíná rychlostí 15 kB za sekundu, ale poté začne významně klesat po 50 mil. Řádcích a začíná docházet k velmi velkým rozptylům (včetně období pouhých 100 s inzerátů / s).

Vstupte do databází NoSQL s protokolovanými slučovacími stromy (a novými problémy)

Asi před deseti lety jsme začali vidět, že řada úložných systémů „NoSQL“ řeší tento problém prostřednictvím stromů strukturovaných slučování (LSM), které snižují náklady na vytváření malých zápisů pouze prováděním větších zápisů pouze pro připojení na disk.

Spíše než provádění „in-place“ zápisů (kde malá změna na existující stránce vyžaduje čtení / zápis celé stránky z / na disk), stromy LSM řadí do fronty několik nových aktualizací (včetně vymazání!) Na stránky a zapisují je jako jedna dávka na disk. Zejména jsou všechny zápisy ve stromu LSM prováděny do tříděné tabulky udržované v paměti, která je poté propláchnuta na disk jako neměnná dávka, když má dostatečnou velikost (jako „tabulka tříděných řetězců“ nebo SSTable). To snižuje náklady na výrobu malých zápisů.

Ve stromu LSM jsou všechny aktualizace nejprve zapsány do paměti seřazené tabulky a poté vyprázdněny na disk jako neměnná dávka, uloženy jako SSTable, který je často indexován v paměti.
(Zdroj: https://www.igvita.com/2012/02/06/sstable-and-log-structured-storage-leveldb/)

Tato architektura - která byla přijata mnoha databázemi „NoSQL“, jako jsou LevelDB, Google BigTable, Cassandra, MongoDB (WiredTiger) a InfluxDB - se může na první pohled zdát skvělá. Přesto zavádí další kompromisy: vyšší požadavky na paměť a špatnou podporu sekundárních indexů.

Požadavky na vyšší paměť: Na rozdíl od stromu B, ve stromu LSM neexistuje jediné uspořádání: žádný globální index, který by nám dal seřazené pořadí na všechny klíče. V důsledku toho bude vyhledávání hodnoty pro klíč složitější: nejprve zkontrolujte tabulku paměti, kde je nejnovější verze klíče; jinak vyhledejte (potenciálně mnoho) tabulek na disku a vyhledejte nejnovější hodnotu přidruženou k tomuto klíči. Aby se zabránilo nadměrnému I / O disku (a pokud jsou samotné hodnoty velké, jako je například obsah webové stránky uložené v BigTable Google), mohou být indexy všech SSTables uchovávány zcela v paměti, což zase zvyšuje nároky na paměť.

Špatná podpora sekundárních indexů: Vzhledem k tomu, že jim chybí globální tříděné pořadí, stromy LSM přirozeně nepodporují sekundární indexy. Různé systémy přidaly nějakou další podporu, například duplikováním dat v jiném pořadí. Nebo emulují podporu bohatších predikátů vytvářením jejich primárního klíče jako zřetězení více hodnot. Tento přístup však přináší náklady vyžadující větší skenování mezi těmito klíči v době dotazu, čímž podporuje pouze položky s omezenou mohutností (např. Diskrétní hodnoty, nikoli číselné).

K tomuto problému existuje lepší přístup. Začněme lepším pochopením dat časových řad.

Data časových řad se liší

Vraťme se o krok zpět a podívejme se na původní problém, který byly vytvořeny relační databáze. Počínaje klíčovým systémem R společnosti IBM v polovině 70. let minulého století byly relační databáze využívány pro tzv. Online transakční zpracování (OLTP).

V rámci OLTP jsou operace často transakční aktualizace různých řádků v databázi. Např. Přemýšlejte o bankovním převodu: uživatel odečte peníze z jednoho účtu a připíše jiný. To odpovídá aktualizacím dvou řádků (nebo dokonce jen dvou buněk) databázové tabulky. Protože k bankovním převodům může dojít mezi libovolnými dvěma účty, jsou dva řádky, které jsou upraveny, do tabulky náhodně rozděleny.

Časová řada dat vychází z mnoha různých nastavení: průmyslové stroje; doprava a logistika; DevOps, datová centra a monitorování serverů ;, a finanční aplikace.

Nyní se podívejme na několik příkladů pracovních časových řad:

  • Monitorování DevOps / server / kontejner. Systém obvykle shromažďuje metriky o různých serverech nebo kontejnerech: využití CPU, volná / použitá paměť, síťové tx / rx, diskové IOPS atd. Každá sada metrik je spojena s časovým razítkem, jedinečným jménem / ID serveru a sadou značek které popisují atribut toho, co se shromažďuje.
  • Data senzoru IoT. Každé zařízení IoT může hlásit více odečtů senzorů za každé časové období. Například pro monitorování životního prostředí a kvality ovzduší by to mohlo zahrnovat: teplotu, vlhkost, barometrický tlak, hladiny zvuku, měření oxidu dusičitého, oxidu uhelnatého, částic, atd. Každá sada měření je spojena s časovým razítkem a jedinečným ID zařízení a mohou obsahovat další metadata.
  • Finanční data. Údaje o finančním klíště mohou zahrnovat toky s časovým razítkem, názvem cenného papíru a jeho aktuální cenou nebo změnou ceny. Dalším typem finančních údajů jsou platební transakce, které by obsahovaly jedinečné ID účtu, časové razítko, částku transakce a jakákoli jiná metadata. (Všimněte si, že tato data se liší od výše uvedeného příkladu OLTP: zde zaznamenáváme každou transakci, zatímco systém OLTP právě odrážel aktuální stav systému.)
  • Správa vozového parku / majetku. Data mohou zahrnovat ID vozidla / aktiva, časové razítko, GPS souřadnice v tomto časovém razítku a jakákoli metadata.

Ve všech těchto příkladech jsou datové sady proudem měření, která zahrnují vkládání „nových dat“ do databáze, obvykle do posledního časového intervalu. I když je možné, že data dorazí mnohem později, než kdy byla vygenerována / časově označena, buď kvůli zpožděním sítě / systému nebo kvůli opravám za účelem aktualizace stávajících dat, je to obvykle výjimkou, nikoli normou.

Jinými slovy, tato dvě pracovní zatížení mají velmi odlišné vlastnosti:

Zápisy OLTP

  • Primárně UPDATY
  • Náhodně distribuované (přes sadu primárních klíčů)
  • Transakce často přes více primárních klíčů

Časové řady Zápisy

  • Primárně VLOŽÍ
  • Především do posledního časového intervalu
  • Primárně spojené s časovým razítkem a samostatným primárním klíčem (např. ID serveru, ID zařízení, ID zabezpečení / účtu, ID vozidla / majetku atd.)

Proč na tom záleží? Jak uvidíme, tyto vlastnosti lze využít k vyřešení problému škálování v relační databázi.

Nový způsob: Adaptivní čas / prostor chunking

Když se předchozí přístupy pokusily vyhnout malým zápisům na disk, pokoušely se řešit širší problém OLTP UPDATE na náhodná místa. Ale jak jsme právě zjistili, pracovní zatížení časových řad se liší: zápisy jsou primárně INSERTS (nikoli UPDATES), do nedávného časového intervalu (nikoli na náhodném místě). Jinými slovy, pracovní zatížení časových řad je připojeno pouze.

To je zajímavé: to znamená, že pokud jsou data tříděna podle času, vždy bychom psali směrem ke „konci“ našeho datového souboru. Organizace dat podle času by nám také umožnila udržovat skutečnou pracovní sadu databázových stránek poměrně malou a udržovat je v paměti. A čtení, o nichž jsme diskutovali méně času, by také mohlo být přínosem: pokud je mnoho čtených dotazů v nedávných intervalech (např. Pro dashboarding v reálném čase), pak by tato data byla již uložena do paměti.

Na první pohled se může zdát, že indexování včas by nám poskytlo efektivní zápisy a čtení zdarma. Ale jakmile chceme nějaké další indexy (např. Jiný primární klíč, jako je ID serveru / zařízení nebo jakékoli sekundární indexy), pak by nás tento naivní přístup vrátil zpět k tomu, abychom pro tento index vytvořili náhodné vložení do našeho B-stromu.

Existuje i jiný způsob, který nazýváme „adaptivní čas / prostorová chunking“. To je to, co používáme v TimescaleDB.

TimescaleDB ukládá každý kus do interní databázové tabulky, takže indexy rostou pouze s velikostí každého kusu, nikoli s celou hypertenzí. Protože vložky jsou z velké části v novějším intervalu, zůstává tento v paměti a vyhýbá se drahým swapům na disk.

Místo pouhého indexování podle času vytváří TimescaleDB oddělené tabulky rozdělením dat podle dvou rozměrů: časového intervalu a primárního klíče (např. ID serveru / zařízení / majetku). Nazýváme je jako kusy, které je odlišují od oddílů, které jsou obvykle definovány rozdělením prostoru primárních klíčů. Protože každý z těchto bloků je uložen jako samotná databázová tabulka a plánovač dotazů si je vědom rozsahů chunku (v čase a prostoru klíčů), může plánovač dotazů okamžitě zjistit, ke kterému datu chunku (bloků) patří operace. (To platí jak pro vkládání řádků, tak pro prořezávání sady bloků, kterých je třeba se dotknout při provádění dotazů.)

Hlavní výhoda tohoto přístupu spočívá v tom, že nyní jsou všechny naše indexy vytvářeny pouze na těchto mnohem menších kusech (tabulkách), nikoli na jediné tabulce představující celý datový soubor. Pokud tedy tyto bloky správně upravíme, můžeme nejnovější tabulky (a jejich B-stromy) zcela uložit do paměti a vyhnout se tomuto problému se swapem na disk při zachování podpory více indexů.

Přístupy k implementaci chunkingu

Oba intuitivní přístupy k navrhování tohoto časového / prostorového chunkingu mají významná omezení:

Přístup č. 1: Intervaly s pevným trváním

Při tomto přístupu mohou mít všechny bloky pevné, stejné časové intervaly, např. 1 den. To funguje dobře, pokud se objem dat shromážděných za interval nezmění. S rostoucí popularitou služeb se však jejich infrastruktura odpovídajícím způsobem rozšiřuje, což vede k více serverům a dalším monitorovacím datům. Podobně úspěšné produkty IoT nasadí stále více zařízení. A jakmile začneme zapisovat příliš mnoho dat do každého kusu, pravidelně se střídáme na disk (a nacházíme se na prvním místě). Na druhou stranu, výběr příliš malých intervalů pro začátek vede k dalším nevýhodám výkonu, např. K nutnosti dotknout se mnoha tabulek v době dotazu.

Každý kus má pevnou dobu trvání. Přesto, pokud se objem dat za čas zvětší, pak se nakonec velikost bloku stane příliš velkou, aby se vešla do paměti.

Přístup č. 2: Kusy pevné velikosti

S tímto přístupem mají všechny bloky pevné cílové velikosti, např. 1 GB. Kus se zapisuje, dokud nedosáhne své maximální velikosti, kdy se stane „uzavřeným“ a jeho omezení časových intervalů budou pevně stanovena. Pozdější data, která spadají do „uzavřeného“ intervalu kusu, však budou stále zapsána do bloku, aby byla zachována správnost časových omezení bloku.

Klíčovou výzvou je, že časový interval chunku závisí na pořadí dat. Zvažte, zda data (i jediný datový bod) dorazí „brzy“ po hodinách nebo dokonce dnech, potenciálně kvůli nesynchronizovaným hodinám nebo kvůli různým zpožděním v systémech s přerušovanou konektivitou. Tento časný datový bod prodlouží časový interval „otevřeného“ bloku, zatímco následná data v čase mohou řídit blok nad jeho cílovou velikost. Logika vložení pro tento přístup je také složitější a nákladnější, což snižuje propustnost pro velké dávkové zápisy (například velké operace COPY), protože databáze musí zajistit, aby vložila data v časovém pořadí, aby určila, kdy by měl být vytvořen nový kus. (i uprostřed operace). Další problémy existují také u bloků s pevnou nebo maximální velikostí, včetně časových intervalů, které nemusí být v souladu s politikou uchovávání dat („smazat data po 30 dnech“).

Časový interval každého kusu je pevně stanoven až po dosažení jeho maximální velikosti. Pokud však data dorazí brzy, vytvoří se pro chunk velký interval a chunk se nakonec stane příliš velkým na to, aby se vešel do paměti.

TimescaleDB používá třetí přístup, který spojuje silné stránky obou přístupů.

Přístup č. 3: Adaptivní intervaly (náš současný design)

Kusy jsou vytvářeny s pevným intervalem, ale interval se přizpůsobuje z kusů na kus na základě změn v objemech dat, aby dosáhl maximální cílové velikosti.

Tím, že se vyhnete časově neomezeným intervalům, zajistí tento přístup, že data, která přijdou brzy, nevytvoří příliš dlouhé časové intervaly, které následně povedou k příliš velkým kouskům. Dále, stejně jako statické intervaly, přirozeně podporuje zásady uchovávání stanovené v čase, např. „Smazat data po 30 dnech“. Vzhledem k časovému chunkingu TimescaleDB jsou takové zásady implementovány pouhým přetažením bloků (tabulek) do databáze. To znamená, že jednotlivé soubory v základním systému souborů lze jednoduše smazat, místo aby bylo nutné mazat jednotlivé řádky, což vyžaduje vymazání / zneplatnění částí základního souboru. Takový přístup se proto vyhýbá fragmentaci v podkladových databázových souborech, což zase zabraňuje potřebě vysávání. A toto vysávání může být ve velmi velkých stolech neúměrně drahé.

Tento přístup přesto zajišťuje, že kusy jsou přiměřeně dimenzovány tak, aby bylo možné udržovat nejnovější v paměti, i když se objemy dat mohou měnit.

Rozdělení oddílů primárním klíčem pak vezme každý časový interval a dále jej rozdělí na několik menších bloků, které všechny sdílejí stejný časový interval, ale jsou nespojité, pokud jde o jejich primární prostor klíčů. To umožňuje lepší paralelizaci jak na serverech s více disky - pro vložky i dotazy -, jakož i na více serverech. Další informace o těchto otázkách najdete v pozdějším příspěvku.

Pokud se objem dat za čas zvětší, pak se interval bloku zmenší, aby se zachovaly bloky správného formátu.
Pokud data dorazí brzy, pak jsou data uložena do „budoucího“ kusu, aby se zachovaly kusy správné velikosti.

Výsledek: 15x zlepšení rychlosti vložení

Udržování správných rozměrů je způsob, jakým dosahujeme našich INSERT výsledků, které předčí vanilkovou PostgreSQL, kterou Ajay již ukázal ve svém dřívějším příspěvku.

Vložte propustnost TimescaleDB vs. PostgreSQL s použitím stejného pracovního vytížení, jaké bylo popsáno výše. Na rozdíl od vanilla PostgreSQL, TimescaleDB udržuje konstantní rychlost vložení (asi 14,4 kB insertů za sekundu, nebo 144 000 metrik za sekundu, s velmi malou odchylkou), nezávisle na velikosti datové sady.

Tato konzistentní propustnost vložky také přetrvává při zápisu velkých šarží řádků v jednotlivých operacích na TimescaleDB (namísto řádek po řádku). Takové šaržové vložky jsou běžnou praxí pro databáze používané ve výrobních prostředích ve velkém měřítku, např. Při přijímání dat z distribuované fronty, jako je Kafka. V takových scénářích může jediný server Timescale přijímat 130 000 řádků (nebo 1,3 M metriky) za sekundu, což je přibližně 15násobek oproti vanilla PostgreSQL, jakmile tabulka dosáhne několika 100 řádků.

Vložte výkon TimescaleDB vs. PostgreSQL při provádění VLOŽENÍ 10 000 řádkových dávek.

souhrn

Relační databáze může být docela výkonná pro data časových řad. Přesto náklady na výměnu / odkládání paměti výrazně ovlivňují jejich výkon. Přístupy NoSQL, které implementují protokolované strukturované slučovací stromy, však problém pouze posunuly, zavádějí vyšší požadavky na paměť a špatnou podporu sekundárních indexů.

Poznáním, že data v časových řadách jsou různá, jsme schopni data uspořádat novým způsobem: adaptivním časovým a prostorovým chunkingem. To minimalizuje swapování na disk udržováním pracovních datových sad dostatečně malých, aby se vešly do paměti, a zároveň nám umožňuje udržovat robustní podporu primárních a sekundárních indexů (a plnou sadu funkcí PostgreSQL). Díky tomu jsme schopni výrazně rozšířit PostgreSQL, což vede k 15násobnému zlepšení míry vložení.

Ale co srovnání výkonu s databázemi NoSQL? Tento příspěvek se blíží.

Mezitím si můžete na GitHubu stáhnout nejnovější verzi TimescaleDB, vydanou na základě povolení Apache 2.

Líbí se vám tento příspěvek? Máte zájem dozvědět se více?

Sledujte nás zde na médiu, podívejte se na náš GitHub, připojte se k naší komunitě Slack a zaregistrujte se do níže uvedeného seznamu e-mailů. Hledáme také!