HSF源码剖析
前言
HSF是一个分布式的远程服务调用框架,其实我更喜欢把分布式几个字去掉,因为HSF本身并不是一个单独的服务(指一个进程),他是附属在你的应用里的一个组件,一个RPC组件(远程过程调用——Remote Procedure Call,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发分布式应用更加容易),当然HSF完全的内容肯定不止这些。
说了那么久HSF全称是什么呢?High-Speed Service Framework
RPC
我们先来看一张图:

有这么一个场景(本来想举一个便具体业务的例子,想想还是已技术实现相关的比较好),监控平台:监控所有主机的状态,这时候每台主机上有一个agent,每个几秒向监控平台上传一次数据(主机内存使用率、硬盘状况、CPU、load、进程信息等等)。
可能在开发的时候最简单的方式就是监控平台有一个http接口,agent每隔几秒请求一次,能够满足需求,但是如果主机数快速增长了很多、监控项越来越多、请求体越来越大,你会发现http的传输效率下降了,每一次调用的耗时增加了。
这时我们会去研究http协议,想去优化这个过程,发现http的过程是:建立连接、发送请求信息、发送响应信息、关闭连接,看到这个过程首先想优化的就是能不能不要每次都去建立连接关闭连接,因为数据上报是个持续的过程;紧接着去研究http头,发现很多协议用不到,繁杂,白白增加了消息体;后来又觉得http的协议解析还原过程很复杂,可以自己开发一个提升性能......
RPC来了,他能满足这些需求,但是前提是需要开发,需要前期成本,所以想项目设计时就要去衡量,不过没事,我们有HSF啊。
我们将上图稍微改造一下:

RPC就讲到这里,毕竟重点是HSF,想要更多的了解RPC,可以上wiki或者网上查询。
HSF架构
其实在我们的应用中,一般情况下你的应用不仅仅是client,也是server,因为你不仅需要去调用其他应用提供的服务,也提供服务给其他应用,所以这样一来,整个hsf的服务调用链路也会很复杂。
从上面两幅图中我们很显然的发现一个问题,就是服务提供者如何告知客户端他提供的服务,所以需要有一个服务注册与发现的地方,在HSF架构中提供这个功能的是configserver,如下图:

上面已经实现了基本的能力,但是如何动态配置负载(线程池大小)、默认配置(configserver地址等)、还有一些特性功能(如路由规则),这时候就需要有一个持久化配置中心,如下图:

重点说一下路由规则,举个例子:通过路由规则配置在服务调用的时候只调用同机房的server,这样子服务调用的耗时肯定比跨机房的耗时短。除此之外hsf里还单独写了unitService进行服务单元发布来区分中心发布,这些番外的东西以后有时间再写个番外篇,这里就不过多阐述了,毕竟这些有点偏场景偏业务的内容以后可能就改成别的方式了。

redis功能:HSF使用Redis存储元数据,每一个HSF Consumer/Provider 都会在启动后、每隔一段时间向redis上报元数据,这些元数据采集起来又提供给HSFOPS做服务治理,包括应用名和服务的映射、服务的元数据等。
服务的注册与发布
- <bean id="hsfTestService"
- class="com.test.service.impl.HsfTestServiceImpl" />
- <bean class="com.taobao.hsf.app.spring.util.HSFSpringProviderBean"
- init-method="init">
- <property name="serviceName" value="hsfTestService" />
- <property name="target" ref="hsfTestService" />
- <property name="serviceInterface">
- <value>com.test.service.HsfTestService
- </value>
- </property>
- <property name="serviceVersion">
- <value>${hsf.common.provider.version}</value>
- </property>
- </bean>
相信同学们对上面这段配置代码很熟悉,那么服务到底是怎么注册的呢,为什么这里配置了这个服务就可以被调用了呢?
从配置文件看到有个关键的bean——HSFSpringProviderBean,还有个关键的初始化方法init,其实init的过程就是服务发布的过程,我们来看看HSFSpringProviderBean中的部分代码:
- public void init() throws Exception {
- // 避免被初始化多次
- if (!providerBean.getInited().compareAndSet(false, true)) {
- return;
- }
- LoggerInit.initHSFLog();
- SpasInit.initSpas();
- providerBean.checkConfig();
- publishIfNotInSpringContainer();
- }
- private void publishIfNotInSpringContainer() {
- if (!isInSpringContainer) {
- LOGGER.warn("[SpringProviderBean]不是在Spring容器中创建, 不推荐使用");
- providerBean.publish();
- }
- }
从代码中很明显的看到服务发布providerBean.publish(),先来看大致类图,类图中有些不是很关键的先省略了:

大致对类图进行解释一下,这也是服务发布的一个过程:
- 服务初始化,首先需要有一个提供服务的service实现类(spring bean)和接口;
- 初始化HSFSpringProviderBean,从配置文件获取服务名称、接口、实现类、版本等等;
- providerBean是HSFApiProviderBean在HSFSpringProviderBean中的变量,HSFSpringProviderBean会将从配置文件获取的服务名称、接口、实现类、版本等等赋值给providerBean;
- providerBean中有个服务实体类ServiceMetadata,providerBean会将服务发布的所有信息放在这里,如接口、实现类、版本等等,在整个发布过程中,ServiceMetadata是所有对象之间的传输对象;
- 这里先来解释一下为什么有HSFSpringProviderBean和HSFApiProviderBean,其实两个可以合并成一个,但是为什么要分开呢?我的理解是对于不同环境的不同实现,比如现在用的是spring环境,那就需要有个spring适配类HSFSpringProviderBean来获取配置信息,假如是其他环境那么就会有另一个适配类,最终把信息统一转成给HSFApiProviderBean,HSFApiProviderBean是来具体操作实现;
- 当执行providerBean.publish()时,会调用ProcessService的publish方法,具体实现类是ProcessComponent;
- 发布的具体流程就是ProcessComponent里:
- 第一步,调用rpcProtocolService来注册发布RPC服务,这个动作是在server本地发布一个线程池,每一个服务都会申请一个线程池,当请求过来时从线程池获取executor进行执行并返回;
- 第二步,检查单元化发布,就unitService在发布前检查是中心发布还是单元发布,对ServiceMetadata设置不同的发布路由;
- 第三步,通过metadataService将ServiceMetadata发布到ConfigServer上;
- 第四步,通过metadataInfoStoreService将ServiceMetadata保存到redis供服务治理或者其他用途。
服务注册发布大致就是这么一个过程。
HSF的Client
现在来看看client是如何去调用服务的。
- <bean id="hsfTestService" class="com.taobao.hsf.app.spring.util.HSFSpringConsumerBean" init-method="init">
- <property name="interfaceName" value="com.test.service.hsfTestService"/>
- <property name="version" value="1.0.0.daily"/>
- </bean>
上面一段配置文件相信在项目中肯定也非常常见,那么他是怎么运作的呢?在spring注入的时候并没有具体的实现类啊,只有一个接口?怎么实现调用的呢?
其实这是我一个好奇心的地方,我想去看个究竟,hsf到底是用何种方式去实现的。
我们先来思考一个问题,那就是没有具体实现类,hsf是如何实现在spring中注册服务的呢?答案就是动态代理,类似mybatis的方式,mybatis在写dao层的时候只是写了个接口,并没有具体实现,hsf跟这种方式很相像。
客户端分两部分来讲解:服务的订阅和被推送,服务的调用。
服务的订阅和被推送
先来看类图:

一样我们通过类图来看服务的订阅和接收过程:
服务初始化,首先需要引入服务接口相关的pom,然后写配置文件;
将需要被调用的服务注册成spring bean,即上面配置文件中的内容。
这里用到了动态代理,通过类图我们可以看到HSFSpringConsumerBean实现了FactoryBean;
FactoryBean:是一个Java Bean,但是它是一个能生产对象的工厂Bean,通过getObject方法返回具体的bean,在spring bean实例化bean的过程中会去判断是不是FactoryBean,如果不是就返回bean,否则返回FactoryBean生产的bean,具体同学们可以去看AbstractBeanFactory的doGetBean方法,里面会调用getObjectForBeanInstance方法,这个方法里有具体实现;
HSFSpringConsumerBean实现了FactoryBean,那么getObject方法具体返回了什么呢?怎么返回的呢?
- @Override
- public Object getObject() throws Exception {
- return consumerBean.getObject();
- }
从代码看得出是调用了consumerBean(HSFApiConsumerBean)的getObject方法返回的,那么我们再来看getObject方法:
- public Object getObject() throws Exception {
- return metadata.getTarget();
- }
这个方法返回的是metadata(ServiceMetadata)的target,那么target是怎么获取的呢?下面重点说明;
- @Override
HSFSpringConsumerBean的init方法调用了consumerBean(HSFApiConsumerBean)的init方法,我们来看consumerBean里init方法的某一段代码:
- ProcessService processService = HSFServiceContainer.getInstance(ProcessService.class);
- try {
- metadata.setTarget(processService.consume(metadata));
- LOGGER.warn("成功生成对接口为[" + metadata.getInterfaceName() + "]版本为[" + metadata.getVersion() + "]的HSF服务调用的代理!");
- } catch (Exception e) {
- LOGGER.error("", "生成对接口为[" + metadata.getInterfaceName() + "]版本为[" + metadata.getVersion()
- + "]的HSF服务调用的代理失败", e);
- // since 2007,一旦初始化异常就抛出
- throw e;
- }
- int waitTime = metadata.getMaxWaitTimeForCsAddress();
- if (waitTime > 0) {
- try {
- metadata.getCsAddressCountDownLatch().await(waitTime, TimeUnit.MILLISECONDS);
- } catch (InterruptedException e) {
- // ignore
- }
- }
这一段代码包含了动态代理对象的具体生成和服务订阅以及服务信息接收;
- ProcessService processService = HSFServiceContainer.getInstance(ProcessService.class);
先说了一下代码逻辑,服务的订阅和服务信息的接收(被推送)在processService中执行,动态代理对象在processService中生成,下面的wait我推测是用来等目标服务信息的推送(当收到订阅的目标具体服务实现,接下来的调用过程才能走通);
看来processService是一个很重要的组件,这边通过processService.consume(metadata)这样的方法调用实现了那么多步骤,target也在这里面生成,说一下这个方法内的逻辑:
首先去缓存中找是否之前target有生成,有就返回;
没有就通过java Proxy生成对象;
订阅服务信息(返回的可调用地址);
保存客户端metadata到redis,返回target。
到此为止,服务代理对象的生成,服务的订阅都完成了,接下来看看服务的调用。
服务的调用
其实通过上面两个部分整个框架已经定好了,服务信息已经注册发布,客户端也获取到了服务的调用地址,接下去就是调用就行,调用呢就是真正的rpc请求了,hsf的rpc是通过netty实现的。
直接上类图:

之前说了动态代理,那么在方法执行时就行进入代理类执行,执行HSFServiceProxy的invoke方法,invoke方法会调用trueInvoke方法:
在trueInvoke里调用RPCProtocolTemplateService,在这里封装HSFRequest,执行具体的invoke方法;
具体的invoke方法调用RPCProtocolService,在这里主要是根据invokeType来确定具体的InvokeService实现,最基本的我们知道hsf服务有同步调用和异步调用,具体实现就在这里;
最后在具体的实现类的获取NettyClient,跟server进行通信,返回HSFResponse。
简单说下服务端的流程:
服务端会启动nettyServer,具体由NettyServerHandler来处理所有rpc请求;
NettyServerHandler会根据HSFRequest找到具体的handler,这边是RPCServerHandler,除此之外还有心跳啊等等handler;
通过handler获取具体执行的executor(这个在之前服务注册那边有讲,每个服务本地会申请线程池,threadpoolexecutor);
new一个HandlerRunnable放进executor执行executor.execute(new HandlerRunnable);
最终在handler里调用ProviderProcessor,ProviderProcessor会找到具体的服务实现类并执行,将执行结果封装成HSFResponse,向client返回HSFResponse。

写在最后
我在这里讲得更多的是主链路,里面有很多具体的细节比如路由、鹰眼追踪、日志、负载等等没有展开讲,其实每个点拿出来都可以写一篇文章,可能对于hsf的开发同学来说,每一个点都会有一个很好玩的故事,那么关于HSF就先讲到这里。
HSF源码剖析的更多相关文章
- jQuery之Deferred源码剖析
一.前言 大约在夏季,我们谈过ES6的Promise(详见here),其实在ES6前jQuery早就有了Promise,也就是我们所知道的Deferred对象,宗旨当然也和ES6的Promise一样, ...
- Nodejs事件引擎libuv源码剖析之:高效线程池(threadpool)的实现
声明:本文为原创博文,转载请注明出处. Nodejs编程是全异步的,这就意味着我们不必每次都阻塞等待该次操作的结果,而事件完成(就绪)时会主动回调通知我们.在网络编程中,一般都是基于Reactor线程 ...
- Apache Spark源码剖析
Apache Spark源码剖析(全面系统介绍Spark源码,提供分析源码的实用技巧和合理的阅读顺序,充分了解Spark的设计思想和运行机理) 许鹏 著 ISBN 978-7-121-25420- ...
- 基于mybatis-generator-core 1.3.5项目的修订版以及源码剖析
项目简单说明 mybatis-generator,是根据数据库表.字段反向生成实体类等代码文件.我在国庆时候,没事剖析了mybatis-generator-core源码,写了相当详细的中文注释,可以去 ...
- STL"源码"剖析-重点知识总结
STL是C++重要的组件之一,大学时看过<STL源码剖析>这本书,这几天复习了一下,总结出以下LZ认为比较重要的知识点,内容有点略多 :) 1.STL概述 STL提供六大组件,彼此可以组合 ...
- SpringMVC源码剖析(四)- DispatcherServlet请求转发的实现
SpringMVC完成初始化流程之后,就进入Servlet标准生命周期的第二个阶段,即“service”阶段.在“service”阶段中,每一次Http请求到来,容器都会启动一个请求线程,通过serv ...
- 自己实现多线程的socket,socketserver源码剖析
1,IO多路复用 三种多路复用的机制:select.poll.epoll 用的多的两个:select和epoll 简单的说就是:1,select和poll所有平台都支持,epoll只有linux支持2 ...
- Java多线程9:ThreadLocal源码剖析
ThreadLocal源码剖析 ThreadLocal其实比较简单,因为类里就三个public方法:set(T value).get().remove().先剖析源码清楚地知道ThreadLocal是 ...
- JS魔法堂:mmDeferred源码剖析
一.前言 avalon.js的影响力愈发强劲,而作为子模块之一的mmDeferred必然成为异步调用模式学习之旅的又一站呢!本文将记录我对mmDeferred的认识,若有纰漏请各位指正,谢谢.项目请见 ...
随机推荐
- 如何指定rman下的备份路径
如果不想使用缺省路径,可以以如下方式来指定: RMAN> configure channel 1 device type disk format '/rman/bak/%F';RMAN> ...
- 使用Python的BeautifulSoup 类库采集网页内容
BeautifulSoup 一个分析.处理DOM树的类库.可以做网络爬虫.模块简称bs4. 安装类库 easy_install beautifulsoup4 pip install beautiful ...
- MAVEN相关文章
Maven入门指南① :Maven 快速入门及简单使用 Maven入门指南② :Maven 常用命令,手动创建第一个 Maven 项目 Maven入门指南③:坐标和依赖 Maven入门指南④:仓库 M ...
- 微服务介绍及Asp.net Core实战项目系列之微服务介绍
0.目录 整体架构目录:ASP.NET Core分布式项目实战-目录 一.微服务选型 在做微服务架构的技术选型的时候,我们以“无侵入”和“社区活跃”为主要的考量点,将来升级为原子服务架构.量子服务架构 ...
- 安装centos minimal 版本后安装mysql详细过程(linux)
本文内容参考自:http://www.centoscn.com/mysql/2014/1211/4290.html PS:Yum(全称为 Yellow dog Updater, Modified)是一 ...
- javaweb(八)——HttpServletResponse对象(二)
一.HttpServletResponse常见应用——生成验证码 1.1.生成随机图片用作验证码 生成图片主要用到了一个BufferedImage类, 生成随机图片范例: 1 package gacl ...
- Python小白学习之如何添加类属性和类方法,修改类私有属性
如何添加类属性和类方法,修改类私有属性 2018-10-26 11:42:24 类属性.定义类方法.类实例化.属性初始化.self参数.类的私有变量的个人学习笔记 直接上实例: class play ...
- 书写可维护的javascript
内容介绍 编写可维护的代码很重要,因为大部分开发人员都花费大量时间维护他人代码. 1.什么是可维护的代码? 一般来说可维护的代码都有以下一些特征: 可理解性---------其他人可以接手代码并理解它 ...
- 微信 msg_sec_check接口PHP 调用
$checkContent = '要检测的内容'; $url = 'https://api.weixin.qq.com/wxa/msg_sec_check?access_token='. $res[& ...
- 亚马逊CEO贝索斯致股东信:阐述公司未来计划
亚马逊CEO 杰夫·贝索斯(Jeff Bezos)今天发布年度股东信, 详细描述了亚马逊的产品.服务和未来计划,当然,信中并没有任何的硬数据,比如说亚马逊Kindle的销量等等.但这封信也包括一些颇令 ...