1. 程式人生 > >一線架構師帶你玩效能優化

一線架構師帶你玩效能優化

1.什麼是系統優化

系統優化一個方面是系統化的對IT系統或交易鏈上的每個環節進行分析並優化,另一個是對單一系統進行瓶頸點分析和調優。但優化的目標大致相同,無非是提高系統的響應速度、吞吐量、降低各層耦合,以應對靈活對邊的市場。

系統優化的3個層次:IT架構治理層、系統層、基礎設施層。

  • IT系統治理層:優化的目的不只是效能優化,還會有為適應業務架構變化而帶來的應用架構優化(如:應用分層、服務治理等)。

  • 系統層:優化的目的包括業務流程優化、資料流程優化(如:提高系統負載、減少系統開銷等)

  • 基礎設施層:優化的目的主要是提高IAAS平臺的能力(如:建立彈性叢集具備橫向擴充套件能力,支援資源快速上下線和轉移等)。

2.系統優化的方法論和思路

什麼是方法論,我個人的理解就是聽起來很牛,做過的人認為是廢話,但可以指明行動方向或持續改進的東西。

2.1  常用方法論

(1)不訪問不必要的資料——減少交易線上不必要的環節,減少故障點和維護點。

(2)就近載入/快取為王——減少不必要的訪問。

(3)故障隔離——不要因為一個系統瓶頸壓垮整個交易平臺。

(4)具備良好的擴充套件能力——合理的利用資源、提高處理效率和避免單點故障。

(5)對交易鏈進行優化提高吞吐量——非同步/減少序列、合理拆分(垂直/水平拆分)、規則前置。

(6)效能和功能同等重要——交易鏈上5個性能變為設計階段90%後為則整體效能為設計時的59%。

2.2  優化的一般思路

640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1

2.3  優化的原則

  • 在應用系統的設計、開發過程用中,應始終把效能放在考慮的範圍內。

  • 確定清晰明確的效能目標是關鍵。

  • 效能調優是伴隨整個專案週期的,最好進行分階段設定目標開展,在達到預期效能目標之後即可對本階段工作進行總結和知識轉移進入下一階段調優工作。

  • 必須保證調優後的程式執行正確。

  • 效能更大程度是取決於良好的設計,調優技巧只是一個輔助手段。

  • 調優過程是疊代漸進的過程,每次調優的結果要反饋到後續的程式碼開發中去。

  • 效能調優不能以犧牲程式碼的可讀性和維護性為代價。

3效能調優

3.1  常見效能問題

3.1.1常見客戶端效能問題

  • 載入慢:第一次啟動慢或者重新載入慢;

  • 無響應:事件出發後頁面假死;

  • 受網路頻寬影響嚴重:因為需要下載大量資原始檔,在一些在網路環境不好的地區頁面;

  • JS記憶體溢位:頻繁對物件的屬性進行操作造成記憶體大量佔用最終溢位。

3.1.2常見的J2EE 系統性能問題

  • 記憶體洩漏:在執行過程中記憶體不斷被佔用而不能被回收,記憶體使用率隨時間或負載的增加呈線性增長,系統處理效率隨著時間或併發的增加而下降,直至將分配給JVM 的記憶體用盡而宕機,或重啟後系統短時間內可恢復正常。

  • 資源洩露:在將資源開啟後未關閉或未成功關閉的問題。這些資源包括資料來源連線,檔案流等。當這些資源經常被開啟而未能成功關閉,就會導致資源洩漏。資料連線洩漏就是常見的資源洩漏問題。

  • 過載:系統過度使用,超出系統所能承受的負荷。

  • 內部資源瓶頸:資源過度使用或分配不足引起資源瓶頸。

  • 執行緒阻塞、執行緒死鎖:執行緒退回到無法完成的同步點造成通訊阻塞。

  • 應用系統響應慢:由於應用本身或SQL不合理的問題,導致響應時間長。

  • 應用系統不穩定,時快時慢的現象發生。

  • 應用系統各種各樣異常情況發生:有些是中介軟體伺服器丟擲的異常、有些是資料端丟擲的異常。

3.1.3常見的資料庫問題

  • 死鎖:因為請求保持或者執行效率低不能及時釋放導或因為迴圈等待致表死鎖;

  • IO繁忙:因為不良SQL或業務邏輯設計不合理導致大量IO等待;

  • CPU使用率居高不下:高併發或快取穿透導致資料庫CPU居高不下或忽高忽低。

3.2  調優的具體工作

天下武功為快不破,首要的就是提高系統的響應時間(響應時間 = 服務處理時間 + 排隊時間),如經典的響應時間曲線所示,我們要做的就是通過程式優化減少服務響應時間,通過提高系統的吞吐量減少系統的排隊時間。

640?wx_fmt=png&wxfrom=5&wx_lazy=1 響應時間曲線(摘自《Oracle效能預測》)

縱軸是響應時間。響應時間是服務時間和排隊時間的總和。橫軸是到達率。隨著每單位時間進入系統事務數的遞增,曲線隨之向右滑動。隨著到達率的繼續增加,在某一時候,排隊時間將陡然上升。當這種情況發生時,響應時間也將陡然上升,效能下降,而使用者感到非常沮喪。

下面通過以往專案中的案例來分析效能優化的具體工作。

3.2.1交易線優化

交易線是從服務的消費者為出發點,看交易在各個層面應該完成的功能,以及功能點之間的關係。功能點之間的關係用有向路徑來表示:

640?wx_fmt=png&wxfrom=5&wx_lazy=1

交易線優化的原則:

  • 最短路徑:減少不必要的環節,避免故障點;

  • 交易完整性:通過沖正或補償交易等確保交易線各環節的事物一致性;

  • 故障隔離和快速定位:遮蔽異常情況對正常交易的影像,通過交易碼或錯誤碼能快速等位問題;

  • 流量控制原則:可以通過對服務通道進行流量控制,並結合優先順序設定優先處理級別高的業務;

  • 超時控制漏斗原則:儘量保持交易線上前端系統超時設定應該大於後端系統。

【案例】隨著架構的演變,過去一站是構建的豎井式系統,逐步發展為現在的以服務為單元可靈活構建的獨立單元:

640?wx_fmt=png&wxfrom=5&wx_lazy=1

在服務治理的過程中原來的核心業務系統被打碎為各種獨立的業務元件,一些中間層平臺型系統基於這些業務元件和流程服務逐漸構建了業務服務,併成為前端應用的快速構建提供業務支撐。在這個過程中服務識別和構建是基礎,交易線的規範是保障,通過交易線規範可以確定服務治理有所為,有所不為,這是因為隨軟體版本迭代,很少有個人能把系統的全部細節都考慮清楚,所以要以規則治理,而不是人治。

要開發一個訂單查詢功能A,服務整合平臺的B和C兩個服務都可以完成相同功能,只是B在C的基礎上增加了一些額外不需要的校驗,按照最短路徑原則這個時候A應該直接呼叫C服務。

640?wx_fmt=png&wxfrom=5&wx_lazy=1

當服務提供者D處理能力不足時,應該及時通知服務消費者C或者按照優先順序丟棄部分訪問通道的請求,前端消費者接收後端流量控制錯誤碼並及時通知使用者。這樣可以避免在系統達到容量限制後,所有使用者級別都被拒絕服務。流量控制的目的之一是保證各系統健康穩定執行。一般使用計數器按照交易型別來檢測交易的併發數,不同交易型別,使用不同計數器。當交易請求到達時,計數器加1,當請求響應或者超時,計數器減1。

3.2.2客戶端優化

客戶端優化的首要目標是加快頁面展現速度,其次是減少對服務端的呼叫。

常見解決辦法:

  • 分析瓶頸點,有針對性優化;

  • 快取為王,通過在客戶端快取靜態資料提升頁面響應時間;

  • 通過GZIP壓縮減少客戶端網路下載流量;

  • 使用壓縮工具對js進行壓縮,減少js檔案大小;

  • 刪除、合併指令碼、樣式表及圖片減少get請求;

  • 無阻塞載入JS

  • 預載入(圖片、css樣式、js指令碼);

  • 按需載入js指令碼;

  • 優化js處理方法提升頁面處理速度。

WEB請求時序圖:

640?wx_fmt=png&wxfrom=5&wx_lazy=1

【案例】下面是某企業內部應用系統客戶端HTTP請求監控記錄:

640?wx_fmt=png&wxfrom=5&wx_lazy=1

從上圖中可以看到共計傳送25次請求(21次命中快取、4次與服務端互動)。

640?wx_fmt=png&wxfrom=5&wx_lazy=1

從統計資訊可以看到:總計請求耗時5.645秒,進行4次網路互動,接收5.9KB資料。傳送110.25KB資料,GZIP壓縮節省了:8KB資料。

後來該頁面通過優化後端請求、合併和壓縮JS/JSP檔案等將頁面響應時間優化到2秒左右。

PS:前端優化最好了解瀏覽器原理、HTTP原理

3.2.3服務端優化

640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1

【案例】記一次資源洩露,具體表現為RESULT-SET未關閉:

640?wx_fmt=png&wxfrom=5&wx_lazy=1 RESULT-SET未關閉統計

根據堆疊跟蹤日誌檢視應用程式發現程式程式碼存在只關閉connection未關閉Statement和ResultSet的問題。

針對關閉connection是否會自動關閉Statement和ResultSet的問題,以及Statement和ResultSet所佔用資源是否會自動釋放問題,JDBC處理規範或JDK規範中做了如下描述:

JDBC處理規範

JDBC. 3.0 Specification——13.1.3 Closing Statement Objects

An application calls the method Statement.close toindicate that it has finished processing a statement. All Statement objectswill be closed when the connection that created them is closed. However, it isgood coding practice for applications to close statements as soon as they havefinished processing them. This allows any external resources that the statementis using to be released immediately.

Closing a Statement object will close and invalidateany instances of ResultSet produced by that Statement object. The resourcesheld by the ResultSet object may not be released until garbage collection runsagain, so it is a good practice to explicitly close ResultSet objects when theyare no longer needed.

These comments about closing Statement objects applyto PreparedStatement and CallableStatement objects as well.

JDBC. 4.0 Specification——13.1.4 Closing Statement Objects

An application calls the method Statement.close toindicate that it has finished processing a statement. All Statement objectswill be closed when the connection that created them is closed. However, it isgood coding practice for applications to close statements as soon as they havefinished processing them. This allows any external resources that the statementis using to be released immediately.

Closing a Statement object will close and invalidateany instances of ResultSet produced by that Statement object. The resourcesheld by the ResultSet object may not be released until garbage collection runsagain, so it is a good practice to explicitly close ResultSet objects when theyare no longer needed.

Once a Statement has been closed, any attempt toaccess any of its methods with the exception of the isClosed or close methodswill result in a SQLException being thrown.

These comments about closing Statement objects applyto PreparedStatement and CallableStatement objects as well.

規範說明:connection.close 自動關閉 Statement.close 自動導致 ResultSet 物件無效(注意只是 ResultSet 物件無效,ResultSet 所佔用的資源可能還沒有釋放)。所以還是應該顯式執行connection、Statement、ResultSet的close方法。特別是在使用connection pool的時候,connection.close 並不會導致物理連線的關閉,不執行ResultSet的close可能會導致更多的資源洩露。

JDK處理規範:

JDK1.4

Note: A ResultSet object is automatically closed by theStatement object that generated it when that Statement object is closed,re-executed, or is used to retrieve the next result from a sequence of multipleresults. A ResultSet object is also automatically closed when it is garbagecollected.

Note: A Statement object is automatically closed when it isgarbage collected. When a Statement object is closed, its current ResultSetobject, if one exists, is also closed.

Note: A Connection object is automatically closed when it is garbagecollected. Certain fatal errors also close a Connection object.

JDK1.5 

Releases this ResultSet object's database and JDBC resources immediatelyinstead of waiting for this to happen when it is automatically closed.

Note: A ResultSetobject is automatically closed by the Statement object that generated it whenthat Statement object is closed, re-executed, or is used to retrieve the nextresult from a sequence of multiple results. A ResultSet object is alsoautomatically closed when it is garbage collected.

規範說明:

1.垃圾回收機制可以自動關閉它們;

2.Statement關閉會導致ResultSet關閉;

3.Connection關閉不一定會導致Statement關閉。

現在應用系統都使用資料庫連線池,Connection關閉並不是物理關閉,只是歸還連線池,所以Statement和ResultSet有可能被持有,並且實際佔用相關的資料庫的遊標資源,在這種情況下,只要長期執行就有可能報“遊標超出資料庫允許的最大值”的錯誤,導致程式無法正常訪問資料庫。

針對該類問題建議:

(1)顯式關閉資料庫資源,尤其是使用Connection Pool的時候;

(2)最優經驗是按照ResultSet,Statement,Connection的順序執行close;

(3)為了避免由於java程式碼有問題導致記憶體洩露,需要在rs.close()和stmt.close()後面一定要加上rs = null和stmt = null,並做好異常處理;

(4)如果一定要傳遞ResultSet,應該使用RowSet,RowSet可以不依賴於Connection和Statement。

3.2.4JVM優化

針對JVM的引數調整是需要謹慎處理的。常見的JVM引數:

heap引數設定

-server –Xmx1G –Xms1G -Xmn512M-XX:PermSize=512M -XX:MaxPermSize=512M -XX:+UseCompressedOops

-server:選擇"server" VM,一定要作為第一個引數,與之相對的引數是-client,"client" VM,增加-server引數會影響jvm的其他引數預設值。HotSpot包括一個直譯器和兩個編譯器(client 和 server,二選一的),解釋與編譯混合執行模式,預設啟動解釋執行。server啟動慢,佔用記憶體多,執行效率高,適用於伺服器端應用,JDK1.6以後在具有64位能力的jdk環境下將預設啟用該模式; client啟動快,佔用記憶體小,執行效率沒有server快,預設情況下不進行動態編譯,通常用於客戶端應用程式或者PC應用開發和除錯。

PS:據報道Hotspot的某些版本Servermode被報告有穩定性問題,因此jvm採用server mode還是client mode 需要通過長時間系統監測來評測。

垃圾回收引數設定

-XX:+DisableExplicitGC-XX:+UseParNewGC-XX:+UseConcMarkSweepGC-XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection-XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSClassUnloadingEnabled

-XX:+DisableExplicitGC禁止System.gc(),免得程式設計師誤呼叫gc方法影響效能;

PS:根據歷史經驗一般垃圾回收時間佔比小於2%則認為對效能影響不大。

日誌類引數

-XX:+PrintClassHistogram -XX:+PrintGCDetails-XX:+PrintGCTimeStamps-Xloggc:log/gc.log 

-XX:+ShowMessageBoxOnError-XX:+HeapDumpOnOutOfMemoryError-XX:+HeapDumpOnCtrlBreak

除錯的時候設定一些日誌引數,如-XX:+PrintClassHistogram -XX:+PrintGCDetails-XX:+PrintGCTimeStamps -Xloggc:log/gc.log,這樣可以從gc.log裡檢視gc頻繁程度,根據此來評估對效能的影響。

除錯的時候設定異常宕機時產生heap dump檔案,-XX:+ShowMessageBoxOnError-XX:+HeapDumpOnOutOfMemoryError -XX:+HeapDumpOnCtrlBreak,這樣可以檢視宕機時系統執行哪些操作。

效能監控類引數設定

-Djava.rmi.server.hostname=Server IP-Dcom.sun.management.jmxremote.port=7091-Dcom.sun.management.jmxremote.ssl=false-Dcom.sun.management.jmxremote.authenticate=false

增加以上引數既可以通過visualVM或jconsole監控遠端JVM的執行情況。

JVM引數調整

調整heap引數和垃圾回收引數,需要通過壓力測試和監控記錄綜合分析最有方案:

Id

引數組合

TransResponse Time

Throughput

Passed Transactions

Heap引數

GC引數

1

2

3

4

5

【案例】應用伺服器執行一段Object例項數量達百萬/千萬級別,使用IBMHeapAnalyzer分析記憶體溢位時生成heapdump檔案,發現89.1%的空間被基礎物件佔用(為從資料庫載入大量記錄導致):

640?wx_fmt=png&wxfrom=5&wx_lazy=1

使用jprofiler監控後發現,大量未釋放的VchBaseVo物件:

640?wx_fmt=png&wxfrom=5&wx_lazy=1

檢視工程程式碼,發現使用Hibernate的list()方法去查詢,hibernatelist()方法優先查詢快取資料,如獲取不到則從資料庫中進行獲取,從資料庫獲取到後Hibernate將會相應的填充一級、二級快取,所以在應用伺服器級別記憶體中出現百萬級的物件佔用記憶體問題,此為hibernate快取的一個有效解決方案,但是在此處確實帶來了效能問題,需要呼叫clear()  釋放一級快取佔用的記憶體資源。

3.2.5資料庫優化

【案例】某企業內部核心業務系統資料庫出現業務高峰CPU使用率居高不下,存在大資料量查詢、多表連線造成查詢效能下降、表索引建立不合理等問題,最終通過以下辦法將業務高峰期CPU使用率控制在30%內:

在SQL*PLUS下執行下面語句: 

SQL> set line 1000  --設定每行顯示1000個字元

SQL> set autotrace traceonly  --顯示執行計劃和統計資訊,但是不顯示查詢輸出

執行效率低下SQL語句:

select variablein0_.TOKENVARIABLEMAP_ as  TOKENVAR7_1_

   from JBPM_VARIABLEINSTANCE variablein0_

 where variablein0_.TOKENVARIABLEMAP_ =  '4888804'

檢視優化前的執行計劃:

執行計劃

----------------------------------------------------------

Plan hash value:  3971367966

-------------------------------------------------------------------------------------------

| Id | Operation  | Name | Rows | Bytes | Cost (%CPU)| Time|

-------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT  |                       |    12 |    612 | 12408   (2)| 00:02:29 |

|*  1 |  TABLE ACCESS FULL| JBPM_VARIABLEINSTANCE  |    12 |   612 | 12408   (2)| 00:02:29 |

-------------------------------------------------------------------------------------------

Predicate  Information (identified by operation id):

---------------------------------------------------

   1 -  filter("VARIABLEIN0_"."TOKENVARIABLEMAP_"=4888804)

統計資訊

----------------------------------------------------------

          1   recursive calls

          1   db block gets

      48995   consistent gets

      48982   physical reads

          0   redo size

       1531   bytes sent via SQL*Net to client

        248   bytes received via SQL*Net from client

          2   SQL*Net roundtrips to/from client

          0   sorts (memory)

          0   sorts (disk)

          9   rows processed

從執行計劃看該語句缺少索引導致全表掃描。消耗總一致性讀佔用為:48995,平均每行一致性讀:48995/9=5444,物理讀為:48982,不滿足正常效能需要。建立索引優化後的執行計劃:

統計資訊

----------------------------------------------------------

           1  recursive calls

           0  db block gets

           6  consistent gets

           4  physical reads

           0  redo size

        1530  bytes sent via SQL*Net to  client

         248  bytes received via SQL*Net  from client

           2  SQL*Net roundtrips to/from  client

           0  sorts (memory)

           0  sorts (disk)

           9  rows processed

從執行計劃看該語句消耗總一致性讀佔用為:6,平均每行一致性讀:6/9=0.67,物理讀為:4,為比較高效的SQL。

一般認為平均每行一致性讀超過100的為執行效率比較低的SQL,10以內為執行效率比較高的SQL。

根據以往優化實踐,引起SQL效率低下的問題主要集中在如下幾個方面:

(1)訪問路徑,主要集中在由於索引缺失或者資料遷移導致索引失效引起的SQL執行時無法使用索引掃描,而被迫使用全表掃描訪問路徑。此時的解決方法是建立缺失的索引或者重建索引。

(2)過度使用子查詢,在某些情形下我們會連線多個大表,而此時由於業務邏輯的需要我們經常會使用到某些子查詢,由於語句的邏輯太過複雜,致使oracle無法自動將子查詢語句轉換為多表連線操作,由此帶來的結果是導致oracle選擇錯誤的執行路徑,帶來語句執行效能的急劇下降。因此,我們需要儘可能使用連線查詢代替子查詢,這樣可以幫助oracle查詢優化器根據資料分割槽情況、索引設計情況,選擇合理的連線順序、連線技術以及表訪問技術,即選擇最高效的執行計劃。

(3)使用繫結變數的好處是可以避免硬解析,好處在此不多談,但帶來的壞處是有可能選擇錯誤的執行計劃,而這有可能引起效能的急劇下降。目前oracle 10g中已經引入繫結變數分級機制來著手處理這個問題, 11g通過建立新的子游標而維護一個新的執行計劃。在11g下我們可以大膽地使用繫結變數。

3.2.6負載均衡優化

負載均衡負責訪問流量分發並提高系統橫向擴充套件能力,避免系統單點故障。下面是某個專案組負載均衡問題分析和優化思路:

640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1

負載均衡演算法:

  1. 隨機(Random):即從pool地址裡隨機選擇一臺,好處:演算法簡單、效能高,請求耗時差別不大時能基本保持後端是均衡的;缺點:如果請求耗時差別較大那麼後端機器容易不均衡。

  2. Round-Robin:根據pool地址列表順序選擇,好處:演算法簡單、效能高,缺點:和隨機一樣如果請求耗時差別較大那麼後端機器容易不均衡。

  3. 按權重:可以給pool中的主機分配權重,之後按照權重分配請求,好處:可以利舊特別是執行多年生產環境積累了不同配置的主機時需要此演算法,但隨著虛擬化該問題已經在IAAS層解決了。

  4. Hash:即對請求資訊做hash後分派到pool中的機器上(一般對靜態資源的載入使用),好處:增加快取命中率;缺點:因為需要讀取請求資訊並做hash,所以需要消耗更多的CPU資源。

  5. 按照響應時間:按照響應時間來分配,好處:可以將請求分配給效能好的主機;缺點:如果請求耗時差別較大那麼後端機器容易不均衡。

  6. 按照最小連線數:根據主機連線數多少來分配,好處:均衡請求資源;缺點:新增伺服器或重啟某一臺會因為瞬間請求量過大而出現效能問題。

會話保持:

  1. 無會話保持:每次請求當認為新的請求重新按照負載均衡演算法分配給後端主機。好處:簡單、效能高;缺點:需要後端服務做無狀態處理;

  2. 基於接入ip保持:同一個IP第一次按照負載均衡演算法分配後,第二次請求還是分配給上次的主機,好處:回話保持比較穩定;缺點:導致部分網路內使用者都連入一臺伺服器;

  3. 基於cookie保持:第一次請求負載均衡器在HTTP請求頭部insert cookie,第二次請求根據請求的HTTP頭中的cookie分配給上次的主機。好處:相對穩定、可以靈活切換;缺點:偶爾因為清除cookie導致回話丟失。

健康檢查:

  1. 基於TCP埠:監聽埠是否啟用,如果未監聽到則將該主機衝pool中剔除,好處:簡單、缺點:有可能容器啟動、應用未啟動就有請求分發過來

  2. 基於Http get/TCP請求:定期向伺服器傳送請求並判斷的返回串與約定的是否一致,如果不一致則將該主機衝pool中剔除,好處:可以精準確定應用是否正常啟動,可以動態控制服務是否線上,缺點:需要編寫指令碼。

作者:孔慶龍

本文轉載自微信公眾號 中生代技術 freshmanTechnology