当前位置: 首页 > 科技观察

非常wow的SpringBoot性能优化长文

时间:2023-03-22 12:58:09 科技观察

SpringBoot已经成为Java中的No.1框架,每天都在蹂躏着百万程序员。当服务压力上升时,SpringBoot服务的优化就会提上日程。本文将详细讲解SpringBoot服务优化的总体思路。本文篇幅较长,最适合收藏。1、有监控才有方向。在我们开始优化SpringBoot服务的性能之前,我们需要做一些准备工作来暴露SpringBoot服务的一些数据。比如你的服务使用了缓存,你需要收集缓存命中率等数据;如果使用数据库连接池,则需要暴露连接池的参数。我们这里使用的监控工具是Prometheus,它是一个时序数据库,可以存储我们的指标。SpringBoot可以轻松连接到Prometheus。创建SpringBoot项目后,首先添加Maven依赖。org.springframework.bootspring-boot-starter-actuatorio.micrometer千分尺-registry-prometheusio.micrometermicrometer-core然后,我们需要在application.properties配置文件,打开相关监控界面。management.endpoint.metrics.enabled=truemanagement.endpoints.web.exposure.include=*management.endpoint.prometheus.enabled=truemanagement.metrics.export.prometheus.enabled=true启动后,我们可以访问http://localhost:8080/actuator/prometheus获取监控数据。监控业务数据也比较简单。您只需要注入一个MeterRegistry实例。这是一个示例代码:@AutowiredMeterRegistryregistry;@GetMapping("/test")@ResponseBodypublicStringtest(){registry.counter("test","from","127.0.0.1","method","test")。增量();return"ok";}从监控连接中,我们可以找到刚刚添加的监控信息。test_total{from="127.0.0.1",method="test",}5.0下面简单介绍下流行的Prometheus监控系统。Prometheus采用pull方式获取监控数据,暴露数据的过程可以交给更完善的telegraf组件。如图所示,我们通常使用Grafana展示监控数据,使用AlertManager组件进行预警。这部分的构建不是我们的重点,有兴趣的同学可以自己研究。下图是一个典型的监控图,可以看到Redis的缓存命中率等等。2、Java生成火焰图火焰图是用来分析程序执行瓶颈的工具。在垂直方向,表示调用栈的深度;在水平方向上,它表示消耗的时间。所以网格的宽度越大,就越有可能成为瓶颈。火焰图也可用于分析Java应用程序。相关操作可以到GitHub下载async-profiler的压缩包。比如我们把它解压到/root目录下。然后以javaagent的方式启动Java应用。命令行如下:java-agentpath:/root/build/libasyncProfiler.so=start,svg,file=profile.svg-jarspring-petclinic-2.3.1.BUILD-SNAPSHOT.jar运行一段时间后时间,停止进程,可以看到在当前目录下生成了profile.svg文件。这个文件可以用浏览器打开,可以一层一层往下浏览,找到需要优化的目标。3.对于一个web服务来说,Skywalking最慢的部分是数据库操作。因此,使用本地缓存和分布式缓存优化可以获得最大的性能提升。对于复杂分布式环境如何定位的问题,这里分享另一个工具:Skywalking。Skywalking是使用探针技术(JavaAgent)实现的。通过在Java启动参数中加入javaagentJar包,可以将性能数据和调用链数据封装起来发送给Skywalking服务器。下载相应的安装包(如果使用Elasticsearch存储,需要下载专用安装包),配置好存储后,即可一键启动。将代理压缩包解压到对应目录。tarxvfskywalking-agent.tar.gz-C/opt/在业务启动参数中加入agent包,例如原启动命令为:java-jar/opt/test-service/spring-boot-demo.jar--spring.profiles.active=dev修改后的启动命令为:java-javaagent:/opt/skywalking-agent/skywalking-agent.jar-Dskywalking.agent.service_name=the-demo-name-jar/opt/test-service/spring-boot-demo.ja--spring.profiles.active=dev访问一些服务链接,打开SkywalkingUI,可以看到如下图的界面。从图中我们可以找到响应慢、QPS高的接口,进行专项优化。4.优化思路对于一个普通的web服务,我们来看一下访问特定数据必须经历的主要环节。如下图,在浏览器中输入对应的域名,需要通过DNS解析到具体的IP地址。为了保证高可用,我们的服务一般都是多副本部署,然后使用Nginx做反向代理和负载均衡。Nginx会根据资源的特点,承担一部分动静分离的功能。其中,动态功能部分会进入我们的SpringBoot服务。SpringBoot默认使用嵌入式Tomcat作为Web容器,使用典型的MVC模式来最终访问我们的数据。5.HTTP优化下面举个例子,看看哪些动作可以加快网页的获取速度。为了描述方便,我们只讨论HTTP1.1协议。使用CDN加速文件获取比较大的文件,尽量使用CDN(ContentDeliveryNetwork)进行分发。甚至一些常用的前端脚本、样式、图片等,都可以放在CDN上。CDN通常可以更快地获取这些文件,并且网页加载速度更快。合理设置Cache-Control值浏览器会通过判断HTTP头Cache-Control的内容来决定是否使用浏览器缓存,这在管理一些静态文件时非常有用。同样作用的头信息也有Expires。Cache-Control表示多久过期,Expires表示什么时候过期。这个参数可以在Nginx配置文件中设置。location~*^.+\.(ico|gif|jpg|jpeg|png)${#缓存1年add_headerCache-Control:no-cache,max-age=31536000;}减少单页请求域名减少每个页面请求的域名个数,尽量保持在4个以内。这是因为,浏览器每次访问后端资源,都需要先查询DNS,再找到对应的IP地址到DNS,然后进行真正的调用。DNS有多层缓存,例如浏览器会缓存一层,本地主机会缓存,ISP服务商缓存等。从DNS到IP地址的转换一般需要20-120ms。减少域名数量可以加快资源获取。开启gzip开启gzip,可以先压缩内容,然后在浏览器中解压。由于减少了传输的大小,因此减少了带宽使用并提高了传输效率。它可以在Nginx中轻松启用。配置如下:gzipon;gzip_min_length1k;gzip_buffers416k;gzip_comp_level6;gzip_http_version1.1;gzip_types文本/普通应用程序/javascript文本/css;压缩资源JavaScript和CSS,甚至HTML。原因类似。目前流行的前后端分离模式,一般都是对这些资源进行压缩。由于连接的创建和关闭,使用keepalive会消耗资源。用户访问我们的服务后,以后会有更多的交互,所以保持长连接可以显着减少网络交互,提高性能。Nginx默认为客户端启用keepalive支持。您可以使用以下两个参数调整其行为。http{keepalive_timeout120s120s;keepalive_requests10000;}Nginx与后端upstream的长连接需要手动开启。参考配置如下:location~/{proxy_passhttp://backend;proxy_http_version1.1;proxy_set_headerConnection"";}6.Tomcat优化Tomcat本身的优化也是很重要的一环。优化参数请参考文末链接。7、自定义web容器如果你的项目并发比较高,想修改最大线程数、最大连接数等配置信息,可以自定义web容器。代码如下。@SpringBootApplication(proxyBeanMethods=false)publicclassAppimplementsWebServerFactoryCustomizer{publicstaticvoidmain(String[]args){SpringApplication.run(PetClinicApplication.class,args);}@Overridepublicvoidcustomize(ConfigurableServletWebServerFactoryfactory){TomcatServletWebServerFactoryf=(TomcatServletWebServerFactory)工厂;f.setProtocol("org.apache.coyote.http11.Http11Nio2Protocol");f.addConnectorCustomizers(c->{Http11NioProtocol协议=(Http11NioProtocol)c.getProtocolHandler();protocol.set(20MaxConnection);protocol.setMaxThreads(200);protocol.setSelectorTimeout(3000);protocol.setSessionTimeout(3000);协议。设置连接超时(3000);});注意上面的代码:我们将其协议设置为org.apache。coyote.http11.Http11Nio2Protocol表示开启Nio2。该参数只有Tomcat8.0之后才有,开启后会增加一些性能。对应如下:默认显示:[root@localhostwrk2-master]#./wrk-t2-c100-d30s-R2000http://172.16.1.57:8080/owners?lastName=Running30stest@http://172.16.1.57:8080/owners?lastName=2threadsand100connections线程校准:平均纬度:4588.131ms,速率采样间隔:16277ms线程校准:平均纬度:4647.927ms,速率采样间隔:16285msThreadStatsAvgStdevMax+/-Stdev延迟16.49s4.98s27.34s63.90%Req/Sec106.501.50108.00100.00%30.03s中的6471个请求,39.31MB读取套接字错误:连接0,读取0,写入0,超时60Requests/sec:215.51Transfer/sec1.31MB打开Nio2显示:[root@localhostwrk2-master]#./wrk-t2-c100-d30s-R2000http://172.16.1.57:8080/owners?lastName=Running30stest@http://172.16.1.57:8080/owners?lastName=2个线程和100个连接线程校准:平均纬度:4358.805ms,速率采样间隔:15835ms线程校准:平均纬度:4622.087ms,速率采样间隔:16293msThreadStatsAvgStdevMax+/-StdevLatency17.47s4.98s26.90s57.69%Req/Sec125.502.50128.00100.00%nr中的7469个请求读取0MB,写入0,超时4Requests8.6:2传输/秒:1.51MB你甚至可以用Undertow替换TomcatUndertow也是一个web容器,更轻量级,占用更少的内容,启动更少的守护进程。变化如下:/groupId>spring-boot-starter-tomcat/dependency>org.springframework.boot弹簧启动-starter-undertow八、各层级的优化方向8.1Controller层Controller层用于接收前端的查询参数,进而构造查询结果。现在很多项目采用前后端分离的架构,所以Controller层的方法一般使用@ResponseBody注解将查询结果解析成JSON数据返回(兼顾效率和可读性)。由于Controller只起到类似功能组合和路由的作用,所以这部分对性能的影响主要体现在数据集的大小上。如果结果集很大,JSON解析组件会花费更多的时间来解析。大的结果集不仅会影响解析时间,还会浪费内存。如果结果集在解析成JSON之前占用的内存是10MB,那么在解析过程中,可能会用到20M甚至更多的内存来做这个工作。我见过很多情况,由于返回的对象嵌套太深,引用了它们不应该引用的对象(例如非常大的byte[]对象),导致内存使用量猛增。所以,对于一般的服务来说,保持结果集简洁是非常有必要的,这也是DTO(数据传输对象)存在的必要条件。如果你的项目中返回的结果结构比较复杂,那么对结果集进行一次转换是非常有必要的。此外,可以使用异步servlet优化控制器层。它的原理是这样的:Servlet收到请求后,将请求转交给一个异步线程进行业务处理,线程自己返回给容器。异步线程处理完业务后,可以直接生成响应数据,也可以继续将请求转发给其他Servlet。8.2Service层Service层用来处理具体的业务,大部分的功能需求都在这里完成。Service层一般采用单例模式(Singleton),很少保存状态,可以被Service复用。Service层的代码组织对代码的可读性和性能有很大的影响。我们常说的设计模式,大部分都是针对Service层的。这里要强调的一点是分布式事务。如上所示,这四个操作分散在三个不同的资源中。为了实现一致性,需要协调三种不同的资源。它们的底层协议和实现方式不同。那么就无法通过Spring提供的Transaction注解来解决,需要借助外部组件来完成。很多人都遇到过,加了一些代码保证一致性后,压测后性能会暴跌。分布式事务是性能杀手,因为它们需要额外的步骤来确保一致性。常用的方法有:两阶段提交方案、TCC、本地消息表、MessageQueue事务消息、分布式事务中间件等。如上图所示,分布式事务要综合考虑改造成本、性能、效力。在分布式事务和非事务之间有一个术语,叫做灵活事务。灵活事务的概念是将业务逻辑和互斥操作从资源层转移到业务层。下面简单对比一下传统事务和灵活事务。ACID关系型数据库最大的特点是事务处理,满足ACID。原子性:要么执行事务中的所有操作,要么不执行任何操作。一致性:系统必须始终处于强一致性状态。隔离性:一个事务的执行不能被其他事务干扰。持久性:提交的事务对数据库中的数据进行永久更改。BASEBASE方法通过牺牲一致性和隔离性来提高可用性和系统性能。BASE是BasiclyAvailable、Soft-state、Eventuallyconsistent的缩写,其中BASE代表:BasiclyAvailable:系统基本可以一直运行并提供服务。软状态(Soft-state):不需要系统一直保持强一致的状态。最终一致性:系统需要在一定时间后达到一致性要求。对于互联网服务,建议使用补偿事务来实现最终一致性。比如通过一系列的定时任务来完成数据的修复。8.4Dao层有合理的数据缓存,我们会尽量避免请求穿透到Dao层。除非你特别熟悉ORM本身提供的缓存特性,否则建议你使用更通用的方式来缓存数据。Dao层主要在于ORM框架的使用。比如在JPA中,如果加入了一对多或者多对多的映射关系,并且没有开启懒加载,在级联查询的时候很容易造成深度检索,导致内存开销大,执行速度慢..在一些数据量比较大的业务中,往往会采用分库分表的方式。在这些分库分表组件中,很多简单的查询语句会被重新解析并分发到各个节点进行计算,最后将结果进行合并。比如简单的count语句selectcount(*)froma,可能会将请求路由到十几张表中进行计算,最后在协调节点上进行统计。执行效率可想而知。目前驱动层的Sharding-JDBC和代理层的MyCAT是比较有代表性的分库分表中间件,都存在这样的问题。这些组件向用户提供的视图是一致的,但我们在编码时必须注意这些差异。总结让我们总结一下。我们简单了解了SpringBoot常见的优化思路。我们介绍了三种新的性能分析工具。一是监控系统Prometheus,可以看到一些具体指标的大小;另一个是火焰图,可以看到具体的代码热点;另一个是Skywalking,可以在分布式环境下分析调用链。当我们对性能有疑虑时,我们会采用类似神农尝百草的方法,对各种评价工具的结果进行分析。SpringBoot自带的web容器是Tomcat,所以我们可以通过调优Tomcat来获得性能的提升。当然,对于服务上层的负载均衡Nginx,我们也提供了一系列的优化思路。最后,我们看了经典MVC架构下Controller、Service、Dao的一些优化方向,重点关注了Service层的分布式事务问题。SpringBoot作为应用广泛的服务框架,在性能优化方面做了大量工作,选用了很多高速组件。比如数据库连接池默认使用HikariCP,Redis缓存框架默认使用Lettuce,本地缓存提供Caffeine等。对于一个常见的与数据库交互的web服务,缓存是主要的优化。