Thymeleaf+SpringBoot2高吞吐量调优技巧

Thymeleaf+SpringBoot2技术如下:

  • Springboot 2.3 + Thymeleaf
  • MongoDB
  • Java

提前监控一些指标:
SpringBoot服务:

  • API 响应代码(5xx 和 4xx)
  • 每个端点的延迟
  • 每个端点每秒的请求数
  • tomcat 指标(Servlet 错误、连接、当前线程)
  • 中央处理器
  • 记忆

数据库集群:
  • 热门查询
  • 复制延迟
  • 读/写延迟
  • 慢速查询
  • 文件锁
  • 系统锁
  • 中央处理器
  • 当前会话

对于 SpringBoot,这可以通过启用管理端点并使用 Prometheus 收集指标并将指标发送到 Grafana 或 Cloudwatch 等方式轻松衡量。指标交付后,在合理的阈值上设置警报。

对于数据库,这取决于技术,您应该在客户端(spring boot dbmetrics)和服务器端都对其进行监控。客户端的监控对于查看是否有任何代理或防火墙不时阻止您的任何命令非常重要。相信我,即使您测试了它并且看起来工作得很好,这些连接丢失也可能会发生,以防万一某些东西配置不正确并且您想要捕获它!例如:代理 sidecar 上的数据库端口上的出站流量配置错误可能会导致 Spring Boot 端出现脏 HTTP 连接,而这些连接已在服务器端关闭。

1、连接池和 HTTP 客户端调优
我检查的第一件事是连接池和 HTTP 客户端设置,这样我就可以了解这些服务可以打开的最大并行连接数、在所有当前连接都繁忙后开始拒绝新连接的速度以及持续多长时间。会等待响应,直到开始超时。

超时
比喻:一条很长的超市队伍,收银员很慢,队伍无限地增长,而你却站在队伍的最后。你要么永远等待,也许在商店关门之前就排到队伍的前面(其他人会一直在你后面排队),或者你(和所有其他人)可以在 3 秒后决定离开那个拥挤的地方,然后过来晚点回来。

超时是一种为客户端提供快速响应的机制,避免上游服务等待并阻止它们接受新请求。另一方面,断路器是一种保护措施,可避免在出现问题(连接断开、CPU 过载等)时使下游服务过载。例如,断路器是指当餐厅满员时,那些服务员将顾客送回家而没有机会等待餐桌的情况。

连接池
远程连接(例如用于与数据库或 REST API 通信的连接)是为每个请求创建的昂贵资源。它需要打开连接、建立握手、验证证书等。

连接池允许我们重用连接来优化性能,并通过维护多个并行连接(每个连接都在其自己的线程中)来增加应用程序的并发性。在给定某些配置的情况下,如果池中的所有连接都繁忙,它还使我们能够灵活地将请求排队一定时间,这样它们就不会立即被拒绝,从而使我们的服务有更多机会在一定时间内成功服务所有请求。

@Bean
HttpClient httpClient(PoolingHttpClientConnectionManager connectionManager) {
    return HttpClientBuilder.create()
            .setConnectionManager( connectionManager )
            .build();
}

@Bean PoolingHttpClientConnectionManager connectionManager() {
    PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
    connectionManager.setMaxTotal( POOL_MAX_TOTAL );
    connectionManager.setDefaultMaxPerRoute( POOL_DEFAULT_MAX_PER_ROUTE );
    return connectionManager;
}

@Bean
ClientHttpRequestFactory clientHttpRequestFactory(HttpClient httpClient) {
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
    factory.setConnectTimeout(CONNECT_TIMEOUT_IN_MILLISECONDS);
    factory.setReadTimeout(READ_TIMEOUT_IN_MILLISECONDS);
    factory.setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT);
    return factory;
}

@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
    RestTemplate restTemplate = new RestTemplateBuilder()
        .requestFactory(() -> clientHttpRequestFactory)
        .build();
    return restTemplate;
}

上面的 beans 将确保其余模板使用的 HTTP 客户端使用一个连接管理器,该管理器具有合理的每条路由的最大连接数和总最大连接数。如果传入请求多于我们能够通过这些设置提供服务的数量,它们将由连接管理器排队,直到达到连接请求超时。如果连接请求超时后由于请求仍在队列中而未执行连接尝试,则请求将失败。请在此处阅读有关不同类型的 HTTP 客户端超时的更多信息。

确保根据您的需求和服务器资源调整常量。请注意,每个连接都会打开一个线程,并且线程受到操作系统资源的限制。因此,您不能简单地将这些限制增加到不合理的值。

2、MongoDB调优
为 MongoDB 集群设置了监控,这样就很容易发现罪魁祸首:有一个文档因对同一文档进行多次并发写入尝试而被锁定。

以上更改确实增加了吞吐量,现在我们的数据库因同一文档中的大量并行写入而过载,这导致需要花费大量时间等待解锁以便下一个查询更新它。

数据库连接池忙于对请求进行排队,因此上游服务也开始让其线程池用于处理传入请求,因为等待响应的其余模板的同步性质,增加了上游服务的 CPU 消耗。

文档锁对于并发来说是必要的,但效果并不好,因为它们可以很容易地开始阻止您的数据库连接,并且它们通常表明您的代码或集合设计存在问题,因此请务必检查它,以防您看到一些迹象表明您的文档正在被破坏。

删除不必要的 save() 调用后,事情开始看起来好多了。

下一步:MongoDB 允许您通过其连接选项覆盖默认值。

  • 连接将根据 minPoolSize 和 maxPoolSize 创建。如果执行查询需要更长的时间并且有新查询进入,则将创建新连接,直到达到 maxPoolSize。
  • 我们还可以使用 waitQueueTimeoutMS 定义查询可以等待执行的时间。
  • 数据库写入时:,您还应该确保查看 wtimeoutMS,默认情况下它会保持连接繁忙,直到数据库完成写入。
  • 如果设置的值不同于默认值(永不超时),您还可以在数据库周围设置一个断路器,以确保不会因为额外的请求而使其过载。
  • 如果您的数据库集群包含多个节点,请确保通过设置 readPreference=secondaryPrefered 来分配读取负载,并注意一致性、读取隔离和新近度

3、Thymeleaf缓存
Thymeleaf为基于内容的静态资源启用了缓存。类似于以下属性:

spring.resources.chain.enabled=true
spring.resources.chain.strategy.content.enabled=true
spring.resources.chain.strategy.content.paths=/**

然而,这三行引入了两个问题。
1- 根据资源内容启用缓存。但是,对于每个请求,都会一遍又一遍地从磁盘读取内容,因此可以重新计算其哈希值,因为缓存本身的哈希计算结果不会被缓存。要解决此问题,请不要忘记添加以下属性:
spring.resources.chain.cache=true

2-不幸的是,该服务没有使用任何基本路径来统一静态资源的分辨率,因此基本上,Thymeleaf 默认情况下会尝试将每个链接作为静态资源从磁盘加载,即使它们只是控制器路径,例如。请记住,磁盘操作通常非常昂贵。

因为我不想通过将所有静态资源移动到资源文件夹中的新目录来引入不兼容的更改,因为这会导致链接更改,并且我有非常明确定义的静态资源路径,所以我可以简单地解决它:使用ResourceHandlerRegistration 中的setOptimizeLocations()