Fizz中大型企業管理型網關 引言 無法適配的市面方案 Fizz的設計 Fizz的性能問題 Fizz的交流發展

引言

我在參與工作的第四年加入了一個國內知名的電商公司,因爲公司的主要業務形態是特賣,用戶量也不小,在併發這一塊的要求比較高。公司使用的一直都是微服務的架構,這是我見到的比較大的一箇中間層團隊的規模,近三分之一的同事從事着中間層服務的開發。在此之前,我從事的是客戶端的開發工作,對接的中間層團隊雖然規模不大,也是相當於團隊近四分之一的規模。而現今的團隊初期的中間層團隊也是接近這個規模的。

開發與調試之痛

在調試上與開發上顯然是比較痛苦的,比如在併發量有要求的場景,當時選擇的是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;
    }

『複雜』的中間層

在之後,我曾經查看過我們一個搜索的中間層邏輯,其服務大概是這麼一個過程。
1、獲取會員的信息,會員卡列表,會員積分餘額,因爲不同級別的會員會有不同價格;
2、獲取用戶的優惠券信息,這部分會對計算的出來的價格產生影響;
3、獲取搜索的結果信息,結果來自三部分,商旅商品的庫存價格,猜你喜歡的庫存價格,推薦位的庫存價格,海外商品的庫存價格。
涉及到的服務有:中間層服務(聚合服務)、會員服務、優惠券服務,推薦服務、企業服務、海外搜索服務、搜索服務。還有各種類型的緩存設施以及數據庫的配置服務。

    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();
    }

這個項目使用的是spring-mvc,但是項目內部的Service確使用了Mono,並且用了block,研發跟我講這是異步編程,我明確指出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的設計

從原點出發的需求

當來我來複盤當初這個選擇的時候,需要再強調下從原點出發的需求:

1、Java技術棧,支持Spring全家桶;
2、方便易用,零陪訓也能編排;
3、動態路由能力,隨時隨地能夠開啓新API;
4、高性能並且集羣可橫向擴展;
5、強熱服務編排能力,支持前後端編碼,隨時隨地更新API;
6、線上編碼邏輯支持;
7、可擴展的安全認證能力,方便日誌記錄;
9、API審覈功能,把握所有服務;
9、可擴展性,強大的插件開發機制;

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有一套平滑替換網關的方案。在最初,Fizz是可以跟其他網關並存的,比如之前提到的基於Vert.x的網關。所以Fizz就有一個類似nginx的反向代理方案,純粹基於路由的實現。於是,在項目的進行的初期,過了Nginx的流量被原原本本的轉發到Fizz然後再到Vert.x,其代理了Vert.x了全部流量。在之後,流量被逐步轉發到後端的微服務,Vert.x上有一部分特別定製公用代碼被下沉到底層微服務端,Vert.x還有中間層服務被完全廢棄,服務器的數量減少三分之一。在我們做完調整之後,原先困擾我的中間層人員以及服務器的問題終於得到解決,我們可以縮少每個同事手中的那一串的服務列表清單,將勞動落實到更有價值的項目上去了。當這一切變得清晰的時候,這個項目也就自然而然顯示了它的價值。

針對渠道,這裏的路由功能也是非常實用的功能,因爲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進行比較,使用相同的環境和條件,測試對象均爲單個節點。

Intel(R) Xeon(R) 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包含了兩個主要的後端項目fizz-gateway, fizz-manager,而fizz-admin作爲Fizz的前端配置界面。fizz-manger與fizz-admin爲Fizz提供圖形化的配置界面。所有的Pipe都能夠在操作界面的進行編寫以及上線。

爲了能夠讓更多的中大型快速發展的團隊能夠應用上這個面向管理,解決實際問題的網關,Fizz接下來將會提供的fizz-gateway-community社區版本的解決方案,而且作爲對外技術的交流,其技術的核心實現將會以GNU v3授權方式進行的開放。fizz-gateway-community的所有的API將會提供所有的對外API以便二次開發使用。fizz-gateway-professional專業版本其因爲與團隊的業務綁定進行商業封閉。而對應的fizz-manger升級爲fizz-manger-professional,fizz-admin-professional作爲商業版本開放二進制包的免費下載,提供使用了GNU v3開源協議的項目免費使用,而商業項目請聯繫我們進行授權。另外插件的提供我們也會選擇合適的時機與各位交流。

無論我們的項目交流是否幫助到各位,我們真誠的希望能夠得各位的任何批評反饋。無論項目技術是否牛逼,完善與否,我們始終不完初心,Fizz,做一個面向中大型企業好用的開放型面向實際擁抱生態的網關。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章