浅谈时序数据库内核:如何用单机扛住亿级数据写入

2024-01-09 13:28:07 浏览数 (2)

版本

日期

备注

1.0

2021.10.19

文章首发

1.0

2021.11.21

针对在公司中分享时添加的内容进行补充

  • 我们通常会在监控页面上根据观察某个时间端的数据。在需要时,会寻找其中更细的时间段来观察。
  • 时序数据库会将告警系统关心的指标推送过去

1.1 Prometheus踩过的坑 在这里,我们先简单复习一下Prometheus中的数据结构。其为典型的k-v对,k(一般叫Series)由MetricNameLablesTimeStamp组成,v则是值。 在早期的设计中,相同的Series会按照一定的规则组织起来,同时也会根据时间去组织文件。于是就变成了一个矩阵: 优点是写可以并行写,读也可以并行读(无论是根据条件还是时间段)。但缺点也很明显:首先是查询会变成一个矩阵,这样的设计容易触发随机读写,这无论在HDD还是SSD上都很难受(有兴趣的同学可以看后面的3.2小节)。 于是Prometheus又改进了一版存储。每一个Series一个文件,每个Series的数据在内存里存满1KB往下刷一次。 这样缓解了随机读写的问题,但也带来新的问题:

  1. 在数据没达到1KB还在内存里时,如果机器carsh了,那么数据则丢失
  2. Series很容易变成特别多,这会导致内存占用居高不下
  3. 继续上面的,当这些数据一口气被刷下去时,磁盘会变得很繁忙
  4. 继上,很多文件会被打开,FD会被消耗完
  5. 当应用很久没上传数据时,内存里的数据该刷不该刷?其实是没法很好的判定的

1.2 InfluxDB踩过的坑 1.2.1 基于LSM Tree的LevelDB LSM Tree的写性能比读性能好的多。不过InfluxDB提供了删除的API,一旦删除发生时,就很麻烦——它会插入一个墓碑记录,并等待一个查询,查询将结果集和墓碑合并,稍后合并程序则会运行,将底层数据删除。并且InfluxDB提供了TTL,这意味着数据删起来是Range删除的。 为了避免这种较慢的删除,InfluxDB采用了分片设计。将不同的时间段切成不同的LevelDB,删除时只需关闭数据库并删文件就好了。不过当数据量很大的时候,会造成文件句柄过多的问题。 1.2.2 基于mmap B Tree的BoltDB BoltDB基于单个文件作为数据存储,基于mmap的B Tree在运行时的性能也并不差。但当写入数据大起来后,事情变得麻烦了起来——如何缓解一次写入数十万个Serires带来的问题? 为了缓解这个问题,InfluxDB引入了WAL,这样可以有效缓解随机写入带来的问题。将多个相邻的写入缓冲,然后一起fresh下去,就像MySQL的BufferPool。不过这并没有解决写入吞吐量下降的问题,这个方法仅仅是拖延了这个问题的出现。 2. 解决方案 细细想来,时序数据库的数据热点只集中在近期数据。而且多写少读、几乎不删改、数据只顺序追加。因此,对于时序数据库我们则可以做出很激进的存储、访问和保留策略(Retention Policies)。 2.1 关键数据结构

  • 参考日志结构的合并树(Log Structured Merge Tree,LSM-Tree)的变种实现代替传统关系型数据库中的B Tree作为存储结构,LSM 适合的应用场景就是写多读少(将随机写变成顺序写),且几乎不删改的数据。一般实现以时间作为key。在InfluxDB中,该结构被称为Time Structured Merge Tree。
  • 时序数据库中甚至还有一种并不罕见却更加极端的形式,叫做轮替型数据库(Round Robin Database,RRD),它是以环形缓冲的思路实现,只能存储固定数量的最新数据,超期或超过容量的数据就会被轮替覆盖,因此它也有着固定的数据库容量,却能接受无限量的数据输入。

2.2 关键策略

  • WAL(Write ahead log,预写日志):和诸多数据密集型应用一样,WAL可以保证数据的持久化,并且缓解随机写的发生。在时序数据库中,它会被当作一个查询数据的载体——当请求发生时,存储引擎会将来自WAL和落盘的数据做合并。另外,它还会做基于Snappy的压缩,这是个耗时较小的压缩算法。
  • 设置激进的数据保留策略,比如根据过期时间(TTL),自动删除相关数据以节省存储空间,同时提高查询性能。对于普通的数据库来说,数据会存储一段时间后被自动删除的这个做法,可以说是不可想象的。
  • 对数据进行再采样(Resampling)以节省空间,比如最近几天的数据可能需要精确到秒,而查询一个月前的冷数据只需要精确到天,查询一年前的数据只要精确到周就够了,这样将数据重新采样汇总,可以节省很多存储空间

3.小结 总体看下来,相比Kafka、HBase来说,时序数据库的内部结构并不简单,非常有学习价值。 3.1 参考链接

  • prometheus.io/docs/promet…
  • docs.influxdata.com/influxdb/v1…
  • blog.csdn.net/cymm_liu/ar…
  • zhuanlan.zhihu.com/p/32710333
  • archive.docs.influxdata.com/influxdb/v0…
  • 周志明:《凤凰架构》

3.2 磁盘随机读写vs顺序读写 3.2.1 HHD HHD的随机读写弱势根本原因在于它的物理结构。当我们对磁盘产生寻址请求时候(可能是读一个区域的数据,或者定位到某个区域来写入数据),首先可以看到的瓶颈就是主轴的转速,其次是磁头臂。 放到现在来看,HHD的随机读写大致为速度为2MB/S、2.2MB/S。而顺序读写大致为200MB/S,220MB/S。 3.2.2 SSD SSD看起来一切都很美好,随机读写速度一般在400MB/S、360MB/S,顺序读写速度一般在560MB/S,550MB/S。 但真正的问题在于它的内部结构。它的最基本物理单位是一个闪存颗粒,多个闪存颗粒可以组成一个page,多个page可以组成一个block。 写入时,会以page为单位,我们可以看到图里是4kb。这意味着你哪怕写1b的数据,也要占据4kb。这还不是最致命的,最致命的是删除,删除是以整个block为单位发生的。图中是512kb,这意味着你哪怕删里面1kb的数据,都要导致写放大发生。

0 人点赞