TSDB时序数据库--存储架构

文章目录

  • 文件存储方式
    • 行存储
      • 概述
      • 应用场景
    • 列存储
      • 概述
      • 应用场景
    • 行存储与列存储的对比
      • 在数据写入上的对比
      • 在数据读取上的对比
    • 行存储与列存储的特性
      • 行式数据库的特性
      • 列式数据库的特性
  • 数据类型
  • 存储模型LSMT(Log-Structured Merge Tree)
    • 概念
    • 组成结构
      • WAL(Write-Ahead Log 预写日志)
      • MemTable
      • SSTable
    • 写数据
      • 过程
      • 优化方式
        • Compact
    • 读数据
      • 过程
      • 优化方式
        • 布隆过滤器
        • 稀疏索引(sparse index)
        • 全量索引(dense index)

文件存储方式

行存储

概述

行存储将每个数据点作为一行存储在数据库中,每行包含多个字段,例如时间戳、测量值等。行存储适合按照时间顺序查询整行数据。

应用场景

  • 基于一列或比较少的列计算的时候;
  • 经常关注一张表某几列而非整表数据的时候;
  • 数据表拥有非常多的列的时候;
  • 数据表有非常多行数据并且需要聚集运算的时候;
  • 数据表列里有非常多的重复数据,有利于高度压缩;

列存储

概述

列存储将数据按列存储在数据库中,每列表示一个指标的时间序列数据,包含多个时间戳和相应的值。列存储适合进行聚合、过滤和统计操作。

应用场景

  • 关注整张表内容,或者需要经常更新数据;
  • 需要经常读取整行数据;
  • 不需要聚集运算,或者快速查询需求;
  • 数据表本身数据行并不多;
  • 数据表的列本身有太多唯一性的数据;

行存储与列存储的对比

在数据写入上的对比

1)行存储的写入是一次完成。如果这种写入建立在操作系统的文件系统上,可以保证写入过程的成功或者失败,数据的完整性因此可以确定。
2)列存储由于需要把一行记录拆分成单列保存,写入次数明显比行存储多(意味着磁头调度次数多,而磁头调度是需要时间的,一般在1ms~10ms),再加上磁头需要在盘片上移动和定位花费的时间,实际时间消耗会更大。所以,行存储在写入上占有很大的优势。
3)还有数据修改,这实际也是一次写入过程。不同的是,数据修改是对磁盘上的记录做删除标记。行存储是在指定位置写入一次,列存储是将磁盘定位到多个列上分别写入,这个过程仍是行存储的列数倍。所以,数据修改也是以行存储占优。

在数据读取上的对比

1)数据读取时,行存储通常将一行数据完全读出,如果只需要其中几列数据的情况,就会存在冗余列,出于缩短处理时间的考量,消除冗余列的过程通常是在内存中进行的。
2)列存储每次读取的数据是集合的一段或者全部,不存在冗余性问题。
3) 两种存储的数据分布。由于列存储的每一列数据类型是同质的,不存在二义性问题。比如说某列数据类型为整型(int),那么它的数据集合一定是整型数据。这种情况使数据解析变得十分容易。相比之下,行存储则要复杂得多,因为在一行记录中保存了多种类型的数据,数据解析需要在多种数据类型之间频繁转换,这个操作很消耗CPU,增加了解析的时间。所以,列存储的解析过程更有利于分析大数据。
4)由于列存储中相同类型的数据在一列中连续存储,利于数据压缩算法的应用,因此列存储通常具有更高的压缩率。这可以节省存储空间,并且减少了磁盘读取的数据量,提高了查询性能。

行存储与列存储的特性

行式数据库的特性

① 数据是按行存储的。
② 没有索引的查询使用大量I/O。比如一般的数据库表都会建立索引,通过索引加快查询效率。
③ 建立索引和物化视图需要花费大量的时间和资源。
④ 面对查询需求,数据库必须被大量膨胀才能满足需求。

列式数据库的特性

① 数据按列存储,即每一列单独存放。
② 数据即索引。
③ 只访问查询涉及的列,可以大量降低系统I/O。
④ 每一列由一个线程来处理,即查询的并发处理性能高。
⑤ 数据类型一致,数据特征相似,可以高效压缩。比如有增量压缩、前缀压缩算法都是基于列存储的类型定制的,所以可以大幅度提高压缩比,有利于存储和网络输出数据带宽的消耗。

列存储在更新频繁的场景下不太适用的原因如下:

  1. 写入开销:列存储将数据按列存储,每列都是单独存储的。当进行频繁的更新操作时,需要修改多个列的数据,这会导致大量的写入操作。由于每次更新都需要对多个列进行写入,写入开销较高,影响性能。
  2. 数据压缩:列存储通常使用列式压缩算法,例如基于字典的压缩、位图压缩等,以减小存储空间和提高查询效率。然而,在频繁更新的场景下,数据的压缩和解压缩操作会频繁发生,增加了系统的开销。
  3. 数据重建:由于列存储的特性,数据的更新可能需要进行数据重建或重排,以确保数据的有序性和压缩效果。频繁的数据更新会导致大量的数据重建操作,增加了系统的负担和复杂性。
  4. 事务支持限制:某些列存储系统在事务支持方面可能受限。由于频繁的更新操作可能涉及多个列,而列存储系统可能不支持跨列的原子性更新,这可能导致数据的一致性问题。

数据类型

时序数据库中可以存储多种类型的数据,包括:
1. 数值型数据:例如温度、湿度、压力等传感器数据,股票价格等。
2. 布尔型数据:例如开关状态、报警状态等。
3. 字符串型数据:例如日志数据、设备状态等。
4. 地理位置数据:例如 GPS 数据、地震数据等。
5. 图像、音频、视频等多媒体数据:例如监控视频、音频信号等。
时序数据库可以根据不同的数据类型进行优化,例如对于数值型数据可以使用压缩算法来减少存储空间,对于地理位置数据可以使用地理索引和空间索引来加速查询等。

存储模型LSMT(Log-Structured Merge Tree)

概念

日志合并树是一个分层、有序、块存储的数据存储结构。LSM Tree的数据分由内存(Memory)+磁盘(Disk)存储,内存由一个MemTable(内存表)和一个或多个Immutable MemTable(不可变内存表)组成,磁盘由多个级别Level的SSTable组成。

组成结构

WAL(Write-Ahead Log 预写日志)

在时序数据库中,WAL(Write-Ahead Log,预写日志)是一种常见的持久化机制。它用于确保数据的持久性和一致性,以应对系统故障或崩溃的情况。
WAL的基本原理是在将数据写入数据库之前,先将数据操作记录写入一个日志文件中。具体过程如下:

  1. 写入WAL:当有写入操作到达时,时序数据库首先将这个写入操作记录到WAL中,包括要写入的数据以及相应的元数据(如时间戳、数据标识等)。这个操作通常是追加方式,即将日志记录追加到WAL的末尾。
  2. 写入内存:同时,数据库将数据写入内存中的数据结构,如LSM树(Log-Structured Merge Tree)或其他索引结构,以支持快速的读取和查询操作。
  3. 提交操作:一旦数据成功写入内存,数据库会将操作标记为已提交。这意味着数据已经持久化到磁盘,并且可以在系统恢复或重启后重新加载。
  4. 刷写WAL:定期或在特定条件下,时序数据库将WAL中的日志批量刷写到磁盘上的数据文件中。这样可以确保数据的持久性,即使在系统崩溃后,通过回放WAL中的日志,可以恢复数据的一致性。

WAL的优势在于它提供了强大的数据恢复能力。通过使用WAL,时序数据库可以在故障发生时快速地从日志中重建数据,从而减少数据丢失的风险。

MemTable

MemTable 通常是一个有序的数据结构,LevelDB 中主要使用跳表(Skiplist)或红黑树。这两种数据结构都能够在 O(log n) 的时间复杂度内实现插入、查找和删除操作,确保了写入和查询的高效性。当有新的数据需要写入 LevelDB 时,首先将数据写入 MemTable。由于 MemTable 存储在内存中,写入速度相对较快。这允许 LevelDB 实现高吞吐的写操作。

SSTable

SSTable(Sorted String Table)是 LevelDB 中用于持久化存储有序键值对的一种数据文件格式。
SSTable由一组数据block和一组元数据block组成,元数据block存储了SSTable数据block的描述信息,如索引、BloomFilter(布隆过滤器)、压缩、统计等信息,索引采用二分数组结构。

为了加速查询操作,SSTable 可能会包含一个 Bloom Filter。Bloom Filter 是一个快速且占用空间较小的数据结构,用于判断一个键是否可能存在于 SSTable 中。这有助于减少不必要的磁盘 I/O 操作。

SSTable 通常会将数据划分为若干个块(block),每个块独立压缩和编码。这种块编码有助于减小磁盘 I/O 操作,因为对数据的读取通常是按块进行的。
一旦 SSTable 文件创建完成,它就是不可变的,即文件内容不会被修改。这种特性有助于简化并发操作和提高读取性能。任何对数据的修改都会生成一个新的 SSTable 文件。

写数据

过程

  1. 写数据时,先将数据缓存到内存中的一个有序树结构中(成为Memtable)(排序方式通常是按照时间戳的顺序进行排序,可以通过跳表(Skip List)或红黑树(Red-Black Tree)等数据结构实现排序)。

数据写入Memtable前会以WAL(write-ahead-log)方式先写log到disk,以使Memtable中数据在机器故障后可根据log恢复。

写采用【先写内存+日志 攒一定量再 批量顺序append磁盘】故写性能非常高,而为提高查效率所做的改进:数据存储上按key排序组织使得可二分查找(单数据查询)或范围查询,且不用移动数据。
但客户端发来的数据并不会按key有序故若数据存储上按key排则持久化时可能需要移动数据从而影响写入性能,怎么办?但进一步做了【在内存对数据按key排序、数据批量append到文件、多层次文件组织、延迟更新】等处理。

  1. 当Memtable里数据达到一定量(个数、大小等)后数据将被批量地,以append形式顺序写入磁盘成为一个文件,此时文件内部是有序的了(Sorted String Table,即SSTable)。

所有写入Memtable或disk的数据均不会被修改:新的add、delete、update操作在内部实现上都是产生新的entry,而不会去修改同key的entry,因此存在冗余:同一个key有多个版本的数据、不同文件间有相同key的entry。

  1. 系统内部会周期性对磁盘上若干文件进行合并,合并时会删除任何重复的更新或删除,由于各文件内部是有序的故合并非常快。(Level Based Compaction)数据库中逻辑上以level对文件分类,下层level的文件由上层level的文件合并而成,level0中不同文件中可能有相同key,但level1及以下level不再会有冗余。

Level Based Compaction基于将数据按照不同的级别(Level)进行组织和管理。每个级别都包含一个或多个数据文件,其中数据文件的大小和数量在不同的级别之间逐渐增加。一般来说,级别越高,数据文件越大,而级别越低,数据文件越小。

优化方式

Compact

周期性地,级别化合并会触发 Compaction 过程,即将不同级别之间的数据文件进行合并和压缩,以减少存储空间的占用和提高查询性能。在 Compaction 过程中,过时或重复的数据将被删除,数据文件将被重新组织,以提供更高效的数据访问

LSM树由多个层级组成,每个层级都包含一个或多个数据文件。在典型的LSM树实现中,包括以下几个主要层级:

  1. MemTable(内存表):MemTable是位于内存中的数据结构,用于缓存最新的写入操作。它是一种有序的键值对存储结构,支持高效的写入操作和快速的读取操作。
  2. 增量层(Level 0):增量层是LSM树中最底层的层级,通常由多个数据文件组成。这些数据文件用于存储刚刚从MemTable中刷写到磁盘的数据。增量层中的数据文件一般较小,并且按照时间戳顺序排序。
  3. 合并层(Level 1及以上):合并层是LSM树中的上层级,包含已经经过合并和压缩的数据文件。在合并过程中,从增量层的数据文件中选取一部分进行合并,生成一个新的更大的数据文件,并按照特定的规则进行排序和压缩。合并层中的数据文件越往上层级,文件的大小和数量越大。
  4. 查询层(Level N):查询层是LSM树的顶层级,其中包含最早写入的数据文件。这些文件通常具有较大的范围和较高的压缩比率,用于支持范围查询和扫描操作。

通过这种层级化的存储结构,LSM树实现了高写入性能和较高的读取性能。写入操作可以快速地写入到内存中的MemTable,而读取操作可以在不同层级的数据文件中进行快速的查找和查询。定期执行合并和压缩操作可以优化数据的存储和读取效率,并控制存储空间的使用。

读数据

过程

  1. 读取数据时,会先在内存里进行查找,如果内存都有就直接返回;
  2. 如果没有查到就会依次下沉,直到所有Level层都查询一边得到最终结果;
  3. 如果SSTable的分层较多,会导致需要把所有都扫描一遍,为了提高效率,可采用布隆过滤器、稀疏索引、全量索引,还有就是对每个组的数据进行压缩、定期合并缩身。

read操作:先从Memtable中查,若未查到则对各disk文件按文件创建时间由新到旧对各文件遍历查。由于文件内部是按key有序的,故相比于纯日志型存储,LSM Tree查询更快(如可用二分查找);由于需要遍历各文件故查询速度相比于B+树差,且文件越多查的性能越慢;

优化方式

布隆过滤器

布隆过滤器内部依赖哈希算法,当检测到莫一条数据是否见过时,有一定概率出现假阳性(False Positive),但一定不会出现假阴性(False Negative)。简而言之,布隆过滤器认为一条数据出现过,那么该条数据很可能出现过;但如果布隆过滤器认为一条数据没有出现过,那么该条数据一定没有出现过。这种特性刚好与此处的需求相契合,即检验某条数据是否缺失。

稀疏索引(sparse index)

稀疏索引是指将有序数据切分成(固定大小的)块,仅对各个块开头的一条数据做索引。

全量索引(dense index)

全量索引对全部数据进行编制索引,其中的任意一条数据发生增删均需要更新索引。
全量索引查询效率更高,达到了理论极限O(log2n),但写入和删除效率低,每次数据增删均需要更新索引而消耗一次IO操作。通常关系型数据库,如MySQL等,其内部采用B tree作为索引结构,即全量索引。

为提高查询效率,措施:增加合并操作频率以减少文件数量、生成大的排序的文件;对每个文件使用布隆过滤器以可快速确定此文件是否有指定key的entry;Level Based Compaction使得可以记录每个level中key的范围(mainfest文件)从而查询时每个level只要查一个文件。

其他补充可参考我之前写过的一篇总结 Java物联网技术知识点总结#时序数据库(Time Series Database,TSDB)

你可能感兴趣的:(时序数据库,架构,数据库)