Java 效能優化之——效能優化的過程方法與求職面經總結

程式人生-vincent發表於2020-12-25

效能優化需要多方面權衡

應用效能低,有很多方面的因素,比如業務需求層面、架構設計層面、硬體/軟體層面等,這裡主要是說的是軟體層面,但也不要忘記效能優化還有其他手段

  • 先舉個業務需求層面的例子。有一個報表業務,查詢非常緩慢,有時候甚至會造成記憶體溢位。經過分析,發現是查詢時間跨度範圍太大造成的,由於業務上的限制,將時間跨度縮小至 1 個月之內之後,查詢速度就快了很多
  • 再舉一個硬體層面的例子。有一個定時任務,可以算是 CPU 密集型的,每次都將 CPU 用得滿滿的。由於系統有架構上的硬傷,無法做到橫向擴容。技術經過評估,如果改造成按照資料分片執行的模式,則需要耗費長達 1 個月的工時。其實在這種情況下,我們通過增加硬體配置的方式,便能解決效能瓶頸問題,為業務改進贏得更多的時間

舉這兩個例子的目的是想要說明,效能優化有很多優化途徑,如果這個效能問題可以通過其他方式解決,那就儘量不要採用調整軟體程式碼的方式,我們儘可能地在效果、工時、手段這三方面之間進行權衡

 

如何找到優化目標?

通常,關注一個硬體資源(比如 CPU),主要關注以下基本要素

利用率: 一般是瞬時值,屬於取樣範圍,用來判斷有沒有峰值,比如 CPU 使用率

飽和度: 一般指資源是否被合理利用,能否用分擔更多的工作。比如,飽和度過高,新請求在特定 queue 裡排隊;再比如,記憶體利用率過低、CPU 利用率過高,就可以考慮空間換時間

錯誤資訊: 錯誤一般發生在問題嚴重的情況下,需要特別關注

聯想資訊: 對引起的原因進行猜測,並用更多的工具驗證猜想,猜測影響因素並不一定是準確的,只是幫助我們分析問題,比如系統響應慢很可能是大量使用了 SWAP 導致的

首先,我們需要找到效能優化的目標,我們依然從 CPU、記憶體、網路、I/O 等層面看一下效能瓶頸可能存在的匿藏之處

1.CPU

檢視 CPU 使用可以使用 top 命令,尤其注意它的負載(load)和使用率,vmstat 命令也可以看到系統的一些執行狀況,我們這裡關注上下文切換和 swap 分割槽的使用情況

2.記憶體

記憶體可以使用 free 命令檢視,尤其關注剩餘記憶體的大小(free)。對於 Linux 系統來說,啟動之後由於各種快取和緩衝區的原因,系統記憶體會被迅速佔滿,所以我們更加關注的是 JVM 的記憶體

top 命令的 RES 列,顯示的就是程式實際佔用的實體記憶體,這個值通常比 jmap 命令獲取的堆記憶體要大,因為它還包含大量的堆外記憶體空間

3.網路

iotop 可以看到佔用網路流量最高的程式;通過 netstat 命令或者 ss 命令,能夠看到當前機器上的網路連線彙總。在一些較底層的優化中,會涉及針對 mtu 的網路優化

4.I/O

通過 iostat 命令,可以檢視磁碟 I/O 的使用情況,如果利用率過高,就需要從使用源頭找原因;類似 iftop,iotop 可以檢視佔用 I/O 最多的程式,很容易可以找到優化目標

5.通用

lsof 命令可以檢視當前程式所關聯的所有資源;sysctl 命令可以檢視當前系統核心的配置引數; dmesg 命令可以顯示系統級別的一些資訊,比如被作業系統的 oom-killer 殺掉的程式就可以在這裡找到

常用工具集合

1.資訊收集

nmon 是一個可以輸出系統整體效能資料的命令列工具,應用較為廣泛

jvisualvm 和 jmc,都是用來獲取 Java 應用效能資料的工具。由於它們是 UI 工具,應用需要開啟 JMX 埠才能夠被遠端連線

2.監控

像 top 這樣的命令,只在問題發生的時候才會有作用。但很多時候,當發生效能問題時,我們並不在電腦旁邊,這就需要有一套工具,定期抓取這些效能資料。通過監控系統,能夠獲取監控指標的歷史時序,通過分析指標趨勢,可估算效能瓶頸點,從資料上支撐我們的分析

目前最流行的組合是 prometheus + grafana + telegraf,可以搭功能強大的監控平臺

3.壓測工具

有時候,我們需要評估系統在一定併發量下面的效能,這時候就可以通過壓測工具給予系統一些壓力

wrk 是一個命令列工具,可以對 HTTP 介面進行壓測;jmeter 是較為專業的壓測工具,可以生成壓測報告。壓測工具配合監控工具,可以正確評估系統當前的效能

4.效能深挖

大多數情況下,僅通過概括性的效能指標,我們無法知曉效能瓶頸的具體細節,這就需要一些比較深入的工具進行追蹤

skywalking 可以用來分析分散式環境下的呼叫鏈問題,可以詳細地看到每一步執行的耗時。但如果你沒有這樣的環境,就可以使用命令列工具 arthas 對方法進行 trace,最終也能夠深挖找到具體的慢邏輯

jvm-profiling-tools,可以生成火焰圖,輔助我們分析問題。另外,更加底層的,針對作業系統的效能測評和調優工具,還有perf和SystemTap,感興趣可以自行研究一下

基本解決方式

1.CPU 問題

CPU 是系統的核心資源,如果 CPU 有瓶頸,很多工和執行緒就獲取不到時間片,便會執行緩慢。如果此時系統的記憶體充足,就要考慮是否可以空間換時間,通過資料冗餘和更優的演算法來減少 CPU 的使用

在 Linux 系統上,通過 top-Hp 便能容易地獲取佔用 CPU 最高的執行緒,進行鍼對性的優化,資源的使用要細分,才能夠進行專項優化

曾經碰見一個棘手的效能問題,執行緒都阻塞在 ForkJoin 執行緒池上,經過仔細排查才分析出,程式碼在等待耗時的 I/O 時,採用了並行流(parallelStrea)處理,但是 Java 預設的方式是所有使用並行流的地方,公用了一個通用的執行緒池,這個執行緒池的並行度只有 CPU 的兩倍。所以請求量一增加,任務就會排隊,造成積壓

2.記憶體問題

記憶體問題通常是 OOM 問題,如果記憶體資源很緊張,CPU 利用率低,則可以考慮時間換空間的方式

SWAP 分割槽使用硬碟來擴充套件可用記憶體的大小,但它的速度非常慢。一般在高併發的應用中,會把 SWAP 關掉,因為它很容易會引起卡頓

3.I/O 問題

我們通常開發的業務系統,磁碟 I/O 負載都比較小,但網路 I/O 都比較繁忙

當遇到磁碟 I/O 佔用高的情況,就要考慮是否是日誌列印得太多導致的。通過調整日誌級別,或者清理無用的日誌程式碼,便可緩解磁碟 I/O 的壓力。業務系統還會有大量的網路 I/O 操作,比如通過 RPC 呼叫一個遠端的服務,我們期望使用 NIO 來減少一些無效的等待,或者使用並行來加快資訊的獲取

還有一種情況,是類似於 ES 這樣的資料庫應用,資料寫入本身,就會造成繁重的磁碟 I/O。這個時候,可以增加硬體的配置,比如換成 SSD 磁碟,或者增加新的磁碟

資料庫服務本身,也會提供非常多的引數,用來調優效能。這部分的配置引數,主要影響緩衝和快取的行為

比如 ES 的 segment 塊大小,translog 的重新整理速度等,都可以被微調。舉個例子,大量日誌寫入 ES 的時候,就可以通過增大 translog 寫盤的間隔,來獲得較大的效能提升

4.網路問題

資料包在網路上傳輸,影響的主要因素就是結果集的大小。通過去除無用的資訊,啟用合理的壓縮,可以獲得較大的效能提升。值得注意的是,這裡的網路傳輸值得不僅僅是針對瀏覽器的,在服務間呼叫中也有著同樣的情況

比如,在 SpringBoot 的配置檔案中,通過配置下面的引數,就可以開啟 gzip

server:
  compression:
    enabled: true
    min-response-size: 1024
    mime-types: ["text/html","application/json","application/octet-stream"]

但是,這個 SpringBoot 服務,通過 Feign 介面從另外一個服務獲取資訊,這個結果集並沒有被壓縮,可以通過替換 Feign 的底層網路工具為 OkHTTP,使用 OkHTTP 的透明壓縮(預設開啟 gzip),即可完成服務間呼叫的資訊壓縮

網路 I/O 的另外一個問題就是頻繁的網路互動,通過將結果集合並,使用批量的方式,可以顯著增加效能,但這種方式的使用場景有限,比較適合非同步的任務處理

使用 netstat 命令,或者 lsof 命令,可以獲取程式所關聯的,TIME_WAIT 和 CLOSE_WAIT 網路狀態的數量,前者可以通過調整核心引數來解決,但後者多是應用程式的 BUG

可以說如果上面的基本解決方式面向的是“面”,那麼程式碼層面的優化,面向的就是具體的“效能瓶頸點”

 

程式碼層面

1.中間層

不同資源之間相互呼叫的效能瓶頸,主要在於資源的速度差異上。解決方式主要是加入一箇中間層,有緩衝 / 快取,以及池化這三種形態,以犧牲資訊的時效性為代價,加快資訊的處理速度

緩衝,使得資源兩方,都能按照自己的節奏進行操作的同時,也可以完全地順序銜接起來。它能夠消除兩方的速度差異,以批量的方式,來減少效能損耗

快取,在系統中的應用非常廣泛,有堆內快取和分散式快取之分。有些對效能要求非常高的場景,甚至會有多級快取的組合形態。我們的目標是儘量提高快取的命中率,以便中間層得其所用

另一種中間層形態,就是對資源進行集中管控,以池化的思想來減少物件的建立成本。在物件的建立成本比較大時,才能體現到池化的價值,否則只會增加程式碼的複雜度

2.資源同步

在我們的編碼中,有時候對資料的一致性要求比較高,就不得不用到鎖和事務,不管是執行緒鎖還是分散式鎖,甚至是適合讀多寫少場景的樂觀鎖,都有一些通用的優化法則

  • 第一,切分衝突資源的粒度,這樣就可以分而治之;

  • 第二,減少資源鎖定的時間,儘快釋放共享資源;

  • 第三,將讀操作與寫操作區分開,進一步減少衝突發生的可能

普通的事務可以通過 Spring 的 @Transactional 註解簡單的實現,但通常業務會涉及多個異構的資源。如無必要,非常不推薦使用分散式事務去解決,而應該採用最終一致性的思想,將互斥操作從資源層上移至業務層

3.組織優化

  • 另外一種有效的方式是通過重構,改變我們程式碼的組織結構

通過設計模式,可以讓我們的程式碼邏輯更加清晰,在效能優化的時候,可以直接定位到要優化的程式碼。曾見過很多需要效能調優的應用程式碼,由於物件的關係複雜和程式碼組織的混亂,想要加入一箇中間層是相當困難的。這個時候,首要的任務是梳理、重構這些程式碼,否則很難進行進一步的效能優化

  • 另外一個對程式設計模式影響較大的就是非同步化

非同步化多采用生產者消費者模式,來減少同步等待造成的效能損耗,但它的程式設計模型難度較大,需要很多額外的工作。比如我們使用 MQ 完成了非同步化,就不得不考慮訊息失敗、重複、死信等保障性功能(產品形態上的改變,不在討論範圍之內)

4.資源利用不足

並不是說系統的資源利用率越低,我們的程式碼寫得就越好。作為一個編碼者,我們要想方設法壓榨系統的剩餘價值,讓所有的資源都輪轉起來。尤其在高併發場景下,這種輪轉就更加重要——屬於在一定壓力下系統的最優狀態

資源不能合理的利用,就是一種浪費。比如,業務應用多屬於 I/O 密集型業務,如果讓請求都阻塞在 I/O 上,就造成了 CPU 資源的浪費。這時候使用並行,就可以在同一時刻承擔更多的任務,併發量就能夠增加;再比如,我們監控到 JVM 的堆空閒空間,長期處於高位,那就可以考慮加大堆內快取的容量,或者緩衝區的容量

PDCA 迴圈方法論

效能優化是一個迴圈的過程,需要根據資料反饋進行實時調整。有時候,測試結果表明,有些優化的效果並不好,就需要回滾到優化前的版本,重新尋找突破點

如上圖,PDCA 迴圈的方法論可以支援我們管理效能優化的過程,它有 4 個步驟

  • P(Planning)計劃階段,找出存在的效能問題,收集效能指標資訊,確定要改進的目標,準備達到這些目標的具體措施;

  • D(do)執行階段,按照設計,將優化措施付諸實踐;

  • C(check)檢查階段,及時檢查優化的效果,及時發現改進過程中的經驗及問題;

  • A(act)處理階段,將成功的優化經驗進行推廣,由點及面進行覆蓋,為負面影響提供解決方案,將錯誤的方法形成經驗

如此周而復始,應用的效能將會逐步提高,如下圖,對於效能優化來說,就可以抽象成下面的方式

既然叫作迴圈,就說明這個過程是可以重複執行的。事實上,在我們的努力下,應用效能會螺旋式上升,最終達到我們的期望

 

求職

1. 關注“效能優化”的副作用問題

效能優化的面試題,一般都是穿插在其他題目裡的。你不僅需要關注“效能優化”本身,還需關注“效能優化”之後的問題,因為等你答出面試官想要的效能優化方案之後,面試官接下來便會追問“這個方案所引起的其他問題”

比如,當你談到你使用快取提高了介面的效能時,面試官會接著問你一些關於快取同步的問題

2.掌握好“效能優化”基礎知識

另外,從上面的總結我們就可以看出,效能優化涉及的知識點非常多,那如何在有限的面試時間裡儘量多地展現自己呢?那便是打好知識基礎,能夠對問題進行詳細準確地作答

  • 你都對JVM做了那些優化,有哪些效能提升?

  • 為什麼網際網路場景下通常使用樂觀鎖?

上述兩個問題比較好回答,因為它的答案相對確定,你只需要講清楚特定的知識點就可以了,而比較麻煩的會是下來這類題目

3.發散、綜合性題目提前準備

如果上面的題是圍繞“點”,那麼下面的題便是圍繞一個“面”

  • 你在專案中做過哪些效能優化方面的工作?

  • 你是如何指導團隊做效能優化的?

如果你僅針對某個知識點進行描述,那麼你的答案就顯得非常單薄。其實你可以從問題發現、問題解決、問題驗證等方面系統性地分別進行描述,並著重談一下在這一過程中自己認為最重要並最熟悉的知識點

相關文章