我的架构梦:(二十六)Dubbo源码分析之服务注册与消费源码剖析

Dubbo源码分析之服务注册与消费源码剖析

    • 一、注册中心Zookeeper剖析
    • 二、服务的注册过程分析
    • 三、URL规则详解 和 服务本地缓存
    • 四、Dubbo 消费过程分析

一、注册中心Zookeeper剖析

注册中心是Dubbo的重要组成部分,主要用于服务的注册与发现,我们可以选择RedisNacosZookeeper作为Dubbo的注册中心,Dubbo推荐用户使用Zookeeper作为注册中心。

1、注册中心Zookeeper目录结构

我们使用一个最基本的服务的注册与消费的Demo来进行说明。

例如:只有一个提供者和消费者。 com.riemann.service.HelloService 为我们所提供的服务。

public interface HelloService {
    String sayHello(String name);
}

Zookeeper的目录结构如下:

+- dubbo
| +- com.riemann.service.HelloService
| | +- consumers
| | | +- consumer://192.168.1.102/com.riemann.service.HelloService?application=dubbo-demo-annotation- consumer&category=consumers&check=false&dubbo=2.0.2&init=false&interface=com.riemann.service.HelloService&methods=sayHello,sayHelloWithPrint,sayHelloWithTransmiss ion,sayHelloWithException&pid=25923&release=2.7.5&side=consumer&sticky=false&timestamp=1583896043650
| | +- providers
| | | +- dubbo://192.168.1.102:20880/com.riemann.service.HelloService?anyhost=true&application=dubbo-demo-annotation-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.riemann.service.HelloService&methods=sayHello,sayHelloWithPrint,sayHelloWithTransmi ssion,sayHelloWithException&pid=25917&release=2.7.5&side=provider&telnet=clear,exit,help,status,log,ls,ps,cd,pwd,invoke,trace,count,select,shutdown&timestamp=1583896023597
| | +- configuration
| | +- routers
  • 可以在这里看到所有的都是在dubbo层级下的
  • dubbo根节点下面是当前所拥有的接口名称,如果有多个接口,则会以多个子节点的形式展开
  • 每个服务下面又分别有四个配置项
    consumers: 当前服务下面所有的消费者列表(URL)
    providers: 当前服务下面所有的提供者列表(URL)
    configuration: 当前服务下面的配置信息信息,provider或者consumer会通过读取这里的配 置信息来获取配置
    routers: 当消费者在进行获取提供者的时,会通过这里配置好的路由来进行适配匹配规则。
  • 可以看到,dubbo基本上很多时候都是通过URL的形式来进行交互获取数据的,在URL中也会保存很多的信息。后面也会对URL的规则做详细介绍。

2、Zookeeper结构图
我的架构梦:(二十六)Dubbo源码分析之服务注册与消费源码剖析_第1张图片

通过这张图我们可以了解到如下信息:

  • 提供者会在目录下进行自身的进行注册。
  • 消费者会在 consumers 目录下进行自身注册,并且监听 provider 目录,以此通过监听提供者增 加或者减少,实现服务发现。
  • Monitor模块会对整个服务级别做监听,用来得知整体的服务情况。以此就能更多的对整体情况做监控。

二、服务的注册过程分析

1、服务注册(暴露)过程

我的架构梦:(二十六)Dubbo源码分析之服务注册与消费源码剖析_第2张图片

首先 ServiceConfig 类拿到对外提供服务的实际类 ref(如:HelloServiceImpl),然后通过 ProxyFactory 接口实现类中的 getInvoker 方法使用 ref 生成一个 AbstractProxyInvoker 实例,
到这一步就完成具体服务到 Invoker 的转化。接下来就是 Invoker 转换到 Exporter 的过程。

查看ServiceConfig
重点查看 ProxyFactoryProtocol 类型的属性 以及 ref

下面我们就看一下Invoker 转换成 Exporter的过程

其中会涉及到 RegistryService接口 RegistryFactory 接口 和 注册provider到注册中心流程的过程

(1)、RegistryService代码解读,这块的代码比较简单,主要是对指定的路径进行注册,解绑,监听和取消监听,查询操作。也是注册中心中最为基础的类。

public interface RegistryService {

	/**
	 * 进行对URL的注册操作,比如provider,consumer,routers等 
	 */
    void register(URL url);

    /**
	 * 解除对指定URL的注册,比如provider,consumer,routers等 
	 */
	void unregister(URL url);

	/**
	 * 增加对指定URL的路径监听,当有变化的时候进行通知操作 
	 */
	void subscribe(URL url, NotifyListener listener);

	/**
	 * 解除对指定URL的路径监听,取消指定的listener 
	 */
	void unsubscribe(URL url, NotifyListener listener);

}

(2)、我们再来看 RegistryFactory ,是通过他来生成真实的注册中心。通过这种方式,也可以保证一 个应用中可以使用多个注册中心。可以看到这里也是通过不同的protocol参数,来选择不同的协议。

@SPI("dubbo")
public interface RegistryFactory {
	/**
	 * 获取注册中心地址 
	 */
    @Adaptive({"protocol"})
    Registry getRegistry(URL url);
}

(3)、下面我们就来跟踪一下,一个服务是如何注册到注册中心上去的。其中比较关键的一个类是 RegistryProtocol ,他负责管理整个注册中心相关协议。并且统一对外提供服务。这里我们主要以 RegistryProtocol.export 方法作为入口,这个方法主要的作用就是将我们需要执行的信息注册并且
导出。

public <T> Exporter<T> export(Invoker<T> originInvoker) throws RpcException {
	// 获取注册中心的地址
	// zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService? application=dubbo-demo-annotation- provider&dubbo=2.0.2&export=dubbo%3A%2F%2F192.168.1.102%3A20880%2Fcom.lagou.serv ice.HelloService%3Fanyhost%3Dtrue%26application%3Ddubbo-demo-annotation- provider%26bind.ip%3D192.168.1.102%26bind.port%3D20880%26deprecated%3Dfalse%26du bbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Dcom.lagou.service.H elloService%26methods%3DsayHello%2CsayHelloWithPrint%2CsayHelloWithTransmission% 2CsayHelloWithException%26pid%3D30998%26release%3D2.7.5%26side%3Dprovider%26teln et%3Dclear%2Cexit%2Chelp%2Cstatus%2Clog%2Cls%2Cps%2Ccd%2Cpwd%2Cinvoke%2Ctrace%2C count%2Cselect%2Cshutdown%26timestamp%3D1583906801486&pid=30998&release=2.7.5&ti mestamp=1583906801477
    URL registryUrl = this.getRegistryUrl(originInvoker);
    // 获取当前提供者需要注册的地址
	// dubbo://192.168.1.102:20880/com.riemann.service.HelloService? anyhost=true&application=dubbo-demo-annotation- provider&bind.ip=192.168.1.102&bind.port=20880&deprecated=false&dubbo=2.0.2&dyna mic=true&generic=false&interface=com.riemann.service.HelloService&methods=sayHello ,sayHelloWithPrint,sayHelloWithTransmission,sayHelloWithException&pid=30998&rele ase=2.7.5&side=provider&telnet=clear,exit,help,status,log,ls,ps,cd,pwd,invoke,tr ace,count,select,shutdown×tamp=1583906801486
    URL providerUrl = this.getProviderUrl(originInvoker);
    // 获取进行注册override协议的访问地址
	// provider://192.168.1.102:20880/com.riemann.service.HelloService? anyhost=true&application=dubbo-demo-annotation- provider&bind.ip=192.168.1.102&bind.port=20880&category=configurators&check=fals e&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.riemann.se rvice.HelloService&methods=sayHello,sayHelloWithPrint,sayHelloWithTransmission,s ayHelloWithException&pid=30998&release=2.7.5&side=provider&telnet=clear,exit,hel p,status,log,ls,ps,cd,pwd,invoke,trace,count,select,shutdown×tamp=158390680 1486
    URL overrideSubscribeUrl = this.getSubscribedOverrideUrl(providerUrl);
    // 增加override的监听器
    RegistryProtocol.OverrideListener overrideSubscribeListener = new RegistryProtocol.OverrideListener(overrideSubscribeUrl, originInvoker);
    this.overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
    // 根据现有的override协议,对注册地址进行改写操作
    providerUrl = this.overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
    // 对当前的服务进行本地导出
	// 完成后即可在看到本地的20880端口号已经启动,并且暴露服务
    RegistryProtocol.ExporterChangeableWrapper<T> exporter = this.doLocalExport(originInvoker, providerUrl);
    // 获取真实的注册中心, 比如我们常用的ZookeeperRegistry
    Registry registry = this.getRegistry(originInvoker);
    // 获取当前服务需要注册到注册中心的providerURL,主要用于去除一些没有必要的参数(比如在本地导出时所使用的qos参数等值)
	// dubbo://192.168.1.102:20880/com.riemann.service.HelloService?anyhost=true&application=dubbo-demo-annotation- provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.riemann.service.HelloService&methods=sayHello,sayHelloWithPrint,sayHelloWithTransmi ssion,sayHelloWithException&pid=30998&release=2.7.5&side=provider&telnet=clear,e xit,help,status,log,ls,ps,cd,pwd,invoke,trace,count,select,shutdown×tamp=15 83906801486
    URL registeredProviderUrl = this.getUrlToRegistry(providerUrl, registryUrl);
    // 获取当前url是否需要进行注册参数
    boolean register = providerUrl.getParameter("register", true);
    if (register) {
    	// 将当前的提供者注册到注册中心上去
        this.register(registryUrl, registeredProviderUrl);
    }
	// 对override协议进行注册,用于在接收到override请求时做适配,这种方式用于适配2.6.x及之 前的版本(混用)
    registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
    // 设置当前导出中的相关信息
    exporter.setRegisterUrl(registeredProviderUrl);
    exporter.setSubscribeUrl(overrideSubscribeUrl);
    // 返回导出对象(对数据进行封装)
    return new RegistryProtocol.DestroyableExporter(exporter);
}

(4)、下面我们再来看看 register 方法, 这里面做的比较简单,主要是从 RegistoryFactory 中获取注册中心,并且进行地址注册。

public void register(URL registryUrl, URL registeredProviderUrl) {
	// 获取注册中心
    Registry registry = this.registryFactory.getRegistry(registryUrl);
    // 对当前的服务进行注册
    registry.register(registeredProviderUrl);
    // ProviderModel 表示服务提供者模型,此对象中存储了与服务提供者相关的信息。 
    // 比如服务的配置信息,服务实例等。每个被导出的服务对应一个 ProviderModel。
    ProviderModel model = ApplicationModel.getProviderModel(registeredProviderUrl.getServiceKey());
    model.addStatedUrl(new RegisterStatedURL(registeredProviderUrl, registryUrl, true));
}

(5)、这里我们再跟里面的register方法之前,先来介绍一下Registry中的类目录结构

+- RegistryService
| +- Registry
| | +- AbstractRegistry
| | | +- FailbackRegistry
| | | | +- ZookeeperRegistry | | | | +- NacosRegistry
| | | | +- ...

目录结构描述如下:

  • 在这里每个层级代表继承自父级
  • 这里面 RegistryService 就是我们之前所讲对外提供注册机制的接口。
  • 其下面 Registry 也同样是一个接口,是对 RegistryService 的集成,并且继承了 Node 接口, 说明注册中心也是基于URL去做的。
  • AbstractRegistry 是对注册中心的封装,其主要会对本地注册地址的封装,主要功能在于远程注册中心不可用的时候,可以采用本地的注册中心来使用。
  • FailbackRegistry 从名字中可以看出来,失败自动恢复,后台记录失败请求,定时重发功能。
  • 最深的一层则更多是真实的第三方渠道实现。

(6)、下面我们来看一下在 FailbackRegistry 中的实现, 可以在这里看到他的主要作用是调用第三方的 实现方式,并且在出现错误时增加重试机制。

public void register(URL url) {
    if (!this.acceptable(url)) {
        this.logger.info("URL " + url + " will not be registered to Registry. Registry " + url + " does not accept service of this protocol type.");
    } else {
    	// 上层调用
		// 主要用于保存已经注册的地址列表
        super.register(url);
        // 将一些错误的信息移除(确保当前地址可以在出现一些错误的地址时可以被删除)
        this.removeFailedRegistered(url);
        this.removeFailedUnregistered(url);

        try {
        	// 发送给第三方渠道进行注册操作
            this.doRegister(url);
        } catch (Exception var6) {
            Throwable t = var6;
            // 记录日志
            boolean check = this.getUrl().getParameter("check", true) && url.getParameter("check", true) && !"consumer".equals(url.getProtocol());
            boolean skipFailback = var6 instanceof SkipFailbackWrapperException;
            if (check || skipFailback) {
                if (skipFailback) {
                    t = var6.getCause();
                }

                throw new IllegalStateException("Failed to register " + url + " to registry " + this.getUrl().getAddress() + ", cause: " + ((Throwable)t).getMessage(), (Throwable)t);
            }

            this.logger.error("Failed to register " + url + ", waiting for retry, cause: " + var6.getMessage(), var6);
            // 后台异步进行重试,也是Failback比较关键的代码
            this.addFailedRegistered(url);
        }

    }
}

(7)、下面我们再来看看ZookeeperdoRegister 方法的实现, 可以看到这里的实现也比较简单,关键在于 toUrlPath 方法的实现。关于 dynamic 的值,我们也在上面有看到,他的URL也是true的。

public void doRegister(URL url) {
    try {
    	// 进行创建地址
        this.zkClient.create(this.toUrlPath(url), url.getParameter("dynamic", true));
    } catch (Throwable var3) {
        throw new RpcException("Failed to register " + url + " to zookeeper " + this.getUrl() + ", cause: " + var3.getMessage(), var3);
    }
}

(8)、解读 toUrlPath 方法。可以看到这里的实现也是比较简单,也验证了我们之前的路径规则。

private String toUrlPath(URL url) {
	// 分类地址 + url字符串
    return this.toCategoryPath(url) + "/" + URL.encode(url.toFullString());
}

private String toCategoryPath(URL url) {
	// 服务名称 + category(在当前的例子中是providers)
    return this.toServicePath(url) + "/" + url.getParameter("category", "providers");
}

private String toServicePath(URL url) {
	// 接口地址
    String name = url.getServiceInterface();
    // 根节点 + 接口地址
    return "*".equals(name) ? this.toRootPath() : this.toRootDir() + URL.encode(name);
}

三、URL规则详解 和 服务本地缓存

1、URL规则详解

URL地址如下:

protocol://host:port/path?key=value&key=value
provider://192.168.20.1:20883/com.riemann.service.HelloService?anyhost=true&application=service-provider2&bind.ip=192.168.20.1&bind.port=20883&category=configurators&check=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.riemann.service

URL主要有以下几部分组成:

  • protocol: 协议,一般像我们的 provider 或者 consumer 在这里都是人为具体的协议
  • host: 当前 provider 或者其他协议所具体针对的地址,比较特殊的像 override 协议所指定的 host就是 0.0.0.0 代表所有的机器都生效
  • port: 和上面相同,代表所处理的端口号
  • path: 服务路径,在 provider 或者 consumer 等其他中代表着我们真实的业务接口
  • key=value: 这些则代表具体的参数,这里我们可以理解为对这个地址的配置。比如我们 provider 中需要具体机器的服务应用名,就可以是一个配置的方式设置上去。

注意:Dubbo中的URLjava中的URL是有一些区别的,如下:

  • 这里提供了针对于参数的 parameter 的增加和减少(支持动态更改)
  • 提供缓存功能,对一些基础的数据做缓存.

2、服务本地缓存

在上面我们有讲到dubbo有对路径进行本地缓存操作。这里我们就对本地缓存进行讲解。

dubbo调用者需要通过注册中心(例如:ZK)注册信息,获取提供者,但是如果频繁往从ZK获取信息,肯定会存在单点故障问题,所以dubbo提供了将提供者信息缓存在本地的方法。

Dubbo在订阅注册中心的回调处理逻辑当中会保存服务提供者信息到本地缓存文件当中(同步/异步两 种方式),以URL纬度进行全量保存。

Dubbo在服务引用过程中会创建registry对象并加载本地缓存文件,会优先订阅注册中心,订阅注册中心失败后会访问本地缓存文件内容获取服务提供信息。

(1)、首先从构造方法讲起, 这里方法比较简单,主要用于确定需要保存的文件信息。并且从系统中读取 已有的配置信息。

public AbstractRegistry(URL url) {
    this.setUrl(url);
    // Start file save timer
    this.syncSaveFile = url.getParameter("save.file", false);
    // 默认保存路径(home/.dubbo/dubbo-registry-appName-address-port.cache)
    String defaultFilename = System.getProperty("user.home") + "/.dubbo/dubbo-registry-" + url.getParameter("application") + "-" + url.getAddress().replaceAll(":", "-") + ".cache";
    String filename = url.getParameter("file", defaultFilename);
    // 创建文件
    File file = null;
    if (ConfigUtils.isNotEmpty(filename)) {
        file = new File(filename);
        if (!file.exists() && file.getParentFile() != null && !file.getParentFile().exists() && !file.getParentFile().mkdirs()) {
            throw new IllegalArgumentException("Invalid registry cache file " + file + ", cause: Failed to create directory " + file.getParentFile() + "!");
        }
    }

    this.file = file;
    // 加载已有的配置文件
    this.loadProperties();
    this.notify(url.getBackupUrls());
}

(2)、我们可以看到这个类中最为关键的一个属性为 properties ,我们可以通过寻找,得知这个属性的设置值只有在一个地方: saveProperties ,我们来看一下这个方法。这里也有一个我们值得关注的点,就是基于版本号的的更改。

private void saveProperties(URL url) {
    if (this.file != null) {
        try {
            StringBuilder buf = new StringBuilder();
            // 获取所有通知到的地址
            Map<String, List<URL>> categoryNotified = (Map)this.notified.get(url);
            if (categoryNotified != null) {
                Iterator var4 = categoryNotified.values().iterator();

                while(var4.hasNext()) {
                    List<URL> us = (List)var4.next();

                    URL u;
                    for(Iterator var6 = us.iterator(); var6.hasNext(); buf.append(u.toFullString())) {
                        u = (URL)var6.next();
                        // 多个地址进行拼接
                        if (buf.length() > 0) {
                            buf.append(' ');
                        }
                    }
                }
            }
			// 保存数据
            this.properties.setProperty(url.getServiceKey(), buf.toString());
            // 保存为一个新的版本号
			// 通过这种机制可以保证后面保存的记录,在重试的时候,不会重试之前的版本
            long version = this.lastCacheChanged.incrementAndGet();
            // 需要同步保存则进行保存
            if (this.syncSaveFile) {
                this.doSaveProperties(version);
            } else {
            	// 否则则异步去进行处理
                this.registryCacheExecutor.execute(new AbstractRegistry.SaveProperties(version));
            }
        } catch (Throwable var8) {
            this.logger.warn(var8.getMessage(), var8);
        }

    }
}

(3)、下面我们再来看看是如何进行保存文件的。这里的实现也比较简单,主要比较关键的代码在于利用文件级锁来保证同一时间只会有一个线程执行。

public void doSaveProperties(long version) {
    if (version >= this.lastCacheChanged.get()) {
        if (this.file != null) {
        	// Save
            try {
            	// 使用文件级别所,来保证同一段时间只会有一个线程进行读取操作
                File lockfile = new File(this.file.getAbsolutePath() + ".lock");
                if (!lockfile.exists()) {
                    lockfile.createNewFile();
                }

                RandomAccessFile raf = new RandomAccessFile(lockfile, "rw");
                Throwable var5 = null;

                try {
                    FileChannel channel = raf.getChannel();
                    Throwable var7 = null;

                    try {
                    	// 利用文件锁来保证并发的执行的情况下,只会有一个线程执行成功(原因在于可能是跨VM的)
                        FileLock lock = channel.tryLock();
                        if (lock == null) {
                            throw new IOException("Can not lock the registry cache file " + this.file.getAbsolutePath() + ", ignore and retry later, maybe multi java process use the file, please config: dubbo.registry.file=xxx.properties");
                        }
						// Save
                        try {
                            if (!this.file.exists()) {
                                this.file.createNewFile();
                            }
							// 将配置的文件信息保存到文件中
                            FileOutputStream outputFile = new FileOutputStream(this.file);
                            Throwable var10 = null;

                            try {
                                this.properties.store(outputFile, "Dubbo Registry Cache");
                            } catch (Throwable var73) {
                                var10 = var73;
                                throw var73;
                            } finally {
                                if (outputFile != null) {
                                    if (var10 != null) {
                                        try {
                                            outputFile.close();
                                        } catch (Throwable var72) {
                                            var10.addSuppressed(var72);
                                        }
                                    } else {
                                        outputFile.close();
                                    }
                                }

                            }
                        } finally {
                        	// 解开文件锁
                            lock.release();
                        }
                    } catch (Throwable var76) {
                        var7 = var76;
                        throw var76;
                    } finally {
                        if (channel != null) {
                            if (var7 != null) {
                                try {
                                    channel.close();
                                } catch (Throwable var71) {
                                    var7.addSuppressed(var71);
                                }
                            } else {
                                channel.close();
                            }
                        }

                    }
                } catch (Throwable var78) {
                    var5 = var78;
                    throw var78;
                } finally {
                    if (raf != null) {
                        if (var5 != null) {
                            try {
                                raf.close();
                            } catch (Throwable var70) {
                                var5.addSuppressed(var70);
                            }
                        } else {
                            raf.close();
                        }
                    }

                }
            } catch (Throwable var80) {
            	// 执行出现错误时,则交给专门的线程去进行重试
                this.savePropertiesRetryTimes.incrementAndGet();
                if (this.savePropertiesRetryTimes.get() >= 3) {
                    this.logger.warn("Failed to save registry cache file after retrying 3 times, cause: " + var80.getMessage(), var80);
                    this.savePropertiesRetryTimes.set(0);
                    return;
                }

                if (version < this.lastCacheChanged.get()) {
                    this.savePropertiesRetryTimes.set(0);
                    return;
                }

                this.registryCacheExecutor.execute(new AbstractRegistry.SaveProperties(this.lastCacheChanged.incrementAndGet()));
                this.logger.warn("Failed to save registry cache file, will retry, cause: " + var80.getMessage(), var80);
            }

        }
    }
}

四、Dubbo 消费过程分析

服务消费流程

我的架构梦:(二十六)Dubbo源码分析之服务注册与消费源码剖析_第3张图片

首先 ReferenceConfig 类的 init 方法调用 createProxy() ,期间使用 Protocol 调用 refer 方法生成 Invoker 实例(如上图中的红色部分),这是服务消费的关键。接下来使用ProxyFactoryInvoker 转换为客户端需要的接口(如:HelloService)。

你可能感兴趣的:(我的架构梦)