1. 程式人生 > >關於服務效能優化思考

關於服務效能優化思考

        在開發過程中常常有這樣的抱怨:         RD1:“這服務怎麼老超時,到底行不行啊?”         RD2:   “啥這點量高峰期就不行了,我得找運維給我加機器,併發量不行啊!”         開發者在開發的過程中都需要時刻注意自己服務的效能,但有時候會常常在思想上犯一個毛病 :“效能做得好沒啥用處,能用就行,流量高峰期擴容吧 ” 。那我們談談為什麼需要對服務進行效能優化?至少有兩點,一是提高產品體驗,服務效能越好響應時間越低,降低超時率,客戶體驗越好;二是節約經濟成本,服務效能好能帶來QPS的提升,現有的資源能承受更大容量。 其實這和衡量服務效能息息相關的,那麼我麼想討論服務的效能指標有哪些呢?

一、服務指標

1.應用效能指標

     QPS/QPM :能承受的併發數,通常一秒為單位

     Response Meantime (RT):請求的平均響應時間,通常單位為毫秒

     TP 50/90/99:響應時間的分佈,通常關心的是top 99,平均響應時間反沒有多大價值

2.機器效能指標

1)記憶體和執行緒指標

JVM本身提供了一組管理的API,通過該API,我們可以獲取得到JVM內部主要執行資訊,包括記憶體各代的資料、JVM當前所有執行緒及其棧相關資訊等等。各種JDK自帶的剖析工具,包括jps、jstack、jinfo、jstat、jmap、jconsole等,都是基於此API開發的。

很多公司的元件採集的JVM的一些引數也是利用JVM 本身的API,例如:
ThreadMXBean來記錄JVM使用記憶體狀況,jvm.thread.count,jvm.thread.daemon.count,jvm.thread.totalstarted.count等實時執行指標。
MemoryInformations來記錄JVM記憶體使用狀況,jvm.memory.used,jvm.memory.oldgen.used,jvm.memory.eden.used等指標。
GarbageCollectorMXBean來記錄GC狀況,ygc,fgc等。

2)cpu指標

jvm.process.cputime:java 程序佔用的cpu時間,單位是ns

load.1minPerCPU :從執行緒等待cpu處理的個數來衡量CPU的空閒程度

cpu.idle /cpu.busy :idle是從時間的角度衡量CPU的空閒程度

cpu.switches:執行緒上下文切換

cpu.iowait:等待IO或者阻塞

說明:

*** 排程器:

其負責排程兩種資源(執行緒,中斷)。排程器針對不同的資源有不同的優先順序,下面三個優先順序從高到低依 次如下:
(中斷)裝置完成操作後通知核心,例如硬體裝置受到一個IO請求;
(系統程序)所有核心操作都在這個級別;
(使用者程序)java程序一般都執行在使用者空間 中,其在排程器中優先順序比較低;

*** 上下文切換概念:

幹活的不是執行緒,而是cpu。Linux核心看每顆CPU處理器都是獨立處理器。在每顆處理器中排程器都需要排程執行緒合理的使用CPU。每個執行緒分配了一個時間片的時間使用CPU,一旦時間片用完了或者高優先順序資源(例如中斷)需要佔用CPU,該執行緒就會放回到排程佇列中讓高優先順序資源使用CPU。這樣的執行緒切換就是所謂的Context Switch(上下文切換)。每發生一次上下文切換資源就會從CPU的使用中挪到排程佇列中。一個系統單位之間內上下文切換的次數越多,表明核心進行的工作越繁忙。

*** CPU執行佇列概念:

每個CPU都維護了一個執行佇列,排程器會一直處於執行和執行執行緒,這些執行緒不是處於執行狀態就是休眠狀態(等待IO或者阻塞)。如果CPU子系統的使用率非常高的情況下,核心排程程式可能跟不上系統的需要。會導致執行佇列越來越長,一些可執行的執行緒一直得不到排程。

*** Load的概念:

正在執行的執行緒個數與執行佇列中執行緒個數的和。例如一個4核系統,現在有4個執行緒在執行,有4個執行緒在執行佇列。所以此時load為8。Linux提供了最近1min,5min,15min的統計。

*** CPU的使用率 :

CPU的使用率是指CPU使用百分比,該指標是系統CPU使用情況重要指標。大部分監控工具提供瞭如下四個維度的監控:
User Time(使用者時間):使用者空間中的使用者態執行緒使用CPU耗時佔整個時間段比例,一般戰65%~90%是合理的
System Time(系統時間):系統執行緒(or 程序)使用CPU耗時佔整個時間段比例,一般不超過30%比較合理
Wait IO:因為等待IO請求空閒時間佔整個時間段比例
Idle:完全空閒時間佔比

一圖便知厲害~:


來討論指標之間的關係

QPS 與 RT 計算 :對於單執行緒這個公式是永遠正確,QPS=(1000/RT)*cpu核數*cpu利用率,  其中RT由包括 :執行緒的cpu計算時間+執行緒的等待時間(io/網路/鎖);

最佳執行緒數 :最佳執行緒數=((執行緒cpu時間+執行緒等待時間)/執行緒cpu時間)* cpu 核數;

執行緒數與qps、rt之間關係

一般來說要提高qps,最好的方法是降低cpu時間,優化cpu時間能達到更好的效果,比如:減少json操作、減少for迴圈、減少資料邏輯運算、序列化等操作。

一般來說要降低rt,則是降低執行緒的等待時間(io/網路/鎖),比如依賴的遠端服務、資料庫的讀寫、鎖等,並且降低著先並不能帶來qps的明顯提升,因為對於我們大多數系統來說,業務邏輯都不是很複雜,需要耗費大量cpu計算的場景很少,因此cpu運算在rt中的佔比不是很高,佔比高的還是執行緒等待時間。

二、優化工具和步驟

利器: 

1.壓測平臺(測試環境和生產環境流量回放)

2.Jprofile(CPU熱點)

3.JProfiler或者MAT(記憶體分析)

4.介面校驗工具(進行一邊優化後對服務提供的介面進行資料校驗,一般適用http+json介面),類似git diff

5.gregs(線上診斷工具,一般檢視載入類,方法資訊,方法呼叫追蹤渲染 

6.通過ngix等調整線上機器的權重,對某臺線上機器施加流量壓力,特別注意這是最後一步,是最真實最有說服力的壓測。

優化一般步驟:

1.服務是幹什麼的?即梳理服務,明確上下游服務以及各依賴服務的效能。

2.為什麼要優化?明確當前服務的效能,明確性能優化的目標,提高qps,rt,機器指標。

3. 怎麼搞?迴圈(壓測 =>熱點分析/瓶頸分析=>優化=>服務介面校驗/介面diff=>同步生產環境)

三、優化方向

1.cpu熱點

利用 JProfiler抓CPU 熱點, 經常會有預期之外的收穫 ,不僅可以頂住專案本身程式碼引起的cpu熱點,也很容易發現呼叫依賴服務封裝的客戶端引起的cpu熱點。如容易發現“富客戶端”引起的cpu熱點,所謂的富客戶端是指一些耗cpu的邏輯程式碼封裝在jar包裡,這必然導致使用者的服務增加cpu壓力。
案例分析:

因為歷史原因,專案嚴重依賴於org.json框架,依賴服務提供的介面基本上都是json介面,這就導致呼叫方拿到json資料之後進行一些列的加工檢查,對JSONObject和JSONArray的get、put、parse、toString等操作滿天飛,,還需要把json解析成java的model。所以json的一系列操作是非常耗cpu的。

總結來說使用org.json的至少面臨兩個嚴重的問題:

(1)業務處理過程混亂:業務處理類之間JSONObject和JSONArray物件到處傳,遇到問題排查程式碼不容易發現在哪裡和哪層添加了什麼欄位。

(2)效能差:做過實驗

  • 在簡單json場景下,org.json序列化耗時是jackson的2.5倍,反序列化耗時是2倍。
  • 中等複雜場景下,org.json的耗時是jackson的4倍。
  • 預計隨著場景更加複雜,jackson的優勢將會更加明顯

解決方案:

(1)基於自定義java模型,實際上操作是依賴服務提供的http+json介面改造為thrift介面。因為有強型別約束之後程式碼會清晰很多,確定屬性的名稱和型別。

(2)如果“改造為基於java模型”無法做到或推動時間較長,必須得依賴某個json庫。那麼要基於jackson2,而不是jackson1,原因是restlet的JacksonRepresentation是綁死在jackson1上,因為最終要把restlet框架更換到springMVC,而springMVC更好支援非同步的改造。

(3)jackson提供三種模式,API流模式(效能最好)、樹模型(類似XML的DOM樹)、資料繫結模型(使用最方便)。


2.依賴服務非同步化

依賴的服務進行非同步化呼叫,主要是避免通過不斷新建執行緒或通過線城池來實現併發引起的執行緒瓶頸。 案例分析: 一個服務的呼叫順序流程圖如下,

通過呼叫的鏈路分析,可以對藍色方框的依賴進行利用執行緒池進行併發呼叫,這樣高併發的時候會很容易造成執行緒太多造成瓶頸,舉個例子:

服務接受到request,開啟一個jetty的執行緒,這個執行緒會併發呼叫10個依賴服務,則會從執行緒池取出10個執行緒進行併發。當request併發量到達1000,會對執行緒的需求放大至1w,假設每個依賴耗時100ms,每秒處理10個依賴,需要對執行緒池大小為1w/10=1000個執行緒,這是不能接受的,因為海量執行緒帶來的非堆記憶體佔用、上下文切換等開銷,會使得程序不堪重負,穩定性下降,更早的達到效能瓶頸。

改造思路:併發+非同步改造,底層服務採用NIO等方式去發起請求和返回結果,依靠事件驅動,在業務層將其轉換為可組合為CompletableFuture(java8)ListenableFuture(java7,基於guava庫),不再需要執行緒池去等待每一個依賴的響應而不再需要業務執行緒去等待,這樣執行緒池不會出現瓶頸。

改造後流程圖:



3.服務框架選型

因為歷史原因,專案的restful介面一直採用restlet框架提供服務。可能當時公司成立之初,springMVC技術還沒成熟,採用更穩妥的restlet框架。

restlet容易暴露的問題:

(1)restlet效能比springMVC差, 效能對比結果是:裸Servlet > SpringMVC >> Restlet

(2)對攔截器的支援,springMVC可以定義Interceptor,restlet要通過maven-aspectj編譯期織入打點機制。

4.JVM 調優

JVM調優一般來說都是出問題或告警的時候注意進行優化,這塊可謂“水無常形 兵無常勢”,具體問題具體分析。

回收器的選擇:

   - CMS?

   - G1?

      ....

關鍵引數
     – 決定Heap大小:-xms -xmx –xmn(-xms=-xmx) ,初始堆的大小==可調堆最大值,避免堆動盪

         ● 取決與作業系統位數和CPU能力
         ● 過小GC頻繁,過大GC中斷時間過長

    – Eden/From/To:決定YounGC,如:-XX:SurvivorRatio
    
– 新生代存活週期:決定FullGC,如: XX:MaxTenuringThreshold

 新生代/舊生代  

    – 避免新生代設定過小

      ● 頻繁YoungGC
      ● 大物件,From/To不足拿Old增長快,FullGC

   – 避免新生代設定過大
      ● 舊生代變小,頻繁FullGC
      ● 新生代變大,YoungGC更耗時

  – 對於我們組大部分系統,可以分配 : 新生代:Heap=33%Young:Old=1:2

5.序列化和反序列化協議優化

高效的序列化協議不僅可以提高系統的通用性、強健性、安全性、優化系統性能,而且會讓系統更加易於除錯、便於擴充套件。在做CPU熱點分析的時候,發現redis序列化和反序列化佔比非常高,已經在重構優化中逐漸成為瓶頸。可以調研從可否redis使用不同hessian版本,能否支援非同步,利用非同步化提高效能。後期遇到什麼問題再補充。

四、優化總結

1.利用效能診斷工具,依靠資料驅動,不能憑感覺

2.見招拆招,具體問題具體分析