如何预热Spring Boot应用? - sebstein


Spring Boot 是用于开发 Java 和 Kotlin 后端的成熟工具。如果您重新启动这样的后端,第一个 REST 调用总是需要很长时间。我研究了为什么会这样,以及如何在应用程序启动时对其进行预热,以便快速处理第一个请求。
在Spring Boot 应用程序启动期间,JVM加载各种类。通过第一个REST 请求,JVM 再次加载许多类,但不会通过对 REST 端点的任何进一步调用加载更多类。
Spring Boot 中的第一个请求很慢,那么为什么第一个请求必须快?API 有约定的响应时间,此外,应用程序每周更新几次,有多个实例,这导致每周有大量的“第一次请求”。
我现在应该如何预热应用程序?第一个天真的想法是在启动时简单地调用 REST API 的所有端点,以便加载所有类。
实际上,这很困难。
解决方案:特殊端点来预热 Spring Boot:
在我的实验中,我注意到只有对任何端点的第一个请求很慢。对另一个端点的下一个请求要快得多。基于这个观察,我萌生了写一个特殊的REST端点,只用于热身的想法。
这个特殊端点就是:在端点中使用一个 DTO,能包含经常使用的数据类型如String、BigInteger 等,还包括经常使用的用于验证的注释:

例如,下面是使用WarmUpController加上相关的请求和响应 DTO:

public class WarmUpRequestDto {
    @NotBlank
    @Pattern(regexp = "warm me up")
    private String warmUpString;

    @Min(10)
    @Max(20)
    private int warmUpNumber;

    @Valid
    private WarmUpEnumDto warmUpEnumDto;

    @NotNull
    private BigDecimal warmUpBigDecimal;
...
}

WarmUpRequestDto 中,有在普通端点中使用的所有数据类型,并且还使用了一个Enum 类。所有属性都进行了注释以进行验证。
如何自动激活这个Rest端点?
在等待ApplicationReadyEvent的PreloadComponent中实现了一个事件侦听器。ApplicationReadyEvent 是当应用程序从 Spring 的角度完全启动时由 Spring 生成的。在函数sendWarmUpRestRequest () 中,我使用Spring WebClient向我的WarmUpController发送一个 REST 请求。

private void sendWarmUpRestRequest() {
    final String serverPort = environment.getProperty("local.server.port");
    final String baseUrl = "http://localhost:" + serverPort;
    final String warmUpEndpoint = baseUrl + "/warmup";

    logger.info("Sending REST request to force initialization of Jackson...");

    final String response = webClientBuilder.build().post()
            .uri(warmUpEndpoint)
            .header(CONTENT_TYPE, APPLICATION_JSON_VALUE)
            .body(Mono.just(createSampleMessage()), WarmUpRequestDto.class)
            .retrieve()
            .bodyToMono(String.class)
            .timeout(Duration.ofSeconds(5))
            .block();

    logger.info("...done, response received: " + response);
}

GitHub 上的示例代码中,我注释掉了对onApplicationEvent(ApplicationReadyEvent 事件)函数的调用。
有兴趣的读者如果想看懂,必须去掉这个注释,重新编译启动应用程序:
mvn package
...
java -verbose:class -jar target/warm-me-up*.jar

几秒钟后,我的终端平静下来,我再次删除了内容。现在我使用 curl 再次执行第一个请求。第一次调用只用了 37 毫秒,而不是未优化的 200 毫秒。
Spring Boot 中还需要预热什么?
我的示例应用程序现在已充分预热。当然,在现实中,事情更复杂,需要做更多的工作。还必须预热以下组件,以便尽快处理第一个请求:

  • 建立数据库连接并进行查询
  • 建立与 Kafka 等消息代理的连接
  • 实例化其他解析库,例如用于 XML 处理的 JAXB
  • 加载本机 C 库

修复这些问题中的任何一个都可能需要几天的工作。但是很好,多亏了 JVM 选项:
-verbose:class

这个参数可以查看 JVM 何时加载了哪些类!