三方服务管理框架(Cube)的设计及应用

本文主要讨论一个面向C端用户的大型服务中,核心服务和非核心服务的交互管理,以及服务管理框架(Cube)的设计和实现。

本文主要包括以下内容:

  • Cube的设计背景和目标
  • Cube的设计和实现要点
  • Cube的使用方式和实际场景

Cube的取名来源于Cube这部科幻电影,电影的内容主要讲述了几个被困在巨大魔方中的人,魔方中的房子可以随意被组合,里面的人却始终逃不出去。

如是将Cube比作服务的管理框架,被Cube纳入管理的服务如同魔方中的房子,可以被随意组合编排管理,而服务的组合、监控、流量控制、降级、日志、并行化均由Cube管理。

1、背景


物流详情的查询页面承载了大量的查询流量,因此很多非物流详情的业务方希望在这个流量洼地进行他们业务透出,如下图中的红框部分。

1

过多业务的透出,不同端的设计不一致,缺乏统一的产品规划、产品的频繁试错和迭代导致这个页面显得杂乱无章,这是产品上的问题,而在技术实现方面则会有以下问题:

  • 查询过程中核心和非核心链路的紧密耦合,现阶段大部分三方服务都在物流详情查询的核心链路上增加一段代码,并附带增加降级、端可见性判断、角色判断等代码。
  • 开发效率和排查问题效率低下,新增一个三方服务,物流详情开发都需要介入,和前端约定字段,和三方服务提供方约定接口,开发测试联调发布。而在出现问题的时候,总是第一时间反馈到物流详情服务端,服务端同学先排查自己的代码,在确认不是自己的问题之后,再将问题转交给对应的服务方。
  • 性能下降,所有三方服务串行调用,各个服务的rt消耗将增加到核心查询接口的rt中,服务越多性能影响越明显,某个接口的性能抖动还会直接影响到核心接口的稳定。
  • 代码腐朽,维护性差,由于缺乏统一的规划,开发添加代码显得很随意,特别是会在物流详情查询返回的核心对象中随意增加非核心的属性字段,其中关于异常信息的字段就有三个,或者将返回对象作为map的value从而导致其他服务的序列化异常,长此以往历史包袱越来越重,维护和开发效率低下。

针对以上问题,我们希望能有一套方案来统一进行解决,以期望达到以下目的:

  • 查询过程中核心服务和非核心服务代码解耦,新增加三方服务不需要改动核心查询链路。
  • 核心服务开发不再介入新增服务的开发和排查过程,对非核心三方服务的管理通过配置来完成,前端和三方服务提供方约定数据格式,而核心服务只负责将数据透传到前端,退化为一个数据通道。同时希望有一定的动态配置能力,实现不用重启核心服务完成新三方服务的添加,进一步提升开发和维护效率。
  • 分析三方服务的依赖性,并行调用没有依赖关系的接口,提升接口性能。
  • 规范三方服务的返回值类型(json类型)以及在核心对象中的属性存放位置(map),并通过框架完成,杜绝人为添加的可能性。
  • 此外还希望对三方服务的管理能提供统一的降级、流量控制、日志、服务状态监控等稳定性保障能力。

2、Cube的设计


当前核心服务和非核心服务的交互图如下:


交互链路

Cube加入之后查询链路中核心服务和非核心服务的交互图如下:


Cube交互链路

Cube的设计用于隔离和管理非核心服务,并希望提供一定的服务动态化配置能力,因此在设计之初就对服务的定义有过讨论,定下了几条设计原则。

2.1 设计原则

服务接入的设计,尽量做到服务声明清晰,服务之间组合灵活,整体上低耦合、高内聚,因此服务的接入需要遵循以下原则:

  • 基础服务的粒度:基础服务的粒度切分到具体的方法,基础服务的声明和对外提供的能力是清晰的,一个三方服务提供的接口中的多个方法会作为多个基础服务对待。

  • 组合服务的调用:组合服务的调用方式现阶段只支持基本服务的顺序链式执行,暂不支持基础服务的复杂组合调用方式。对于无法通过组合基础服务来实现的复杂逻辑,需要把负责逻辑内聚到一个基础服务(方法)中。

  • 服务治理的框架化:整体设计上尽量框架化,框架不关心服务的内部逻辑,只聚焦服务的管理、组合、调用,通过基础服务粒度的划分,服务的组合来解除服务之间的耦合度。

2.2 设计类图

设计类图

整体设计上主要分为7大块:

  • SrvBean:服务定义的实体类,包括SingleSrvBean、ChainSrvBean。
  • SrvBeanXmlReader:SrvBean的xml定义的解析类,包括SingleSrvBeanXmlReader、ChainSrvBeanXmlReader,负责加载定义好的xml文件,解析并通过SrvBeanReaderUtil生成具体的SrvBean实体类。
  • SrvBeanFactory:SrvBean的工厂,负责加载SrvBeanXmlReader,保存从SrvBeanReaderUtil生成的SrvBean实体类,负责对外根据srvId来获取具体的SrvBean实体类。
  • CubeSrvInvokeEngine、CubeRuntime:CubeSrvInvokeEngine调用核心引擎,负责整个引擎的启动初始化CubeRuntime;CubeRuntime核心运行时,负责加载SrvBeanXmlReader、构建SrvBeanFactory和InvokerFactory以及加载processor。
  • InvokerFactory:具体的调用实现的工厂,hsfInteface的调用、hsfGen泛化调用、client包方法的调用、方法链式调用的实现,注意,singleInvoker的实现需要关心spring的applicationContext,因为srvBean中只配置了接口和方法名称,实际调用的时候需要根据方法和名称从applicationContext获取真实的实例。
  • processor扩展:包括ISrvBeanPostProcessor和IInvokerProcessor接口,ISrvBeanPostProcessor接口在srvBean被创建前后调用,用于校验srvBean的配置是否正确;IInvokerProcessor接口在具体的invoker.invoke(...)前后调用,用于校验入参、记录日志、动态修改降级表达式等。
  • Utils&Enum:SrvInvokeTypeEnmu、SrvTypeEnum、Param、SpringExpressUtil、SrvBeanAop

2.3 基础服务

三方服务提供的接口中的方法被抽象为SingleSrvBean,SingleSrvBean作为Cube的基础服务存在,SingleSrvBean需要提供关于服务接口方法调用的基本信息,包括:

  • srvId,服务接口方法id
  • srvName,服务接口方法名称
  • srvDesc,服务接口方法描述
  • invokeType,接口调用类型,包括泛化(hsfGen)、client&rpc接口(clien)
  • invokeVersion,接口版本号,泛化时需要
  • invokeGroup,接口组,泛化时需要
  • interfaceName,接口全类型名
  • methodName,具体方法名称
  • timeOut,超时时间
  • inParams,接口入参全类型列表
  • outParam,接口出参类型

一个具体基础服务的定义如下:


基础服务接口方法定义

2.4 服务编排

将三方服务接口方法抽象为SingleSrvBean之后,则可以按照业务需求进行基础服务编排,基础服务编排之后被抽象为ChainSrvBean。

ChainSrvBean的主要配置信息包括:

  • srvId,服务接口方法id
  • srvName,服务接口方法名称
  • srvDesc,服务接口方法描述
  • inParams,接口入参全类型列表
  • outParam,接口出参类型,ChainSrvBean的返回值目前为map类型,key为三方服务名称,value为三方服务接口返回值的json序列化格式
  • timeOut,超时时间
  • chainInvokers,需要调用的singleSrvBean列表

查询非核心服务的服务编排如下:


服务编排

2.5 服务接口调用模式

泛化调用

在服务配置的调用方式上,推荐使用泛化调用,因为泛化调用不需要引入非核心业务的jar包,也就意味着增加一个非核心业务的调用,只需要进行配置的更改即可,不用发布自己的代码。增加的非核心业务的调用、监控、流量控制、降级、日志、并行化都由cube完成,从而和核心业务代码进行隔离,做到对核心业务的零侵入。

泛化调用时需要设置版本号(invokeVersion)和调用组(invokeGroup),在Cube中实现为HsfGenInvoker。

client&rpc调用

也有部分业务更愿意提供client包的调用方式,可以将一些复杂的逻辑进行前置或者设置本地缓存,对于这种业务,在接入上cube支持client接口的调用方式,但是这种情况下则需要进行增加jar包,进行接口配置,发布代码,而新增的非核心业务的接口调用、监控、流量控制、降级、日志、并行化依然由cube管理。client调用在Cube中实现为ClientInvoker。

2.6 动态配置

Cube提供动态改变服务配置的能力,有了动态配置能力也就意味着新增三方服务接口或者对服务进行编排不需要发布代码(泛化调用模式下),将开发从繁杂的业务中抽出来。

ISrvBeanFactory接口提供resetBeanFactoryFromContent(String content, SrvBeanTypeEnum srvBeanTypeEnum)方法来用于动态修改Cube的服务管理配置。

2.7 框架扩展

Cube框架为了扩展性的考虑,而又不想过多的牺牲整体性,在设计上参考了spring中BeanPostProcessor的实现,在框架中提供两个扩展点进行接口回调。

ISrvBeanPostProcessor

ISrvBeanPostProcessor在SrvBean被创建前后调用,用于在SrvBean创建前后进行一些检测,Cube中默认实现以下ISrvBeanPostProcessor:

  • SrvBeanRequiredFieldCheckProcessor用于检测SrvBean的必备配置是否存在,如srvId、interfaceName、methodName等。
  • SingleSrvBeanInvokeTypeCheckProcessor用于检测在非泛化调用时SrvBean中配置的interfaceName是否在spring容器中进行过注册。

IInvokerProcessor

IInvokerProcessor在Invoker.invoke(...)前后调用,Cube中默认实现以下IInvokerProcessor:

  • InvokerLogProcessor用于记录invoke调用前后的参数、返回值和rt,并打印日志。
  • InvokerParamCheckProcessor用于调用SingleSrvBean时的参数检查,检测传入的参数和配置的参数个数类型是否一致。

除此之外Cube的使用者,可以自己根据需要实现ISrvBeanPostProcessor和IInvokerProcessor,并向spring容器进行注册,Cube在启动的时候会去spring中检测实现了ISrvBeanPostProcessor和IInvokerProcessor接口的实例,并注册到CubeRuntime中,在SrvBean初始化和invoker被调用时,依次调用各个Processor。

2.8 流量控制

流量控制的目的是为了替三方服务挡下无效请求,绝大部分三方服务的展示是有条件的,有的只在特定的端上展现,有的只针对特定的行业展现,有的值针对特定的角色展现,因此需要对请求流量进行过滤,以免所有请求都打到后端的服务,而且大多数服务也承受不起全量的查询流量。

目前针对三方服务的流量控制是在chainSrvBean的配置中,通过spring el表达式来完成。

流量控制表达式

通过spring el表达式即可满足基本的的流量控制逻辑,如需要满足特定的请求端来源和业务线,如果需要更复杂和更精细化的流量控制逻辑,而spring el表达式已经不满足,而可以实现IInvokerProcessor接口,其中在调用前的回调中通过groovy语言来完成。

2.9 降级控制

降级控制是为了提供紧急情况下关闭某个服务的能力,在目前对Cube的时候用中,通过继承IInvokerProcessor,实现了InvokerDegradationProcessor,在processBeforeInvoke中根据配置需要降级的singleSrvBeanId来进行判断,如果命中则进行降级操作,请求直接返回。

降级控制

服务状态监控

在对三方服务的管理中,特别是非核心的服务,可以对服务的状态进行监控,如果某个服务出现大量的超时,可能是这个服务出现了抖动或者性能瓶颈,则可以考虑根据其服务状态动态的进行降级,以免拖累整个接口的调用。

2.10 并行化

在一次调用过程中,我们很容易从chaiSrvBean的配置中分析出来多个三方服务接口之间的依赖关系,如果彼此之间没有依赖则可以对接口进行并行化调用。

依赖分析

如图,在调用chainSrvBean的过程中,会依次调用到三个原子服务,即singleSrvId7,singleSrvId6,singleSrvId5,而singleSrvId7、singleSrvId6服务的入参只依赖chainSrvBean的入参和globalParams,除此之外不在依赖其他singleSrv的出参。

而singleSrvId5则依赖singleSrvId6的返回值,所以singleSrvId7和singleSrvId6可以并行调用,而singleSrvId5则需要等待singleSrvId6调用完成才能继续进行调用。

在大多数的非核心业务提供的接口中,他们其实在业务上是彼此不感知的,也就是在接口参数上是隔离的,因此并行化带来较为客观的rt下降收益,特别是在接口依赖很多的情况下。

同时也提供关闭并行化的配置,即在原子接口服务(singleSrvBean)的配置中声明不参与并行化调用。

2.11 数据透传

Cube的目的就是框架化的管理各种非核心三方服务,在接入的时候将三方服务的数据进行透传给使用者(主要是前端),从而使核心服务退化成非核心三方服务的数据通道。

所以chainSrvBean的返回值被设置成map<String,Object>的形式,key为三方服务的名称,value为三方服务接口返回值,而chainSrvBean的返回值则作为核心查询的返回对象中的一个属性返回给前端。

在数据透传时需要注意三方服务返回值的类型,特别是client的使用方式,因为在client的使用方式下,核心服务会引入三方提供的包含有接口返回值的jar包,并将返回值作为Object塞入chainSrvBean的返回值map中,但是其他其他使用了该核心服务三方服务接口的应用,则会因为缺少jar而缺失类型,在序列化的时候报错。

所以chainSrvBean中所有singleSrv的返回值在往map<String,Object>里塞的时候,都会进行json序列为jsonObject。

2.12 性能影响

高速反射

Cube中会多处使用到反射,如参数属性的get/set,method的invoke调用,因此反射的开销需要特别考虑,为此引入了ReflectASM高速反射包,它利用字节码生成的方式实现了更为高效的反射机制,并且使用时会缓存生成的字节码对象,不用每次在进行方法调用时都需要去进行反射对象的字节码生成。

实际测试,使用ReflectASM进行反射调用相比直接使用java原生的反射调用要快3到4倍。

spring el

Cube中多处使用spring el表达式来进行调用的条件判断,用于流量过滤和条件判断,因此spring el的性能也需要特别注意,在使用上对spring el表达式编译结果进行缓存,不用每次都去编译。

整体上引入cube框架之后,相比于原来直接通过代码调用开销会增加3ms左右。

2.13 其他

typeAlias

参考ibatis的xml设计,typeAlias用于配置文件中全类型名的替换。

typeAlias

如图,在配置文件中任何需要用到typeAlias中配置的全类型名的地方,都可以用typeAlias中定义的alias来代替,用以简化xml文件的配置。

globalParams

globalParams用于提供一些静态的工具类,这些静态工具在spring el表达中会用到。

globalParams

3、实际使用


3.1 参数抽象

物流详情查询的核心服务和各种非核心服务,基本上都和订单信息、包裹信息、物流信息、角色信息、查询控制信息、用户登陆信息相关,因此在将这些基础信息作为非核心服务查询的入参(inParams),再加一些静态工具类和配置类(globalParams),基本上可以满足服务编排时的需求。

3.2 使用方式

  1. 引入cube的jar包

     <dependency>
             <groupId>com.taobao.cube</groupId>
             <artifactId>cube</artifactId>
             <version>1.2-SNAPSHOT</version>
     </dependency>
    
  2. 按业务需求对接入的三方服务进行singleSrvBeanConfig.xml配置,根据业务需要进行服务编排,配置chainSrvBeanConfig.xml文件,也可以通过ISrvBeanFactory接口提供resetBeanFactoryFromContent方法动态化配置。

  3. 应用的spring文件中配置CubeSrvInvokeEngine。

  4. 应用启动之后,CubeSrvInvokeEngine会被启动,如果启动有异常则会报InitErrorException,需要去查看具体启动异常情况,如果启动正常,则可以开始使用CubeSrvInvokeEngine进行服务调用。

    cubeSrvInvokeEngine.invokeChainSrvBean(10001, orderDO, locOrderDO, pac, role, option, loginUid)则会调用srvId为10001的chainSrvBean,chainSrvBean则会依次调用配置的各个singleSrvBean,最终将各个singleSrvBean的返回值作为map返回。

    使用