美文网首页
不一样的dubbo原理讲解(分享)

不一样的dubbo原理讲解(分享)

作者: 小雪的笔记 | 来源:发表于2018-06-05 20:23 被阅读0次

    转自:https://blog.csdn.net/manzhizhen/article/details/53025666
    我们知道,对于服务治理框架来说,服务通信(RPC)和服务管理两部分必不可少,而服务管理又分为服务注册、服务发现和服务人工介入,我们来看看Dubbo框架的结构图

    image.png

    图中可以看出,服务提供者Provider往服务注册中心Registry注册服务,而的消费者Consumer从服务注册中心订阅它需要的服务,而不是全部服务,当有新的Provider出现,或者现有Provider宕机,注册中心Registry都应该能尽早发现,并将新的Provider列表推送给对应的Consumer,有了这样的机制,Dubbo才能做到Failover,而Failover的时效性,由注册中心Registry的实现决定。

        Dubbo线上支持三种注册中心:自带的SimpleRegistry、Redis和Zookeeper,当然,最常用的还是Zookeeper作为注册中心,因为太多分布式的中间件需要依赖Zookeeper作为协作者。那么怎么才能让Dubbo知道我们使用哪个实现作为注册中心呢?我们只需要在dubbo的xml配置文件中配置dubbo:registry节点即可:
    

    <dubbo:registry id="dubboRegistry"protocol="zookeeper"address="${dubbo.registry.address}"/>

    没错,protocol就指明了注册中心的实现。

        要想做到服务的可靠,避免分布式系统的单点问题,除了Provider可以集群部署外,注册中心的弱依赖也是必须的,注册中心的宕机,不会影响现有服务的运行,只是不能注册新的服务和进行服务发现,Failover还是可以做的,比如Consumer可以通过服务调用来简单判断当前的Provier是否可用。如果某个Consumer宕机了,当它重启后,发现注册中心也挂了,那咋办?为了防止这种问题出现,Dubbo的Consumer会将自己需要的Provider列表在本地保存一份,当然,里面也包括自己暴露的服务信息(即自己也作为Provider),我们可以看看AbstractRegistry中的实现:
    

    public AbstractRegistry(URL url) {

    setUrl(url);
    

    // 启动文件保存定时器

    syncSaveFile= url.getParameter(Constants.REGISTRY_FILESAVE_SYNC_KEY,false);
    
    String filename =url.getParameter(Constants.FILE_KEY, System.getProperty("user.home") + "/.dubbo/dubbo-registry-"+ url.getHost() +".cache");
    

    File file = null;

    if (ConfigUtils.isNotEmpty(filename)) {
    
       file = newFile(filename);
    
       if(! file.exists() &&file.getParentFile() !=null&&! file.getParentFile().exists()){
    
           if(! file.getParentFile().mkdirs()){
    
                throw new IllegalArgumentException("Invalid registry store file "+ file +", cause: Failed to create directory" + file.getParentFile()+"!");
    
            }
    
        }
    
    }
    
    this.file= file;
    

    loadProperties();

    notify(url.getBackupUrls());

    }

    注意看黄底代码部分,如果没有在属性文件中配置file(Constants.FILE_KEY),就将在用户的当前用户目录/.dubbo/目录下新建一个dubbo-registry开头的保存所有URL信息的Cache文件,通常来说一个应用可以在多个注册中心暴露自己的服务,也可以从多个注册中心订阅不同的服务,所以这里的Cache文件名加入了注册中心的主机名。还有一个lock文件,用来防止不同的JVM进程同时修改Cache文件,注意,这里只是防止,所以意味着同一目录的Cache文件可以由多个JVM进程共享,当多个JVM进程恰巧同时修改Cache文件时,将会有一个进程获取lock文件的锁失败,见保存Cache的过程的AbstractRegistry#doSaveProperties方法的片段:

    FileChannel channel = raf.getChannel();

    try {

    FileLocklock = channel.tryLock();
    

    if (lock == null) {

       thrownew IOException("Can not lock theregistry cache file "+file.getAbsolutePath() + ", ignore and retrylater, maybe multi java process use the file, please config:dubbo.registry.file=xxx.properties");
    
    }
    

    这将导致某个URL更新到Cache文件失败,但Dubbo提供了重试机制,以保证Cache文件中信息能和内存中的信息最终一致。但不要认为Cache文件中的Provider和Consumer列表是和当前运行的服务一致,因为当一个服务部署多个应用时,Cache文件被多个JVM同时写的概率还是很大的,所以这时总有JVM进程度lock文件获取锁失败(即FileChannel#tryLock()失败),这时它只能乖乖稍后重试了。写Cache的方式也很简单粗暴,即先读取整个Cache文件,然后再往其写入当前处理的URL,然后再全量写入,可见,如果某个服务(URL)已经不再使用,它有可能一直存在于Cache文件中。

         保存Cache还分为同步保存和异步保存,我们知道内存中服务列表的更新相对于服务调用来说肯定是异步的,但为啥保存Cache文件还要分同步和异步呢?因为在Dubbo中,服务(或者叫URL)是一个个来更新的,也就是说,当服务比较多时,使用异步保存Cache文件能使应用启动和服务更新速度更快,而整个更新过程是由AbstractRegistry#notify来触发的。
    
      我们再来看看如果选择使用Zookeeper用来做Dubbo的注册中心,那么Provider和Consumer的数据在上是怎么存储的。Dubbo在ZK的所有数据都在/dubbo节点下,如下图:
    

    /dubbo

    /com.manzhizhen.user.Service1

    /consumers

    /routers

    /providers

    /configurators

    /com.manzhizhen.user.Service2

    /consumers

    /routers

    /providers

    /configurators

    /com.manzhizhen.user.Service3

    /consumers

    /routers

    /providers

    /configurators

    我们可以看到,每个服务(URL)在dubbo节点下都会有一个对应的ZK持久化节点,而每个服务节点下面都会有四个持久化子节点,代表消费者(consumer)、路由(routers)、提供者(providers)和配置(configurators),consumer和providers节点好理解,放的就是该URL下消费者和提供者的URL全部信息,而routers和configurators主要用于控制路由规则,这在正常情况下是用的比较少的,所以这两个节点数据通常为空。

     现在我们说说和服务注册相关的两个异常信息, 先给出Dubbo的集群容错图:
    
    image.png

    一个常见的异常信息是"Forbid consumer XXXXXaccess service XXXXX from registry XXXXX use dubbo version 2.5.3, Please checkregistry access list (whitelist/blacklist).",当我们需要调用服务时,会先从本地的注册目录也就是RegistryDirectory来拿取调用(Invoker)列表,见上图Directory节点,RegistryDirectory#doList代码片段如下:

    public List<Invoker<T>> doList(Invocation invocation) {

    if (forbidden) {
    
       thrownew RpcException(RpcException.FORBIDDEN_EXCEPTION,"Forbid consumer "+ NetUtils.getLocalHost() + " access service" +getInterface().getName() + " from registry "+ getUrl().getAddress() +" use dubbo version " + Version.getVersion() + ", Please check registry access list(whitelist/blacklist).");
    
    }
    

    List<Invoker<T>> invokers = null;

    Map<String, List<Invoker<T>>>localMethodInvokerMap =this.methodInvokerMap;// local reference

    可见,当forbidden为false时,会抛出该异常信息,当注册中心给它推送最新的Provider列表时,上面的forbidden的值已经变成了false,见RegistryDirectory#refreshInvoker代码片段:

    private void refreshInvoker(List<URL>invokerUrls){

    if(invokerUrls !=null&&invokerUrls.size() ==1&& invokerUrls.get(0) !=null
    
            && Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {
    
        this.forbidden=true;//禁止访问
    
        this.methodInvokerMap=null; // 置空列表
    
        destroyAllInvokers(); // 关闭所有Invoker
    
    } else{
    
       this.forbidden=false;//允许访问
    
        Map<String, Invoker<T>>oldUrlInvokerMap =this.urlInvokerMap;// local reference
    
        if (invokerUrls.size() ==0&&this.cachedInvokerUrls!=null){
    
           invokerUrls.addAll(this.cachedInvokerUrls);
    
       } else{
    
           this.cachedInvokerUrls=new HashSet<URL>();
    
            this.cachedInvokerUrls.addAll(invokerUrls);//缓存invokerUrls列表,便于交叉对比
    
        }
    

    从上面代码可以看出,当该URL协议为empty时,说明该URL已经被禁止(forbidden)了,那什么时候URL的协议会被设置成empty呢?我们看看ZookeeperRegistry#toUrlsWithEmpty方法:

    private List<URL> toUrlsWithEmpty(URLconsumer, String path, List<String> providers) {

    List<URL> urls = toUrlsWithoutEmpty(consumer, providers);

    if (urls == null || urls.isEmpty()) {
    
      int i = path.lastIndexOf('/');
    
      String category = i < 0? path : path.substring(i +1);
    
      URL empty = consumer.setProtocol(Constants.EMPTY_PROTOCOL).addParameter(Constants.CATEGORY_KEY, category);
    
       urls.add(empty);
    
    }
    
    return urls;
    

    }

    可见,当providers列表为空时,也就是某个URL下没有活着的Provider时,Consumer会将本地的invokerUrl的协议设置成empty,而toUrlsWithEmpty是在ZookeeperRegistry订阅方法doSubscribe中被调用的,这里不再给出代码。

    另一个是"Failed to invoke the method XXXXXin the service XXXXX. No provider available for the service XXXXX from registryXXXXX on the consumer XXXXX using the dubbo version 2.5.3. Please check if theproviders have been started and registered.",因为每次调用时都会去检查调用列表,如果列表有多个可用服务(即多个Provider),将会使用配置的负载均衡方式来选择一个服务来调用,但如果服务列表为空,则会抛异常,也就是在上图的Invoker节点抛出异常,这种情况一般是说明当前没有可用的Provider,见AbstractClusterInvoker#checkInvokers代码:

    protected void checkInvokers(List<Invoker<T>> invokers, Invocation invocation) {

    if (invokers == null|| invokers.size() ==0) {
    
       thrownew RpcException("Failed to invokethe method "
    
                + invocation.getMethodName() +" in the service "+ getInterface().getName()
    
                + ". No provideravailable for the service "+directory.getUrl().getServiceKey()
    
                + " from registry" + directory.getUrl().getAddress()
    
                + " on the consumer" + NetUtils.getLocalHost()
    
                + " using the dubboversion " + Version.getVersion()
    
                + ". Please check ifthe providers have been started and registered.");
    
    }
    

    }

    对于这两个异常的直接结论是,如果某个URL去注册中心注册过,但后来该URL下没有Provider了,那么此时Consumer调用Provider将报第一种异常;如果Consumer调用了一个从未去注册中心注册过的URL,则会报第二种异常。

    需要明确一点的是,注册中心的两个重要目的是服务发现和服务人工介入,线上的Provider和Consumer都不能强依赖注册中心,哪怕注册中心是双机部署,但要做到对注册中心的弱依赖,Consumer端需要有简单的负载均衡和Failover机制。
    文末附上dubbo调用逻辑


    image.png

    相关文章

      网友评论

          本文标题:不一样的dubbo原理讲解(分享)

          本文链接:https://www.haomeiwen.com/subject/abbmsftx.html