分布式时序数据库-功能框架-苦思冥想

OK ! 继上一篇文章,分布式时序数据库-不破不立(zhuanlan.zhihu.com/p/14),主要想表达是在当前整个大数据环境下,我们为什么要做分布式时序数据库?故而,写了几点时序数据库的设计目标。但是,没有表明当前时序数据库领域的现状如何,这里可以参照另一篇文章时序数据-存储初识1(zhuanlan.zhihu.com/p/69),这篇文档里对InfluxDB、OpentsDB,主要从存储设计的角度进行了分析。


从当前IOT领域的技术趋势、“新基建”的国家战略导向、行业大数据项目的市场需求出发,我会认为时序数据库不是一个单独的数据库产品或者技术。而是,围绕传统工业互联网升级换代、大数据中心基础设施建设,助力传统行业,充分发挥数据在线及时性价值、潜在生产价值、整合创新价值,构建从数据流->数据流、数据流->控制流的完整产品或技术生态


如果,具体到时序数据库本身的应用价值,从产品或者技术角度来说,会具体应用到什么地方呢?

如下图所示,这是前几天做工业互联网协议OPC-UA数据接入的时候,从时序数据库角度,对于边缘侧数据存储和计算的一些思考。


在边缘计算的领域,为了发挥数据在线及时性的价值,时序数据必然需要在靠近设备地方进行存储、进行窗口计算、进行多维聚合分析、进行高并发持续写入。那么,是不是时序数据库可以在边缘计算中,作为存储引擎呢?


从大数据中心基础设施建设的角度来看,针对时序数据的存储,时序数据库必然会作为关键组件存在。那么,围绕整个时序数据库的数据流转逻辑,在这整个生态链条中,会基本涵盖面向数据采集、数据计算、数据存储、数据模型(此处,人工智能的模型应用)、数据可视等各个方面


所以,路漫漫。


以上叨叨,也只是个人对时序数据库周边生态的思考做了简单的概述。因为,你会发现不管是InfluxDB、Prometheus,还是openTSDB的发展,都离不开其数据库生态的壮大。而OpenTSDB本身就可以继承Hadoop生态,是吧 !


好了!废话不多! 这篇文章的正题还是为了介绍一下,后面我们要做的时序数据库--一个基本功能框架


整个基本框架主要内容还是在数据存储这一块,因为核心就是存储。如何组织数据处理逻辑、设计合理的存储结构,才能适应高并发的持续写入、才能做到多维组合查询、才能降低数据存储成本,对吧。至少这个单节点数据库例,在IDEA开发环境中,每秒轻松写上几百万的时序点是没有任何问题的。

对于底层的数据存储,基本上可以分为wal管理、缓存管理、index管理、存储管理、元数据管理5个大的模块,主要负责时序数据的存储。


往上一层主要是数据库节点对于读写请求的处理,这里主要包含了读写请求处理、读写响应处理、读写请求分发、自定义协议解析、节点初始化等。


最上面一层就是客户端,客户端到数据库节点的通信,其实是基于protobuf,采用自定义协议进行数据收发的。


右边这里,其实只是把时序数据库每个模块的配置抽象出来,使用yaml进行时序数据库节点配置。


除此之外,还有一个比较重要的模块,需要单独说一下。因为整个时序数据库是使用Golang写的。所以,这里的元数据中心,使用了etcd,主要负责元数据的存储、节点初始化注册、节点状态监听、节点数据存储、库表元数据存储等。


OK,接下来就分别介绍一下,关于存储的几个主要模块。

wal 管理

wal管理这里,主要是为了做数据故障恢复。主要包含wal buffer管理、wal file管理、wal数据压缩、全局递增ID生成器。

1、对于wal buffer 是使用map+链表的结构设计,链表中的每个节点维护一个大小可控的数组切片,存储着每一个时刻写进来的数据,数据中的内容就是递增ID、时间戳、操作类型、数据、还有大小。

type walData struct {
   sequenceId int64
   timestamp int64
   operate []byte
   data []byte
   size int
}

2、对于 wal file 会根据配置的wal file 大小,进行文件的自动分割。比较简单,主要是把wal buffer 中的数据,持久化到磁盘。不过,这里还没有做过期wal file的自动清除,可以考虑加个wal的文件头,用于记录wal文件元数据信息,方便进行wal file的处理。


3、对于wal 数据压缩,这里主要使用了snappy数据压缩,不过后期可以考虑其他压缩算法。因为这个压缩算法对于时序数据库的压缩和delta-delta相比,还是有一定距离。真的是浪费了磁盘IO。


4、对于wal 的数据恢复,还没有做。后面会完善。

缓存 管理

1、对于 buffer的设计,主要是根据database+table+tagset = seriesKey 对写入进来的数据进行了映射,这样便于后期在数据持久化的时候,进行数据的分片写入。当然,这里还有一个buffer的过期策略,可以配置超时时间、批量刷写的条数,将buffer中的数据,刷写到持久化引擎。


2、为了节省buffer占用的内容空间,可以将buffer中的databaseName、tableName、tagSet 进行编码。


3、为了满足数据及时可查的要求,这里可以生成一个buffer的快照。当然,这一块,在目前的项目中还没有做。目前的数据读取时直接读取的底层存储。


index管理

1、index管理这里,主要考虑的对于时序数据的查询场景,通常都是时间范围内,组合维度标签或者组合维度和指标的查询。所以,这里在数据写入的时候,默认开启索引生成。这里有两个索引内存结构,一个负责写索引、一个负责读索引。

type Index struct {
   mu sync.RWMutex
   writeData map[string]*indexNodeLinked
   readData map[string]*indexNode
   ttl time.Duration
   size int
   count int
   *indexKv
}

对于写索引,会定时、批量的将索引数据存储在本地数据节点。

对于读索引,会持久化在内存中,因为每次做数据查询的时候,不想在扫一遍磁盘。


2、索引文件目前,是没有做索引文件本省的数据分片的, 不过可以配置多个索引目录,将索引分片写到不同的索引文件。

3、索引构建,这里的索引构建是指在节点启动的时候,去扫描索引文件,自动帮你构建索引的内存数据结构。

4、当然,目前也没有做索引数据的内存编码,也没做什么压缩。所以,这里会综合考虑看看。

存储管理

整个存储管理是这里的核心,因为这里会针对buffer中的数据,进行数据分片、数据编码、数据压缩,并且通过合理的数据结构,写到数据表中。

type compressPoints struct {
   maxTime int64
   minTime int64
   chunk compress.Chunk
   fieldKey string
   count int
}

1、数据分片,这里的数据分片主要是根据数据时间范围对数据会进行分片,因为在分布式设计里,已经使用Hash一致性对数据进行了分片。这里需要保证每个节点的数据是按照时间范围分片的,主要是为了方面以后时间范围内的查询是吧。(更好的组织数据的写入,就是为了方便数据查询,但是这会带来数据写入的复杂读,并且会影响写入性能,所以需要综合考虑!)

2、数据压缩,主要采用delta-delta 压缩,这个算法确实非常适合对时序数据的压缩。目测,是snappy压缩的5-10倍。

3、数据编码,如上面的数据结构,这里对于每个压缩块,设置了数据头。包含这个压缩块的时间范围、版本、大小、条数等等信息。

元数据管理

哎呀 ! 终于说到元数据管理了。

这里元数据注册中心使用的etcd。

这里配置了两个元数据节点,分别是元数据节点、数据库节点

metaPrefix: "/silver/metaData/"
nodePrefix: "/sliver/node/"

1、数据库节点,会在初始化启动的时候注册到元数据中心,并配置心跳时间,监听节点状态。

2、元数据节点,也会在初始化启动的时候,针对本地存储的元数据进行注册。同时,在元数据进行变化的时候,会同步到元数据中心。

其实,如果不做索引的话,我根本不需要元数据中心,可以很无奈。因为,要做到P2P网络对称的分布式架构的话,我需要每个节点都提供服务。我不想再每个节点之间同步index,这样会有些复杂。所以,才引入了第三方组件etcd。


都是想想做做的这个过程,有些东西的设计肯定也不是那么完善。但是,我很欢迎能够有请教和交流的人,与我一起,这该是多么幸运的事情啊!


总结一下,以上基本还是功能框架的基本介绍,中间有穿插一些设计的思路,但是不是很完备。应该会在下一篇技术架构的介绍中,会介绍更多的设计思路。 在后面实践的文章中,对每个模块的详细设计进行介绍。


好了,今天连续写了两篇,是该歇歇了。


心情就像今天的天空一样。

发布于 05-21

文章被以下专栏收录