0%

RPC架构层实现灰度和AOP

微服务架构体系首先要解决系统间连通性的问题。有两大流派:

  • 基于RPC: 比如Apache Thrift,阿里Dubbo等等。
  • 基于REST: 基于HTTP的RESTful,借助HTTP/1.1的持久链接和HTTP/2.0的多路复用,已经完全适合server-to-server的高频次、低时延、少连接的通信场景,不输给二进制协议的RPC。但其方便性、跨平台型和生态性则远超RPC。这也是诸如Java领域的SpringCloud发展迅速的重要原因。

不过大型互联网公司,绝大多数的历史系统已经是基于RPC的,开发者和运维都轻车熟路,重新更换代价很高,必要性当前看也不大。但使用过程中,RPC有个很大的痛点,就是无法在架构层面实现系统间的AOP机制

大家了解Nginx这种代理模式,本质上就是架构层面的AOP机制。我们可以在Nginx上做流量分发(流量分发机制可演变出负载均衡和灰度发布等应用场景)、做认证授权、做缓存加速、甚至做报文改写(这要求代理层不仅工作在HTTP协议头层,还需要工作在协议体层,比如开源的jsproxy应用)。但是RPC框架,只在服务发现时,客户端跟第三方PRC注册中心通信,之后都是客户端与服务提供方的Direct Access,而不像Nginx模式下,客户端与服务提供方之间的每个请求都需要经过Nginx Proxy,减少了中间环节,提升了响应时延,但同时损失了架构层的AOP机制。

下面我们以AOP的一种应用场景——灰度发布,来做个简要分析。

灰度发布的枢纽

如下图所示,灰度发布的枢纽是分流器——无论实现方式如何,逻辑上需要在路由到现有代码或灰度代码前,有个决策开关。

image-20190420174912084

通常来说,一个分流器需要包括两个方面的内容:

  • 分流器决策因素:决策因素是业务相关的,往往不可预知。当然也万变不离其中,从数学上可以抽象为请求对象各属性的一个函数关系。比如大家常见的白名单,就是请求对象关联的账号属性隶属于某个集合的函数关系。更复杂一点的,黑区白名单,请求对象的地域属性不在某个集合,并且账号属性在某个集合的函数关系。再演变更复杂的决策就可以用到基于Rete算法的规则引擎。
  • 分流器植入点位:植入点位指的是分流器工作在上下游信息链路的哪个环节。大体可以总结为3种:
    • 反向代理: 典型的就是HTTP应用,往往在类似Nginx的反向代理层做分流。
    • 应用分支: 在业务代码内,同一个进程,用两个分支实现。既可以硬编码,也可以基于类似Spring框架的AOP。
    • 应用转发: 在业务代码内,现有代码和灰度代码独立部署,运行在不同的进程内,然后在应用层转发请求响应。

我们先看Nginx代理层模式和RPC应用层模式对分流器的具体实现,再看如何在RPC架构层(而不是应用层)实现分流器。

Nginx代理层模式

image-20190627114301418

如上图所示,HTTP服务的分流通常在反向代理Nginx上做,跟业务逻辑是分离的。与此同时,Nginx支持Lua脚本,可以实现定制化的流量分发策略(就是前面说的分流器决策因素),比如依据账号白名单和地区白名单等。

RPC应用层模式

image-20190807141242862

上图刻画了两个RPC服务:一个是JPService,是现有运行在生产环境下的老版本;另一个是WTService,是重构的新版本,即将上线,但为了降低风险,需要先在预发环境进行灰度发布。为了充分说明问题,我们可以让两者的方法签名都不一样(通常我们重构是不改变方法签名的)。

  • JPService描述(生产版本):
1
2
3
interface JPService {
JPResponse doService(JPRequest r);
}

它的服务端实现是JPServiceSupply,客户端由RPC框架自动生成代码,叫JPServiceDemand。

  • WTService描述(预发版本):
    1
    2
    3
    interface WTService {
    WTResponse doService(WTRequest r);
    }

它的服务端实现是WTServiceSupply,客户端由RPC框架自动生成代码,叫WTServiceDemand。

如何实现灰度发布呢?需要在JPService服务端向WTService服务端按一定规则导流。依据开闭原则单一职责原则,设计JPServiceGrayProxy负责流量分发,而由JPServiceWTAdaptor负责异构服务转换,最终做到不修改已有的JPServiceSupply代码完成灰度发布。

  • JPServiceGrayProxy伪代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class JPServiceGrayProxy implements JPService {

private JPService jpServiceAdaptor = new JPServiceWTAdaptor();
private JPService jpServiceSupply = new JPServiceSupply();

public JPResponse doService(JPRequest jpReq) {
if (jpReq.账号 in {白名单} ) {
return jpServiceAdaptor.doService(jpReq);
} else {
return jpServiceSupply.doService(jpReq);
}
}

}
  • JPServiceWTAdaptor伪代码
1
2
3
4
5
6
7
8
9
10
11
12
class JPServiceWTAdaptor implements JPService {

private WTService wtService = new WTServiceDemond();

public JPResponse doService(JPRequest jpReq) {
WTRequest wtReq = convert(jpReq);
WTResponse wtRes = wtService.doService(wtReq);
JPResponse jpRes = convert(wtRes);
return jpRes;
}

}

尽管上述代码结构清晰,但是依然很繁琐,因为这仅仅是一个灰度接口,如果有10个,100个呢?每个都要开发。于是我们思考,能不能RPC架构层解决呢,做到类似Nginx的彻底跟业务逻辑分离。

RPC架构层模式

在RPC架构层实现分流器,有两种可行的方式,分别叫RPC-Applet模式和RPC-Proxy模式。

RPC-Applet模式

一个RPC服务端实例用 (S, V, I) 三元组表示。分别指Service服务名,Version服务版本和 Instance服务实例。

  • Service: 相同的Service,表示同一类功能集合;
  • Version: 不同的Version 表示相同服务的不同实现,比如线上版本和灰度版本;
  • Instance: 服务运行的进程。用于多主机部署,实现流量负载均衡。

image-20190627122642153

上图最重要的是在服务发现阶段(包括变更推送),引入了Version Selector Injection机制。通过注册中心将分流器策略推送到客户端,客户端需要增加一个运行时来执行这段脚本。这非常类似Java早年在浏览器上支持的Applet,所以姑且把这个名字叫AppletRuntime

RPC-Proxy模式

类似Nginx之于HTTP。只是当服务注册时,ProxyVersion作为保留字,客户端进行服务发现时,只返回ProxyVersion,然后由ProxyVersion在服务端分发给Version#AVersion#B

image-20190627133041068