黑莲技术资源论坛

作者: 123456790
查看: 76|回复: 0

SpringBoot性能優化大全,賊好使

SpringBoot性能優化大全,賊好使

[复制链接]
123456790 | 显示全部楼层 发表于: 2022-8-6 09:43:14
123456790 发表于: 2022-8-6 09:43:14 | 显示全部楼层 |阅读模式
查看: 76|回复: 0
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 服務來說,緩存是最主要的優化手,但細節決定成敗。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|小黑屋|黑莲技术资源论坛 ( 闽ICP备18016623号-7 )|网站地图

GMT+8, 2022-8-19 04:12 , Processed in 0.581619 second(s), 26 queries .

Powered by BBS.HL1.NET X3.4 © 2020-2022

本站IT社区(bbs.hl1.net)所有的资源教程均来自网友分享及互联网收集

快速回复 返回顶部 返回列表