Mac OS安装victoriametrics
docker run -it --name vm -v /Users/xxx/data/vm:/victoria-metrics-data -p 8428:8428 victoriametrics/victoria-metrics
启动后,查看运行参数
http://localhost:8428/metrics
http://localhost:8428/debug/pprof/
使用influxdb协议写入vm
pom.xml引入依赖
将数据写入vm示范代码
package example;
import com.influxdb.client.InfluxDBClient;
import com.influxdb.client.InfluxDBClientFactory;
import com.influxdb.client.WriteApi;
import com.influxdb.client.domain.WritePrecision;
import com.influxdb.client.write.Point;
import java.time.Instant;
public class Write2VM {
public static void main(String[] args) {
String bucket = "flink";
String org = "galaxy";
InfluxDBClient client = InfluxDBClientFactory.create("http://localhost:8428");
Point point = Point
.measurement("mem")
.addTag("vm", "pointWay")
.addField("used_percent", 66)
.time(Instant.now(), WritePrecision.NS);
try (WriteApi writeApi = client.getWriteApi()) {
writeApi.writePoint(bucket, org, point);
}
}
}
在grafana中查看数据点
命令行查询
curl -G 'http://localhost:8428/api/v1/export' -d 'match={__name__=~"mem_used_percent.*"}'
结果:
{"metric":{"__name__":"mem_used_percent","vm":"pointWay"},"values":[66,88],"timestamps":[1634307675617,1634308728583]}
命令行curl写入数据到vm
大家好,又见面了,我是你们的朋友全栈君。
1.基本概念
时序数据库(Time Series Database)是用于存储和管理时间序列数据的专业化数据库。时序数据库特别适用于物联网设备监控和互联网业务监控场景。
下面介绍下时序数据库的一些基本概念(不同的时序数据库称呼略有不同)。
1.1 度量(metric)
监测数据的指标,例如风力和温度。相当于关系型数据库中的table。
1.2 标签(tag)
指标项监测针对的具体对象,属于指定度量下的数据子类别。一个标签(Tag)由一个标签键(TagKey)和一个对应的标签值(TagValue)组成。
例如在监测数据的时候,指定度量(Metric)是“气温”,“城市(TagKey)= 杭州(TagValue)”就是一个标签(Tag),则监测的就是杭州市的气温。更多标签示例:机房 = A 、IP = 172.220.110.1。
1.3 域(field)
在指定度量下数据的子类别,一般情况下存放的是会随着时间戳的变化而变化的数据。一个metric可支持多个field,如metric为风力,该度量可以有两个field:direction和speed。
1.4 度量值(value)
度量对应的数值,如56°C、1000r/s等(实际中不带单位)。如果有多个field,每个field都有相应的value。不同的field支持不同的数据类型写入。对于同一个field,如果写入了某个数据类型的value之后,相同的field不允许写入其他数据类型。
1.5 时间戳(Timestamp)
数据(度量值)产生的时间点。
1.6 数据点 (Data Point)
针对监测对象的某项指标(由度量和标签定义)按特定时间间隔(连续的时间戳)采集的每个度量值就是一个数据点。1个metric+1个field(可选)+1个timestamp+1个value + n个tag(n>=1)”唯一定义了一个数据点。相当于关系型数据库中的row。
1.7 时间序列(Time Series)
1个metric+1个field(可选) +n个tag(n>=1)”定义了一个时间序列。主要是针对某个监测对象的某项指标(由度量和标签定义)的描述。某个时间序列上产生的数据值的增加,不会导致时间序列的增加。
例1(单域):对温度的时间序列监测值
温度(temperature)作为一个度量(metric),共4个数据点,每个数据点由如下组成:
timestamp:时间戳
三个tag:每个tag都是一个key-value对,tag的key分别是deivceID、floor、room。
一个field:温度值
其中4个数据点使用的metric、tag是相同的,所以是同一个时间序列。如图所示:
例2(多域,单一数据源采集):记录一段时间内的某个集群里各机器上各端口的出入流量,每半小时记录一个观测值。
网络(Network)作为一个度量(metric),总共7个数据点。每个数据点由以下部分组成:
timestamp:时间戳
两个tag:host、port,代表每个point归属于哪台机器的哪个端口
两个field:bytes_in、bytes_out,代表piont的测量值,半小时内出入流量的平均值
同一个host、同一个port,每半小时产生一个point,随着时间的增长,field(bytes_in、bytes_out)不断变化。
如host:host4,port:51514,timestamp从02:00 到02:30的时间段内,bytes_in 从 37.937上涨到38.089,bytes_out从2897.26上涨到3009.86,说明这一段时间内该端口服务压力升高。
例3(多域,多个数据源采集):监测风力的两个域(speed和direction)的监测数据,监测数据来自不同的传感器
风力(wind)作为一个度量(metric),总共2个时间序列,8个数据点。每个数据点由以下部分组成:
3个tag:key分别是sensor、city、province。为了表示在广东省深圳市传感器编号95D8-7913上传风向(direction)数据,可以将这个数据点的tag为标记为sensor=95D8-7913、city=深圳、province=广东。
两个域:风向(direction)和速度(speed),分别来自不同的传感器。
如图,当使用的是metric、field和tag是相同的时,是同一个时间序列。
将数据采用metric+field的方式存储的优势在于,可以在同一个时间序列下联合查询。以上图为例,要查询1467627246000-1467627249000时间内风力(wind)的情况,可以联合查询多个field的值,得到下图的数据。
1.8 时间精度
时间线数据的写入时间精度——毫秒、秒、分钟、小时或者其他稳定时间频度。例如,每秒一个温度数据的采集频度,每 5 分钟一个负载数据的采集频度。
1.9-1 数据组(Data Group)
可以按标签这些数据分成不同的数据组。用来对比不同监测对象(由标签定义)的同一指标(由度量定义)的数据。
例如,将温度指标数据按照不同城市进行分组查询,操作类似于该 SQL 语句:select temperature from xxx group by city where city in (shanghai, hangzhou)。
1.9-2 聚合( Aggregation)
可以对一段时间的数据点做聚合,如每10分钟的和值、平均值、最大值、最小值等。
例如,当选定了某个城市某个城区的污染指数时,通常将各个环境监测点的指标数据平均值作为最终区域的指标数据,这个计算过程就是空间聚合。
2.应用场景
2.1系统运维和业务实时监控
在业务服务器上部署各种脚本客户端,实时采集服务器指标数据(IO指标、CPU指标、带宽内存指标等等),业务相关数据(方法调用异常次数、响应延迟、JVM GC相关数据等等)、数据库相关数据(读取延迟、写入延迟等等),很显然,这些数据都是时间序列相关的。客户端采集和实时计算之后会发送给哨兵服务器,哨兵服务器会将这些数据进行存储,并实现实现监控和分析的展现,供用户查询。
如下图所示,用户可以登录哨兵系统查看某台服务器的负载,负载曲线就是按照时间进行绘制的,带有明显的时序特征:
2.2 物联网设备状态监控存储分析
在可预知的未来3~5年,随着物联网以及工业4.0的到来,所有设备都会携带传感器并联网,传感器收集的时序数据将严重依赖TSDB的实时分析能力、存储能力以及查询统计能力。
上图是一个智慧工厂示意图,工厂中所有设备都会携带传感设备,这些传感设备会实时采集设备温度、压力等基本信息,并发送给服务器端进行实时分析、存储以及后期的查询统计。除此之外,比如现在比较流行的各种穿戴设备,以后都可以联网,穿戴设备上采集的心跳信息、血流信息、体感信息等等也都会实时传输给服务器进行实时分析、存储以及查询统计。
PS:阿里云拥有自主研发的时序数据库产品 TSDB ,此产品在阿里内部磨练多年, 历经多次双十一高严苛场景的功能和性能验证, 在应用监控,服务器资源监控,数据库监控, 智慧园区设备监控,以及盒马新零售边缘设备监控都有丰富的落地使用场景 ,覆盖了阿里集团80%以上的时序监控业务。
3.基本特点
时序业务和普通业务在很多方面都有巨大的区别,归纳起来主要有如下几个方面:
3.1 持续产生海量数据,没有波峰波谷
举几个简单的例子,比如类似哨兵的监控系统,假如现在系统监控1w台服务器的各类指标,每台服务器每秒采集100种metrics,这样每秒钟将会有100w的TPS。再比如说,现在比较流行的运动手环,假如当前有100w人佩戴,每个手环一秒只采集3种metrcis(心跳、脉搏、步数),这样每秒钟也会产生300w的TPS。
3.2 数据都是插入操作,基本没有更新删除操作
时序业务产生的数据很少有更新删除的操作,基于这样的事实,在时序数据库架构设计上会有很大的简化。
3.3 近期数据关注度更高,未来会更关注流式处理这个环节,时间久远的数据极少被访问,甚至可以丢弃
这个很容易理解,哨兵系统我们通常最关心最近一小时的数据,最多看看最近3天的数据,很少去看3天以前的数据。随着流式计算的到来,时序数据在以后的发展中必然会更关注即时数据的价值,这部分数据的价值毫无疑问也是最大的。数据产生之后就可以根据某些规则进行报警是一个非常常见并重要的场景,报警时效性越高,对业务越有利。
3.4 数据存在多个维度的标签,往往需要多维度联合查询以及统计查询。
时序数据另一个非常重要的功能是多维度聚合统计查询,比如业务需要统计最近一小时广告主google发布在USA地区的广告点击率和总收入分别是多少,这是一个典型的多维度聚合统计查询需求。这个需求通常对实效性要求不高,但对查询聚合性能有比较高的要求。
4.TSDB核心特性
总结起来TSDB需要关注的技术点主要有这么几个:
4.1 高吞吐量写入能力
这是针对时序业务持续产生海量数据这么一个特点量身定做的,当前要实现系统高吞吐量写入,必须要满足两个基本技术点要求:系统具有水平扩展性和单机LSM体系结构。系统具有水平扩展性很容易理解,单机肯定是扛不住的,系统必须是集群式的,而且要容易加节点扩展,说到底,就是扩容的时候对业务无感知,目前Hadoop生态系统基本上都可以做到这一点;而LSM体系结构是用来保证单台机器的高吞吐量写入,LSM结构下数据写入只需要写入内存以及追加写入日志,这样就不再需要随机将数据写入磁盘,HBase、Kudu以及Druid等对写入性能有要求的系统目前都采用的这种结构。
4.2 数据分级存储/TTL
这是针对时序数据冷热性质定制的技术特性。数据分级存储要求能够将最近小时级别的数据放到内存中,将最近天级别的数据放到SSD,更久远的数据放到更加廉价的HDD或者直接使用TTL过期淘汰掉。
4.3 高压缩率
提供高压缩率有两个方面的考虑,一方面是节省成本,这很容易理解,将1T数据压缩到100G就可以减少900G的硬盘开销,这对业务来说是有很大的诱惑的。另一个方面是压缩后的数据可以更容易保证存储到内存中,比如最近3小时的数据是1T,我现在只有100G的内存,如果不压缩,就会有900G的数据被迫放到硬盘上,这样的话查询开销会非常之大,而使用压缩会将这1T数据都放入内存,查询性能会非常之好。
4.4 多维度查询能力
时序数据通常会有多个维度的标签来刻画一条数据,就是上文中提到的维度列。如何根据随机几个维度进行高效查询就是必须要解决的一个问题,这个问题通常需要考虑位图索引或者倒排索引技术。
4.5 高效聚合能力
时序业务一个通用的需求是聚合统计报表查询,比如哨兵系统中需要查看最近一天某个接口出现异常的总次数,或者某个接口执行的最大耗时时间。这样的聚合实际上就是简单的count以及max,问题是如何能高效的在那么大的数据量的基础上将满足条件的原始数据查询出来并聚合,要知道统计的原始值可能因为时间比较久远而不在内存中哈,因此这可能是一个非常耗时的操作。目前业界比较成熟的方案是使用预聚合,就是在数据写进来的时候就完成基本的聚合操作。
未来技术点:异常实时检测、未来预测等等。
5.传统关系型数据库存储时序数据的问题
很多人可能认为在传统关系型数据库上加上时间戳一列就能作为时序数据库。数据量少的时候确实也没问题。但时序数据往往是由百万级甚至千万级终端设备产生的,写入并发量比较高,属于海量数据场景。
5.1 MySQL在海量的时序数据场景下存在如下问题:
存储成本大:对于时序数据压缩不佳,需占用大量机器资源;
维护成本高:单机系统,需要在上层人工的分库分表,维护成本高;
写入吞吐低:单机写入吞吐低,很难满足时序数据千万级的写入压力;
查询性能差:适用于交易处理,海量数据的聚合分析性能差。
5.2 使用Hadoop生态(Hadoop、Spark等)存储时序数据会有以下问题:
数据延迟高:离线批处理系统,数据从产生到可分析,耗时数小时、甚至天级;
查询性能差:不能很好的利用索引,依赖MapReduce任务,查询耗时一般在分钟级。
5.3 时序数据库需要解决以下几个问题:
时序数据的写入:如何支持每秒钟上千万上亿数据点的写入。
时序数据的读取:如何支持在秒级对上亿数据的分组聚合运算。
成本敏感:由海量数据存储带来的是成本问题。如何更低成本的存储这些数据,将成为时序数据库需要解决的重中之重。
6.时序数据库发展简史与现状
目前,DB-Engines把时间序列数据库作为独立的目录来分类统计,下图就是2018年业内流行的时序数据库的关注度排名和最近5年的变化趋势。
VictoriaMetrics是一个快速高效且可扩展的监控解决方案和时序数据库,可以作为Prometheus的长期远端存储,具备的特性有:
- 支持prometheus查询api,同时实现了一个metricsql 查询语言
- 支持全局查询视图,支持多prometheus 实例写数据到VictoriaMetrics,然后提供一个统一的查询
- 支持集群
- 高性能
- 支持多种协议,包括influxdb line协议,prometheus metrics,graphite ,prometheus远端写api,opentsdb http协议等
- 高压缩比
本文主要分析下VictoriaMetrics的存储机制,包括整体架构、数据模型、磁盘目录、文件格式等部分,对应的源码版本号为v1.45.0。
1.整体架构
VictoriaMetrics的集群主要由vmstorage、vminsert、vmselect等三部分组成,其中,vmstorage 负责提供数据存储服务; vminsert 是数据存储 vmstorage 的代理,使用一致性hash算法进行写入分片; vmselect 负责数据查询,根据输入的查询条件从 vmstorage 中查询数据。
VictoriaMetrics的这个三个组件每个组件都可以单独进行扩展,并运行在大多数合适软件上。vmstorage采用shared-nothing架构,优点是 vmstorage的节点相互之间无感知,相互之间无需通信,不共享任何数据,增加了集群的可用性、简化了集群的运维和集群的扩展。
另外,VictoriaMetrics集群支持多个隔离的租户特性,又名命名空间。租户通过accountID (或者accountID:projectID) 来标识,这些ID放在请求URL中。有关VictoriaMetrics租户的一些事实:
- 每个accountID和projectID由[0 .. 2 ^ 32)范围内的任意32位整数标识。如果缺少projectID,则会将其自动分配为0。预计有关租户的其他信息(例如身份验证令牌,租户名称,限制,计费等)将存储在单独的关系数据库中。此数据库必须由位于VictoriaMetrics群集前面的单独服务(例如vmauth)管理。
- 将第一个数据点写入给定租户时,将自动创建租户。
- 所有租户的数据平均分布在可用的vmstorage节点之间。当不同的租户具有不同的数据量和不同的查询负载时,这可以保证在vmstorage节点之间平均负载。
整体上来说,VictoriaMetrics支持多租户,但租户的信息需要使用额外的关系型数据库来存储,且VictoriaMetrics不支持在单个请求中查询多个租户。
2.数据模型
开门见山,**VictoriaMetrics采用的数据模型是单值模型,且只支持浮点数指标**。那么到底什么是单值模型呢?目前,常见的时序数据库的数据模型,主要分成单值模型和多值模型。这里简单说明下单值模型和多值模型,整体上,可以认为单值模型是多值模型的一个特例。
单值模型是根据**业务指标数据**建模,按照单个指标的细粒度进行数据使用和逻辑存储,如下图所示,一行数据只有一个指标值,即value列。目前采用单值模型的时序数据库,有OpenTSDB、 KairosDB、Prometheus等。
多值的模型则是针对**数据源**建模,我们每一行数据针对的是一个数据源,它的被测量的多个指标在同一列上,如下图所示,一行数据有多个指标值,即有cpu和io两列。目前采用多值模型的时序数据库,有InfluxDB、TimescaleDB等。
3.磁盘目录
VictoriaMetrics的根目录下主要包括4个目录或文件,如下图所示。其中,最主要的是数据目录data和索引目录indexdb,flock.lock文件为文件锁文件,用于VictoriaMetrics进程锁住文件,不允许别的进程进行修改目录或文件。
3.1 数据目录
数据目录data的具体结构,如下图所示,在图中使用红色文字,对主要目录或文件做了简单说明,其中最主要的是**big**目录和**small**目录,这两个目录的结构是一样的。其中,在VictoriaMetrics中,使用table来表示的数据或者索引的根目录,而实际上VictoriaMetrics中没有实际的表table级别目录。
在small目录下,以月为单位不断生成partition目录,比如上图中的2020_11目录,对应的实现在源码lib/storage/partition.go中。partition目录包括part目录、临时目录tmp、 事务目录txn等三个目录。
内存中的数据每刷盘一次就会生成一个part目录,如上图中的"708_354_20201103102134.255_20201103102149.255_1643F83394CA24A7",目录名中的708表示这个目录下包含的数据行数rowsCount, 目录名中的354表示这个目录中包含的数据块数blocksCount, 20201103102134.255表示目录中包含的数据的最小时间戳,20201103102149.255表示目录中包含的数据的最大时间戳,1643F83394CA24A7是生成这个目录时的系统纳秒时间戳的16进制表示,对应的实现逻辑在源码lib/storage/part.go中;
看到这里,可能会有一些疑问?比如为何要分成big和small目录, 或者说big目录和small中的数据关系是什么? 这个需要从VictoriaMetrics的compaction机制讲起。
在VictoriaMetrics中,small目录和big目录都会周期性检查,是否需要做part的合并。VictoriaMetrics默认每个10ms检查一次partition目录下的part是否需要做merge。如果检查出有满足merge条件的parts,则这些parts合并为一个part。如果没有满足条件的part进行merge,则以10ms为基进行指数休眠,最大休眠时间为10s。
VictoriaMetrics在写数据时,先写入在small目录下的相应partition目录下面的,small目录下的每个partition最多256个part。VictoriaMetrics在Compaction时,默认一次最多合并15个part,且small目录下的part最多包含1000W行数据,即1000W个数据点。因此,当一次待合并的parts中包含的行数超过1000W行时,其合并的输出路径为big目录下的同名partition目录下。
因此,big目录下的数据由small目录下的数据在后台compaction时合并生成的。 那么为什么要分成big目录和small目录呢?
这个主要是从磁盘空间占用上来考虑的。时序数据经常读取最近写入的数据,较少读历史数据。而且,时序数据的数据量比较大,通常会使用压缩算法进行压缩。
数据进行压缩后,读取时需要解压,采用不同级别的压缩压缩算法其解压速度不一样,通常压缩级别越高,其解压速度越慢。在VictoriaMetrics在时序压缩的基础上,又采用了ZSTD这个通用压缩算法进一步压缩了数据,以提高压缩率。在small目录中的part数据,采用的是低级别的ZSTD,而big目录下的数据,采用的是高级别的ZSTD。
因此,VictoriaMetrics分成small目录和big目录,主要是兼顾近期数据的读取和历史数据的压缩率。
3.2 索引目录
索引目录indexdb的具体结构,如下图所示,在图中使用红色文字,对主要目录或文件做了简单说明。与数据目录不同的是,indexdb目录下由多个table目录,每个table目录代表一个完整的自治的索引,每个table目录下,又有多个不同的part目录,part命名方式比较简单,有文件包含的item数itemsCount和block数blocksCount, 以及根据系统纳秒时间戳自增生成的mergeIdx的16进制表示。
![](/img/2021-02-23/index_dir.png)
indexdb下面的形如"1643F4F397B53DEE"是怎么生成的,或者什么时候切换新的目录写索引的呢?VictoriaMetrics会根据用户设置的数据保留周期retention来定期滚动索引目录,当前一个索引目录的保留时间到了,就会切换一个新的目录,重新生成索引。
4. 文件格式
在介绍具体的文件格式之前,不得不提下VictoriaMetrics对于写入数据的处理过程。下图是VictoriaMetrics支持的Prometheus协议的一个写入示例。
VictoriaMetrics在接受到写入请求时,会对请求中包含的时序数据做转换处理,如下图所示。首先先根据包含metric和labels的MetricName生成一个唯一标识TSID,然后metric + labels + TSID作为索引index, TSID + timestamp + value作为数据data,最后索引index和数据data分别进行存储和检索。
因此,VictoriaMetrics的数据整体上分成索引和数据两个部分,因此文件格式整体上会有两个部分。其中,索引部分主要是用于支持按照label或者tag进行多维检索。与大多数时序数据库的数据组织方式一样,比如InfluxDB、Prometheus、OpenTSDB等,VictoriaMetrics也是按时间线来组织数据的,即数据存储时,先将数据按TSID进行分组,然后每个TSID的包含的数据点各自使用列式压缩存储。
4.1 索引文件
VictoriaMetrics每次内存Flush或者后台Merge时生成的索引part,主要包含metaindex.bin、index.bin、lens.bin、items.bin等4个文件。这四个文件的关系如下图所示, metaindex.bin文件通过metaindex_row索引index.bin文件,index.bin文件通过indexBlock中的blockHeader同时索引lens.bin文件和items.bin文件。
metaindex.bin文件中,包含一系列的metaindex_row, 每个metaindex_row中包含最小项firstItem、索引块包含的块头部数blockHeadersCount、索引块偏移indexBlockOffset、索引块大小indexBlockSize。
- metaindex_row在文件中的位置按照firstItem的大小的字典序排序存储,以支持二分检索;
- metaindex.bin文件使用ZSTD进行压缩;
- metaindex.bin文件中的内容在part打开时,会全部读出加载至内存中,以加速查询过滤;
- metaindex_row包含的firstItem为其索引的IndexBlock中所有blockHeader中的字典序最小的firstItem;
- 查找时根据firstItem进行二分检索;
index.bin文件中,包含一系列的indexBlock, 每个indexBlock又包含一系列blockHeader,每个blockHeader的包含item的公共前缀commonPrefix、最小项firstItem、itemsData的序列化类型marshalType、itemsData包含的item数、item块的偏移itemsBlockOffset等内容。
- 每个indexBlock使用ZSTD压缩算法进行压缩;
- 在indexBlock中查找时,根据firstItem进行二分检索blockHeader;
items.bin文件中,包含一系列的itemsData, 每个itemsData又包含一系列的item。
- itemsData会根据情况是否使用ZTSD压缩,当item个数小于2时,或者itemsData的长度小于64字节时,不压缩;当itemsData使用ZSTD压缩后的大小,大于原始itemsData的0.9倍时,则不压缩,否则使用ZSTD算法进行压缩。
- 每个item在存储时,去掉了blockHeader中的公共前缀commonPrefix以提高压缩率。
lens.bin文件中,包含一系列的lensData, 每个lensData又包含一系列8字节的长度len, 长度len标识items.bin文件中对应item的长度。在读取或者需要解析itemsData中的item时,先要读取对应的lensData中对应的长度len。 当itemsData进行压缩时,lensData会先使用异或算法进行压缩,然后再使用ZSTD算法进一步压缩。
VictoriaMetrics索引文件都是围绕着item来组织的,那么item的结构是什么样子的?或者item的种类有哪些? 在VictoriaMetrics中item的整体上是一个KeyValue结构的字节数组,共计有7种类型,每种类型的item通过固定前缀来区分,前缀类型如下图所示。
VictoriaMetrics是怎么基于item支持tag多维检索的呢? 这里就不得不提下VictoriaMetrics的TSID的生成过程。
VictoriaMetrics的MetricName的结构如下所示,包含MetricGroup字节数组和Tag数组,其中,MetricGroup是可选的,每个Tag由Key和Value等字节数组构成。
VictoriaMetrics的TSID的结构如下所示,包含MetricGroupID,JobID,InstanceID,MetricID等四个字段,其中除了MetricID外,其他三个字段都是可选的。这个几个ID的生成方法如下:
- metricGroupID根据MetricName中的MetricGroup使用xxhash的sum64算法生成。
- JobID和InstanceID分别由MetricName中的tag的第一个tag和第二个tag使用xxhash的sum64算法生成。为什么使用第一个tag和第二个tag?这是因为VictoriaMetrics在写入时,将写入请求中的JobID和InstanceID放在了Tag数组的第一个和第二个位置。
- MetricID,使用VictoriaMetrics进程启动时的系统纳秒时间戳自增生成。
因为TSID中除了MetricID外,其他字段都是可选的,因此TSID中可以始终作为有效信息的只有metricID,因此VictoriaMetrics的在构建tag到TSID的字典过程中,是直接存储的tag到metricID的字典。
以写入http_requests_total{status="200", method="GET"}为例,则MetricName为http_requests_total{status="200", method="GET"}, 假设生成的TSID为{metricGroupID=0, jobID=0, instanceID=0, metricID=51106185174286},则VictoriaMetrics在写入时就构建了如下几种类型的索引item,其他类型的索引item是在后台或者查询时构建的。
- metricName -> TSID, 即http_requests_total{status="200", method="GET"} -> {metricGroupID=0, jobID=0, instanceID=0, metricID=51106185174286}
- metricID -> metricName,即51106185174286 -> http_requests_total{status="200", method="GET"}
- metricID -> TSID,即51106185174286 -> {metricGroupID=0, jobID=0, instanceID=0, metricID=51106185174286}
- tag -> metricID,即 status="200" -> 51106185174286, method="GET" -> 51106185174286, "" = http_requests_total -> 51106185174286
有了这些索引的item后,就可以支持基于tag的多维检索了,在当给定查询条件http_requests_total{status="200"}时,VictoriaMetrics先根据给定的tag条件,找出每个tag的metricID列表,然后求所有tag的metricID列表的交集,然后根据交集中的metricID,再到索引文件中检索出TSID,根据TSID就可以到数据文件中查询数据了,在返回结果之前,再根据TSID中的metricID,到索引文件中检索出对应的写入时的原始MetircName。
但是由于VictoriaMetrics的tag到metricID的字典,没有将相同tag的所有metricID放在一起存储,在检索时,一个tag可能需要查询多次才能得到完整的metricID列表。另外查询出metricID后,还要再到索引文件中去检索TSID才能去数据文件查询数据,又增加了一次IO开销。这样来看的话,VictoriaMetrics的索引文件在检索时,如果命中的时间线比较多的情况下,其IO开销会比较大,查询延迟也会比较高。
4.2 数据文件
VictoriaMetrics每次内存Flush或者后台Merge时生成的数据part,包含metaindex.bin、index.bin、timestamps.bin、values.bin等4个文件。这四个文件的关系如下所示。metaindex.bin文件索引index.bin文件,index.bin文件同时索引timestamps.bin和values.bin文件。
metaindex.bin文件中,包含一系列的metaindex_row, 每个metaindex_row中包含时间线标识TSID、最小时间戳minTimestamp、最大时间戳maxTimestamp、索引块偏移indexBlockOffset、索引块大小indexBlockSize、索引块包含的块头部数blockHeadersCount。
- metaindex_row在文件中的位置按照TSID的大小的字典序排序存储;
- metaindex.bin文件使用ZSTD进行压缩;
- metaindex.bin文件中的内容在part打开时,会全部读出加载至内存中,以加速查询过滤;
- metaindex_row包含时间线标识TSID为其索引的IndexBlock中所有blockHeader中的最小时间标识TSID;
- metaindex_row包含最小时间戳minTimestamp为其索引的IndexBlock中所有blockHeader中的最小时间戳minTimestamp;
- metaindex_row包含最大时间戳maxTimestamp为其索引的IndexBlock中所有blockHeader中的最大时间戳maxTimestamp;
- 查找时根据TSID进行二分检索;
index.bin文件中,包含一系列的indexBlock, 每个indexBlock又包含一系列blockHeader,每个blockHeader的包含时间线标识TSID、最小时间戳minTimestamp、最大时间戳maxTimestamp、第一个指标值firstValue、时间戳数据块偏移timestampsBlockOffset、指标值数据块偏移valuesBlockOffset等内容。
- 每个indexBlock使用ZSTD压缩算法进行压缩;
- 查找时,线性遍历blockHeader查找TSID;
timestamps.bin文件中,包含一系列时间线的时间戳压缩块timestampsData; values.bin文件中,包含的一系列时间线的指标值压缩块valuesData。 其中,timestampsData和values.data会根据时序数据特征进行压缩,整体上的压缩思路是:先做时序压缩,然后在做通用压缩。比如,先做delta-of-delta计算或者异或计算,然后根据情况做zig-zag,最后再根据情况做一次ZSTD压缩,VictoriaMetrics支持的压缩算法或者类型主要有6种,如下图所示,压缩编码源码在lib/encoding/encoding.go文件中。
VictoriaMetrics文档中提及在生产环境中,每个数据点(8字节时间戳 + 8字节value共计16字节)压缩后小于1个字节,最高可达 0.4字节,如下所示。
5. VictoriaMetrics总结
VictoriaMetrics的整体的存储设计还是不错的,比如数据时间分区、数据压缩率高、丰富的生态协议等。但VictoriaMetrics的标签索引、数据可靠性、支持的数据类型等方面还存在一些不足。比如,标签索引查询多次IO,可能在时间线数量非常多的场景下,其检索效率会比较低,且没有WAL,在写入时可能会存在数据丢失的风险。目前只支持浮点数类型,不支持布尔、字符串、字节数组等类型。