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

很哇的SpringBoot性能优化长文!

时间:2023-03-20 17:22:14 科技观察

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.Skywalking对于一个web服务来说,最慢的部分就是数据库操作。因此,使用本地缓存和分布式缓存优化可以获得最大的性能提升。对于复杂分布式环境如何定位的问题,这里分享另一个工具:Skywalking。Skywalking是使用探针技术(JavaAgent)实现的。通过在Java的启动参数中添加javaagentJar包,可以将性能数据和调用链数据封装起来发送给Skywalking服务器。下载对应的安装包(如果使用ES存储,需要下载专用安装包),配置好存储后,即可一键启动。将代理压缩包解压到对应目录。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访问一些服务的链接,打开Skywalking的UI,可以看到如下图的界面。从图中我们可以找到响应慢、QPS高的接口,进行专项优化。157234041047154.优化思路对于一个普通的Web服务,我们来看一下访问特定数据必须经历的主要环节。如下图,在浏览器中输入对应的域名,需要通过DNS解析到具体的IP地址。为了保证高可用,我们的服务一般都是多副本部署,然后使用Nginx做反向代理和负载均衡。Nginx会根据资源的特点,承担一部分动静分离的功能。其中,动态功能部分会进入我们的SpringBoot服务。SpringBoot默认使用内嵌的tomcat作为web容器,使用典型的MVC模式,最终访问我们的数据。五、HTTP优化下面举个例子,看看哪些动作可以加快网页的获取速度。为了描述方便,我们只讨论HTTP1.1协议。1、使用CDN加速文件获取比较大的文件,尽量使用CDN(ContentDeliveryNetwork)进行分发。甚至一些常用的前端脚本、样式、图片等,都可以放在CDN上。CDN通常可以加快这些文件的检索速度,并且页面加载速度更快。2、合理设置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;}3.减单page请求的域名数减少每个页面请求的域名数,尽量保持在4个以内。这是因为,浏览器每次访问后端资源,都需要先查询DNS,然后找到DNS对应的IP地址,然后进行真正的调用。DNS有多层缓存,例如浏览器会缓存一层,本地主机会缓存,ISP服务商会缓存等。从DNS到IP地址的转换通常需要20-120ms。减少域名数量可以加快资源获取。4.开启gzip开启gzip,可以先压缩内容,然后在浏览器中解压。由于减少了传输的大小,因此减少了带宽使用并提高了传输效率。它可以在nginx中轻松启用。配置如下:gzipon;gzip_min_length1k;gzip_buffers416k;gzip_comp_level6;gzip_http_version1.1;gzip_types文本/普通应用程序/javascript文本/css;5.压缩资源JavaScript、CSS,甚至HTML都被压缩。原因类似。目前流行的前后端分离模式,一般都是对这些资源进行压缩。6、使用keepalive会因为连接的创建和关闭而消耗资源。用户访问我们的服务后,以后会有更多的交互,所以保持长连接可以显着减少网络交互,提高性能。默认情况下,nginx为客户端启用keepavlide支持。您可以使用以下两个参数调整其行为。http{keepalive_timeout120s120s;keepalive_requests10000;}需要手动开启nginx与后端upstream的长连接。参考配置如下:location~/{proxy_passhttp://backend;proxy_http_version1.1;proxy_set_headerConnection"";}六.Tomcat优化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之后才有,开启后会增加一些性能。对比如下:默认。8080/owners?lastName=Running30stest@http://172.16.1.57:8080/owners?lastName=2个线程和100个连接线程校准:平均纬度:4588.131ms,速率采样间隔:16277ms线程校准:平均纬度。:4647.927ms,速率采样间隔:16285msThreadStatsAvgStdevMax+/-StdevLatency16.49s4.98s27.34s63.90%Req/Sec106.501.50108.00100.00%6471requestsin30.03s,read39.31MBrequests读0,写0,超时60Requests/sec:215.51Transfer/sec:1.31MBNio2。[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,速率采样间隔:16293ms线程统计AvgStdevMax+/-StdevLatency17.47s4.98s26.90s57.69%Req/Sec125.502.50128.00100.00%7469requestsin30.04s,45.38MBreadSocketerrors:connect0,read0,write0,timeout4Requests/sec:248.64Transfer/sec:1.51MB你甚至可以将tomcat替换成undertow。undertow也是一个web容器,更轻量级,占用更少的内容,启动更少的守护进程。更改方法如下:spring-boot-starter-tomcatorg.springframework.boot弹簧-boot-starter-undertow八。各层级的优化方向Controller层Controller层用于接收前端的查询参数,进而构造查询结果。现在很多项目都是采用前后端分离的架构,所以controller层的方法一般使用@ResponseBody注解将查询结果解析成JSON数据返回(兼顾效率和可读性)。由于controller只起到类似功能组合和路由的作用,所以这部分对性能的影响主要体现在数据集的大小上。如果结果集很大,JSON解析组件会花费更多的时间来解析。大的结果集不仅会影响解析时间,还会浪费内存。如果结果集在解析成JSON之前占用的内存是10MB,那么在解析过程中,可能会用到20M甚至更多的内存来做这个工作。我见过很多情况,由于返回的对象嵌套太深,引用了不应该引用的对象(例如非常大的byte[]对象),导致内存使用量猛增。所以,对于一般的服务来说,保持结果集简洁是非常有必要的,这也是DTO(数据传输对象)存在的必要条件。如果你的项目返回一个复杂的结果结构,那么对结果集进行转换是非常必要的。此外,Controller层可以使用异步Servlet进行优化。它的原理是这样的:Servlet收到请求后,将请求转交给一个异步线程进行业务处理,线程自己返回给容器。异步线程处理完业务后,可以直接生成响应数据,也可以继续将请求转发给其他Servlet。服务层服务层用于处理具体的业务,大部分的功能需求都在这里完成。服务层一般采用单例模式(原型),很少保存状态,可以被控制器复用。服务层的代码组织对代码的可读性和性能有很大的影响。我们常说的设计模式,大部分都是针对服务层的。这里要强调的一点是分布式事务。如上所示,这四个操作分散在三个不同的资源中。为了实现一致性,需要协调三种不同的资源。它们的底层协议和实现方式不同。那么就无法通过Spring提供的Transaction注解来解决,需要借助外部组件来完成。很多人都遇到过,加了一些代码保证一致性后,压测后性能会暴跌。分布式事务是性能杀手,因为它们需要额外的步骤来确保一致性。常用的方法有:两阶段提交方案、TCC、本地消息表、MQ事务消息、分布式事务中间件等。如上图所示,分布式事务要从改造成本、性能、有效性等方面综合考虑.在分布式事务和非事务之间有一个术语,叫做灵活事务。灵活事务的概念是将业务逻辑和互斥操作从资源层转移到业务层。下面简单对比一下传统事务和灵活事务。ACID关系型数据库,最大的特点是事务处理,满足ACID。原子性:要么执行事务中的所有操作,要么不执行任何操作。一致性:系统必须始终处于强一致性状态。隔离性:一个事务的执行不能被其他事务干扰。持久性:提交的事务对数据库中的数据进行永久更改。BASEBASE方法通过牺牲一致性和隔离性来提高可用性和系统性能。BASE是BasiclyAvailable、Soft-state、Eventuallyconsistent的缩写,其中BASE代表:BasiclyAvailable:系统基本可以一直运行并提供服务。软状态(Soft-state):不需要系统一直保持强一致的状态。最终一致性:系统需要在一定时间后达到一致性要求。对于互联网服务,建议使用补偿事务来实现最终一致性。比如通过一系列的定时任务来完成数据的修复。详情请参考以下文章。常用的分布式事务有哪些?我应该使用哪个?Dao层有合理的数据缓存,我们会尽量避免请求穿透到Dao层。除非你特别熟悉ORM本身提供的缓存特性,否则建议你使用更通用的方式来缓存数据。Dao层主要在于ORM框架的使用。比如在JPA中,如果加入了一对多或者多对多的映射关系,并且没有开启懒加载,在级联查询的时候很容易造成深度检索,导致内存开销大,执行速度慢..在一些数据量比较大的业务中,往往会采用分库分表的方式。在这些分库分表组件中,很多简单的查询语句会被重新解析并分发到各个节点进行计算,最后将结果进行合并。比如简单的count语句selectcount(*)froma,可能会将请求路由到十几张表中进行计算,最后在协调节点上进行统计。执行效率可想而知。目前分库分表的中间件比较有代表性的是驱动层的ShardingJdbc和代理层的MyCat,都存在这样的问题。这些组件向用户提供的视图是一致的,但我们在编码时必须注意这些差异。让我们在下面总结一下。我们简单了解了下SpringBoot常见的优化思路。我们介绍了三种新的性能分析工具。一是监控系统Prometheus,可以看到一些具体指标的大小;另一个是火焰图,可以看到具体的代码热点;另一个是Skywalking,可以在分布式环境下分析调用链。当我们对性能有疑虑时,我们会采用类似神农尝百草的方法,对各种评价工具的结果进行分析。SpringBoot自带的web容器是Tomcat,所以我们可以通过调优Tomcat来获得性能的提升。当然,对于服务上层的负载均衡Nginx,我们也提供了一系列的优化思路。最后,我们看了经典MVC架构下Controller、Service、Dao的一些优化方向,重点关注了Service层的分布式事务问题。这是一个具体的优化示例。5秒到1秒,记一次“非常”显着的性能优化作为应用广泛的服务框架,SpringBoot在性能优化方面做了大量的工作,选用了很多高速组件。比如数据库连接池默认使用hikaricp,Redis缓存框架默认使用lettuce,本地缓存提供caffeine。对于与数据库交互的常见Web服务,缓存是主要的优化工具。但细节决定成败。如果你想把系统优化到极致,需要参考下面的文章。优秀的表现の文库(非广告)