Spring Cloud 注册中心 ZooKeeper 入门
1. 概述
在《ZooKeeper 极简入门》文章中,我们一起完成了 ZooKeeper 的学习,并完成了 ZooKeeper 服务器的搭建。
本文我们来学习 Spring Cloud ZooKeeper 提供的 spring-cloud-zookeeper-discovery
组件,基于 Spring Cloud 的编程模型,接入 ZooKeeper 作为注册中心,实现服务的注册与发现。
2. 注册中心原理
在开始搭建 ZooKeeper Discovery 的示例之前,我们先来简单了解**册中心的原理。
在使用注册中心时,一共有三种角色:服务提供者(Service Provider)、服务消费者(Service Consumer)、注册中心(Registry)。
在一些文章中,服务提供者被称为 Server,服务消费者被称为 Client。胖友们知道即可。
三个角色交互如下图所示:
① Provider:
- 启动时,向 Registry 注册自己为一个服务(Service)的实例(Instance)。
- 同时,定期向 Registry 发送心跳,告诉自己还存活。
- 关闭时,向 Registry 取消注册。
② Consumer:
- 启动时,向 Registry 订阅使用到的服务,并缓存服务的实例列表在内存中。
- 后续,Consumer 向对应服务的 Provider 发起调用时,从内存中的该服务的实例列表选择一个,进行远程调用。
- 关闭时,向 Registry 取消订阅。
③ Registry:
- Provider 超过一定时间未心跳时,从服务的实例列表移除。
- 服务的实例列表发生变化(新增或者移除)时,通知订阅该服务的 Consumer,从而让 Consumer 能够刷新本地缓存。
当然,不同的注册中心可能在实现原理上会略有差异。例如说,Eureka 注册中心,并不提供通知功能,而是 Eureka Client 自己定期轮询,实现本地缓存的更新。
另外,Provider 和 Consumer 是角色上的定义,一个服务同时即可以是 Provider 也可以作为 Consumer。例如说,优惠劵服务可以给订单服务提供接口,同时又调用用户服务提供的接口。
3. 快速入门
示例代码对应仓库:
本小节,我们来搭建一个 ZooKeeper Discovery 组件的快速入门示例。步骤如下:
- 首先,搭建一个服务提供者
demo-provider
,注册服务到 ZooKeeper 中。 - 然后,搭建一个服务消费者
demo-consumer
,从 ZooKeeper 获取到demo-provider
服务的实例列表,选择其中一个示例,进行 HTTP 远程调用。
3.1 搭建服务提供者
创建 labx-25-sc-zookeeper-discovery-demo01-provider
项目,作为服务提供者 demo-provider
。最终项目代码如下图所示:
3.1.1 引入依赖
在 pom.xml
文件中,主要引入 Spring Cloud ZooKeeper Discovery 相关依赖。代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>labx-25</artifactId>
<groupId>cn.iocoder.springboot.labs</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>labx-25-sc-zookeeper-discovery-demo01-provider</artifactId>
<properties>
<spring.boot.version>2.2.4.RELEASE</spring.boot.version>
<spring.cloud.version>Hoxton.SR1</spring.cloud.version>
</properties>
<!--
引入 Spring Boot、Spring Cloud、Spring Cloud Alibaba 三者 BOM 文件,进行依赖版本的管理,防止不兼容。
在 https://dwz.cn/mcLIfNKt 文章中,Spring Cloud Alibaba 开发团队推荐了三者的依赖关系
-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- 引入 SpringMVC 相关依赖,并实现对其的自动配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 引入 Spring Cloud ZooKeeper Discovery 相关依赖,将 ZooKeeper 作为注册中心,并实现对其的自动配置 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
</dependencies>
</project>
引入 spring-cloud-starter-zookeeper-discovery
依赖,将 ZooKeeper 作为注册中心,并实现对它的自动配置。
3.1.2 配置文件
创建 application.yaml
配置文件,添加 ZooKeeper Discovery 配置项。配置如下:
spring:
application:
name: demo-provider # Spring 应用名
cloud:
zookeeper:
connect-string: 127.0.0.1:2181
# ZooKeeper 作为注册中心的配置项,对应 ZooKeeperDiscoveryProperties 配置类
discovery:
root: /services # ZooKeeper 数据存储的根节点,默认为 /services
server:
port: 18080 # 服务器端口。默认为 8080
① spring.cloud.zookeeper
配置项,设置 ZooKeeper 客户端的配置,对应 ZooKeeperProperties 配置类。
-
connect-string
配置项,设置 ZooKeeper 服务器的地址。
② spring.cloud.zookeeper.discovery
配置项,设置 ZooKeeper Discovery 配置项,对应 ZooKeeperDiscoveryProperties 配置类。
-
root
配置项,设置 ZooKeeper 数据存储的根节点,默认为/services
。稍后,我们来具体演示下哈~
3.1.3 DemoProviderApplication
创建 DemoProviderApplication 类,创建应用启动类,并提供 HTTP 接口。代码如下:
@SpringBootApplication
@EnableDiscoveryClient
public class DemoProviderApplication {
public static void main(String[] args) {
SpringApplication.run(DemoProviderApplication.class, args);
}
@RestController
static class TestController {
@GetMapping("/echo")
public String echo(String name) {
return "provider:" + name;
}
}
}
① @SpringBootApplication
注解,被添加在类上,声明这是一个 Spring Boot 应用。Spring Cloud 是构建在 Spring Boot 之上的,所以需要添加。
② @EnableDiscoveryClient
注解,开启 Spring Cloud 的注册发现功能。不过从 Spring Cloud Edgware 版本开始,实际上已经不需要添加 @EnableDiscoveryClient
注解,只需要引入 Spring Cloud 注册发现组件,就会自动开启注册发现的功能。例如说,我们这里已经引入了 spring-cloud-starter-zookeeper-discovery
依赖,就不用再添加 @EnableDiscoveryClient
注解了。
拓展小知识:在 Spring Cloud Common 项目中,定义了 DiscoveryClient 接口,作为通用的发现客户端,提供读取服务和读取服务列表的 API 方法。而想要集成到 Spring Cloud 体系的注册中心的组件,需要提供对应的 DiscoveryClient 实现类。
例如说,Spring Cloud Alibaba Nacos Discovery 提供了 NacosDiscoveryClient 实现,Spring Cloud ZooKeeper Discovery 提供了 ZooKeeperDiscoveryClient 实现。
如此,所有需要使用到的地方,只需要获取到 DiscoveryClient 客户端,而无需关注具体实现,保证其通用性。
③ TestController 类,提供了 /echo
接口,返回 provider:${name}
结果。
3.1.4 简单测试
通过 DemoProviderApplication 启动服务提供者。启动完成后,我们使用 ZooKeeper 自带的客户端,查看 ZooKeeper /services
目录下的数据。
# 进入 ZooKeeper 控制台
$ bin/zkCli.sh
# 查看 /services 目录
$ ls /services
[demo-provider]
# 查看 /services/demo-provider 目录
$ ls /services/demo-provider
[80b4d65d-a5d9-4df3-8a4b-6a3e8bca8fb3]
# 查看服务实例 80b4d65d-a5d9-4df3-8a4b-6a3e8bca8fb3 的数据
$ get /services/demo-provider/80b4d65d-a5d9-4df3-8a4b-6a3e8bca8fb3
{"name":"demo-provider","id":"80b4d65d-a5d9-4df3-8a4b-6a3e8bca8fb3","address":"10.101.16.12","port":18080,"sslPort":null,"payload":{"@class":"org.springframework.cloud.zookeeper.discovery.ZookeeperInstance","id":"application-1","name":"demo-provider","metadata":{}},"registrationTimeUTC":1591619511852,"serviceType":"DYNAMIC","uriSpec":{"parts":[{"value":"scheme","variable":true},{"value":"://","variable":false},{"value":"address","variable":true},{"value":":","variable":false},{"value":"port","variable":true}]}}
通过 address
和 port
属性,可以看到该服务实例的 IP 地址和 PORT 端口。
3.2 搭建服务消费者
创建 labx-25-sc-zookeeper-discovery-demo01-consumer
项目,作为服务提供者 demo-consumer
。最终项目代码如下图所示:
整个项目的代码,和服务提供者是基本一致的,毕竟是示例代码 ????
3.2.1 引入依赖
和「3.1.1 引入依赖」一样,只是修改 Maven <artifactId />
为 labx-25-sc-zookeeper-discovery-demo01-consumer
,见 pom.xml
文件。
3.2.2 配置文件
创建 application.yaml
配置文件,添加相应配置项。配置如下:
spring:
application:
name: demo-consumer # Spring 应用名
cloud:
zookeeper:
connect-string: 127.0.0.1:2181
# Zookeeper 作为注册中心的配置项,对应 ZookeeperDiscoveryProperties 配置类
discovery:
root: /services # Zookeeper 数据存储的根节点,默认为 /services
server:
port: 28080 # 服务器端口。默认为 8080
和「3.1.2 配置文件」基本一致,主要是将配置项目 spring.application.name
修改为 demo-consumer
。
3.2.3 DemoConsumerApplication
创建 DemoConsumerApplication 类,创建应用启动类,并提供一个调用服务提供者的 HTTP 接口。代码如下:
@SpringBootApplication
// @EnableDiscoveryClient
public class DemoConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(DemoConsumerApplication.class, args);
}
@Configuration
public class RestTemplateConfiguration {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@RestController
static class TestController {
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
@GetMapping("/hello")
public String hello(String name) {
// <1> 获得服务 `demo-provider` 的一个实例
ServiceInstance instance;
if (true) {
// 获取服务 `demo-provider` 对应的实例列表
List<ServiceInstance> instances = discoveryClient.getInstances("demo-provider");
// 选择第一个
instance = instances.size() > 0 ? instances.get(0) : null;
} else {
instance = loadBalancerClient.choose("demo-provider");
}
// <2> 发起调用
if (instance == null) {
throw new IllegalStateException("获取不到实例");
}
String targetUrl = instance.getUri() + "/echo?name=" + name;
String response = restTemplate.getForObject(targetUrl, String.class);
// 返回结果
return "consumer:" + response;
}
}
}
① @EnableDiscoveryClient
注解,因为已经无需添加,所以我们进行了注释,原因在上面已经解释过。
② RestTemplateConfiguration 配置类,创建 RestTemplate Bean。RestTemplate 是 Spring 提供的 HTTP 调用模板工具类,可以方便我们稍后调用服务提供者的 HTTP API。
③ TestController 提供了 /hello
接口,用于调用服务提供者的 /demo
接口。代码略微有几行,我们来稍微解释下哈。
discoveryClient
属性,DiscoveryClient 对象,服务发现客户端,上文我们已经介绍过。这里我们注入的不是 Nacos Discovery 提供的 NacosDiscoveryClient,保证通用性。未来如果我们不使用 Nacos 作为注册中心,而是使用 Eureka 或则 ZooKeeper 时,则无需改动这里的代码。
loadBalancerClient
属性,LoadBalancerClient 对象,负载均衡客户端。稍后我们会使用它,从 Nacos 获取的服务 demo-provider
的实例列表中,选择一个进行 HTTP 调用。
拓展小知识:在 Spring Cloud Common 项目中,定义了LoadBalancerClient 接口,作为通用的负载均衡客户端,提供从指定服务中选择一个实例、对指定服务发起请求等 API 方法。而想要集成到 Spring Cloud 体系的负载均衡的组件,需要提供对应的 LoadBalancerClient 实现类。
例如说,Spring Cloud Netflix Ribbon 提供了 RibbonLoadBalancerClient 实现。
如此,所有需要使用到的地方,只需要获取到 DiscoveryClient 客户端,而无需关注具体实现,保证其通用性。???? 不过貌似 Spring Cloud 体系中,暂时只有 Ribbon 一个负载均衡组件。
当然,LoadBalancerClient 的服务的实例列表,是来自 DiscoveryClient 提供的。
/hello
接口,示例接口,对服务提供者发起一次 HTTP 调用。
-
<1>
处,获得服务demo-provider
的一个实例。这里我们提供了两种方式的代码,分别基于 DiscoveryClient 和 LoadBalancerClient。 -
<2>
处,通过获取到的服务实例 ServiceInstance 对象,拼接请求的目标 URL,之后使用 RestTemplate 发起 HTTP 调用。
3.2.4 简单测试
① 通过 DemoConsumerApplication 启动服务消费者。启动完成后,我们使用 ZooKeeper 自带的客户端,查看 ZooKeeper /services
目录下的数据。
# 进入 ZooKeeper 控制台
$ bin/zkCli.sh
# 查看 /services 目录
$ ls /services
[demo-consumer, demo-provider]
注意,服务消费者和服务提供是一种角色的概念,本质都是一种服务,都是可以注册自己到注册中心的。
② 访问服务消费者的 http://127.0.0.1:28080/hello?name=yudaoyuanma 接口,返回结果为 "consumer:provider:yudaoyuanma"
。说明,调用远程的服务提供者成功。
③ 关闭服务提供者后,再次访问 http://127.0.0.1:28080/hello?name=yudaoyuanma 接口,返回结果为报错提示 "获取不到实例"
,说明我们本地缓存的服务 demo-provider
的实例列表已刷新,没有任何实例。
???? 这里我们并没有演示启动多个服务提供者的测试,胖友可以自己尝试下哟。
4. 多环境配置
同一个服务,我们会部署到开发、测试、预发布、生产等环境中,那么我们需要在项目中,添加不同环境的 ZooKeeper 配置。
一般情况下,开发和测试使用同一个 ZooKeeper,预发布和生产使用另一个 ZooKeeper。那么针对相同的 ZooKeeper,我们怎么实现不同环境的隔离呢?
我们可以通过 spring.cloud.zookeeper.discovery.root
配置项,设置不同 Zookeeper 目录进行数据的存储。例如说,生产环境使用 services-prod
目录,预发布使用 services-pre
目录。
之后,在结合《Spring Boot 配置文件入门》的「6. 多环境配置」小节,使用不同的 application-{env}.yaml
配置文件中,设置不同的 spring.cloud.zookeeper.discovery.root
配置项即可。
推荐阅读
-
Spring Cloud Config 配置中心实践过程中,你需要了解这些细节!
-
Spring Cloud Alibaba | Nacos服务中心初探
-
spring cloud 入门系列八:使用spring cloud sleuth整合zipkin进行服务链路追踪
-
服务注册中心之ZooKeeper系列(一)
-
跟我学SpringCloud | 第七篇:Spring Cloud Config 配置中心高可用和refresh
-
Spring-Cloud-Netflix-Eureka注册中心
-
spring cloud 入门系列五:使用Feign 实现声明式服务调用
-
第1章 Spring Cloud 构建微服务架构(一)服务注册与发现
-
Spring Cloud实战之初级入门(四)— 利用Hystrix实现服务熔断与服务监控
-
Spring Cloud实战之初级入门(六)— 服务网关zuul