1.背景
什么是API網(wǎng)關,它的作用是什么盆偿,產(chǎn)生的背景是啥柒爸?
從架構的角度來看,API網(wǎng)關暴露http接口服務,其本身不涉及業(yè)務邏輯,只負責包括請求路由事扭、負載均衡捎稚、權限驗證、流量控制句旱、緩存等等功能阳藻。其定位類似于Nginx請求轉(zhuǎn)發(fā)、但功能要多于Nginx,背后連接了成百上千個后臺服務谈撒,這些服務協(xié)議可能是rest的,也可能是rpc協(xié)議等等匾南。
網(wǎng)關的定位決定了它生來就需要高性能啃匿、高效率的。網(wǎng)關對接著成百上千的服務接口,承受者高并發(fā)的業(yè)務需求蛆楞,因此我們對其性能要求嚴苛溯乒,其基本功能如下:
協(xié)議轉(zhuǎn)換
一般從前端請求來的都是HTTP接口,而在分布式構建的背景下,我們后臺服務基本上都是以RPC協(xié)議而開發(fā)的服務豹爹。這樣就需要網(wǎng)關作為中間層對請求和響應作轉(zhuǎn)換裆悄。
將HTTP協(xié)議轉(zhuǎn)換為RPC,然后返回時將RPC協(xié)議轉(zhuǎn)換為HTTP協(xié)議請求路由
網(wǎng)關背后可能連接著成本上千個服務,其需要根據(jù)前端請求url來將請求路由到后端節(jié)點中去臂聋,這其中需要做到負載均衡統(tǒng)一鑒權
對于鑒權操作不涉及到業(yè)務邏輯光稼,那么可以在網(wǎng)關層進行處理,不用下層到業(yè)務邏輯孩等。統(tǒng)一監(jiān)控
由于網(wǎng)關是外部服務的入口艾君,所以我們可以在這里監(jiān)控我們想要的數(shù)據(jù),比如入?yún)⒊鰠⒁薹剑溌窌r間冰垄。流量控制,熔斷降級
對于流量控制权她,熔斷降級非業(yè)務邏輯可以統(tǒng)一放到網(wǎng)關層虹茶。
有很多業(yè)務都會自己去實現(xiàn)一層網(wǎng)關層,用來接入自己的服務隅要,但是對于整個公司來說這還不夠蝴罪。
2.Flurry Dubbo API網(wǎng)關
Flurry是云集自研的一款輕量級、異步流式化拾徙、針對Dubbo的高性能API網(wǎng)關洲炊。與業(yè)界大多數(shù)網(wǎng)關不同的是,flurry自己實現(xiàn)了 http與dubbo協(xié)議互轉(zhuǎn)的流式化的dubbo-json協(xié)議,可高性能暂衡、低內(nèi)存要求的對http和dubbo協(xié)議進行轉(zhuǎn)換询微。除此之外,其基于 netty作為服務容器狂巢,提供服務元數(shù)據(jù)模型等等都是非常具有特點的撑毛。下面我們將詳細介紹 flurry的特性:
2.1 基于Netty容器
傳統(tǒng)網(wǎng)關大多采用tomcat作為容器,其請求于響應沒有做到異步,tomcat會有一個核心線程池來處理請求和響應,如果RT比較高的話,將會對性能有一定的影響。
Flurry 網(wǎng)關請求響應基于Netty線程模型,后者是實現(xiàn)了Reactive,反應式模式規(guī)范的,其設計就是來榨干CPU的唧领,可以大幅提升單機請求響應的處理能力藻雌。
最終,Flurry通過使用Netty線程模型和NIO通訊協(xié)議實現(xiàn)了HTTP請求和響應的異步化。
每一次http請求最終都會由Netty的一個Client Handler來處理,其最終以異步模式請求后臺服務斩个,并返回一個CompletableFuture,當有結果返回時才會將結果返回給前端胯杭。
見下面一段例子:
- ServerProcessHandler
public class ServerProcessHandler extends SimpleChannelInboundHandler<RequestContext> {
public void handlerPostAsync(RequestContext context, ChannelHandlerContext ctx) throws RpcException {
CompletableFuture<String> jsonResponse = asyncSender.sendAsync(context, ctx);
jsonResponse.whenComplete((result, t) -> {
long et = System.currentTimeMillis();
if (t != null) {
String errorMessage = HttpHandlerUtil.wrapCode(GateWayErrorCode.RemotingError);
doResponse(ctx, errorMessage, context.request());
} else {
doResponse(ctx, HttpHandlerUtil.wrapSuccess(context.requestUrl(), result), context.request());
}
});
}
//省略部分代碼
public static void sendHttpResponse(ChannelHandlerContext ctx, String content, FullHttpRequest request) {
//將結果寫回Client,返回前端
ctx.writeAndFlush(response);
}
}
2.2 服務元數(shù)據(jù)模型
所謂的服務元數(shù)據(jù)信息,是指服務接口的方法列表受啥、每個方法的輸入輸出參數(shù)信息做个、每個參數(shù)的字段信息以及上述(方法、參數(shù)以及字段)的注釋,其非常類似于Java Class的反射信息滚局。
元數(shù)據(jù)的作用
有了服務元數(shù)據(jù),我們就可以不必需要服務的API包,并能夠清晰的知道整個服務API的定義居暖。
這在Dubbo服務Mock調(diào)用、服務測試藤肢、文檔站點太闺、流式調(diào)用等等場景下都可以發(fā)揮搶到的作用。
元數(shù)據(jù)與注冊中心數(shù)據(jù)比較
\ | 服務元數(shù)據(jù) | 注冊中心數(shù)據(jù) |
---|---|---|
變化頻率 | 基本不變,隨著服務迭代更改而更改 | 隨著服務節(jié)點上下線而隨時進行變化 |
職責 | 描述服務嘁圈,定義服務的基本屬性 | 存儲地址列表 |
使用場景 | 無API依賴包模式調(diào)用(直接json),文檔站點省骂,Mock測試等 | 服務治理和調(diào)用 |
交互模型 | 服務編譯期生成元信息,client通過接口調(diào)用模式獲取 | 發(fā)布訂閱模型,線上進行交互 |
元數(shù)據(jù)中心的價值
小孩子才分對錯丑孩,成年人只看利弊冀宴。額外引入一個元數(shù)據(jù)生成機制,必然帶來運維成本温学、理解成本略贮、遷移成本等問題,那么它具備怎樣的價值仗岖,來說服大家選擇它呢逃延?上面我們介紹元數(shù)據(jù)中心時已經(jīng)提到了服務測試、服務 MOCK 等場景轧拄,這一節(jié)我們重點探討一下元數(shù)據(jù)中心的價值和使用場景揽祥。
那么,Dubbo服務元數(shù)據(jù)能夠利用到哪些場景呢檩电?下面我們來詳細描述拄丰。
2.3 流式協(xié)議轉(zhuǎn)換
flurry網(wǎng)關對外提供的是HTTP Rest風格的接口府树,并輔以JSON數(shù)據(jù)的形式進行傳輸。因此flurry需要做到將外部的HTTP協(xié)議請求轉(zhuǎn)換為dubbo RPC協(xié)議,然后再將dubbo協(xié)議的返回結果轉(zhuǎn)換為HTTP協(xié)議返回給前端料按。
Dubbo原生泛化實現(xiàn)
泛化模式就是針對Dubbo Consumer端沒有服務接口API包的情況下,使用Map的形式將POJO的每一個屬性映射為Key,Value的模式來請求Dubbo服務端奄侠,在Dubbo服務端處理完成請求之后,再將結果POJO轉(zhuǎn)為Map的形式返回給消費端载矿。
使用泛化模式來實現(xiàn)對Http <-> Dubbo的轉(zhuǎn)換流程大致如下圖所示:
Http請求垄潮,數(shù)據(jù)通過JSON傳輸,其格式嚴格按照接口POJO屬性。返回結果再序列化為Json返回前端∶瓶現(xiàn)在大多數(shù)開源的網(wǎng)關,在dubbo協(xié)議適配上都是采用的泛化模式來做到協(xié)議轉(zhuǎn)換的,這其中就包括 Soul 等弯洗。
泛化模式的協(xié)議轉(zhuǎn)換數(shù)據(jù)流動流程:
JsonString -> JSONObject(Map) -> Binary
將JSON 字符串轉(zhuǎn)換為 JSON 對象模型(JSONObject),此處通過第三方JSON映射框架(如Google的Gson, 阿里的FastJSON等)來做,然后將Map通過Hessian2 協(xié)議序列化為Binaray。
泛化缺點
泛化過程數(shù)據(jù)流會經(jīng)過了三次轉(zhuǎn)換, 會產(chǎn)生大量的臨時對象, 有很大的內(nèi)存要求逢勾。使用反射方式對于旨在榨干服務器性能以獲取高吞吐量的系統(tǒng)來說, 難以達到性能最佳牡整。
同時服務端也會對泛化請求多一重 Map <-> POJO 的來回轉(zhuǎn)換的過程。整體上溺拱,與普通的Dubbo調(diào)用相比有10-20%的損耗果正。
自定義Dubbo-Json協(xié)議
我們的需求是要打造一款高性能、異步盟迟、流式的Dubbo API網(wǎng)關,當然要對不能容忍上述泛化帶來的序列化的痛點,針對http與dubbo協(xié)議轉(zhuǎn)換潦闲,我們的要求如下:
自定義的Dubbo-Json協(xié)議參考了dapeng-soa 的流式解析協(xié)議的思想,詳情請參考:dapeng-json
針對上述泛化模式轉(zhuǎn)換Dubbo協(xié)議的缺點,我們在flurry-core 中的 Dubbo-Json 序列化協(xié)議做到了這點攒菠,下面我們來講解它是如何高效率的完成JsonString到 dubbo hessian2 序列化buffer的轉(zhuǎn)換的。
高性能
此協(xié)議用來在網(wǎng)關中作為http和rpc協(xié)議的轉(zhuǎn)換,要求其能實現(xiàn)高效的序列化以及反序列化, 以支持網(wǎng)關海量請求的處理.低內(nèi)存使用
雖然大部分情況下的JSON請求歉闰、返回都是數(shù)據(jù)量較小的場景, 但作為平臺框架, 也需要應對更大的JSON請求和返回, 比如1M辖众、甚至10M. 在這些場景下, 如果需要占用大量的內(nèi)存, 那么勢必導致巨大的內(nèi)存需求, 同時引發(fā)頻繁的GC操作, 也會聯(lián)動影響到整個網(wǎng)關的性能.
Dubbo-Json參考了XML SAX API的設計思想, 創(chuàng)造性的引入了JSON Stream API, 采用流式的處理模式, 實現(xiàn)JSON 對 hessian2 的雙向轉(zhuǎn)換, 無論數(shù)據(jù)包有多大, 都可以在一定固定的內(nèi)存規(guī)模內(nèi)完成.
- 容錯性好
前面我們引入了服務元數(shù)據(jù)的概念,因此在此基礎上,通過元數(shù)據(jù)提供的接口文檔站點,自動生成請求數(shù)據(jù)模板,每一個屬性的類型精確定義和敬,譬如必須使用正確的數(shù)據(jù)類型, 有的字段是必填的等凹炸。
那么在Json -> Hessian2Buffer的轉(zhuǎn)換過程中,如果出現(xiàn)與元數(shù)據(jù)定義不符合的情況昼弟,就回直接報錯啤它,定位方便。
流式協(xié)議解析過程
流式協(xié)議舱痘,顧名思義就是邊讀取邊解析,數(shù)據(jù)像水流一樣在管道中流動,邊流動邊解析变骡,最后,數(shù)據(jù)解析完成時芭逝,轉(zhuǎn)換成的hessian協(xié)議也已全部寫入到了buffer中塌碌。
這里處理的核心思想就是實現(xiàn)自己的Json to hessian2 buffer 的語法和此法解析器,并配合前文提及的元數(shù)據(jù)功能,對每一個讀取到的json片段通過元數(shù)據(jù)獲取到其類型,并使用 hessian2協(xié)議以具體的方式寫入到buffer中旬盯。
JSON結構簡述
首先我們來看看JSON的結構. 一個典型的JSON結構體如下
{
"request": {
"orderNo": "1023101",
"productCount": 13,
"totalAmount: 16.54
}
}
其對應Java POJO 自然就是上述三個屬性,這里我們略過台妆。下面是POJO生成的元數(shù)據(jù)信息
<struct namespace="org.apache.dubbo.order" name="OrderRequest">
<fields>
<field tag="0" name="orderNo" optional="false" privacy="false">
<dataType>
<kind>STRING</kind>
</dataType>
</field>
<field tag="1" name="productCount" optional="false" privacy="false">
<dataType>
<kind>INTEGER</kind>
</dataType>
</field>
<field tag="2" name="totalAmount" optional="false" privacy="false">
<dataType>
<kind>DOUBLE</kind>
</dataType>
</field>
</fields>
</struct>
Json解析器
相比XML而言翎猛,JSON數(shù)據(jù)類型比較簡單, 由Object/Array/Value/String/Boolean/Number
等元素組成, 每種元素都由特定的字符開和結束. 例如Object以'{'以及'}'這兩個字符標志開始以及結束, 而Array是'['以及']'. 簡單的結構使得JSON比較容易組裝以及解析。
如圖,我們可以清晰的了解JSON的結構,那么對上述JSON進行解析時,當每一次解析到一個基本類型時,先解析到key接剩,然后根據(jù)key到元數(shù)據(jù)信息中獲取到其value類型切厘,然后直接根據(jù)對應類型的hessian2序列化器將其序列化到byte buffer中。
當解析到引用類型,即 Struct類型時,我們將其壓入棧頂,就和java方法調(diào)用壓棧操作類似搂漠。
通過上面的步驟一步一步迂卢,每解析一步Json,就將其寫入到byte buffer中桐汤,最終完成整個流式的解析過程而克。
拿上面json為例:
{
"request": {
"orderNo": "1023101",
"productCount": 13,
"totalAmount: 16.54
}
}
解析器每一步會解析一個單元,首先解析到request,然后根據(jù)請求附帶的接口怔毛、版本等信息找到當前請求服務的元數(shù)據(jù)信息(元數(shù)據(jù)信息緩存在網(wǎng)關之中员萍,定期刷新)。
開始解析屬性,解析到 orderNo key時拣度,通過元數(shù)據(jù)中得知其數(shù)據(jù)類型為String碎绎,然后再解析完value之后,通過調(diào)用hessian2的writeString API將其寫入到hessian2序列化的byte buffer 緩存中。
依次解析后面的屬性抗果,當全部屬性解析完成之后,解析到最后一個 } 時,此時證明數(shù)據(jù)全部解析完畢,于是hessian buffer 通過flush 將數(shù)據(jù)全部寫入到bytebuffer中筋帖,請求直接發(fā)送出去。
Dubbo服務端再接收到這個序列化的buffer之后冤馏,會像其他普通dubbo consumer調(diào)用服務的模式一樣的去解析,然后反序列化為請求實體,進行業(yè)務邏輯處理日麸。
flurry網(wǎng)關再接受到返回的數(shù)據(jù)時,在沒有反序列化之前逮光,其是一個hessian2的二級制字節(jié)流,我們?nèi)匀煌ㄟ^dubbo-json的解析模式,直接將反序列化出來的屬性寫為Json String
總結:
上述整個請求和響應,網(wǎng)關處理如下:
- request: Json -> hessian2二進制字節(jié)流
- response:hessian2二進制字節(jié)流 -> json
請求和響應中沒有像泛化模式中的中間對象轉(zhuǎn)換代箭,直接一步到位,沒有多余的臨時對象占用內(nèi)存,沒有多余的數(shù)據(jù)轉(zhuǎn)換,整個過程像在管道中流式的進行涕刚。
2.4 flurry網(wǎng)關與tomcat接入層比較
傳統(tǒng)的dubbo服務接入層是采用tomcat作為容器來實現(xiàn)嗡综,每一個業(yè)務模塊對應一個tomcat應用,其本身需要以來dubbo各個服務的API接口包,tomcat中啟動幾十個傳統(tǒng)的dubbo consumer 服務杜漠,然后通過webmvc的模式提供http接口极景。
如上圖所示,flurry dubbo網(wǎng)關不必依賴任何dubbo接口API包,而是直接通過獲取服務元數(shù)據(jù)碑幅、并通過dubbo-json流式協(xié)議來調(diào)用后端服務戴陡。其本身不會耦合業(yè)務邏輯。
2.5 性能測試
測試環(huán)境
硬件部署與參數(shù)調(diào)整
機型 | 角色 | CPU | 內(nèi)存 | 帶寬 |
---|---|---|---|---|
CVM虛擬機 | API網(wǎng)關 | 8核 | 16GB | 10Mbp/s |
CVM虛擬機 X2 | Dubbo Provider | 4核 | 8GB | 10Mbp/s |
CVM虛擬機 | wrk壓測機 | 8核 | 16GB | 10Mbp/s |
測試目的
對基于Y-Hessian的 異步化沟涨、流式轉(zhuǎn)換的Yunji Dubbo API網(wǎng)關進行性能壓測恤批,了解它的處理能力極限是多少,這樣有便于我們推斷其上線后的處理能力,以及對照現(xiàn)有的Tomcat接入層模式的優(yōu)勢,能夠節(jié)約多少資源裹赴,做到心里有數(shù)喜庞。
測試腳本
性能測試場景
網(wǎng)關入?yún)?.5k請求json诀浪,深度為3層嵌套結構嘴脾,服務端接收請求纲堵,返回0.5k返回包
網(wǎng)關入?yún)?k請求json,深度為3層嵌套結構,服務端接收請求余佛,返回0.5k返回包
上述場景均使用wrk在壓測節(jié)點上進行5~10min鐘的壓測晰房,壓測參數(shù)基本為12線程256連接或者512連接求摇,以發(fā)揮最大的壓測性能。
測試結果
性能指標(量化)
場景名稱 | wrk壓測參數(shù) | Avg RT | 實際QPS值 |
---|---|---|---|
0.5k數(shù)據(jù) | -t12 -c256 | 3.77ms | 69625 |
0.5k數(shù)據(jù) | -t12 -c 512 | 7.26ms | 71019 |
2k數(shù)據(jù) | -t12 -c256 | 5.48ms | 46576 |
2k數(shù)據(jù) | -t12 -c512 | 10.80ms | 46976 |
運行狀況(非量化)
API網(wǎng)關(8c16g)運行良好殊者,壓測期間CPU占用為550%左右
兩個 Dubbo Provider 服務運行良好与境,CPU占用為100%左右
各個角色內(nèi)存運行穩(wěn)定,無OOM猖吴,無不合理的大內(nèi)存占用等摔刁。
3.總結
flurry集Dubbo網(wǎng)關、異步海蔽、流式共屈、高性能于一身,其目標就是替代一些以tomcat作為dubbo消費者的接入層,以更少的節(jié)點獲得更多的性能提升党窜,節(jié)約硬件資源和軟件資源拗引。
4.后續(xù)
后續(xù)在flurry的基礎上,將實現(xiàn)鑒權管理幌衣、流量控制寺擂、限流熔斷、監(jiān)控收集等等功能
5.參考項目
Flurry: 基于Dubbo服務的高性能泼掠、異步、流式網(wǎng)關
dubbo-json: 自定義的Dubbo協(xié)議垦细,支持流式序列化模式择镇,為flurry網(wǎng)關序列化/反序列化組件。
Yunji-doc-site: 與元數(shù)據(jù)集成相關的項目,以及文檔站點
dapeng-soa: Dapeng-soa 是一個輕量級括改、高性能的微服務框架腻豌,構建在Netty以及定制的精簡版Thrift之上。 同時嘱能,從Thrift IDL文件自動生成的服務元數(shù)據(jù)信息是本框架的一個重要特性吝梅,很多其它重要特性都依賴于服務元數(shù)據(jù)信息。 最后惹骂,作為一站式的微服務解決方案苏携,Dapeng-soa還提供了一系列的腳手架工具以支持用戶快速的搭建微服務系統(tǒng)
dapeng-json:dapeng-json協(xié)議介紹