中间件 – 案例:如何基于Dubbo进行网关设计?

中间件核心技术与实战专栏目录总览

这节课我们通过一个真实的业务场景来看看 Dubbo 网关(开放平台)的设计要领。

设计背景

要设计一个网关,我们首先要知道它的设计背景。

2017 年,我从传统行业脱身,正式进入物流行业。说来也非常巧,我当时加入的是公司的网关项目组,主要解决泛化调用与协议转换代码的开发问题。刚进公司不久,网关项目组就遇到了技术难题。快递物流行业的业务量可以比肩互联网,从那时候开始,我的传统技术思维开始向互联网技术思维转变。

当时网关项目组的核心任务就是确保能够快速接入各个电商平台。我来简单说明一下具体的场景。

中间件 - 案例:如何基于Dubbo进行网关设计?

解释一下上面这个图。

物流公司内部已经基于 Dubbo 构建了订单中心微服务域,其中创建订单接口的定义如下:​

中间件 - 案例:如何基于Dubbo进行网关设计?

外部电商平台众多,每一家电商平台内部都有自己的标准,并不会遵循统一的标准。例如在淘宝中,当用户购买商品后,淘宝内部会定义一个统一的订单外派接口。它的请求包可能是这样的:

{

"seller_id":189,

"buyer":"dingwei",

"order":[

{

"goods_name":"华为笔记本",

"num":1,

"price":500000

},

{

"goods_name":"华为手表",

"num":1,

"price":200000

}

]

}

但拼多多内部定义的订单外派接口,它的请求包可能是下面这样的:

<order>

<seller_uid>189</seller_uid>

<buyer_uid>dingwei</buyer_uid>

<order_items>

<order_item>

<goods_name>华为笔记本</goods_name>

<num>1</num>

<price>500000</price>

</order_item>

<order_item>

<goods_name>华为手表</goods_name>

<num>1</num>

<price>200000</price>

</order_item>

</order_items>

</order>

当电商的快递件占据快递公司总业务量的大半时,电商平台的话语权是高于快递公司的。也就是说,电商平台不管下游对接哪家物流公司,都会下发自己公司内部定义的订单派发接口,适配工作需要由物流公司自己来承担。

那站在物流公司的角度,应该怎么做呢?总不能每接入一个电商平台就为它们开发一套下单服务吧?那样的话,随着越来越多的电商平台接入,系统的复杂度会越来越高,可维护性将越来越差。

设计方案

正是在这样的背景下,网关平台被立项开发出来了。这个网关平台是怎么设计的呢?在设计的过程中需要解决哪些常见的问题?

我认为,网关的设计至少需要包括三个方面,分别是签名验证、服务配置和限流。

先说签名验证。保证请求的安全是系统设计需要优先考虑的。业界有一种非常经典的通信安全校验机制:验证签名。

这种机制的做法是,客户端与服务端会首先采用 HTTPS 进行通信,确保传输过程的私密性。

客户端在发送请求时,先将请求参数按参数名称进行排序,然后按顺序拼接成字符串,格式为 key1=a & key2=b。接下来,客户端使用一个约定的密钥对拼接出来的参数字符串进行签名,生成签名字符串(我们用 sign 表示签名字符串)并追加到 URL。通常,还会在 URL 中追加一个发送时间戳(时间戳不参与签名验证)。

服务端在接收到客户端的请求后,先从请求中解析出所有的参数,同样按照参数名对参数进行排序,然后使用同样的密钥对参数进行签名。得到的签名字符串需要与客户端计算的签名字符串进行对比,如果两者不同,则请求无效。与此同时,通常我们还需要将服务端当前的时间戳与客户端时间戳进行对比,如果相差超过一定的时间,同样认为请求无效,这个操作主要是为了避免使用同一个连接对网络进行连续攻击。

这整个过程里有一个非常重要的点,就是密钥自始至终并没有在网络上进行过传播,它的安全性可以得到十足的保证。签名验证的流程大概可以用下面这张图表示:

中间件 - 案例:如何基于Dubbo进行网关设计?

如果要对验证签名进行产品化设计,我们通常需要:

为不同的接入端(电商平台)创建不同的密钥,并通过安全的方式告知他们;

为不同的接入端(电商平台)配置签名算法。

在确保能够安全通信后,接下来就是网关设计最核心的部分了:服务接口配置化。它主要包括两个要点:微服务调用协议(Dubbo 服务描述)和接口定义与参数映射。

我们先来看一下微服务调用协议的配置,设计的原型界面如下图所示:

中间件 - 案例:如何基于Dubbo进行网关设计?

将所有的微服务(细化到方法级名称)维护到网关系统中,网关应用就可以使用 Dubbo 提供的编程 API,根据这些元信息动态构建一个个消费者(服务调用者),进而通过创建的服务调用客户端发起 RPC 远程调用,最终实现网关应用的 Dubbo 服务调用。

基于这些元信息构建消费者对象的关键代码如下:

public static GenericService getInvoker(String serviceInterface, String version, List<String> methods, int retry, String registryAddr ) {

ReferenceConfig referenceConfig = new ReferenceConfig();

// 关于消费者通用参数,可以从配置文件中获取,本示例取消

ConsumerConfig consumerConfig = new ConsumerConfig();

consumerConfig.setTimeout(3000);

consumerConfig.setRetries(2);

referenceConfig.setConsumer(consumerConfig);

//应用程序名称

ApplicationConfig applicationConfig = new ApplicationConfig();

applicationConfig.setName("GateWay");

referenceConfig.setApplication(applicationConfig);

// 注册中心

RegistryConfig registry = new RegistryConfig();

registry.setAddress(registryAddr);

registry.setProtocol("zookeeper");

referenceConfig.setRegistry(registry);

// 设置服务接口名称

referenceConfig.setInterface(serviceInterface);

// 设置服务版本

referenceConfig.setVersion(version);

referenceConfig.setMethods(new ArrayList<MethodConfig>());

for(String method : methods) {

MethodConfig methodConfig = new MethodConfig();

methodConfig.setName(method);

referenceConfig.getMethods().add(methodConfig);

}

referenceConfig.setGeneric("true");// 开启dubbo的泛化调用

return (GenericService) referenceConfig.get();

}

通过 getInvoker 方法发起调用远程 RPC 服务,这样,网关应用就成为了对应服务的消费者。

因为网关应用引入服务规约(API 包)不太现实,所以这里使用的是泛化调用,这样方便网关应用不受约束地构建消费者对象。

值得注意的是,ReferenceConfig 实例很重,它封装了与注册中心的连接以及所有服务提供者的连接,需要被缓存起来。因此,在真实的生产实践中,我们需要将 ReferenceConfig 对象存储到缓存中。否则,重复生成的 ReferenceConfig 可能造成性能问题并伴随着内存和连接泄漏。

除了 ReferenceConfig,其实 getInvoker 生成对象也可以进行缓存,缓存的 key 通常为接口名称、版本和注册中心。

那如果配置信息动态发生了变化,例如需要添加新的服务,这时候网关应用如何做到动态感知呢?我们通常可以用基于 MQ 的方式来解决这个问题。具体的解决方案如下:

中间件 - 案例:如何基于Dubbo进行网关设计?

也就是说,用户如果在网关运营平台上修改原有服务协议(Dubbo 服务)或者添加新的服务协议,变动后的协议会首先存储到数据库中,然后运营平台发送一条消息到 MQ,紧接着 Gateway 的后台进程以广播模式进行订阅。这样,所有后台网关进程都可以感知。

如果是对已有服务协议进行修改,在具体实践时有一个小细节请你一定注意。我们先看看这段代码:

Map<String /* 缓存key */,GenericService> invokerCache;

GenericService newInvoker = getInvoker(...);//参数省略

GenericService oldInvoker = invokerCache.get(key);

invokerCache.put(newInvoker);//先缓存新的invoker

// 然后再销毁旧的invoker对象

oldInvoker.destory();

如果已经存在对应的 Invoker 对象,为了不影响现有调用,应该先用新的 Invoker 对象去更新缓存,然后再销毁旧的 Invoker 对象。

上面的方法解决了网关调用公司内部的 Dubbo 微服务问题,但还有另外一个非常重要的问题,怎么配置服务接口相关参数呢?

联系这节课前面的场景,我们需要在页面上配置公司内部 Dubbo 服务与外部电商的接口映射。

中间件 - 案例:如何基于Dubbo进行网关设计?

为此,我们专门建立了一条参数映射协议:

中间件 - 案例:如何基于Dubbo进行网关设计?

参数映射设计的说明如下。

请求类型:主要分为请求参数与响应参数;

字段名称:Dubbo 服务对应的字段名称;

字段类型:Dubbo 服务对应字段的属性;

字段所属类:Dubbo 服务对应字段所属类型;

节点名称:外部请求接口对应的字段名称;

显示顺序:排序字段。

由于网关采取了泛化调用,在编写转换代码时,主要是遍历传入的参数,根据每一个字段查询对应的转换规则,然后转换为 Map,返回值则刚好相反,是将 Map 转换为 XML 或者 JSON。

在真正请求调用时,根据映射规则构建出请求参数 Map 后,通过 Dubbo 的泛化调用执行真正的调用:

GenericService genericService = (GenericService) invokeBean;

Map invokerPams;//省略转换过程

// 参数类型数组

String[] paramTypes = new String[1];

paramTypes[0]="java.util.Map";

// 参数值数组

Object[] paramValues = new Object[1];

​

invokerPams.put("class", "net.codingw.oms.vo.OrderItemVo");

paramValues[0] = invokerPams;

//由于我们已经转化为java.util.Map,并且Map中,需要有一个key为class的,表示服务端需要转化的类型,这个从协议转换器中获取

Object result = genericService.$invoke(this.getInvokeMethod(), paramTypes, paramValues);

这样,网关就具备了高扩展性和稳定性,可以非常灵活地支撑业务的扩展,为不同的电商平台配置不同的参数转换,从而在内部只需要开发一套接口就可以非常灵活地支撑业务的扩展,基本做到网关代码零修改。

总结

这节课,我通过一个真实的场景,详细介绍了网关设计的需求背景,然后针对网关设计的痛点给出了设计方案。通过对这个方案中关键代码的解读,你应该能够更加深刻地理解 Dubbo 泛化调用背后的逻辑,真正做到理论与实际相结合。

值得注意的是,我们这节课提到的转换协议也是一绝,它使用中括号来定义多层嵌套结构,使得该协议具有普适性。

这节课我们通过一个真实的业务场景来看看 Dubbo 网关(开放平台)的设计要领。

设计背景

要设计一个网关,我们首先要知道它的设计背景。

2017 年,我从传统行业脱身,正式进入物流行业。说来也非常巧,我当时加入的是公司的网关项目组,主要解决泛化调用与协议转换代码的开发问题。刚进公司不久,网关项目组就遇到了技术难题。快递物流行业的业务量可以比肩互联网,从那时候开始,我的传统技术思维开始向互联网技术思维转变。

当时网关项目组的核心任务就是确保能够快速接入各个电商平台。我来简单说明一下具体的场景。

中间件 - 案例:如何基于Dubbo进行网关设计?

解释一下上面这个图。

物流公司内部已经基于 Dubbo 构建了订单中心微服务域,其中创建订单接口的定义如下:​

中间件 - 案例:如何基于Dubbo进行网关设计?

外部电商平台众多,每一家电商平台内部都有自己的标准,并不会遵循统一的标准。例如在淘宝中,当用户购买商品后,淘宝内部会定义一个统一的订单外派接口。它的请求包可能是这样的:

{

"seller_id":189,

"buyer":"dingwei",

"order":[

{

"goods_name":"华为笔记本",

"num":1,

"price":500000

},

{

"goods_name":"华为手表",

"num":1,

"price":200000

}

]

}

但拼多多内部定义的订单外派接口,它的请求包可能是下面这样的:

<order>

<seller_uid>189</seller_uid>

<buyer_uid>dingwei</buyer_uid>

<order_items>

<order_item>

<goods_name>华为笔记本</goods_name>

<num>1</num>

<price>500000</price>

</order_item>

<order_item>

<goods_name>华为手表</goods_name>

<num>1</num>

<price>200000</price>

</order_item>

</order_items>

</order>

当电商的快递件占据快递公司总业务量的大半时,电商平台的话语权是高于快递公司的。也就是说,电商平台不管下游对接哪家物流公司,都会下发自己公司内部定义的订单派发接口,适配工作需要由物流公司自己来承担。

那站在物流公司的角度,应该怎么做呢?总不能每接入一个电商平台就为它们开发一套下单服务吧?那样的话,随着越来越多的电商平台接入,系统的复杂度会越来越高,可维护性将越来越差。

设计方案

正是在这样的背景下,网关平台被立项开发出来了。这个网关平台是怎么设计的呢?在设计的过程中需要解决哪些常见的问题?

我认为,网关的设计至少需要包括三个方面,分别是签名验证、服务配置和限流。

先说签名验证。保证请求的安全是系统设计需要优先考虑的。业界有一种非常经典的通信安全校验机制:验证签名。

这种机制的做法是,客户端与服务端会首先采用 HTTPS 进行通信,确保传输过程的私密性。

客户端在发送请求时,先将请求参数按参数名称进行排序,然后按顺序拼接成字符串,格式为 key1=a & key2=b。接下来,客户端使用一个约定的密钥对拼接出来的参数字符串进行签名,生成签名字符串(我们用 sign 表示签名字符串)并追加到 URL。通常,还会在 URL 中追加一个发送时间戳(时间戳不参与签名验证)。

服务端在接收到客户端的请求后,先从请求中解析出所有的参数,同样按照参数名对参数进行排序,然后使用同样的密钥对参数进行签名。得到的签名字符串需要与客户端计算的签名字符串进行对比,如果两者不同,则请求无效。与此同时,通常我们还需要将服务端当前的时间戳与客户端时间戳进行对比,如果相差超过一定的时间,同样认为请求无效,这个操作主要是为了避免使用同一个连接对网络进行连续攻击。

这整个过程里有一个非常重要的点,就是密钥自始至终并没有在网络上进行过传播,它的安全性可以得到十足的保证。签名验证的流程大概可以用下面这张图表示:

中间件 - 案例:如何基于Dubbo进行网关设计?

如果要对验证签名进行产品化设计,我们通常需要:

为不同的接入端(电商平台)创建不同的密钥,并通过安全的方式告知他们;

为不同的接入端(电商平台)配置签名算法。

在确保能够安全通信后,接下来就是网关设计最核心的部分了:服务接口配置化。它主要包括两个要点:微服务调用协议(Dubbo 服务描述)和接口定义与参数映射。

我们先来看一下微服务调用协议的配置,设计的原型界面如下图所示:

中间件 - 案例:如何基于Dubbo进行网关设计?

将所有的微服务(细化到方法级名称)维护到网关系统中,网关应用就可以使用 Dubbo 提供的编程 API,根据这些元信息动态构建一个个消费者(服务调用者),进而通过创建的服务调用客户端发起 RPC 远程调用,最终实现网关应用的 Dubbo 服务调用。

基于这些元信息构建消费者对象的关键代码如下:

public static GenericService getInvoker(String serviceInterface, String version, List<String> methods, int retry, String registryAddr ) {

ReferenceConfig referenceConfig = new ReferenceConfig();

// 关于消费者通用参数,可以从配置文件中获取,本示例取消

ConsumerConfig consumerConfig = new ConsumerConfig();

consumerConfig.setTimeout(3000);

consumerConfig.setRetries(2);

referenceConfig.setConsumer(consumerConfig);

//应用程序名称

ApplicationConfig applicationConfig = new ApplicationConfig();

applicationConfig.setName("GateWay");

referenceConfig.setApplication(applicationConfig);

// 注册中心

RegistryConfig registry = new RegistryConfig();

registry.setAddress(registryAddr);

registry.setProtocol("zookeeper");

referenceConfig.setRegistry(registry);

// 设置服务接口名称

referenceConfig.setInterface(serviceInterface);

// 设置服务版本

referenceConfig.setVersion(version);

referenceConfig.setMethods(new ArrayList<MethodConfig>());

for(String method : methods) {

MethodConfig methodConfig = new MethodConfig();

methodConfig.setName(method);

referenceConfig.getMethods().add(methodConfig);

}

referenceConfig.setGeneric("true");// 开启dubbo的泛化调用

return (GenericService) referenceConfig.get();

}

通过 getInvoker 方法发起调用远程 RPC 服务,这样,网关应用就成为了对应服务的消费者。

因为网关应用引入服务规约(API 包)不太现实,所以这里使用的是泛化调用,这样方便网关应用不受约束地构建消费者对象。

值得注意的是,ReferenceConfig 实例很重,它封装了与注册中心的连接以及所有服务提供者的连接,需要被缓存起来。因此,在真实的生产实践中,我们需要将 ReferenceConfig 对象存储到缓存中。否则,重复生成的 ReferenceConfig 可能造成性能问题并伴随着内存和连接泄漏。

除了 ReferenceConfig,其实 getInvoker 生成对象也可以进行缓存,缓存的 key 通常为接口名称、版本和注册中心。

那如果配置信息动态发生了变化,例如需要添加新的服务,这时候网关应用如何做到动态感知呢?我们通常可以用基于 MQ 的方式来解决这个问题。具体的解决方案如下:

中间件 - 案例:如何基于Dubbo进行网关设计?

也就是说,用户如果在网关运营平台上修改原有服务协议(Dubbo 服务)或者添加新的服务协议,变动后的协议会首先存储到数据库中,然后运营平台发送一条消息到 MQ,紧接着 Gateway 的后台进程以广播模式进行订阅。这样,所有后台网关进程都可以感知。

如果是对已有服务协议进行修改,在具体实践时有一个小细节请你一定注意。我们先看看这段代码:

Map<String /* 缓存key */,GenericService> invokerCache;

GenericService newInvoker = getInvoker(...);//参数省略

GenericService oldInvoker = invokerCache.get(key);

invokerCache.put(newInvoker);//先缓存新的invoker

// 然后再销毁旧的invoker对象

oldInvoker.destory();

如果已经存在对应的 Invoker 对象,为了不影响现有调用,应该先用新的 Invoker 对象去更新缓存,然后再销毁旧的 Invoker 对象。

上面的方法解决了网关调用公司内部的 Dubbo 微服务问题,但还有另外一个非常重要的问题,怎么配置服务接口相关参数呢?

联系这节课前面的场景,我们需要在页面上配置公司内部 Dubbo 服务与外部电商的接口映射。

中间件 - 案例:如何基于Dubbo进行网关设计?

为此,我们专门建立了一条参数映射协议:

中间件 - 案例:如何基于Dubbo进行网关设计?

参数映射设计的说明如下。

请求类型:主要分为请求参数与响应参数;

字段名称:Dubbo 服务对应的字段名称;

字段类型:Dubbo 服务对应字段的属性;

字段所属类:Dubbo 服务对应字段所属类型;

节点名称:外部请求接口对应的字段名称;

显示顺序:排序字段。

由于网关采取了泛化调用,在编写转换代码时,主要是遍历传入的参数,根据每一个字段查询对应的转换规则,然后转换为 Map,返回值则刚好相反,是将 Map 转换为 XML 或者 JSON。

在真正请求调用时,根据映射规则构建出请求参数 Map 后,通过 Dubbo 的泛化调用执行真正的调用:

GenericService genericService = (GenericService) invokeBean;

Map invokerPams;//省略转换过程

// 参数类型数组

String[] paramTypes = new String[1];

paramTypes[0]="java.util.Map";

// 参数值数组

Object[] paramValues = new Object[1];

​

invokerPams.put("class", "net.codingw.oms.vo.OrderItemVo");

paramValues[0] = invokerPams;

//由于我们已经转化为java.util.Map,并且Map中,需要有一个key为class的,表示服务端需要转化的类型,这个从协议转换器中获取

Object result = genericService.$invoke(this.getInvokeMethod(), paramTypes, paramValues);

这样,网关就具备了高扩展性和稳定性,可以非常灵活地支撑业务的扩展,为不同的电商平台配置不同的参数转换,从而在内部只需要开发一套接口就可以非常灵活地支撑业务的扩展,基本做到网关代码零修改。

总结

这节课,我通过一个真实的场景,详细介绍了网关设计的需求背景,然后针对网关设计的痛点给出了设计方案。通过对这个方案中关键代码的解读,你应该能够更加深刻地理解 Dubbo 泛化调用背后的逻辑,真正做到理论与实际相结合。

值得注意的是,我们这节课提到的转换协议也是一绝,它使用中括号来定义多层嵌套结构,使得该协议具有普适性。

这节课我们通过一个真实的业务场景来看看 Dubbo 网关(开放平台)的设计要领。

设计背景

要设计一个网关,我们首先要知道它的设计背景。

2017 年,我从传统行业脱身,正式进入物流行业。说来也非常巧,我当时加入的是公司的网关项目组,主要解决泛化调用与协议转换代码的开发问题。刚进公司不久,网关项目组就遇到了技术难题。快递物流行业的业务量可以比肩互联网,从那时候开始,我的传统技术思维开始向互联网技术思维转变。

当时网关项目组的核心任务就是确保能够快速接入各个电商平台。我来简单说明一下具体的场景。

中间件 - 案例:如何基于Dubbo进行网关设计?

解释一下上面这个图。

物流公司内部已经基于 Dubbo 构建了订单中心微服务域,其中创建订单接口的定义如下:​

中间件 - 案例:如何基于Dubbo进行网关设计?

外部电商平台众多,每一家电商平台内部都有自己的标准,并不会遵循统一的标准。例如在淘宝中,当用户购买商品后,淘宝内部会定义一个统一的订单外派接口。它的请求包可能是这样的:

{

"seller_id":189,

"buyer":"dingwei",

"order":[

{

"goods_name":"华为笔记本",

"num":1,

"price":500000

},

{

"goods_name":"华为手表",

"num":1,

"price":200000

}

]

}

但拼多多内部定义的订单外派接口,它的请求包可能是下面这样的:

<order>

<seller_uid>189</seller_uid>

<buyer_uid>dingwei</buyer_uid>

<order_items>

<order_item>

<goods_name>华为笔记本</goods_name>

<num>1</num>

<price>500000</price>

</order_item>

<order_item>

<goods_name>华为手表</goods_name>

<num>1</num>

<price>200000</price>

</order_item>

</order_items>

</order>

当电商的快递件占据快递公司总业务量的大半时,电商平台的话语权是高于快递公司的。也就是说,电商平台不管下游对接哪家物流公司,都会下发自己公司内部定义的订单派发接口,适配工作需要由物流公司自己来承担。

那站在物流公司的角度,应该怎么做呢?总不能每接入一个电商平台就为它们开发一套下单服务吧?那样的话,随着越来越多的电商平台接入,系统的复杂度会越来越高,可维护性将越来越差。

设计方案

正是在这样的背景下,网关平台被立项开发出来了。这个网关平台是怎么设计的呢?在设计的过程中需要解决哪些常见的问题?

我认为,网关的设计至少需要包括三个方面,分别是签名验证、服务配置和限流。

先说签名验证。保证请求的安全是系统设计需要优先考虑的。业界有一种非常经典的通信安全校验机制:验证签名。

这种机制的做法是,客户端与服务端会首先采用 HTTPS 进行通信,确保传输过程的私密性。

客户端在发送请求时,先将请求参数按参数名称进行排序,然后按顺序拼接成字符串,格式为 key1=a & key2=b。接下来,客户端使用一个约定的密钥对拼接出来的参数字符串进行签名,生成签名字符串(我们用 sign 表示签名字符串)并追加到 URL。通常,还会在 URL 中追加一个发送时间戳(时间戳不参与签名验证)。

服务端在接收到客户端的请求后,先从请求中解析出所有的参数,同样按照参数名对参数进行排序,然后使用同样的密钥对参数进行签名。得到的签名字符串需要与客户端计算的签名字符串进行对比,如果两者不同,则请求无效。与此同时,通常我们还需要将服务端当前的时间戳与客户端时间戳进行对比,如果相差超过一定的时间,同样认为请求无效,这个操作主要是为了避免使用同一个连接对网络进行连续攻击。

这整个过程里有一个非常重要的点,就是密钥自始至终并没有在网络上进行过传播,它的安全性可以得到十足的保证。签名验证的流程大概可以用下面这张图表示:

中间件 - 案例:如何基于Dubbo进行网关设计?

如果要对验证签名进行产品化设计,我们通常需要:

为不同的接入端(电商平台)创建不同的密钥,并通过安全的方式告知他们;

为不同的接入端(电商平台)配置签名算法。

在确保能够安全通信后,接下来就是网关设计最核心的部分了:服务接口配置化。它主要包括两个要点:微服务调用协议(Dubbo 服务描述)和接口定义与参数映射。

我们先来看一下微服务调用协议的配置,设计的原型界面如下图所示:

中间件 - 案例:如何基于Dubbo进行网关设计?

将所有的微服务(细化到方法级名称)维护到网关系统中,网关应用就可以使用 Dubbo 提供的编程 API,根据这些元信息动态构建一个个消费者(服务调用者),进而通过创建的服务调用客户端发起 RPC 远程调用,最终实现网关应用的 Dubbo 服务调用。

基于这些元信息构建消费者对象的关键代码如下:

public static GenericService getInvoker(String serviceInterface, String version, List<String> methods, int retry, String registryAddr ) {

ReferenceConfig referenceConfig = new ReferenceConfig();

// 关于消费者通用参数,可以从配置文件中获取,本示例取消

ConsumerConfig consumerConfig = new ConsumerConfig();

consumerConfig.setTimeout(3000);

consumerConfig.setRetries(2);

referenceConfig.setConsumer(consumerConfig);

//应用程序名称

ApplicationConfig applicationConfig = new ApplicationConfig();

applicationConfig.setName("GateWay");

referenceConfig.setApplication(applicationConfig);

// 注册中心

RegistryConfig registry = new RegistryConfig();

registry.setAddress(registryAddr);

registry.setProtocol("zookeeper");

referenceConfig.setRegistry(registry);

// 设置服务接口名称

referenceConfig.setInterface(serviceInterface);

// 设置服务版本

referenceConfig.setVersion(version);

referenceConfig.setMethods(new ArrayList<MethodConfig>());

for(String method : methods) {

MethodConfig methodConfig = new MethodConfig();

methodConfig.setName(method);

referenceConfig.getMethods().add(methodConfig);

}

referenceConfig.setGeneric("true");// 开启dubbo的泛化调用

return (GenericService) referenceConfig.get();

}

通过 getInvoker 方法发起调用远程 RPC 服务,这样,网关应用就成为了对应服务的消费者。

因为网关应用引入服务规约(API 包)不太现实,所以这里使用的是泛化调用,这样方便网关应用不受约束地构建消费者对象。

值得注意的是,ReferenceConfig 实例很重,它封装了与注册中心的连接以及所有服务提供者的连接,需要被缓存起来。因此,在真实的生产实践中,我们需要将 ReferenceConfig 对象存储到缓存中。否则,重复生成的 ReferenceConfig 可能造成性能问题并伴随着内存和连接泄漏。

除了 ReferenceConfig,其实 getInvoker 生成对象也可以进行缓存,缓存的 key 通常为接口名称、版本和注册中心。

那如果配置信息动态发生了变化,例如需要添加新的服务,这时候网关应用如何做到动态感知呢?我们通常可以用基于 MQ 的方式来解决这个问题。具体的解决方案如下:

中间件 - 案例:如何基于Dubbo进行网关设计?

也就是说,用户如果在网关运营平台上修改原有服务协议(Dubbo 服务)或者添加新的服务协议,变动后的协议会首先存储到数据库中,然后运营平台发送一条消息到 MQ,紧接着 Gateway 的后台进程以广播模式进行订阅。这样,所有后台网关进程都可以感知。

如果是对已有服务协议进行修改,在具体实践时有一个小细节请你一定注意。我们先看看这段代码:

Map<String /* 缓存key */,GenericService> invokerCache;

GenericService newInvoker = getInvoker(...);//参数省略

GenericService oldInvoker = invokerCache.get(key);

invokerCache.put(newInvoker);//先缓存新的invoker

// 然后再销毁旧的invoker对象

oldInvoker.destory();

如果已经存在对应的 Invoker 对象,为了不影响现有调用,应该先用新的 Invoker 对象去更新缓存,然后再销毁旧的 Invoker 对象。

上面的方法解决了网关调用公司内部的 Dubbo 微服务问题,但还有另外一个非常重要的问题,怎么配置服务接口相关参数呢?

联系这节课前面的场景,我们需要在页面上配置公司内部 Dubbo 服务与外部电商的接口映射。

中间件 - 案例:如何基于Dubbo进行网关设计?

为此,我们专门建立了一条参数映射协议:

中间件 - 案例:如何基于Dubbo进行网关设计?

参数映射设计的说明如下。

请求类型:主要分为请求参数与响应参数;

字段名称:Dubbo 服务对应的字段名称;

字段类型:Dubbo 服务对应字段的属性;

字段所属类:Dubbo 服务对应字段所属类型;

节点名称:外部请求接口对应的字段名称;

显示顺序:排序字段。

由于网关采取了泛化调用,在编写转换代码时,主要是遍历传入的参数,根据每一个字段查询对应的转换规则,然后转换为 Map,返回值则刚好相反,是将 Map 转换为 XML 或者 JSON。

在真正请求调用时,根据映射规则构建出请求参数 Map 后,通过 Dubbo 的泛化调用执行真正的调用:

GenericService genericService = (GenericService) invokeBean;

Map invokerPams;//省略转换过程

// 参数类型数组

String[] paramTypes = new String[1];

paramTypes[0]="java.util.Map";

// 参数值数组

Object[] paramValues = new Object[1];

​

invokerPams.put("class", "net.codingw.oms.vo.OrderItemVo");

paramValues[0] = invokerPams;

//由于我们已经转化为java.util.Map,并且Map中,需要有一个key为class的,表示服务端需要转化的类型,这个从协议转换器中获取

Object result = genericService.$invoke(this.getInvokeMethod(), paramTypes, paramValues);

这样,网关就具备了高扩展性和稳定性,可以非常灵活地支撑业务的扩展,为不同的电商平台配置不同的参数转换,从而在内部只需要开发一套接口就可以非常灵活地支撑业务的扩展,基本做到网关代码零修改。

总结

这节课,我通过一个真实的场景,详细介绍了网关设计的需求背景,然后针对网关设计的痛点给出了设计方案。通过对这个方案中关键代码的解读,你应该能够更加深刻地理解 Dubbo 泛化调用背后的逻辑,真正做到理论与实际相结合。

值得注意的是,我们这节课提到的转换协议也是一绝,它使用中括号来定义多层嵌套结构,使得该协议具有普适性。

这节课我们通过一个真实的业务场景来看看 Dubbo 网关(开放平台)的设计要领。

设计背景

要设计一个网关,我们首先要知道它的设计背景。

2017 年,我从传统行业脱身,正式进入物流行业。说来也非常巧,我当时加入的是公司的网关项目组,主要解决泛化调用与协议转换代码的开发问题。刚进公司不久,网关项目组就遇到了技术难题。快递物流行业的业务量可以比肩互联网,从那时候开始,我的传统技术思维开始向互联网技术思维转变。

当时网关项目组的核心任务就是确保能够快速接入各个电商平台。我来简单说明一下具体的场景。

中间件 - 案例:如何基于Dubbo进行网关设计?

解释一下上面这个图。

物流公司内部已经基于 Dubbo 构建了订单中心微服务域,其中创建订单接口的定义如下:​

中间件 - 案例:如何基于Dubbo进行网关设计?

外部电商平台众多,每一家电商平台内部都有自己的标准,并不会遵循统一的标准。例如在淘宝中,当用户购买商品后,淘宝内部会定义一个统一的订单外派接口。它的请求包可能是这样的:

{

"seller_id":189,

"buyer":"dingwei",

"order":[

{

"goods_name":"华为笔记本",

"num":1,

"price":500000

},

{

"goods_name":"华为手表",

"num":1,

"price":200000

}

]

}

但拼多多内部定义的订单外派接口,它的请求包可能是下面这样的:

<order>

<seller_uid>189</seller_uid>

<buyer_uid>dingwei</buyer_uid>

<order_items>

<order_item>

<goods_name>华为笔记本</goods_name>

<num>1</num>

<price>500000</price>

</order_item>

<order_item>

<goods_name>华为手表</goods_name>

<num>1</num>

<price>200000</price>

</order_item>

</order_items>

</order>

当电商的快递件占据快递公司总业务量的大半时,电商平台的话语权是高于快递公司的。也就是说,电商平台不管下游对接哪家物流公司,都会下发自己公司内部定义的订单派发接口,适配工作需要由物流公司自己来承担。

那站在物流公司的角度,应该怎么做呢?总不能每接入一个电商平台就为它们开发一套下单服务吧?那样的话,随着越来越多的电商平台接入,系统的复杂度会越来越高,可维护性将越来越差。

设计方案

正是在这样的背景下,网关平台被立项开发出来了。这个网关平台是怎么设计的呢?在设计的过程中需要解决哪些常见的问题?

我认为,网关的设计至少需要包括三个方面,分别是签名验证、服务配置和限流。

先说签名验证。保证请求的安全是系统设计需要优先考虑的。业界有一种非常经典的通信安全校验机制:验证签名。

这种机制的做法是,客户端与服务端会首先采用 HTTPS 进行通信,确保传输过程的私密性。

客户端在发送请求时,先将请求参数按参数名称进行排序,然后按顺序拼接成字符串,格式为 key1=a & key2=b。接下来,客户端使用一个约定的密钥对拼接出来的参数字符串进行签名,生成签名字符串(我们用 sign 表示签名字符串)并追加到 URL。通常,还会在 URL 中追加一个发送时间戳(时间戳不参与签名验证)。

服务端在接收到客户端的请求后,先从请求中解析出所有的参数,同样按照参数名对参数进行排序,然后使用同样的密钥对参数进行签名。得到的签名字符串需要与客户端计算的签名字符串进行对比,如果两者不同,则请求无效。与此同时,通常我们还需要将服务端当前的时间戳与客户端时间戳进行对比,如果相差超过一定的时间,同样认为请求无效,这个操作主要是为了避免使用同一个连接对网络进行连续攻击。

这整个过程里有一个非常重要的点,就是密钥自始至终并没有在网络上进行过传播,它的安全性可以得到十足的保证。签名验证的流程大概可以用下面这张图表示:

中间件 - 案例:如何基于Dubbo进行网关设计?

如果要对验证签名进行产品化设计,我们通常需要:

为不同的接入端(电商平台)创建不同的密钥,并通过安全的方式告知他们;

为不同的接入端(电商平台)配置签名算法。

在确保能够安全通信后,接下来就是网关设计最核心的部分了:服务接口配置化。它主要包括两个要点:微服务调用协议(Dubbo 服务描述)和接口定义与参数映射。

我们先来看一下微服务调用协议的配置,设计的原型界面如下图所示:

中间件 - 案例:如何基于Dubbo进行网关设计?

将所有的微服务(细化到方法级名称)维护到网关系统中,网关应用就可以使用 Dubbo 提供的编程 API,根据这些元信息动态构建一个个消费者(服务调用者),进而通过创建的服务调用客户端发起 RPC 远程调用,最终实现网关应用的 Dubbo 服务调用。

基于这些元信息构建消费者对象的关键代码如下:

public static GenericService getInvoker(String serviceInterface, String version, List<String> methods, int retry, String registryAddr ) {

ReferenceConfig referenceConfig = new ReferenceConfig();

// 关于消费者通用参数,可以从配置文件中获取,本示例取消

ConsumerConfig consumerConfig = new ConsumerConfig();

consumerConfig.setTimeout(3000);

consumerConfig.setRetries(2);

referenceConfig.setConsumer(consumerConfig);

//应用程序名称

ApplicationConfig applicationConfig = new ApplicationConfig();

applicationConfig.setName("GateWay");

referenceConfig.setApplication(applicationConfig);

// 注册中心

RegistryConfig registry = new RegistryConfig();

registry.setAddress(registryAddr);

registry.setProtocol("zookeeper");

referenceConfig.setRegistry(registry);

// 设置服务接口名称

referenceConfig.setInterface(serviceInterface);

// 设置服务版本

referenceConfig.setVersion(version);

referenceConfig.setMethods(new ArrayList<MethodConfig>());

for(String method : methods) {

MethodConfig methodConfig = new MethodConfig();

methodConfig.setName(method);

referenceConfig.getMethods().add(methodConfig);

}

referenceConfig.setGeneric("true");// 开启dubbo的泛化调用

return (GenericService) referenceConfig.get();

}

通过 getInvoker 方法发起调用远程 RPC 服务,这样,网关应用就成为了对应服务的消费者。

因为网关应用引入服务规约(API 包)不太现实,所以这里使用的是泛化调用,这样方便网关应用不受约束地构建消费者对象。

值得注意的是,ReferenceConfig 实例很重,它封装了与注册中心的连接以及所有服务提供者的连接,需要被缓存起来。因此,在真实的生产实践中,我们需要将 ReferenceConfig 对象存储到缓存中。否则,重复生成的 ReferenceConfig 可能造成性能问题并伴随着内存和连接泄漏。

除了 ReferenceConfig,其实 getInvoker 生成对象也可以进行缓存,缓存的 key 通常为接口名称、版本和注册中心。

那如果配置信息动态发生了变化,例如需要添加新的服务,这时候网关应用如何做到动态感知呢?我们通常可以用基于 MQ 的方式来解决这个问题。具体的解决方案如下:

中间件 - 案例:如何基于Dubbo进行网关设计?

也就是说,用户如果在网关运营平台上修改原有服务协议(Dubbo 服务)或者添加新的服务协议,变动后的协议会首先存储到数据库中,然后运营平台发送一条消息到 MQ,紧接着 Gateway 的后台进程以广播模式进行订阅。这样,所有后台网关进程都可以感知。

如果是对已有服务协议进行修改,在具体实践时有一个小细节请你一定注意。我们先看看这段代码:

Map<String /* 缓存key */,GenericService> invokerCache;

GenericService newInvoker = getInvoker(...);//参数省略

GenericService oldInvoker = invokerCache.get(key);

invokerCache.put(newInvoker);//先缓存新的invoker

// 然后再销毁旧的invoker对象

oldInvoker.destory();

如果已经存在对应的 Invoker 对象,为了不影响现有调用,应该先用新的 Invoker 对象去更新缓存,然后再销毁旧的 Invoker 对象。

上面的方法解决了网关调用公司内部的 Dubbo 微服务问题,但还有另外一个非常重要的问题,怎么配置服务接口相关参数呢?

联系这节课前面的场景,我们需要在页面上配置公司内部 Dubbo 服务与外部电商的接口映射。

中间件 - 案例:如何基于Dubbo进行网关设计?

为此,我们专门建立了一条参数映射协议:

中间件 - 案例:如何基于Dubbo进行网关设计?

参数映射设计的说明如下。

请求类型:主要分为请求参数与响应参数;

字段名称:Dubbo 服务对应的字段名称;

字段类型:Dubbo 服务对应字段的属性;

字段所属类:Dubbo 服务对应字段所属类型;

节点名称:外部请求接口对应的字段名称;

显示顺序:排序字段。

由于网关采取了泛化调用,在编写转换代码时,主要是遍历传入的参数,根据每一个字段查询对应的转换规则,然后转换为 Map,返回值则刚好相反,是将 Map 转换为 XML 或者 JSON。

在真正请求调用时,根据映射规则构建出请求参数 Map 后,通过 Dubbo 的泛化调用执行真正的调用:

GenericService genericService = (GenericService) invokeBean;

Map invokerPams;//省略转换过程

// 参数类型数组

String[] paramTypes = new String[1];

paramTypes[0]="java.util.Map";

// 参数值数组

Object[] paramValues = new Object[1];

​

invokerPams.put("class", "net.codingw.oms.vo.OrderItemVo");

paramValues[0] = invokerPams;

//由于我们已经转化为java.util.Map,并且Map中,需要有一个key为class的,表示服务端需要转化的类型,这个从协议转换器中获取

Object result = genericService.$invoke(this.getInvokeMethod(), paramTypes, paramValues);

这样,网关就具备了高扩展性和稳定性,可以非常灵活地支撑业务的扩展,为不同的电商平台配置不同的参数转换,从而在内部只需要开发一套接口就可以非常灵活地支撑业务的扩展,基本做到网关代码零修改。

总结

这节课,我通过一个真实的场景,详细介绍了网关设计的需求背景,然后针对网关设计的痛点给出了设计方案。通过对这个方案中关键代码的解读,你应该能够更加深刻地理解 Dubbo 泛化调用背后的逻辑,真正做到理论与实际相结合。

值得注意的是,我们这节课提到的转换协议也是一绝,它使用中括号来定义多层嵌套结构,使得该协议具有普适性。

这节课我们通过一个真实的业务场景来看看 Dubbo 网关(开放平台)的设计要领。

设计背景

要设计一个网关,我们首先要知道它的设计背景。

2017 年,我从传统行业脱身,正式进入物流行业。说来也非常巧,我当时加入的是公司的网关项目组,主要解决泛化调用与协议转换代码的开发问题。刚进公司不久,网关项目组就遇到了技术难题。快递物流行业的业务量可以比肩互联网,从那时候开始,我的传统技术思维开始向互联网技术思维转变。

当时网关项目组的核心任务就是确保能够快速接入各个电商平台。我来简单说明一下具体的场景。

中间件 - 案例:如何基于Dubbo进行网关设计?

解释一下上面这个图。

物流公司内部已经基于 Dubbo 构建了订单中心微服务域,其中创建订单接口的定义如下:​

中间件 - 案例:如何基于Dubbo进行网关设计?

外部电商平台众多,每一家电商平台内部都有自己的标准,并不会遵循统一的标准。例如在淘宝中,当用户购买商品后,淘宝内部会定义一个统一的订单外派接口。它的请求包可能是这样的:

{

"seller_id":189,

"buyer":"dingwei",

"order":[

{

"goods_name":"华为笔记本",

"num":1,

"price":500000

},

{

"goods_name":"华为手表",

"num":1,

"price":200000

}

]

}

但拼多多内部定义的订单外派接口,它的请求包可能是下面这样的:

<order>

<seller_uid>189</seller_uid>

<buyer_uid>dingwei</buyer_uid>

<order_items>

<order_item>

<goods_name>华为笔记本</goods_name>

<num>1</num>

<price>500000</price>

</order_item>

<order_item>

<goods_name>华为手表</goods_name>

<num>1</num>

<price>200000</price>

</order_item>

</order_items>

</order>

当电商的快递件占据快递公司总业务量的大半时,电商平台的话语权是高于快递公司的。也就是说,电商平台不管下游对接哪家物流公司,都会下发自己公司内部定义的订单派发接口,适配工作需要由物流公司自己来承担。

那站在物流公司的角度,应该怎么做呢?总不能每接入一个电商平台就为它们开发一套下单服务吧?那样的话,随着越来越多的电商平台接入,系统的复杂度会越来越高,可维护性将越来越差。

设计方案

正是在这样的背景下,网关平台被立项开发出来了。这个网关平台是怎么设计的呢?在设计的过程中需要解决哪些常见的问题?

我认为,网关的设计至少需要包括三个方面,分别是签名验证、服务配置和限流。

先说签名验证。保证请求的安全是系统设计需要优先考虑的。业界有一种非常经典的通信安全校验机制:验证签名。

这种机制的做法是,客户端与服务端会首先采用 HTTPS 进行通信,确保传输过程的私密性。

客户端在发送请求时,先将请求参数按参数名称进行排序,然后按顺序拼接成字符串,格式为 key1=a & key2=b。接下来,客户端使用一个约定的密钥对拼接出来的参数字符串进行签名,生成签名字符串(我们用 sign 表示签名字符串)并追加到 URL。通常,还会在 URL 中追加一个发送时间戳(时间戳不参与签名验证)。

服务端在接收到客户端的请求后,先从请求中解析出所有的参数,同样按照参数名对参数进行排序,然后使用同样的密钥对参数进行签名。得到的签名字符串需要与客户端计算的签名字符串进行对比,如果两者不同,则请求无效。与此同时,通常我们还需要将服务端当前的时间戳与客户端时间戳进行对比,如果相差超过一定的时间,同样认为请求无效,这个操作主要是为了避免使用同一个连接对网络进行连续攻击。

这整个过程里有一个非常重要的点,就是密钥自始至终并没有在网络上进行过传播,它的安全性可以得到十足的保证。签名验证的流程大概可以用下面这张图表示:

中间件 - 案例:如何基于Dubbo进行网关设计?

如果要对验证签名进行产品化设计,我们通常需要:

为不同的接入端(电商平台)创建不同的密钥,并通过安全的方式告知他们;

为不同的接入端(电商平台)配置签名算法。

在确保能够安全通信后,接下来就是网关设计最核心的部分了:服务接口配置化。它主要包括两个要点:微服务调用协议(Dubbo 服务描述)和接口定义与参数映射。

我们先来看一下微服务调用协议的配置,设计的原型界面如下图所示:

中间件 - 案例:如何基于Dubbo进行网关设计?

将所有的微服务(细化到方法级名称)维护到网关系统中,网关应用就可以使用 Dubbo 提供的编程 API,根据这些元信息动态构建一个个消费者(服务调用者),进而通过创建的服务调用客户端发起 RPC 远程调用,最终实现网关应用的 Dubbo 服务调用。

基于这些元信息构建消费者对象的关键代码如下:

public static GenericService getInvoker(String serviceInterface, String version, List<String> methods, int retry, String registryAddr ) {

ReferenceConfig referenceConfig = new ReferenceConfig();

// 关于消费者通用参数,可以从配置文件中获取,本示例取消

ConsumerConfig consumerConfig = new ConsumerConfig();

consumerConfig.setTimeout(3000);

consumerConfig.setRetries(2);

referenceConfig.setConsumer(consumerConfig);

//应用程序名称

ApplicationConfig applicationConfig = new ApplicationConfig();

applicationConfig.setName("GateWay");

referenceConfig.setApplication(applicationConfig);

// 注册中心

RegistryConfig registry = new RegistryConfig();

registry.setAddress(registryAddr);

registry.setProtocol("zookeeper");

referenceConfig.setRegistry(registry);

// 设置服务接口名称

referenceConfig.setInterface(serviceInterface);

// 设置服务版本

referenceConfig.setVersion(version);

referenceConfig.setMethods(new ArrayList<MethodConfig>());

for(String method : methods) {

MethodConfig methodConfig = new MethodConfig();

methodConfig.setName(method);

referenceConfig.getMethods().add(methodConfig);

}

referenceConfig.setGeneric("true");// 开启dubbo的泛化调用

return (GenericService) referenceConfig.get();

}

通过 getInvoker 方法发起调用远程 RPC 服务,这样,网关应用就成为了对应服务的消费者。

因为网关应用引入服务规约(API 包)不太现实,所以这里使用的是泛化调用,这样方便网关应用不受约束地构建消费者对象。

值得注意的是,ReferenceConfig 实例很重,它封装了与注册中心的连接以及所有服务提供者的连接,需要被缓存起来。因此,在真实的生产实践中,我们需要将 ReferenceConfig 对象存储到缓存中。否则,重复生成的 ReferenceConfig 可能造成性能问题并伴随着内存和连接泄漏。

除了 ReferenceConfig,其实 getInvoker 生成对象也可以进行缓存,缓存的 key 通常为接口名称、版本和注册中心。

那如果配置信息动态发生了变化,例如需要添加新的服务,这时候网关应用如何做到动态感知呢?我们通常可以用基于 MQ 的方式来解决这个问题。具体的解决方案如下:

中间件 - 案例:如何基于Dubbo进行网关设计?

也就是说,用户如果在网关运营平台上修改原有服务协议(Dubbo 服务)或者添加新的服务协议,变动后的协议会首先存储到数据库中,然后运营平台发送一条消息到 MQ,紧接着 Gateway 的后台进程以广播模式进行订阅。这样,所有后台网关进程都可以感知。

如果是对已有服务协议进行修改,在具体实践时有一个小细节请你一定注意。我们先看看这段代码:

Map<String /* 缓存key */,GenericService> invokerCache;

GenericService newInvoker = getInvoker(...);//参数省略

GenericService oldInvoker = invokerCache.get(key);

invokerCache.put(newInvoker);//先缓存新的invoker

// 然后再销毁旧的invoker对象

oldInvoker.destory();

如果已经存在对应的 Invoker 对象,为了不影响现有调用,应该先用新的 Invoker 对象去更新缓存,然后再销毁旧的 Invoker 对象。

上面的方法解决了网关调用公司内部的 Dubbo 微服务问题,但还有另外一个非常重要的问题,怎么配置服务接口相关参数呢?

联系这节课前面的场景,我们需要在页面上配置公司内部 Dubbo 服务与外部电商的接口映射。

中间件 - 案例:如何基于Dubbo进行网关设计?

为此,我们专门建立了一条参数映射协议:

中间件 - 案例:如何基于Dubbo进行网关设计?

参数映射设计的说明如下。

请求类型:主要分为请求参数与响应参数;

字段名称:Dubbo 服务对应的字段名称;

字段类型:Dubbo 服务对应字段的属性;

字段所属类:Dubbo 服务对应字段所属类型;

节点名称:外部请求接口对应的字段名称;

显示顺序:排序字段。

由于网关采取了泛化调用,在编写转换代码时,主要是遍历传入的参数,根据每一个字段查询对应的转换规则,然后转换为 Map,返回值则刚好相反,是将 Map 转换为 XML 或者 JSON。

在真正请求调用时,根据映射规则构建出请求参数 Map 后,通过 Dubbo 的泛化调用执行真正的调用:

GenericService genericService = (GenericService) invokeBean;

Map invokerPams;//省略转换过程

// 参数类型数组

String[] paramTypes = new String[1];

paramTypes[0]="java.util.Map";

// 参数值数组

Object[] paramValues = new Object[1];

​

invokerPams.put("class", "net.codingw.oms.vo.OrderItemVo");

paramValues[0] = invokerPams;

//由于我们已经转化为java.util.Map,并且Map中,需要有一个key为class的,表示服务端需要转化的类型,这个从协议转换器中获取

Object result = genericService.$invoke(this.getInvokeMethod(), paramTypes, paramValues);

这样,网关就具备了高扩展性和稳定性,可以非常灵活地支撑业务的扩展,为不同的电商平台配置不同的参数转换,从而在内部只需要开发一套接口就可以非常灵活地支撑业务的扩展,基本做到网关代码零修改。

总结

这节课,我通过一个真实的场景,详细介绍了网关设计的需求背景,然后针对网关设计的痛点给出了设计方案。通过对这个方案中关键代码的解读,你应该能够更加深刻地理解 Dubbo 泛化调用背后的逻辑,真正做到理论与实际相结合。

值得注意的是,我们这节课提到的转换协议也是一绝,它使用中括号来定义多层嵌套结构,使得该协议具有普适性。

这节课我们通过一个真实的业务场景来看看 Dubbo 网关(开放平台)的设计要领。

设计背景

要设计一个网关,我们首先要知道它的设计背景。

2017 年,我从传统行业脱身,正式进入物流行业。说来也非常巧,我当时加入的是公司的网关项目组,主要解决泛化调用与协议转换代码的开发问题。刚进公司不久,网关项目组就遇到了技术难题。快递物流行业的业务量可以比肩互联网,从那时候开始,我的传统技术思维开始向互联网技术思维转变。

当时网关项目组的核心任务就是确保能够快速接入各个电商平台。我来简单说明一下具体的场景。

中间件 - 案例:如何基于Dubbo进行网关设计?

解释一下上面这个图。

物流公司内部已经基于 Dubbo 构建了订单中心微服务域,其中创建订单接口的定义如下:​

中间件 - 案例:如何基于Dubbo进行网关设计?

外部电商平台众多,每一家电商平台内部都有自己的标准,并不会遵循统一的标准。例如在淘宝中,当用户购买商品后,淘宝内部会定义一个统一的订单外派接口。它的请求包可能是这样的:

{

"seller_id":189,

"buyer":"dingwei",

"order":[

{

"goods_name":"华为笔记本",

"num":1,

"price":500000

},

{

"goods_name":"华为手表",

"num":1,

"price":200000

}

]

}

但拼多多内部定义的订单外派接口,它的请求包可能是下面这样的:

<order>

<seller_uid>189</seller_uid>

<buyer_uid>dingwei</buyer_uid>

<order_items>

<order_item>

<goods_name>华为笔记本</goods_name>

<num>1</num>

<price>500000</price>

</order_item>

<order_item>

<goods_name>华为手表</goods_name>

<num>1</num>

<price>200000</price>

</order_item>

</order_items>

</order>

当电商的快递件占据快递公司总业务量的大半时,电商平台的话语权是高于快递公司的。也就是说,电商平台不管下游对接哪家物流公司,都会下发自己公司内部定义的订单派发接口,适配工作需要由物流公司自己来承担。

那站在物流公司的角度,应该怎么做呢?总不能每接入一个电商平台就为它们开发一套下单服务吧?那样的话,随着越来越多的电商平台接入,系统的复杂度会越来越高,可维护性将越来越差。

设计方案

正是在这样的背景下,网关平台被立项开发出来了。这个网关平台是怎么设计的呢?在设计的过程中需要解决哪些常见的问题?

我认为,网关的设计至少需要包括三个方面,分别是签名验证、服务配置和限流。

先说签名验证。保证请求的安全是系统设计需要优先考虑的。业界有一种非常经典的通信安全校验机制:验证签名。

这种机制的做法是,客户端与服务端会首先采用 HTTPS 进行通信,确保传输过程的私密性。

客户端在发送请求时,先将请求参数按参数名称进行排序,然后按顺序拼接成字符串,格式为 key1=a & key2=b。接下来,客户端使用一个约定的密钥对拼接出来的参数字符串进行签名,生成签名字符串(我们用 sign 表示签名字符串)并追加到 URL。通常,还会在 URL 中追加一个发送时间戳(时间戳不参与签名验证)。

服务端在接收到客户端的请求后,先从请求中解析出所有的参数,同样按照参数名对参数进行排序,然后使用同样的密钥对参数进行签名。得到的签名字符串需要与客户端计算的签名字符串进行对比,如果两者不同,则请求无效。与此同时,通常我们还需要将服务端当前的时间戳与客户端时间戳进行对比,如果相差超过一定的时间,同样认为请求无效,这个操作主要是为了避免使用同一个连接对网络进行连续攻击。

这整个过程里有一个非常重要的点,就是密钥自始至终并没有在网络上进行过传播,它的安全性可以得到十足的保证。签名验证的流程大概可以用下面这张图表示:

中间件 - 案例:如何基于Dubbo进行网关设计?

如果要对验证签名进行产品化设计,我们通常需要:

为不同的接入端(电商平台)创建不同的密钥,并通过安全的方式告知他们;

为不同的接入端(电商平台)配置签名算法。

在确保能够安全通信后,接下来就是网关设计最核心的部分了:服务接口配置化。它主要包括两个要点:微服务调用协议(Dubbo 服务描述)和接口定义与参数映射。

我们先来看一下微服务调用协议的配置,设计的原型界面如下图所示:

中间件 - 案例:如何基于Dubbo进行网关设计?

将所有的微服务(细化到方法级名称)维护到网关系统中,网关应用就可以使用 Dubbo 提供的编程 API,根据这些元信息动态构建一个个消费者(服务调用者),进而通过创建的服务调用客户端发起 RPC 远程调用,最终实现网关应用的 Dubbo 服务调用。

基于这些元信息构建消费者对象的关键代码如下:

public static GenericService getInvoker(String serviceInterface, String version, List<String> methods, int retry, String registryAddr ) {

ReferenceConfig referenceConfig = new ReferenceConfig();

// 关于消费者通用参数,可以从配置文件中获取,本示例取消

ConsumerConfig consumerConfig = new ConsumerConfig();

consumerConfig.setTimeout(3000);

consumerConfig.setRetries(2);

referenceConfig.setConsumer(consumerConfig);

//应用程序名称

ApplicationConfig applicationConfig = new ApplicationConfig();

applicationConfig.setName("GateWay");

referenceConfig.setApplication(applicationConfig);

// 注册中心

RegistryConfig registry = new RegistryConfig();

registry.setAddress(registryAddr);

registry.setProtocol("zookeeper");

referenceConfig.setRegistry(registry);

// 设置服务接口名称

referenceConfig.setInterface(serviceInterface);

// 设置服务版本

referenceConfig.setVersion(version);

referenceConfig.setMethods(new ArrayList<MethodConfig>());

for(String method : methods) {

MethodConfig methodConfig = new MethodConfig();

methodConfig.setName(method);

referenceConfig.getMethods().add(methodConfig);

}

referenceConfig.setGeneric("true");// 开启dubbo的泛化调用

return (GenericService) referenceConfig.get();

}

通过 getInvoker 方法发起调用远程 RPC 服务,这样,网关应用就成为了对应服务的消费者。

因为网关应用引入服务规约(API 包)不太现实,所以这里使用的是泛化调用,这样方便网关应用不受约束地构建消费者对象。

值得注意的是,ReferenceConfig 实例很重,它封装了与注册中心的连接以及所有服务提供者的连接,需要被缓存起来。因此,在真实的生产实践中,我们需要将 ReferenceConfig 对象存储到缓存中。否则,重复生成的 ReferenceConfig 可能造成性能问题并伴随着内存和连接泄漏。

除了 ReferenceConfig,其实 getInvoker 生成对象也可以进行缓存,缓存的 key 通常为接口名称、版本和注册中心。

那如果配置信息动态发生了变化,例如需要添加新的服务,这时候网关应用如何做到动态感知呢?我们通常可以用基于 MQ 的方式来解决这个问题。具体的解决方案如下:

中间件 - 案例:如何基于Dubbo进行网关设计?

也就是说,用户如果在网关运营平台上修改原有服务协议(Dubbo 服务)或者添加新的服务协议,变动后的协议会首先存储到数据库中,然后运营平台发送一条消息到 MQ,紧接着 Gateway 的后台进程以广播模式进行订阅。这样,所有后台网关进程都可以感知。

如果是对已有服务协议进行修改,在具体实践时有一个小细节请你一定注意。我们先看看这段代码:

Map<String /* 缓存key */,GenericService> invokerCache;

GenericService newInvoker = getInvoker(...);//参数省略

GenericService oldInvoker = invokerCache.get(key);

invokerCache.put(newInvoker);//先缓存新的invoker

// 然后再销毁旧的invoker对象

oldInvoker.destory();

如果已经存在对应的 Invoker 对象,为了不影响现有调用,应该先用新的 Invoker 对象去更新缓存,然后再销毁旧的 Invoker 对象。

上面的方法解决了网关调用公司内部的 Dubbo 微服务问题,但还有另外一个非常重要的问题,怎么配置服务接口相关参数呢?

联系这节课前面的场景,我们需要在页面上配置公司内部 Dubbo 服务与外部电商的接口映射。

中间件 - 案例:如何基于Dubbo进行网关设计?

为此,我们专门建立了一条参数映射协议:

中间件 - 案例:如何基于Dubbo进行网关设计?

参数映射设计的说明如下。

请求类型:主要分为请求参数与响应参数;

字段名称:Dubbo 服务对应的字段名称;

字段类型:Dubbo 服务对应字段的属性;

字段所属类:Dubbo 服务对应字段所属类型;

节点名称:外部请求接口对应的字段名称;

显示顺序:排序字段。

由于网关采取了泛化调用,在编写转换代码时,主要是遍历传入的参数,根据每一个字段查询对应的转换规则,然后转换为 Map,返回值则刚好相反,是将 Map 转换为 XML 或者 JSON。

在真正请求调用时,根据映射规则构建出请求参数 Map 后,通过 Dubbo 的泛化调用执行真正的调用:

GenericService genericService = (GenericService) invokeBean;

Map invokerPams;//省略转换过程

// 参数类型数组

String[] paramTypes = new String[1];

paramTypes[0]="java.util.Map";

// 参数值数组

Object[] paramValues = new Object[1];

​

invokerPams.put("class", "net.codingw.oms.vo.OrderItemVo");

paramValues[0] = invokerPams;

//由于我们已经转化为java.util.Map,并且Map中,需要有一个key为class的,表示服务端需要转化的类型,这个从协议转换器中获取

Object result = genericService.$invoke(this.getInvokeMethod(), paramTypes, paramValues);

这样,网关就具备了高扩展性和稳定性,可以非常灵活地支撑业务的扩展,为不同的电商平台配置不同的参数转换,从而在内部只需要开发一套接口就可以非常灵活地支撑业务的扩展,基本做到网关代码零修改。

总结

这节课,我通过一个真实的场景,详细介绍了网关设计的需求背景,然后针对网关设计的痛点给出了设计方案。通过对这个方案中关键代码的解读,你应该能够更加深刻地理解 Dubbo 泛化调用背后的逻辑,真正做到理论与实际相结合。

值得注意的是,我们这节课提到的转换协议也是一绝,它使用中括号来定义多层嵌套结构,使得该协议具有普适性。

这节课我们通过一个真实的业务场景来看看 Dubbo 网关(开放平台)的设计要领。

设计背景

要设计一个网关,我们首先要知道它的设计背景。

2017 年,我从传统行业脱身,正式进入物流行业。说来也非常巧,我当时加入的是公司的网关项目组,主要解决泛化调用与协议转换代码的开发问题。刚进公司不久,网关项目组就遇到了技术难题。快递物流行业的业务量可以比肩互联网,从那时候开始,我的传统技术思维开始向互联网技术思维转变。

当时网关项目组的核心任务就是确保能够快速接入各个电商平台。我来简单说明一下具体的场景。

中间件 - 案例:如何基于Dubbo进行网关设计?

解释一下上面这个图。

物流公司内部已经基于 Dubbo 构建了订单中心微服务域,其中创建订单接口的定义如下:​

中间件 - 案例:如何基于Dubbo进行网关设计?

外部电商平台众多,每一家电商平台内部都有自己的标准,并不会遵循统一的标准。例如在淘宝中,当用户购买商品后,淘宝内部会定义一个统一的订单外派接口。它的请求包可能是这样的:

{

"seller_id":189,

"buyer":"dingwei",

"order":[

{

"goods_name":"华为笔记本",

"num":1,

"price":500000

},

{

"goods_name":"华为手表",

"num":1,

"price":200000

}

]

}

但拼多多内部定义的订单外派接口,它的请求包可能是下面这样的:

<order>

<seller_uid>189</seller_uid>

<buyer_uid>dingwei</buyer_uid>

<order_items>

<order_item>

<goods_name>华为笔记本</goods_name>

<num>1</num>

<price>500000</price>

</order_item>

<order_item>

<goods_name>华为手表</goods_name>

<num>1</num>

<price>200000</price>

</order_item>

</order_items>

</order>

当电商的快递件占据快递公司总业务量的大半时,电商平台的话语权是高于快递公司的。也就是说,电商平台不管下游对接哪家物流公司,都会下发自己公司内部定义的订单派发接口,适配工作需要由物流公司自己来承担。

那站在物流公司的角度,应该怎么做呢?总不能每接入一个电商平台就为它们开发一套下单服务吧?那样的话,随着越来越多的电商平台接入,系统的复杂度会越来越高,可维护性将越来越差。

设计方案

正是在这样的背景下,网关平台被立项开发出来了。这个网关平台是怎么设计的呢?在设计的过程中需要解决哪些常见的问题?

我认为,网关的设计至少需要包括三个方面,分别是签名验证、服务配置和限流。

先说签名验证。保证请求的安全是系统设计需要优先考虑的。业界有一种非常经典的通信安全校验机制:验证签名。

这种机制的做法是,客户端与服务端会首先采用 HTTPS 进行通信,确保传输过程的私密性。

客户端在发送请求时,先将请求参数按参数名称进行排序,然后按顺序拼接成字符串,格式为 key1=a & key2=b。接下来,客户端使用一个约定的密钥对拼接出来的参数字符串进行签名,生成签名字符串(我们用 sign 表示签名字符串)并追加到 URL。通常,还会在 URL 中追加一个发送时间戳(时间戳不参与签名验证)。

服务端在接收到客户端的请求后,先从请求中解析出所有的参数,同样按照参数名对参数进行排序,然后使用同样的密钥对参数进行签名。得到的签名字符串需要与客户端计算的签名字符串进行对比,如果两者不同,则请求无效。与此同时,通常我们还需要将服务端当前的时间戳与客户端时间戳进行对比,如果相差超过一定的时间,同样认为请求无效,这个操作主要是为了避免使用同一个连接对网络进行连续攻击。

这整个过程里有一个非常重要的点,就是密钥自始至终并没有在网络上进行过传播,它的安全性可以得到十足的保证。签名验证的流程大概可以用下面这张图表示:

中间件 - 案例:如何基于Dubbo进行网关设计?

如果要对验证签名进行产品化设计,我们通常需要:

为不同的接入端(电商平台)创建不同的密钥,并通过安全的方式告知他们;

为不同的接入端(电商平台)配置签名算法。

在确保能够安全通信后,接下来就是网关设计最核心的部分了:服务接口配置化。它主要包括两个要点:微服务调用协议(Dubbo 服务描述)和接口定义与参数映射。

我们先来看一下微服务调用协议的配置,设计的原型界面如下图所示:

中间件 - 案例:如何基于Dubbo进行网关设计?

将所有的微服务(细化到方法级名称)维护到网关系统中,网关应用就可以使用 Dubbo 提供的编程 API,根据这些元信息动态构建一个个消费者(服务调用者),进而通过创建的服务调用客户端发起 RPC 远程调用,最终实现网关应用的 Dubbo 服务调用。

基于这些元信息构建消费者对象的关键代码如下:

public static GenericService getInvoker(String serviceInterface, String version, List<String> methods, int retry, String registryAddr ) {

ReferenceConfig referenceConfig = new ReferenceConfig();

// 关于消费者通用参数,可以从配置文件中获取,本示例取消

ConsumerConfig consumerConfig = new ConsumerConfig();

consumerConfig.setTimeout(3000);

consumerConfig.setRetries(2);

referenceConfig.setConsumer(consumerConfig);

//应用程序名称

ApplicationConfig applicationConfig = new ApplicationConfig();

applicationConfig.setName("GateWay");

referenceConfig.setApplication(applicationConfig);

// 注册中心

RegistryConfig registry = new RegistryConfig();

registry.setAddress(registryAddr);

registry.setProtocol("zookeeper");

referenceConfig.setRegistry(registry);

// 设置服务接口名称

referenceConfig.setInterface(serviceInterface);

// 设置服务版本

referenceConfig.setVersion(version);

referenceConfig.setMethods(new ArrayList<MethodConfig>());

for(String method : methods) {

MethodConfig methodConfig = new MethodConfig();

methodConfig.setName(method);

referenceConfig.getMethods().add(methodConfig);

}

referenceConfig.setGeneric("true");// 开启dubbo的泛化调用

return (GenericService) referenceConfig.get();

}

通过 getInvoker 方法发起调用远程 RPC 服务,这样,网关应用就成为了对应服务的消费者。

因为网关应用引入服务规约(API 包)不太现实,所以这里使用的是泛化调用,这样方便网关应用不受约束地构建消费者对象。

值得注意的是,ReferenceConfig 实例很重,它封装了与注册中心的连接以及所有服务提供者的连接,需要被缓存起来。因此,在真实的生产实践中,我们需要将 ReferenceConfig 对象存储到缓存中。否则,重复生成的 ReferenceConfig 可能造成性能问题并伴随着内存和连接泄漏。

除了 ReferenceConfig,其实 getInvoker 生成对象也可以进行缓存,缓存的 key 通常为接口名称、版本和注册中心。

那如果配置信息动态发生了变化,例如需要添加新的服务,这时候网关应用如何做到动态感知呢?我们通常可以用基于 MQ 的方式来解决这个问题。具体的解决方案如下:

中间件 - 案例:如何基于Dubbo进行网关设计?

也就是说,用户如果在网关运营平台上修改原有服务协议(Dubbo 服务)或者添加新的服务协议,变动后的协议会首先存储到数据库中,然后运营平台发送一条消息到 MQ,紧接着 Gateway 的后台进程以广播模式进行订阅。这样,所有后台网关进程都可以感知。

如果是对已有服务协议进行修改,在具体实践时有一个小细节请你一定注意。我们先看看这段代码:

Map<String /* 缓存key */,GenericService> invokerCache;

GenericService newInvoker = getInvoker(...);//参数省略

GenericService oldInvoker = invokerCache.get(key);

invokerCache.put(newInvoker);//先缓存新的invoker

// 然后再销毁旧的invoker对象

oldInvoker.destory();

如果已经存在对应的 Invoker 对象,为了不影响现有调用,应该先用新的 Invoker 对象去更新缓存,然后再销毁旧的 Invoker 对象。

上面的方法解决了网关调用公司内部的 Dubbo 微服务问题,但还有另外一个非常重要的问题,怎么配置服务接口相关参数呢?

联系这节课前面的场景,我们需要在页面上配置公司内部 Dubbo 服务与外部电商的接口映射。

中间件 - 案例:如何基于Dubbo进行网关设计?

为此,我们专门建立了一条参数映射协议:

中间件 - 案例:如何基于Dubbo进行网关设计?

参数映射设计的说明如下。

请求类型:主要分为请求参数与响应参数;

字段名称:Dubbo 服务对应的字段名称;

字段类型:Dubbo 服务对应字段的属性;

字段所属类:Dubbo 服务对应字段所属类型;

节点名称:外部请求接口对应的字段名称;

显示顺序:排序字段。

由于网关采取了泛化调用,在编写转换代码时,主要是遍历传入的参数,根据每一个字段查询对应的转换规则,然后转换为 Map,返回值则刚好相反,是将 Map 转换为 XML 或者 JSON。

在真正请求调用时,根据映射规则构建出请求参数 Map 后,通过 Dubbo 的泛化调用执行真正的调用:

GenericService genericService = (GenericService) invokeBean;

Map invokerPams;//省略转换过程

// 参数类型数组

String[] paramTypes = new String[1];

paramTypes[0]="java.util.Map";

// 参数值数组

Object[] paramValues = new Object[1];

​

invokerPams.put("class", "net.codingw.oms.vo.OrderItemVo");

paramValues[0] = invokerPams;

//由于我们已经转化为java.util.Map,并且Map中,需要有一个key为class的,表示服务端需要转化的类型,这个从协议转换器中获取

Object result = genericService.$invoke(this.getInvokeMethod(), paramTypes, paramValues);

这样,网关就具备了高扩展性和稳定性,可以非常灵活地支撑业务的扩展,为不同的电商平台配置不同的参数转换,从而在内部只需要开发一套接口就可以非常灵活地支撑业务的扩展,基本做到网关代码零修改。

总结

这节课,我通过一个真实的场景,详细介绍了网关设计的需求背景,然后针对网关设计的痛点给出了设计方案。通过对这个方案中关键代码的解读,你应该能够更加深刻地理解 Dubbo 泛化调用背后的逻辑,真正做到理论与实际相结合。

值得注意的是,我们这节课提到的转换协议也是一绝,它使用中括号来定义多层嵌套结构,使得该协议具有普适性。