SOA解决方案——HSF(High-speed Service Framework)是阿里系主要采用的服务框架,其目的是作为桥梁联通不同的业务系统,解耦系统之间的实现依赖。
HSF简介
▐ 背景
单体应用的主要问题是不同的业务相互纠缠在一起,面对快速发展的业务,这种开发模型和架构不利于业务发展。为了解决这个问题,需要对应用进行拆分。将不同的业务分拆到多个应用中,让不同的应用分别承担不同的功能。
在单体应用时代,某个服务想要调用其他服务在本地就可以完成,而应用拆分为多个系统后,相互之间进行通信的方式就不能依赖本地,而必须要走远程,此时,一个高效、稳定的RPC框架就变得非常重要。
随着业务的不断发展,承载不同业务的应用数量以及单个应用下的服务数量都急剧膨胀,对于服务的管理变得愈发重要,在RPC框架刚开始使用的时候,可能只有几个应用,几十个服务,如果规模扩充到上万应用,几十万个服务,RPC调用反而不是重头戏,而重要的是如何能高效的组织这些服务。
一款优秀的RPC框架一般需要(或者最好能够具备)以下服务治理能力:
- 服务的方便检索,查询服务,包括服务的提供者与消费者信息
- 服务的快捷测试,能够简单、高效的进行服务测试
- 服务的路由,根据调用的服务名等运行时信息,服务消费方能够路由到对应的服务提供方指定的机器上
- 服务的归组,能够在统一机器资源维度上,让服务提供方具备服务自动归组的能力
这些特性都已经超越了一个普通RPC框架的范畴,而提供这些能力的RPC框架才能被称之为SOA(Service-Oriented Architecture)框架。
▐ 是什么
阿里SOA解决方案——HSF(High-speed Service Framework),高速服务框架。该框架是阿里系主要采用的服务框架,其目的是作为桥梁联通不同的业务系统,解耦系统之间的实现依赖。其高速体现在底层的非阻塞I/O以及优秀的序列化机制上,实现了同步和异步调用方式,并且有一套软负载体系,实现分布式应用。
▐ 特性
-
高性能的服务调用
低侵入,HSF基于Java接口完成透明的RPC调用,用户对服务是否在本地不做感知,不侵入用户代码。高性能,HSF提供基于非阻塞I/O上的高性能调用。多语言,多语言支持完善,提供了C++以及Node.js客户端,支持HTTP REST调用。
-
大流量的场景应对
客户端负载均衡,HSF在客户端基于服务地址列表做负载均衡,不需要借助其他负载均衡设备,高效完成负载均衡工作。多种选址策略,HSF客户端在调用时提供了多种选址策略。上下线策略,HSF提供了优雅上下线的能力,保证服务在重启时对客户端的影响面减到最小,客户端调用在服务端重启时表现平滑。
-
全方位的服务治理
服务管理功能,HSF运维平台提供了服务查询、测试和Mock功能,支持用户通过服务名(一般是接口名+版本号)查询服务的提供者,或者通过输入参数对已有的服务进行调用测试。规则管理功能,HSF运维平台支持使用归组、路由以及同机房等规则对客户端发起的调用进行干预,使客户端调用变得更加智能。
▐ 基本结构
功能结构图:
HSF功能结构上分为6个部分,分别是:「服务消费方」、「服务提供方」、「地址注册中心」、「持久化配置中心」、「元数据存储中心」和「HSF运维平台」(HSF 控制台),它们组合在一起可以提供全功能的分布式服务,其中服务消费方、服务提供方和地址注册中心是必需的,上述功能结构的介绍如下表:
▐ 调用过程
作为服务消费方,客户端线程首先会将用户的参数也就是请求对象进行序列化,将序列化之后的内容放置到请求通信对象中,请求通信对象对应的是HSF协议,它包含诸如请求Id等多个与请求对象无关的内容。请求通信对象会提交给I/O线程,在I/O线程中完成编码,最终发送到服务提供方,此时客户端线程会等待结果返回,处于等待状态。
服务提供方的I/O线程接收到二进制内容,解码后生成通信请求对象并将其递交给HSF服务端线程,在HSF服务端线程完成反序列化还原成请求对象,然后发起反射调用,得到结果,也就是响应对象。响应对象会在HSF服务端线程中完成序列化,并放置到通信响应对象中。HSF服务端线程会将通信响应对象提交给I/O线程,在I/O线程中完成编码,最终发送回服务消费方。
服务消费方收到二进制内容,在I/O线程中完成解码,生成响应通信对象,并唤醒客户端线程,客户端线程会根据响应通信对象中的内容完成反序列化,最终拿到响应对象,一次远程调用结束。
基本使用
▐ 服务接口定义
在接口定义模块中定义接口,将其打为jar包,发布到Maven仓库中。
public interface HelloWorldService {
/**
* 根据参数中指定的名字,生成问候语
*
* @param name 被问候的姓名
* @return 问候语
*/
String sayHi(String name);
}
▐ 业务代码实现
业务模块依赖接口定义模块,实现接口,编写业务代码。
<!-- 依赖接口定义模块 -->
<dependency>
<groupId>com.alibaba.middleware</groupId>
<artifactId>hsf-guide-api</artifactId>
</dependency>
// 实现接口
public class HelloWorldServiceImpl implements HelloWorldService {
@Override
public String sayHi(String name) {
// 编写业务代码
if (name == null || name.length() == 0) {
return null;
}
return "Hi, " + name + "! Welcome to the HSF world.";
}
}
▐ 服务发布
-
API的方式
<!-- 依赖业务模块 -->
<dependency>
<groupId>com.alibaba.middleware</groupId>
<artifactId>hsf-guide-biz</artifactId>
</dependency>
<!-- 依赖HSF -->
<dependency>
<groupId>com.taobao.hsf</groupId>
<artifactId>hsf-all</artifactId>
</dependency>
在Main方法中将服务发布出去。
// [定义] 服务的实现
Object target = new HelloWorldServiceImpl();
// [设置] HSF服务发布逻辑
HSFApiProviderBean hsfApiProviderBean = new HSFApiProviderBean();
// [设置] 发布服务的接口
hsfApiProviderBean.setServiceInterface("com.alibaba.middleware.hsf.guide.api.service.HelloWorldService");
// [设置] 服务的实现对象
hsfApiProviderBean.setTarget(target);
// [设置] 服务的版本
hsfApiProviderBean.setServiceVersion("1.0.0");
// [设置] 服务的归组
hsfApiProviderBean.setServiceGroup("HSF");
// [设置] 服务的响应时间
hsfApiProviderBean.setClientTimeout(3000);
// [设置] 服务传输业务对象时的序列化类型
hsfApiProviderBean.setSerializeType("hessian2");
// [发布] HSF服务
hsfApiProviderBean.init();
-
注解的方式
<!-- 依赖starter -->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>pandora-hsf-spring-boot-starter</artifactId>
<version>2023-04-release</version>
</dependency>
将@HSFProvider
配置到业务模块的实现类上。
@HSFProvider(serviceInterface = HelloWorldService.class, serviceGroup = "HSF", serviceVersion = "1.0.0", clientTimeout = 3000, serializeType = "hessian2")
public class HelloWorldServiceImpl implements HelloWorldService {
@Override
public String sayHi(String name) {
if (name == null || name.length() == 0) {
return null;
}
return "Hi, " + name + "! Welcome to the HSF world.";
}
}
▐ 服务调用
-
API的方式
在Main方法中调用服务端业务代码。
HSFApiConsumerBean hsfApiConsumerBean = new HSFApiConsumerBean();
// [设置] 订阅服务的接口
hsfApiConsumerBean.setInterfaceName("com.alibaba.middleware.hsf.guide.api.service.HelloWorldService");
// [设置] 服务的版本
hsfApiConsumerBean.setVersion("1.0.0");
// [设置] 服务的组别
hsfApiConsumerBean.setGroup("HSF");
// [订阅] HSF服务,同步等待地址推送,默认false(异步),同步默认超时时间为3000ms
hsfApiConsumerBean.init(true);
// [代理] 获取HSF代理
HelloWorldService helloWorldService = (HelloWorldService) hsfApiConsumerBean.getObject();
// [调用] 像调用本地接口一样,发起HSF调用
String hi = helloWorldService.sayHi("松张");
System.out.println(hi);
-
注解的方式
创建配置类,用
@HSFConsumer
标记要调用的接口。
@Configuration
public class HsfConfig {
@HSFConsumer(serviceVersion = "1.0.0", serviceGroup = "HSF")
HelloWorldService helloWorldService;
}
通过注入的方式使用。
@Autowired
HelloWorldService helloWorldService;
调用方式
同步调用也可以叫阻塞调用,它将阻塞当前线程,然后执行调用,调用完毕后再继续向下进行。
HSF的IO操作都是异步的,客户端同步调用的本质是做future.get(timeout)
操作,等待服务端的结果返回,这里的timeout就是客户端生效的超时时间(默认3000ms)。
HSF默认的同步调用时序图:
对于客户端来说,并不是所有的HSF服务都是需要同步等待服务端返回结果的,对于这些服务,HSF提供异步调用的形式,让客户端不必同步阻塞在HSF操作上。异步调用在发起调用时,HSF服务的调用结果都是返回类型的默认值,如返回类型是int,则会返回0,返回类型是Object,则会返回null。而真正的结果,是在HSFResponseFuture或者回调函数(callback)中获得的。
▐ Future异步调用
HSF发起调用后,用户可以在上下文中获取跟返回结果关联的HSFFuture对象,然后用户可以在任意时刻调用HSFFuture.getResponse(timeout)
获取服务端的返回结果。Future异步调用时序图:
API配置客户端Future异步调用。
HSFApiConsumerBean hsfApiConsumerBean = new HSFApiConsumerBean();
hsfApiConsumerBean.setInterfaceName("com.alibaba.middleware.hsf.guide.api.service.HelloWorldService");
hsfApiConsumerBean.setVersion("1.0.0");
hsfApiConsumerBean.setGroup("HSF");
// [设置] 异步future调用
List<String> asyncCallMethods = new ArrayList<String>();
// [格式] name:{methodName};type:future
asyncCallMethods.add("name:sayHi;type:future");
hsfApiConsumerBean.setAsyncallMethods(asyncCallMethods);
hsfApiConsumerBean.init(true);
HelloWorldService helloWorldService = (HelloWorldService) hsfApiConsumerBean.getObject();
String hi = helloWorldService.sayHi("松张");
// 运行后控制台打印null
System.out.println(hi);
// 及时在当前调用上下文中获取future对象;因为该对象是放在ThreadLocal中的,同一线程中后续调用会覆盖future对象,所以要及时取出
HSFFuture hsfFuture = HSFResponseFuture.getFuture();
// do something else
try {
// 这里才是真正地获取结果,如果调用还未完成,将阻塞等待结果,3000ms是等待结果的最大时间
System.out.println(hsfFuture.getResponse(3000));
} catch (Throwable e) {
e.printStackTrace();
}
▐ Callback异步调用
客户端配置为Callback方式调用时,需要配置一个实现了
HSFResponseCallback
接口的Listener,结果返回之后,HSF会调用
HSFResponseCallback
中的
onAppResponse
方法。
// 实现了HSFResponseCallback接口的Listener
public class MyCallbackHandler implements HSFResponseCallback {
@Override
public void onAppException(Throwable t) {
t.printStackTrace();
}
@Override
public void onAppResponse(Object o) {
// 取callback调用时设置的上下文
Object context = CallbackInvocationContext.getContext();
// 打印远程调用结果 + callback调用时设置的上下文
System.out.println(o.toString() + context);
}
@Override
public void onHSFException(HSFException e) {
e.printStackTrace();
}
}
Callback异步调用时序图:
API配置客户端Callback异步调用:
HSFApiConsumerBean hsfApiConsumerBean = new HSFApiConsumerBean();
hsfApiConsumerBean.setInterfaceName("com.alibaba.middleware.hsf.guide.api.service.HelloWorldService");
hsfApiConsumerBean.setVersion("1.0.0");
hsfApiConsumerBean.setGroup("HSF");
// [设置] 异步callback调用
List<String> asyncCallMethods = new ArrayList<String>();
// [格式] name:{methodName};type:callback;listener:{listenerFullyQualifiedName}
asyncCallMethods.add("name:sayHi;type:callback;listener:com.alibaba.middleware.hsf.guide.client.handler.CallbackHandler");
hsfApiConsumerBean.setAsyncallMethods(asyncCallMethods);
hsfApiConsumerBean.init(true);
HelloWorldService helloWorldService = (HelloWorldService) hsfApiConsumerBean.getObject();
// 可选步骤,设置上下文。CallbackHandler中通过api可以获取到
CallbackInvocationContext.setContext(" in callback");
String hi = helloWorldService.sayHi("松张");
// 运行后控制台打印null
System.out.println(hi);
// 清理上下文
CallbackInvocationContext.setContext(null);
// do something else
▐ 泛化调用
相较于需要依赖业务模块API jar包的正常调用方式,泛化调用不需要依赖二方包,使用其特定的
GenericService
接口,传入需要调用的方法名、方法签名和参数值进行调用服务。泛化调用适用于一些网关应用(没办法依赖所有服务的二方包),其中HSF-OPS服务测试也是依赖泛化调用功能的。
API配置客户端泛化调用:
HSFApiConsumerBean hsfApiConsumerBean = new HSFApiConsumerBean();
hsfApiConsumerBean.setInterfaceName("com.alibaba.middleware.hsf.guide.api.service.HelloWorldService");
hsfApiConsumerBean.setVersion("1.0.0");
hsfApiConsumerBean.setGroup("HSF");
// [设置] 泛化配置
hsfApiConsumerBean.setGeneric("true");
hsfApiConsumerBean.init(true);
// 使用泛化接口获取代理
GenericService genericHelloWorldService = (GenericService)hsfApiConsumerBean.getObject();
// [调用] 发起HSF泛化调用,返回指定类型的result
String helloWorldStr = (String)genericHelloWorldService.$invoke("sayHi",
// 方法入参类型数组(xxx.getClass().getName())
new String[] {String.class.getName()},
// 参数,如果是pojo,则需要转成Map
new Object[] {"松张"});
System.out.println(helloWorldStr);
HSFConsumerBean
设置generic
为true,标识HSF客户端忽略加载不到接口的异常。
GenericService
提供的$invoke
方法包含了真实调用的方法名、入参类型和参数值,以便服务端找到该方法。由于没有依赖服务端的API jar包,传入的参数如果是自定义的DTO,需要转成客户端可以序列化的Map类型。
功能配置
▐ 调用上下文
com.taobao.hsf.util.RequestCtxUtil
类中提供了基于ThreadLocal设置和获取调用上下文内容的静态方法,每次调用getXXX方法,在获取到XXX属性的值后会将该属性从ThreadLocal中remove掉,保证该属性值仅作用于当前线程的单次调用。
▐ 序列化方式
为了在网络中传输数据,需要通过序列化将java对象转为byte数组,反序列化则相反。HSF支持的序列化方式有java
、hessian
、hessian2
、json
和kyro
,默认使用的是hessian2
。java
的兼容性最好,kyro
性能最强,hessian2
和json
比较均衡。
▐ 超时配置
客户端和服务端都可以设置超时时间,客户端的优先级比服务端的高,默认的超时时间是3000ms。在设置超时时间时不仅要考虑业务执行时间,还需要加上序列化和网络通讯的时间。推荐根据业务需要为每个服务配置合适的超时时间。
不同方式设置超时时间的优先级、范围、作用域信息如下表。
即客户端优先于服务端,方法优先于接口。
▐ 服务端线程池
服务端线程池是用来执行业务逻辑的线程池,默认设置如下表。
可以通过JVM启动参数和代码的方式进行配置。
▐ 路由规则
HSF路由规则保存在Diamond持久化配置中心中。作用在消费者发起HSF服务调用的选址阶段,通过被客户端订阅的方式动态的更新规则内容。
规则以服务名.RULES作为DataId、服务的组别作为GroupId,采用Groovy脚本编写具体的规则内容。支持接口路由、方法路由和参数路由这三种路由方式。
一般推荐在HSF-OPS中进行路由规则的管理与配置。
一个简单的接口路由示例如下图所示:
▐ 归组规则
与路由规则一样,归组规则也保存在Diamond中。在HSF服务的发布期生效,将指定服务的组别修改为特定的值。这样,不同分组中的HSF服务实例就组成了以group为单位的集群,实现服务端集群的划分,从而仅针对部分客户端工作。
规则以应用名.GROUPINGRULE(JVM参数:-Dproject.name)作为DataId、「HSF」作为GroupId,采用XML格式编写具体的规则内容。
注意:在线上环境配置归组规则前要明确配置成功后产生的效果,避免出现消费者找不到服务提供方地址的问题。
一个简单的归组规则配置如下图所示:
▐ 同机房规则
同机房规则保存在Diamond中。作用在消费者发起HSF服务调用的选址阶段,根据机房网段信息优先选择同一个机房的服务提供方发起调用,从而减少跨机房流量的产生。
规则以服务名.RULES作为DataId、服务的组别作为GroupId,采用XML格式编写具体的规则内容。
注意:
- 同机房规则默认是关闭的
- 同机房规则是根据网段作为虚拟机房进行地址选取的
- 同机房规则与路由规则使用了相同的Diamond配置,如果已经配置了路由规则,在原有路由规则的基础上append即可
同机房规则配置如下图所示:
设计模式
设计模式是在软件设计中反复出现的一些问题的解决方案的经验总结。它们是一种被广泛接受的最佳实践,可用于解决特定类型的问题或完成特定类型的任务。
六大设计原则为设计模式提供理论支持,它们分别是:
- 单一职责原则
:应该有且仅有一个原因引起类的变更 - 里氏替换原则:
所有引用父类的地方必须能透明地使用其子类的对象 - 依赖倒置原则:
面向接口编程,依赖抽象而非细节 - 接口隔离原则:接口尽量细化,接口中的方法尽可能的少
- 迪米特法则:
一个对象应该对其他对象有最少的了解 - 开闭原则:对拓展开放,对修改关闭
HSF中使用了哪些经典的设计模式呢?
▐ 责任链模式
定义:使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。
介绍:想要在HSF流程发生的过程中对其进行拓展,需要实现
ProtocolInterceptor
,它的继承树如下图所示。
ProtocolInterceptor
实现了
Protocol
接口,里面只增加了一个方法
void setProtocol(Protocol protocol);
,其目的就是形成一个
Protocol
链条,这样就能将
ProtocolInterceptor
的扩展点拼装到流程链条上。HSF提供了
AbstractDelegateProtocolInterceptor
,可以通过继承该抽象类,重写
List<ServiceURL> export();
等方法,轻松的实现流程的扩展。
链的构建为了链能正常运行起来,首先需要构建链。HSF是通过以下方式拼装流程这条链的:
Protocol protocol = HSFServiceContainer.getInstance(Protocol.class);
List<ProtocolInterceptor> handlers = HSFServiceContainer.getInstances(ProtocolInterceptor.class);
//init
Protocol last = protocol;
for (int i = handlers.size() - 1; i >= 0; i--) {
handlers.get(i).setProtocol(last);
last = handlers.get(i);
}
return last;
HSFServiceContainer.getInstances(ProtocolInterceptor.class);
会返回优先级从高到低的
xxxProtocolInterceptor
(通过
@Order(int)
注解排序,值越小优先级越高),最后返回的是优先级最高的
ProtocolInterceptor
节点。
链的执行:抽象类和具体的实现类在
export
服务导出这个场景下的执行流程如下。
public abstract class AbstractDelegateProtocolInterceptor implements ProtocolInterceptor {
protected Protocol protocol;
@Override
public List<ServiceURL> export(ServiceMetadata serviceMetadata, InvocationHandler invocationHandler) {
return protocol.export(serviceMetadata,invocationHandler);
}
}
@Order(250)
public class EagleEyeProtocolInterceptor extends AbstractDelegateProtocolInterceptor {
/**
* container信息(edas)
*/
private ContainerInfo containerInfo = HSFServiceContainer.getInstance(ContainerInfo.class);
@Override
public List<ServiceURL> export(ServiceMetadata serviceMetadata, InvocationHandler invocationHandler) {
if (containerInfo.isSupportContainer()) {
serviceMetadata.addProperty(HSFConstants.CONTAINER_ID_KEY, containerInfo.getContainerId());
}
return protocol.export(serviceMetadata, invocationHandler);
}
}
此处具体的实现类以「EagleEye的启动阶段拦截」为例,需要在调用protocol.export(serviceMetadata, invocationHandler);
之前编写业务代码。
▐ 代理模式
定义:为其他对象提供一种代理以控制对这个对象的访问。
介绍:服务消费方使用代理模式调用服务提供方的方法,获取返回结果。服务消费方通过代理类与服务提供方建立TCP连接,进行网络通信,将方法和入参传输给服务提供方后,服务提供方通过反射调用指定方法,得到结果,再通过网络将结果传给服务消费方。
创建代理对象:
Object proxy = proxyFactory.getProxy(metadata, decorateInterfaces);
public Object getProxy(ServiceMetadata metadata, Class<?>... interfacesArray) {
try {
JdkProxyInvocationHandler jdkProxyInvocationHandler = new JdkProxyInvocationHandler(metadata);
Object instance = Proxy.newProxyInstance(metadata.getIfClazz().getClassLoader(), interfacesArray, jdkProxyInvocationHandler);
jdkProxyInvocationHandler.init(instance);
return instance;
} catch (Throwable t) {
throw new HSFException("failed to generate jdk proxy",t);
}
}
调用方法:
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
ApplicationModelFactory.setCurrentApplication(serviceMetadata.getApplicationModel());
ConsumerMethodModel methodModel = serviceMetadata.getConsumerServiceModel().getMethodModel(method);
return InvocationUtil.invoke(methodModel, args);
}
▐ 观察者模式
定义:定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。
介绍:以服务消费方监听路由规则为例,服务消费方通过RegistryProtocolInterceptor
与注册中心进行交互时,会根据当前路由规则构建相应的监听器,监听路由规则的变化,保证调用服务提供方的方法时使用的是最新的路由规则。
注册监听器:
public void registerListener(Object listener) {
synchronized (eventBus) {
if (lastRule != null) {
eventBusHelp.register(listener);
eventBusHelp.post(lastRule);
eventBusHelp.unregister(listener);
}
eventBus.register(listener);
}
}
▐ 装饰模式
定义:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更为灵活。
介绍:服务消费方生成调用方法的代理对象的时候,会指定代理对象实现了哪些接口,这些接口都是经过装饰的。
装饰接口:
/**
* 使用该接口对客户端元数据进行处理,返回需要装饰的接口
*/
@Shared
@Scope(Scope.Option.SINGLETON)
public interface ProxyDecorator {
/**
* 用来装饰当前的调用接口
*
* @param serviceMetadata 客户端元数据
* @return 装饰接口,如果不进行装饰返回null
*/
Class<?> decorate(ServiceMetadata serviceMetadata);
}
// 生成调用远程HSF服务的代理
ProxyDecoratorGenerator proxyDecoratorGenerator = HSFServiceContainer.getInstance(
ProxyDecoratorGenerator.class);
// 获取装饰后的接口
Class<?>[] decorateInterfaces = proxyDecoratorGenerator.getDecorateInterfaces(metadata);
ProxyFactory proxyFactory = HSFServiceContainer.getInstance(ProxyFactory.class, metadata.getProxyStyle());
Object proxy = proxyFactory.getProxy(metadata, decorateInterfaces);
Method[] methods = proxyFactory.getMethods(proxy);
团队介绍
我们来自淘天集团的营销与平台策略技术。我们支撑大促 (双11、618 等)和日销业务;同时我们也直面竞对,深入参与淘宝好价、百亿补贴、聚划算等日销业务的价格心智打造。秉承“简单、开放、自驱”的技术文化,在使命与责任中互相成就,共同成长,践行让业务先赢,技术氛围浓郁。
本文分享自微信公众号 - 大淘宝技术(AlibabaMTT)。