專案上線一年,整理了一份SpringBoot效能優化方案!

語言: CN / TW / HK

文章來源:https://juejin.cn/post/7062548565800779789

前言

SpringBoot已經成為Java屆的No.1框架,每天都在蹂躪著數百萬的程式設計師們。當服務的壓力上升,對SpringBoot服務的優化就會被提上議程。

本文將詳細講解SpringBoot服務優化的一般思路,並附上若干篇輔助文章作為開胃菜。

1.有監控才有方向

在開始對SpringBoot服務進行效能優化之前,我們需要做一些準備,把SpringBoot服務的一些資料暴露出來。

比如,你的服務用到了快取,就需要把快取命中率這些資料進行收集;用到了資料庫連線池,就需要把連線池的引數給暴露出來。

我們這裡採用的監控工具是Prometheus,它是一個是時序資料庫,能夠儲存我們的指標。SpringBoot可以非常方便的接入到Prometheus中。

建立一個SpringBoot專案後,首先,加入maven依賴。

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>

然後,我們需要在 application.properties  配置檔案中,開放相關的監控介面。

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 來獲取監控資料。

想要監控業務資料也是比較簡單的。你只需要注入一個MeterRegistry例項即可。下面是一段示例程式碼:

@Autowired
MeterRegistry registry;

@GetMapping("/test")
@ResponseBody
public String test() {
registry.counter("test",
"from", "127.0.0.1",
"method", "test"
).increment();

return "ok";
}

從監控連線中,我們可以找到剛剛新增的監控資訊。

test_total{from="127.0.0.1",method="test",} 5.0

這裡簡單介紹一下流行的Prometheus監控體系,Prometheus使用拉的方式獲取監控資料,這個暴露資料的過程可以交給功能更加齊全的telegraf元件。

如圖,我們通常使用Grafana進行監控資料的展示,使用AlertManager元件進行提前預警。這一部分的搭建工作不是我們的重點,感興趣的同學可自行研究。下圖便是一張典型的監控圖,可以看到Redis的快取命中率等情況。

2.Java生成火焰圖

火焰圖是用來分析程式執行瓶頸的工具。在縱向,表示的是呼叫棧的深度;橫向表明的是消耗的時間。所以格子的寬度越大,越說明它可能是一個瓶頸。

火焰圖也可以用來分析Java應用。可以從github上下載 async-profiler 的壓縮包 進行相關操作。

比如,我們把它解壓到/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 檔案,這個檔案是可以用瀏覽器開啟的,一層層向下瀏覽,即可找到需要優化的目標。

3.Skywalking

對於一個web服務來說,最緩慢的地方就在於資料庫操作。所以,使用本地快取和分散式快取優化,能夠獲得最大的效能提升。

對於如何定位到複雜分散式環境中的問題,我這裡想要分享另外一個工具:Skywalking。

Skywalking是使用探針技術(JavaAgent)來實現的。通過在Java的啟動引數中,加入javaagent的Jar包,即可將效能資料和呼叫鏈資料封裝、傳送到Skywalking的伺服器。

下載相應的安裝包(如果使用ES儲存,需要下載專用的安裝包),配置好儲存之後,即可一鍵啟動。

將agent的壓縮包,解壓到相應的目錄。

tar xvf skywalking-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又比較高的的介面,進行專項優化。

4.優化思路

對一個普通的Web服務來說,我們來看一下,要訪問到具體的資料,都要經歷哪些主要的環節。

如下圖,在瀏覽器中輸入相應的域名,需要通過DNS解析到具體的IP地址上。為了保證高可用,我們的服務一般都會部署多份,然後使用Nginx做反向代理和負載均衡。

Nginx根據資源的特性,會承擔一部分動靜分離的功能。其中,動態功能部分,會進入我們的SpringBoot服務。

SpringBoot預設使用內嵌的tomcat作為Web容器,使用典型的MVC模式,最終訪問到我們的資料。

5.HTTP優化

下面我們舉例來看一下,哪些動作能夠加快網頁的獲取。為了描述方便,我們僅討論HTTP1.1協議的。

1.使用CDN加速檔案獲取

比較大的檔案,儘量使用CDN(Content Delivery Network)分發。甚至是一些常用的前端指令碼、樣式、圖片等,都可以放到CDN上。CDN通常能夠加快這些檔案的獲取,網頁載入也更加迅速。

2.合理設定Cache-Control值

瀏覽器會判斷HTTP頭Cache-Control的內容,用來決定是否使用瀏覽器快取,這在管理一些靜態檔案的時候,非常有用。相同作用的頭資訊還有Expires。Cache-Control表示多久之後過期,Expires則表示什麼時候過期。

這個引數可以在Nginx的配置檔案中進行設定。

location ~* ^.+\.(ico|gif|jpg|jpeg|png)$ { 
# 快取1年
add_header Cache-Control: no-cache, max-age=31536000;
}

3.減少單頁面請求域名的數量

減少每個頁面請求的域名數量,儘量保證在4個之內。這是因為,瀏覽器每次訪問後端的資源,都需要先查詢一次DNS,然後找到DNS對應的IP地址,再進行真正的呼叫。

DNS有多層快取,比如瀏覽器會快取一份、本地主機會快取、ISP服務商快取等。從DNS到IP地址的轉變,通常會花費 20-120ms 的時間。減少域名的數量,可加快資源的獲取。

4.開啟gzip

開啟gzip,可以先把內容壓縮後,瀏覽器再進行解壓。由於減少了傳輸的大小,會減少頻寬的使用,提高傳輸效率。

在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進行壓縮。道理類似,現在流行的前後端分離模式,一般都是對這些資源進行壓縮的。

6.使用keepalive

由於連線的建立和關閉,都需要耗費資源。使用者訪問我們的服務後,後續也會有更多的互動,所以保持長連線可以顯著減少網路互動,提高效能。

nginx預設開啟了對客戶端的keep avlide支援。你可以通過下面兩個引數來調整它的行為。

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優化

Tomcat本身的優化,也是非常重要的一環。可以直接參考下面的文章。

搞定tomcat重要引數調優!

7.自定義Web容器

如果你的專案併發量比較高,想要修改最大執行緒數、最大連線數等配置資訊,可以通過自定義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);
});
}
}

注意上面的程式碼,我們設定了它的協議為 org.apache.coyote.http11.Http11Nio2Protocol ,意思就是開啟了Nio2。這個引數在Tomcat8.0之後才有,開啟之後會增加一部分效能。對比如下:

預設。

[[email protected] 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。

[[email protected] 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容器,更加輕量級一些,佔用的內容更少,啟動的守護程序也更少,更改方式如下:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

8.各個層次的優化方向

Controller層

controller層用於接收前端的查詢引數,然後構造查詢結果。現在很多專案都採用前後端分離的架構,所以controller層的方法,一般會使用@ResponseBody註解,把查詢的結果,解析成JSON資料返回(兼顧效率和可讀性)。

由於controller只是充當了一個類似功能組合和路由的角色,所以這部分對效能的影響就主要體現在資料集的大小上。如果結果集合非常大,JSON解析元件就要花費較多的時間進行解析。

大結果集不僅會影響解析時間,還會造成記憶體浪費。假如結果集在解析成JSON之前,佔用的記憶體是10MB,那麼在解析過程中,有可能會使用20M或者更多的記憶體去做這個工作。我見過很多案例,由於返回物件的巢狀層次太深、引用了不該引用的物件(比如非常大的byte[]物件),造成了記憶體使用的飆升。

所以,對於一般的服務,保持結果集的精簡,是非常有必要的,這也是DTO(data transfer object)存在的必要。如果你的專案,返回的結果結構比較複雜,對結果集進行一次轉換是非常有必要的。

另外,可以使用非同步Servlet對Controller層進行優化。它的原理如下:Servlet 接收到請求之後,將請求轉交給一個非同步執行緒來執行業務處理,執行緒本身返回至容器,非同步執行緒處理完業務以後,可以直接生成響應資料,或者將請求繼續轉發給其它 Servlet。

Service層

service層用於處理具體的業務,大部分功能需求都是在這裡完成的。service層一般是使用單例模式(prototype),很少會儲存狀態,而 且可以被controller 複用。

service層的程式碼組織,對程式碼的可讀性、效能影響都比較大。我們常說的設計模式,大多數都是針對於service層來說的。

這裡要著重提到的一點,就是分散式事務。

如上圖,四個操作分散在三個不同的資源中。要想達到一致性,需要三個不同的資源進行統一協調。它們底層的協議,以及實現方式,都是不一樣的。那就無法通過Spring 提供的Transaction註解 來解決,需要藉助外部的元件來完成。

很多人都體驗過,加入了一些保證一致性的程式碼,一壓測,效能掉的驚掉下巴。分散式事務是效能殺手,因為它要使用額外的步驟去保證一致性,常用的方法有:兩階段提交方案、TCC、本地訊息表、MQ事務訊息、分散式事務中介軟體等。

如上圖,分散式事務要在改造成本、效能、實效等方面進行綜合考慮。有一個介於分散式事務和非事務之間的名詞,叫 柔性事務 柔性事務的理念是將業務邏輯和互斥操作,從資源層上移至業務層面。

關於傳統事務和柔性事務,我們來簡單比較一下。

ACID

關係資料庫, 最大的特點就是事務處理, 即滿足ACID。

  • 原子性(Atomicity):事務中的操作要麼都做,要麼都不做。

  • 一致性(Consistency):系統必須始終處在強一致狀態下。

  • 隔離性(Isolation):一個事務的執行不能被其他事務所幹擾。

  • 持續性(Durability):一個已提交的事務對資料庫中資料的改變是永久性的。

BASE

BASE方法通過犧牲一致性和孤立性來提高可用性和系統性能。

BASE為Basically Available, Soft-state, Eventually consistent三者的縮寫,其中BASE分別代表:

  • 基本可用(Basically Available):系統能夠基本執行、一直提供服務。

  • 軟狀態(Soft-state):系統不要求一直保持強一致狀態。

  • 最終一致性(Eventual consistency):系統需要在某一時刻後達到一致性要求。

網際網路業務,推薦使用補償事務,完成最終一致性。比如,通過一系列的定時任務,完成對資料的修復。具體可以參照下面的文章。

常用的 分散式事務 都有哪些?我該用哪個?

Dao層

經過合理的資料快取,我們都會盡量避免請求穿透到Dao層。除非你對ORM本身提供的快取特性特別的熟悉,否則,都推薦你使用更加通用的方式去快取資料。

Dao層 ,主要在於對ORM框架的使用上。比如,在JPA中,如果加了一對多或者多對多的對映關係,而又沒有開啟懶載入,級聯查詢的時候就容易造成深層次的檢索,造成了記憶體開銷大、執行緩慢的後果。

在一些資料量比較大的業務中,多采用分庫分表的方式。在這些分庫分表元件中,很多簡單的查詢語句,都會被重新解析後分散到各個節點進行運算,最後進行結果合併。

舉個例子, select count(*) from a 這句簡單的count語句,就可能將請求路由到十幾張表中去運算,最後在協調節點進行統計,執行效率是可想而知的。目前,分庫分表中介軟體,比較有代表性的是驅動層的ShardingJdbc和代理層的MyCat,它們都有這樣的問題。這些元件提供給使用者的檢視是一致的,但我們在編碼的時候,一定要注意這些區別。

End

下面我們來總結一下。

我們簡單看了一下SpringBoot常見的優化思路。我們介紹了三個新的效能分析工具。一個是監控系統Prometheus,可以看到一些具體的指標大小;一個是火焰圖,可以看到具體的程式碼熱點;一個是Skywalking,可以分析分散式環境中的呼叫鏈。在對效能有疑惑的時候,我們都會採用類 似於 神農氏嘗百草 方式,綜合各種測評工具的結果進行分析。

SpringBoot自身的Web容器是Tomcat,那我們就可以通過對Tomcat的調優來獲取效能提升。當然,對於服務上層的負載均衡Nginx,我們也提供了一系列的優化思路。

最後,我們看了在經典的MVC架構下,Controller、Service、Dao的一些優化方向,並著重看了Service層的分散式事務問題。

這裡有一個具體的優化示例。

5秒到1秒,記一次效果“非常”顯著的效能優化

SpringBoot作為一個廣泛應用的服務框架,在效能優化方面已經做了很多工作,選用了很多高速元件。比如,資料庫連線池預設使用hikaricp,Redis快取框架預設使用lettuce,本地快取提供caffeine等。對於一個普通的於資料庫交 互的Web服務來說,快取是最主要的優化手。但細節決定成敗,你要是想對系統做極致的優化,還需要參考下面的這篇文章。

卓越效能 の 軍火庫(非廣告)

歡迎掃碼加入儒猿技術交流群,每天晚上20:00都有Java面試、Redis、MySQL、RocketMQ、SpringCloudAlibaba、Java架構等技術答疑分享,更能跟小夥伴們一起交流技術

另外推薦儒猿課堂的1元系列課程給您,歡迎加入一起學習~

網際網路Java工程師面試突擊課

(1元專享)

SpringCloudAlibaba零基礎入門到專案實戰

(1元專享)

億級流量下的電商詳情頁系統實戰專案

(1元專享)

Kafka訊息中介軟體核心原始碼精講

(1元專享)

12個實戰案例帶你玩轉Java併發程式設計

(1元專享)

Elasticsearch零基礎入門到精通

(1元專享)

基於Java手寫分散式中介軟體系統實戰

(1元專享)

基於ShardingSphere的分庫分表實戰課

(1元專享)