在Java中,Influxdb2 写入数据时,线程及内存逐渐飙升的问题及解决方案

在Java中,Influxdb2 写入数据时,线程及内存逐渐飙升的问题及解决方案

influxDB的概念

InfluxDB 2.x 是一个开源的时序数据库,用于存储、查询和可视化时间序列数据。时序数据库是一种特殊的数据库,它专门用于处理按时间顺序排列的数据,例如工业传感器数据、应用程序日志等等。

InfluxDB 2.x 的数据模型由“桶”(bucket)、“测量”(measurement)、“标签”(tag)和“字段”(field)四个概念组成。其中,“桶”是存储数据的最高层级,相当于关系数据库中的数据库;“测量”相当于关系数据库中的表格,用于保存一类相似的数据;“标签”是用于对数据进行分类和过滤的元数据,类似于关系数据库中的索引;“字段”则用于保存实际的数据值。通过这些概念的组合,InfluxDB 2.x 可以方便地存储和查询时间序列数据。

使用influxDB

引入依赖
<dependency>
  <groupId>com.influxdb</groupId>
  <artifactId>influxdb-client-java</artifactId>
  <version>3.0.1</version>
  <scope>compile</scope>
</dependency>
配置文件influx2.properties,也可以直接写在*.yml中
influx2.url= http://localhost:8086
influx2.org= my_influxdb
influx2.token= BqtcXzzG1SzFzkR9w0bPAsjVKQ8pSxb-szoCFREtY8zPCi2FoNPMqqofTqsv4VDXWwYUgGb4PtHXT73AwymZlQ==
influx2.bucket= iot
读取配置文件
@Component
@ConfigurationProperties(prefix = "influx2")
@PropertySource(value = "influx2.properties")
public class InfluxDBProperties {

    private String url;

    private String token;

    private String org;

    private String bucket;

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public String getOrg() {
        return org;
    }

    public void setOrg(String org) {
        this.org = org;
    }

    public String getBucket() {
        return bucket;
    }

    public void setBucket(String bucket) {
        this.bucket = bucket;
    }

}
influxDB config
@Configuration
public class InfluxDBConfig {

    @Bean
    public InfluxDBClient influxDBClient() {
        return InfluxDBClientFactory.create();
    }
}

操作influxDB

可以直接使用 influxdb-client-java 包,这个包中的InfluxDBClient提供了多种操作influxDB的方法,实现由 Influx API Service 定义的 HTTP API ,如:

WriteApi 实现类,异步非阻塞 API,用于将时间序列数据写入 InfluxDB 2.0

WriteApiBlocking实现类,同步阻塞 API,用于将时间序列数据写入 InfluxDB 2.0

QueryApi实现类,实现查询 HTTP API 端点

DeleteApi实现类,用于从 InfluxDB 2.0 中删除时间序列数据的 API

使用方式
@Autowired
InfluxDBClient influxDBClient;
// 也可以这样
// private InfluxDBClient influxDBClient = SpringUtils.getBean(InfluxDBClient.class);
@Autowired
private InfluxDBProperties properties;
// 查询
StringBuffer buffer = new StringBuffer();
buffer.append("from(bucket: \"" + bucketName + "\") ");
buffer.append("|> range(start:" + start + ", stop:" + stop + ") ");
buffer.append("|> filter(fn: (r) => r._measurement == \"" + tableName + "\") ");
influxDBClient.getQueryApi().query(buffer.toString());
// 删除
DeletePredicateRequest deletePredicateRequest = new DeletePredicateRequest();
deletePredicateRequest.start(LocalDateTime.now().atOffset(ZoneOffset.ofHours(0)).minusDays(5));
deletePredicateRequest.stop(LocalDateTime.now().atOffset(ZoneOffset.ofHours(0)));
influxDBClient.getDeleteApi().delete(deletePredicateRequest, properties.getBucket(), properties.getOrg());
// 单个新增
Point point = Point.measurement(measurement).addFields(fields).time(Instant.now(), WritePrecision.NS);
influxDBClient.getWriteApi().writePoint(point);
// 批量新增
List list = new ArrayList<>();
for(int i=0;i<10000;i++){
	Point point = Point.measurement(measurement).addFields(fields).time(Instant.now(), WritePrecision.NS);
  list.add(point);
}
influxDBClient.getWriteApi().writePoints(list);

以上只是列举了基本的使用,更多使用请参考InfluxDBClient提供的接口。

写入数据时,线程及内存逐渐飙升的问题

定位线程来源

先看代码

  private InfluxDBClient influxDBClient = SpringUtils.getBean(InfluxDBClient.class);
	
	@Override
	public void run() {
		List<Point> list = new ArrayList<>();
		Set<String> devices = service.getCacheSet(host);
		int total = 0;
		for (String device : devices) {
			if (device.indexOf(UserConstants.REDIS_KEY_SEPARATOR) != -1) {
				Map<String, String> data = service.getCacheMap(device);

				Map<String, String> filteredMap = data.entrySet().stream()
						.filter(x -> !x.getKey().startsWith("_")).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

				Point point = InfluxUtils.convertPoint(device, filteredMap);
				total += filteredMap.size();
				list.add(point);
			}
		}
		// 写入influxDB
		influxDBClient.getWriteApi().writePoints(list);
	}

在使用任务对influxDB进行重复新增时,在JVM的线程栈中,出现了大量的 RxNewThreadScheduler 这个线程组,我们对代码进行排查,发现Job构建时,influxdb 插件会将统计数据,通过HTTP请求,存储到influxdb数据库中。Influxdb插件在执行HTTP请求时,利用 OkHttp + RxJava 的方式完成。于是将对 influxdb 插件上报统计数据到influxdb 数据库的关键流程源码做分析:

influxDBClient.getWriteApi().writePoints(list);

进入看源码:

@Nonnull
@Override
public WriteApi getWriteApi() {
	return getWriteApi(WriteOptions.DEFAULTS);
}

获取 influxdb 写入的api,并将统计数据通过api发送 比较关键的就是这个写 API 的配置:WriteOptions.DEFAULTS,我们看下他具体的配置:

/**
* Default configuration with values that are consistent with Telegraf.
*/
public static final WriteOptions DEFAULTS = WriteOptions.builder().build();

// ... 省略其他代码 
public static class Builder {

        private int batchSize = DEFAULT_BATCH_SIZE;
        private int flushInterval = DEFAULT_FLUSH_INTERVAL;
        private int jitterInterval = DEFAULT_JITTER_INTERVAL;
        private int retryInterval = DEFAULT_RETRY_INTERVAL;
        private int maxRetries = DEFAULT_MAX_RETRIES;
        private int maxRetryDelay = DEFAULT_MAX_RETRY_DELAY;
        private int maxRetryTime = DEFAULT_MAX_RETRY_TIME;
        private int exponentialBase = DEFAULT_EXPONENTIAL_BASE;
        private int bufferLimit = DEFAULT_BUFFER_LIMIT;
        private Scheduler writeScheduler = Schedulers.newThread();
        private BackpressureOverflowStrategy backpressureStrategy = BackpressureOverflowStrategy.DROP_OLDEST;

}
 /// ... 省略其他代码 

其中比较关键的是 I/O 线程调度器Scheduler,这个是 RxJava 中提供的,他的实现是Schedulers.newThread(),我们在进入Schedulers.newThread() 看源码:

@NonNull
static final Scheduler NEW_THREAD;
///... 省略其他代码
static {
  // ...
  NEW_THREAD = RxJavaPlugins.initNewThreadScheduler(new NewThreadTask());
}
///... 省略其他代码
@NonNull
public static Scheduler newThread() {
	return RxJavaPlugins.onNewThreadScheduler(NEW_THREAD);
}
///...  省略其他代码
public static Scheduler onNewThreadScheduler(@NonNull Scheduler defaultScheduler) {
        Function<? super Scheduler, ? extends Scheduler> f = onNewThreadHandler;
        if (f == null) {
            return defaultScheduler;
        }
        return apply(f, defaultScheduler);
    }

可以看到,真正的处理逻辑 是交给了 newThreadScheduler 去处理的。newThreadScheduler 的初始化中,创建了一哥NewThreadTask,真正的线程处理逻辑交给他。

static final class NewThreadTask implements Callable<Scheduler> {
  @Override
  public Scheduler call() throws Exception {
  	return NewThreadHolder.DEFAULT;
  }
}

NewThreadTask 实现了Callable 接口并重写了 call 方法,所以真正执行时,会调用该类的 call 方法,而call 方法中,返回的调度器 是NewThreadScheduler 这个调度器。而NewThreadScheduler 这个类 ,正是我们开始排查时在JVM中大量出现的类RxNewThreadScheduler这个调度器,它在真正执行工作的时候,会创建一个NewThreadWorker,NewThreadWorker 所使用的线程池,最终创建出来的是一个最大线程池数量特别巨大的线程池。当Job重复写入时,influxdb的写入量也飙升,而influxdb所用的IO线程调度器RxJava,创建的线程池是几乎没有上限的,这就导致influxdb在写 入量很高时,创建的线程数也多,最终导致线程数飙升。

解决方案

使用同步写入

influxDBClient.getWriteApiBlocking().writePoints(list);
// 或
WriteApiBlocking writeApi = influxDBClient.getWriteApiBlocking();
writeApi.writePoints(list);
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

氵我是大明星

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值