SpringBoot 已經成為 Java 屆的 No.1 框架,每天都在蹂躪著數百萬的程序員們。當服務的壓力上升,對 SpringBoot 服務的優化就會被提上議程。
本文將詳細講解 SpringBoot 服務優化的一般思路,適合收藏之。
有監控才有方向
在開始對 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 的緩存命中率等情況。
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 文件,這個文件是可以用瀏覽器打開的,一層層向下瀏覽,即可找到需要優化的目標。
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 又比較高的的接口,進行專項優化。
優化思路
對一個普通的 Web 服務來說,我們來看一下,要訪問到具體的數據,都要經歷哪些主要的環節。
如下圖,在瀏覽器中輸入相應的域名,需要通過 DNS 解析到具體的 IP 地址上。為了保證高可用,我們的服務一般都會部署多份,然後使用 Nginx 做反向代理和負載均衡。
Nginx 根據資源的特性,會承擔一部分動靜分離的功能。其中,動態功能部分,會進入我們的 SpringBoot 服務。
SpringBoot 默認使用內嵌的 tomcat 作為 Web 容器,使用典型的 MVC 模式,最終訪問到我們的數據。
HTTP優化
下面我們舉例來看一下,哪些動作能夠加快網頁的獲取。為了描述方便,我們僅討論 HTTP1.1 協議的。
| 使用 CDN 加速文件獲取
比較大的文件,盡量使用 CDN(Content Delivery Network)分發。甚至是一些常用的前端腳本、樣式、圖片等,都可以放到 CDN 上。CDN 通常能夠加快這些文件的獲取,網頁加載也更加迅速。
| 合理設置 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;
}
| 減少單頁面請求域名的數量
減少每個頁面請求的域名數量,盡量保證在 4 個之內。這是因為,瀏覽器每次訪問後端的資源,都需要先查詢一次 DNS,然後找到 DNS 對應的 IP 地址,再進行真正的調用。
DNS 有多層緩存,比如瀏覽器會緩存一份、本地主機會緩存、ISP 服務商緩存等。從 DNS 到 IP 地址的轉變,通常會花費 20-120ms 的時間。減少域名的數量,可加快資源的獲取。
| 開啟 gzip
開啟 gzip,可以先把內容壓縮後,瀏覽器再進行解壓。由于減少了傳輸的大小,會減少帶寬的使用,提高傳輸效率。
在 Nginx 中可以很容易的開啟。配置如下︰
gzipon;
gzip_min_length1k;
gzip_buffers416k;
gzip_comp_level6;
gzip_http_version1.1;
gzip_types text/plain application/javascript text/css;
| 對資源進行壓縮
對 JavaScript 和 CSS,甚至是 HTML 進行壓縮。道理類似,現在流行的前後端分離模式,一般都是對這些資源進行壓縮的。
| 使用 keepalive
由于連接的創建和關閉,都需要耗費資源。用戶訪問我們的服務後,後續也會有更多的互動,所以保持長連接可以顯著減少網絡交互,提高性能。
nginx 默認開啟了對客戶端的 keep avlide 支持。你可以通過下面兩個參數來調整它的行為。
http {
keepalive_timeout120s120s;
keepalive_requests10000;
}
nginx 與後端 upstream 的長連接,需要手工開啟,參考配置如下︰
location ~ /{
proxy_pass http://backend;
proxy_http_version1.1;
proxy_set_header Connection "";
}
Tomcat優化
Tomcat 本身的優化,也是非常重要的一環。可以直接參考︰《搞定tomcat重要參數調優!》
自定義Web容器
如果你的項目並發量比較高,想要修改最大線程數、最大連接數等配置信息,可以通過自定義 Web 容器的方式,代碼如下所示︰
@SpringBootApplication(proxyBeanMethods = false)
publicclassAppimplementsWebServerFactoryCustomizer<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 之後才有,開啟之後會增加一部分性能。對比如下︰
默認︰
[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 and100 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.501.50108.00100.00%
6471 requests in 30.03s, 39.31MB read
Socket errors: connect0, read0, write0, 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 and100 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.502.50128.00100.00%
7469 requests in 30.04s, 45.38MB read
Socket errors: connect0, read0, write0, 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>
各個層次的優化方向
| 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,它們都有這樣的問題。
這些組件提供給使用者的視圖是一致的,但我們在編碼的時候,一定要注意這些區別。
總 結
下面我們來總結一下。我們簡單看了一下 SpringBoot 常見的優化思路,介紹了三個新的性能分析工具。
一個是監控系統 Prometheus,可以看到一些具體的指標大小;一個是火焰圖,可以看到具體的代碼熱點;一個是 Skywalking,可以分析分布式環境中的調用鏈。
在對性能有疑惑的時候,我們都會采用類似于神農氏嘗百草的方式,綜合各種測評工具的結果進行分析。
SpringBoot 自身的 Web 容器是 Tomcat,那我們就可以通過對 Tomcat 的調優來獲取性能提升。當然,對于服務上層的負載均衡 Nginx,我們也提供了一系列的優化思路。
最後,我們看了在經典的 MVC 架構下,Controller、Service、Dao 的一些優化方向,並著重看了 Service 層的分布式事務問題。
SpringBoot 作為一個廣泛應用的服務框架,在性能優化方面已經做了很多工作,選用了很多高速組件。
比如,數據庫連接池默認使用 hikaricp,Redis 緩存框架默認使用 lettuce,本地緩存提供 caffeine 等。
對于一個普通的于數據庫交互的 Web 服務來說,緩存是最主要的優化手,但細節決定成敗。 |