如今,时间序列数据应用程序(例如,数据中心/服务器/微服务/容器监视,传感器/ IoT分析,财务数据分析等)正在激增。
结果,时间序列数据库很流行(这里有33个)。其中大多数放弃了传统关系数据库的陷阱,并采用了通常被称为NoSQL模型的模型。使用模式相似:最近的一项调查显示,对于时序数据而言,开发人员更喜欢NoSQL而不是关系数据库,比例超过2:1。
关系数据库包括: MySQL,MariaDB服务器,PostgreSQL。NoSQL数据库包括: Elastic,InfluxDB,MongoDB,Cassandra,Couchbase,Graphite,Prometheus,ClickHouse,OpenTSDB,DalmatinerDB,KairosDB和RiakTS。资料来源: Percona。
通常,采用NoSQL时间序列数据库的原因是按比例缩小的。关系数据库具有大多数NoSQL数据库所没有的许多有用功能(强大的二级索引支持;复杂的谓词;丰富的查询语言; JOIN等),但是它们很难扩展。
而且由于时间序列数据堆积非常迅速,因此许多开发人员认为关系数据库不适合它。
我们采取不同的,有点异端的立场:关系数据库对于时间序列数据可能非常强大。一个人只需要解决缩放问题。那就是我们在TimescaleDB中所做的。
当我们两个星期前宣布TimescaleDB,我们收到了很多积极的反馈来自社区。但是我们也从怀疑者那里听到消息,他们发现很难相信人们应该(或者可以)在关系数据库(在我们的例子中是PostgreSQL)上建立一个可伸缩的时间序列数据库。
有两种不同的考虑扩展的方法:扩展以使一台机器可以存储更多数据,而扩展可以使数据可以存储在多台机器上。
为什么两者都很重要?在N个服务器的群集中进行横向扩展的最常见方法是将数据集分区或分片为N个分区。如果每个服务器的吞吐量或性能受到限制(即无法扩展),则群集的总体吞吐量将大大降低。
这篇文章讨论扩大规模。(向外发布帖子将在以后发布。)
特别是,这篇文章解释了:
- 为什么关系数据库通常无法很好地扩展
- LSM树(通常在NoSQL数据库中使用)如何无法充分解决许多时间序列应用程序的需求
- 时间序列数据如何独特,如何利用这些差异克服扩展问题以及一些性能结果
我们的动机是双重的:对于面临类似问题的任何人,分享我们所学到的知识;而对于那些使用TimescaleDB时间序列数据考虑(包括怀疑!),说明我们的一些设计决策。
为什么数据库通常无法很好地扩展:交换内存非常昂贵
在一台计算机上扩展数据库性能的一个常见问题是内存和磁盘之间的成本/性能之间的重大权衡。尽管内存比磁盘快,但它的价格要昂贵得多:比固态存储(如Flash)贵20倍,比硬盘贵100倍。最终,我们的整个数据集将无法容纳在内存中,这就是为什么我们需要将数据和索引写入磁盘的原因。
对于关系数据库,这是一个古老的,常见的问题。在大多数关系数据库下,表存储为固定大小的数据页面(例如PostgreSQL中的8KB页面)的集合,系统在其上构建数据结构(例如B树)以对数据建立索引。使用索引,查询可以快速找到具有指定ID(例如银行帐号)的行,而无需扫描整个表或以某种排序的顺序“遍历”表。
现在,如果数据和索引的工作集很小,我们可以将其保存在内存中。
但是,如果数据足够大以至于我们无法容纳内存中B树的所有(类似固定大小的)页面,那么在我们从磁盘读取页面时,更新树的随机部分可能会涉及大量的磁盘I / O。进入内存,在内存中进行修改,然后写回到磁盘(在驱逐出空间以容纳其他B树页面时)。并且像PostgreSQL这样的关系数据库为每个表索引保留一个B树(或其他数据结构),以便有效地找到该索引中的值。因此,随着您索引更多列,问题变得更加复杂。
实际上,由于数据库仅在页面大小的边界内访问磁盘,因此即使看似很小的更新也可能导致发生这些交换:要更改一个单元,数据库可能需要交换出现有的8KB页面并将其写回到磁盘,然后在修改之前阅读新页面。
但是,为什么不使用较小或可变大小的页面呢?有两个很好的理由:最大程度地减少磁盘碎片,以及(对于旋转的硬盘而言)最大程度地减少物理上将磁头移动到新位置所需的“搜索时间”(通常为5-10ms)的开销。
固态驱动器(SSD)呢?尽管诸如NAND闪存驱动器之类的解决方案消除了任何物理“寻找”时间,但它们只能以页面级粒度(今天通常为8KB)读取或写入。因此,即使要更新单个字节,SSD固件也需要从磁盘读取8KB页面到其缓冲区缓存,修改页面,然后将更新后的8KB页面写回到新的磁盘块。
从PostgreSQL的性能图中可以看出换入和换出内存的成本,其中插入吞吐量随表大小而下降,并且方差增大(取决于请求是在内存中命中还是需要从磁盘中获取(可能是多个))。
插入吞吐量作为PostgreSQL 9.6.2表大小的函数,在具有基于SSD的(高级LRS)存储的Azure标准DS4 v2(8核)计算机上以10个工作程序运行。客户将单独的行插入数据库中(每行有12列:时间戳,索引的随机选择的主ID和10个其他数字指标)。PostgreSQL的速率开始于每秒15K次插入,但随后在50M行之后开始显着下降,并开始经历非常高的变化(包括每秒100次插入的周期)。
使用日志结构的合并树输入NoSQL数据库(以及新问题)
大约十年前,我们开始看到许多“ NoSQL”存储系统通过日志结构合并(LSM)树解决了此问题,这些树通过仅对磁盘执行较大的仅追加写入操作而减少了进行较小写入操作的成本。
LSM树不执行“就地”写入(对现有页面进行小的更改需要从磁盘读/写整个页面),而是将几个新更新(包括删除!)排队到页面中,并将它们写为页面。单批到磁盘。特别是,将对LSM树中的所有写入都执行到内存中维护的排序表,然后在大小足够时将其作为不可变批处理刷新到磁盘(作为“排序字符串表”或SSTable)。这减少了进行小写操作的成本。
在LSM树中,所有更新首先被写入内存中的排序表,然后作为不可变批处理刷新到磁盘,并存储为SSTable,该表通常在内存中建立索引。资料来源:igvita.com
起初,这种体系结构已被LevelDB,Google BigTable,Cassandra,MongoDB(WiredTiger)和InfluxDB等许多“ NoSQL”数据库采用 。然而,这带来了其他折衷:更高的内存需求和较差的二级索引支持。
更高的内存要求:与B树不同,在LSM树中没有单个排序:没有全局索引可以为我们提供所有键的排序顺序。因此,查找密钥的值变得更加复杂:首先,检查内存表中密钥的最新版本;否则,请查看(可能有很多)磁盘上的表以查找与该密钥关联的最新值。为了避免过多的磁盘I / O(如果值本身很大,例如存储在Google BigTable中的网页内容),则所有SSTable的索引都可以完全保留在内存中,从而增加了内存需求。
二级索引支持差:由于它们缺乏全局排序顺序,因此LSM树自然不支持二级索引。各种系统都增加了一些附加支持,例如通过以不同顺序复制数据。或者,他们通过将主键构建为多个值的串联来模拟对更丰富谓词的支持。然而,这种方法的代价是需要在查询时在这些键之间进行较大的扫描,从而仅支持基数受限的项(例如,离散值,而不是数字项)。
有一个更好的方法来解决此问题。让我们从更好地了解时间序列数据开始。
时间序列数据不同
让我们退后一步,看看关系数据库旨在解决的原始问题。从1970年代中期IBM开创性的System R开始,关系数据库被用于所谓的在线事务处理(OLTP)。
在OLTP下,操作通常是对数据库中各行的事务更新。例如,想想银行转帐:用户从一个帐户借钱,而在另一个帐户贷记。这对应于对数据库表的两行(甚至只有两个单元格)的更新。因为任何两个帐户之间都可能发生银行转帐,所以修改后的两行在表上有些随机分布。
时间序列数据来自许多不同的设置:工业机器;运输和物流;DevOps,数据中心和服务器监控; 和财务应用程序。
现在,让我们考虑一些时间序列工作负载的示例:
- DevOps /服务器/容器监视。系统通常收集有关不同服务器或容器的度量:CPU使用率,可用/已用内存,网络发送/接收,磁盘IOPS等。每组度量均与时间戳,唯一服务器名称/ ID和一组标记相关联描述所收集内容的属性。
- 物联网传感器数据。每个物联网设备可以在每个时间段报告多个传感器读数。例如,对于环境和空气质量监测,它可能包括:温度,湿度,大气压力,声音水平,二氧化氮,一氧化碳,颗粒物等的测量。每组读数都与时间戳和唯一的设备ID相关联,并且可能包含其他元数据。
- 财务数据。金融报价数据可以包括带有时间戳的流,证券的名称及其当前价格和/或价格变化。另一种类型的财务数据是支付交易,其中将包括唯一的帐户ID,时间戳,交易金额以及任何其他元数据。(请注意,此数据与上面的OLTP示例不同:此处记录的是每笔交易,而OLTP系统仅反映了系统的当前状态。)
- 车队/资产管理。数据可以包括车辆/资产ID,时间戳,该时间戳处的GPS坐标以及任何元数据。
在所有这些示例中,数据集都是一连串的测量,涉及将“新数据”插入数据库中,通常是插入最新时间间隔。尽管由于网络/系统延迟或由于对现有数据的更正而导致数据到达的时间可能比生成/标记的时间晚得多,但这通常是例外,而不是规范。
换句话说,这两个工作负载具有非常不同的特征:
OLTP写入
- 主要是更新
- 随机分布(在主键集上)
- 通常跨多个主键进行交易
时间序列写
- 主要是插入
- 主要到最近的时间间隔
- 主要与时间戳和单独的主键(例如,服务器ID,设备ID,安全性/帐户ID,车辆/资产ID等)相关联
TimescaleDB将每个块存储在内部数据库表中,因此索引仅随每个块的大小而增长,而不随整个超表的大小而增长。由于插入大部分是在最近的间隔中进行,因此该插入仍保留在内存中,从而避免了昂贵的磁盘交换。
为什么这么重要?正如我们将看到的,可以利用这些特性来解决关系数据库上的扩展问题。
一种新方法:自适应时间/空间分块
当以前的方法试图避免对磁盘的小写操作时,他们试图解决更广泛的OLTP问题,即对随机位置的UPDATE。但是,正如我们刚刚确定的那样,时间序列的工作量是不同的:写入主要是在最近的时间间隔(不是随机位置)中进行INSERTS(不是UPDATES)。换句话说,时序工作负载仅附加。
这很有趣:这意味着,如果按时间对数据进行排序,我们将始终朝着数据集的“末端”进行写入。按时间组织数据还可以使我们将数据库页面的实际工作集保持很小,并将其维护在内存中。而且,我们花了较少时间讨论的读取操作也可以受益:如果许多读取查询到最近的时间间隔(例如,对于实时仪表板),则该数据将已经缓存在内存中。
乍一看,按时编制索引似乎可以为我们免费提供高效的读写功能。但是一旦我们想要任何其他索引(例如,另一个主键,例如服务器/设备ID或任何辅助索引),那么这种幼稚的方法将使我们回到为该索引随机插入B树中的方式。
还有另一种方式,我们称为“自适应时间/空间分块”。这就是我们在TimescaleDB中使用的。
TimescaleDB不仅根据时间建立索引,还通过根据两个维度(时间间隔和主键(例如,服务器/设备/资产ID))拆分数据来构建不同的表。我们将它们称为大块,以区分它们与分区,分区通常是通过分割主键空间来定义的。因为这些块中的每个块都存储为数据库表本身,并且查询计划者知道该块的范围(在时间和键空间上),所以查询计划者可以立即知道操作数据属于哪个块。(这既适用于插入行,也适用于修剪执行查询时需要触摸的一组块。)
这种方法的主要好处是,现在我们所有的索引都只建立在这些较小的块(表)上,而不是一个代表整个数据集的表。因此,如果我们适当地调整这些块的大小,则可以将最新的表(及其B树)完全放入内存中,并避免此磁盘交换问题,同时保持对多个索引的支持。
实现分块的方法
设计此时间/空间分块的两种直观方法各自都有明显的局限性:
方法1:固定持续时间间隔
在这种方法下,所有块都可以具有固定的相同时间间隔,例如1天。如果每个时间间隔收集的数据量保持不变,则此方法效果很好。但是,随着服务的普及,它们的基础结构也会相应扩展,从而导致更多的服务器和更多的监视数据。同样,成功的物联网产品将部署越来越多的设备。一旦我们开始向每个块写入太多数据,我们就会定期交换到磁盘上(并发现自己回到了第一个平方)。另一方面,选择过小的间隔开始会导致其他性能下降,例如,必须在查询时触摸很多表。
每个块都有固定的时间长度。但是,如果每次数据量增加,则最终块大小将变得太大而无法容纳在内存中。
方法2:固定大小的块
使用这种方法,所有块都具有固定的目标大小,例如1GB。块被写入直到达到最大大小为止,此时该块“关闭”并且其时间间隔约束变得固定。但是,落入块的“关闭”间隔内的以后数据仍将被写入块,以保持块的时间约束的正确性。
一个关键的挑战是块的时间间隔取决于数据的顺序。考虑数据(甚至单个数据点)是否提前几个小时甚至几天到达,这可能是由于时钟不同步,还是由于具有间歇性连接的系统中的延迟有所变化。早期的数据点将延长“开放”数据块的时间间隔,而随后的按时数据可以将数据块驱动到其目标大小以上。这种方法的插入逻辑也更加复杂和昂贵,这降低了大批量写入(例如大型COPY操作)的吞吐量,因为数据库需要确保按时间顺序插入数据以确定何时应创建新块(即使在中间操作)。固定大小或最大大小的块还存在其他问题,包括可能与数据保留策略不一致的时间间隔(“ 30天后删除数据”)。
每个块的时间间隔只有在达到其最大大小后才固定。但是,如果数据提早到达,这将为块创建较大的间隔,并且块最终变得太大而无法容纳在内存中。
TimescaleDB采用第三种方法,结合了这两种方法的优势。
方法3:自适应间隔(我们当前的设计)
块以固定间隔创建,但是该间隔会根据数据量的变化从块到块进行调整,以达到最大目标大小。
通过避免开放式间隔,这种方法可确保提早到达的数据不会产生太长的时间间隔,而这会导致随后的块过大。此外,像静态间隔一样,它更自然地支持按时指定的保留策略,例如“ 30天后删除数据”。给定TimescaleDB基于时间的分块,可以通过简单地在数据库中删除块(表)来实现此类策略。这意味着可以简单地删除基础文件系统中的单个文件,而不需要删除个别行,这需要擦除/无效基础文件的某些部分。因此,这种方法避免了底层数据库文件中的碎片,从而避免了清理工作的需要。。而且,在非常大的桌子上,这种吸尘可能会非常昂贵。
尽管如此,这种方法仍可确保适当地调整块的大小,以便即使数据量可能发生变化,最新的块也可以保留在内存中。
然后,按主键进行分区将占用每个时间间隔,并将其进一步拆分为多个较小的块,它们均共享相同的时间间隔,但就其主键空间而言却是不相交的。这可以在具有多个磁盘的服务器(用于插入和查询)以及多个服务器上实现更好的并行化。有关这些问题的更多信息,请参见后续文章。
如果单位时间的数据量增加,则块间隔将减小,以保持正确大小的块。 如果数据提早到达,则将数据存储到“未来”块中以维护正确大小的块。
结果:插入率提高15倍
保持块大小正确是我们实现INSERT结果的方法,该结果超过了普通PostgreSQL,这是Ajay在早先的文章中已经展示的。
使用前面所述的相同工作量,插入TimescaleDB与PostgreSQL的吞吐量。与原始PostgreSQL不同,TimescaleDB保持恒定的插入率(每秒约14.4K插入,或每秒144K度量,变化非常小),与数据集大小无关。
当在单个操作中将大批行写入TimescaleDB(而不是逐行)时,这种一致的插入吞吐量也仍然存在。对于更大规模生产环境中使用的数据库,例如从诸如Kafka的分布式队列中提取数据时,这种批量插入是常见的做法。在这种情况下,一台Timescale服务器每秒可以接收130K行(或130万个指标),大约是表格达到100M行时的15倍。
在执行10,000行批处理的INSERT时,插入TimescaleDB与PostgreSQL的吞吐量。
摘要
关系数据库对于时间序列数据可能非常强大。但是,换入/换出内存的成本会显着影响其性能。但是实现日志结构化合并树的NoSQL方法仅解决了问题,引入了更高的内存需求和较差的二级索引支持。
通过认识到时序数据是不同的,我们能够以一种新的方式来组织数据:自适应时间/空间分块。通过使工作数据集足够小以适合内存,从而最大程度地减少了到磁盘的交换,同时使我们能够保持强大的主索引和辅助索引支持(以及PostgreSQL的全部功能集)。结果,我们能够大幅扩展 PostgreSQL,从而使插入率提高15倍。
但是与NoSQL数据库的性能比较又如何呢?该职位即将发布。
同时,您可以在GitHub上下载最新版本的TimescaleDB,该版本已根据许可的Apache 2许可发布。
本文地址:https://blog.csdn.net/allway2/article/details/107898094