最近在作分布式相关的工做,因为人手不够只能我一我的来怼;看着这段时间的加班表想一想就是够惨的。java
不过其中也有遇到的很多有意思的事情从此再拿来分享,今天重点来讨论服务的注册与发现。node
个人业务比较简单,只是须要知道如今有哪些服务实例可供使用就能够了(并非作远程调用,只须要拿到信息便可)。git
要实现这一功能最简单的方式能够在应用中配置全部的服务节点,这样每次在使用时只须要经过某种算法从配置列表中选择一个就能够了。github
但这样会有一个很是严重的问题:算法
因为应用须要根据应用负载状况来灵活的调整服务节点的数量,这样个人配置就不能写死。apache
否则就会出现要么新增的节点没有访问或者是已经 down 掉的节点却有请求,这样确定是不行的。api
每每要解决这类分布式问题都须要一个公共的区域来保存这些信息,好比是否能够利用 Redis?缓存
每一个节点启动以后都向 Redis 注册信息,关闭时也删除数据。服务器
其实就是存放节点的 ip + port
,而后在须要知道服务节点信息时候只须要去 Redis 中获取便可。网络
以下图所示:
但这样会致使每次使用时都须要频繁的去查询 Redis,为了不这个问题咱们能够在每次查询以后在本地缓存一份最新的数据。这样优先从本地获取确实能够提升效率。
但一样又会出现新的问题,若是服务提供者的节点新增或者删除消费者这边根本就不知道状况。
要解决这个问题最早想到的应该就是利用定时任务按期去更新服务列表。
以上的方案确定不完美,而且不优雅。主要有如下几点:
因此咱们须要一个更加靠谱的解决方案,这样的场景其实和 Dubbo 很是相似。
用过的同窗确定对这张图不陌生。
引用自 Dubbo 官网
其中有一块很是核心的内容(红框出)就是服务的注册与发现。
一般来讲消费者是须要知道服务提供者的网络地址(ip + port)才能发起远程调用,这块内容和我上面的需求其实很是相似。
而 Dubbo 则是利用 Zookeeper 来解决问题。
在具体讨论怎么实现以前先看看 Zookeeper 的几个重要特性。
Zookeeper 实现了一个相似于文件系统的树状结构:
这些节点被称为 znode(名字叫什么不重要),其中每一个节点均可以存放必定的数据。
最主要的是 znode 有四种类型:
root-1
)考虑下上文使用 Redis 最大的一个问题是什么?
其实就是不能实时的更新服务提供者的信息。
那利用 Zookeeper 是怎么实现的?
主要看第三个特性:瞬时节点
Zookeeper 是一个典型的观察者模式。
这样咱们就能够实时获取服务节点的信息,同时也只须要在第一次获取列表时缓存到本地;也不须要频繁和 Zookeeper 产生交互,只用等待通知更新便可。
而且无论应用什么缘由节点 down 掉后也会在 Zookeeper 中删除该信息。
这样实现方式就变为这样。
为此我新建了一个应用来进行演示:
就是一个简单的 SpringBoot 应用,只是作了几件事情。
我在本地启动了两个应用分别是:127.0.0.1:8083,127.0.0.1:8084
。来看看效果图。
两个应用启动完成:
当前 Zookeeper 的可视化树状结构:
当想知道全部的服务节点信息时:
想要获取一个可用的服务节点时:
这里只是采起了简单的轮询。
当 down 掉一个节点时:应用会收到通知更新本地缓存。同时 Zookeeper 中的节点会自动删除。
再次获取最新节点时:
当节点恢复时天然也能获取到最新信息。本地缓存也会及时更新。
实现起来倒也比较简单,主要就是 ZKClient 的 api 使用。
贴几段比较核心的吧。
启动注册 Zookeeper。
主要逻辑都在这个线程中。
/route
根节点,建立的时候会判断是否已经存在。/route
,这样才能在其余服务上下线时候得到通知。监听到服务变化
public void subscribeEvent(String path) {
zkClient.subscribeChildChanges(path, new IZkChildListener() {
@Override
public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception {
logger.info("清除/更新本地缓存 parentPath=【{}】,currentChilds=【{}】", parentPath,currentChilds.toString());
//更新全部缓存/先删除 再新增
serverCache.updateCache(currentChilds) ;
}
});
}
复制代码
能够看到这里是更新了本地缓存,该缓存采用了 Guava 提供的 Cache,感兴趣的能够查看以前的源码分析。
/** * 更新全部缓存/先删除 再新增 * * @param currentChilds */
public void updateCache(List<String> currentChilds) {
cache.invalidateAll();
for (String currentChild : currentChilds) {
String key = currentChild.split("-")[1];
addCache(key);
}
}
复制代码
同时在客户端提供了一个负载算法。
其实就是一个轮询的实现:
/** * 选取服务器 * * @return */
public String selectServer() {
List<String> all = getAll();
if (all.size() == 0) {
throw new RuntimeException("路由列表为空");
}
Long position = index.incrementAndGet() % all.size();
if (position < 0) {
position = 0L;
}
return all.get(position.intValue());
}
复制代码
固然这里能够扩展出更多的如权重、随机、LRU 等算法。
Zookeeper 天然是一个很棒的分布式协调工具,利用它的特性还能够有其余做用。
在实现注册、发现这一需求时,Zookeeper 其实并非最优选。
因为 Zookeeper 在 CAP 理论中选择了 CP(一致性、分区容错性),当 Zookeeper 集群有半数节点不可用时是不能获取到任何数据的。
对于一致性来讲天然没啥问题,但在注册、发现的场景下更加推荐 Eureka
,已经在 SpringCloud 中获得验证。具体就不在本文讨论了。
但鉴于个人使用场景来讲 Zookeeper 已经可以胜任。
本文全部完整代码都托管在 GitHub。
一个看似简单的注册、发现功能实现了,但分布式应用远远不止这些。
因为网络隔离以后带来的一系列问题还须要咱们用其余方式一一完善;后续会继续更新分布式相关内容,感兴趣的朋友不妨持续关注。
你的点赞与转发是最大的支持。