# 引言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网关的设计。
# 走上自研网关之路
咱们须要网关么?网关层解决什么问题?这两个问题不言而喻。咱们须要网关,由于它能够帮咱们解决负载均衡、聚合、受权、监控、限流、日志、权限控制等一系列的问题。同时,咱们也须要中间层,细化服务颗粒度的微服务让咱们不得不经过中间层聚合它们。
而咱们不须要的是复杂的编码、冗余的胶水代码,以及冗长的发布流程。
为解决这些问题,咱们须要让网关与中间层模糊界限,抹去网关和中间层隔阂,让网关支持中间层动态编码,尽量少的发布部署。为实现这个目的,只须要用一个简洁的网关模型并同时利用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快速下线功能以及降级响应功能。
## 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,一个面向大中型企业的管理型网关。