您好,登錄后才能下訂單哦!
本篇內容介紹了“性能超高的API網關之怎么使用Fizz Gateway”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
中間層在Web網站上的部署偏前,一般部署于防火墻及Nginx之后,更多面向C端用戶服務,所以在性能并發量上有較高的要求,大部分團隊在選型上會選擇異步框架。正因為其直接面向C端,變化較多,大部分需要經常性地變更或者配置的代碼都會安排在這一層次,發布非常頻繁。此外,很多團隊使用編譯型語言進行編碼,而非解釋型語言。這三個因素組合在一起,使得開發者調試與開發非常痛苦。比如,我們曾經選擇Play2框架,這是一個異步Java框架,需要開發者能夠流暢地編寫異步,但是熟悉調試技巧的同事也不多。在代碼里面配置了各種請求參數,以及結果處理,看似非常簡單,但是聯調、單元測試、或者配置文件修改之后等待Java編譯花費的時間和精力是巨大的。如果異步編碼規范也有問題,這對開發者來說無疑是一種折磨。
public F.Promise<BaseDto<List<Good>>> getGoodsByCondi(final StringBuilder searchParams, final GoodsQueryParam param) { final Map<String, String> params = new TreeMap<String, String>(); final OutboundApiKey apiKey = OutboundApiKeyUtils.getApiKey("search.api"); params.put("apiKey", apiKey.getApiKey()); params.put("service", "Search.getMerchandiseBy"); if(StringUtils.isNotBlank(param.getSizeName())){ try { searchParams.append("sizes:" + URLEncoder.encode(param.getSizeName(), "utf-8") + ";"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } if (param.getStock() != null) { searchParams.append("hasStock:" + param.getStock() + ";"); } if (param.getSort() != null && !param.getSort().isEmpty()) { searchParams.append("orderBy:" + param.getSort() + ";"); } searchParams.append("limit:" + param.getLimit() + ";page:" + param.getStart()); params.put("traceId", "open.api.vip.com"); ApiKeySignUtil.getApiSignMap(params,apiKey.getApiSecret(),"apiSign"); String url = RemoteServiceUrl.SEARCH_API_URL; Promise<HttpResponse> promise = HttpInvoker.get(url, params); final GoodListBaseDto retVal = new GoodListBaseDto(); Promise<BaseDto<List<Good>>> goodListPromise = promise.map(new Function<HttpResponse, BaseDto<List<Good>>>() { @Override public BaseDto<List<Good>> apply(HttpResponse httpResponse)throws Throwable { JsonNode json = JsonUtil.toJsonNode(httpResponse.getBody()); if (json.get("code").asInt() != 200) { Logger.error("Error :" + httpResponse.getBody()); return new BaseDto<List<Good>>(CommonError.SYS_ERROR); } JsonNode result = json.get("items"); Iterator<JsonNode> iterator = result.elements(); final List<Good> goods = new ArrayList<Good>(); while (iterator.hasNext()) { final Good good = new Good(); JsonNode goodJson = iterator.next(); good.setGid(goodJson.get("id").asText()); good.setDiscount(String.format("%.2f", goodJson.get("discount").asDouble())); good.setAgio(goodJson.get("setAgio").asText()); if (goodJson.get("brandStoreSn") != null) { good.setBrandStoreSn(goodJson.get("brandStoreSn").asText()); } Iterator<JsonNode> whIter = goodJson.get("warehouses").elements(); while (whIter.hasNext()) { good.getWarehouses().add(whIter.next().asText()); } if (goodJson.get("saleOut").asInt() == 1) { good.setSaleOut(true); } good.setVipPrice(goodJson.get("vipPrice").asText()); goods.add(good); } retVal.setData(goods); return retVal; } }); if(param.getBrandId() != null && !param.getBrandId().isEmpty()))){ final Promise<List<ActiveTip>> pmsPromise = service.getActiveTipsByBrand(param.getBrandId()); return goodListPromise.flatMap(new Function<BaseDto<List<Good>>, Promise<BaseDto<List<Good>>>>() { @Override public Promise<BaseDto<List<Good>>> apply(BaseDto<List<Good>> listBaseDto) throws Throwable { return pmsPromise.flatMap(new Function<List<ActiveTip>, Promise<BaseDto<List<Good>>>>() { @Override public Promise<BaseDto<List<Good>>> apply(List<ActiveTip> activeTips) throws Throwable { retVal.setPmsList(activeTips); BaseDto<List<Good>> baseDto = (BaseDto<List<Good>>)retVal; return Promise.pure(baseDto); } }); } }); } return goodListPromise; }
上述代碼只是摘抄了其中一個過程函數。如果我們將中間層的場景設置得更為復雜一些,我們要解決的就不僅僅是編碼性能、編碼質量、編碼時間的問題。
## “復雜”場景問題
微服務顆粒度較細,為了實現簡潔的前端邏輯以及較少的服務調用次數,我們針對C端的大部分輸出是聚合的結果。比如,我們一個搜索的中間層邏輯,其服務是這樣一個過程:
獲取會員信息、會員卡列表、會員積分余額,因為不同級別的會員會有不同價格;
獲取用戶的優惠券信息,這部分會對計算出來的價格產生影響;
獲取搜索的結果信息,結果來自三部分,商旅商品的庫存價格,猜你喜歡的庫存價格,推薦位的庫存價格,海外商品的庫存價格。
這其中涉及到的服務有:中間層服務(聚合服務)、會員服務、優惠券服務、推薦服務、企業服務、海外搜索服務、搜索服務。此外,還有各種類型的緩存設施以及數據庫的配置服務。
public List<ExtenalProduct> searchProduct(String traceId, ExtenalProductQueryParam param, MemberAssetVO memberAssetVO, ProductInfoResultVO resultVO,boolean needAddPrice) { // 用戶可用優惠券的configId String configIds = memberAssetVO == null ? null : memberAssetVO.getConfigIds(); // 特殊項目,限制不能使用優惠券功能 if(customProperties.getIgnoreChannel().contains(param.getChannelCode())) { configIds = null; } final String configIdConstant = configIds; // 主搜索列表信息 Mono<List<ExtenalProduct>> innInfos = this.search(traceId, param, configIds, resultVO); return innInfos.flatMap(inns -> { // 商旅產品推薦 Mono<ExtenalProduct> busiProduct = this.recommendProductService.getBusiProduct(traceId, param, configIdConstant); // 會員產品推薦(猜您喜歡) Mono<ExtenalProduct> guessPref = this.recommendProductService.getGuessPref(traceId, param, configIdConstant); // 業務相關查詢 String registChainId = memberAssetVO == null || memberAssetVO.getMember() == null ? null : memberAssetVO.getMember().getRegistChainId(); Mono<ExtenalProduct> registChain = this.recommendProductService.registChain(traceId, param, configIdConstant, registChainId); // 店長熱推產品 Mono<ExtenalProduct> advert = this.recommendProductService.advert(traceId, param, configIdConstant); return Mono.zip(busiProduct, guessPref, registChain, advert).flatMap(product -> { // 推薦位(廣告位)包裝 List<ExtenalProduct> products = recommendProductService.setRecommend(inns, product.getT1(), product.getT2(), product.getT3(), product.getT4(), param); // 設置其他參數 return this.setOtherParam(traceId, param, products, memberAssetVO); }); }).block(); }
這個服務的Service層會經常性地根據產品需求和底層微服務接口的變更做出調整改變,而研發的接口調用時序圖卻因為團隊的這些更改對應不上代碼。
除了上述問題外,該服務中的多個微服務異步調用聚合的編碼問題也未能被妥善處理,因為其使用的Spring-MVC框架編碼風格是同步的,而Service層卻使用了異步的Mono,只能不合時宜地用block。這些代碼更改、文檔缺失、編碼質量共同組成了中間層的代碼管理問題。
## 野蠻發展問題
我參與過一個初創技術團隊建設。最開始,因為快速開發的需要,我們傾向于做一個胖服務,但當團隊規模開始擴大時,我們卻需要逐步地將胖服務分拆為微服務,開始產生中間層團隊,他們的主要目的是應用于底層服務的聚合。
但是,有一段時間,我們的招聘速度并不能完全趕上服務數量的增長速度,于是寫底層的同事就需要不斷地切換編碼思路。因為除了要編寫分拆之后的底層微服務,還要編寫聚合的中間層服務。
當我停掉某一些項目時,開始整頓人手,我又發現一個殘酷事實:每個人手上都有數十個中間層服務,因此無法換掉任何一個人。因為經過多次地換手,同事們已經搞不清中間服務的聯系。
另外,還有各種授權方式,因為團隊一直以來的野蠻成長,各種授權方式都混在一起,既有簡單的,又有復雜的,既有合理的,還有不合理的。總之,團隊沒有人能搞清楚。
經過一段時間的發展后,通過整理線上服務,我們發現很多資源浪費,比如有時候,僅僅一個接口就使用了一個微服務。在早起,這些微服務是有較大規模請求的,但是后來,項目被遺棄,也沒有了流量,但是運行的接口依然在線上。而作為團隊管理人員的我甚至沒有任何書面上接口匯總的統計信息。
當老板告訴我,把合作公司對接的服務暫停時,我無法做到邏輯上停機返回一個業務異常。作為一個多渠道發展的上游庫存供應商,我們對接的渠道很多,提供給客戶的接口有很多特別定制的需求,這些需求一般就在中間的邏輯控制代碼里面,渠道下線了,也不會做任何調整,因為開發者需要根據需求來進行代碼更新。
而且,中間層團隊對外聯合調試也是長久以來存在的一個問題。經常有前端同事向我抱怨,后端的同事不肯增加數據處理邏輯的代碼,而作為前端,他們不得不增加很多轉換數據的代碼來適配界面的邏輯。而像在小程序這種的對包大小進行限制的環境里,這些代碼的移動在發展后期就成為一個老大難問題。
# 網關的選型失敗
當時,市面上存在兩種類型的解決方案:
中間層的解決方案。中間層方案一般提供裸異步服務、其他插件以及功能根據需求自定義,部分中間層的服務經過改造后也具備網關的部分功能。
網關的解決方案。網關方案一般圍繞著微服務全家桶提供,或者自成一派,提供通用型的功能(如路由功能)。當然,部分網關經過自定義改造也能加入中間層的業務功能。
我們的業務發展變化非常快。如果市面上已有的網關方案能滿足需求,我們又有能力進行二次開發,我們非常樂意使用。
當時,Eolinker是我們的API 自動測試的供應商,提供了對應的管理型網關,但語言是Go。而我們團隊的技術棧主要以Java為主,運維的部署方案也一直圍繞著Java,這意味我們的選型就偏窄,因此不得不放棄這一想法。
在之前,我們也選擇過Kong網關,但是引入一個新的復雜技術棧是一件成本不低的事情,比如,Lua的招聘與二次開發是難以避免的痛。
另外,Gravitee、Zuul、Vert.x 都是不同小規模團隊使用過的網關。談及最多的特性是:
1、支持熔斷、流量控制和過載保護
2、支持特別高的并發
3、秒殺
然而,對商業而言,熔斷、流量控制和過載保護應該是最后考慮的措施。而且,對一個成長中的團隊來說,服務的過載崩潰是需要經歷較長時間的業務沉淀。
另外,秒殺業務的流量更多是維持一個普通水平,其偶爾的高并發也是在我們團隊處理能力范圍之內。換句話說,選型時,更多的是需要結合實際,而不是考慮類似阿里巴巴的流量,我只需考慮中等水平以上并且具備集群擴展性的方式即可。
此前,我們團隊使用比較廣的網關是Vert.x,編碼風格是這樣的,華麗酷炫。
private void dispatchRequests(RoutingContext context) { int initialOffset = 5; // length of `/api/` // run with circuit breaker in order to deal with failure circuitBreaker.execute(future -> { // (1) getAllEndpoints().setHandler(ar -> { // (2) if (ar.succeeded()) { List<Record> recordList = ar.result(); // get relative path and retrieve prefix to dispatch client String path = context.request().uri(); if (path.length() <= initialOffset) { notFound(context); future.complete(); return; } String prefix = (path.substring(initialOffset) .split("/"))[0]; // generate new relative path String newPath = path.substring(initialOffset + prefix.length()); // get one relevant HTTP client, may not exist Optional<Record> client = recordList.stream() .filter(record -> record.getMetadata().getString("api.name") != null) .filter(record -> record.getMetadata().getString("api.name").equals(prefix)) // (3) .findAny(); // (4) simple load balance if (client.isPresent()) { doDispatch(context, newPath, discovery.getReference(client.get()).get(), future); // (5) } else { notFound(context); // (6) future.complete(); } } else { future.fail(ar.cause()); } }); }).setHandler(ar -> { if (ar.failed()) { badGateway(ar.cause(), context); // (7) } }); }
但是,Vert.x社區缺乏支持以及入門成本高的問題一直存在,而團隊甚至找不到更多合適的同事來維護代碼。
以上網關的選型失敗讓我們意識到,市面沒有完全符合我們公司的情況的“瑞士軍刀”,由此我們開始走上了自研之路,開始進行Fizz網關的設計。
# 走上自研網關之路
我們需要網關么?網關層解決什么問題?這兩個問題不言而喻。我們需要網關,因為它可以幫我們解決負載均衡、聚合、授權、監控、限流、日志、權限控制等一系列的問題。同時,我們也需要中間層,細化服務顆粒度的微服務讓我們不得不通過中間層聚合它們。
而我們不需要的是復雜的編碼、冗余的膠水代碼,以及冗長的發布流程。
為解決這些問題,我們需要讓網關與中間層模糊界限,抹去網關和中間層隔閡,讓網關支持中間層動態編碼,盡可能少的發布部署。為實現這個目的,只需要用一個簡潔的網關模型并同時利用low-code特性盡可能地去覆蓋中間層的功能即可。
## 從原點出發的需求
在復盤當初這個選擇時,我需要再強調下從原點出發的需求:
1、Java技術棧,支持Spring全家桶;
2、方便易用,零培訓也能編排;
3、動態路由能力,隨時隨地能夠開啟新API;
4、高性能且集群可橫向擴展;
5、強熱服務編排能力,支持前后端編碼,隨時隨地更新API;
6、線上編碼邏輯支持;
7、可擴展的安全認證能力,方便日志記錄;
API審核功能,把控所有服務;
可擴展性,強大的插件開發機制;
## Fizz 的技術選型
在選型Spring WebFlux后,因為其單體較強的特性,同事建議命名為Fizz(Fizz是競技游戲《英雄聯盟》中的英雄角色之一,它是一個近戰法師,其擁有AP中數一數二的單體爆發,因此可以克制大部分法師,可以作為一個很好地反制英雄使用)。
WebFlux是一個典型非阻塞異步的框架,它的核心是基于Reactor的相關API實現的。 相對于傳統的web框架來說,它可以運行在諸如Netty、Undertow和支持Servlet3.1的容器上,因此它運行環境的可選擇性要比傳統web框架多很多。
而Spring WebFlux 是一個異步非阻塞式的 Web 框架,它能夠充分利用多核 CPU 的硬件資源去處理大量的并發請求。其依賴Spring的技術棧,代碼風格是這樣的:
public Mono<ServerResponse> getAll(ServerRequest serverRequest) { printlnThread("獲取所有用戶"); Flux<User> userFlux = Flux.fromStream(userRepository.getUsers().entrySet().stream().map(Map.Entry::getValue)); return ServerResponse.ok() .body(userFlux, User.class); }
## Fizz的核心實現
對我們而言,這是一個從零開始的項目,很多同事剛開始沒有信心。我為這個服務寫了第一個服務編排代碼的核心包fizz,并把這個commit寫為“開工大吉”。
我打算所有的服務聚合的定義就靠一個配置文件解決。那么,就有這樣的模型:如果把用戶請求作為輸入,那么響應自然就是輸出,這就是一個管道Pipe;在一個Pipe中,會有不同的Step,對應不同的串聯的步驟;而在一個Step,至少有一個存在著一個Input接收上一個步驟處理的輸出,所有的Input都是并聯的,并且可以并行執行;貫穿于Pipe的生命周期中存在唯一的Context保存中間上下文。
而在每個Input的輸入與輸出,我增加了動態腳本的擴展能力,到現在已經支持JavaScript和groove兩種能力,支持JavaScript的前端邏輯可以在后端得到必要擴展。而我們的配置文件僅僅需要這樣一個腳本:
// 聚合接口配置var aggrAPIConfig = { name: "input name", // 自定義的聚合接口名 debug: false, // 是否為調試模式,默認falsetype: "REQUEST", // 類型,REQUEST/MYSQLmethod: "GET/POST",path: "/proxy/aggr-hotel/hotel/rates", // 格式:/aggr/+服務名+路徑, 分組名以aggr-開頭,表示聚合接口langDef: { // 可選,提示語言定義,入參驗證失敗時依據配置提供不同語言的提示信息,目前支持中文、英文langParam: "input.request.body.languageCode", // 入參語言字段langMapping: { // 字段值與語言的映射關系zh: "0", // 中文en: "1" // 英文}},headersDef: { // 可選,定義聚合接口header部分參數,使用JSON Schema規范(詳見:http://json-schema.org/specification.html),用于參數驗證,接口文檔生成type:"object",properties:{ appId:{ type:"string",title:"應用ID",description:"描述"}},required: ["appId"]},paramsDef: { // 可選,定義聚合接口parameter部分參數,使用JSON Schema規范(詳見:http://json-schema.org/specification.html),用于參數驗證,接口文檔生成type:"object",properties:{ lang:{ type:"string",title:"語言",description:"描述"}}},bodyDef: { // 可選,定義聚合接口body部分參數,使用JSON Schema規范(詳見:http://json-schema.org/specification.html),用于參數驗證,接口文檔生成type:"object",properties:{ userId:{ type:"string",title:"用戶名",description:"描述"}},required: ["userId"]},scriptValidate: { // 可選,用于headersDef、paramsDef、bodyDef無法覆蓋的入參驗證場景type: "", // groovysource: "" // 腳本返回List<String>對象,null:驗證通過,List:錯誤信息列表},validateResponse:{ // 入參驗證失敗響應,處理方式同dataMapping.responsefixedBody: { // 固定的body"code": -411},fixedHeaders: { // 固定header"a":"b"},headers: { // 引用的header},body: { // 引用的header"msg": "validateMsg"},script: { type: "", // groovysource: ""}},dataMapping: { // 聚合接口數據轉換規則response:{ fixedBody: { // 固定的body"code":"b"},fixedHeaders: { // 固定header"a":"b"}, headers: { // 引用的header,默認為源數據類型,如果要轉換類型則以目標類型+空格開頭,如:"int ""abc": "int step1.requests.request1.headers.xyz"},body: { // 引用的header,默認為源數據類型,如果要轉換類型則以目標類型+空格開頭,如:"int ""abc": "int step1.requests.request1.response.id","inn.innName": "step1.requests.request2.response.hotelName","ddd": { // 腳本, 當腳本的返回對象里包含有_stopAndResponse字段且值為true時,會終請求并把腳本的返回結果響應給瀏覽器"type": "groovy","source": ""}},script: { // 腳本計算body的值type: "", // groovysource: ""}}},stepConfigs: [{ // step的配置name: "step1", // 步驟名稱stop: false, // 是否在執行完當前step就返回dataMapping: { // step response數據轉換規則response: { fixedBody: { // 固定的body"a":"b"},body: { // step result"abc": "step1.requests.request1.response.id","inn.innName": "step1.requests.request2.response.hotelName"},script: { // 腳本計算body的值type: "", // groovysource: ""}}}, requests:[ //每個step可以調用多個接口{ // 自定義的接口名 name: "request1", // 接口名,格式request+N type: "REQUEST", // 類型,REQUEST/MYSQL url: "", // 默認url,當環境url為null時使用devUrl: "http://baidu.com", // testUrl: "http://baidu.com", // preUrl: "http://baidu.com", // prodUrl: "http://baidu.com", // method: "GET", // GET/POST, default GETtimeout: 3000, // 超時時間 單位毫秒,允許1-10000秒之間的值,不填或小于1毫秒取默認值3秒,大于10秒取10秒condition: { type: "", // groovysource: "return \"ABC\".equals(variables.get(\"param1\")) && variables.get(\"param2\") >= 10;" // 腳本執行結果返回TRUE執行該接口調用,FALSE不執行},fallback: { mode: "stop|continue", // 當請求失敗時是否繼續執行defaultResult: "" // 當mode=continue時,可設置默認的響應報文(json string)},dataMapping: { // 數據轉換規則request:{ fixedBody: { },fixedHeaders: { },fixedParams: { },headers: { //默認為源數據類型,如果要轉換類型則以目標類型+空格開頭,如:"int ""abc": "step1.requests.request1.headers.xyz"},body:{ "*": "input.request.body.*", // * 用于透傳一個json對象"inn.innId": "int step1.requests.request1.response.id" // 默認為源數據類型,如果要轉換類型則以目標類型+空格開頭,如:"int "},params:{ //默認為源數據類型,如果要轉換類型則以目標類型+空格開頭,如:"int ""userId": "input.requestBody.userId"},script: { // 腳本計算body的值type: "", // groovysource: ""}},response: { fixedBody: { },fixedHeaders: { },headers: { "abc": "step1.requests.request1.headers.xyz"},body:{ "inn.innId": "step1.requests.request1.response.id"},script: { // 腳本計算body的值//type: "", // groovysource: ""}}}}]}]}
運行的上下文格式為:
// 運行時上下文,用于保存客戶輸入和每個步驟的輸入與輸出結果var stepContext = { // 是否DEBUG模式 debug:false,// elapsed time elapsedTimes: [{ [actionName]: 123, // 操作名稱:耗時}],// input datainput: { request:{ path: "",method: "GET/POST",headers: { },body: { },params: { }},response: { // 聚合接口的響應headers: { },body: { }}},// step namestepName: { // step request datarequests: { request1: { request:{ url: "",method: "GET/POST",headers: { },body: { }},response: { headers: { },body: { }}},request2: { request:{ url: "",method: "GET/POST",headers: { },body: { }},response: { headers: { },body: { }}}//...},// step result result: { }}}
當我把Input從僅僅看成一個輸入以及輸出,加上數據處理的中間過程,那么,它就具備了很大的擴展可能性。比如,在代碼中,我們甚至可以編寫一個MysqlInput的類,其擴展Input
public class MySQLInput extends Input { }
其僅僅需要定義Input的少量類方法,就能支持MySQL的輸入,甚至與動態解析MySQL腳本,并且做數據解析變換。
public class Input { protected String name; protected InputConfig config; protected InputContext inputContext; protected StepResponse lastStepResponse = null; protected StepResponse stepResponse; public void setConfig(InputConfig inputConfig) { config = inputConfig; } public InputConfig getConfig() { return config; } public void beforeRun(InputContext context) { this.inputContext = context; } public String getName() { if (name == null) { return name = "input" + (int)(Math.random()*100); } return name; } /** * 檢查該Input是否需要運行,默認都運行 * @stepContext Step上下文 * @return TRUE:運行 */ public boolean needRun(StepContext<String, Object> stepContext) { return Boolean.TRUE; } public Mono<Map> run() { return null; } public void setName(String configName) { this.name = configName; } public StepResponse getStepResponse() { return stepResponse; } public void setStepResponse(StepResponse stepResponse) { this.stepResponse = stepResponse; } }
而擴展編碼的內容并不會涉及異步處理問題。這樣,Fizz已經較為友好地處理了異步邏輯。
## Fizz的服務編排
可視化的后臺可以進行Fizz的服務編排功能,雖然以上的核心代碼并不是很復雜,但是其已經足夠將我們整個步驟抽象化。現在,可視化的界面通過fizz-manager只需要生成對應的配置文件,并且讓其可以快速地更新加載即可。通過定義的Request Input中的請求頭、請求體和Query參數,以及校驗規則或者自定義腳本實現復雜的邏輯校驗,在定義其Fallback,我們實現了一個Request Input,通過一些的Step組裝,最終一個經過線上編排的服務就能實時投入使用。如果是只讀接口,甚至我們建議直接在線實時測試,當然支持測試接口和正式接口隔離,支持返回上下文,可以查看整個執行過程中各個步驟和請求的輸入與輸出。
## Fizz的腳本驗證
當內置的腳本驗證方式不足夠覆蓋場景時,Fizz還提供更靈活的腳本編程。
// javascript腳本函數名不能修改function dyFunc(paramsJsonStr) { // 上下文, 數據結構請參考 context.js var context = JSON.parse(paramsJsonStr)['context']; // common為內置的上下文便捷操作工具類,詳情請參考common.js;例如: // var data = common.getStepRespBody(context, 'step2', 'request1', 'data'); // do something // 自定義返回結果,如果返回的Object里含有_stopAndResponse=true字段時將會終止請求并把腳本結果響應給客戶端(主要用于有異常情況要終止請求的場景) var result = { // _stopAndResponse: true,msgCode: '0',message: '',data: null }; // 返回結果為Array或Object時要先轉為json字符串 return JSON.stringify(result);}
## Fizz的數據處理
Fizz具備對請求的輸入和輸出進行數據變換的能力,它充分利用了json path的特性通過加載配置文件的定義對Input的輸入以及輸出進行變化以便得到合理結果。
## Fizz的強大路由
Fizz的動態路由功能也設計得較為實用。它有一套平滑替換網關的方案。在最初,Fizz是可以跟其他網關并存的,比如之前提到的基于Vert.x的網關。所以,Fizz就有一個類似Nginx的反向代理方案,純粹基于路由的實現。于是,在項目初期,通過Nginx的流量被原原本本的轉發到Fizz,然后再到Vert.x,其代理了Vert.x全部流量。之后,流量被逐步轉發到后端的微服務,Vert.x上有一部分特別定制的公用代碼被下沉到底層微服務端,Vert.x還有中間層服務被完全廢棄,服務器的數量減少50%。在我們做完調整后,原先困擾我的中間層人員以及服務器的問題終于得到解決,我們可以縮減每個同事手中的那一串服務列表清單,將工作落到更有價值的項目上去。當這一切變得清晰時,這個項目也就自然而然顯示了它的價值。
針對渠道,這里的路由功能也有非常實用的功能。因為Fizz服務組概念的存在,讓它能針對不同渠道設置不同的組,從而解決渠道差別的問題。實際上,線上可以存在多組不同版本的API,也同時變相的解決API版本管理的問題。
## Fizz的可擴展鑒權
Fizz針對授權也有特別的解決方案。我們公司組建比較早,團隊里有多年編寫的老舊代碼,所以在代碼上也會有多種鑒權方式。同時,另外也有外部平臺支持方面的問題,比如在App和在微信上的代碼,就需要使用不同的鑒權支持。
上圖顯示的是通過的配置方式的驗簽配置。實際上,Fizz提供了兩種方式:一種公用的內置驗簽,一種是自定義插件驗簽。用戶使用時通過下拉菜單就能進行方便選擇。
## Fizz的插件化設計
在Fizz設計初期,我們就充分考慮到插件的重要性,因此設計了方便實現的插件標準。當然,這個需要開發者會對異步編程有很深的了解,這個特性適合有定制需求的團隊。插件僅僅需要繼承PluginFilter即可,并且只有兩個函數需要被實現:
public abstract class PluginFilter { private static final Logger log = LoggerFactory.getLogger(PluginFilter.class);public Mono<Void> filter(ServerWebExchange exchange, Map<String, Object> config, String fixedConfig) { return Mono.empty();}public abstract Mono<Void> doFilter(ServerWebExchange exchange, Map<String, Object> config, String fixedConfig);}
## Fizz的管理功能
中大型企業的資源保護也是相當重要。一旦所有的流量通過Fizz,便需要在Fizz建立對應的路由功能,而對應的API審核制度也是其一大特點,所有公司API接口的資源都被方便的保護起來,有嚴格的審核機制保證每個API都是經過團隊的管理人員審核。并且,它具備API快速下線功能以及降級響應功能。
## Fizz的其他功能
當然,Fizz適配Spring的全家桶,使用配置中心Apollo,能夠進行均衡負載,訪問日志、黑白名單等一系列我們認為該有的網關功能。
# Fizz的性能問題
雖然不以性能作為賣點,但是這并不代表著Fizz的性能就很差。得益與WebFlux的加成,我們將Fizz與官方spring-cloud-gateway進行比較,使用相同的環境和條件,測試對象均為單個節點。測試結果,我們的QPS比spring-cloud-gateway略高。當然,我們還有想當的想象空間可以優化。
Intel? Xeon? CPU X5675 @ 3.07GHz
Linux version 3.10.0-327.el7.x86_64
Intel? Xeon? CPU X5675 @ 3.07GHz
Linux version 3.10.0-327.el7.x86_64
| 條件 | QPS(/s) | 90% Latency(ms) |
| — | — | — |
| 直接訪問后端 | 9087.46 | 10.76 |
| fizz-gateway | 5927.13 | 19.86 |
| spring-cloud-gateway | 5044.04 | 22.91 |
在設計Fizz之初,我們就考慮到企業內部復雜的中間層情況:它可以截流所有的流量,能并行且逐步替換現有網關。所以在內部推行時,Fizz很順利。最初研發時,我們選取了C端業務作為目標業務,發布上線時僅替換其中部分復雜的場景,經過一個季度的試用,我們解決了性能和內存等各種問題。在版本穩定后,Fizz被推廣到整個BU的業務線替代原先繁多的應用網關,緊接著是整個公司的適用的業務都開始使用。原來我們C端、B端兩個中間層團隊研發能夠騰出手來從事底層業務的研發,中間層人員雖然減少了,但是研發效率卻有很大提升,比如原先需要多天開發的一組復制型服務研發時間縮短為之前的七分之一。借助Fizz,我們開展進行服務合并工作,中間層的服務器減少50%,而服務的承載能力卻是上升的。
# Fizz的交流發展
前期,Fizz僅依靠配置就開始規模化的使用,但隨著使用人數的增加,配置文件編寫和管理需要讓我們開始擴展這個項目。現在,Fizz包含兩個主要的后端項目fizz-gateway、 fizz-manager。fizz-admin是作為Fizz的前端配置界面,fizz-manager與fizz-admin為Fizz提供圖形化的配置界面。所有的Pipe都能夠在操作界面進行編寫以及上線。
為了能讓更多的中大型快速發展的團隊能夠應用上這個面向管理,解決實際問題的網關,Fizz提供了fizz-gateway-community社區版本的解決方案,而且作為對外技術的交流,其技術的核心實現將會以GNU v3授權方式進行的開放。fizz-gateway-community的所有API將會公布以便二次開發使用。因為fizz-gateway-professional專業版本與團隊業務綁定,所以進行商業封閉。而對應的管理平臺代碼fizz-manger-professional作為商業版本開放二進制包的免費下載,提供給使用了GNU v3開源協議的項目免費使用(如果您的項目是商業性質,請聯系我們進行授權)。另外,Fizz已有的豐富插件我們也會選擇合適的時機與各位交流。
無論我們的項目交流是否能幫到各位,我們真誠希望能得到各位的反饋。不管項目技術是否牛逼,完善與否,我們始終不忘初心:Fizz,一個面向大中型企業的管理型網關。
“性能超高的API網關之怎么使用Fizz Gateway”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。