RPC_03_Dubbo注册中心

4 篇文章 0 订阅


Dubbo注册中心

Dubbo微服务体系中,注册中心是核心组件之一。Dubbo通过注册中心实现了分布式环境中各服务之间的注册与发现,是各个分布式节点之间的纽带。


一、注册中心概述

1. 主要作用

  • 动态加入
  • 动态发现
  • 动态调整
  • 统一配置

2. 主要模块

注册中心源码存在于dubbo-registry中,包含五个子模块

  • 基本api和抽象类
  • zookeeper实现
  • redis实现
  • 基于内存的default实现
  • multicast模式的实现

*官方推荐的是基于zookeeper的实现

若需要对Dubbo注册中心进行扩展,则可以基于RegistryFactory和Registry进行扩展。

3.工作流程

  • 服务提供者启动,向注册中心写入元数据信息,并订阅配置元数据信息
  • 消费者启动,向注册中心写入元数据信息,并订阅服务提供者、路由和配置元数据信息
  • 服务治理中心(dubbo-admin)启动时,会订阅所有的消费者、服务提供者、路由和配置元数据信息
  • 有新的提供者加入或者有提供者离开时,注册中心中的服务提供者目录会发生变化,变化信息会动态通知给消费者和服务治理中心
  • 消费者发生调用时,会异步地将调用、统计信息上报给监控中心(dubbo-monitor-simple)

二、数据结构

1. ZooKeeper

采用树形结构进行存储,结构如下

  • /dubbo (分组名称,默认为dubbo,由<dubbo:registry>指定)
    • service (接口名称,com.xxx.Xxxx)
      下面的节点均为持久节点
      • providers (包含多个服务者URL元数据信息)
      • consumers (包含多个消费者URL元数据信息)
      • routers (包含多个用于消费者的URL元数据信息)
      • configurators (包含多个用于服务者的动态配置URL元数据信息)

*服务元数据中所有参数都是以键值对的形式存储的

2. Redis

采用Root、Service、Type、URL四层结构,采用key-Map结构实现了存储,结构如下:

RedisKey - RedisValue [ MapKey - MapValue ]

*RedisValue是Redis原生的Map结构,MapValue存储超时时间


三、订阅/发布

订阅和发布是注册中心的核心功能之一,可以自动将服务提供者的状态通知给消费者和服务治理中心,并更新本地的配置信息。

  • 服务提供者的注册是为了让消费者感知服务的存在,从而进行远程调用,也为了让服务治理中心感知提供者的上线
  • 消费者的注册是为了让服务治理中心可以发现自己

1. ZooKeeper实现

发布和取消的实现

  • 发布只需要在zk的指定路径上创建对应的url即可
  • 取消只需要将对应的路径删除即可

订阅的实现

订阅的实现有pull(客户端定时拉取)和push(注册中心主动推送)两种,Dubbo采取启动时拉取,之后接收事件重新拉取的模式。

  • 服务端暴露服务时,订阅configurators,用于监听动态配置
  • 消费端启动时,订阅providers、routers和configurators,分别对应提供者路由动态配置变更通知

Dubbo在dubbo-remoting-zookeeper模块中实现了zk客户端的统一封装,采用了两种开源客户端:

  • Apache Curator
  • zkClient
    *可以使用<dubbo:registry>的client属性设置不同客户端的实现,默认采用Curator

订阅的流程
采取事件通知+客户端拉取的方式

  • 客户端第一次连接客户中心时,下载全量数据
  • 客户端在订阅的节点上注册一个watcher,与注册中心保持TCP长连接
  • 在每个节点发生变化时,注册中心根据watcher的回调主动通知客户端(事件通知)
  • 客户端收到事件通知后,会把对应节点的全量数据拉取下来(客户端拉取,可以配置相关限制)

*zk的每个节点都有一个版本号,当某个节点的数据发生变化时,对应的版本号就会发生变化,并触发watcher事件,将数据推送给订阅方

zk全量订阅相关代码
代码来自zookeeperRegistry.doSubscribe方法

// 判断是否为全量订阅
if ("*".equals(url.getServiceInterface())) {
	String root = this.toRootPath();
	// 获取缓存中当前url的所有listener
	ConcurrentMap<NotifyListener, ChildListener> listeners = (ConcurrentMap)this.zkListeners.get(url);
	// 若为空,则说明缓存中还未添加当前url的listeners,进行初始化
	if (listeners == null) {
	    this.zkListeners.putIfAbsent(url, new ConcurrentHashMap());
	    listeners = (ConcurrentMap)this.zkListeners.get(url);
	}
	// 从缓存中获取传入的listener的子listener
	ChildListener zkListener = (ChildListener)listeners.get(listener);
	// 若zkListener为空,则新建
	if (zkListener == null) {
	    listeners.putIfAbsent(listener, new ChildListener() {
	    	// 子节点发生变更时,执行的内部方法
	        public void childChanged(String parentPath, List<String> currentChilds) {
	            Iterator i$ = currentChilds.iterator();
				// 遍历当前节点的所有子节点
	            while(i$.hasNext()) {
	                String child = (String)i$.next();
	                child = URL.decode(child);
	                // 若该节点的子节点还没有订阅,则进行订阅
	                if (!ZookeeperRegistry.this.anyServices.contains(child)) {
	                    ZookeeperRegistry.this.anyServices.add(child);
	                    ZookeeperRegistry.this.subscribe(url.setPath(child).addParameters(new String[]{"interface", child, "check", String.valueOf(false)}), listener);
	                }
	            }
	
	        }
	    });
	    zkListener = (ChildListener)listeners.get(listener);
	}
	// 在zk中创建根节点(持久节点)
	this.zkClient.create(root, false);
	// 向根节点添加子节点(对应当前url的节点,可能已有,可能本次新建)
	List<String> services = this.zkClient.addChildListener(root, zkListener);
	if (services != null && !services.isEmpty()) {
	    Iterator i$ = services.iterator();
		// 遍历所有子节点进行订阅
	    while(i$.hasNext()) {
	        String service = (String)i$.next();
	        service = URL.decode(service);
	        this.anyServices.add(service);
	        this.subscribe(url.setPath(service).addParameters(new String[]{"interface", service, "check", String.valueOf(false)}), listener);
	    }
	}
}

zk类别订阅相关代码
代码同样来自zookeeperRegistry.doSubscribe方法

List<URL> urls = new ArrayList();
// 由url获取一组要订阅的路径
String[] arr$ = this.toCategoriesPath(url);
int len$ = arr$.length;
// 与全量订阅代码相似,查找缓存,若缓存中没有对应节点,则创建新的节点
for(int i$ = 0; i$ < len$; ++i$) {
    String path = arr$[i$];
    ConcurrentMap<NotifyListener, ChildListener> listeners = (ConcurrentMap)this.zkListeners.get(url);
    if (listeners == null) {
        this.zkListeners.putIfAbsent(url, new ConcurrentHashMap());
        listeners = (ConcurrentMap)this.zkListeners.get(url);
    }

    ChildListener zkListener = (ChildListener)listeners.get(listener);
    if (zkListener == null) {
        listeners.putIfAbsent(listener, new ChildListener() {
            public void childChanged(String parentPath, List<String> currentChilds) {
                ZookeeperRegistry.this.notify(url, listener, ZookeeperRegistry.this.toUrlsWithEmpty(url, parentPath, currentChilds));
            }
        });
        zkListener = (ChildListener)listeners.get(listener);
    }

    this.zkClient.create(path, false);
    List<String> children = this.zkClient.addChildListener(path, zkListener);
    if (children != null) {
        urls.addAll(this.toUrlsWithEmpty(url, path, children));
    }
}
// 调用方法,更新本地缓存
this.notify(url, listener, urls);

*在订阅过程中,根据订阅的不同类别,更新不同的缓存数据,如:订阅providers,更新Invoker服务列表;订阅routers,更新本地路由规则列表;订阅configuators,更新或覆盖本地动态参数列表

2. Redis实现

发布的实现

  • 采用过期机制publish/subscribe通道
  • 发布时,会在Redis中创建相关的key(有过期时间),并发布一条register事件消息
  • 发布者需要定时刷新key的过期时间,超时则被视为下线
  • 若某次刷新时Redis中key已过期,则应在通道中广播自身的上线事件

下线的实现

  • 若主动下线,则主动在通道中广播unregister事件消息,消费者收到后会更新本地缓存的服务列表
  • 若key超时下线,则不会进行广播,需要依赖服务治理中心(dubbo-admin),服务治理中心进行定时调度和清理下线的服务,并广播对应服务的unregister事件,保证数据的一致性

四、缓存机制

为了避免每次调用都从注册中心获取服务列表,注册中心采用缓存机制,空间换时间,具体代码在抽象类AbstractRegistry中。

  • 本地缓存在内存中会保存一份(Properties对象),在磁盘上也会持久化一份(File对象引用)
  • 服务初始化时,会从磁盘中将数据读取到Properties对象中
  • 缓存可以同步保存和异步保存(采用线程池,具有重试机制)

五、重试机制

所有的Registry都继承了FailbackRegistry,其中定义了一个ScheduledExecutorService,用来定时(默认5000ms)调用retry方法,重试之前失败的操作。


六、注册中心的重点设计模式

1. 模板模式

  • AbstractRegistry实现了Registry接口中的注册、订阅、查询、通知等方法,实现了缓存持久化方法
  • FailbackRegistry继承AbstractRegistry,重写并完善了默认方法,添加了重试机制,添加了四个未实现的模板方法
  • 具体的ZookeeperRegistry和RedisRegistry都继承了FailbackRegistry类,并进行了自己的实现

2. 工厂模式

  • 所有注册中心实例,都是通过对应的工厂创建/获取来的,采用单例模式
  • 可以根据url中的相关值(protocol)使用工厂类获取对应的注册中心

RegistryFactory接口

@SPI("dubbo")
public interface RegistryFactory {
	// 用来获取注册中心的方法,具体实现由子类定义,实际调用对象由url指定
    @Adaptive({"protocol"})
    Registry getRegistry(URL var1);
}

参考

  • 深入理解 Apache Dubbo 与实战
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值