HSF源码剖析

前言

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

说了那么久HSF全称是什么呢?High-Speed Service Framework

RPC

我们先来看一张图:

hsf-up.png

很多同学看了这张图可能会觉得这跟http的过程有什么区别?

有这么一个场景(本来想举一个便具体业务的例子,想想还是已技术实现相关的比较好),监控平台:监控所有主机的状态,这时候每台主机上有一个agent,每个几秒向监控平台上传一次数据(主机内存使用率、硬盘状况、CPU、load、进程信息等等)。

可能在开发的时候最简单的方式就是监控平台有一个http接口,agent每隔几秒请求一次,能够满足需求,但是如果主机数快速增长了很多、监控项越来越多、请求体越来越大,你会发现http的传输效率下降了,每一次调用的耗时增加了。

这时我们会去研究http协议,想去优化这个过程,发现http的过程是:建立连接、发送请求信息、发送响应信息、关闭连接,看到这个过程首先想优化的就是能不能不要每次都去建立连接关闭连接,因为数据上报是个持续的过程;紧接着去研究http头,发现很多协议用不到,繁杂,白白增加了消息体;后来又觉得http的协议解析还原过程很复杂,可以自己开发一个提升性能......

RPC来了,他能满足这些需求,但是前提是需要开发,需要前期成本,所以想项目设计时就要去衡量,不过没事,我们有HSF啊。

我们将上图稍微改造一下:

hsf-http-rpc.png

现在从图中可以看着,client和server之间有一条长连接,并且我们有自己的协议体:RpcRequest和RpcResponse。

RPC就讲到这里,毕竟重点是HSF,想要更多的了解RPC,可以上wiki或者网上查询。

HSF架构

其实在我们的应用中,一般情况下你的应用不仅仅是client,也是server,因为你不仅需要去调用其他应用提供的服务,也提供服务给其他应用,所以这样一来,整个hsf的服务调用链路也会很复杂。

从上面两幅图中我们很显然的发现一个问题,就是服务提供者如何告知客户端他提供的服务,所以需要有一个服务注册与发现的地方,在HSF架构中提供这个功能的是configserver,如下图:

hsf-configserver.png

从上图可以看出server端启动的时候会向configserver注册自己提供的服务,client会向configserver订阅需要的服务,configserver通过订阅信息将相关服务提供者的地址以及其他关键信息推送给client

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

hsf-diamond.png

client和server启动的时候会先去diamond获取需要的配置信息,如最关键的服务注册中心的类型和地址,除此之外之外还有服务治理的类型和地址等。

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

hsf-ops.png

相信大家都用过hsf服务治理网站,通过这个网站可以看到有哪些服务、服务提供者的地址是多少、有多少提供者、具体的消费者是谁,hsf通过configserver、redis、diamond里的存储信息获取到这些信息。

redis功能:HSF使用Redis存储元数据,每一个HSF Consumer/Provider 都会在启动后、每隔一段时间向redis上报元数据,这些元数据采集起来又提供给HSFOPS做服务治理,包括应用名和服务的映射、服务的元数据等。

服务的注册与发布

hsf-server.png

接下来我们把这个server解开,看看里面是怎么样的。

<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(),先来看大致类图,类图中有些不是很关键的先省略了:

hsf-server-uml.png

大致对类图进行解释一下,这也是服务发布的一个过程:

  1. 服务初始化,首先需要有一个提供服务的service实现类(spring bean)和接口;
  2. 初始化HSFSpringProviderBean,从配置文件获取服务名称、接口、实现类、版本等等;
  3. providerBean是HSFApiProviderBean在HSFSpringProviderBean中的变量,HSFSpringProviderBean会将从配置文件获取的服务名称、接口、实现类、版本等等赋值给providerBean;
  4. providerBean中有个服务实体类ServiceMetadata,providerBean会将服务发布的所有信息放在这里,如接口、实现类、版本等等,在整个发布过程中,ServiceMetadata是所有对象之间的传输对象;
  5. 这里先来解释一下为什么有HSFSpringProviderBean和HSFApiProviderBean,其实两个可以合并成一个,但是为什么要分开呢?我的理解是对于不同环境的不同实现,比如现在用的是spring环境,那就需要有个spring适配类HSFSpringProviderBean来获取配置信息,假如是其他环境那么就会有另一个适配类,最终把信息统一转成给HSFApiProviderBean,HSFApiProviderBean是来具体操作实现;
  6. 当执行providerBean.publish()时,会调用ProcessService的publish方法,具体实现类是ProcessComponent;
  7. 发布的具体流程就是ProcessComponent里:
    • 第一步,调用rpcProtocolService来注册发布RPC服务,这个动作是在server本地发布一个线程池,每一个服务都会申请一个线程池,当请求过来时从线程池获取executor进行执行并返回;
    • 第二步,检查单元化发布,就unitService在发布前检查是中心发布还是单元发布,对ServiceMetadata设置不同的发布路由;
    • 第三步,通过metadataService将ServiceMetadata发布到ConfigServer上;
    • 第四步,通过metadataInfoStoreService将ServiceMetadata保存到redis供服务治理或者其他用途。

服务注册发布大致就是这么一个过程。

HSF的Client

hsf-client.png

现在来看看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跟这种方式很相像。

客户端分两部分来讲解:服务的订阅和被推送,服务的调用。

服务的订阅和被推送

先来看类图:

hsf-client-uml.png

一样我们通过类图来看服务的订阅和接收过程:

  1. 服务初始化,首先需要引入服务接口相关的pom,然后写配置文件;

  2. 将需要被调用的服务注册成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是怎么获取的呢?下面重点说明;

    • 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中生成,下面的wait我推测是用来等目标服务信息的推送(当收到订阅的目标具体服务实现,接下来的调用过程才能走通);

    • 看来processService是一个很重要的组件,这边通过processService.consume(metadata)这样的方法调用实现了那么多步骤,target也在这里面生成,说一下这个方法内的逻辑:

      • 首先去缓存中找是否之前target有生成,有就返回;

      • 没有就通过java Proxy生成对象;

      • 订阅服务信息(返回的可调用地址);

      • 保存客户端metadata到redis,返回target。

      target.png

到此为止,服务代理对象的生成,服务的订阅都完成了,接下来看看服务的调用。

服务的调用

其实通过上面两个部分整个框架已经定好了,服务信息已经注册发布,客户端也获取到了服务的调用地址,接下去就是调用就行,调用呢就是真正的rpc请求了,hsf的rpc是通过netty实现的。

直接上类图:

hsf-call.png

之前说了动态代理,那么在方法执行时就行进入代理类执行,执行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。

total.png

写在最后

我在这里讲得更多的是主链路,里面有很多具体的细节比如路由、鹰眼追踪、日志、负载等等没有展开讲,其实每个点拿出来都可以写一篇文章,可能对于hsf的开发同学来说,每一个点都会有一个很好玩的故事,那么关于HSF就先讲到这里。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,579评论 18 139
  • JAVA面试题 1、作用域public,private,protected,以及不写时的区别答:区别如下:作用域 ...
    JA尐白阅读 1,143评论 1 0
  • 一缕清香东自西,遥看飞雪压枝低。 梨花早入诗心去,五月梢头谁作题?
    梦之旅_926e阅读 467评论 0 7
  • 一段感情的结束不管是哪一方先开始的放弃,都不能把错误都归结在一个人身上。或许刚开始的时候都有想过要好好在一起吧。我...
    酷啦啦阅读 474评论 0 0
  • 冬日的暖阳,夏日的清风,秋天的夕阳,都不如你春天的颦颦一笑。 故事的开头总是这样,适逢其会,猝不及防。故事的结局总...
    你我的剧本阅读 118评论 0 0