當前位置:
首頁 > 知識 > 經歷400多天打磨,HSF的架構和性能有哪些新突破?

經歷400多天打磨,HSF的架構和性能有哪些新突破?

摘要: 從HSF2.1到HSF2.2的目標是提升HSF框架的擴展性、易用性和性能,相當於將原有HSF推倒了重新來過,因此涉及到的內容眾多,這裡不做展開,我們僅從架構升級、性能優化和運維提升三個方面來聊一下HSF2.2做了什麼,有什麼能幫到大家,以及如何解決了雙十一的一些問題。

經歷400多天打磨,HSF的架構和性能有哪些新突破?

2017年的雙十一圓滿結束了,1682億的成交額再一次刷新了記錄,而HSF(High Speed Framework,分散式服務框架)當天調用量也突破了3.5萬億次,調用量是2016年雙十一的三倍多。為了這不平凡的一天中,HSF經歷了一年多的磨練,從架構升級到性能優化,從可用性提升到運維提升,完成了從2.1到2.2的全面升級,截止到雙十一開始的一刻,HSF2.2在全網機器佔比超過75%,並成功接受了大促考驗。

從HSF2.1到HSF2.2的目標是提升HSF框架的擴展性、易用性和性能,相當於將原有HSF推倒了重新來過,因此涉及到的內容眾多,這裡不做展開,我們僅從架構升級、性能優化和運維提升三個方面來聊一下HSF2.2做了什麼,有什麼能幫到大家,以及如何解決了雙十一的一些問題。

架構升級

HSF2.1主要被用戶抱怨的一點就是擴展性不強,比如:不少用戶想攔截調用請求但無法方便做到,只好退而求其次,使用代理來包裝HSF的客戶端或者服務端,這樣做一來顯得繁瑣,二來出現問題不易排查。不光是用戶的擴展支持的不好,框架自身對不同的開源組件適配也顯得力不從心,比如:支持ZooKeeper作為註冊中心的方案一直難以實施,原因在於框架內部與ConfigServer耦合過於緊密。無論是用戶還是框架的維護者,都受限於現有架構,這是HSF需要改變的地方。

HSF2.2的架構與原有版本有了顯著變化,首先是層與層之間的邊界清晰,其次組件之間的關係職責明確,最後是擴展可替換的思想貫穿其中,下面我們先看一下新的架構:

經歷400多天打磨,HSF的架構和性能有哪些新突破?

可以看到HSF2.2從宏觀上分為了三個域:框架、應用和服務,其中框架是對上層多個應用做支撐的基礎,而服務屬於具體的某個應用,從這種劃分可以看出,HSF2.2是天然支持多應用(或多模塊)部署的。每個域中,都有各自的功能組件,比如:服務域中的InvocationHandler就是對服務調用邏輯的抽象,通過擴展其中的ClientFilter或者ServerFilter完成對客戶端或者服務端的調用處理工作,這裡只是羅列了主要的功能組件和擴展點,以下是對它們的說明:

服務 ,對應於一個介面,它可以提供服務或者消費服務


組件名 擴展點 說明
InvocationHandler ClientFilter 客戶端調用攔截器,擴展它實現調用端的請求攔截處理
InvocationHandler ServerFilter 服務端調用攔截器,擴展它實現服務端的請求攔截處理
Router AbstractMultiTargetRouter 客戶端調用多節點選擇路由器,擴展它實現客戶端調用多節點的路由
Router AbstractSingleTargetRouter 客戶端調用單節點選擇路由器,擴展它實現客戶端調用單節點的路由

應用 ,對應於一個應用或者一個模塊,它管理著一組消費或者發布的服務


組件名 擴展點 說明
Registry AddressListener 註冊中心地址監聽器,擴展它可以獲取到註冊中心推送的地址列表
Registry AddressListenerInteceptor 註冊中心地址攔截器,擴展它可以監聽到註冊中心推送地址並加以控制
Protocol ProtocolInterceptor 訂閱發布流程,擴展它可以捕獲應用發布或者消費的具體服務

框架 , 對應於框架底層支持,它統一管理著線程、鏈接等資源


組件名 擴展點 說明
Serialize Serializer 序列化和反序列化器,需要實現新的序列化方式可以擴展它
Packet PacketFactory 協議工廠, 適配不同的RPC框架協議需要擴展它
Stream StreamLifecycleListener 長連接生命周期管理監聽器,擴展它可以監聽到長連接的建立和銷毀等生命周期
Stream StreamMessageListener 長連接事件監聽器,擴展它可以監聽到每條連接上跑的數據

分類介紹了HSF2.2的主要組件後,我們來看看架構的改變,擴展性的提升,能夠為業務方,為他們更好的支持雙十一帶來什麼。

純非同步調用攔截器

HSF非同步future調用被業務方越來越多的使用,但是eagleeye對future調用的耗時統計上一直不準,因為future調用返回的是默認值,所以業務看到的是一次飛快的調用返回,它真實的耗時該如何度量?信息平台承載了集團的眾多內部服務業務,一直苦於沒有辦法全面的監控HSF的調用記錄,也無法做到異常報警,難道還要讓所有的使用方配置代理?

HSF2.2設計的InvocationHandler組件以及擴展,完美的解決了這個問題,我們近距離觀察一下InvocationHandler以及其擴展,以客戶端調用場景為例:一次調用是如何穿過用戶擴展的調用攔截器的。

經歷400多天打磨,HSF的架構和性能有哪些新突破?

可以看到,HSF2.2定義的調用攔截器不是一個簡單的正向invoke過程,它還具備響應回來時的逆向onResponse過程,這樣一來一回很好的詮釋了HSF同步調用非同步執行的調用模型。用戶可以通過在一次調用中計算invoke和onResponse之間的時差來獲得調用的正確耗時,而不用關心HSF調用是同步還是非同步,用戶也可以擴展一個調用攔截器穿插在其中,用於監控HSF的調用內容,而不用讓應用方做任何代碼上的改造。

多應用支持

多模塊部署一直是業務方同學探索的熱點,從合併部署開始,HSF就有限的支持了多應用部署,但是一直沒有將其融入到架構中。在HSF2.2中,應用不僅僅是作為託管服務的容器而是成為核心領域,多應用序列化的難題,就在應用層解決。

(模塊在HSF2.2中也會被認為是一個應用,以下提到模塊或者應用可以認為是一個概念。)

HSF2.2支持多個應用部署的結構如下圖所示:

經歷400多天打磨,HSF的架構和性能有哪些新突破?

可以看到,HSF2.2框架域處於最底層,它向上為應用提供了線程、連接等多種資源的支持,而應用能夠發布或者訂閱多個服務,這些服務能夠被應用統一管理,也支持更加精細化的配置,層與層的司職明確讓HSF具有了良好的適應性。

性能優化

RPC流程中,序列化是開銷很大的環節。尤其現在業務對象都很複雜,序列化開銷更不容小覷。

下面先介紹下今年HSF針對Hessian的幾個優化點。

優化1:元數據共享

hessian序列化會將兩種信息寫到輸出流:

  1. 元數據:即類全名,欄位名

  2. 值數據:即各個欄位對應值(如果欄位是複雜類型,則會遞歸傳遞該複雜類型的元數據和內部欄位的值數據)

在hessian1協議里,每次寫出Class A的實例時,都會寫出Class A的元數據和值數據,就是會重複傳輸相同的元數據。針對這點,hessian2協議做了一個優化就是:在「同一次序列化上下文」里,如果存在Class A的多個實例,只會對Class A的元數據傳輸一次。該元數據會在對端被緩存起來重複使用,下次再序列化Class A的對象時,只需要先寫出對元數據的一個引用句柄(緩存中的index,用一個int表示),然後直接寫出值數據即可。接受方通過元數據句柄即可知道後面的值數據對應的類型。

這是一個極大的提升。因為編碼欄位名字(就是字元串)所需的位元組數很可能比它對應的值(可能只是一個byte)更多。

不過在官方的hessian里,這個優化有兩個限制:

  1. 序列化過程中類型對應的Class結構不能改變

  2. 元數據引用只能在「同一個序列化上下文」,這裡的「上下文」就是指同一個HessianOutput和HessianInput。因為元數據的id分配和緩存分別是在HessianOutput和HessianInput里進行的

限制1我們可以接受,一般DO不會再運行時改變。但是限制2不太友好,因為針對每次請求的序列化和反序列化,HSF都需要使用全新構造的HessianOutput和HessianInput。這就導致每次請求都需要重新發送上次請求已經發送過的元數據。

針對限制2,HSF實現了跨請求元數據共享,這樣只要發送過一次元數據,以後就再也不用發送了,進一步減少傳輸的數據量。實現機制如下:

  1. 修改hessian代碼,將元數據id分配和緩存的數據結構從HessianOutput和HessianInput剝離出來。

  2. 修改HSF代碼,將上述剝離出來的數據結構作為連接級別的上下文保存起來。

  3. 每次構造HessianOutput和HessianInput時將其作為參數傳入。這樣就達了跨請求復用元數據的目的。

該優化的效果取決於業務對象中,元數據所佔的比例,如果「精心」構造對象,使得元數據所佔比例很大,那麼測試表現會很好,不過這沒有意義。我們還是選取線上核心應用的真實業務對象來測試。從線上tcp dump了一個真實業務對象,測試同學以此編寫測試用例得到測試數據如下:

  1. 新版本比老版本CPU利用率下降10%左右

  2. 新版本的網路流量相比老版本減少約17%

線上核心應用壓測結果顯示數據流量下降一般在15%~20%之間。

優化2:UTF8解碼優化

hessian傳輸的字元串都是utf8編碼的,反序列化時需要進行解碼。

hessian現行的解碼方式是逐個字元進行。代碼如下:

private int parseUTF8Char() throws IOException { int ch = _offset < _length ? (_buffer[_offset++] & 0xff) : read();
if (ch < 0x80)
return ch; else if ((ch & 0xe0) == 0xc0) {
int ch1 = read(); int v = ((ch & 0x1f) << 6) + (ch1 & 0x3f); return v; } else if ((ch & 0xf0) == 0xe0) {
int ch1 = read(); int ch2 = read(); int v = ((ch & 0x0f) << 12) + ((ch1 & 0x3f) << 6) + (ch2 & 0x3f); return v; } else
throw error("bad utf-8 encoding at " + codeName(ch));}

UTF8是變長編碼,有三種格式:

1 byte format: 0xxxxxxx

2 byte format: 110xxxxx 10xxxxxx

3 byte format: 1110xxxx 10xxxxxx 10xxxxxx

上面的代碼是對每個位元組,通過位運算判斷屬於哪一種格式,然後分別解析。

優化方式是:通過unsafe將8個位元組作為一個long讀取,然後通過一次位運算判斷這8個位元組是否都是「1 byte format」,如果是(很大概率是,因為常用的ASCII都是「1 byte format」),則可以將8個位元組直接解碼返回。以前8次位運算,現在只需要一次了。如果判斷失敗,則按老的方式,逐個位元組進行解碼。主要代碼如下:

private boolean parseUTF8Char_improved() throws IOException {
while (_chunkLength > 0) {
if (_offset >= _length && !readBuffer()) {
return false;
}
int sizeOfBufferedBytes = _length - _offset; int toRead = sizeOfBufferedBytes <= _chunkLength ? sizeOfBufferedBytes : _chunkLength;
// fast path for ASCII int numLongs = toRead >> 3;
for (int i = 0; i < numLongs; i++) {
long currentOffset = baseOffset + _offset; long test = unsafe.getLong(_buffer, currentOffset);
if ((test & 0x8080808080808080L) == 0L) { _chunkLength -= 8;
toRead -= 8;
for (int j = 0; j < 8; j++) { _sbuf.append((char)(_buffer[_offset++])); }
} else {
break;
}
}
for (int i = 0; i < toRead; i++) { _chunkLength--;
int ch = (_buffer[_offset++] & 0xff);
if (ch < 0x80) { _sbuf.append((char)ch);
} else if ((ch & 0xe0) == 0xc0) {
int ch1 = read();
int v = ((ch & 0x1f) << 6) + (ch1 & 0x3f);
_sbuf.append((char)v);
} else if ((ch & 0xf0) == 0xe0) {
int ch1 = read();
int ch2 = read();
int v = ((ch & 0x0f) << 12) + ((ch1 & 0x3f) << 6) + (ch2 & 0x3f);
_sbuf.append((char)v);
} else
throw error("bad utf-8 encoding at " + codeName(ch));
}
}
return true;
}

同樣使用線上dump的業務對象進行對比,測試結果顯示該優化帶來了 17% 的性能提升:

time: 981

improved utf8 decode time: 810

(981-810)/981 = 0.1743119266055046

優化3:異常流程改進

hessian在一些異常場景下,雖然業務不一定報錯,但是性能卻很差。今年我們針對兩個異常場景做了優化。

UnmodifiableSet構造異常

某核心應用壓測發現大量如下錯誤:

java.util.Collections$UnmodifiableSet

========================================================================================

java.lang.Throwable.(Throwable.java:264)

java.lang.Exception.(Exception.java:66)

java.lang.ReflectiveOperationException.(ReflectiveOperationException.java:56)

java.lang.InstantiationException.(InstantiationException.java:63)

java.lang.Class.newInstance(Class.java:427)

com.taobao.hsf.com.caucho.hessian.io.CollectionDeserializer.createList(CollectionDeserializer.java:107)

com.taobao.hsf.com.caucho.hessian.io.CollectionDeserializer.readLengthList(CollectionDeserializer.java:88)

com.taobao.hsf.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:1731)

com.taobao.hsf.com.caucho.hessian.io.UnsafeDeserializer$ObjectFieldDeserializer.deserialize(UnsafeDeserializer.java:387)

這是Hessian對UnmodifiableSet反序列化的一個問題,因為UnmodifiableSet沒有默認構造函數,所以Class.newInstance會拋出,出錯之後Hessian會使用HashSet進行反序列化,所以業務不會報錯,但是應用同學反饋每次都處理異常,對性能影響比較大。

經歷400多天打磨,HSF的架構和性能有哪些新突破?

重複載入缺失類型

某核心應用壓測發現大量線程block在ClassLoader.loadClass方法

"HSFBizProcessor-DEFAULT-7-thread-1107" #3049 daemon prio=10 os_prio=0 tid=0x00007fd127cad000 nid=0xc29 waiting for monitor entry [0x00007fd0da2c9000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.taobao.pandora.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:133)
- waiting to lock <0x0000000741ec9870> (a java.lang.Object)
atjava.lang.ClassLoader.loadClass(ClassLoader.java:380)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
at com.taobao.hsf.com.caucho.hessian.io.SerializerFactory.getDeserializer(SerializerFactory.java:681)

排查確認是業務二方包升級,服務端客戶端二方包版本不一致,導致老版本的一邊反覆嘗試載入對應的新增類型。雖然該問題並沒有導致業務錯誤,但是確嚴重影響了性能,按應用同學的反饋,如果每次都嘗試載入缺失類型,性能會下降一倍。

針對這種情況,修改hessian代碼,將確認載入不到的類型緩存起來,以後就不再嘗試載入了。

優化4: map操作數組化

大型系統里多個模塊間經常通過Map來交互信息,互相只需要耦合String類型的key。常見代碼如下:

public static final String key = "mykey";
Map<String,Object> attributeMap = new HashMap<String,Object>();Object value = attributeMap.get(key);

大量的Map操作也是性能的一大消耗點。HSF今年嘗試將Map操作進行了優化,改進為數組操作,避免了Map操作消耗。新的範例代碼如下:

public static final AttributeNamespace ns = AttributeNamespace.createNamespace("mynamespace");
public static final AttributeKey key = new AttributeKey(ns, "mykey");
DefaultAttributeMap attributeMap = new DefaultAttributeMap(ns, 8);Object value = attributeMap.get(key);

工作機制簡單說明如下:

  1. key類型由String改為自定義的AttributeKey,AttributeKey會在初始化階段就去AttributeNamespace申請一個固定id

  2. map類型由HashMap改為自定義的DefaultAttributeMap,DefaultAttributeMap 內部使用數組存放數據

  3. 操作DefaultAttributeMap直接使用AttributeKey里存放的id作為index訪問數組即可,避免了hash計算等一系列操作。核心就是將之前的字元串key和一個固定的id對應起來,作為訪問數組的index

對比HashMap和DefaultAttributeMap,性能提升約30%。

HashMap put time(ms) : 262

ArrayMap put time(ms) : 185

HashMap get time(ms) : 184

ArrayMap get time(ms) : 126

在剛剛過去的 2017 年雙 11 中,HSF服務治理(HSFOPS)上線了諸多提升應用安全、運維效率的新功能,如:

  1. 服務鑒權

  2. 單機運維

  3. 連接預熱

其中,在今年雙11中發揮最大價值的,當屬 HSF 連接預熱功能。

HSF 連接預熱功能,可以幫助應用提前建立起和 Providers 之間的連接,從而避免首次調用時因連接建立造成的 rt 損耗。

HSF服務治理平台的連接預熱功能,支持指定應用、機器分組以及單元環境的預熱,並且可以指定預熱執行計劃,並支持按預熱批次暫停、恢復預熱。在 2017 雙 11 預熱完成後,目標應用集合整體連接數增長大於50%,部分應用增長達 40 倍,符合連接預熱的預期。

經歷400多天打磨,HSF的架構和性能有哪些新突破?

小結

架構升級使HSF能夠從容的應對未來的挑戰,性能優化讓我們在調用上追求極致,運維提升更是將眼光拔高到全局去思考問題,這些改變共同的組成了HSF2.1到HSF2.2的變革,而這次變革不僅僅是穩定的支撐2017年的雙十一,而是開啟了服務框架下一個十年。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 雲棲社區 的精彩文章:

面向開發者的2018年AI趨勢分析

TAG:雲棲社區 |