dubbo对注册中心的分装

695 阅读10分钟

Dubbo最主要干2个事情。第一个,对服务的导出和引入。第二个,维护注册中心的服务。对于第一个,尽量参考Dubbo官方文档,有详细解释。对于第二个,这边详细说说。

首先找到服务注册和引用的入口RegistryProtocol,这部分不清楚的话,可以参考文档Dubbo官方文档-服务导出

public void register(URL registryUrl, URL registedProviderUrl) {
  Registry registry = registryFactory.getRegistry(registryUrl);
  registry.register(registedProviderUrl);
}
@Override
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
	// ----------------------此处省略一堆代码------------------------
  registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
	// ----------------------此处省略一堆代码------------------------
}

private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
	// ----------------------此处省略一堆代码------------------------
  registry.register(subscribeUrl.addParameters(Constants.CATEGORY_KEY, Constants.CONSUMERS_CATEGORY,
                                                 Constants.CHECK_KEY, String.valueOf(false)));
  // ----------------------此处省略一堆代码------------------------
}

上面省略了大量的代码,只是把关于registry的几个调用点贴出来了。可以看到有registersubscribe两个行为。然后看看RegistryService接口中,总共有多少行为。

public interface RegistryService {
		// 向注册中心注册url
    void register(URL url);
		// 从注册中心删除(卸载)url
    void unregister(URL url);
		// 向注册中心注册对某个目录的监听
    void subscribe(URL url, NotifyListener listener);
		// 从注册中心删除(卸载)对某个目录的监听
    void unsubscribe(URL url, NotifyListener listener);
		// 从注册中心查询服务
    List<URL> lookup(URL url);
}

RegistryService可以看到除了上面出现的2个行为,还有2个反向的。还有一个查询。很容易理解。把RegistryService中的5个行为抽象出来之后,就可以有很好的扩展性。可以适配rediszookeeper,数据库,或者开发者自定义实现方式。

注册过程分为2步

1.向注册中心发送注册(Register)请求(具体注册的内容和存储形式要看使用的是什么注册中心)

2.向注册中心发起订阅请求(订阅的具体内容,和订阅形式要看使用的是什么注册中心)

删除注册过程分为2步

1.向注册中心发送删除注册(UnRegister)请求

2.向注册中心发起取消订阅(Unsubscribe)请求

看下类结构图:

结构很清晰。Dubbo给出了4种实现。先从Zookeeper开始说。看下示意图。

zookeeper作为注册中心

服务启动并且注册完成后,每个服务都会在zookeeper上建立4个永久目录。可以从上图中看到。找到代码ZookeeperRegistry.toCategoriesPath。可以看到4个目录的建立是在这个地方设置的。

private String[] toCategoriesPath(URL url) {
// ----------------------此处省略一堆代码------------------------		
		categories = new String[]{Constants.PROVIDERS_CATEGORY, Constants.CONSUMERS_CATEGORY,
                Constants.ROUTERS_CATEGORY, Constants.CONFIGURATORS_CATEGORY};
// ----------------------此处省略一堆代码------------------------
}

其中configuratorsrouters并不常用,主要在配置和服务治理的时候才会用到。常用的是providersconsumers。如果一个服务有N个Provider,那在providers下面会产生N个子节点。同样的,如果一个服务有N个Consumer,那在consumers下面会产生N个子节点。关于子节点中对服务的描述会比较长,这边给出一个示意图,调试代码的可以直接登录到Zookeeper查看自己的服务情况。

注册过程是在zookeeper上建立目录并且把服务的描述信息写进去。除此之外还需要订阅它关注的服务。服务也就是目录(Dubbo把它抽象为Directory)。ProviderConsumer都有个自己关心的几个目录。

Provider只关心configurators,因为它不关心有多少Consumer,或者Consumer的路由策略。所以Provider会监视configurators

Consumer会关心configuratorsroutersproviders,它需要关心各个provider的动态,和自己的路由策略。所以Consumer会分别监视configuratorsroutersproviders

对于所监视的目录,只要children发生任何变化都会触发childChanged事件。然后做出相应处理。

比如:Consumer监视providers的动态,当/dubbo/com.xxx.XxxService/providers目录下减少服务时。会触发Consumer重新搜有这个目录下所有的服务,并且重新维护Consumer的服务列表(具体对服务列表的维护需要关注Dubbo Directory模块的代码),客户代码再次调用这个服务时,被删除的服务节点不会再收到服务请求。

Dubbo为了兼容多个zk客户端,还搞了一个ZookeeperTransprter层。官方给出了2种实现,分别是curatorzkClient。看下类图结构。

还是比较清晰的。看下ZookeeperTransporter的代码。

@SPI("curator")// 默认使用curator
public interface ZookeeperTransporter {

		// 建立连接,并且返回一个ZookeeperClient,后续的一些列对zookeeper的操作都需要以ZookeeperClient为委托。
    ZookeeperClient connect(URL url);
}

public class CuratorZookeeperTransporter implements ZookeeperTransporter {

  	// 与zookeeper建立连接的过程就是new CuratorZookeeperClient,连接的过程在构造方法中。
    @Override
    public ZookeeperClient connect(URL url) {
        return new CuratorZookeeperClient(url);
    }
}

Zookeeper建立连接的过程就是new CuratorZookeeperClient,连接的过程在构造方法中。

public class CuratorZookeeperClient extends AbstractZookeeperClient<CuratorWatcher> {

    public CuratorZookeeperClient(URL url) {
        super(url);
        try {
            CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder()
                    .connectString(url.getBackupAddress())
                    .retryPolicy(new RetryNTimes(1, 1000))
                    .connectionTimeoutMs(5000);
            String authority = url.getAuthority();
            if (authority != null && authority.length() > 0) {
                builder = builder.authorization("digest", authority.getBytes());
            }
            client = builder.build();
						// ----------------------此处省略一堆代码------------------------
            client.start();// 建立连接
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }
}

ZookeeperTransporter的抽象,不仅抽象了connet的连接过程。还抽象了对节点的所有操作行为。看下ZookeeperClient的所有行为。

public interface ZookeeperClient {
		// 创建节点(节点目录,是否是零时节点)
    void create(String path, boolean ephemeral);
		// 删除节点(节点目录)
    void delete(String path);
		// 获取节点下的children
    List<String> getChildren(String path);
		// 添加对children的监听
    List<String> addChildListener(String path, ChildListener listener);
		// 删除对children的监听
    void removeChildListener(String path, ChildListener listener);
		// 添加zookeeper状态监听
    void addStateListener(StateListener listener);
		// 删除zookeeper状态监听
    void removeStateListener(StateListener listener);
		// 是否处于连接状态
    boolean isConnected();
  	// 关闭连接
    void close();
    URL getUrl();
}

上述抽象行为几乎覆盖了Zookeeper主要操作,DubboZookeeper的行为抽象不需要覆盖Zookeeper的所有操作,只需要关注Dubbo本身是否够用,够用即可。通过上面的类图可以看到ZookeeperClient有两个实现,分别是CuratorZookeeperClientZkclientZookeeperClient。这边看下Dubbo默认用的CuratorZookeeperClient。代码较多,这边只列出部分以示说明。

public class CuratorZookeeperClient extends AbstractZookeeperClient<CuratorWatcher> {
		
  	// 建立连接后,持有的客户端。也就是ZookeeperClient的委托。所有的实际节点的操作都转交给它
    private final CuratorFramework client;

    public CuratorZookeeperClient(URL url) {
        // 建立连接
    }

    @Override
    public void createPersistent(String path) {
        try {// 创建永久节点,用client委托
            client.create().forPath(path);
        } catch (NodeExistsException e) {
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }

    @Override
    public void createEphemeral(String path) {
        try {// 创建临时节点,用client委托
            client.create().withMode(CreateMode.EPHEMERAL).forPath(path);
        } catch (NodeExistsException e) {
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }

    @Override
    public void delete(String path) {
        try {// 删除节点,用client委托
            client.delete().forPath(path);
        } catch (NoNodeException e) {
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }

    @Override
    public List<String> getChildren(String path) {
        try {// 获取子节点,用client委托
            return client.getChildren().forPath(path);
        } catch (NoNodeException e) {
            return null;
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }
}

以上的示例以及解释,能够从结构上搞清楚ZookeeperTransporter

redis作为注册中心

Dubbo在使用Redis作为注册中心时,有2个小细节需要提醒下。

1.当redis设置了密码。那么就需要在配置时加上密码。会这么配置。

<dubbo:registry protocol="redis" address="redis://host:6379" password="password" />

启动会发现报错了。

java.lang.IllegalArgumentException: Invalid url, password without username!

说是没有设置username,但是Redis没有用户名这一说啊。怎么搞?

我之前看过一篇文章,解决方案是把Dubbo的源代码改了,把抛出IllegalArgumentException的地方注释掉,然后用修改后的class替换上去。方法能OK。但个人感觉没必要哈。Dubbo既然检查username,那就随便给它来一个。

<dubbo:registry protocol="redis" address="redis://host:6379" password="password" username="anything" />

上述代码是RedisRegistry在初始化Redis连接的部分代码。可以看到在连接Redis时,只是使用了password,而没有使用username,也就是说,username写成啥都行。那不就简单绕过去了么。没有必要去修改人家源代码。

2.Redis有多个DB的,Dubbo默认使用0号DB,怎么自定义呢?

还是上面这个问题的文章,解决方案是把JedisPool的源码修改下,在初始化的时候,把DB给写死,我去,这操作。。。也没的说了。

其实这个问题很简单,Dubbo是可以配置param的。

看上图,可以看到Dubbo这边取的是db.index这个paramerter。那就给它一个呗。

<dubbo:registry protocol="redis" address="redis://host:6379" password="password" username="anything">
		<dubbo:parameter key="db.index" value="1"></dubbo:parameter>
</dubbo:registry>

以上两个问题,直接看下源码就能猜到解决方案了。下面说下RedisRegistry的实现。

Zookeeper不同的是,Dubbo没有给RedisTransporter的适配。不知道是什么原因。不管了,看下RedisRegistry

RedisRegistry的构造方法。用来创建Redis连接。

public RedisRegistry(URL url) {
  // ----------------------此处省略一堆代码------------------------  
  for (String address : addresses) {
        int i = address.indexOf(':');
        String host;
        int port;
        if (i > 0) {
            host = address.substring(0, i);
            port = Integer.parseInt(address.substring(i + 1));
        } else {
            host = address;
            port = DEFAULT_REDIS_PORT;
        }
    		// 建立连接
        this.jedisPools.put(address, new JedisPool(config, host, port,
                url.getParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT), StringUtils.isEmpty(url.getPassword()) ? null : url.getPassword(),
                url.getParameter("db.index", 0)));
    }
  // ----------------------此处省略一堆代码------------------------
}
public void doRegister(URL url) {
  // ----------------------此处省略一堆代码------------------------
  jedis.hset(key, value, expire);// 写入redis
  jedis.publish(key, Constants.REGISTER);// 发布register消息
  // ----------------------此处省略一堆代码------------------------
}

public void doUnregister(URL url) {
	// ----------------------此处省略一堆代码------------------------
  jedis.hdel(key, value);// 从redis删除key
  jedis.publish(key, Constants.UNREGISTER);// 发布unregister消息
  // ----------------------此处省略一堆代码------------------------           
}

doRegister是做注册动作,使用hset方法写入redis。写完redis,还要发布register消息,通知关注此服务的节点。如果当前服务是Provider,发布register消息后,关注此服务的Consumer就会接到消息,并且从Redis再次全量获取服务列表,拉回去之后刷新自己的维护的服务列表。

doUnRegister是删除注册动作,也就是卸载服务,使用hdel方法删除Rediskey,还要发布unregister消息,通知关注此服务的节点。如果当前服务是Provider,发布unregister消息后,关注此服务的Consumer就会接到消息,并且从Redis再次全量获取服务列表,拉回去之后刷新自己的维护的服务列表。

@Override
public void doSubscribe(final URL url, final NotifyListener listener) {
    String service = toServicePath(url);// 结构:/dubbo/com.xxx.XxxService
    Notifier notifier = notifiers.get(service);
    if (notifier == null) {
        Notifier newNotifier = new Notifier(service);
        notifiers.putIfAbsent(service, newNotifier);
        notifier = notifiers.get(service);
        if (notifier == newNotifier) {
          	// 每个服务都会搞一个线程,这个线程用来向redis订阅对当前这个服务的所有监听
          	// channel = "/dubbo/com.xxx.XxxService/*"
            notifier.start();
        }
    }
    // 下面这部分代码省略了,这边稍微说下它的作用
		// 监听这个服务之后,首次先把当前这个服务中下的节点信息拿出来做一个初始化的update通知。具体可以再看看代码理解下
}
private class Notifier extends Thread {
  	// ----------------------此处省略一堆代码------------------------    
    @Override
    public void run() {
        while (running) {// 死循环,直到running被置为false
            try {
                if (!isSkip()) {
                    try {
                        for (Map.Entry<String, JedisPool> entry : jedisPools.entrySet()) {
                            JedisPool jedisPool = entry.getValue();
                            try {
                                jedis = jedisPool.getResource();
                                try {
                                    if (service.endsWith(Constants.ANY_VALUE)) {
                                        if (!first) {
                                            first = false;
                                            Set<String> keys = jedis.keys(service);
                                            if (keys != null && !keys.isEmpty()) {
                                                for (String s : keys) {
                                                    doNotify(jedis, s);
                                                }
                                            }
                                            resetSkip();
                                        }
                                      	// 订阅服务 
                                      	// channel结构:/dubbo/com.xxx.XxxService/*
                                        jedis.psubscribe(new NotifySub(jedisPool), service); // blocking
                                    } else {
                                        if (!first) {
                                            first = false;
                                            doNotify(jedis, service);
                                            resetSkip();
                                        }
                                      	// 订阅服务 
                                      	// channel结构:/dubbo/com.xxx.XxxService/*
                                        jedis.psubscribe(new NotifySub(jedisPool), service + Constants.PATH_SEPARATOR + Constants.ANY_VALUE); // blocking
                                    }
                                    break;
                                } finally {
                                    jedis.close();
                                }
                            } catch (Throwable t) { // Retry another server
                                logger.warn("Failed to subscribe service from redis registry. registry: " + entry.getKey() + ", cause: " + t.getMessage(), t);
                                // If you only have a single redis, you need to take a rest to avoid overtaking a lot of CPU resources
                                sleep(reconnectPeriod);
                            }
                        }
                    } catch (Throwable t) {
                        logger.error(t.getMessage(), t);
                        sleep(reconnectPeriod);
                    }
                }
            } catch (Throwable t) {
                logger.error(t.getMessage(), t);
            }
        }
    }
		// ----------------------此处省略一堆代码------------------------    
}
private class NotifySub extends JedisPubSub {
		// ----------------------此处省略一堆代码------------------------
    @Override
    public void onMessage(String key, String msg) {
        if (logger.isInfoEnabled()) {
            logger.info("redis event: " + key + " = " + msg);
        }
      	// 检查是是否是register或者unregister
        if (msg.equals(Constants.REGISTER)
                || msg.equals(Constants.UNREGISTER)) {
            try {
                Jedis jedis = jedisPool.getResource();
                try {
                    doNotify(jedis, key);// 通知
                } finally {
                    jedis.close();
                }
            } catch (Throwable t) { // TODO Notification failure does not restore mechanism guarantee
                logger.error(t.getMessage(), t);
            }
        }
    }
		// ----------------------此处省略一堆代码------------------------
}

从上面3部分代码,可以完整明白,Redis的注册,订阅,消息发布,消息接收并且通知等过程。

DubboRedis还是有些偏心啊,没有给它做Transporter层。