Dubbo服务注册发现的实现

以Zookeeper作为Dubbo服务的注册中心为例, 先来看看如何使用:

到webapps/ROOT/WEB-INF下,有一个dubbo.properties文件,里面指向Zookeeper ,使用的是Zookeeper 的注册中心

服务端配置

客户端配置:


    
  
      
  

从配置上看, 可以以ApplicationConfig,RegistryConfig,ServiceConfig,ReferenceConfig这几个类为入口来分析.

 

这几个类主要存放配置信息, 需要关注:

1, dubbo是如何将配置类转变为spring上下文中的bean,

2, 如何暴露服务,

3, 在暴露服务的时候,

4, 是如何在zookeeper上注册的,

5, 客户端是如何发现服务的,

6, 如何发起远程服务调用的,

7, 服务端在收到请求之后, 是如何找到对应的服务的?

 

1,spring 配置读取,解析, 再到生成bean, 放到spring上下文的过程.

dubbo自定义了名称空间"dubbo",

spring支持自定义名称空间, 需要以下几步操作

  1. 继承抽象类NamespaceHandlerSupport, 在子类中调用registerBeanDefinitionParser方法, 注册解析器, 如

registerBeanDefinitionParser("service",new DubboBeanDefinitionParser(ServiceBean.class,true));

说明 dubbo:service, 最终会生成ServiceBean, 解析转换的细节是spring的源码范畴, 不再深究.

  1. 在META-INF下, 编写xsd定义文件

其中, dubbo.xsd 是xsd定义文件, spring.handlers, 指定了dubbo名称空间节点解析器,spring.schemas配置告诉名称空xsd文件在哪里.从com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler中可以看到配置节点对应生成的bean实例:

registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));

registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));

registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));

registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));

registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));

registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));

registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));

registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));

registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));

registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));

我们着重来看ServiceBean和ReferenceBean, 这两个分别涉及到服务的暴露及引用.

从ServiceBean的类继承关系及实现接口来看:

public class ServiceBean extends ServiceConfig implements InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener, BeanNameAware {

继承自ServiceConfig将得到配置属性及暴露服务等相关方法.

实现InitializingBean接口, spring在实例化完成之后, 将自动调用afterPropertiesSet方法做初始化

实现DisposableBean接口, spring在容器销毁的时候, 会调用destroy方法.

实现ApplicationContextAware接口, spring会给这个bean注入ApplicationContext, serviceBean中通过applicationContext抓了很多bean注进来.

实现了ApplicationListener接口, 会监听spring的特有的应用生命周期事件 onApplicationEvent, ServiceBean监听ContextRefreshedEvent事件, 再上下文初始化完成之后, 如果服务未暴露(export)再暴露一下.

实现了BeanNameAware 接口, 将beanName设置为beanid.

从serviceBean的afterPropertiesSet逻辑可以看出, 在读取配置到ServiceConfig后, 在上下文中, 根据ServiceConfig配置属性找到对应的bean注入, 完了调用ServiceConfig的export() 方法暴露服务.

export() 方法做了很多初始化属性(找相关bean来注入), 某些属性如果未配置, 使用默认值注入, 还有就是一些校验逻辑.

继续跟踪到export 跟踪到doExportUrls():

private void doExportUrls() {

        List registryURLs = loadRegistries(true);

        for (ProtocolConfig protocolConfig : protocols) {

            doExportUrlsFor1Protocol(protocolConfig, registryURLs);

        }

    }

先获取所有注册中心地址, 可能配置了多个, 就是下面这个配置

这里只使用了zookeeper作为注册中心.

断点调试查到registryURLs  为:

registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=hello-world-app&check=false&dubbo=2.0.0&pid=47708®istry=zookeeper&subscribe=false×tamp=1467342589223

然后根据配置的协议(protocols), 来暴露服务, 如果未配置协议, 默认的是:dubbo

配置在dubbo-default.properties  dubbo.provider.protocol=dubbo.

protocols 会从ProviderConfig里取,在ServiceConfig判空写默认值的时候, 实例化了ProviderConfig, 完了会给这个实例写入默认值, 其中的protocols就是从默认配置文件里取的.

private void checkDefault() {

        if (provider == null) {

            provider = new ProviderConfig();

        }

        appendProperties(provider);

    }

再回到doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List registryURLs)方法看看具体的实现, 这个方法比较长, 吐槽一下写得不是很好, 缩进嵌套太深, 可读性差.

1, 方法一开始先从protocolConfig上取协议名称, 如果取不到仍然默认协议为"dubbo"

================2~5步为了得到需要暴露的服务的主机IP=======================

2, 从protocolConfig上取host, 取不到, 从provider上取host. (对应dubbo:protocol的host属性, 官方手册解释为

服务主机名,多网卡选择或指定VIP及域名时使用,为空则自动查找本机IP,-建议不要配置,让Dubbo自动获取本机IP)

3, 如果host是无效的本地地址(isInvalidLocalHost):

host == null

                    || host.length() == 0

                    || host.equalsIgnoreCase("localhost")

                    || host.equals("0.0.0.0")

                    || (LOCAL_IP_PATTERN.matcher(host).matches());

通过InetAddress.自动获取本机IP.

4, 如果仍然是无效的或者是本地地址,  遍历注册中心的url, 发起socket连接, 然后通过socket.getLocalAddress().getHostAddress()得到主机地址

5, 还是得不到无效的或者是本地地址的话, 通过NetUtils.getLocalHost()得到主机ip, 这个方法通过遍历本地网卡,返回第一个合理的IP。

================2~5步为了得到需要暴露的服务的主机IP=======================

================6~7 为了得到端口========================

6, 从protocolConfig上取端口, 取不到从provider上取, 仍然取不到的话, 从dubboProtocol上获取默认端口(20880)

final int defaultPort = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(name).getDefaultPort();

上面分析了这个name, 如果没配置的话, 取的dubbo. 这个Extension取到的是DubboProtocol.

7, 如果配置了协议但是XxxProtocol上没有默认端口, 那就随机生成一个端口.通过NetUtils.getAvailablePort(defaultPort)取得.

================6~7 为了得到端口========================

8, 准备一些公共默认参数值, 如Constants.SIDE_KEY,Constants.DUBBO_VERSION_KEY,Constants.TIMESTAMP_KEY等, 写入各核心实例(application,module,provider,protocolConfig,serviceConfig等)

9,如果有配置method子标签, 如:

    

        

        

        

    

10, 遍历method子标签

  1. 写入method的重试次数值, (从service标签上取, 来自第8步的map), 如果没有的话写0
  2. 遍历method下面的argument子标签,
    1. 与这个要暴露的服务的接口方法匹配, 用index或者type属性来匹配, 并做标记存到map里;
    2. 看手册说这个argument配置主要用来反向代理回调用的, 暂时没看到关于callback的处理, 可能到了实际调用的时候, 才会用到.

====================9~10 处理method及其argument子标签, 看配置手册, 还有parameter子标签, 这里没处理.

11, 如果是泛化实现. 往上面那个map写入标记generic = true;methods Constants.ANY_VALUE (就是一个*号, 表示任意方法)

(

泛接口实现方式主要用于服务器端没有API接口及模型类元的情况,参数及返回值中的所有POJO均用Map表示,通常用于框架集成,比如:实现一个通用的远程服务Mock框架,可通过实现GenericService接口处理所有服务请求。)

如果不是泛化实现,  给map写入revision, 取的是接口类的版本. 然后看看这个接口有没有包装器类, 要把所有包装器类的method都加进来, 用逗号隔开拼一个字符串写入methods

12, 如果有配置要求token, 默认的话, 随机给一个uuid, 写入map, 否则就用配置给定的token值. token作令牌验证用的.

13, 如果协议是"injvm", 就不需要注册.并且给map标记notify=false

14, 从protocolConfig上取得ContextPath, 如果为空, 从providerConfig上取.

15, 用host, port, contextPath等创建URL.

16, 如果这个url使用的协议(如dubbo)存在ConfiguratorFactory的扩展, 调用对应的扩展来配置修改url

目前只有override,absent, 用于向注册中心修改注册信息.

override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&timeout=1000

17, 从url中获取scope信息, 如果scope=none 啥都不做, 不暴露服务.

18, 如果scope != remote, 就在本地暴露服务

if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {

                exportLocal(url);

 }

但是exportLocal方法里头, 只有当url不是injvm(LOCAL_PROTOCOL)的时候, 才去做一些暴露操作

也就是说injvm 在exportLocal里什么都不做

if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {

    URL local = URL.valueOf(url.toFullString())

            .setProtocol(Constants.LOCAL_PROTOCOL)

            .setHost(NetUtils.LOCALHOST)

            .setPort(0);

    Exporter exporter = protocol.export(

            proxyFactory.getInvoker(ref, (Class) interfaceClass, local));

    exporters.add(exporter);

    logger.info("Export dubbo service " + interfaceClass.getName() +" to local registry");

}

19, 如果scope配置不是local. 遍历每一个注册中心url

如果存在monitor, 则在url参数里添加monitor信息.

20, 通过proxyFactory将url, 接口类型转化成invoker

proxyFactory在这里是由javassist实现的, 也就是JavassistProxyFactory

@SPI("javassist")

public interface ProxyFactory{

在JavassistProxyFactory的getInvoker中, 会去找这个接口类的Wrapper类

21, 通过protocol将invoker暴露出去

Exporter exporter = protocol.export(invoker);

这里protocol 根据url中的registry协议, 尝试去获取RegistryProtocol

Protocol是如何知道要根据url的协议来创建Protocol?

 

在ServiceConfig中, Protocol一开始初始化是:

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

这说明是在调用方法的时候, 再决定用哪个扩展

下面是javassist生成的Protocol$Adpative字节码代码, 一目了然.

从下面这个调用链可以看出, registryProtocol不存在, 然后去创建, 然后注入各种属性, 属性实例不存在, 又调用各种create扩展的方法, ..

最后得到RegistryProtocol, 并执行其export方法.

断点执行时发现, 在调用registryProtocol的export方法之前, 还调用了两个Protocol的Wrapper:

根据ExtensionLoader里的逻辑可知, 只要某个类实现了Protocol接口, 又有Protocol类型入参的构造方法, 可认为此类是Protocol的包装器类.

ExtensionLoader的getExtension方法只返回最后最外层的包装器类

所以从上图调用关系来看,RegistryProtocol的export方法最后才被执行.

这两个包装器类主要用来添加过滤器及监听器.

RegistryProtocol的export方法:

1, 先做本地服务暴露(doLocalExport()),调用DubboProtocol export一下.

完了之后, 将"dubbo://192.168...."这个url作为key,绑定刚才暴露返回的exporter.

DubboProtocol export过程涉及底层具体协议(如Netty)创建服务的细节,不再深入探讨.

2,获得注册中心对象Registry(registryFactory根据协议zookeeper,知道返回的是ZookeeperRegistry

3, 可以看到往zookeeper上注册,实际就是调用zkClient往zk树上创建一个路径节点.

zkClient.create(toUrlPath(url),url.getParameter(Constants.DYNAMIC_KEY,true));

4,根据"dubbo://...."的注册url, 转换得到一个"provider://..."的URL, 绑定到一个OverrideListener, registry 订阅这个provider url的变化通知,并交由对应的listener处理.

5,最后实例化一个Exporter返回.

ServiceConfig 最后将exporter缓存, 至此, 以上便是服务暴露的过程.

总结一下就是在配置解析读取装配成bean之后初始化根据配置协议找到注册中心(Zookeeper)注册找到对应服务Protocol(DubboProtocol)暴露服务.

下面再来看看服务引用的过程:

从ReferenceBean看起, 这个类继承自ReferenceConfig, 因此可以得到关于dubbo:Reference的配置属性, 实现了FactoryBean,ApplicationContextAware,InitializingBean,DisposableBean接口:

实现FactoryBean接口, 定制实例化bean的逻辑, 可以定制返回的bean实例,返回是否单例, 返回实例的类型.

实现ApplicationContextAware, spring会给注入ApplicationContext

实现InitializingBean, 会自动调用afterPropertiesSet, 完成实例化之后的一些初始化工作

实现DisposableBean接口, spring会自动调用destroy方法, 做资源销毁或者回收操作.

ReferenceBean的配置解析装配没啥好说的, 主要看afterPropertiesSet方法, 初始化做了哪些事情:

afterPropertiesSet中貌似仍然做了一大堆初始化的事情, 一直到最后:

if (b != null && b.booleanValue()) {

            getObject();

 }

getObject();会触发ReferenceConfig的init方法:

public synchronized T get() {

        if (destroyed){

            throw new IllegalStateException("Already destroyed!");

        }

            if (ref == null) {

                    init();

            }

            return ref;

    }

init 逻辑:

写入默认值之类的逻辑不再详细说明,核心逻辑:

ref=createProxy(map);

map 写了很多配置参数及公共参数, 默认参数等等.

通过注册中心配置拼装URL

Listus=loadRegistries(false);

调试过程中,抓取的us值

registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=consumer-of-helloworld-app&check=false&dubbo=2.0.0&pid=408500®istry=zookeeper×tamp=1467478634195

invoker=refprotocol.refer(interfaceClass,urls.get(0));

这里 refprotocol 是RegistryProtocol, 因为传入的url参数是registry协议

Protocol refprotocol=ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

RegistryProtocol中的refer方法:

1, 将"registry://..."协议的url转成"zookeeper://..."的协议url

2, registryFactory据此得到对应的Registry类, 我们使用zookeeper做实验, 实际就是ZookeeperRegistry

3,调用doRefer方法,

  1. 创建RegistryDirectory,
  2. 构建订阅url , 调试得到consumer://192.168.99.1/com.linzp.dubbo.test.DemoService?application=consumer-of-helloworld-app&dubbo=2.0.0&interface=com.linzp.dubbo.test.DemoService&methods=sayHello&pid=408500&side=consumer×tamp=1467478613998
  3. 将此comsumer订阅url, 到注册中心注册
  4. 注册目录(RegistryDirectory)订阅subscribeUrl的通知, 此过程中会把invoker封装为invokerDelegate并在RegistryDirectory中缓存urlInvokerMap
  5. 最后由cluster.join(directory)返回invoker实例

cluster实际是FailoverCluster(Cluster接口上有@SPI(FailoverCluster.NAME))

join方法直接返回new FailoverClusterInvoker(directory)

  1. 最后使用proxyFactory为invoker创建代理返回.
  2. proxyFactory.getProxy(invoker);
  3. proxyFactory 此刻是JavassistProxyFactory,从它的getProxy中可以得知
  4. return(T)Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));

这里给interfaces接口创建代理(字节码实例), 并传入InvokerInvocationHandler实例化. 具体字节码代理生成逻辑有待深究.

DemoService demoService = (DemoService)context.getBean("demoService");

得到的bean实际就是这个代理实例

当调用时, String hello = demoService.sayHello("world");

实际发起调用的是包装了invoker的InvokerInvocationHandler对象.

下面是断点调试的截图, 可以发现InvokerInvocationHandler中的invoker变成了MockClusterInvoker, 是由代理实例生成的, 暂时不知道为啥传入的是MockClusterInvoker

跟进到MockClusterInvoker 的invoker继续观察, 可以看到这时的invoker是FailoverClusterInvoker, 怀疑是在字节码代理类中使用了包装器类

继续跟进, 由于FailoverClusterInvoker中没有invoke方法, 可以找到是在父类AbstractClusterInvoker中实现的.

 

父类做了一些负载均衡的逻辑, 最后调用doInvoke抽象方法, 在FailoverClusterInvoker中实现的:

doInvoke从父类的select方法得到一个负载均衡计算后的invoker, 并调用:

Result result = invoker.invoke(invocation);

继续往上跟踪, 一直跟到DubboInvoker的doInvoke方法

这个方法里头使用ExchangeClient发起远程调用

return (Result)currentClient.request(inv,timeout).get();

这个再往下挖, 就是基于具体rpc协议(如netty)层面上的调用了, 不再此专题细究.

总结一下客户端发起远程调用的大致思路是:

将配置转化为子节码生成的代理实例存到spring上下文中调用时通过代理实例找到相关协议方法发起远程调用.

你可能感兴趣的:(分布式,系统架构)