InfluxDB 是一个由 InfluxData 开发的开源时序型数据库。使用 GO 语言开发,特别适合用于处理和分析资源监控数据这种时序相关数据。
那么相比于关系型数据库有何优势?
例如
occupancy_rate,cpu=core4 use=75,free=25
influxDB使用序列的方式去管理数据
图中的foodships为influxdb的measurement,相当于关系型数据库中的表
tags(图中的park_id,planet)相当于关系型数据库中的索引项
图中的#_foodships相当于关系型数据中的未建索引的列
在influxDB中,唯一的measurement、tag_set和filed(一个字段)组合是一个序列
比如tag有手机号,地块id,属性有经度、纬度、设备时间
那么该手机号地块号下的序列应该有三个,分别是经度序列,纬度序列,设备时间序列
假如我们有这样一个场景,我们需要查询一段时间的数据,对于传统的关系型数据库来说,我们很有可能需要多次寻址找到多个元组才能完成查询,而时序数据库是吧索引打到了一批次的数据上,在这种场景下的读写,时序数据库性能是远强于B+树数据库的
现在已经说明了基本的概念和数据的存储,但是还没有提到最重要的时间。在influxdb中,时间也是索引,数据在入库时,会按照时间戳进行排序。这样,我们在进行查询时,一般遵循下面的思路
(1)先指定要从哪个存储桶查询数据
(2)指定数据的时间范围
(3)指定measurement、tag_set和field说明我要查询哪个序列
【示例】
from(bucket: "example_java")
|> range(start:2024-11-01T00:00:00Z, stop:2024-11-03T00:00:00Z)
|> filter(fn: (r) => r["cpu"] == "core1")
在上面的示例中我们看到了第一行指定了从哪个存储桶中查找,第二行指定了查询的数据的时间范围,第三行指定了查询的measurement,tag为cpu,但是因为没有过滤tag条件,我们查询出来了两条序列,而且每一行都是通过|> 符号将上一个函数的输出传递给下一个函数,类似于linux的管道
在range函数中,需要接收两个参数,start和stop,其中stop可以省略,省略的话默认为当前时间
start和stop既可以使用相对的时间区间也可以使用绝对的时间戳,在上面例子中使用的是绝对的时间戳,接下来我们将其换成相对的时间,其格式为时间间隔,如h/m/s
from(bucket: "example_java")
|> range(start:-3d)
|> filter(fn: (r) => r["_measurement"] == "occupancy_rate")
我们可以添加上tag的过滤条件,指定我们想要查询的序列
from(bucket: "example_java")
|> range(start:2024-11-01T00:00:00Z, stop:2024-11-03T00:00:00Z)
|> filter(fn: (r) => r["_measurement"] == "occupancy_rate")
|> filter(fn: (r) => r["cpu"] == "core1")
这次只查询出了cpu为core1的序列
但是假如我们有多个属性,我们用这种方式查询到的会是这种形式
from(bucket: "example_java")
|> range(start:2024-11-01T00:00:00Z, stop:2024-11-03T00:00:00Z)
|> filter(fn: (r) => r["_measurement"] == "occupancy_rate")
|> filter(fn: (r) => r["cpu"] == "core3")
可以看到,现在是长数据的格式,那么如果我们想让数据变得成宽数据格式,可以使用pivot函数
from(bucket: "example_java")
|> range(start:2024-11-01T00:00:00Z, stop:2024-11-03T00:00:00Z)
|> filter(fn: (r) => r["_measurement"] == "occupancy_rate")
|> filter(fn: (r) => r["cpu"] == "core3")
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
其中 rowKey: [“time”]指定哪些列将用来标识唯一的行,通常使用时间戳列 “_time”-可以指定多个列
columnKey: [“_field”]指定哪些列的值将被转换为新的列名 这里指定 “field”,意味着 “_field” 列中的值将变成新的列名
valueColumn: "_value"指定哪一列包含实际的数据值,这些值将填充到新生成的列中
还有一些常见的函数
min(最小值) max(最大值), mean(平均值)
例如
from(bucket: "example_java")
|> range(start:2024-11-01T00:00:00Z, stop:2024-11-03T00:00:00Z)
|> filter(fn: (r) => r["_measurement"] == "occupancy_rate")
|> filter(fn: (r) => r["cpu"] == "core3")
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|> max(column: "free")
1、引入依赖
<dependency>
<groupId>com.influxdbgroupId>
<artifactId>influxdb-client-javaartifactId>
<version>7.2.0version>
dependency>
2、在配置文件中配置参数
spring:
influxdb:
url:
token:
bucket:
measurement:
org:
3、配置类
@Configuration
public class InfluxConfig {
@Value("${spring.influxdb.url}")
private String url;
@Value("${spring.influxdb.token}")
private String token;
@Value("${spring.influxdb.org}")
private String org;
@Value("${spring.influxdb.bucket}")
private String bucket;
@Bean
public InfluxDBClient influxDBClient() {
return InfluxDBClientFactory.create(influxDBClientOptions());
}
@Bean
public InfluxDBClientOptions influxDBClientOptions() {
return InfluxDBClientOptions.builder()
.url(url)
.authenticateToken(token.toCharArray())
.org(org)
.bucket(bucket)
.build();
}
@Bean
public WriteApi writeApi(InfluxDBClient influxDBClient) {
// 异步写的API
return influxDBClient.getWriteApi();
}
@Bean
public QueryApi queryApi(InfluxDBClient influxDBClient){
return influxDBClient.getQueryApi();
}
@Bean
public DeleteApi deleteApi(InfluxDBClient influxDBClient){
return influxDBClient.getDeleteApi();
}
public void closeInfluxDBClient() {
if (influxDBClient() != null) {
influxDBClient().close();
}
}
}
public void test(){
String flux = "occupancy_rate,cpu=core4 use=75,free=25";
writeApi.writeRecord(WritePrecision.NS,flux);
}
@Measurement(name = "occupancy_rate")
public class OccupancyRate {
@Column(name = "use")
private Double use;
@Column(name = "free")
private Double free;
public Double getUse() {
return use;
}
public void setUse(Double use) {
this.use = use;
}
public Double getFree() {
return free;
}
public void setFree(Double free) {
this.free = free;
}
}
public void insertByPOJO(@RequestBody OccupancyRate occupancyRate){
writeApi.writeMeasurement(WritePrecision.NS,occupancyRate);
}
public void insertByPoint(@RequestBody OccupancyRate occupancyRate){
Point point = Point.measurement("occupancy_rate")
.addTag("cpu","core4")
.addField("use",occupancyRate.getUse())
.addField("free",occupancyRate.getFree())
.time(Instant.now(), WritePrecision.NS);
writeApi.writePoint(point);
}
如果想得到同步阻塞写入,可以通过下面方式获取阻塞写入API
WriteApiBlocking writeApiBlocking = influxDBClient.getWriteApiBlocking();
public List<OccupancyRate> query(){
String flux = """
from(bucket: "example_java")
|> range(start:2024-11-02T12:30:40Z, stop:2024-11-03T00:00:00Z)
|> filter(fn: (r) => r["_measurement"] == "occupancy_rate")
|> filter(fn: (r) => r["cpu"] == "core4")
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
""";
List<FluxTable> fluxTables = queryApi.query(flux);
System.out.println(fluxTables);
List<OccupancyRate> occupancyRates = new ArrayList<>();
fluxTables.forEach(table -> {
table.getRecords().forEach(record -> {
OccupancyRate occupancyRate = new OccupancyRate();
occupancyRate.setFree(Double.valueOf(record.getValueByKey("free").toString()));
occupancyRate.setUse(Double.valueOf(record.getValueByKey("use").toString()));
occupancyRates.add(occupancyRate);
});
});
return occupancyRates;
}
public List<OccupancyRate> query(){
String flux = """
from(bucket: "example_java")
|> range(start:2024-11-02T12:30:40Z, stop:2024-11-03T00:00:00Z)
|> filter(fn: (r) => r["_measurement"] == "occupancy_rate")
|> filter(fn: (r) => r["cpu"] == "core4")
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
""";
List<OccupancyRate> result = queryApi.query(flux, OccupancyRate.class);
return result;
}
InfluxDB 2.7 不支持通过字段删除数据。
支持通过以下方式删除数据
当删除请求成功完成后,被删除的数据将不再可查询,但会保留在磁盘上直到压缩服务运行
public void deleteByPhoneNumber(LocalDateTime startTime, LocalDateTime endTime) {
//开始时间和结束时间转换为OffsetDateTime类型
OffsetDateTime start = startTime.atOffset(OffsetDateTime.now().getOffset());
OffsetDateTime stop = endTime.atOffset(OffsetDateTime.now().getOffset());
//删除条件
String predicate = "cpu=core4";
DeletePredicateRequest request = new DeletePredicateRequest()
.predicate(predicate)
.start(start)
.stop(stop);
deleteApi.delete(request, bucket, org);
}
大数据存储下要解决的问题
1、时序数据在降采样后会存在大批量的数据删除
2、单机环境存放大量数据时不能占用过多文件句柄
3、数据存储需要热备份
4、大数据场景下写吞吐量要跟得上
5、存储需要具备良好的压缩性能
数据写入的流程
InfluxDB中的SeriesKey的概念就是通常在时序数据库领域被称为 时间线 的概念
每个SeriesKey代表一条唯一时间线,可以追踪特定指标的历史变化
SeriesKey的组成:
measurement名称 + tag集合(经过排序的tag key-value对)
在influxDB中,能且只能对一个Bucket指定一个Rentention Policy(保留策略),通过RP可以对指定的bucket中保存的时序数据的保留时间(duration)进行设置。一但一个bucket的duration去顶后,那么在该bucket的时序数据将会在这个duration范围内进一步按时间进行分片从而数据分成一个一个的shard为单位进行保存,每个分片是独立的TSM存储
例如:如果在新建bucket在未显式指定保留策略的情况下,默认的Duration为永久,Shard分片时间为7天
WAL是一种数据库常用的持久化机制:
在influxDB中存在两种WAL,一种是WriteWAL,一种是DeleteWAL
WriteWAL
DeleteWAL
Cache 是 WAL 中当前存储的所有数据点的内存副本。这些点按键(即测量值、标签集和唯一字段进行组织。每个字段都按其自己的时间顺序范围保存。Cache 数据在内存中时不进行压缩。
对存储引擎的查询将合并来自缓存的数据和来自 TSM 文件的数据。查询在查询处理时对缓存中的数据副本执行。这样,查询运行时的写入不会影响结果。
(举个例子,比如我图书馆有藏书区和临时防书区,临时放书区存放刚购入的书,假如我查询图书馆某部分的图书,会将临时借书区和藏书区的图书结果合并)
发送到缓存的删除将清除给定的键或给定键的特定时间范围。
Cache 公开了一些快照行为控件。两个最重要的控件是内存限制。有一个下限,cache-snapshot-memory-size当超过该下限时,将触发 TSM 文件的快照并删除相应的 WAL 段。还有一个上限,cache-max-memory-size当超过该上限时,将导致 Cache 拒绝新的写入。这些配置有助于防止内存不足的情况,并对写入数据速度快于实例持久保存数据的客户端施加背压。每次写入时都会检查内存阈值。
比如有一个水箱,有一个下限水位,当水量达到下限水位的时候就往水箱里面注水,当水位达到上限水位后就拒绝水的进入
其他快照控制是基于时间的。空闲阈值,cache-snapshot-write-cold-duration如果在指定的时间间隔内没有收到写入,则强制缓存将快照保存到 TSM 文件。
定时执行
通过重新读取磁盘上的 WAL 文件,在重启时重新创建内存缓存。
快照,将缓存和WAL中的值转换成TSM文件,释放WAL段使用的内存和磁盘空间
级别压缩(分为1-4级):随着TSM文件的增长,TSM 文件从快照压缩为级别 1 文件。多个级别 1 文件被压缩以生成级别 2 文件。该过程持续进行,直到文件达到级别 4(完全压缩)和 TSM 文件的最大大小。
索引优化:当许多 4 级 TSM 文件累积起来时,内部索引会变得更大,访问成本也会更高。索引优化压缩会将系列和索引拆分到一组新的 TSM 文件中,将给定系列的所有点排序到一个 TSM 文件中。在索引优化之前,每个 TSM 文件都包含大多数或所有系列的点,因此每个文件都包含相同的系列索引。索引优化之后,每个 TSM 文件都包含来自最少系列的点,并且文件之间的系列重叠很少。
完全压缩:当分片长时间处于写入冷状态或分片上发生删除时,将运行完全压缩(4 级压缩)。完全压缩会生成一组最佳的 TSM 文件,并包括来自级别和索引优化压缩的所有优化。一旦分片完全压缩,除非存储了新的写入或删除,否则不会在其上运行任何其他压缩。
删除操作的执行过程
1、WAL记录要删除的measurement或series信息,确保系统崩溃后仍能够恢复删除操作
2、立即清除所有与删除操作相关的缓存中的数据
3、对于包含被删除数据的TSM文件创建对应的墓碑文件,墓碑文件记录了哪些数据需要被忽略,删除
在完全压缩发生前触发查询时
1、读取TSM文件数据
2、检查墓碑文件
3、过滤掉被标记删除的数据
4、返回结果
在压缩发生之前,删除的数据是不会从磁盘上物理删除的,等待压缩发生之后,重写TSM文件时会过滤掉删除的数据