Dubbo Hession反序列化導致CPU佔用飆高用例分析

拜倫發表於2019-10-11

一、Dubbo簡介

Dubbo是 阿里開源的分散式遠端服務呼叫框架。

1、Dubbo協議
Dubbo支援dubbo、rmi、hessian、http、webservice、thrift、redis等多種協議,Dubbo官網是推薦
我們使用Dubbo協議的,預設也是用的dubbo協議。

介紹幾種常見的協議:
1)dubbo協議
   預設協議,使用基於mina1.1.7+hessian3.2.1的tbremoting互動。 
   連線個數:單連線 
   連線方式:長連線 
   傳輸協議:TCP 
   傳輸方式:NIO非同步傳輸 
   序列化:Hessian二進位制序列化 
   適用範圍:傳入傳出引數資料包較小(建議小於100K),消費者比提供者個數多,單一消費者無法壓滿提供
   者,儘量不要用dubbo協議傳輸大檔案或超大字串 
   適用場景:常規遠端服務方法呼叫
   1、dubbo預設採用dubbo協議,dubbo協議採用單一長連線和NIO非同步通訊,適合於小資料量大併發的服務
   呼叫,以及服務消費者機器數遠大於服務提供者機器數的情況 
   2、他不適合傳送大資料量的服務,比如傳檔案,傳視訊等,除非請求量很低。
2)rmi協議
   Java標準的遠端呼叫協議。 
   連線個數:多連線 
   連線方式:短連線 
   傳輸協議:TCP 
   傳輸方式:同步傳輸 
   序列化:Java標準二進位制序列化 
   適用範圍:傳入傳出引數資料包大小混合,消費者與提供者個數差不多,可傳檔案。 
   適用場景:常規遠端服務方法呼叫,與原生RMI服務互操作
   RMI協議採用JDK標準的java.rmi.*實現,採用阻塞式短連線和JDK標準序列化方式。

3)hessian協議
   基於Hessian的遠端呼叫協議。 
   連線個數:多連線 
   連線方式:短連線 
   傳輸協議:HTTP 
   傳輸方式:同步傳輸 
   序列化:表單序列化 
   適用範圍:傳入傳出引數資料包大小混合,提供者比消費者個數多,可用瀏覽器檢視,可用表單或URL傳入
   引數,暫不支援傳檔案。 
   適用場景:需同時給應用程式和瀏覽器JS使用的服務。
   1、Hessian協議用於整合Hessian的服務,Hessian底層採用Http通訊,採用Servlet暴露服務,Dubbo
   預設內嵌Jetty作為伺服器實現。 
   2、Hessian是Caucho開源的一個RPC框架:http://hessian.caucho.com,其通訊效率高於
   WebService和Java自帶的序列化。 
4)http 協議
   基於http表單的遠端呼叫協議。參見:[HTTP協議使用說明] 
   連線個數:多連線 
   連線方式:短連線 
   傳輸協議:HTTP 
   傳輸方式:同步傳輸 
   序列化:表單序列化 
   適用範圍:傳入傳出引數資料包大小混合,提供者比消費者個數多,可用瀏覽器檢視,可用表單或URL
   傳入引數,暫不支援傳檔案。 
   適用場景:需同時給應用程式和瀏覽器JS使用的服務。
5)webservice協議
   基於WebService的遠端呼叫協議。 
   連線個數:多連線 
   連線方式:短連線 
   傳輸協議:HTTP 
   傳輸方式:同步傳輸 
   序列化:SOAP文字序列化 
   適用場景:系統整合,跨語言呼叫
複製程式碼

二、序列化

序列化是將一個物件變成一個二進位制流就是序列化,反序列化是將二進位制流轉換成物件。

為什麼要序列化?
1. 減小記憶體空間和網路傳輸的頻寬
2. 分散式的可擴充套件性
3. 通用性,介面可共用。

Dubbo序列化支援java、compactedjava、nativejava、fastjson、dubbo、fst、hessian2、kryo,
其中預設hessian2。其中java、compactedjava、nativejava屬於原生java的序列化。

1)dubbo序列化
   阿里尚未開發成熟的高效java序列化實現,阿里不建議在生產環境使用它。
2)hessian2序列化
   hessian是一種跨語言的高效二進位制序列化方式。但這裡實際不是原生的hessian2序列化,而是阿里修改過的,
   它是dubboRPC預設啟用的序列化方式。前提是被序列化的類得實現Serializable介面。
3)json序列化
   目前有兩種實現,一種是採用的阿里的fastjson庫,另一種是採用dubbo中自己實現的簡單json庫,但其實
   現都不是特別成熟,而且json這種文字序列化效能一般不如上面兩種二進位制序列化。
4)java序列化
   主要是採用JDK自帶的Java序列化實現,效能很不理想。

dubbo序列化主要由Serialization(序列化策略)、
DataInput(反序列化,二進位制->物件)、
DataOutput(序列化,物件->二進位制流)來進行資料的序列化與反序列化。

hessian 是一個比較老的序列化實現了,而且它是跨語言的,所以不是單獨針對java進行優化的。
而dubbo RPC實際上完全是一種Java to Java的遠端呼叫,其實沒有必要採用跨語言的序列化方式
(當然肯定也不排斥跨語言的序列化)。
現在有一些新的序列化:
專門針對Java語言的:Kryo,FST等等
跨語言的:Protostuff,ProtoBuf,Thrift,Avro,MsgPack等等
這些序列化方式的效能多數都顯著優於 hessian2 (甚至包括尚未成熟的dubbo序列化)。
所以我們可以為 dubbo 引入 Kryo 和 FST 這兩種高效 Java 來優化 dubbo 的序列化。
複製程式碼

三、案例分析

電商網站搞大促,整點秒殺活動,吸引了大量使用者,瞬間流量暴增,商品詳情繫統報警,通過監控看,
所有依賴的RPC服務呼叫RT都很長,監控系統CPU,已經滿負荷運轉。

診斷步驟:
1、通過監控頁面檢視基本的指標,
   CPU 接近100%
   記憶體正常
   系統RT很長
   系統load很高
   網路正常
   服務方RT正常。
2、登伺服器
   top 命令 檢視系統實時指標
   tail 命令,查詢 error,warn日誌
   jstack 命令 dump 執行緒檢視執行緒堆疊資訊
複製程式碼

Dubbo Hession反序列化導致CPU佔用飆高用例分析
發現大量nio執行緒呼叫getDeserializer,至此疑點集中在:hessian反序列化後拋異常。根據以上異常棧,檢視hessian-lite原始碼 (hessian-lite 3.2.4以下版本),程式碼解析如下:

Dubbo Hession反序列化導致CPU佔用飆高用例分析
可看出:

    如果遠端與本地jar版本不一致的情況,會造成反射載入類異常(ClassNotFound),同時會列印日誌資訊、
    異常toString;而這幾個動作(反射、拋異常、異常日誌等)都是cpu高消耗行為;
    又因為類載入失敗,下次介面呼叫時,這些邏輯都會重新執行一遍,所以在整點活動的高併發場景下,
    將會造成嚴重cpu效能損耗;符合以上各階段驗證結果
複製程式碼

3、問題原因

  就是服務端在物件上增加新的型別,客戶端沒有升級對應的版本,導致客戶端使用dubbo呼叫服務端,hessian
  序列化時,出現ClassNotFound的異常,不斷的出發上述原始碼裡的try{}catch{}裡的 反射載入類,並拋
  異常,這些都是cpu消費的大戶。
複製程式碼

4、問題解決

   1、更換序列化方式
      可更換成如kryo, fastjson等其他序列化方式,以上幾種不存在此類效能缺陷,且總體效能較優
      風險:可能會帶來其他相容性問題,更換、驗證成本並不低
   2、升級hessian-lite
      升級dubbo 至 2.6.6,因為這版本已將hessian-lite升級為3.2.5
      這個問題在 hessian-lite 3.2.5 已經fixed:
      https://github.com/apache/dubbo/issues/351
      hessian-lite 3.2.5 解決方式是將每個class的異常都快取住,相關程式碼如下(紅框內):
複製程式碼

Dubbo Hession反序列化導致CPU佔用飆高用例分析

相關文章