Java、Spring Boot 和 MongoDB:性能分析和改进

Java、Spring Boot 和 MongoDB:性能分析和改进

让我们分析一下 Java、Spring Boot、MongoDB 的性能,看看 JDK 在哪里花费的时间最多。我们还将介绍高影响、低变化的性能改进。

AngularAndSpring项目在启动时运行(@Async + @EventListener)或每天一次(@Scheduled)报价的平均计算。它在PrepareDataTask类中实现。它由TaskStarter类在启动时启动。它计算新可用报价的平均值。必须重新计算几年数据的平均报价,以使性能变得有趣。

一切都是在 Linux x64 上使用 Terumium Jdk 17 和 MongoDb 4.4 完成的。

在启动时准备数据

要在应用程序启动时运行计算,TaskStarter 具有以下initAvgs()方法:

@Async
@EventListener(ApplicationReadyEvent.class)
public void initAvgs() {
	log.info("ApplicationReady");
	this.prepareDataTask.createBsAvg();
	this.prepareDataTask.createBfAvg();
	this.prepareDataTask.createIbAvg();
	this.prepareDataTask.createCbHAvg();
}
复制代码

@Async’ 注释在不同的线程上运行该方法,以便启动可以在该方法完成之前完成。

‘ @EventListener(
ApplicationReadyEvent.class)’ 在应用程序开始接受请求之前运行 ApplicationReadEvent 上的方法。

然后按顺序调用方法并处理注释,因为方法是从不同的类调用的。

通过 Cron 准备数据

PrepareDataTask 类有例如这个方法来启动一个平均计算任务:

@Scheduled(cron = "0 10 2 ? * ?")
@SchedulerLock(name = "coinbase_avg_scheduledTask", 
        lockAtLeastFor = "PT1M", lockAtMostFor = "PT23H")
@Timed(value = "create.cb.avg", percentiles = { 0.5, 0.95, 0.99 })
public void createCbHAvg() {
	this.coinbaseService.createCbAvg();
}
复制代码

@Scheduled’ 注释每天在 2.10 点运行该方法。

‘ @SchedulerLock’ 注释使数据库条目停止运行两次的方法。每个锁的名称必须是唯一的。数据库锁确保作业仅在水平缩放的情况下在一个实例上启动。

‘ @Timed’ 注释告诉 mirometer 记录方法运行时间的百分位数。

运行创建平均方法

BitfinexService 、BitstampService、ItbitService、CoinbaseService类有一个开始平均计算的方法,这里显示的是 Coinbase:createAvg

public void createCbAvg() {
	LocalDateTime start = LocalDateTime.now();
	log.info("CpuConstraint property: " + this.cpuConstraint);
	if (this.cpuConstraint) {
		this.createCbHourlyAvg();
		this.createCbDailyAvg();
		log.info(this.serviceUtils.createAvgLogStatement(start, 
                        "Prepared Coinbase Data Time:"));
	} else {
		// This can only be used on machines without 
                // cpu constraints.
		CompletableFuture<String> future7 = CompletableFuture
                        .supplyAsync(() -> {
			       this.createCbHourlyAvg();
   			       return "createCbHourlyAvg() Done.";
		        }, CompletableFuture.
                               delayedExecutor(10, TimeUnit.SECONDS));
		CompletableFuture<String> future8 = CompletableFuture.
                        supplyAsync(() -> {
				this.createCbDailyAvg();
				return "createCbDailyAvg() Done.";
			}, CompletableFuture.
                                delayedExecutor(10, TimeUnit.SECONDS));
		String combined = Stream.of(future7, future8)
                        .map(CompletableFuture::join)
                        .collect(Collectors.joining(" "));
		log.info(combined);
	}
}
复制代码

首先cpuConstraint记录并检查该属性。它由环境变量“ ”在application.propertiesCPU_CONSTRAINT文件中设置,默认为“false”。在应用程序可用的 CPU 少于 2 个的 Kubernetes 部署中,它应该设置为 true。

如果该cpuConstraint属性设置为 true,则 ‘ createCbHourlyAvg()’ 和 ‘ createCbDailyAvg()’ 方法将依次运行以减少 cpu 负载。

如果该cpuConstraint属性设置为 false,则 ‘ createCbHourlyAvg()’ 和 ‘ createCbDailyAvg()’ 方法同时运行CompletableFutures。DelayedExecutor用于给 MongoDb 几秒钟的时间在工作之间安定下来。

‘ Stream’ 用于等待CompletableFutures和连接它们的结果。

然后记录结果。

计算平均值

BitfinexService、BitstampService、ItbitService、CoinbaseService 类有 create??Avg 方法。以 CoinbaseService的 ‘ createCbHourlyAvg()’ 为例:

private void createCbHourlyAvg() {
	LocalDateTime startAll = LocalDateTime.now();
	MyTimeFrame timeFrame = this.serviceUtils.
                createTimeFrame(CB_HOUR_COL, QuoteCb.class, true);
	SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy");
	Calendar now = Calendar.getInstance();
        now.setTime(Date.from(LocalDate.now()
                .atStartOfDay().atZone(ZoneId.
                 systemDefault()).toInstant()));
	while (timeFrame.end().before(now)) {
		Date start = new Date();
		Query query = new Query();
		query.addCriteria(				 
                        Criteria.where("createdAt").gt(timeFrame.
                        begin().getTime()).
                        lt(timeFrame.end().getTime()));
		// Coinbase
		Mono<Collection<QuoteCb>> collectCb = 
                        this.myMongoRepository.
                        find(query, QuoteCb.class).collectList()
                             .map(quotes -> 
                                      makeCbQuoteHour(quotes, 
                                      timeFrame.begin(),
                                      timeFrame.end()));
			this.myMongoRepository.insertAll(collectCb, 
                              CB_HOUR_COL).blockLast();
		timeFrame.begin().add(Calendar.DAY_OF_YEAR, 1);
		timeFrame.end().add(Calendar.DAY_OF_YEAR, 1);
		log.info("Prepared Coinbase Hour Data for: " + 
                sdf.format(timeFrame.begin().getTime()) + " Time: "
                        + (new Date().getTime() - start.getTime()) 
                        + "ms");
	}
	log.info(this.serviceUtils.createAvgLogStatement(startAll,
                "Prepared Coinbase Hourly Data Time:"));
}
复制代码

Java、Spring Boot 和 MongoDB:性能分析和改进

原创2022-03-09 14:31·橙橙C

Java、Spring Boot 和 MongoDB:性能分析和改进

让我们分析一下 Java、Spring Boot、MongoDB 的性能,看看 JDK 在哪里花费的时间最多。我们还将介绍高影响、低变化的性能改进。

AngularAndSpring项目在启动时运行(@Async + @EventListener)或每天一次(@Scheduled)报价的平均计算。它在PrepareDataTask类中实现。它由TaskStarter类在启动时启动。它计算新可用报价的平均值。必须重新计算几年数据的平均报价,以使性能变得有趣。

一切都是在 Linux x64 上使用 Terumium Jdk 17 和 MongoDb 4.4 完成的。

在启动时准备数据

要在应用程序启动时运行计算,TaskStarter 具有以下initAvgs()方法:

@Async
@EventListener(ApplicationReadyEvent.class)
public void initAvgs() {
	log.info("ApplicationReady");
	this.prepareDataTask.createBsAvg();
	this.prepareDataTask.createBfAvg();
	this.prepareDataTask.createIbAvg();
	this.prepareDataTask.createCbHAvg();
}
复制代码

@Async’ 注释在不同的线程上运行该方法,以便启动可以在该方法完成之前完成。

‘ @EventListener(
ApplicationReadyEvent.class)’ 在应用程序开始接受请求之前运行 ApplicationReadEvent 上的方法。

然后按顺序调用方法并处理注释,因为方法是从不同的类调用的。

通过 Cron 准备数据

PrepareDataTask 类有例如这个方法来启动一个平均计算任务:

@Scheduled(cron = "0 10 2 ? * ?")
@SchedulerLock(name = "coinbase_avg_scheduledTask", 
        lockAtLeastFor = "PT1M", lockAtMostFor = "PT23H")
@Timed(value = "create.cb.avg", percentiles = { 0.5, 0.95, 0.99 })
public void createCbHAvg() {
	this.coinbaseService.createCbAvg();
}
复制代码

@Scheduled’ 注释每天在 2.10 点运行该方法。

‘ @SchedulerLock’ 注释使数据库条目停止运行两次的方法。每个锁的名称必须是唯一的。数据库锁确保作业仅在水平缩放的情况下在一个实例上启动。

‘ @Timed’ 注释告诉 mirometer 记录方法运行时间的百分位数。

运行创建平均方法

BitfinexService 、BitstampService、ItbitService、CoinbaseService类有一个开始平均计算的方法,这里显示的是 Coinbase:createAvg

public void createCbAvg() {
	LocalDateTime start = LocalDateTime.now();
	log.info("CpuConstraint property: " + this.cpuConstraint);
	if (this.cpuConstraint) {
		this.createCbHourlyAvg();
		this.createCbDailyAvg();
		log.info(this.serviceUtils.createAvgLogStatement(start, 
                        "Prepared Coinbase Data Time:"));
	} else {
		// This can only be used on machines without 
                // cpu constraints.
		CompletableFuture<String> future7 = CompletableFuture
                        .supplyAsync(() -> {
			       this.createCbHourlyAvg();
   			       return "createCbHourlyAvg() Done.";
		        }, CompletableFuture.
                               delayedExecutor(10, TimeUnit.SECONDS));
		CompletableFuture<String> future8 = CompletableFuture.
                        supplyAsync(() -> {
				this.createCbDailyAvg();
				return "createCbDailyAvg() Done.";
			}, CompletableFuture.
                                delayedExecutor(10, TimeUnit.SECONDS));
		String combined = Stream.of(future7, future8)
                        .map(CompletableFuture::join)
                        .collect(Collectors.joining(" "));
		log.info(combined);
	}
}
复制代码

首先cpuConstraint记录并检查该属性。它由环境变量“ ”在application.propertiesCPU_CONSTRAINT文件中设置,默认为“false”。在应用程序可用的 CPU 少于 2 个的 Kubernetes 部署中,它应该设置为 true。

如果该cpuConstraint属性设置为 true,则 ‘ createCbHourlyAvg()’ 和 ‘ createCbDailyAvg()’ 方法将依次运行以减少 cpu 负载。

如果该cpuConstraint属性设置为 false,则 ‘ createCbHourlyAvg()’ 和 ‘ createCbDailyAvg()’ 方法同时运行CompletableFutures。DelayedExecutor用于给 MongoDb 几秒钟的时间在工作之间安定下来。

‘ Stream’ 用于等待CompletableFutures和连接它们的结果。

然后记录结果。

计算平均值

BitfinexService、BitstampService、ItbitService、CoinbaseService 类有 create??Avg 方法。以 CoinbaseService的 ‘ createCbHourlyAvg()’ 为例:

private void createCbHourlyAvg() {
	LocalDateTime startAll = LocalDateTime.now();
	MyTimeFrame timeFrame = this.serviceUtils.
                createTimeFrame(CB_HOUR_COL, QuoteCb.class, true);
	SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy");
	Calendar now = Calendar.getInstance();
        now.setTime(Date.from(LocalDate.now()
                .atStartOfDay().atZone(ZoneId.
                 systemDefault()).toInstant()));
	while (timeFrame.end().before(now)) {
		Date start = new Date();
		Query query = new Query();
		query.addCriteria(				 
                        Criteria.where("createdAt").gt(timeFrame.
                        begin().getTime()).
                        lt(timeFrame.end().getTime()));
		// Coinbase
		Mono<Collection<QuoteCb>> collectCb = 
                        this.myMongoRepository.
                        find(query, QuoteCb.class).collectList()
                             .map(quotes -> 
                                      makeCbQuoteHour(quotes, 
                                      timeFrame.begin(),
                                      timeFrame.end()));
			this.myMongoRepository.insertAll(collectCb, 
                              CB_HOUR_COL).blockLast();
		timeFrame.begin().add(Calendar.DAY_OF_YEAR, 1);
		timeFrame.end().add(Calendar.DAY_OF_YEAR, 1);
		log.info("Prepared Coinbase Hour Data for: " + 
                sdf.format(timeFrame.begin().getTime()) + " Time: "
                        + (new Date().getTime() - start.getTime()) 
                        + "ms");
	}
	log.info(this.serviceUtils.createAvgLogStatement(startAll,
                "Prepared Coinbase Hourly Data Time:"));
}
复制代码

‘ createTimeFrame(…)’ 方法查找集合中的最后一个平均小时文档或报价集合中的第一个条目,并返回第一天以计算平均值。

在 while 循环中,计算当天的每小时平均值。首先,为当天的“createdAt”时间范围设置搜索条件。’createdAt’ 属性有一个索引来提高搜索性能。该项目使用响应式 MongoDb 驱动程序。因此,’find(…).collectList()’ 方法返回一个Mono<Collection>(Spring Reactor)映射到平均值的引号。

然后将该 Mono 与“ insertAll(…).blockLast()”一起存储。’ blockLast()’ 启动反应流并确保存储平均值。

然后将“timeFrame”设置为第二天并写入日志条目。

使用大型 Pojo

QuoteCb类如下所示`:

@Document
public class QuoteCb implements Quote {
	
	@Id
	private ObjectId _id;
	@Indexed
	@JsonProperty
	private Date createdAt = new Date();

	private final BigDecimal aed;
	private final BigDecimal afn;
	private final BigDecimal all;
	private final BigDecimal amd;
        ... 
        // 150 properties more
复制代码

Pojo 用作 MongoDb ‘@Document’ 和 ‘@Id’ 和 ‘@Indexed’ ‘createdAt’。它有 150 多个“BigDecimal”属性和一个构造函数来设置它们。为了避免必须编写映射器来获取和设置值,CoinbaseService 类具有 ‘ avgCbQuotePeriod(…)’ 和 ‘ createGetMethodHandle(…)’ 方法:

private QuoteCb avgCbQuotePeriod(QuoteCb q1, QuoteCb q2, long count) {
	Class[] types = new Class[170];
	for (int i = 0; i < 170; i++) {
		types[i] = BigDecimal.class;
	}
	QuoteCb result = null;
	try {
             BigDecimal[] bds = new BigDecimal[170];
	     IntStream.range(0, QuoteCb.class.getConstructor(types)
                .getParameterAnnotations().length)
		.forEach(x -> {
 		    try {
                       MethodHandle mh = createGetMethodHandle(types, x);
		       BigDecimal num1 = (BigDecimal) mh.invokeExact(q1);
		       BigDecimal num2 = (BigDecimal) mh.invokeExact(q2);
		       bds[x] = this.serviceUtils
                           .avgHourValue(num1, num2, count);
		    } catch (Throwable e) {
			throw new RuntimeException(e);
		    }
                });
	    result = QuoteCb.class.getConstructor(types)
               .newInstance((Object[]) bds);
 	    result.setCreatedAt(q1.getCreatedAt());
	} catch (NoSuchMethodException | SecurityException | 
           InstantiationException | IllegalAccessException
	   | IllegalArgumentException | InvocationTargetException e) {
 	   throw new RuntimeException(e);
	}
	return result;
}

private MethodHandle createGetMethodHandle(Class[] types, int x)
   throws NoSuchMethodException, IllegalAccessException {
   MethodHandle mh = cbMethodCache.get(Integer.valueOf(x));
   if (mh == null) {
      synchronized (this) {
	 mh = cbMethodCache.get(Integer.valueOf(x));
	 if (mh == null) {
	   JsonProperty annotation = (JsonProperty) QuoteCb.class.
              getConstructor(types).getParameterAnnotations()[x][0];
	   String fieldName = annotation.value();
	   String methodName = String.format("get%s%s",
		fieldName.substring(0, 1).toUpperCase(),
		fieldName.substring(1).toLowerCase());
	   if ("getTry".equals(methodName)) {
	        methodName = methodName + "1";
	   }
	   MethodType desc = MethodType
                  .methodType(BigDecimal.class);
	   mh = MethodHandles.lookup().findVirtual(QuoteCb.class, 
                  methodName, desc);
	   cbMethodCache.put(Integer.valueOf(x), mh);
  	 }
      }
   }
   return mh;
}
复制代码

首先,为 ‘QuoteCb’ 类的构造函数创建类型数组。然后创建构造函数参数的“BigDecimal”数组。然后 ‘ foreach(…)’ 遍历 ‘QuoteCb’ 类吸气剂。

在 ‘ createGetMethodHandle(…)’ 方法中,构造函数参数的 getter 方法句柄被返回或创建。方法句柄缓存在静态ConcurrentHashMap中,因为它们仅在同步块中创建一次(每小时和每天的平均值同时执行)。

然后使用方法句柄来获取两个 Pojos 的值,并使用 ‘ serviceUtils.avgHourValue(…)’ 方法计算平均值。然后将该值存储在构造函数参数数组中。

使用方法句柄的值访问非常快。在构造函数中有这么多参数的对象创建对 cpu 负载的影响非常小。

其他 Pojos 只有一个充满参数的手,并且使用普通的构造函数调用和 getter 调用进行计算,就像在 BitstampService 中使用 QuoteBs 的 ‘ avgQuote(…)’ 方法完成的那样:

private QuoteBs avgBsQuote(QuoteBs q1, QuoteBs q2, long count) {
   QuoteBs myQuote = new QuoteBs(
      this.serviceUtils.avgHourValue(q1.getHigh(), q2.getHigh(), count),
      this.serviceUtils.avgHourValue(q1.getLast(), q2.getLast(), count),
      q1.getTimestamp(),
      this.serviceUtils.avgHourValue(q1.getBid(), q2.getBid(), count),
      this.serviceUtils.avgHourValue(q1.getVwap(), q2.getVwap(), count),
      this.serviceUtils.avgHourValue(q1.getVolume(), q2.getVolume(), 
         count),
      this.serviceUtils.avgHourValue(q1.getLow(), q2.getLow(), count),
      this.serviceUtils.avgHourValue(q1.getAsk(), q2.getAsk(), count),
      this.serviceUtils.avgHourValue(q1.getOpen(), q2.getOpen(), count));
   myQuote.setCreatedAt(q1.getCreatedAt());
   return myQuote;
}
复制代码

实施结论

@AsyncSpring Boot 支持在应用程序启动时使用注释“ ”和“ ”轻松启动平均计算运行@EventListener。’ @Scheduled’ 注释使创建 cron 作业变得容易,而带有 ‘ ‘ 注释的ShedLock库@SchedulerLock可以水平扩展运行 cron/启动作业的应用程序。Spring Boot 和 MongoDb 驱动程序的反应特性使得将 db 数据从 finder 流向 mapper 到 insertAll 成为可能。

Helm Chart 中的 Kubernetes 设置扩展

Kubernetes 集群的 Minikube 设置可以在 minikubeSetup.sh 中找到。环境变量“ CPU_CONSTRAINT”在values.yaml中设置。kubTemplate.yaml中的 cpu 和内存限制已更新:

limits:
  memory: "3G"
  cpu: "0.6"
requests:
  memory: "1G"
  cpu: "0.3"
复制代码

Java、Spring Boot 和 MongoDB:性能分析和改进

原创2022-03-09 14:31·橙橙C

Java、Spring Boot 和 MongoDB:性能分析和改进

让我们分析一下 Java、Spring Boot、MongoDB 的性能,看看 JDK 在哪里花费的时间最多。我们还将介绍高影响、低变化的性能改进。

AngularAndSpring项目在启动时运行(@Async + @EventListener)或每天一次(@Scheduled)报价的平均计算。它在PrepareDataTask类中实现。它由TaskStarter类在启动时启动。它计算新可用报价的平均值。必须重新计算几年数据的平均报价,以使性能变得有趣。

一切都是在 Linux x64 上使用 Terumium Jdk 17 和 MongoDb 4.4 完成的。

在启动时准备数据

要在应用程序启动时运行计算,TaskStarter 具有以下initAvgs()方法:

@Async
@EventListener(ApplicationReadyEvent.class)
public void initAvgs() {
	log.info("ApplicationReady");
	this.prepareDataTask.createBsAvg();
	this.prepareDataTask.createBfAvg();
	this.prepareDataTask.createIbAvg();
	this.prepareDataTask.createCbHAvg();
}
复制代码

@Async’ 注释在不同的线程上运行该方法,以便启动可以在该方法完成之前完成。

‘ @EventListener(
ApplicationReadyEvent.class)’ 在应用程序开始接受请求之前运行 ApplicationReadEvent 上的方法。

然后按顺序调用方法并处理注释,因为方法是从不同的类调用的。

通过 Cron 准备数据

PrepareDataTask 类有例如这个方法来启动一个平均计算任务:

@Scheduled(cron = "0 10 2 ? * ?")
@SchedulerLock(name = "coinbase_avg_scheduledTask", 
        lockAtLeastFor = "PT1M", lockAtMostFor = "PT23H")
@Timed(value = "create.cb.avg", percentiles = { 0.5, 0.95, 0.99 })
public void createCbHAvg() {
	this.coinbaseService.createCbAvg();
}
复制代码

@Scheduled’ 注释每天在 2.10 点运行该方法。

‘ @SchedulerLock’ 注释使数据库条目停止运行两次的方法。每个锁的名称必须是唯一的。数据库锁确保作业仅在水平缩放的情况下在一个实例上启动。

‘ @Timed’ 注释告诉 mirometer 记录方法运行时间的百分位数。

运行创建平均方法

BitfinexService 、BitstampService、ItbitService、CoinbaseService类有一个开始平均计算的方法,这里显示的是 Coinbase:createAvg

public void createCbAvg() {
	LocalDateTime start = LocalDateTime.now();
	log.info("CpuConstraint property: " + this.cpuConstraint);
	if (this.cpuConstraint) {
		this.createCbHourlyAvg();
		this.createCbDailyAvg();
		log.info(this.serviceUtils.createAvgLogStatement(start, 
                        "Prepared Coinbase Data Time:"));
	} else {
		// This can only be used on machines without 
                // cpu constraints.
		CompletableFuture<String> future7 = CompletableFuture
                        .supplyAsync(() -> {
			       this.createCbHourlyAvg();
   			       return "createCbHourlyAvg() Done.";
		        }, CompletableFuture.
                               delayedExecutor(10, TimeUnit.SECONDS));
		CompletableFuture<String> future8 = CompletableFuture.
                        supplyAsync(() -> {
				this.createCbDailyAvg();
				return "createCbDailyAvg() Done.";
			}, CompletableFuture.
                                delayedExecutor(10, TimeUnit.SECONDS));
		String combined = Stream.of(future7, future8)
                        .map(CompletableFuture::join)
                        .collect(Collectors.joining(" "));
		log.info(combined);
	}
}
复制代码

首先cpuConstraint记录并检查该属性。它由环境变量“ ”在application.propertiesCPU_CONSTRAINT文件中设置,默认为“false”。在应用程序可用的 CPU 少于 2 个的 Kubernetes 部署中,它应该设置为 true。

如果该cpuConstraint属性设置为 true,则 ‘ createCbHourlyAvg()’ 和 ‘ createCbDailyAvg()’ 方法将依次运行以减少 cpu 负载。

如果该cpuConstraint属性设置为 false,则 ‘ createCbHourlyAvg()’ 和 ‘ createCbDailyAvg()’ 方法同时运行CompletableFutures。DelayedExecutor用于给 MongoDb 几秒钟的时间在工作之间安定下来。

‘ Stream’ 用于等待CompletableFutures和连接它们的结果。

然后记录结果。

计算平均值

BitfinexService、BitstampService、ItbitService、CoinbaseService 类有 create??Avg 方法。以 CoinbaseService的 ‘ createCbHourlyAvg()’ 为例:

private void createCbHourlyAvg() {
	LocalDateTime startAll = LocalDateTime.now();
	MyTimeFrame timeFrame = this.serviceUtils.
                createTimeFrame(CB_HOUR_COL, QuoteCb.class, true);
	SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy");
	Calendar now = Calendar.getInstance();
        now.setTime(Date.from(LocalDate.now()
                .atStartOfDay().atZone(ZoneId.
                 systemDefault()).toInstant()));
	while (timeFrame.end().before(now)) {
		Date start = new Date();
		Query query = new Query();
		query.addCriteria(				 
                        Criteria.where("createdAt").gt(timeFrame.
                        begin().getTime()).
                        lt(timeFrame.end().getTime()));
		// Coinbase
		Mono<Collection<QuoteCb>> collectCb = 
                        this.myMongoRepository.
                        find(query, QuoteCb.class).collectList()
                             .map(quotes -> 
                                      makeCbQuoteHour(quotes, 
                                      timeFrame.begin(),
                                      timeFrame.end()));
			this.myMongoRepository.insertAll(collectCb, 
                              CB_HOUR_COL).blockLast();
		timeFrame.begin().add(Calendar.DAY_OF_YEAR, 1);
		timeFrame.end().add(Calendar.DAY_OF_YEAR, 1);
		log.info("Prepared Coinbase Hour Data for: " + 
                sdf.format(timeFrame.begin().getTime()) + " Time: "
                        + (new Date().getTime() - start.getTime()) 
                        + "ms");
	}
	log.info(this.serviceUtils.createAvgLogStatement(startAll,
                "Prepared Coinbase Hourly Data Time:"));
}
复制代码

‘ createTimeFrame(…)’ 方法查找集合中的最后一个平均小时文档或报价集合中的第一个条目,并返回第一天以计算平均值。

在 while 循环中,计算当天的每小时平均值。首先,为当天的“createdAt”时间范围设置搜索条件。’createdAt’ 属性有一个索引来提高搜索性能。该项目使用响应式 MongoDb 驱动程序。因此,’find(…).collectList()’ 方法返回一个Mono<Collection>(Spring Reactor)映射到平均值的引号。

然后将该 Mono 与“ insertAll(…).blockLast()”一起存储。’ blockLast()’ 启动反应流并确保存储平均值。

然后将“timeFrame”设置为第二天并写入日志条目。

使用大型 Pojo

QuoteCb类如下所示:

@Document
public class QuoteCb implements Quote {
	
	@Id
	private ObjectId _id;
	@Indexed
	@JsonProperty
	private Date createdAt = new Date();

	private final BigDecimal aed;
	private final BigDecimal afn;
	private final BigDecimal all;
	private final BigDecimal amd;
        ... 
        // 150 properties more
复制代码

Pojo 用作 MongoDb ‘@Document’ 和 ‘@Id’ 和 ‘@Indexed’ ‘createdAt’。它有 150 多个“BigDecimal”属性和一个构造函数来设置它们。为了避免必须编写映射器来获取和设置值,CoinbaseService 类具有 ‘ avgCbQuotePeriod(…)’ 和 ‘ createGetMethodHandle(…)’ 方法:

private QuoteCb avgCbQuotePeriod(QuoteCb q1, QuoteCb q2, long count) {
	Class[] types = new Class[170];
	for (int i = 0; i < 170; i++) {
		types[i] = BigDecimal.class;
	}
	QuoteCb result = null;
	try {
             BigDecimal[] bds = new BigDecimal[170];
	     IntStream.range(0, QuoteCb.class.getConstructor(types)
                .getParameterAnnotations().length)
		.forEach(x -> {
 		    try {
                       MethodHandle mh = createGetMethodHandle(types, x);
		       BigDecimal num1 = (BigDecimal) mh.invokeExact(q1);
		       BigDecimal num2 = (BigDecimal) mh.invokeExact(q2);
		       bds[x] = this.serviceUtils
                           .avgHourValue(num1, num2, count);
		    } catch (Throwable e) {
			throw new RuntimeException(e);
		    }
                });
	    result = QuoteCb.class.getConstructor(types)
               .newInstance((Object[]) bds);
 	    result.setCreatedAt(q1.getCreatedAt());
	} catch (NoSuchMethodException | SecurityException | 
           InstantiationException | IllegalAccessException
	   | IllegalArgumentException | InvocationTargetException e) {
 	   throw new RuntimeException(e);
	}
	return result;
}

private MethodHandle createGetMethodHandle(Class[] types, int x)
   throws NoSuchMethodException, IllegalAccessException {
   MethodHandle mh = cbMethodCache.get(Integer.valueOf(x));
   if (mh == null) {
      synchronized (this) {
	 mh = cbMethodCache.get(Integer.valueOf(x));
	 if (mh == null) {
	   JsonProperty annotation = (JsonProperty) QuoteCb.class.
              getConstructor(types).getParameterAnnotations()[x][0];
	   String fieldName = annotation.value();
	   String methodName = String.format("get%s%s",
		fieldName.substring(0, 1).toUpperCase(),
		fieldName.substring(1).toLowerCase());
	   if ("getTry".equals(methodName)) {
	        methodName = methodName + "1";
	   }
	   MethodType desc = MethodType
                  .methodType(BigDecimal.class);
	   mh = MethodHandles.lookup().findVirtual(QuoteCb.class, 
                  methodName, desc);
	   cbMethodCache.put(Integer.valueOf(x), mh);
  	 }
      }
   }
   return mh;
}
复制代码

首先,为 ‘QuoteCb’ 类的构造函数创建类型数组。然后创建构造函数参数的“BigDecimal”数组。然后 ‘ foreach(…)’ 遍历 ‘QuoteCb’ 类吸气剂。

在 ‘ createGetMethodHandle(…)’ 方法中,构造函数参数的 getter 方法句柄被返回或创建。方法句柄缓存在静态ConcurrentHashMap中,因为它们仅在同步块中创建一次(每小时和每天的平均值同时执行)。

然后使用方法句柄来获取两个 Pojos 的值,并使用 ‘ serviceUtils.avgHourValue(…)’ 方法计算平均值。然后将该值存储在构造函数参数数组中。

使用方法句柄的值访问非常快。在构造函数中有这么多参数的对象创建对 cpu 负载的影响非常小。

其他 Pojos 只有一个充满参数的手,并且使用普通的构造函数调用和 getter 调用进行计算,就像在 BitstampService 中使用 QuoteBs 的 ‘ avgQuote(…)’ 方法完成的那样:

private QuoteBs avgBsQuote(QuoteBs q1, QuoteBs q2, long count) {
   QuoteBs myQuote = new QuoteBs(
      this.serviceUtils.avgHourValue(q1.getHigh(), q2.getHigh(), count),
      this.serviceUtils.avgHourValue(q1.getLast(), q2.getLast(), count),
      q1.getTimestamp(),
      this.serviceUtils.avgHourValue(q1.getBid(), q2.getBid(), count),
      this.serviceUtils.avgHourValue(q1.getVwap(), q2.getVwap(), count),
      this.serviceUtils.avgHourValue(q1.getVolume(), q2.getVolume(), 
         count),
      this.serviceUtils.avgHourValue(q1.getLow(), q2.getLow(), count),
      this.serviceUtils.avgHourValue(q1.getAsk(), q2.getAsk(), count),
      this.serviceUtils.avgHourValue(q1.getOpen(), q2.getOpen(), count));
   myQuote.setCreatedAt(q1.getCreatedAt());
   return myQuote;
}
复制代码

实施结论

@AsyncSpring Boot 支持在应用程序启动时使用注释“ ”和“ ”轻松启动平均计算运行@EventListener。’ @Scheduled’ 注释使创建 cron 作业变得容易,而带有 ‘ ‘ 注释的ShedLock库@SchedulerLock可以水平扩展运行 cron/启动作业的应用程序。Spring Boot 和 MongoDb 驱动程序的反应特性使得将 db 数据从 finder 流向 mapper 到 insertAll 成为可能。

Helm Chart 中的 Kubernetes 设置扩展

Kubernetes 集群的 Minikube 设置可以在 minikubeSetup.sh 中找到。环境变量“ CPU_CONSTRAINT”在values.yaml中设置。kubTemplate.yaml中的 cpu 和内存限制已更新:

limits:
  memory: "3G"
  cpu: "0.6"
requests:
  memory: "1G"
  cpu: "0.3"
复制代码

对于 MongoDb 部署。

limits:
  memory: "768M"
  cpu: "1.4"
requests:
  memory: "256M"
  cpu: "0.5"
复制代码

对于AngularAndSpring项目部署,由于这些 cpu 限制,MongoDb 永远不会达到其 cpu 限制。

表现

平均计算每晚使用调度程序运行,并且有最后几天的数据数据要处理。它在几秒钟或更短的时间内完成。调度器有自己的线程池,不会干扰用户的请求。在重新计算超过 3 年数据的平均值后,性能变得有趣。Bitstamp 和 Coinbase 报价具有不同的结构,因此比较它们的平均计算性能很有趣。两个数据集都有日期索引,并且一天的所有报价都被查询一次。

Coinbase 数据集

Coinbase pojo对于不同的货币有超过 150 个 BigDecimal 值。每分钟有一个 pojo。每天1440 pojos。

位戳数据集

Bitstamp pojo对于一种货币有 8 个 BigDecimal 值。每分钟有 8 个 pojo。每天 11520 个 pojo。

原始性能

在一台有 4 核的机器上,有足够的内存可供 Jdk 和 MongoDb 使用,每日和每小时的平均计算可以与单独运行大致相同的时间同时运行。

  • Bitstamp 同时 Java CpuCore 100-140% MongoDb CpuCore 40-50% 780 秒。
  • Bitstamp 仅每小时 Java CpuCore 60-70% MongoDb CpuCore ~20% 790 秒。
  • Coinbase 同时 Java CpuCore 160-190% MongoDb CpuCore ~10% 1680 秒。
  • Coinbase 仅每小时 Java CpuCore 90-100% MongoDb CpuCore ~5% 1620 秒。

具有更多值的 Coinbase pojo 似乎对 Jdk 核心施加了更多负载,而 Bitstamp 数据集的大量 pojo 似乎对 MongoDb 核心施加了更多负载。

Coinbase Pojo 性能瓶颈

Coinbase 导入是最慢的,分析器显示虚拟机使用了大约 512 MB 的可用内存并且没有内存压力。G1 垃圾收集器的暂停时间小于 100 毫秒,内存图表看起来很正常。按方法花费的 cpu 时间显示 60% 的时间花费在创建 BigDecimal 对象上,25% 的时间花费在划分它们上。所有其他值均低于 5%。内存快照显示内存中接近最大数量的 300 万个 BigDecimal 对象(~120 MB)。它们每隔几秒就被收集一次,没有明显的 gc 暂停。

原始性能的结论

MongoDb 没有 I/O 或 Cpu 和 2 GB 缓存的限制。由于 BigDecimal 类的大量创建/计算,Jdk 在 Coinbase 计算中处于 cpu 限制。G1 gc 没有显示任何问题。具有 150 多个参数的构造函数不是问题。

Kubernetes 中受限的资源性能

要查看在内存和 cpu 限制下性能如何,项目和 MongoDb 在 minikube 集群中运行,jdk 具有 1.4 cpucores 和 768 Mb 内存,MongoDb 具有 0.6 cpucores 和 3 Gb。平均计算按预期执行较慢,但 Coinbase 计算使 jdk 的 cpu 资源不足,以至于 Shedlock 库无法及时更新数据库锁(10 秒)。因此,在application.propertiesCPU_CONSTRAINT中检查了“”环境变量以从并发计算切换到顺序计算。

结论

平均计算使用了太多的 BigDecimal 来加快速度。查询大型数据集的每日报价也不高效。在正常操作下,这两个瓶颈都不是问题,如果需要完全重新计算平均值,它就可以很好地工作。G1 垃圾收集器的性能很好。调查性能瓶颈非常有趣,并为性能关键代码提供了见解。结果表明,性能瓶颈可能在一个令人惊讶的地方,并且像 150+ 参数构造函数这样的猜测并不相关。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享