开源时间序列数据库分析 – 第三部分

在这篇文章中,我们分析了受欢迎的开源时序数据库引擎的时序数据存储和计算能力。

请只提供一个选项:这本书是由周肇峰所写的。

InfluxDB: 时序数据库

InfluxDB在DB-Engines的时序数据库中排名第一,这是非常自然的事情。从功能丰富性,易用性和底层实现的角度来看,它有许多亮点,值得进行详细分析。

首先,我将简要总结一些重要的特征。

    • シンプルなアーキテクチャ:スタンドアロンのInfluxDBの場合、バイナリだけをインストールする必要があり、外部からの依存関係なしに使用できます。ここでは、いくつかのネガティブな例を紹介します。OpenTSDBの最下層はHBaseなので、ZooKeeperやHDFSなどを併用する必要があります。Hadoopの技術スタックに慣れていないと、一般的に運用や保守が難しいのも不満の一つです。KairosDBの方が若干良いです。CassandraやZooKeeperに依存しており、スタンドアローンでのテストにはH2を使うことができます。一般的に、外部の分散データベースに依存するTSDBは、完全に自己完結したTSDBよりもアーキテクチャがやや複雑です。結局のところ、成熟した分散データベースはそれ自体が非常に複雑ではありますが、それはクラウドコンピューティングの時代になって完全に解消されました。

 

    • TSMエンジン:自社開発のTSMストレージエンジンを最下層に採用しています。TSMもLSMの考え方に基づいており、非常に強力な書き込み能力と高い圧縮率を実現しています。より詳細な分析は、以下のセクションで行います。

 

    • InfluxQL:SQLライクなクエリ言語が提供されており、データベースの利用を大幅に促進します。ユーザビリティにおけるデータベースの進化の最終目標は、クエリ言語の提供です。

 

    • 継続的なクエリ:CQでは、データベースは自動ロールアップと事前集計をサポートすることができます。一般的なクエリ操作については、CQを使用して事前計算による高速化を行うことができます。

 

    • 時系列インデックス:タグは効率的な検索のためにインデックス化されています。OpenTSDBやKairosDBなどと比較して、この機能によりInfluxDBのタグ検索の効率化が図られています。OpenTSDBではタグの検索に関して多くのクエリ最適化が行われています。しかし、HBaseの関数やデータモデルによって制限されており、これらの最適化は機能していません。しかし、現在の安定版の実装では、メモリベースのインデックスを使用しており、実装が比較的簡単で、クエリ効率が最も高いです。しかし、これにも多くの問題があり、以下のセクションで詳しく説明します。

 

    • プラグインのサポート:データベースはカスタムプラグインをサポートしており、 Graphite、collectd、OpenTSDBなどの様々なプロトコルをサポートするように拡張することができます。

 

    以下のセクションでは、主に基本的な概念、TSMストレージエンジン、連続クエリ、TimeSeriesインデックスについて詳細に分析します。

基本概念

首先,让我们学习一下InfluxDB的基本概念。以下是具体的示例。

INSERT machine_metric,cluster=Cluster-A,hostname=host-a cpu=10 1501554197019201823

以下是将数据写入InfluxDB的命令行。该数据的组成要素如下:

    • 測定(Measurement):測定の概念はOpenTSDBのメトリックと似ており、データの監視指標の名前を表しています。例えば、上の例では機械指標の監視なので、そのMeasurementはmachine_metricという名前になっています。

 

    • タグ:OpenTSDBのタグの概念と同様に、タグは対象の異なる次元を記述するために使用されます。1 つ以上のタグが許可されており、各タグはタグキーとタグ値で構成されています。

 

    • フィールド:OpenTSDBの論理データ・モデルでは、メトリック・データの行は1つの値に対応します。InfluxDBでは、測定データの1行が複数の値に対応し、各値はフィールドによって区別されます。

 

    • Timestamp(タイムスタンプ):時系列データの必須属性。データの時間点を表します。InfluxDBの時間粒度はナノ秒単位まで正確にできることがわかります。

 

    • TimeSeries:計測+タグの組み合わせで、InfluxDBではTimeSeriesと呼ばれています。TimeSeriesは時系列のことです。時間に基づいて特定の時間点を位置づけることができるので、TimeSeries + Field + Timestamp を使って特定の値を位置づけることができます。これは重要な概念であり、後のセクションで述べます。

 

    最後に、各測定のデータは、次の図に示すように、論理的に大きなデータテーブルに整理されています。
image.png

当执行查询时,InfluxDB支持基于度量的任意维度查询。您可以指定任意的标签或字段来执行查询。根据上述数据示例,您可以构建以下查询条件。

SELECT * FROM "machine_metric" WHERE time > now() - 1h;  
SELECT * FROM "machine_metric" WHERE "cluster" = "Cluster-A" AND time > now() - 1h;
SELECT * FROM "machine_metric" WHERE "cluster" = "Cluster-A" AND cpu > 5 AND time > now() - 1h;

从数据模型和查询标准来看,标签和字段没有区别。从语义角度来看,标签用于描述测量值,而字段用于描述数值。从内部实现角度来看,标签具有完全索引,而字段则没有索引,因此基于标签的查询标准比基于字段的标准更高效。

TSM (Team SoloMid) 只需要一个选项

InfluxDB的底层存储引擎经历了从LevelDB到BlotDB,再到自家开发的TSM的过程。关于整个选择和转换的考虑可以在官方网站的资料中找到。整个考虑过程是值得学习的。关于技术选择和变革的思考不仅仅是描述产品属性,还会给人带来不断的惊喜。

我简要概述了从选择存储引擎到转换的整个流程。第一阶段选择的是LevelDB。选择LevelDB的主要原因是其底层数据结构采用了LSM,具有高写入性能、高写入吞吐量,并且相对于时序数据属性有较好的匹配性。在LevelDB中,数据被存储为KeyValue,并按照Key进行排序。由于InfluxDB中使用的Key是SeriesKey+Timestamp的组合,通过按照Timestamp对相同SeriesKey的数据进行排序和存储,可以实现非常高效的时间范围扫描。

然而,在使用LevelDB时存在的最大问题是InfluxDB支持历史数据的自动删除(Retention Policy)。对于时间序列数据的场景,自动删除数据通常是指删除连续时间段内的历史数据大块。由于LevelDB不支持Range Delete或TTL,因此只能一次只能使用一个键进行删除,导致删除流量的压力很大。而且,在LSM数据结构中,实际的物理删除不是瞬时的,只有在启用了压缩时才会生效。对于TSDB的数据删除实践,可以大致分为两个类别。

1、数据分片:根据不同的时间范围,数据会被分割到不同的分片中。时间序列数据的写入是随着时间的推移线性生成的,因此生成的分片也会随着时间的推移线性增加。写入通常在最新的分区进行,而不需要对多个分片进行哈希。分片的优点是物理删除保留数据非常简单,只需简单删除整个分片即可。缺点是保留精度相对较大,是整个分片,而保留时间粒度取决于分片的时间跨度。分片可以在应用层或者存储引擎层实现。例如,可以使用RocksDB的列族作为数据分片。InfluxDB采用了这种模型,默认的保留策略下的数据会形成7天的分片。

2、TTL:底层数据引擎直接提供数据自动过期功能。可以为每个数据输入设置有效期限,当达到指定时间后,存储引擎会自动删除物理数据。该方法的优点是保留精度非常高,达到了第二级别和行级别的保留。缺点是由于LSM的实现,在压缩时会发生物理删除,因此时效性较低。RocksDB、HBase、Cassandra、阿里云表格存储都提供了TTL功能。

InfluxDB采用第一政策,将数据分割到多个不同的分片中,每个分片都是独立的数据库实例。随着执行时间的增长,分片数量也会增加。每个分片都是独立的数据库实例,最底层是独立的LevelDB存储引擎,因此可能存在每个存储引擎打开的文件数量过多的问题。随着分片增加,最终处理时打开的文件句柄数量很快就会达到上限。LevelDB在最底层使用了级别压实策略,这是文件数量过多的原因之一。实际上,级别压实策略并不适用于时序数据的写入,在InfluxDB中并未提及这个原因。

由于客户提供了关于过多文件句柄的重要反馈,InfluxDB在选择新的存储引擎时选择了BoltDB,代替了LevelDB。BoltDB的底层数据结构是mmap B+树。选择BoltDB的原因如下:1. 采用与LevelDB相同的语义API;2. 采用了易于集成且跨平台的功能性纯Go实现;3. 由于仅使用一个文件来存储一个数据库,解决了过多文件句柄消耗的问题等。这是选择BoltDB的主要原因。然而,BoltDB的B+树结构在写入能力方面不如LSM,并产生大量的随机写入。因此,在使用BoltDB后,InfluxDB很快面临了IOPS的问题。当数据库大小达到数GB时,经常遇到IOPS瓶颈,对写入能力产生了重大影响。之后,InfluxDB在BoltDB之前添加了WAL层,并首先将数据写入WAL等,采用了一些写入优化策略,但WAL可以确保按顺序将数据写入磁盘。然而,最终对BoltDB的写入仍然消耗大量的IOPS资源。

经过几个BoltDB的小版本后,我们最终在内部开发了适用于InfluxDB的TSM。TSM的设计目标是解决LevelDB过多的文件句柄问题,并解决BoltDB的写入性能问题。TSM代表时间结构化合并树(Time-Structured Merge Tree)。它的概念类似于LSM,但通过基于时序数据属性进行一些特殊优化。TSM的关键组件如下:

1、预写式日志(WAL):数据首先被写入WAL中,然后流向内存索引和缓存。WAL中的数据会同步地刷新到磁盘,以确保数据的持久性。缓存中的数据会异步地刷新到TSM文件中。如果在数据被持久化到TSM文件之前进程崩溃,WAL中的数据将被用于恢复缓存中的数据,这个过程类似于LSM。
2、缓存:TSM的缓存类似于LSM的内存表。内部数据来自WAL,没有被持久化到TSM文件。如果进程发生故障切换,缓存中的数据将根据WAL重新构建。缓存中的数据存储在SortedMap中,其键值由TimeSeries+Timestamp组成。因此,内存中的数据按照TimeSeries进行组织,TimeSeries的数据按照时间顺序存储。

3、TSM文件:TSM文件与LSM的SSTable类似。TSM文件由头部、块、索引、尾部四个部分组成。最重要的部分是块和索引。
1、块:每个块存储一段时间的TimeSeries值,即存储一段时间内某个测量的标签集对应的字段的所有值。为了达到最佳的压缩效率,块内采用不同的压缩策略,根据字段不同值的类型。
2、索引:文件中的索引信息存储了每个TimeSeries下所有数据块的位置信息。索引数据按照TimeSeries键的词法顺序进行排序。非常大的完整索引数据不会被加载到内存中,而是只有一些关键字被索引。此间接索引还包含一些辅助位置信息,如时间的最小值和最大值,文件中键的最小值和最大值等。最重要的是,一些键的文件偏移量信息和它们的索引数据被保存在其中。为了找到TimeSeries的索引数据位置,首先要根据内存中的一些键信息找到最相似的索引偏移量,然后从文件内容的起始位置开始顺序扫描,准确地确定该键的索引数据位置。

4、压缩(Compaction):压缩是将写优化的数据存储格式优化为读优化的数据存储格式的过程。这是LSM结构存储引擎为了存储和查询优化而必不可少的功能。存储引擎的质量取决于压缩策略和算法的质量。在时间序列数据的场景中,更新和删除操作很少进行,数据基本上是按时间顺序生成的,基本上没有重复的情况。压缩主要承担压缩和索引优化的角色。

1、LevelCompaction(层压缩): InfluxDB将TSM文件分为4个层级(层级1至4)。压缩仅在同一层级的文件内进行。在压缩后,同一层级的文件将升级到下一个层级。根据时序数据生成的属性,层级越高,数据生成时间越早,访问热度越低。只有通过缓存内的数据生成的TSM文件才被称为快照。多个快照被压缩后生成层级1的TSM文件,然后层级1的文件被压缩后生成层级2的TSM文件。不同层级文件的压缩使用不同的算法。低层级文件的压缩使用较低的CPU消耗方法,例如不进行块解压缩或块合并。为了进一步提高高层级文件的压缩率,进行块解压缩和块合并。我们理解这样的设计是一个权衡。比较通常在后台运行。为了不影响实时数据写入,压缩消耗的资源严格控制,但资源有限的情况下会影响压缩速度。然而,由于低层级数据较新且热门,需要压缩以加速查询。因此,InfluxDB采用了完全根据时序数据的写入属性和查询属性进行设计的低资源消耗的压缩策略。

2、IndexOptimizationCompaction:当积累了相当数量的Level4文件时,索引会变得非常大,导致查询效率相对较低。查询效率低的主要原因是同一TimeSeries数据存在于多个TSM文件中,不可避免地需要在多个文件之间进行数据合并。因此,IndexOptimizationCompaction主要用于将相同TimeSeries下的数据合并到同一个TSM文件中,以最小化不同TSM文件之间的TimeSeries重复率。

3、FullCompaction:当InfluxDB判断一个分片长时间没有被写入数据时,会执行数据的完全压缩。FullCompaction是LevelCompaction和IndexOptimization的结合。在完全压缩后,除非有新数据写入或发生删除,否则不会对该分片执行进一步的压缩。这种策略是针对冷数据的整理,主要目的是提高压缩率。

连续查询

关于InfluxDB中数据预聚合和精度降低的问题,有两个建议方针。一个是利用InfluxDB的数据计算引擎Kapacitor,另一个是利用InfluxDB附带的连续查询功能。

CREATE CONTINUOUS QUERY "mean_cpu" ON "machine_metric_db"
BEGIN
SELECT mean("cpu") INTO "average_machine_cpu_5m" FROM "machine_metric" GROUP BY time(5m),cluster,hostname
END

下面是一个简单的CQL,用于构建连续查询。这样,InfluxDB就可以启动定时任务,每5分钟按照集群+主机名的维度对“machine_metric”下的所有数据进行聚合,并计算字段“cpu”的平均值,然后将最终结果写入新的测量值“average_machine_cpu_5m”。

InfluxDB 的连续查询类似于 KairosDB 的自动卷起功能。这些都在单个节点上进行调度。数据聚合比实时的 StreamCompute 延迟更高,同时存储也会面临巨大的读取压力。

时间序列指数

InfluxDB不仅支持保存和计算时间序列数据,还能提供多维查询。通过对时间序列进行索引化,InfluxDB实现了更快速的多维查询。关于数据和索引的描述如下所示。

InfluxDB看起来实际上像是两个数据库,一个是用于时序数据存储的数据库,另一个是用于测量、标签、字段元数据的倒排索引。

在InfluxDB 1.3之前,只支持基于内存的TimeSeries索引(以下简称TSI),这意味着所有的TimeSeries索引都存储在内存中,虽然这是有益的,但也存在许多问题。然而,在最新的InfluxDB 1.3中,提供了可选择的另一种索引创建方法。新的索引创建方法将索引存储在磁盘上。

基于内存的索引

    // Measurement represents a collection of time series in a database. It also
    // contains in memory structures for indexing tags. Exported functions are
    // goroutine safe while un-exported functions assume the caller will use the
    // appropriate locks.
    type Measurement struct {
     database string
     Name     string `json:"name,omitempty"`
     name     []byte // cached version as []byte

     mu         sync.RWMutex
     fieldNames map[string]struct{}

     // in-memory index fields
     seriesByID          map[uint64]*Series              // lookup table for series by their id
     seriesByTagKeyValue map[string]map[string]SeriesIDs // map from tag key to value to sorted set of series ids

     // lazyily created sorted series IDs
     sortedSeriesIDs SeriesIDs // sorted list of series IDs in this measurement
    }

    // Series belong to a Measurement and represent unique time series in a database.
    type Series struct {
     mu          sync.RWMutex
     Key         string
     tags        models.Tags
     ID          uint64
     measurement *Measurement
     shardIDs    map[uint64]struct{} // shards that have this series defined
    }

以下是InfluxDB 1.3源代码中基于内存的索引数据结构的定义。它主要由两个关键的数据结构组成。

系列:TimeSeries支持并存储与TimeSeries及其所属的分片相关的一些基本属性。

    • Key: 対応する測定+タグのシリアル化された文字列。

 

    • Tag: Timeseriesの下にあるすべてのタグキーとタグ値。

 

    • ID: 一意の整数 ID。

 

    • measurement: シリーズが属する測定。

 

    shardIDs: シリーズを含むすべてのShardIDのリスト。

每个测量都与内存中的测量结构相对应,并且内部包含一些索引以加快查询速度。

    • seriesByID: SeriesIDを介してSeriesをクエリするマップ。

 

    • seriesByTagKeyValue。2層のマップ。1層目はタグキーに対応するすべてのタグ値、2層目はタグ値に対応するすべてのSeriesのIDです。ご覧のように、TimeSeriesのベースが大きくなると、このマップはかなり多くのメモリを消費します。

 

    sortedSeriesIDs: ソートされたSeriesIDのリスト。

全内存索引结构具有高效的多维查询的优点,但也存在一些问题。

    • TimeSeriesのベースは主にメモリサイズによって制限されています。TimeSeriesの数が上限を超えると、データベース全体が利用できなくなります。この種の問題は一般的にタグキーの設計が間違っていることが原因です。例えば、タグキーはランダムなIDです。一度この問題が発生すると、復旧は困難です。手動でデータを削除するしかありません。

 

    プロセスが再起動した場合、メモリ内にインデックスを構築するためにすべてのTSMファイルからTimeSeriesの完全な情報をロードする必要があるため、データを回復するのに長い時間がかかります。

基于磁盘的索引

关于全内存索引的问题,在最新的InfluxDB1.3版本中,提供了额外的索引实现。由于代码设计的可扩展性,索引模块和存储引擎模块都是插件化的。您可以通过设置选择要使用的索引。

image.png

在InfluxDB中,实现了一种特殊的存储引擎用于存储索引数据。其结构类似于LSM。它是一种基于磁盘的索引结构,如图所示。详细信息请参阅设计文档。

索引数据首先被写入WAL(Write-Ahead-Log)。WAL中的数据由LogEntry组成,每个LogEntry对应一个TimeSeries,并包含与测量、标签和检验和相关的信息。当向WAL成功写入时,数据将会进入基于内存的索引结构中。当WAL累积到一定大小时,LogFile将会刷新到IndexFile中。IndexFile的逻辑结构与基于内存的索引结构相匹配,它显示了从测量到tagkey,从tagkey到tagvalue,从tagvalue到TimeSeries的映射结构。InfluxDB使用mmap访问文件,并为文件中的每个映射保存HashIndex,以加速查询。

此外,当IndexFiles达到指定的容量时,InfluxDB会提供一种合并机制,将多个IndexFiles合并为一个,以节省存储空间并加快查询速度。

摘要

InfluxDB的所有组件都是自主开发的。自主开发的优点是可以根据时间序列数据的属性来设计每个组件,从而最大化性能。整个社区的活动也很活跃,但是经常发生大规模的功能升级,如存储格式的变更和索引实现的更改等,对用户来说相当不方便。总的来说,我对InfluxDB的开发持乐观态度。不幸的是,InfluxDB的集群版本不是开源的。

本博客是从英文版翻译而来的,请您点击这里查看原文。我们使用了部分机器翻译。如果有翻译错误,请您指出,我们将不胜感激。

阿里巴巴云拥有两个数据中心在日本,并且拥有全球60多个可用区,是2019年加特纳评选出的亚太地区第一的云基础设施供应商。
请点击此处查看阿里巴巴云的详细信息。
阿里巴巴云日本官方页面。

广告
将在 10 秒后关闭
bannerAds