远程服务的发布与引用

之前分析了Dubbo本地服务的发布与引用,那篇文章充其量只能算是热身,一是因为整个服务的发布与引用流程非常简单直接,二是既然是rpc框架,其核心必然在于远程服务的发布与引用。下面就将着重分析dubbo框架中远程服务的发布与引用的流程。

远程服务发布

dubbo远程服务暴露流程

远程服务的暴露过程可以分为如下四个步骤:

  • 根据要暴露服务的接口,方法名称等参数组装URL
  • 利用JavassitProxyFactory创建Invoker的过程
  • 在RegistryProtocol中根据URL创建对应的Exporter,比如DubboExporter,然后根据URL中的参数创建指定socket server,监听指定端口
  • 向注册中心注册所提供服务的URL,并且订阅客户端注册的消息

其中前个步骤跟本地服务的发布基本上是一样的,这里不再赘述。

第三步中,除了创建Exporter外,还会根据URL参数创建socket server,监听指定端口。这个socket server从底层来看是基于Java NIO的,Dubbo中默认使用Netty框架作为底层通信框架。关于Netty,这里不会深入解释,之后会专门研究。在服务创建这一块,dubbo大量使用了 装饰器模式 ,每一层装饰都是对功能的增强,比如有负责心跳消息处理的HeartbeatHandler,有负责双向消息处理的HeaderExchangeHandler等等,关于Handler的装饰过程如下图。当服务创建完成后,一旦有客户端连接,针对不同的消息,对应的handler会进行相应的处理。这里不得不强调一下AllChannelHandler,这个Handler维护了一个 线程池 ,每当有消息过来时,这个线程池会调度指定线程进行消息处理。最后将处理的结果序列化后发送给客户端。

handler包装过程

第四步中主要做了两件事情,一个是向注册中心注册所提供服务的URL,并且publish一个注册的消息,另外一个是订阅客户端注册消息。第一个操作是dubbo服务发现的重要组成部分,在远程服务引用部分会着重介绍,这里只提一点,就是每个服务在向注册中心注册服务时,都有一个过期时间,默认时1min,用来指示所暴露的服务是否依然处于工作状态,以redis注册中心为例,在创建RedisRegistry时,会同时创建一个定时器,定时器负责定时更新所有注册服务的过期时间,更新代码如下,这样客户端就可以根据这个过期时间来判断所拿到的服务是否available。第二个订阅客户端注册消息,现在还不知道这一步的目的是什么,但我猜想可能可以做白名单用(瞎猜)。

this.expireFuture = expireExecutor.scheduleWithFixedDelay(new Runnable() {  
  public void run() {
    try {
      deferExpired(); // 延长过期时间
    } catch (Throwable t) { // 防御性容错
      logger.error("Unexpected exception occur at defer expire time, cause: " + t.getMessage(), t);
    }
  }
}, expirePeriod / 2, expirePeriod / 2, TimeUnit.MILLISECONDS);

private void deferExpired() {  
  for (Map.Entry<String, JedisPool> entry : jedisPools.entrySet()) {
    JedisPool jedisPool = entry.getValue();
    try {
      Jedis jedis = jedisPool.getResource();
      try {
        for (URL url : new HashSet<URL>(getRegistered())) {
          if (url.getParameter(Constants.DYNAMIC_KEY, true)) {
            String key = toCategoryPath(url);
            // hset 如果覆盖已存在key,则返回0,如果新创建key,则返回1
            if (jedis.hset(key, url.toFullString(), String.valueOf(System.currentTimeMillis() + expirePeriod)) == 1) {
              jedis.publish(key, Constants.REGISTER);
            }
          }
        }
        if (admin) {
          clean(jedis);
        }
        if (! replicate) {
          break;// 如果服务器端已同步数据,只需写入单台机器
        }
      } finally {
        jedisPool.returnResource(jedis);
      }
    } catch (Throwable t) {
      logger.warn("Failed to write provider heartbeat to redis registry. registry: " + entry.getKey() + ", cause: " + t.getMessage(), t);
    }
  }
}

远程服务发引用

dubbo远程服务引用流程

远程服务的引用涉及到的东西比较多,这里只是分析了他的主要流程,还有一些实现上的细节后面会慢慢研究。远程服务的引用大体上可以分为如下几步:

  • 根据接口,方法,方法类型等创建consumer端URL
  • 向注册中心注册consumer端的URL,并publish一条register的消息
  • 服务发现过程,所有发现的服务存放在RegistryDirectory中
  • 根据所发现服务,创建指定类型的Invoker,并在创建过程中进行socket的连接,每一个Invoker可以只对应一个连接,也可以对应多个连接,类似连接池的概念。
  • 利用JavassistProxyFactory创建代理
  • 当通过上述代理调用接口方法时,会触发InvokerInvocationHandler的invoke方法,根据方法名称,参数类型,参数创建RpcInvocation对象,调用指定Invoker,在dubbo中,默认使用FailoverClusterInvoker

上述第一步中的URL组装过程,和第二步中的consumer端的注册过程,大体跟server端一样,这里不再赘述。

服务发现过程又分为两个步骤,一个是consumer端第一次创建时根据要获取的服务,主动去注册中心读取所有目前可用的服务,另一个是,consumer端会同时订阅该服务的所有消息,当有新服务注册到注册中心时,consumer端会收到这个注册消息,然后进行本地服务的更新,不过需要注意的时,这个更新服务的过程时 全量更新,不是增量更新

此时,consumer端从注册中心拿到的这些服务是一个个的URL,这些URL包含了所提供服务的ip地址,端口号,接口名,方法等等信息,consumer端可以根据这些信息将所有获得的URL转换成指定的Invoker,这个转换invoker的过程除了要创建指定的Invoker之外,还包括socket连接。跟服务端一样,这个创建也使用了同样的装饰器模式,其作用这里不再赘述。这里假设每个URL对应一台提供服务的机器,那么每个Invoker就对应一台提供服务的机器,在Invoker中包含socket的连接的客户端,可以是一个,也可以是多个,多个连接的话就相当于一个连接池的概念。

创建代理的过程在本地服务的引用中分析的已经比较清楚了,这个过程基本上一样,亦不再赘述。

当通过代理调用接口方法时,会触发InvokerInvocationHandler的invoke方法,接着调用调用指定Invoker,dubbo中默认使用FailoverClusterInvoker。在FailoverClusterInvoker的invoke方法中,首先会根据调用方法的方法名称去RegistryDirectory中拿到所有Invoker,然后采用相应的负载均衡算法,进行Invoker的选择。dubbo中实现的负载算法包括ConsistentHash,Random,LeastActive以及RoundRobin算法,这些算法之后会专门研究,这里不再对这些算法进行深入分析。拿到Invoker时,调用该Invoker的invoke方法,以DubboInvoker为例,在invoke方法中,首先会选择一个socket的连接,然后利用该连接将所调用方法的信息,即RpcInvocation发送至远端服务,远端服务在收到该请求后,根据接口,方法等信息获取制定的Exporter,进而获取对应的Invoker,进行真正方法的调用,最后将调用结果返回至consumer端,这样就完成了整个rpc的调用。

总结

上文从远程服务的发布和引用描述了这个rpc的调用过程,但是都仅描述了provider端和consumer端各自的实现方式,具体到通信层,并没有做深入研究。此外就是在rpc中,还有非常重要的一块本文没有提及,那就是序列化,这些都是后面要继续进行研究的。这也明确了今后的研究方向: * Netty框架 * Dubbo中的序列化方式 * Dubbo中负载均衡算法的原理及实现

Shaohang Zhao

Read more posts by this author.