性能超高的API網關:Fizz Gateway自研之路

 

# 引言javascript

在參與電商工做第一年,我從事客戶端開發工做。雖然團隊規模不大,可是對接的中間層團隊人數,卻至關於團隊近四分之一的規模。工做第四年,我又加入國內一家知名的電商公司。這家公司的主要業務形態是特賣,中間層團隊佔團隊的人數近三分之一。而如今,我所帶領的團隊,在發展初期,中間層團隊也是接近這個規模。
三個團隊都是電商團隊,用戶規模較大,在併發上要求較高,而且採用微服務架構,由中臺底層提供各類電商服務(如訂單、庫存)和通用服務(如搜索),因此中間層團隊須要通過各類受權和認證調用各個BU的服務,從而組裝出前端適配的接口。由於對C端業務的接口繁多,因此中間層佔用着團隊寶貴的人力資源。並且團隊成立時間越久,累積的接口越多,有效的管理如此繁多的接口是一個使人頭疼的問題。html

## 中間層的系列問題前端

開發調試問題

中間層在Web網站上的部署偏前,通常部署於防火牆及Nginx以後,更多面向C端用戶服務,因此在性能併發量上有較高的要求,大部分團隊在選型上會選擇異步框架。正由於其直接面向C端,變化較多,大部分須要常常性地變動或者配置的代碼都會安排在這一層次,發佈很是頻繁。此外,不少團隊使用編譯型語言進行編碼,而非解釋型語言。這三個因素組合在一塊兒,使得開發者調試與開發很是痛苦。好比,咱們曾經選擇Play2框架,這是一個異步Java框架,須要開發者可以流暢地編寫異步,可是熟悉調試技巧的同事也很少。在代碼裏面配置了各類請求參數,以及結果處理,看似很是簡單,可是聯調、單元測試、或者配置文件修改以後等待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;
	}

上述代碼只是摘抄了其中一個過程函數。若是咱們將中間層的場景設置得更爲複雜一些,咱們要解決的就不只僅是編碼性能、編碼質量、編碼時間的問題。web

## 「複雜」場景問題spring

微服務顆粒度較細,爲了實現簡潔的前端邏輯以及較少的服務調用次數,咱們針對C端的大部分輸出是聚合的結果。好比,咱們一個搜索的中間層邏輯,其服務是這樣一個過程:sql

獲取會員信息、會員卡列表、會員積分餘額,由於不一樣級別的會員會有不一樣價格;數據庫

獲取用戶的優惠券信息,這部分會對計算出來的價格產生影響;
獲取搜索的結果信息,結果來自三部分,商旅商品的庫存價格,猜你喜歡的庫存價格,推薦位的庫存價格,海外商品的庫存價格。編程

這其中涉及到的服務有:中間層服務(聚合服務)、會員服務、優惠券服務、推薦服務、企業服務、海外搜索服務、搜索服務。此外,還有各類類型的緩存設施以及數據庫的配置服務。json

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 都是不一樣小規模團隊使用過的網關。談及最多的特性是:

一、支持熔斷、流量控制和過載保護
二、支持特別高的併發
三、秒殺

然而,對商業而言,熔斷、流量控制和過載保護應該是最後考慮的措施。並且,對一個成長中的團隊來講,服務的過載崩潰是須要經歷較長時間的業務沉澱。

另外,秒殺業務的流量更可能是維持一個普通水平,其偶爾的高併發也是在咱們團隊處理能力範圍以內。換句話說,選型時,更多的是須要結合實際,而不是考慮相似阿里巴巴的流量,我只需考慮中等水平以上而且具有集羣擴展性的方式便可。

此前,咱們團隊使用比較廣的網關是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網關的設計。

# 走上自研網關之路

咱們須要網關麼?網關層解決什麼問題?這兩個問題不言而喻。咱們須要網關,由於它能夠幫咱們解決負載均衡、聚合、受權、監控、限流、日誌、權限控制等一系列的問題。同時,咱們也須要中間層,細化服務顆粒度的微服務讓咱們不得不經過中間層聚合它們。
而咱們不須要的是複雜的編碼、冗餘的膠水代碼,以及冗長的發佈流程。

Fizz的設計考量

爲解決這些問題,咱們須要讓網關與中間層模糊界限,抹去網關和中間層隔閡,讓網關支持中間層動態編碼,儘量少的發佈部署。爲實現這個目的,只須要用一個簡潔的網關模型並同時利用low-code特性儘量地去覆蓋中間層的功能便可。

架構圖

## 從原點出發的需求

在覆盤當初這個選擇時,我須要再強調下從原點出發的需求:
一、Java技術棧,支持Spring全家桶;
二、方便易用,零培訓也能編排;
三、動態路由能力,隨時隨地可以開啓新API;
四、高性能且集羣可橫向擴展;
五、強熱服務編排能力,支持先後端編碼,隨時隨地更新API;
六、線上編碼邏輯支持;
七、可擴展的安全認證能力,方便日誌記錄;
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, // 是否爲調試模式,默認false
    type: "REQUEST", // 類型,REQUEST/MYSQL
    method: "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: "", // groovy
        source: "" // 腳本返回List<String>對象,null:驗證經過,List:錯誤信息列表
    },
    validateResponse:{
   // 入參驗證失敗響應,處理方式同dataMapping.response
        fixedBody: {
    // 固定的body
            "code": -411
        },
        fixedHeaders: {
   // 固定header
            "a":"b"
        },
        headers: {
    // 引用的header
        },
        body: {
    // 引用的header
            "msg": "validateMsg"
        },
        script: {
   
            type: "", // groovy
            source: ""
        }
    },
    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: "", // groovy
                source: ""
            }
        }
    },
    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: "", // groovy
                    source: ""
                }
            }
        }, 
        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 GET
                timeout: 3000, // 超時時間 單位毫秒,容許1-10000秒之間的值,不填或小於1毫秒取默認值3秒,大於10秒取10秒
                condition: {
   
                    type: "", // groovy
                    source: "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: "", // groovy
                            source: ""
                        }
                    },
                    response: {
   
                    	fixedBody: {
   
                        	
                        },
                        fixedHeaders: {
   
                        	
                        },
                        headers: {
   
                            "abc": "step1.requests.request1.headers.xyz"
                        },
                        body:{
   
                            "inn.innId": "step1.requests.request1.response.id"
                        },
                        script: {
   // 腳本計算body的值
                            //type: "", // groovy
                            source: ""
                        }
                    }
                }
        	}
        ]
    }]
}

運行的上下文格式爲:

// 運行時上下文,用於保存客戶輸入和每一個步驟的輸入與輸出結果
var stepContext = {
   
	// 是否DEBUG模式
	debug:false,
	// elapsed time
	elapsedTimes: [{
   
		[actionName]: 123, // 操做名稱:耗時
	}],
    // input data
    input: {
   
        request:{
   
        	path: "",
            method: "GET/POST",
            headers: {
   },
            body: {
   },
            params: {
   }
        },
        response: {
    // 聚合接口的響應
            headers: {
   },
            body: {
   }
        }
    },
    // step name
    stepName: {
   
        // step request data
        requests: {
   
            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快速下線功能以及降級響應功能。

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之初,咱們就考慮到企業內部複雜的中間層狀況:它能夠截流全部的流量,能並行且逐步替換現有網關。因此在內部推行時,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,一個面向大中型企業的管理型網關。

相關文章
相關標籤/搜索