#頭條創作挑戰賽#
在開始對 SpringBoot 服務進行性能優化之前,你需要做一些準備,把 SpringBoot 服務的一些資料暴露出來。比如,你的服務用到了緩存,就需要把緩存命中率這些資料進行收集;用到了資料庫連接配接池,就需要把連接配接池的參數給暴露出來。
我們這裡采用的監控工具是 Prometheus,它是一個是時序資料庫,能夠存儲我們的名額。SpringBoot 可以非常友善地接入到 Prometheus 中。
SpringBoot 如何開啟監控?
建立一個 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
啟動之後,我們就可以通過通路監控接口來擷取監控資料。
想要監控業務資料也是比較簡單的,你隻需要注入一個 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 檔案,這個檔案是可以用浏覽器打開的。 如下圖所示,縱向,表示的是調用棧的深度;橫向,表明的是消耗的時間。是以格子的寬度越大,越說明它可能是一個瓶頸。一層層向下浏覽,即可找到需要優化的目标。
優化思路
對一個普通的 Web 服務來說,我們來看一下,要通路到具體的資料,都要經曆哪些主要的環節?
如下圖,在浏覽器中輸入相應的域名,需要通過 DNS 解析到具體的 IP 位址上,為了保證高可用,我們的服務一般都會部署多份,然後使用 Nginx 做反向代理和負載均衡。
Nginx 根據資源的特性,會承擔一部分動靜分離的功能。其中,動态功能部分,會進入我們的SpringBoot 服務。
SpringBoot 預設使用内嵌的 tomcat 作為 Web 容器,使用典型的 MVC 模式,最終通路到我們的資料。
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 與後端 上遊的長連接配接,需要手工開啟,參考配置如下:
location ~ /{
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
自定義 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。這個參數在 Tomcat 8.0之後才有,開啟之後會增加一部分性能。 對比如下(測試項目代碼見 spring-petclinic-main):
預設。
[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 容器,更加輕量級一些,占用的記憶體更少,啟動的守護程序也更少,更改方式如下:
<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>
其實,對于 tomcat 優化最為有效的,還是 JVM 參數的配置,你可以參考上一課時的内容進行調整。 比如,使用下面的參數啟動,QPS 由 248 上升到 308。
-XX:+UseG1GC -Xmx2048m -Xms2048m -XX:+AlwaysPreTouch
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,即可看到下圖的界面。這些名額可以類比“01 | 理論分析:性能優化,有哪些衡量名額?需要注意什麼?”提到的衡量名額去了解,我們就可以從圖中找到響應比較慢 QPS 又比較高的接口,進行專項優化。
各個層次的優化方向
1.Controller 層
controller 層用于接收前端的查詢參數,然後構造查詢結果。現在很多項目都采用前後端分離的架構,是以 controller 層的方法,一般會使用 @ResponseBody 注解,把查詢的結果,解析成 JSON 資料傳回(兼顧效率和可讀性)。
由于 controller 隻是充當了一個類似功能組合和路由的角色,是以這部分對性能的影響就主要展現在資料集的大小上。如果結果集合非常大,JSON 解析元件就要花費較多的時間進行解析,
大結果集不僅會影響解析時間,還會造成記憶體浪費。
假如結果集在解析成 JSON 之前,占用的記憶體是 10MB,那麼在解析過程中,有可能會使用 20M 或者更多的記憶體去做這個工作。
我見過很多案例,由于傳回對象的嵌套層次太深、引用了不該引用的對象(比如非常大的 byte[] 對象),造成了記憶體使用的飙升。
是以,對于一般的服務,保持結果集的精簡,是非常有必要的,這也是 DTO(資料傳輸對象)存在的必要。如果你的項目,傳回的結果結構比較複雜,對結果集進行一次轉換是非常有必要的。
2.服務層
service 層用于處理具體的業務,大部分功能需求都是在這裡完成的。Service 層一般是使用單例模式(prototype),很少會儲存狀态,而且可以被 controller 複用。
Service 層的代碼組織,對代碼的可讀性、性能影響都比較大。我們常說的設計模式,大多數都是針對 service 層來說的。
Service 層會頻繁使用更底層的資源,通過組合的方式擷取我們所需要的資料,大多數可以通過我們前面課時提供的優化思路進行優化。
這裡要着重提到的一點,就是分布式事務。
如上圖,四個操作分散在三個不同的資源中。要想達到一緻性,需要三個不同的資源 MySQL、MQ、ElasticSearch 進行統一協調。它們底層的協定,以及實作方式,都是不一樣的,那就無法通過 Spring 提供的 Transaction 注解來解決,需要借助外部的元件來完成。
很多人都體驗過,加入了一些保證一緻性的代碼,一壓測,性能掉得驚掉下巴。分布式事務是性能殺手,因為它要使用額外的步驟去保證一緻性,常用的方法有:兩階段送出方案、TCC、本地消息表、MQ 事務消息、分布式事務中間件等。
如上圖,分布式事務要在改造成本、性能、時效等方面進行綜合考慮。有一個介于分布式事務和非事務之間的名詞,叫作柔性事務。柔性事務的理念是将業務邏輯和互斥操作,從資源層上移至業務層面。
關于傳統事務和柔性事務,我們來簡單比較一下。
1.關系資料庫
關系資料庫, 最大的特點就是事務處理, 即滿足 ACID。
- 原子性(Atomicity):事務中的操作要麼都做,要麼都不做。
- 一緻性(Consistency):系統必須始終處在強一緻狀态下。
- 隔離性(Isolation):一個事務的執行不能被其他事務所幹擾。
- 持久性(Durability):一個已送出的事務對資料庫中資料的改變是永久性的。
2.BASE
BASE 方法通過犧牲一緻性和孤立性來提高可用性和系統性能。
BASE 為 Basic Available、Soft-state、Eventually consistent 三者的縮寫,其中 BASE 分别代表:
- 基本可用(Basic Available):系統能夠基本運作、一直提供服務。
- 軟狀态(Soft-state):系統不要求一直保持強一緻狀态。
- 最終一緻性(Finalual consistency):系統需要在某一時刻後達到一緻性要求。
網際網路業務,推薦使用補償事務,完成最終一緻性。比如,通過一系列的定時任務,完成對資料的修複。
3.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 服務來說,緩存是最主要的優化手段。