文档章节

分布式(一) 搞定服务注册与发现

crossoverJie
 crossoverJie
发布于 2018/08/27 08:40
字数 2291
阅读 608
收藏 13

背景

最近在做分布式相关的工作,由于人手不够只能我一个人来怼;看着这段时间的加班表想想就是够惨的。

不过其中也有遇到的不少有意思的事情今后再拿来分享,今天重点来讨论服务的注册与发现

分布式带来的问题

我的业务比较简单,只是需要知道现在有哪些服务实例可供使用就可以了(并不是做远程调用,只需要拿到信息即可)。

要实现这一功能最简单的方式可以在应用中配置所有的服务节点,这样每次在使用时只需要通过某种算法从配置列表中选择一个就可以了。

但这样会有一个非常严重的问题:

由于应用需要根据应用负载情况来灵活的调整服务节点的数量,这样我的配置就不能写死。

不然就会出现要么新增的节点没有访问或者是已经 down 掉的节点却有请求,这样肯定是不行的。

往往要解决这类分布式问题都需要一个公共的区域来保存这些信息,比如是否可以利用 Redis?

每个节点启动之后都向 Redis 注册信息,关闭时也删除数据。

其实就是存放节点的 ip + port,然后在需要知道服务节点信息时候只需要去 Redis 中获取即可。

如下图所示:

但这样会导致每次使用时都需要频繁的去查询 Redis,为了避免这个问题我们可以在每次查询之后在本地缓存一份最新的数据。这样优先从本地获取确实可以提高效率。

但同样又会出现新的问题,如果服务提供者的节点新增或者删除消费者这边根本就不知道情况。

要解决这个问题最先想到的应该就是利用定时任务定期去更新服务列表。

以上的方案肯定不完美,并且不优雅。主要有以下几点:

  • 基于定时任务会导致很多无效的更新。
  • 定时任务存在周期性,没法做到实时,这样就可能存在请求异常。
  • 如果服务被强行 kill,没法及时清除 Redis,这样这个看似可用的服务将永远不可用!

所以我们需要一个更加靠谱的解决方案,这样的场景其实和 Dubbo 非常类似。

用过的同学肯定对这张图不陌生。

引用自 Dubbo 官网

其中有一块非常核心的内容(红框出)就是服务的注册与发现。

通常来说消费者是需要知道服务提供者的网络地址(ip + port)才能发起远程调用,这块内容和我上面的需求其实非常类似。

而 Dubbo 则是利用 Zookeeper 来解决问题。

Zookeeper 能做什么

在具体讨论怎么实现之前先看看 Zookeeper 的几个重要特性。

Zookeeper 实现了一个类似于文件系统的树状结构:

这些节点被称为 znode(名字叫什么不重要),其中每个节点都可以存放一定的数据。

最主要的是 znode 有四种类型:

  • 永久节点(除非手动删除,节点永远存在)
  • 永久有序节点(按照创建顺序会为每个节点末尾带上一个序号如:root-1
  • 瞬时节点(创建客户端与 Zookeeper 保持连接时节点存在,断开时则删除并会有相应的通知)
  • 瞬时有序节点(在瞬时节点的基础上加上了顺序)

考虑下上文使用 Redis 最大的一个问题是什么?

其实就是不能实时的更新服务提供者的信息。

那利用 Zookeeper 是怎么实现的?

主要看第三个特性:瞬时节点

Zookeeper 是一个典型的观察者模式。

  • 由于瞬时节点的特点,我们的消费者可以订阅瞬时节点的父节点。
  • 当新增、删除节点时所有的瞬时节点也会自动更新。
  • 更新时会给订阅者发起通知告诉最新的节点信息。

这样我们就可以实时获取服务节点的信息,同时也只需要在第一次获取列表时缓存到本地;也不需要频繁和 Zookeeper 产生交互,只用等待通知更新即可。

并且不管应用什么原因节点 down 掉后也会在 Zookeeper 中删除该信息。

效果演示

这样实现方式就变为这样。

为此我新建了一个应用来进行演示:

https://github.com/crossoverJie/netty-action/tree/master/netty-action-zk

就是一个简单的 SpringBoot 应用,只是做了几件事情。

  • 应用启动时新开一个线程用于向 Zookeeper 注册服务。
  • 同时监听一个节点用于更新本地服务列表。
  • 提供一个接口用于返回一个可有的服务节点。

我在本地启动了两个应用分别是:127.0.0.1:8083,127.0.0.1:8084。来看看效果图。

两个应用启动完成:


当前 Zookeeper 的可视化树状结构:


当想知道所有的服务节点信息时:


想要获取一个可用的服务节点时:

这里只是采取了简单的轮询。


当 down 掉一个节点时:应用会收到通知更新本地缓存。同时 Zookeeper 中的节点会自动删除。


再次获取最新节点时:


当节点恢复时自然也能获取到最新信息。本地缓存也会及时更新。

编码实现

实现起来倒也比较简单,主要就是 ZKClient 的 api 使用。

贴几段比较核心的吧。

注册

启动注册 Zookeeper。

主要逻辑都在这个线程中。

  • 首先创建父节点。如上图的 Zookeeper 节点所示;需要先创建 /route 根节点,创建的时候会判断是否已经存在。
  • 接着需要判断是否需要将自己注册到 Zookeeper 中,因为有些节点只是用于服务发现,他自身是不需要承担业务功能(是我自己项目的需求)。
  • 将当前应用的所在 ip 以及端口注册上去,同时需要监听根节点 /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 其实并不是最优选。

由于 Zookeeper 在 CAP 理论中选择了 CP(一致性、分区容错性),当 Zookeeper 集群有半数节点不可用时是不能获取到任何数据的。

对于一致性来说自然没啥问题,但在注册、发现的场景下更加推荐 Eureka,已经在 SpringCloud 中得到验证。具体就不在本文讨论了。

但鉴于我的使用场景来说 Zookeeper 已经能够胜任。

总结

本文所有完整代码都托管在 GitHub。

https://github.com/crossoverJie/netty-action

一个看似简单的注册、发现功能实现了,但分布式应用远远不止这些。

由于网络隔离之后带来的一系列问题还需要我们用其他方式一一完善;后续会继续更新分布式相关内容,感兴趣的朋友不妨持续关注。

你的点赞与转发是最大的支持。

© 著作权归作者所有

crossoverJie

crossoverJie

粉丝 732
博文 99
码字总数 191646
作品 0
江北
后端工程师
私信 提问
加载中

评论(2)

crossoverJie
crossoverJie 博主

引用来自“让BEETL从地球上消失”的评论

zookeeper适合做服务注册吗???? 没问题吗??
后文有提到。
让BEETL从地球上消失
zookeeper适合做服务注册吗???? 没问题吗??
Springcloud应用在阿里云Kubernetes上的IP互通实践

问题 在应用微服务化方案中,Springcloud是比较常见的选择,毕竟其对于Java 的程序员来说比较友好,基于Springboot的编程方式也使得门槛比较低。但是在将Springcloud的应用运行到Kubernetes容...

了哥-duff
2018/06/25
0
0
SpringCloud微服务实战---服务的注册和发现(Eureka)

一、Spring Cloud简介 Spring Cloud是基于SpringBoot的,为开发人员提供了快速构建分布式系统的一些工具,包括配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布...

jamesese
2018/07/30
0
0
Netty + ZooKeeper 实现简单的服务注册与发现

一. 背景 最近的一个项目:我们的系统接收到上游系统的派单任务后,会推送到指定的门店的相关设备,并进行相应的业务处理。 二. Netty 的使用 在接收到派单任务之后,通过 Netty 推送到指定门...

Tony沈哲
06/18
0
0
SpringCloud学习之soa基础

一、soa简单介绍   1)面向服务的架构(SOA)是一个组件模型,它将应用程序的不同功能单元(称为服务)通过这些服务之间定义良好的接口和契约联系起来。SOA是解决复杂业务模块,提高扩展性...

java~nick
2017/11/13
0
0
SpringCloud - 学习笔记

一、SpringCloud简介 SpringCloud是一个基于SpringBoot实现的云应用开发工具,为开发人员提供了快速构建分布式系统的一些工具,包括配置管理、服务发现、断路器、路由、微代理、控制总线、全...

Mr_欢先生
03/29
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Jenkins World 贡献者峰会及专家答疑展位

本文首发于:Jenkins 中文社区 原文链接 作者:Marky Jackson 译者:shunw Jenkins World 贡献者峰会及专家答疑展位 本文为 Jenkins World 贡献者峰会活动期间的记录 Jenkins 15周岁啦!Jen...

Jenkins中文社区
34分钟前
8
0
杂谈:面向微服务的体系结构评审中需要问的三个问题

面向微服务的体系结构如今风靡全球。这是因为更快的部署节奏和更低的成本是面向微服务的体系结构的基本承诺。 然而,对于大多数试水的公司来说,开发活动更多的是将现有的单块应用程序转换为...

liululee
48分钟前
7
0
OSChina 周二乱弹 —— 我等饭呢,你是不是来错食堂了?

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @ 自行车丢了:给主编推荐首歌 《クリスマスの夜》- 岡村孝子 手机党少年们想听歌,请使劲儿戳(这里) @烽火燎原 :国庆快来,我需要长假! ...

小小编辑
今天
520
9
玩转 Springboot 2 之热部署(DevTools)

Devtools 介绍 SpringBoot 提供了热部署的功能,那啥是热部署累?SpringBoot官方是这样说的:只要类路径上的文件发生更改,就会自动重新启动应用程序。在IDE中工作时,这可能是一个有用的功能...

桌前明月
今天
6
0
CSS--列表

一、列表标识项 list-style-type none:去掉标识项 disc:默认实心圆 circle:空心圆 squire:矩形 二、列表项图片 list-style-img: 取值:url(路径) 三、列表项位置 list-style-position:...

wytao1995
今天
10
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部