1.背景
什么是API網關,它的作用是什么,產生的背景是啥?
從架構的角度來看,API網關暴露http接口服務,其本身不涉及業務邏輯,只負責包括請求路由、負載均衡、權限驗證、流量控制、緩存等等功能。其定位類似於Nginx請求轉發、但功能要多於Nginx,背后連接了成百上千個后台服務,這些服務協議可能是rest的,也可能是rpc協議等等。
網關的定位決定了它生來就需要高性能、高效率的。網關對接着成百上千的服務接口,承受者高並發的業務需求,因此我們對其性能要求嚴苛,其基本功能如下:
-
協議轉換
一般從前端請求來的都是HTTP接口,而在分布式構建的背景下,我們后台服務基本上都是以RPC協議而開發的服務。這樣就需要網關作為中間層對請求和響應作轉換。
將HTTP協議轉換為RPC,然后返回時將RPC協議轉換為HTTP協議 -
請求路由
網關背后可能連接着成本上千個服務,其需要根據前端請求url來將請求路由到后端節點中去,這其中需要做到負載均衡 -
統一鑒權
對於鑒權操作不涉及到業務邏輯,那么可以在網關層進行處理,不用下層到業務邏輯。 -
統一監控
由於網關是外部服務的入口,所以我們可以在這里監控我們想要的數據,比如入參出參,鏈路時間。 -
流量控制,熔斷降級
對於流量控制,熔斷降級非業務邏輯可以統一放到網關層。
有很多業務都會自己去實現一層網關層,用來接入自己的服務,但是對於整個公司來說這還不夠。
2.Flurry Dubbo API網關
Flurry是雲集自研的一款輕量級、異步流式化、針對Dubbo的高性能API網關。與業界大多數網關不同的是,flurry自己實現了 http與dubbo協議互轉的流式化的dubbo-json協議,可高性能、低內存要求的對http和dubbo協議進行轉換。除此之外,其基於 netty作為服務容器,提供服務元數據模型等等都是非常具有特點的。下面我們將詳細介紹 flurry的特性:
2.1 基於Netty容器
傳統網關大多采用tomcat作為容器,其請求於響應沒有做到異步,tomcat會有一個核心線程池來處理請求和響應,如果RT比較高的話,將會對性能有一定的影響。
Flurry 網關請求響應基於Netty線程模型,后者是實現了Reactive,反應式模式規范的,其設計就是來榨干CPU的,可以大幅提升單機請求響應的處理能力。
最終,Flurry通過使用Netty線程模型和NIO通訊協議實現了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 服務元數據模型
所謂的服務元數據信息,是指服務接口的方法列表、每個方法的輸入輸出參數信息、每個參數的字段信息以及上述(方法、參數以及字段)的注釋,其非常類似於Java Class的反射信息。
元數據的作用
有了服務元數據,我們就可以不必需要服務的API包,並能夠清晰的知道整個服務API的定義。
這在Dubbo服務Mock調用、服務測試、文檔站點、流式調用等等場景下都可以發揮搶到的作用。
元數據與注冊中心數據比較
\ | 服務元數據 | 注冊中心數據 |
---|---|---|
變化頻率 | 基本不變,隨着服務迭代更改而更改 | 隨着服務節點上下線而隨時進行變化 |
職責 | 描述服務,定義服務的基本屬性 | 存儲地址列表 |
使用場景 | 無API依賴包模式調用(直接json),文檔站點,Mock測試等 | 服務治理和調用 |
交互模型 | 服務編譯期生成元信息,client通過接口調用模式獲取 | 發布訂閱模型,線上進行交互 |
元數據中心的價值
小孩子才分對錯,成年人只看利弊。額外引入一個元數據生成機制,必然帶來運維成本、理解成本、遷移成本等問題,那么它具備怎樣的價值,來說服大家選擇它呢?上面我們介紹元數據中心時已經提到了服務測試、服務 MOCK 等場景,這一節我們重點探討一下元數據中心的價值和使用場景。
那么,Dubbo服務元數據能夠利用到哪些場景呢?下面我們來詳細描述。
2.3 流式協議轉換
flurry網關對外提供的是HTTP Rest風格的接口,並輔以JSON數據的形式進行傳輸。因此flurry需要做到將外部的HTTP協議請求轉換為dubbo RPC協議,然后再將dubbo協議的返回結果轉換為HTTP協議返回給前端。
Dubbo原生泛化實現
泛化模式就是針對Dubbo Consumer端沒有服務接口API包的情況下,使用Map的形式將POJO的每一個屬性映射為Key,Value的模式來請求Dubbo服務端,在Dubbo服務端處理完成請求之后,再將結果POJO轉為Map的形式返回給消費端。
使用泛化模式來實現對Http <-> Dubbo的轉換流程大致如下圖所示:
Http請求,數據通過JSON傳輸,其格式嚴格按照接口POJO屬性。返回結果再序列化為Json返回前端。現在大多數開源的網關,在dubbo協議適配上都是采用的泛化模式來做到協議轉換的,這其中就包括 Soul 等。
泛化模式的協議轉換數據流動流程:
JsonString -> JSONObject(Map) -> Binary
將JSON 字符串轉換為 JSON 對象模型(JSONObject),此處通過第三方JSON映射框架(如Google的Gson, 阿里的FastJSON等)來做,然后將Map通過Hessian2 協議序列化為Binaray。
泛化缺點
-
泛化過程數據流會經過了三次轉換, 會產生大量的臨時對象, 有很大的內存要求。使用反射方式對於旨在榨干服務器性能以獲取高吞吐量的系統來說, 難以達到性能最佳。
-
同時服務端也會對泛化請求多一重 Map <-> POJO 的來回轉換的過程。整體上,與普通的Dubbo調用相比有10-20%的損耗。
自定義Dubbo-Json協議
我們的需求是要打造一款高性能、異步、流式的Dubbo API網關,當然要對不能容忍上述泛化帶來的序列化的痛點,針對http與dubbo協議轉換,我們的要求如下:
自定義的Dubbo-Json協議參考了dapeng-soa 的流式解析協議的思想,詳情請參考:dapeng-json
針對上述泛化模式轉換Dubbo協議的缺點,我們在flurry-core 中的 Dubbo-Json 序列化協議做到了這點,下面我們來講解它是如何高效率的完成JsonString到 dubbo hessian2 序列化buffer的轉換的。
-
高性能
此協議用來在網關中作為http和rpc協議的轉換,要求其能實現高效的序列化以及反序列化, 以支持網關海量請求的處理. -
低內存使用
雖然大部分情況下的JSON請求、返回都是數據量較小的場景, 但作為平台框架, 也需要應對更大的JSON請求和返回, 比如1M、甚至10M. 在這些場景下, 如果需要占用大量的內存, 那么勢必導致巨大的內存需求, 同時引發頻繁的GC操作, 也會聯動影響到整個網關的性能.
Dubbo-Json參考了XML SAX API的設計思想, 創造性的引入了JSON Stream API, 采用流式的處理模式, 實現JSON 對 hessian2 的雙向轉換, 無論數據包有多大, 都可以在一定固定的內存規模內完成.
- 容錯性好
前面我們引入了服務元數據的概念,因此在此基礎上,通過元數據提供的接口文檔站點,自動生成請求數據模板,每一個屬性的類型精確定義,譬如必須使用正確的數據類型, 有的字段是必填的等。
那么在Json -> Hessian2Buffer的轉換過程中,如果出現與元數據定義不符合的情況,就回直接報錯,定位方便。
流式協議解析過程
流式協議,顧名思義就是邊讀取邊解析,數據像水流一樣在管道中流動,邊流動邊解析,最后,數據解析完成時,轉換成的hessian協議也已全部寫入到了buffer中。
這里處理的核心思想就是實現自己的Json to hessian2 buffer 的語法和此法解析器,並配合前文提及的元數據功能,對每一個讀取到的json片段通過元數據獲取到其類型,並使用 hessian2協議以具體的方式寫入到buffer中。
JSON結構簡述
首先我們來看看JSON的結構. 一個典型的JSON結構體如下
{
"request": {
"orderNo": "1023101",
"productCount": 13,
"totalAmount: 16.54
}
}
其對應Java POJO 自然就是上述三個屬性,這里我們略過。下面是POJO生成的元數據信息
<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數據類型比較簡單, 由Object/Array/Value/String/Boolean/Number
等元素組成, 每種元素都由特定的字符開和結束. 例如Object以'{'以及'}'這兩個字符標志開始以及結束, 而Array是'['以及']'. 簡單的結構使得JSON比較容易組裝以及解析。
如圖,我們可以清晰的了解JSON的結構,那么對上述JSON進行解析時,當每一次解析到一個基本類型時,先解析到key,然后根據key到元數據信息中獲取到其value類型,然后直接根據對應類型的hessian2序列化器將其序列化到byte buffer中。
當解析到引用類型,即 Struct類型時,我們將其壓入棧頂,就和java方法調用壓棧操作類似。
通過上面的步驟一步一步,每解析一步Json,就將其寫入到byte buffer中,最終完成整個流式的解析過程。
拿上面json為例:
{
"request": {
"orderNo": "1023101",
"productCount": 13,
"totalAmount: 16.54
}
}
-
解析器每一步會解析一個單元,首先解析到request,然后根據請求附帶的接口、版本等信息找到當前請求服務的元數據信息(元數據信息緩存在網關之中,定期刷新)。
-
開始解析屬性,解析到 orderNo key時,通過元數據中得知其數據類型為String,然后再解析完value之后,通過調用hessian2的writeString API將其寫入到hessian2序列化的byte buffer 緩存中。
-
依次解析后面的屬性,當全部屬性解析完成之后,解析到最后一個 } 時,此時證明數據全部解析完畢,於是hessian buffer 通過flush 將數據全部寫入到bytebuffer中,請求直接發送出去。
-
Dubbo服務端再接收到這個序列化的buffer之后,會像其他普通dubbo consumer調用服務的模式一樣的去解析,然后反序列化為請求實體,進行業務邏輯處理。
-
flurry網關再接受到返回的數據時,在沒有反序列化之前,其是一個hessian2的二級制字節流,我們仍然通過dubbo-json的解析模式,直接將反序列化出來的屬性寫為Json String
總結:
上述整個請求和響應,網關處理如下:
- request: Json -> hessian2二進制字節流
- response:hessian2二進制字節流 -> json
請求和響應中沒有像泛化模式中的中間對象轉換,直接一步到位,沒有多余的臨時對象占用內存,沒有多余的數據轉換,整個過程像在管道中流式的進行。
2.4 flurry網關與tomcat接入層比較
傳統的dubbo服務接入層是采用tomcat作為容器來實現,每一個業務模塊對應一個tomcat應用,其本身需要以來dubbo各個服務的API接口包,tomcat中啟動幾十個傳統的dubbo consumer 服務,然后通過webmvc的模式提供http接口。
如上圖所示,flurry dubbo網關不必依賴任何dubbo接口API包,而是直接通過獲取服務元數據、並通過dubbo-json流式協議來調用后端服務。其本身不會耦合業務邏輯。
2.5 性能測試
測試環境
硬件部署與參數調整
機型 | 角色 | CPU | 內存 | 帶寬 |
---|---|---|---|---|
CVM虛擬機 | API網關 | 8核 | 16GB | 10Mbp/s |
CVM虛擬機 X2 | Dubbo Provider | 4核 | 8GB | 10Mbp/s |
CVM虛擬機 | wrk壓測機 | 8核 | 16GB | 10Mbp/s |
測試目的
對基於Y-Hessian的 異步化、流式轉換的Yunji Dubbo API網關進行性能壓測,了解它的處理能力極限是多少,這樣有便於我們推斷其上線后的處理能力,以及對照現有的Tomcat接入層模式的優勢,能夠節約多少資源,做到心里有數。
測試腳本
性能測試場景
-
網關入參0.5k請求json,深度為3層嵌套結構,服務端接收請求,返回0.5k返回包
-
網關入參2k請求json,深度為3層嵌套結構,服務端接收請求,返回0.5k返回包
上述場景均使用wrk在壓測節點上進行5~10min鍾的壓測,壓測參數基本為12線程256連接或者512連接,以發揮最大的壓測性能。
測試結果
性能指標(量化)
場景名稱 | wrk壓測參數 | Avg RT | 實際QPS值 |
---|---|---|---|
0.5k數據 | -t12 -c256 | 3.77ms | 69625 |
0.5k數據 | -t12 -c 512 | 7.26ms | 71019 |
2k數據 | -t12 -c256 | 5.48ms | 46576 |
2k數據 | -t12 -c512 | 10.80ms | 46976 |
運行狀況(非量化)
-
API網關(8c16g)運行良好,壓測期間CPU占用為550%左右
-
兩個 Dubbo Provider 服務運行良好,CPU占用為100%左右
-
各個角色內存運行穩定,無OOM,無不合理的大內存占用等。
3.總結
flurry集Dubbo網關、異步、流式、高性能於一身,其目標就是替代一些以tomcat作為dubbo消費者的接入層,以更少的節點獲得更多的性能提升,節約硬件資源和軟件資源。
4.后續
后續在flurry的基礎上,將實現鑒權管理、流量控制、限流熔斷、監控收集等等功能
5.參考項目
Flurry: 基於Dubbo服務的高性能、異步、流式網關
dubbo-json: 自定義的Dubbo協議,支持流式序列化模式,為flurry網關序列化/反序列化組件。
Yunji-doc-site: 與元數據集成相關的項目,以及文檔站點
dapeng-soa: Dapeng-soa 是一個輕量級、高性能的微服務框架,構建在Netty以及定制的精簡版Thrift之上。 同時,從Thrift IDL文件自動生成的服務元數據信息是本框架的一個重要特性,很多其它重要特性都依賴於服務元數據信息。 最后,作為一站式的微服務解決方案,Dapeng-soa還提供了一系列的腳手架工具以支持用戶快速的搭建微服務系統
dapeng-json:dapeng-json協議介紹