天天看點

java性能優化實戰:如何優化SpringBoot 服務性能

作者:一個即将退役的碼農

#頭條創作挑戰賽#

在開始對 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
           

啟動之後,我們就可以通過通路監控接口來擷取監控資料。

java性能優化實戰:如何優化SpringBoot 服務性能

想要監控業務資料也是比較簡單的,你隻需要注入一個 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 元件。

java性能優化實戰:如何優化SpringBoot 服務性能

如上圖,我們通常使用 Grafana 進行監控資料的展示,使用 AlertManager 元件進行提前預警。這一部分的搭建工作不是我們的重點,感興趣的同學可以自行研究。

下圖便是一張典型的監控圖,可以看到 Redis 的緩存命中率等情況。

java性能優化實戰:如何優化SpringBoot 服務性能

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 檔案,這個檔案是可以用浏覽器打開的。 如下圖所示,縱向,表示的是調用棧的深度;橫向,表明的是消耗的時間。是以格子的寬度越大,越說明它可能是一個瓶頸。一層層向下浏覽,即可找到需要優化的目标。

java性能優化實戰:如何優化SpringBoot 服務性能

優化思路

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

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

java性能優化實戰:如何優化SpringBoot 服務性能

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 又比較高的接口,進行專項優化。

java性能優化實戰:如何優化SpringBoot 服務性能

各個層次的優化方向

1.Controller 層

controller 層用于接收前端的查詢參數,然後構造查詢結果。現在很多項目都采用前後端分離的架構,是以 controller 層的方法,一般會使用 @ResponseBody 注解,把查詢的結果,解析成 JSON 資料傳回(兼顧效率和可讀性)。

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

大結果集不僅會影響解析時間,還會造成記憶體浪費。

假如結果集在解析成 JSON 之前,占用的記憶體是 10MB,那麼在解析過程中,有可能會使用 20M 或者更多的記憶體去做這個工作。

我見過很多案例,由于傳回對象的嵌套層次太深、引用了不該引用的對象(比如非常大的 byte[] 對象),造成了記憶體使用的飙升。

是以,對于一般的服務,保持結果集的精簡,是非常有必要的,這也是 DTO(資料傳輸對象)存在的必要。如果你的項目,傳回的結果結構比較複雜,對結果集進行一次轉換是非常有必要的。

2.服務層

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

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

Service 層會頻繁使用更底層的資源,通過組合的方式擷取我們所需要的資料,大多數可以通過我們前面課時提供的優化思路進行優化。

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

java性能優化實戰:如何優化SpringBoot 服務性能

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

很多人都體驗過,加入了一些保證一緻性的代碼,一壓測,性能掉得驚掉下巴。分布式事務是性能殺手,因為它要使用額外的步驟去保證一緻性,常用的方法有:兩階段送出方案、TCC、本地消息表、MQ 事務消息、分布式事務中間件等。

java性能優化實戰:如何優化SpringBoot 服務性能

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

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

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 服務來說,緩存是最主要的優化手段。