本篇內(nèi)容介紹了“性能超高的API網(wǎng)關(guān)之怎么使用Fizz Gateway”的有關(guān)知識,在實(shí)際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!
成都創(chuàng)新互聯(lián)公司,專注為中小企業(yè)提供官網(wǎng)建設(shè)、營銷型網(wǎng)站制作、成都響應(yīng)式網(wǎng)站建設(shè)公司、展示型成都網(wǎng)站建設(shè)、網(wǎng)站建設(shè)等服務(wù),幫助中小企業(yè)通過網(wǎng)站體現(xiàn)價(jià)值、有效益。幫助企業(yè)快速建站、解決網(wǎng)站建設(shè)與網(wǎng)站營銷推廣問題。
中間層在Web網(wǎng)站上的部署偏前,一般部署于防火墻及Nginx之后,更多面向C端用戶服務(wù),所以在性能并發(fā)量上有較高的要求,大部分團(tuán)隊(duì)在選型上會選擇異步框架。正因?yàn)槠渲苯用嫦駽端,變化較多,大部分需要經(jīng)常性地變更或者配置的代碼都會安排在這一層次,發(fā)布非常頻繁。此外,很多團(tuán)隊(duì)使用編譯型語言進(jìn)行編碼,而非解釋型語言。這三個(gè)因素組合在一起,使得開發(fā)者調(diào)試與開發(fā)非常痛苦。比如,我們曾經(jīng)選擇Play2框架,這是一個(gè)異步Java框架,需要開發(fā)者能夠流暢地編寫異步,但是熟悉調(diào)試技巧的同事也不多。在代碼里面配置了各種請求參數(shù),以及結(jié)果處理,看似非常簡單,但是聯(lián)調(diào)、單元測試、或者配置文件修改之后等待Java編譯花費(fèi)的時(shí)間和精力是巨大的。如果異步編碼規(guī)范也有問題,這對開發(fā)者來說無疑是一種折磨。
public F.Promise>> getGoodsByCondi(final StringBuilder searchParams, final GoodsQueryParam param) { final Map params = new TreeMap (); 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 promise = HttpInvoker.get(url, params); final GoodListBaseDto retVal = new GoodListBaseDto(); Promise >> goodListPromise = promise.map(new Function >>() { @Override public BaseDto > 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
>(CommonError.SYS_ERROR); } JsonNode result = json.get("items"); Iterator
iterator = result.elements(); final List goods = new ArrayList (); 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 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 > pmsPromise = service.getActiveTipsByBrand(param.getBrandId()); return goodListPromise.flatMap(new Function
>, Promise >>>() { @Override public Promise >> apply(BaseDto > listBaseDto) throws Throwable { return pmsPromise.flatMap(new Function
, Promise
>>>() { @Override public Promise >> apply(List activeTips) throws Throwable { retVal.setPmsList(activeTips); BaseDto > baseDto = (BaseDto
>)retVal; return Promise.pure(baseDto); } }); } }); } return goodListPromise; }
上述代碼只是摘抄了其中一個(gè)過程函數(shù)。如果我們將中間層的場景設(shè)置得更為復(fù)雜一些,我們要解決的就不僅僅是編碼性能、編碼質(zhì)量、編碼時(shí)間的問題。
## “復(fù)雜”場景問題
微服務(wù)顆粒度較細(xì),為了實(shí)現(xiàn)簡潔的前端邏輯以及較少的服務(wù)調(diào)用次數(shù),我們針對C端的大部分輸出是聚合的結(jié)果。比如,我們一個(gè)搜索的中間層邏輯,其服務(wù)是這樣一個(gè)過程:
獲取會員信息、會員卡列表、會員積分余額,因?yàn)椴煌墑e的會員會有不同價(jià)格;
獲取用戶的優(yōu)惠券信息,這部分會對計(jì)算出來的價(jià)格產(chǎn)生影響;
獲取搜索的結(jié)果信息,結(jié)果來自三部分,商旅商品的庫存價(jià)格,猜你喜歡的庫存價(jià)格,推薦位的庫存價(jià)格,海外商品的庫存價(jià)格。
這其中涉及到的服務(wù)有:中間層服務(wù)(聚合服務(wù))、會員服務(wù)、優(yōu)惠券服務(wù)、推薦服務(wù)、企業(yè)服務(wù)、海外搜索服務(wù)、搜索服務(wù)。此外,還有各種類型的緩存設(shè)施以及數(shù)據(jù)庫的配置服務(wù)。
public ListsearchProduct(String traceId, ExtenalProductQueryParam param, MemberAssetVO memberAssetVO, ProductInfoResultVO resultVO,boolean needAddPrice) { // 用戶可用優(yōu)惠券的configId String configIds = memberAssetVO == null ? null : memberAssetVO.getConfigIds(); // 特殊項(xiàng)目,限制不能使用優(yōu)惠券功能 if(customProperties.getIgnoreChannel().contains(param.getChannelCode())) { configIds = null; } final String configIdConstant = configIds; // 主搜索列表信息 Mono > innInfos = this.search(traceId, param, configIds, resultVO); return innInfos.flatMap(inns -> { // 商旅產(chǎn)品推薦 Mono
busiProduct = this.recommendProductService.getBusiProduct(traceId, param, configIdConstant); // 會員產(chǎn)品推薦(猜您喜歡) Mono guessPref = this.recommendProductService.getGuessPref(traceId, param, configIdConstant); // 業(yè)務(wù)相關(guān)查詢 String registChainId = memberAssetVO == null || memberAssetVO.getMember() == null ? null : memberAssetVO.getMember().getRegistChainId(); Mono registChain = this.recommendProductService.registChain(traceId, param, configIdConstant, registChainId); // 店長熱推產(chǎn)品 Mono advert = this.recommendProductService.advert(traceId, param, configIdConstant); return Mono.zip(busiProduct, guessPref, registChain, advert).flatMap(product -> { // 推薦位(廣告位)包裝 List products = recommendProductService.setRecommend(inns, product.getT1(), product.getT2(), product.getT3(), product.getT4(), param); // 設(shè)置其他參數(shù) return this.setOtherParam(traceId, param, products, memberAssetVO); }); }).block(); }
這個(gè)服務(wù)的Service層會經(jīng)常性地根據(jù)產(chǎn)品需求和底層微服務(wù)接口的變更做出調(diào)整改變,而研發(fā)的接口調(diào)用時(shí)序圖卻因?yàn)閳F(tuán)隊(duì)的這些更改對應(yīng)不上代碼。
除了上述問題外,該服務(wù)中的多個(gè)微服務(wù)異步調(diào)用聚合的編碼問題也未能被妥善處理,因?yàn)槠涫褂玫腟pring-MVC框架編碼風(fēng)格是同步的,而Service層卻使用了異步的Mono,只能不合時(shí)宜地用block。這些代碼更改、文檔缺失、編碼質(zhì)量共同組成了中間層的代碼管理問題。
## 野蠻發(fā)展問題
我參與過一個(gè)初創(chuàng)技術(shù)團(tuán)隊(duì)建設(shè)。最開始,因?yàn)榭焖匍_發(fā)的需要,我們傾向于做一個(gè)胖服務(wù),但當(dāng)團(tuán)隊(duì)規(guī)模開始擴(kuò)大時(shí),我們卻需要逐步地將胖服務(wù)分拆為微服務(wù),開始產(chǎn)生中間層團(tuán)隊(duì),他們的主要目的是應(yīng)用于底層服務(wù)的聚合。
但是,有一段時(shí)間,我們的招聘速度并不能完全趕上服務(wù)數(shù)量的增長速度,于是寫底層的同事就需要不斷地切換編碼思路。因?yàn)槌艘帉懛植鹬蟮牡讓游⒎?wù),還要編寫聚合的中間層服務(wù)。
當(dāng)我停掉某一些項(xiàng)目時(shí),開始整頓人手,我又發(fā)現(xiàn)一個(gè)殘酷事實(shí):每個(gè)人手上都有數(shù)十個(gè)中間層服務(wù),因此無法換掉任何一個(gè)人。因?yàn)榻?jīng)過多次地?fù)Q手,同事們已經(jīng)搞不清中間服務(wù)的聯(lián)系。
另外,還有各種授權(quán)方式,因?yàn)閳F(tuán)隊(duì)一直以來的野蠻成長,各種授權(quán)方式都混在一起,既有簡單的,又有復(fù)雜的,既有合理的,還有不合理的。總之,團(tuán)隊(duì)沒有人能搞清楚。
經(jīng)過一段時(shí)間的發(fā)展后,通過整理線上服務(wù),我們發(fā)現(xiàn)很多資源浪費(fèi),比如有時(shí)候,僅僅一個(gè)接口就使用了一個(gè)微服務(wù)。在早起,這些微服務(wù)是有較大規(guī)模請求的,但是后來,項(xiàng)目被遺棄,也沒有了流量,但是運(yùn)行的接口依然在線上。而作為團(tuán)隊(duì)管理人員的我甚至沒有任何書面上接口匯總的統(tǒng)計(jì)信息。
當(dāng)老板告訴我,把合作公司對接的服務(wù)暫停時(shí),我無法做到邏輯上停機(jī)返回一個(gè)業(yè)務(wù)異常。作為一個(gè)多渠道發(fā)展的上游庫存供應(yīng)商,我們對接的渠道很多,提供給客戶的接口有很多特別定制的需求,這些需求一般就在中間的邏輯控制代碼里面,渠道下線了,也不會做任何調(diào)整,因?yàn)殚_發(fā)者需要根據(jù)需求來進(jìn)行代碼更新。
而且,中間層團(tuán)隊(duì)對外聯(lián)合調(diào)試也是長久以來存在的一個(gè)問題。經(jīng)常有前端同事向我抱怨,后端的同事不肯增加數(shù)據(jù)處理邏輯的代碼,而作為前端,他們不得不增加很多轉(zhuǎn)換數(shù)據(jù)的代碼來適配界面的邏輯。而像在小程序這種的對包大小進(jìn)行限制的環(huán)境里,這些代碼的移動(dòng)在發(fā)展后期就成為一個(gè)老大難問題。
# 網(wǎng)關(guān)的選型失敗
當(dāng)時(shí),市面上存在兩種類型的解決方案:
中間層的解決方案。中間層方案一般提供裸異步服務(wù)、其他插件以及功能根據(jù)需求自定義,部分中間層的服務(wù)經(jīng)過改造后也具備網(wǎng)關(guān)的部分功能。
網(wǎng)關(guān)的解決方案。網(wǎng)關(guān)方案一般圍繞著微服務(wù)全家桶提供,或者自成一派,提供通用型的功能(如路由功能)。當(dāng)然,部分網(wǎng)關(guān)經(jīng)過自定義改造也能加入中間層的業(yè)務(wù)功能。
我們的業(yè)務(wù)發(fā)展變化非常快。如果市面上已有的網(wǎng)關(guān)方案能滿足需求,我們又有能力進(jìn)行二次開發(fā),我們非常樂意使用。
當(dāng)時(shí),Eolinker是我們的API 自動(dòng)測試的供應(yīng)商,提供了對應(yīng)的管理型網(wǎng)關(guān),但語言是Go。而我們團(tuán)隊(duì)的技術(shù)棧主要以Java為主,運(yùn)維的部署方案也一直圍繞著Java,這意味我們的選型就偏窄,因此不得不放棄這一想法。
在之前,我們也選擇過Kong網(wǎng)關(guān),但是引入一個(gè)新的復(fù)雜技術(shù)棧是一件成本不低的事情,比如,Lua的招聘與二次開發(fā)是難以避免的痛。
另外,Gravitee、Zuul、Vert.x 都是不同小規(guī)模團(tuán)隊(duì)使用過的網(wǎng)關(guān)。談及最多的特性是:
1、支持熔斷、流量控制和過載保護(hù)
2、支持特別高的并發(fā)
3、秒殺
然而,對商業(yè)而言,熔斷、流量控制和過載保護(hù)應(yīng)該是最后考慮的措施。而且,對一個(gè)成長中的團(tuán)隊(duì)來說,服務(wù)的過載崩潰是需要經(jīng)歷較長時(shí)間的業(yè)務(wù)沉淀。
另外,秒殺業(yè)務(wù)的流量更多是維持一個(gè)普通水平,其偶爾的高并發(fā)也是在我們團(tuán)隊(duì)處理能力范圍之內(nèi)。換句話說,選型時(shí),更多的是需要結(jié)合實(shí)際,而不是考慮類似阿里巴巴的流量,我只需考慮中等水平以上并且具備集群擴(kuò)展性的方式即可。
此前,我們團(tuán)隊(duì)使用比較廣的網(wǎng)關(guān)是Vert.x,編碼風(fēng)格是這樣的,華麗酷炫。
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()) { ListrecordList = 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 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社區(qū)缺乏支持以及入門成本高的問題一直存在,而團(tuán)隊(duì)甚至找不到更多合適的同事來維護(hù)代碼。
以上網(wǎng)關(guān)的選型失敗讓我們意識到,市面沒有完全符合我們公司的情況的“瑞士軍刀”,由此我們開始走上了自研之路,開始進(jìn)行Fizz網(wǎng)關(guān)的設(shè)計(jì)。
# 走上自研網(wǎng)關(guān)之路
我們需要網(wǎng)關(guān)么?網(wǎng)關(guān)層解決什么問題?這兩個(gè)問題不言而喻。我們需要網(wǎng)關(guān),因?yàn)樗梢詭臀覀兘鉀Q負(fù)載均衡、聚合、授權(quán)、監(jiān)控、限流、日志、權(quán)限控制等一系列的問題。同時(shí),我們也需要中間層,細(xì)化服務(wù)顆粒度的微服務(wù)讓我們不得不通過中間層聚合它們。
而我們不需要的是復(fù)雜的編碼、冗余的膠水代碼,以及冗長的發(fā)布流程。
為解決這些問題,我們需要讓網(wǎng)關(guān)與中間層模糊界限,抹去網(wǎng)關(guān)和中間層隔閡,讓網(wǎng)關(guān)支持中間層動(dòng)態(tài)編碼,盡可能少的發(fā)布部署。為實(shí)現(xiàn)這個(gè)目的,只需要用一個(gè)簡潔的網(wǎng)關(guān)模型并同時(shí)利用low-code特性盡可能地去覆蓋中間層的功能即可。
## 從原點(diǎn)出發(fā)的需求
在復(fù)盤當(dāng)初這個(gè)選擇時(shí),我需要再強(qiáng)調(diào)下從原點(diǎn)出發(fā)的需求:
1、Java技術(shù)棧,支持Spring全家桶;
2、方便易用,零培訓(xùn)也能編排;
3、動(dòng)態(tài)路由能力,隨時(shí)隨地能夠開啟新API;
4、高性能且集群可橫向擴(kuò)展;
5、強(qiáng)熱服務(wù)編排能力,支持前后端編碼,隨時(shí)隨地更新API;
6、線上編碼邏輯支持;
7、可擴(kuò)展的安全認(rèn)證能力,方便日志記錄;
API審核功能,把控所有服務(wù);
可擴(kuò)展性,強(qiáng)大的插件開發(fā)機(jī)制;
## Fizz 的技術(shù)選型
在選型Spring WebFlux后,因?yàn)槠鋯误w較強(qiáng)的特性,同事建議命名為Fizz(Fizz是競技游戲《英雄聯(lián)盟》中的英雄角色之一,它是一個(gè)近戰(zhàn)法師,其擁有AP中數(shù)一數(shù)二的單體爆發(fā),因此可以克制大部分法師,可以作為一個(gè)很好地反制英雄使用)。
WebFlux是一個(gè)典型非阻塞異步的框架,它的核心是基于Reactor的相關(guān)API實(shí)現(xiàn)的。 相對于傳統(tǒng)的web框架來說,它可以運(yùn)行在諸如Netty、Undertow和支持Servlet3.1的容器上,因此它運(yùn)行環(huán)境的可選擇性要比傳統(tǒng)web框架多很多。
而Spring WebFlux 是一個(gè)異步非阻塞式的 Web 框架,它能夠充分利用多核 CPU 的硬件資源去處理大量的并發(fā)請求。其依賴Spring的技術(shù)棧,代碼風(fēng)格是這樣的:
public MonogetAll(ServerRequest serverRequest) { printlnThread("獲取所有用戶"); Flux userFlux = Flux.fromStream(userRepository.getUsers().entrySet().stream().map(Map.Entry::getValue)); return ServerResponse.ok() .body(userFlux, User.class); }
## Fizz的核心實(shí)現(xiàn)
對我們而言,這是一個(gè)從零開始的項(xiàng)目,很多同事剛開始沒有信心。我為這個(gè)服務(wù)寫了第一個(gè)服務(wù)編排代碼的核心包fizz,并把這個(gè)commit寫為“開工大吉”。
我打算所有的服務(wù)聚合的定義就靠一個(gè)配置文件解決。那么,就有這樣的模型:如果把用戶請求作為輸入,那么響應(yīng)自然就是輸出,這就是一個(gè)管道Pipe;在一個(gè)Pipe中,會有不同的Step,對應(yīng)不同的串聯(lián)的步驟;而在一個(gè)Step,至少有一個(gè)存在著一個(gè)Input接收上一個(gè)步驟處理的輸出,所有的Input都是并聯(lián)的,并且可以并行執(zhí)行;貫穿于Pipe的生命周期中存在唯一的Context保存中間上下文。
而在每個(gè)Input的輸入與輸出,我增加了動(dòng)態(tài)腳本的擴(kuò)展能力,到現(xiàn)在已經(jīng)支持JavaScript和groove兩種能力,支持JavaScript的前端邏輯可以在后端得到必要擴(kuò)展。而我們的配置文件僅僅需要這樣一個(gè)腳本:
// 聚合接口配置var aggrAPIConfig = { name: "input name", // 自定義的聚合接口名 debug: false, // 是否為調(diào)試模式,默認(rèn)falsetype: "REQUEST", // 類型,REQUEST/MySQLmethod: "GET/POST",path: "/proxy/aggr-hotel/hotel/rates", // 格式:/aggr/+服務(wù)名+路徑, 分組名以aggr-開頭,表示聚合接口langDef: { // 可選,提示語言定義,入?yún)Ⅱ?yàn)證失敗時(shí)依據(jù)配置提供不同語言的提示信息,目前支持中文、英文langParam: "input.request.body.languageCode", // 入?yún)⒄Z言字段langMapping: { // 字段值與語言的映射關(guān)系zh: "0", // 中文en: "1" // 英文}},headersDef: { // 可選,定義聚合接口header部分參數(shù),使用JSON Schema規(guī)范(詳見:http://json-schema.org/specification.html),用于參數(shù)驗(yàn)證,接口文檔生成type:"object",properties:{ appId:{ type:"string",title:"應(yīng)用ID",description:"描述"}},required: ["appId"]},paramsDef: { // 可選,定義聚合接口parameter部分參數(shù),使用JSON Schema規(guī)范(詳見:http://json-schema.org/specification.html),用于參數(shù)驗(yàn)證,接口文檔生成type:"object",properties:{ lang:{ type:"string",title:"語言",description:"描述"}}},bodyDef: { // 可選,定義聚合接口body部分參數(shù),使用JSON Schema規(guī)范(詳見:http://json-schema.org/specification.html),用于參數(shù)驗(yàn)證,接口文檔生成type:"object",properties:{ userId:{ type:"string",title:"用戶名",description:"描述"}},required: ["userId"]},scriptValidate: { // 可選,用于headersDef、paramsDef、bodyDef無法覆蓋的入?yún)Ⅱ?yàn)證場景type: "", // groovysource: "" // 腳本返回List對象,null:驗(yàn)證通過,List:錯(cuò)誤信息列表},validateResponse:{ // 入?yún)Ⅱ?yàn)證失敗響應(yīng),處理方式同dataMapping.responsefixedBody: { // 固定的body"code": -411},fixedHeaders: { // 固定header"a":"b"},headers: { // 引用的header},body: { // 引用的header"msg": "validateMsg"},script: { type: "", // groovysource: ""}},dataMapping: { // 聚合接口數(shù)據(jù)轉(zhuǎn)換規(guī)則response:{ fixedBody: { // 固定的body"code":"b"},fixedHeaders: { // 固定header"a":"b"}, headers: { // 引用的header,默認(rèn)為源數(shù)據(jù)類型,如果要轉(zhuǎn)換類型則以目標(biāo)類型+空格開頭,如:"int ""abc": "int step1.requests.request1.headers.xyz"},body: { // 引用的header,默認(rèn)為源數(shù)據(jù)類型,如果要轉(zhuǎn)換類型則以目標(biāo)類型+空格開頭,如:"int ""abc": "int step1.requests.request1.response.id","inn.innName": "step1.requests.request2.response.hotelName","ddd": { // 腳本, 當(dāng)腳本的返回對象里包含有_stopAndResponse字段且值為true時(shí),會終請求并把腳本的返回結(jié)果響應(yīng)給瀏覽器"type": "groovy","source": ""}},script: { // 腳本計(jì)算body的值type: "", // groovysource: ""}}},stepConfigs: [{ // step的配置name: "step1", // 步驟名稱stop: false, // 是否在執(zhí)行完當(dāng)前step就返回dataMapping: { // step response數(shù)據(jù)轉(zhuǎn)換規(guī)則response: { fixedBody: { // 固定的body"a":"b"},body: { // step result"abc": "step1.requests.request1.response.id","inn.innName": "step1.requests.request2.response.hotelName"},script: { // 腳本計(jì)算body的值type: "", // groovysource: ""}}}, requests:[ //每個(gè)step可以調(diào)用多個(gè)接口{ // 自定義的接口名 name: "request1", // 接口名,格式request+N type: "REQUEST", // 類型,REQUEST/MYSQL url: "", // 默認(rèn)url,當(dāng)環(huán)境url為null時(shí)使用devUrl: "http://baidu.com", // testUrl: "http://baidu.com", // preUrl: "http://baidu.com", // prodUrl: "http://baidu.com", // method: "GET", // GET/POST, default GETtimeout: 3000, // 超時(shí)時(shí)間 單位毫秒,允許1-10000秒之間的值,不填或小于1毫秒取默認(rèn)值3秒,大于10秒取10秒condition: { type: "", // groovysource: "return \"ABC\".equals(variables.get(\"param1\")) && variables.get(\"param2\") >= 10;" // 腳本執(zhí)行結(jié)果返回TRUE執(zhí)行該接口調(diào)用,F(xiàn)ALSE不執(zhí)行},fallback: { mode: "stop|continue", // 當(dāng)請求失敗時(shí)是否繼續(xù)執(zhí)行defaultResult: "" // 當(dāng)mode=continue時(shí),可設(shè)置默認(rèn)的響應(yīng)報(bào)文(json string)},dataMapping: { // 數(shù)據(jù)轉(zhuǎn)換規(guī)則request:{ fixedBody: { },fixedHeaders: { },fixedParams: { },headers: { //默認(rèn)為源數(shù)據(jù)類型,如果要轉(zhuǎn)換類型則以目標(biāo)類型+空格開頭,如:"int ""abc": "step1.requests.request1.headers.xyz"},body:{ "*": "input.request.body.*", // * 用于透傳一個(gè)json對象"inn.innId": "int step1.requests.request1.response.id" // 默認(rèn)為源數(shù)據(jù)類型,如果要轉(zhuǎn)換類型則以目標(biāo)類型+空格開頭,如:"int "},params:{ //默認(rèn)為源數(shù)據(jù)類型,如果要轉(zhuǎn)換類型則以目標(biāo)類型+空格開頭,如:"int ""userId": "input.requestBody.userId"},script: { // 腳本計(jì)算body的值type: "", // groovysource: ""}},response: { fixedBody: { },fixedHeaders: { },headers: { "abc": "step1.requests.request1.headers.xyz"},body:{ "inn.innId": "step1.requests.request1.response.id"},script: { // 腳本計(jì)算body的值//type: "", // groovysource: ""}}}}]}]}
運(yùn)行的上下文格式為:
// 運(yùn)行時(shí)上下文,用于保存客戶輸入和每個(gè)步驟的輸入與輸出結(jié)果var stepContext = { // 是否DEBUG模式 debug:false,// elapsed time elapsedTimes: [{ [actionName]: 123, // 操作名稱:耗時(shí)}],// input datainput: { request:{ path: "",method: "GET/POST",headers: { },body: { },params: { }},response: { // 聚合接口的響應(yīng)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: { }}}
當(dāng)我把Input從僅僅看成一個(gè)輸入以及輸出,加上數(shù)據(jù)處理的中間過程,那么,它就具備了很大的擴(kuò)展可能性。比如,在代碼中,我們甚至可以編寫一個(gè)MysqlInput的類,其擴(kuò)展Input
public class MySQLInput extends Input { }
其僅僅需要定義Input的少量類方法,就能支持MySQL的輸入,甚至與動(dòng)態(tài)解析MySQL腳本,并且做數(shù)據(jù)解析變換。
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是否需要運(yùn)行,默認(rèn)都運(yùn)行 * @stepContext Step上下文 * @return TRUE:運(yùn)行 */ public boolean needRun(StepContextstepContext) { return Boolean.TRUE; } public Mono
而擴(kuò)展編碼的內(nèi)容并不會涉及異步處理問題。這樣,F(xiàn)izz已經(jīng)較為友好地處理了異步邏輯。
## Fizz的服務(wù)編排
可視化的后臺可以進(jìn)行Fizz的服務(wù)編排功能,雖然以上的核心代碼并不是很復(fù)雜,但是其已經(jīng)足夠?qū)⑽覀冋麄€(gè)步驟抽象化?,F(xiàn)在,可視化的界面通過fizz-manager只需要生成對應(yīng)的配置文件,并且讓其可以快速地更新加載即可。通過定義的Request Input中的請求頭、請求體和Query參數(shù),以及校驗(yàn)規(guī)則或者自定義腳本實(shí)現(xiàn)復(fù)雜的邏輯校驗(yàn),在定義其Fallback,我們實(shí)現(xiàn)了一個(gè)Request Input,通過一些的Step組裝,最終一個(gè)經(jīng)過線上編排的服務(wù)就能實(shí)時(shí)投入使用。如果是只讀接口,甚至我們建議直接在線實(shí)時(shí)測試,當(dāng)然支持測試接口和正式接口隔離,支持返回上下文,可以查看整個(gè)執(zhí)行過程中各個(gè)步驟和請求的輸入與輸出。
## Fizz的腳本驗(yàn)證
當(dāng)內(nèi)置的腳本驗(yàn)證方式不足夠覆蓋場景時(shí),F(xiàn)izz還提供更靈活的腳本編程。
// javascript腳本函數(shù)名不能修改function dyFunc(paramsJsonStr) { // 上下文, 數(shù)據(jù)結(jié)構(gòu)請參考 context.js var context = JSON.parse(paramsJsonStr)['context']; // common為內(nèi)置的上下文便捷操作工具類,詳情請參考common.js;例如: // var data = common.getStepRespBody(context, 'step2', 'request1', 'data'); // do something // 自定義返回結(jié)果,如果返回的Object里含有_stopAndResponse=true字段時(shí)將會終止請求并把腳本結(jié)果響應(yīng)給客戶端(主要用于有異常情況要終止請求的場景) var result = { // _stopAndResponse: true,msgCode: '0',message: '',data: null }; // 返回結(jié)果為Array或Object時(shí)要先轉(zhuǎn)為json字符串 return JSON.stringify(result);}
## Fizz的數(shù)據(jù)處理
Fizz具備對請求的輸入和輸出進(jìn)行數(shù)據(jù)變換的能力,它充分利用了json path的特性通過加載配置文件的定義對Input的輸入以及輸出進(jìn)行變化以便得到合理結(jié)果。
## Fizz的強(qiáng)大路由
Fizz的動(dòng)態(tài)路由功能也設(shè)計(jì)得較為實(shí)用。它有一套平滑替換網(wǎng)關(guān)的方案。在最初,F(xiàn)izz是可以跟其他網(wǎng)關(guān)并存的,比如之前提到的基于Vert.x的網(wǎng)關(guān)。所以,F(xiàn)izz就有一個(gè)類似Nginx的反向代理方案,純粹基于路由的實(shí)現(xiàn)。于是,在項(xiàng)目初期,通過Nginx的流量被原原本本的轉(zhuǎn)發(fā)到Fizz,然后再到Vert.x,其代理了Vert.x全部流量。之后,流量被逐步轉(zhuǎn)發(fā)到后端的微服務(wù),Vert.x上有一部分特別定制的公用代碼被下沉到底層微服務(wù)端,Vert.x還有中間層服務(wù)被完全廢棄,服務(wù)器的數(shù)量減少50%。在我們做完調(diào)整后,原先困擾我的中間層人員以及服務(wù)器的問題終于得到解決,我們可以縮減每個(gè)同事手中的那一串服務(wù)列表清單,將工作落到更有價(jià)值的項(xiàng)目上去。當(dāng)這一切變得清晰時(shí),這個(gè)項(xiàng)目也就自然而然顯示了它的價(jià)值。
針對渠道,這里的路由功能也有非常實(shí)用的功能。因?yàn)镕izz服務(wù)組概念的存在,讓它能針對不同渠道設(shè)置不同的組,從而解決渠道差別的問題。實(shí)際上,線上可以存在多組不同版本的API,也同時(shí)變相的解決API版本管理的問題。
## Fizz的可擴(kuò)展鑒權(quán)
Fizz針對授權(quán)也有特別的解決方案。我們公司組建比較早,團(tuán)隊(duì)里有多年編寫的老舊代碼,所以在代碼上也會有多種鑒權(quán)方式。同時(shí),另外也有外部平臺支持方面的問題,比如在App和在微信上的代碼,就需要使用不同的鑒權(quán)支持。
上圖顯示的是通過的配置方式的驗(yàn)簽配置。實(shí)際上,F(xiàn)izz提供了兩種方式:一種公用的內(nèi)置驗(yàn)簽,一種是自定義插件驗(yàn)簽。用戶使用時(shí)通過下拉菜單就能進(jìn)行方便選擇。
## Fizz的插件化設(shè)計(jì)
在Fizz設(shè)計(jì)初期,我們就充分考慮到插件的重要性,因此設(shè)計(jì)了方便實(shí)現(xiàn)的插件標(biāo)準(zhǔn)。當(dāng)然,這個(gè)需要開發(fā)者會對異步編程有很深的了解,這個(gè)特性適合有定制需求的團(tuán)隊(duì)。插件僅僅需要繼承PluginFilter即可,并且只有兩個(gè)函數(shù)需要被實(shí)現(xiàn):
public abstract class PluginFilter { private static final Logger log = LoggerFactory.getLogger(PluginFilter.class);public Monofilter(ServerWebExchange exchange, Map config, String fixedConfig) { return Mono.empty();}public abstract Mono doFilter(ServerWebExchange exchange, Map config, String fixedConfig);}
## Fizz的管理功能
中大型企業(yè)的資源保護(hù)也是相當(dāng)重要。一旦所有的流量通過Fizz,便需要在Fizz建立對應(yīng)的路由功能,而對應(yīng)的API審核制度也是其一大特點(diǎn),所有公司API接口的資源都被方便的保護(hù)起來,有嚴(yán)格的審核機(jī)制保證每個(gè)API都是經(jīng)過團(tuán)隊(duì)的管理人員審核。并且,它具備API快速下線功能以及降級響應(yīng)功能。
## Fizz的其他功能
當(dāng)然,F(xiàn)izz適配Spring的全家桶,使用配置中心Apollo,能夠進(jìn)行均衡負(fù)載,訪問日志、黑白名單等一系列我們認(rèn)為該有的網(wǎng)關(guān)功能。
# Fizz的性能問題
雖然不以性能作為賣點(diǎn),但是這并不代表著Fizz的性能就很差。得益與WebFlux的加成,我們將Fizz與官方spring-cloud-gateway進(jìn)行比較,使用相同的環(huán)境和條件,測試對象均為單個(gè)節(jié)點(diǎn)。測試結(jié)果,我們的QPS比spring-cloud-gateway略高。當(dāng)然,我們還有想當(dāng)?shù)南胂罂臻g可以優(yōu)化。
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 |
在設(shè)計(jì)Fizz之初,我們就考慮到企業(yè)內(nèi)部復(fù)雜的中間層情況:它可以截流所有的流量,能并行且逐步替換現(xiàn)有網(wǎng)關(guān)。所以在內(nèi)部推行時(shí),F(xiàn)izz很順利。最初研發(fā)時(shí),我們選取了C端業(yè)務(wù)作為目標(biāo)業(yè)務(wù),發(fā)布上線時(shí)僅替換其中部分復(fù)雜的場景,經(jīng)過一個(gè)季度的試用,我們解決了性能和內(nèi)存等各種問題。在版本穩(wěn)定后,F(xiàn)izz被推廣到整個(gè)BU的業(yè)務(wù)線替代原先繁多的應(yīng)用網(wǎng)關(guān),緊接著是整個(gè)公司的適用的業(yè)務(wù)都開始使用。原來我們C端、B端兩個(gè)中間層團(tuán)隊(duì)研發(fā)能夠騰出手來從事底層業(yè)務(wù)的研發(fā),中間層人員雖然減少了,但是研發(fā)效率卻有很大提升,比如原先需要多天開發(fā)的一組復(fù)制型服務(wù)研發(fā)時(shí)間縮短為之前的七分之一。借助Fizz,我們開展進(jìn)行服務(wù)合并工作,中間層的服務(wù)器減少50%,而服務(wù)的承載能力卻是上升的。
# Fizz的交流發(fā)展
前期,F(xiàn)izz僅依靠配置就開始規(guī)模化的使用,但隨著使用人數(shù)的增加,配置文件編寫和管理需要讓我們開始擴(kuò)展這個(gè)項(xiàng)目。現(xiàn)在,F(xiàn)izz包含兩個(gè)主要的后端項(xiàng)目fizz-gateway、 fizz-manager。fizz-admin是作為Fizz的前端配置界面,fizz-manager與fizz-admin為Fizz提供圖形化的配置界面。所有的Pipe都能夠在操作界面進(jìn)行編寫以及上線。
為了能讓更多的中大型快速發(fā)展的團(tuán)隊(duì)能夠應(yīng)用上這個(gè)面向管理,解決實(shí)際問題的網(wǎng)關(guān),F(xiàn)izz提供了fizz-gateway-community社區(qū)版本的解決方案,而且作為對外技術(shù)的交流,其技術(shù)的核心實(shí)現(xiàn)將會以GNU v3授權(quán)方式進(jìn)行的開放。fizz-gateway-community的所有API將會公布以便二次開發(fā)使用。因?yàn)閒izz-gateway-professional專業(yè)版本與團(tuán)隊(duì)業(yè)務(wù)綁定,所以進(jìn)行商業(yè)封閉。而對應(yīng)的管理平臺代碼fizz-manger-professional作為商業(yè)版本開放二進(jìn)制包的免費(fèi)下載,提供給使用了GNU v3開源協(xié)議的項(xiàng)目免費(fèi)使用(如果您的項(xiàng)目是商業(yè)性質(zhì),請聯(lián)系我們進(jìn)行授權(quán))。另外,F(xiàn)izz已有的豐富插件我們也會選擇合適的時(shí)機(jī)與各位交流。
無論我們的項(xiàng)目交流是否能幫到各位,我們真誠希望能得到各位的反饋。不管項(xiàng)目技術(shù)是否牛逼,完善與否,我們始終不忘初心:Fizz,一個(gè)面向大中型企業(yè)的管理型網(wǎng)關(guān)。
“性能超高的API網(wǎng)關(guān)之怎么使用Fizz Gateway”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!