基于 Zookeeper 实现服务注册和服务发现

news2025/2/22 13:06:18

文章目录

  • 前言
  • 声明
  • 前置知识
    • 服务注册和发现
    • Zookeeper
  • 工作原理
  • 实现过程
    • 注册中心
    • 服务注册
    • 服务发现
  • 总结

前言

无论是采用SOA还是微服务架构,都需要使用服务注册和服务发现组件。我刚开始接触 Dubbo 时一直对服务注册/发现以及 Zookeeper 的作用感到困惑,现在看来是因为对分布式系统的理解不够深入,对 Dubbo 和 Zookeeper 的工作原理不够清楚。

本文将基于 Zookeeper 实现服务注册和服务发现功能,如果跟我一样有同样的困惑,希望可以通过本文了解其他组件如何使用 Zookeeper 作为注册中心的工作原理。

声明

文章中所提供的代码仅供参考,旨在帮助缺乏基础知识的开发人员更好地理解服务注册和服务发现的概念。请注意,这些代码并不适用于实际应用中

前置知识

服务注册和发现

在SOA或微服务架构中,由于存在大量的服务以及可能的相互调用,为了更有效地管理这些服务,我们通常需要引入一个统一的地方,即注册中心,来集中管理它们,而注册中心最基本的功能就是服务注册/发现。

  • 服务注册:将该服务实例的元数据(如IP地址、端口号、健康状态等)注册到注册中心,这样其他服务或客户端可以发现和使用该服务。
  • 服务发现:当一个服务需要调用别的服务时,使用静态配置是不可行的,这个时候可以去注册中心获取可用的服务实例并调用。

Zookeeper

Zookeeper 是一个传统的分布式协调服务,它更多的被用来作为一个协调器使用,比如来协调管理 Hadoop 集群、协调 Kafka 的 leader 选举等。

为什么会有组件将其视为一个注册中心使用?我想有几个原因:

  1. Zookeeper 在分布式系统中具有更强的一致性和可靠性,可以确保各个服务的注册信息保持一致。
  2. Zookeeper 使用内存存储数据,具有很高的读写性能。这对于注册中心来说非常关键,因为它需要快速地响应客户端的请求。
  3. Zookeeper 的 Watcher 机制可以让客户端监听指定节点的变化。当某个节点(注册中心)发生变化时,Zookeeper 可以通知其他服务实现实时更新。

工作原理

以下图为例,可以看到 Dubbo 是如何使用 Zookeeper 实现服务注册/发现的。

在这里插入图片描述

  1. 服务提供者向 /dubbo/com.foo.BarService/providers 目录下写入自己的 URL 地址。
  2. 服务消费者订阅 /dubbo/com.foo.BarService/providers 目录下的提供者 URL 地址。并向 /dubbo/com.foo.BarService/consumers 目录下写入自己的 URL 地址。

这里的目录就是 Zookeeper 的数据结构,原理非常简单,本质上就是服务提供者和消费者按照约定在 Zookeeper 上读写数据,同时借用其 Watcher 机制、临时节点和可靠性等特性高效的实现以下功能:

  • 当提供者服务出现断电等异常停机时,注册中心能自动删除提供者信息。
  • 当注册中心重启时,能自动恢复注册数据以及订阅请求。

实现过程

注册中心

下面通过 Zookeeper 的 Java API 实现一个只包含服务注册/发现的注册中心,代码如下:

public class RegistrationCenter {

    // 连接信息
    private String connectString = "192.168.10.11:2181,192.168.10.11:2182,192.168.10.11:2183";

    // 超时时间
    private int sessionTimeOut = 30000;

    private final String ROOT_PATH = "/servers";

    private ZooKeeper client;

    public RegistrationCenter() {
        this(null);
    }

    public RegistrationCenter(Consumer<List<String>> consumer) {
        try {
            getConnection(null == consumer ? null : watchedEvent -> {
                //监听服务器地址的上下线
                if (watchedEvent.getType() == Watcher.Event.EventType.NodeChildrenChanged) {
                    try {
                        consumer.accept(subServers());
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            Stat stat = client.exists(ROOT_PATH, false);
            if (stat == null) {
                //创建根节点
                client.create(ROOT_PATH, ROOT_PATH.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * @param serverName 将服务器注册到zk集群时,所需的服务名称
     * @param metadata   服务元数据
     * @throws Exception
     */
    public void doRegister(String serverName, Metadata metadata) throws Exception {


        /**
         * ZooDefs.Ids.OPEN_ACL_UNSAFE: 此权限表示允许所有人访问该节点(服务器)
         * CreateMode.EPHEMERAL_SEQUENTIAL: 由于服务器是动态上下线的,上线后存在,下线后不存在,所以是临时节点
         * 而服务器一般都是有序号的,所以是临时、有序的节点.
         */
        String node = client.create(ROOT_PATH + "/" + serverName, metadata.toString().getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

        System.out.println(serverName + " 已经上线");
    }


    /**
     * 发现/订阅服务
     */
    public List<String> subServers() throws InterruptedException, KeeperException {
        List<String> zkChildren = client.getChildren(ROOT_PATH, true);
        List<String> servers = new ArrayList<>();
        zkChildren.forEach(node -> {
            //拼接服务完整信息
            try {
                byte[] data = client.getData(ROOT_PATH + "/" + node, false, null);
                servers.add(new String(data));
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        return servers;
    }

    private void getConnection(Watcher watcher) throws IOException {
        this.client = new ZooKeeper(connectString, sessionTimeOut, watcher);
    }

    /**
     * 服务元数据
     */
    public static class Metadata {
        public Metadata() {
        }

        public Metadata(String ip, int port) {
            this.ip = ip;
            this.port = port;
        }

        private String ip;

        private int port;

        public String getIp() {
            return ip;
        }

        public void setIp(String ip) {
            this.ip = ip;
        }

        public int getPort() {
            return port;
        }

        public void setPort(int port) {
            this.port = port;
        }

        @Override
        public String toString() {
            return "{" + "ip='" + ip + '\'' + ", port=" + port + '}';
        }
    }
}

该类中两个核心方法:doRegister()subServers() 服务注册和订阅。

  • doRegister()主要是往 Zookeeper 中创建了一个临时节点数据,临时节点的优势就是当服务出现断电等异常停机时,节点会自动删除。
  • subServers()则是去 Zookeeper 读取了所有服务提供者的信息,并且监听了节点状态,当节点发生创建、删除、更新等事件时重新获取服务者的信息,做到数据实时更新。

至此,一个简单的注册中心就完成了,当然,如果要实现一个成熟的注册中心,还要考虑负载均衡、高可用性和容错、服务治理和路由控制等功能,这里先不展开。

服务注册

当有了注册中心,服务提供者就可以调用 doRegister() 进行注册,代码如下:

public class ProviderServer {
    public static void main(String[] args) throws Exception {
        RegistrationCenter registrationCenter = new RegistrationCenter();
        registrationCenter.doRegister("provider", new RegistrationCenter.Metadata("127.0.0.1", 8080));
        Thread.sleep(Long.MAX_VALUE);
    }
}

服务发现

同样,服务消费者可以调用 subServers() 发现服务提供者,同时当服务提供者发生变化时会通知到消费者。代码如下:

public class ConsumerServer {
    public static void main(String[] args) throws Exception {
        RegistrationCenter registrationCenter = new RegistrationCenter(newServers -> {
            System.out.println("服务更新了..."+newServers);
        });
        List<String> servers = registrationCenter.subServers();
        System.out.println(servers);
        Thread.sleep(Long.MAX_VALUE);
    }
}

总结

服务注册和服务发现功能是为了解决分布式系统中的服务管理和通信问题而设计的,经过不断的发展与负载均衡、健康监测、服务治理和路由控制等功能完善成为一个注册中心。服务注册和服务发现有助于实现系统的弹性和可扩展性,因为新的服务实例可以动态地加入系统,而无需手动配置和修改已有的代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/969993.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

100天精通Python(可视化篇)——第99天:Pyecharts绘制多种炫酷K线图参数说明+代码实战

文章目录 专栏导读一、K线图介绍1. 说明2. 应用场景 二、配置说明三、K线图实战1. 普通k线图2. 添加辅助线3. k线图鼠标缩放4. 添加数据缩放滑块5. K线周期图表 书籍推荐 专栏导读 &#x1f525;&#x1f525;本文已收录于《100天精通Python从入门到就业》&#xff1a;本专栏专…

UI自动化之混合框架

什么是混合框架&#xff0c;混合框架就是将数据驱动与关键字驱动结合在一起&#xff0c;主要用来回归业务主流程&#xff0c;将核心流程串联起来。 上一篇我们写到了关键字驱动框架&#xff0c;关键字驱动框架是针对一个业务场景的单条测试用例的。 我们以163邮箱的登录到创建…

官方发布:Mac 版 Visual Studio IDE将于明年 8 月 31 日停止支持

近日&#xff0c;微软官方宣布&#xff1a;适用于 Mac 平台的 Visual Studio 集成开发环境&#xff08;IDE&#xff09;已经启动 "退休" 进程。Visual Studio for Mac 17.6 将继续支持 12 个月&#xff0c;持续到 2024 年 8 月 31 日。 微软表示在未来的 1 年内将重…

Matlab图像处理-中值滤波

中值滤波 中值滤波也是基于空间域&#xff08;邻域&#xff09;操作的&#xff0c;中值滤波是将像素邻域内灰度的中值来代替中心像素的值&#xff0c;把不同灰度的像素点看起来更接近于邻域内的像素点。 优点是可以很好的过滤掉椒盐噪声。中值滤波是非线性的图像处理方法&…

Matlab图像处理-均值滤波

均值滤波 均值滤波所使用的运算是卷积。均值滤波用邻域内像素的平均值来代替中心像素的值&#xff0c;相当于低通滤波&#xff0c;有将图像模糊化的趋势&#xff0c;对椒盐噪声基本无能为力。 在MATLAB中&#xff0c;可使用imfilter函数来实现线性空间滤波&#xff0c;该函数的…

被动操作系统指纹识别的强大功能可实现准确的物联网设备识别

到 2030 年&#xff0c;企业网络和互联网上的物联网设备数量预计将达到290 亿。这种指数级增长无意中增加了攻击面。 每个互连设备都可能为网络攻击和安全漏洞创造新的途径。Mirai 僵尸网络通过使用数千个易受攻击的 IoT 设备对关键互联网基础设施和热门网站发起大规模 DDoS 攻…

查看占用GPU资源的 进程所属docker容器,并杀死 docker 中的僵尸进程!

查看占用GPU资源的所属docker 进程&#xff0c;并杀死 docker 中的僵尸进程&#xff01; 问题描述&#xff1a;查看当前占用GPU资源的进程属于哪个Docker容器杀死 docker 中的僵尸进程 问题描述&#xff1a; 问题1&#xff1a;一台服务器&#xff0c;每个人在上面 run 一个容器…

企业主流全链路监控系统 - OpenTelemetry(二)

OpenTelemetry 二 4. 部署&#xff08;python&#xff09;准备工作&#xff08;1/5&#xff09;创建 HTTP Server&#xff08;2/5&#xff09;Automatic instrumentation&#xff08;3/5&#xff09;增加观测项&#xff08;Manual&#xff09;&#xff08;4/5&#xff09;向 Co…

win11右键菜单栏改回win10

1.右键 WIN 图标&#xff0c;点击 " 终端(管理员) ” 2.执行以下命令 reg add "HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32" /f /ve3.重启电脑

【c++随笔03】构造函数、析构函数、拷贝构造函数、移动构造函数

【c随笔03】构造函数、析构函数、拷贝构造函数、移动构造函数 一、构造函数1、为何要有构造函数&#xff1f;2、构造函数定义3、无参构造、带参构造4、构造函数注意事项4.1 构造函数是特殊的&#xff0c;不是常规的成员函数&#xff0c;不能直接调d1.Date() 。4.2 如果通过无参…

R语言数据管理

1.将变量加入列表中 > mydata<-data.frame(x1c(2,3,4,5),x2c(2,5,7,9)) > mydatax1 x2 1 2 2 2 3 5 3 4 7 4 5 9 > sumx<-x1x2 Error: object x1 not found > sumx<-mydata$x1mydata$x2 > sumx [1] 4 8 11 14 > ls() [1] "mydata&…

【Java 基础篇】StringBuilder的魔力:Java字符串处理探究

在Java编程中&#xff0c;字符串是一个常见的数据类型&#xff0c;用于存储文本信息。然而&#xff0c;与字符串相关的操作可能会导致性能问题&#xff0c;因为字符串是不可变的&#xff0c;每次对字符串进行操作都会创建一个新的字符串对象。为了解决这个问题&#xff0c;Java…

RT-Thread I/O设备模型(一)

I/O设备模型 绝大部分的嵌入式系统都包括一些I/O&#xff08;Input/Output&#xff0c;输入/输出&#xff09;设备&#xff0c;例如仪器上的数据显示屏&#xff0c;工业设备上的串口通信、数据采集设备上用于保存数据的 Flash 或 SD 卡&#xff0c;以及网络设备的以太网接口等…

docker 笔记11: Docker容器监控之CAdvisor+InfluxDB+Granfana

1.原生命令 docker stats命令的结果 是什么 2.是什么 容器监控3剑客 CAdvisor监控收集InfluxDB存储数据Granfana展示图表 3.CAdvisor 4.InfluxDB 5.Granfana 6.总结 7.compose容器编排&#xff0c;一套带走 新建目录 7.1新建3件套组合的 docker-compose.yml version: 3.1vo…

如何写出一篇优秀的博客

写一篇优秀的博客需要经过以下几个步骤&#xff1a; 确定博客的主题和目的 首先要确定博客的主题和目的。这可以根据自己的专业领域、兴趣爱好或者行业热点来定。博客的主题应该具有一定的深度&#xff0c;能够吸引读者的关注&#xff0c;同时博客的目的应该明确&#xff0c;是…

基于x86_64 ubuntu22.04的framebuffer编程

文章目录 前言一、framebuffer简介二、framebuffer接口1.framebuffer设备描述信息2.framebuffer访问接口3.查询/设置可更改信息 三、使用步骤 前言 前段时间由于笔记本没有保管好&#xff0c;LCD显示屏压碎了。于是&#xff0c;将笔记本电脑拆开查看LCD型号。在淘宝上下单买了…

【Java 基础篇】玩转 Java String:技巧与实践

在Java编程中&#xff0c;字符串&#xff08;String&#xff09;是一个非常常见的数据类型&#xff0c;用于存储文本信息。无论是处理用户输入、读取文件内容还是与外部系统进行通信&#xff0c;字符串都扮演着重要的角色。本篇博客将深入讨论Java中的字符串&#xff08;String…

双边滤波 Bilateral Filtering

本文是对图像去噪领域经典的双边滤波法的一个简要介绍与总结&#xff0c;论文链接如下&#xff1a; https://users.soe.ucsc.edu/~manduchi/Papers/ICCV98.pdf 1.前言引入 对一副原始灰度图像&#xff0c;我们将它建模为一张二维矩阵u&#xff0c;每个元素称为一个像素pixel&am…

PID串行多闭环控制与并行多闭环控制的优缺点分析和应用比较

导言&#xff1a; 在自动控制领域&#xff0c;PID控制器是一种经典的控制策略&#xff0c;被广泛应用于各种工业和非工业过程。随着控制系统的复杂性增加&#xff0c;PID串行多闭环控制和PID并行多闭环控制成为解决复杂控制问题的重要方法。本文将从优点和缺点的角度对这两种控…

大数据Flink(七十二):SQL窗口的概述和Over Windows

文章目录 SQL窗口的概述和Over Windows 一、窗口的概述