SpringBoot 性能這樣優(yōu)化,同事直呼哇塞!
SpringBoot已經(jīng)成為Java屆的No.1框架,每天都在蹂躪著數(shù)百萬的程序員們。當服務的壓力上升,對SpringBoot服務的優(yōu)化就會被提上議程。
本文將詳細講解SpringBoot服務優(yōu)化的一般思路,并附上若干篇輔助文章作為開胃菜。
本文較長,最適合收藏之。
1.有監(jiān)控才有方向
在開始對SpringBoot服務進行性能優(yōu)化之前,我們需要做一些準備,把SpringBoot服務的一些數(shù)據(jù)暴露出來。
比如,你的服務用到了緩存,就需要把緩存命中率這些數(shù)據(jù)進行收集;用到了數(shù)據(jù)庫連接池,就需要把連接池的參數(shù)給暴露出來。
我們這里采用的監(jiān)控工具是Prometheus,它是一個是時序數(shù)據(jù)庫,能夠存儲我們的指標。SpringBoot可以非常方便的接入到Prometheus中。
創(chuàng)建一個SpringBoot項目后,首先,加入maven依賴。
<dependency>
?????<groupId>org.springframework.bootgroupId>
?????<artifactId>spring-boot-starter-actuatorartifactId>
?dependency>
?<dependency>
?????<groupId>io.micrometergroupId>
?????<artifactId>micrometer-registry-prometheusartifactId>
?dependency>
?<dependency>
?????<groupId>io.micrometergroupId>
?????<artifactId>micrometer-coreartifactId>
?dependency>
然后,我們需要在application.properties配置文件中,開放相關(guān)的監(jiān)控接口。
management.endpoint.metrics.enabled=true
management.endpoints.web.exposure.include=*
management.endpoint.prometheus.enabled=true
management.metrics.export.prometheus.enabled=true
啟動之后,我們就可以通過訪問 http://localhost:8080/actuator/prometheus 來獲取監(jiān)控數(shù)據(jù)。

想要監(jiān)控業(yè)務數(shù)據(jù)也是比較簡單的。你只需要注入一個MeterRegistry實例即可。下面是一段示例代碼:
@Autowired
MeterRegistry?registry;
@GetMapping("/test")
@ResponseBody
public?String?test()?{
????registry.counter("test",
????????????"from",?"127.0.0.1",
????????????"method",?"test"
????).increment();
????return?"ok";
}
從監(jiān)控連接中,我們可以找到剛剛添加的監(jiān)控信息。
test_total{from="127.0.0.1",method="test",}?5.0
這里簡單介紹一下流行的Prometheus監(jiān)控體系,Prometheus使用拉的方式獲取監(jiān)控數(shù)據(jù),這個暴露數(shù)據(jù)的過程可以交給功能更加齊全的telegraf組件。

如圖,我們通常使用Grafana進行監(jiān)控數(shù)據(jù)的展示,使用AlertManager組件進行提前預警。這一部分的搭建工作不是我們的重點,感興趣的同學可自行研究。下圖便是一張典型的監(jiān)控圖,可以看到Redis的緩存命中率等情況。

2.Java生成火焰圖
火焰圖是用來分析程序運行瓶頸的工具。在縱向,表示的是調(diào)用棧的深度;橫向表明的是消耗的時間。所以格子的寬度越大,越說明它可能是一個瓶頸。
火焰圖也可以用來分析Java應用??梢詮膅ithub上下載async-profiler的壓縮包 進行相關(guān)操作。
比如,我們把它解壓到/root/目錄。然后以javaagent的方式來啟動Java應用。命令行如下:
java?-agentpath:/root/build/libasyncProfiler.so=start,svg,file=profile.svg?-jar?spring-petclinic-2.3.1.BUILD-SNAPSHOT.jar
運行一段時間后,停止進程,可以看到在當前目錄下,生成了profile.svg文件,這個文件是可以用瀏覽器打開的,一層層向下瀏覽,即可找到需要優(yōu)化的目標。
3.Skywalking
對于一個web服務來說,最緩慢的地方就在于數(shù)據(jù)庫操作。所以,使用本地緩存和分布式緩存優(yōu)化,能夠獲得最大的性能提升。
對于如何定位到復雜分布式環(huán)境中的問題,我這里想要分享另外一個工具:Skywalking。
Skywalking是使用探針技術(shù)(JavaAgent)來實現(xiàn)的。通過在Java的啟動參數(shù)中,加入javaagent的Jar包,即可將性能數(shù)據(jù)和調(diào)用鏈數(shù)據(jù)封裝、發(fā)送到Skywalking的服務器。
下載相應的安裝包(如果使用ES存儲,需要下載專用的安裝包),配置好存儲之后,即可一鍵啟動。
將agent的壓縮包,解壓到相應的目錄。
tar?xvf?skywalking-agent.tar.gz??-C?/opt/
在業(yè)務啟動參數(shù)中加入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又比較高的的接口,進行專項優(yōu)化。

4.優(yōu)化思路
對一個普通的Web服務來說,我們來看一下,要訪問到具體的數(shù)據(jù),都要經(jīng)歷哪些主要的環(huán)節(jié)。
如下圖,在瀏覽器中輸入相應的域名,需要通過DNS解析到具體的IP地址上。為了保證高可用,我們的服務一般都會部署多份,然后使用Nginx做反向代理和負載均衡。
Nginx根據(jù)資源的特性,會承擔一部分動靜分離的功能。其中,動態(tài)功能部分,會進入我們的SpringBoot服務。

SpringBoot默認使用內(nèi)嵌的tomcat作為Web容器,使用典型的MVC模式,最終訪問到我們的數(shù)據(jù)。
5.HTTP優(yōu)化
下面我們舉例來看一下,哪些動作能夠加快網(wǎng)頁的獲取。為了描述方便,我們僅討論HTTP1.1協(xié)議的。
1.使用CDN加速文件獲取
比較大的文件,盡量使用CDN(Content Delivery Network)分發(fā)。甚至是一些常用的前端腳本、樣式、圖片等,都可以放到CDN上。CDN通常能夠加快這些文件的獲取,網(wǎng)頁加載也更加迅速。
2.合理設置Cache-Control值
瀏覽器會判斷HTTP頭Cache-Control的內(nèi)容,用來決定是否使用瀏覽器緩存,這在管理一些靜態(tài)文件的時候,非常有用。相同作用的頭信息還有Expires。Cache-Control表示多久之后過期,Expires則表示什么時候過期。
這個參數(shù)可以在Nginx的配置文件中進行設置。
location ~* ^.+\.(ico|gif|jpg|jpeg|png)$ {
# 緩存1年
add_header Cache-Control: no-cache, max-age=31536000;
}
3.減少單頁面請求域名的數(shù)量
減少每個頁面請求的域名數(shù)量,盡量保證在4個之內(nèi)。這是因為,瀏覽器每次訪問后端的資源,都需要先查詢一次DNS,然后找到DNS對應的IP地址,再進行真正的調(diào)用。
DNS有多層緩存,比如瀏覽器會緩存一份、本地主機會緩存、ISP服務商緩存等。從DNS到IP地址的轉(zhuǎn)變,通常會花費20-120ms的時間。減少域名的數(shù)量,可加快資源的獲取。
4.開啟gzip
開啟gzip,可以先把內(nèi)容壓縮后,瀏覽器再進行解壓。由于減少了傳輸?shù)拇笮?,會減少帶寬的使用,提高傳輸效率。
在nginx中可以很容易的開啟。配置如下:
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_comp_level 6;
gzip_http_version 1.1;
gzip_types text/plain application/javascript text/css;
5.對資源進行壓縮
對JavaScript和CSS,甚至是HTML進行壓縮。道理類似,現(xiàn)在流行的前后端分離模式,一般都是對這些資源進行壓縮的。
6.使用keepalive
由于連接的創(chuàng)建和關(guān)閉,都需要耗費資源。用戶訪問我們的服務后,后續(xù)也會有更多的互動,所以保持長連接可以顯著減少網(wǎng)絡交互,提高性能。
nginx默認開啟了對客戶端的keep avlide支持。你可以通過下面兩個參數(shù)來調(diào)整它的行為。
http {
keepalive_timeout 120s 120s;
keepalive_requests 10000;
}
nginx與后端upstream的長連接,需要手工開啟,參考配置如下:
location ~ /{
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
6.Tomcat優(yōu)化
Tomcat本身的優(yōu)化,也是非常重要的一環(huán)??梢灾苯訁⒖枷旅娴奈恼隆?/p>
搞定tomcat重要參數(shù)調(diào)優(yōu)!
7.自定義Web容器
如果你的項目并發(fā)量比較高,想要修改最大線程數(shù)、最大連接數(shù)等配置信息,可以通過自定義Web容器的方式,代碼如下所示。
@SpringBootApplication(proxyBeanMethods?=?false)
public?class?App?implements?WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>?{
?public?static?void?main(String[]?args)?{
??SpringApplication.run(PetClinicApplication.class,?args);
?}
?@Override
?public?void?customize(ConfigurableServletWebServerFactory?factory)?{
??TomcatServletWebServerFactory?f?=?(TomcatServletWebServerFactory)?factory;
????????f.setProtocol("org.apache.coyote.http11.Http11Nio2Protocol");
??f.addConnectorCustomizers(c?->?{
???Http11NioProtocol?protocol?=?(Http11NioProtocol)?c.getProtocolHandler();
???protocol.setMaxConnections(200);
???protocol.setMaxThreads(200);
???protocol.setSelectorTimeout(3000);
???protocol.setSessionTimeout(3000);
???protocol.setConnectionTimeout(3000);
??});
?}
}
注意上面的代碼,我們設置了它的協(xié)議為org.apache.coyote.http11.Http11Nio2Protocol,意思就是開啟了Nio2。這個參數(shù)在Tomcat8.0之后才有,開啟之后會增加一部分性能。對比如下:
默認。
[root@localhost?wrk2-master]#?./wrk?-t2?-c100?-d30s?-R2000?http://172.16.1.57:8080/owners?lastName=
Running?30s?test?@?http://172.16.1.57:8080/owners?lastName=
??2?threads?and?100?connections
??Thread?calibration:?mean?lat.:?4588.131ms,?rate?sampling?interval:?16277ms
??Thread?calibration:?mean?lat.:?4647.927ms,?rate?sampling?interval:?16285ms
??Thread?Stats???Avg??????Stdev?????Max???+/-?Stdev
????Latency????16.49s?????4.98s???27.34s????63.90%
????Req/Sec???106.50??????1.50???108.00????100.00%
??6471?requests?in?30.03s,?39.31MB?read
??Socket?errors:?connect?0,?read?0,?write?0,?timeout?60
Requests/sec:????215.51
Transfer/sec:??????1.31MB
Nio2。
[root@localhost?wrk2-master]#?./wrk?-t2?-c100?-d30s?-R2000?http://172.16.1.57:8080/owners?lastName=
Running?30s?test?@?http://172.16.1.57:8080/owners?lastName=
??2?threads?and?100?connections
??Thread?calibration:?mean?lat.:?4358.805ms,?rate?sampling?interval:?15835ms
??Thread?calibration:?mean?lat.:?4622.087ms,?rate?sampling?interval:?16293ms
??Thread?Stats???Avg??????Stdev?????Max???+/-?Stdev
????Latency????17.47s?????4.98s???26.90s????57.69%
????Req/Sec???125.50??????2.50???128.00????100.00%
??7469?requests?in?30.04s,?45.38MB?read
??Socket?errors:?connect?0,?read?0,?write?0,?timeout?4
Requests/sec:????248.64
Transfer/sec:??????1.51MB
你甚至可以將tomcat替換成undertow。undertow也是一個Web容器,更加輕量級一些,占用的內(nèi)容更少,啟動的守護進程也更少,更改方式如下:
<dependency>
??????<groupId>org.springframework.bootgroupId>
??????<artifactId>spring-boot-starter-webartifactId>
??????<exclusions>
????????<exclusion>
??????????<groupId>org.springframework.bootgroupId>
??????????<artifactId>spring-boot-starter-tomcatartifactId>
????????exclusion>
??????exclusions>
????dependency>
????<dependency>
??????<groupId>org.springframework.bootgroupId>
??????<artifactId>spring-boot-starter-undertowartifactId>
????dependency>
8.各個層次的優(yōu)化方向
Controller層
controller層用于接收前端的查詢參數(shù),然后構(gòu)造查詢結(jié)果?,F(xiàn)在很多項目都采用前后端分離的架構(gòu),所以controller層的方法,一般會使用@ResponseBody注解,把查詢的結(jié)果,解析成JSON數(shù)據(jù)返回(兼顧效率和可讀性)。
由于controller只是充當了一個類似功能組合和路由的角色,所以這部分對性能的影響就主要體現(xiàn)在數(shù)據(jù)集的大小上。如果結(jié)果集合非常大,JSON解析組件就要花費較多的時間進行解析。
大結(jié)果集不僅會影響解析時間,還會造成內(nèi)存浪費。假如結(jié)果集在解析成JSON之前,占用的內(nèi)存是10MB,那么在解析過程中,有可能會使用20M或者更多的內(nèi)存去做這個工作。我見過很多案例,由于返回對象的嵌套層次太深、引用了不該引用的對象(比如非常大的byte[]對象),造成了內(nèi)存使用的飆升。
所以,對于一般的服務,保持結(jié)果集的精簡,是非常有必要的,這也是DTO(data transfer object)存在的必要。如果你的項目,返回的結(jié)果結(jié)構(gòu)比較復雜,對結(jié)果集進行一次轉(zhuǎn)換是非常有必要的。
另外,可以使用異步Servlet對Controller層進行優(yōu)化。它的原理如下:Servlet 接收到請求之后,將請求轉(zhuǎn)交給一個異步線程來執(zhí)行業(yè)務處理,線程本身返回至容器,異步線程處理完業(yè)務以后,可以直接生成響應數(shù)據(jù),或者將請求繼續(xù)轉(zhuǎn)發(fā)給其它 Servlet。
Service層
service層用于處理具體的業(yè)務,大部分功能需求都是在這里完成的。service層一般是使用單例模式(prototype),很少會保存狀態(tài),而且可以被controller復用。
service層的代碼組織,對代碼的可讀性、性能影響都比較大。我們常說的設計模式,大多數(shù)都是針對于service層來說的。
這里要著重提到的一點,就是分布式事務。

如上圖,四個操作分散在三個不同的資源中。要想達到一致性,需要三個不同的資源進行統(tǒng)一協(xié)調(diào)。它們底層的協(xié)議,以及實現(xiàn)方式,都是不一樣的。那就無法通過Spring提供的Transaction注解來解決,需要借助外部的組件來完成。
很多人都體驗過,加入了一些保證一致性的代碼,一壓測,性能掉的驚掉下巴。分布式事務是性能殺手,因為它要使用額外的步驟去保證一致性,常用的方法有:兩階段提交方案、TCC、本地消息表、MQ事務消息、分布式事務中間件等。

如上圖,分布式事務要在改造成本、性能、實效等方面進行綜合考慮。有一個介于分布式事務和非事務之間的名詞,叫做柔性事務。柔性事務的理念是將業(yè)務邏輯和互斥操作,從資源層上移至業(yè)務層面。
關(guān)于傳統(tǒng)事務和柔性事務,我們來簡單比較一下。
ACID
關(guān)系數(shù)據(jù)庫, 最大的特點就是事務處理, 即滿足ACID。
原子性(Atomicity):事務中的操作要么都做,要么都不做。 一致性(Consistency):系統(tǒng)必須始終處在強一致狀態(tài)下。 隔離性(Isolation):一個事務的執(zhí)行不能被其他事務所干擾。 持續(xù)性(Durability):一個已提交的事務對數(shù)據(jù)庫中數(shù)據(jù)的改變是永久性的。
BASE
BASE方法通過犧牲一致性和孤立性來提高可用性和系統(tǒng)性能。
BASE為Basically Available, Soft-state, Eventually consistent三者的縮寫,其中BASE分別代表:
基本可用(Basically Available):系統(tǒng)能夠基本運行、一直提供服務。 軟狀態(tài)(Soft-state):系統(tǒng)不要求一直保持強一致狀態(tài)。 最終一致性(Eventual consistency):系統(tǒng)需要在某一時刻后達到一致性要求。
互聯(lián)網(wǎng)業(yè)務,推薦使用補償事務,完成最終一致性。比如,通過一系列的定時任務,完成對數(shù)據(jù)的修復。具體可以參照下面的文章。
常用的 分布式事務 都有哪些?我該用哪個?
Dao層
經(jīng)過合理的數(shù)據(jù)緩存,我們都會盡量避免請求穿透到Dao層。除非你對ORM本身提供的緩存特性特別的熟悉,否則,都推薦你使用更加通用的方式去緩存數(shù)據(jù)。
Dao層,主要在于對ORM框架的使用上。比如,在JPA中,如果加了一對多或者多對多的映射關(guān)系,而又沒有開啟懶加載,級聯(lián)查詢的時候就容易造成深層次的檢索,造成了內(nèi)存開銷大、執(zhí)行緩慢的后果。
在一些數(shù)據(jù)量比較大的業(yè)務中,多采用分庫分表的方式。在這些分庫分表組件中,很多簡單的查詢語句,都會被重新解析后分散到各個節(jié)點進行運算,最后進行結(jié)果合并。
舉個例子,select count(*) from a這句簡單的count語句,就可能將請求路由到十幾張表中去運算,最后在協(xié)調(diào)節(jié)點進行統(tǒng)計,執(zhí)行效率是可想而知的。目前,分庫分表中間件,比較有代表性的是驅(qū)動層的ShardingJdbc和代理層的MyCat,它們都有這樣的問題。這些組件提供給使用者的視圖是一致的,但我們在編碼的時候,一定要注意這些區(qū)別。
End
下面我們來總結(jié)一下。
我們簡單看了一下SpringBoot常見的優(yōu)化思路。我們介紹了三個新的性能分析工具。一個是監(jiān)控系統(tǒng)Prometheus,可以看到一些具體的指標大??;一個是火焰圖,可以看到具體的代碼熱點;一個是Skywalking,可以分析分布式環(huán)境中的調(diào)用鏈。在對性能有疑惑的時候,我們都會采用類似于神農(nóng)氏嘗百草的方式,綜合各種測評工具的結(jié)果進行分析。
SpringBoot自身的Web容器是Tomcat,那我們就可以通過對Tomcat的調(diào)優(yōu)來獲取性能提升。當然,對于服務上層的負載均衡Nginx,我們也提供了一系列的優(yōu)化思路。
最后,我們看了在經(jīng)典的MVC架構(gòu)下,Controller、Service、Dao的一些優(yōu)化方向,并著重看了Service層的分布式事務問題。
SpringBoot作為一個廣泛應用的服務框架,在性能優(yōu)化方面已經(jīng)做了很多工作,選用了很多高速組件。比如,數(shù)據(jù)庫連接池默認使用hikaricp,Redis緩存框架默認使用lettuce,本地緩存提供caffeine等。對于一個普通的于數(shù)據(jù)庫交互的Web服務來說,緩存是最主要的優(yōu)化手。
完!
程序汪資料鏈接
堪稱神級的Spring Boot手冊,從基礎(chǔ)入門到實戰(zhàn)進階
臥槽!字節(jié)跳動《算法中文手冊》火了,完整版 PDF 開放下載!
臥槽!阿里大佬總結(jié)的《圖解Java》火了,完整版PDF開放下載!
字節(jié)跳動總結(jié)的設計模式 PDF 火了,完整版開放下載!
歡迎添加程序汪個人微信 itwang009? 進粉絲群或圍觀朋友圈

