文章目录
- 前言
- 声明
- 前置知识
- 服务注册和发现
- Zookeeper
- 工作原理
- 实现过程
- 注册中心
- 服务注册
- 服务发现
- 总结
前言
无论是采用SOA还是微服务架构,都需要使用服务注册和服务发现组件。我刚开始接触 Dubbo 时一直对服务注册/发现以及 Zookeeper 的作用感到困惑,现在看来是因为对分布式系统的理解不够深入,对 Dubbo 和 Zookeeper 的工作原理不够清楚。
本文将基于 Zookeeper 实现服务注册和服务发现功能,如果跟我一样有同样的困惑,希望可以通过本文了解其他组件如何使用 Zookeeper 作为注册中心的工作原理。
声明
文章中所提供的代码仅供参考,旨在帮助缺乏基础知识的开发人员更好地理解服务注册和服务发现的概念。请注意,这些代码并不适用于实际应用中。
前置知识
服务注册和发现
在SOA或微服务架构中,由于存在大量的服务以及可能的相互调用,为了更有效地管理这些服务,我们通常需要引入一个统一的地方,即注册中心,来集中管理它们,而注册中心最基本的功能就是服务注册/发现。
- 服务注册:将该服务实例的元数据(如IP地址、端口号、健康状态等)注册到注册中心,这样其他服务或客户端可以发现和使用该服务。
- 服务发现:当一个服务需要调用别的服务时,使用静态配置是不可行的,这个时候可以去注册中心获取可用的服务实例并调用。
Zookeeper
Zookeeper 是一个传统的分布式协调服务,它更多的被用来作为一个协调器使用,比如来协调管理 Hadoop 集群、协调 Kafka 的 leader 选举等。
为什么会有组件将其视为一个注册中心使用?我想有几个原因:
- Zookeeper 在分布式系统中具有更强的一致性和可靠性,可以确保各个服务的注册信息保持一致。
- Zookeeper 使用内存存储数据,具有很高的读写性能。这对于注册中心来说非常关键,因为它需要快速地响应客户端的请求。
- Zookeeper 的 Watcher 机制可以让客户端监听指定节点的变化。当某个节点(注册中心)发生变化时,Zookeeper 可以通知其他服务实现实时更新。
工作原理
以下图为例,可以看到 Dubbo 是如何使用 Zookeeper 实现服务注册/发现的。
- 服务提供者向
/dubbo/com.foo.BarService/providers
目录下写入自己的 URL 地址。 - 服务消费者订阅
/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;}@Overridepublic 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);}
}
总结
服务注册和服务发现功能是为了解决分布式系统中的服务管理和通信问题而设计的,经过不断的发展与负载均衡、健康监测、服务治理和路由控制等功能完善成为一个注册中心。服务注册和服务发现有助于实现系统的弹性和可扩展性,因为新的服务实例可以动态地加入系统,而无需手动配置和修改已有的代码。