天天看點

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

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

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

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

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

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

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

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

(2)就近加載/緩存為王——減少不必要的通路。

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

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

(5)對交易鍊進行優化提高吞吐量——異步/減少串行、合理拆分(垂直/水準拆分)、規則前置。

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

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

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

确定清晰明确的性能目标是關鍵。

性能調優是伴随整個項目周期的,最好進行分階段設定目标開展,在達到預期性能目标之後即可對本階段工作進行總結和知識轉移進入下一階段調優工作。

必須保證調優後的程式運作正确。

性能更大程度是取決于良好的設計,調優技巧隻是一個輔助手段。

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

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

3性能調優

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

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

受網絡帶寬影響嚴重:因為需要下載下傳大量資源檔案,在一些在網絡環境不好的地區頁面;

js記憶體溢出:頻繁對對象的屬性進行操作造成記憶體大量占用最終溢出。

記憶體洩漏:在運作過程中記憶體不斷被占用而不能被回收,記憶體使用率随時間或負載的增加呈線性增長,系統處理效率随着時間或并發的增加而下降,直至将配置設定給jvm 的記憶體用盡而當機,或重新開機後系統短時間内可恢複正常。

資源洩露:在将資源打開後未關閉或未成功關閉的問題。這些資源包括資料源連接配接,檔案流等。當這些資源經常被打開而未能成功關閉,就會導緻資源洩漏。資料連接配接洩漏就是常見的資源洩漏問題。

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

内部資源瓶頸:資源過度使用或配置設定不足引起資源瓶頸。

線程阻塞、線程死鎖:線程退回到無法完成的同步點造成通信阻塞。

應用系統響應慢:由于應用本身或sql不合理的問題,導緻響應時間長。

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

應用系統各種各樣異常情況發生:有些是中間件伺服器抛出的異常、有些是資料端抛出的異常。

死鎖:因為請求保持或者執行效率低不能及時釋放導或因為循環等待緻表死鎖;

io繁忙:因為不良sql或業務邏輯設計不合理導緻大量io等待;

cpu使用率居高不下:高并發或緩存穿透導緻資料庫cpu居高不下或忽高忽低。

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

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

響應時間曲線(摘自《oracle性能預測》)

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

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

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

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

交易線優化的原則:

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

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

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

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

逾時控制漏鬥原則:盡量保持交易線上前端系統逾時設定應該大于後端系統。

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

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

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

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

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

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

用戶端優化的首要目标是加快頁面展現速度,其次是減少對服務端的調用。

常見解決辦法:

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

緩存為王,通過在用戶端緩存靜态資料提升頁面響應時間;

通過gzip壓縮減少用戶端網絡下載下傳流量;

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

删除、合并腳本、樣式表及圖檔減少get請求;

無阻塞加載js

預加載(圖檔、css樣式、js腳本);

按需加載js腳本;

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

web請求時序圖:

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

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

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

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

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

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

後來該頁面通過優化後端請求、合并和壓縮js/jsp檔案等将頁面響應時間優化到2秒左右。

ps:前端優化最好了解浏覽器原理、http原理

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

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

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

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

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.

規範說明: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。

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

heap參數設定

-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%的空間被基礎對象占用(為從資料庫加載大量記錄導緻):

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

使用jprofiler監控後發現,大量未釋放的vchbasevo對象:

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

檢視工程代碼,發現使用hibernate的list()方法去查詢,hibernatelist()方法優先查詢緩存資料,如擷取不到則從資料庫中進行擷取,從資料庫擷取到後hibernate将會相應的填充一級、二級緩存,是以在應用伺服器級别記憶體中出現百萬級的對象占用記憶體問題,此為hibernate緩存的一個有效解決方案,但是在此處确實帶來了性能問題,需要調用clear()  釋放一級緩存占用的記憶體資源。

【案例】某企業内部核心業務系統資料庫出現業務高峰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)<b>通路路徑</b>,主要集中在由于索引缺失或者資料遷移導緻索引失效引起的sql執行時無法使用索引掃描,而被迫使用全表掃描通路路徑。此時的解決方法是建立缺失的索引或者重建索引。

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

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

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

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

負載均衡算法:

随機(random):即從pool位址裡随機選擇一台,好處:算法簡單、性能高,請求耗時差别不大時能基本保持後端是均衡的;缺點:如果請求耗時差别較大那麼後端機器容易不均衡。

round-robin:根據pool位址清單順序選擇,好處:算法簡單、性能高,缺點:和随機一樣如果請求耗時差别較大那麼後端機器容易不均衡。

按權重:可以給pool中的主機配置設定權重,之後按照權重配置設定請求,好處:可以利舊特别是運作多年生産環境積累了不同配置的主機時需要此算法,但随着虛拟化該問題已經在iaas層解決了。

hash:即對請求資訊做hash後分派到pool中的機器上(一般對靜态資源的加載使用),好處:增加緩存命中率;缺點:因為需要讀取請求資訊并做hash,是以需要消耗更多的cpu資源。

按照響應時間:按照響應時間來配置設定,好處:可以将請求配置設定給性能好的主機;缺點:如果請求耗時差别較大那麼後端機器容易不均衡。

按照最小連接配接數:根據主機連接配接數多少來配置設定,好處:均衡請求資源;缺點:新增伺服器或重新開機某一台會因為瞬間請求量過大而出現性能問題。

會話保持:

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

基于接入ip保持:同一個ip第一次按照負載均衡算法配置設定後,第二次請求還是配置設定給上次的主機,好處:回話保持比較穩定;缺點:導緻部分網絡内使用者都連入一台伺服器;

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

健康檢查:

基于tcp端口:監聽端口是否啟用,如果未監聽到則将該主機沖pool中剔除,好處:簡單、缺點:有可能容器啟動、應用未啟動就有請求分發過來

基于http get/tcp請求:定期向伺服器發送請求并判斷的傳回串與約定的是否一緻,如果不一緻則将該主機沖pool中剔除,好處:可以精準确定應用是否正常啟動,可以動态控制服務是否線上,缺點:需要編寫腳本。

作者:孔慶龍

本文轉載自微信公衆号 中生代技術 freshmantechnology