写点什么

微服务中台技术解析之网关 (dubbo-rest) 实践

用户头像
小江
关注
发布于: 2021 年 04 月 10 日
微服务中台技术解析之网关(dubbo-rest)实践

设计背景

我们作为一个跨境电商公司,在按业务功能进行垂直划分后形成了导购/营销/运营/商家等多个产品线,由不同团队负责。而在业务流程层面,各个产品线也都封装了自己的基础服务对外提供 api 接口,这些 api 接口有的是 dubbo 接口,有的是 http 接口,而背后支持的技术团队有的用 Java 开发,有的用 python,也有的用 nodejs。因此形成了多语言,多协议交互的复杂情况,加上对外提供 api 入口,需要鉴权、路由、流控等诸多公共功能,需要由一个前置 api 网关负责这些职责,其在业务架构中的位置如下图所示:

网关在业务架构中的位置


设计目标

网关作为统一的 api 入口,可以做得很大很复杂,也可以做得满足基本要求即可。早期没有网关的时候,很多 dubbo 接口需要以 thrift 协议暴露给 python 业务团队,而后期 springboot 基本是公司主流技术栈,Java 工程师在写 dubbo 接口时候还得维护一份支持 thrift 的接口契约,给业务迭代带来许多不便。因此在引入了网关后,可以通过 http 接口交互。

结合公司自身的业务诉求,业务方现状,我们确定要达到如下目标:

  • 转发协议支持 http/dubbo

  • 支持路由转发

  • 统一的鉴权 &权限

  • 区分内部外部调用

  • 支持服务发现 &自动注册

  • 基本的防攻击策略

  • 熔断限流

  • 提供网关管理 UI


方案比较

网关接收到 http 请求后,需要进行协议转发,http 转发比较简单,使用 http client 发起请求即可。而如何转发到 dubbo 服务接口,则有两种方式,一种是以 rest 协议发起调用,另一种是以 dubbo 协议发起调用,两种方式对比如下:

=============================================================================

方案 | 网关侧 | 业务方侧

=============================================================================

gateway+rest | 使用 http client 发起 | 需要以 rest 协议暴露服务,http 长链接对大部分应用有侵入

=============================================================================

gateway+dubbo | 使用 dubbo client 发起 | 以 dubbo 协议暴露,支持注入 http header 等参数

=============================================================================


对于 gateway+rest 方案来说,因为大部分 springboot 应用都使用了 spring-web 开启 http 服务,故用 rest 暴露 dubbo 服务所要求的 http 长链接跟正常业务逻辑的短链接有冲突,而且网关侧用 http client 发起调用,容错只能靠网关重试,不能使用到 dubbo 框架提供的集群容错能力。


而对于 gateway+dubbo 方案,侵入较少,业务方改动较少,故最终我们选择了这个方案来实现 dubbo 协议转发。

总体架构


关于这个架构图,一个需要注意点是,图中的 dubbo-rest 并不是指需要业务方以 rest 协议暴露服务,而是我们设计的一个用来进行网关服务自动发现 &注册的组件,随着统一接入框架被客户端引入,在需要暴露服务到网关时通过配置触发开启工作。


下面我们来解释下整个工作流程。

首先,决定暴露 dubbo 服务到网关的业务方会打开一个配置,触发 dubbo-rest 组件工作,将 dubbo 接口和 rest 服务映射关系上传到元数据中心(zk 集群)。

接着,由于网关也引入了 dubbo-rest 组件,通过感知变更通知,会获取到 rest 服务和 dubbo 接口的映射关系。

随后,应用启动后,业务方可以在网关管理入口对需要暴露的 rest 服务进行路由配置,过滤配置等,这些信息会推送到 Apollo(一个配置管理组件),而网关在转发请求时会拉取到这些配置。

再接着,网关会将收到的 http 请求和 rest 服务做匹配,找到请求所属的 dubbo 接口。

最后,网关会拼接 dubbo 接口所需的参数,发起泛化调用(关于 dubbo 泛化调用,参考官方文档),并将结果返回给调用方。

实现难点

dubb 接口与 rest 服务映射

元数据上传时机

要将 dubbo 服务暴露到网关,需要上传接口信息到 zookeeper 集群,而上传的步骤分成两步:

  • 第一步,本地预注册。监听 dubbo 服务暴露完成事件 ServiceBeanExportedEvent,将接口元数据保存到本地。

  • 第二步,注册到远端。监听 spring 事件 ApplicationReadyEvent ,将预注册的元数据上传到 zookeeper 集群。

元数据内容

根据后续要执行协议转换所需信息,元数据内容要包括 dubbo 接口本身信息,以及 rest 服务匹配所需的其他信息,如下:

DubboMethodMetadata

public class DubboMethodMetadata {
String applicationName;
String interfaceName;
String methodName;
Map<Integer, String> indexToParameterType;
String returnType;
String version;
String group;}
复制代码


RestMethodMetadata

public class RestMethodMetadata {
String applicationName;
List<String> verbs;
Map<String, Collection<String>> headers;
List<String> uriPatterns;
Map<Integer, Collection<String>> indexToParameterName;
DubboMethodMetadata dubboMethodMetadata;
boolean allParamsInBody;}
复制代码


在抽取接口元数据时,我们选择了 feign 组件进行辅助(Feign地址),该组件在 java client 和 rest url 之间的转换已经很成熟,支持标准的 JAX-RS,Spring4,Feign 等 rest 参数映射。

rest 参数注入

在 rest 参数注入这块,对业务方 dubbo 接口还是有侵入,因为网关采用泛化调用业务方 dubbo 服务,业务方在声明 rest 参数注入时,需要使用新 dubbo 接口,举个例子:

Result<PaymentNoticeDTO> paymentReturn(String channel, String key, Map<String, String> params);
@Override@RequestMapping(value = "/paymentReturn/{key}/{channel}/", method = RequestMethod.GET)public Result<PaymentNoticeDTO> paymentReturn( @PathVariable String key, @PathVariable String channel, @SpringQueryMap Map<String, String> params){ // TODO implementation}
复制代码

上面的例子是该接口方法需要注入路径参数和查询串 Map,因此,业务方需要为这个暴露到网关的服务生成新的 dubbo 接口。


下面我们看下这个 dubbo 接口经过参数抽取后的元数据:

{	"allParamsInBody": false,	"applicationName": "cf-buy",	"dubboMethodMetadata": {		"applicationName": "cf-buy",		"indexToParameterType": {			0: "java.lang.String",			1: "java.lang.String",			2: "java.util.Map"		},		"interfaceName": "com.clubfactory.cf.buy.client.service.BuyService",		"methodName": "paymentReturn",		"returnType": "com.clubfactory.boot.client.result.Result"	},	"headers": {},	"indexToParameterName": {		0: ["key"],		1: ["channel"],		2: ["$QUERYS$"]	},	"uriPatterns": ["/paymentReturn/{key}/{channel}/"],	"verbs": ["GET"]}
复制代码


从元数据中可以看到,uriPatterns 是用来跟请求 url 做匹配,匹配后获得的 keychannel 参数值将会暂存为后面拼接泛化参数使用。而 indexToParameterTypeindexToParameterName 也在拼接泛化参数时候使用。


以上面的例子为例,假设网关收到的请求为 http://gateway-host/gw/xxx.BuyService/paymentReturn/xxxKey/xxxChannel/?query1=value1&query2=value2

则最后拼接的调用为

Map<String,Object> queryMap = new HashMap<>();queryMap.put("query1","value1");queryMap.put("query2","value2");
genericService.$invoke( "xxx.BuyService.paymentReturn", new String[]{"java.lang.String","java.lang.String","java.util.Map"}, new Object[]{"xxxKey","xxxChannel",queryMap});
复制代码

经验 &总结

由于业务需求紧急,在网关的设计和实现中,做了适当权衡和裁剪,使得遗留了不少问题,在后续迭代中排查和处理难度增加。

比如,关于网关异常错误码的设计就不够人性化,调用出错后原因不够准确明了,需要二次排查,但由于网关项目推广后较多业务方已接入,使得后续调整错误码规范非常困难,这是一个教训。

再比如,网关因为负责发起 dubbo 泛化调用,后端接口响应性能不一,没有考虑到接口隔离问题,使得慢响应接口拖慢网关整体 qps,并且在严重时干扰到其他接口执行,这也是设计时候的一个考虑不足,需要避免。

用户头像

小江

关注

~做一个安静的码男子~ 2019.02.11 加入

软件工程师,目前在电商公司做研发效能平台,中间件维护开发相关工作

评论

发布
暂无评论
微服务中台技术解析之网关(dubbo-rest)实践