經歷400多天打磨,HSF的架構和性能有哪些新突破?
摘要: 從HSF2.1到HSF2.2的目標是提升HSF框架的擴展性、易用性和性能,相當於將原有HSF推倒了重新來過,因此涉及到的內容眾多,這裡不做展開,我們僅從架構升級、性能優化和運維提升三個方面來聊一下HSF2.2做了什麼,有什麼能幫到大家,以及如何解決了雙十一的一些問題。
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的架構與原有版本有了顯著變化,首先是層與層之間的邊界清晰,其次組件之間的關係職責明確,最後是擴展可替換的思想貫穿其中,下面我們先看一下新的架構:
可以看到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以及其擴展,以客戶端調用場景為例:一次調用是如何穿過用戶擴展的調用攔截器的。
可以看到,HSF2.2定義的調用攔截器不是一個簡單的正向invoke過程,它還具備響應回來時的逆向onResponse過程,這樣一來一回很好的詮釋了HSF同步調用非同步執行的調用模型。用戶可以通過在一次調用中計算invoke和onResponse之間的時差來獲得調用的正確耗時,而不用關心HSF調用是同步還是非同步,用戶也可以擴展一個調用攔截器穿插在其中,用於監控HSF的調用內容,而不用讓應用方做任何代碼上的改造。
多應用支持
多模塊部署一直是業務方同學探索的熱點,從合併部署開始,HSF就有限的支持了多應用部署,但是一直沒有將其融入到架構中。在HSF2.2中,應用不僅僅是作為託管服務的容器而是成為核心領域,多應用序列化的難題,就在應用層解決。
(模塊在HSF2.2中也會被認為是一個應用,以下提到模塊或者應用可以認為是一個概念。)
HSF2.2支持多個應用部署的結構如下圖所示:
可以看到,HSF2.2框架域處於最底層,它向上為應用提供了線程、連接等多種資源的支持,而應用能夠發布或者訂閱多個服務,這些服務能夠被應用統一管理,也支持更加精細化的配置,層與層的司職明確讓HSF具有了良好的適應性。
性能優化
RPC流程中,序列化是開銷很大的環節。尤其現在業務對象都很複雜,序列化開銷更不容小覷。
下面先介紹下今年HSF針對Hessian的幾個優化點。
優化1:元數據共享
hessian序列化會將兩種信息寫到輸出流:
元數據:即類全名,欄位名
值數據:即各個欄位對應值(如果欄位是複雜類型,則會遞歸傳遞該複雜類型的元數據和內部欄位的值數據)
在hessian1協議里,每次寫出Class A的實例時,都會寫出Class A的元數據和值數據,就是會重複傳輸相同的元數據。針對這點,hessian2協議做了一個優化就是:在「同一次序列化上下文」里,如果存在Class A的多個實例,只會對Class A的元數據傳輸一次。該元數據會在對端被緩存起來重複使用,下次再序列化Class A的對象時,只需要先寫出對元數據的一個引用句柄(緩存中的index,用一個int表示),然後直接寫出值數據即可。接受方通過元數據句柄即可知道後面的值數據對應的類型。
這是一個極大的提升。因為編碼欄位名字(就是字元串)所需的位元組數很可能比它對應的值(可能只是一個byte)更多。
不過在官方的hessian里,這個優化有兩個限制:
序列化過程中類型對應的Class結構不能改變
元數據引用只能在「同一個序列化上下文」,這裡的「上下文」就是指同一個HessianOutput和HessianInput。因為元數據的id分配和緩存分別是在HessianOutput和HessianInput里進行的
限制1我們可以接受,一般DO不會再運行時改變。但是限制2不太友好,因為針對每次請求的序列化和反序列化,HSF都需要使用全新構造的HessianOutput和HessianInput。這就導致每次請求都需要重新發送上次請求已經發送過的元數據。
針對限制2,HSF實現了跨請求元數據共享,這樣只要發送過一次元數據,以後就再也不用發送了,進一步減少傳輸的數據量。實現機制如下:
修改hessian代碼,將元數據id分配和緩存的數據結構從HessianOutput和HessianInput剝離出來。
修改HSF代碼,將上述剝離出來的數據結構作為連接級別的上下文保存起來。
每次構造HessianOutput和HessianInput時將其作為參數傳入。這樣就達了跨請求復用元數據的目的。
該優化的效果取決於業務對象中,元數據所佔的比例,如果「精心」構造對象,使得元數據所佔比例很大,那麼測試表現會很好,不過這沒有意義。我們還是選取線上核心應用的真實業務對象來測試。從線上tcp dump了一個真實業務對象,測試同學以此編寫測試用例得到測試數據如下:
新版本比老版本CPU利用率下降10%左右
新版本的網路流量相比老版本減少約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進行反序列化,所以業務不會報錯,但是應用同學反饋每次都處理異常,對性能影響比較大。
重複載入缺失類型
某核心應用壓測發現大量線程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);
工作機制簡單說明如下:
key類型由String改為自定義的AttributeKey,AttributeKey會在初始化階段就去AttributeNamespace申請一個固定id
map類型由HashMap改為自定義的DefaultAttributeMap,DefaultAttributeMap 內部使用數組存放數據
操作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)上線了諸多提升應用安全、運維效率的新功能,如:
服務鑒權
單機運維
連接預熱
其中,在今年雙11中發揮最大價值的,當屬 HSF 連接預熱功能。
HSF 連接預熱功能,可以幫助應用提前建立起和 Providers 之間的連接,從而避免首次調用時因連接建立造成的 rt 損耗。
HSF服務治理平台的連接預熱功能,支持指定應用、機器分組以及單元環境的預熱,並且可以指定預熱執行計劃,並支持按預熱批次暫停、恢復預熱。在 2017 雙 11 預熱完成後,目標應用集合整體連接數增長大於50%,部分應用增長達 40 倍,符合連接預熱的預期。
小結
架構升級使HSF能夠從容的應對未來的挑戰,性能優化讓我們在調用上追求極致,運維提升更是將眼光拔高到全局去思考問題,這些改變共同的組成了HSF2.1到HSF2.2的變革,而這次變革不僅僅是穩定的支撐2017年的雙十一,而是開啟了服務框架下一個十年。
TAG:雲棲社區 |