InfluxDB 是如何工作的(1)用户故事及思路

270 阅读5分钟

这是我之前在公司内部的一个系列分享,整理成文字版发布。

前言:在云厂的工作中,总是会面对大量的时序数据。InfluxDB 是工作中最常用的时序数据库。时序数据与更为常用的关系数据的区别是什么,为什么 InfluxDB 可以高性能低存储空间消耗的存储海量时序数据。

一、用户故事

时序数据有什么特点?分析一个具体的需求:如何存储一个数据中心内所有服务器 CPU 分核心使用率?

就是需要表达:设备在某个时间状态

打开任务管理器,切换到性能 Tab,就可以看见自己设备的 CPU 使用率图。

图片.png

只看其中 CPU 占用率的图,如果把它还原成表的形式:

IP地址CPU核时间使用率
192.168.1.1#051
192.168.1.1#1517
192.168.1.1#063
192.168.1.1#1615

可以归类为:

  • IP地址、CPU核:共同表达了是哪一个设备,把这些统称为标签
  • 时间
  • 使用率:设备的状态,即

这就是时序数据的基本数据模型。

对比常见的用户表:

用户名密码上次登陆时间
tsrs$2a$10$Tf9Qw0W8UOW2P……5
rsts$2a$10$EvLK4wtA4xSHYU……10

发现时序数据会有这些特点:

  • 一定会存在时间
  • 标签会经常重复,因为一个设备会持续的产生数据,同一个设备的标签是一致的。

数据操作的需求:

  • 增:批量增,所有的设备会间隔 1s 上报自己的 CPU 使用率
  • 删:区间删,删除 7 天之前的所有数
  • 改:不会改,不会出现「把 4 月份的CPU使用率改为 0」的情况
  • 查:时间区间+标签查,找出过去十分钟 192.168.1.1 CPU#0 核的使用率

对比用户系统:

  • 增:单条增,注册
  • 删:单条删,销户
  • 改:单条改,改密
  • 查:单条查,登录

然后是计算机本身的特点:

无论是HDD、SSD还是内存连续读写性能都远高于随机读写,这种差距甚至可以达到三个数量级,其中,内存的随机读写性能是最高的。如下图(注意对数坐标)

各存储操作对比

二、思路

假设现在重新设计一个时序数据库:

因为硬盘和内存的大小总是有限的,如果能尽量少的存储数据,就可以极大的降低成本。而且同一个数据源上报的数据的标签值总是一致的。在存储数据中心内所有服务器 CPU 分核心使用率这个场景中,数据中发生变化的只有时间。那么标签只需要存一次。

可以把拥有同样标签的数据称之为一个系列

根据这个思路改造一下上面的表,拆分成两张:

方案一

系列表:

编号标签值
1IP=192.168.1.1;CPU=#0
2IP=192.168.1.1;CPU=#1

数据表:

系列编号时间使用率
151
2517
163
2615

因为查询请求总是类似 「找出过去十分钟 192.168.1.1 #0 CPU核的使用率」这样的针对一个系列的查询。而连读读写总是比随机读写要快得多得多。那么把一个系列的数据组织到一起将会有很大的益处。再改造一下设计,把数据按系列拆分开:

方案二

系列表:

表号标签值
1IP=192.168.1.1;CPU=#0
2IP=192.168.1.1;CPU=#1

表①:

时间使用率
51
63

表②:

时间使用率
517
615

但是,这导致了写变成随机操作。

因为内存是所有存储设备中随机性能最好的,可以利用内存先存起来,再改一改方案二。

方案二A

把数据先按方案二写到内存上,攒一段时间的数据再统一写到硬盘上。

似乎很完美了。

但是,假如在内存上的数据写入硬盘之前发生了意外断电,这些数据就会丢失。继续改方案二。

方案二B

写入内存的同时,也按照 方案一中数据表 的形式写一份到硬盘中,把它称之为 WAL(Write-ahead logging),对硬盘来说是连续写,可以有很好的性能 。内存和硬盘上的两份数据都写入成功才是成功。

如果发生断电,程序重启后先读取硬盘上的 WAL,恢复内存中尚未写入硬盘的数据。虽然读取 WAL 恢复内存结构的成本比较高,但断电并不总是发生。

三、InfluxDB 是怎么干的

InfluxDB 的做法是类似的:

func (e *Engine) WritePoints(ctx context.Context, points []models.Point) error {

    // 略去预处理代码

    
    // first try to write to the cache

    if err := e.Cache.WriteMulti(values); err != nil {

        return err

    }
    

    if e.WALEnabled {

        if _, err := e.WAL.WriteMulti(ctx, values); err != nil {

            return err

        }

    }

    return seriesErr
}

先写入了内存上的 Cache,然后写入硬盘上的 WAL两者都写入成功后才返回。

最后:我们根据时序数据和计算机本身的特点重新梳理了思路,反推出了与 InfluxDB 类似的方案 。虽然一直在用关系数据库中的表进行说明,但是这并不表示 InfluxDB 使用关系数据库做为其存储引擎。同时,InfluxDB 中的数据布局与本文并不完全一致。在下一篇文章中将了解如何把 InfluxDB 在自己的机器上编译运行起来,进行断点调试,并详细的探索 InfluxDB 中 Cache 的结构。

扩展阅读: