服务注册与发现

1. Spring版本说明

本文档对工业界常用的服务治理框架、及其对应的技术栈进行预研。相关技术栈均基于JDK + Spring生态,因此在探索具体的服务治理框架之前,我们需要先对jdk、spring、springBoot、springCloud的相关版本进行相关梳理及选定:

Spring Framework springBoot springCloud JDK
Spring Framework 4.3.7.RELEASE 1.5.2.RELEASE Dalston.RC1 JDK6-8
Spring Framework 4.3.13.RELEASE 1.5.9.RELEASE Edgware.RELEASE JDK6-8
Spring Framework 5.0.6.RELEASE 2.0.2.RELEASE Finchley.BUILD-SNAPSHOT JDK8-10
Spring Framework 5.0.7.RELEASE 2.0.3.RELEASE Finchley.RELEASE JDK8-10
Spring Framework 5.1.2.RELEASE 2.1.x.RELEASE** Greenwich JDK8-12
Spring Framework 5.* 2.2.x.RELEASE Hoxton JDK8-12

根据spring官方说明,Spring Framework 5.1.*为目前的推荐版本,对该版本的官方支持将持续到2019年底,而后将由5.2.版本取代,而就springCloud的生态演进而言,Finchley版本相较于Dalston,有了极大的丰富与优化。
因此,本文档相关技术栈均基于*
SpringCloud Finchley**版本

关于SpringBoot的版本时间线如下:

  • 2014年04月01号,Spring Boot 发布 v1.0.0.RELEASE,Spring Boot 正式商用
  • 2014年06月11号,Spring Boot 发布 v1.1.0.RELEASE,主要修复了若干 Bug
  • 2014年12月11号,Spring Boot 发布 v1.2.0.RELEASE,此版本更新的特性比较多,主要集成了 Servlet 3.1,支持 JTA、J2EE 等。
  • 2015年11月16号,Spring Boot 发布 v1.3.0.RELEASE,增加了新 spring-boot-devtools 模块,缓存自动配置、颜色 banners 等新特性。
  • 2016年07月29号,Spring Boot 发布 v1.4.0.RELEASE,以 Spring 4.3 为基础进行的构建,更新了很多第三方库的支持,重点增加了 Neo4J, Couchbase、 Redis 等 Nosql 的支持。
  • 2017年01月30号,Spring Boot 发布 v1.5.0.RELEASE,更新了动态日志修改,增加 Apache Kafka、LDAP、事物管理等特性的支持。
  • 2018年03月01号,Spring Boot 发布 v2.0.0.RELEASE
  • 2018年10月30号,Spring Boot 发布 v2.1.0.RELEASE

2. CAP原则

CAP原则又称CAP定理(1998年由加州大学的计算机科学家 Eric Brewer 提出),指的是在一个分布式系统中有三个指标:一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)

CAP 原则指的是,这三个指标最多只能同时实现两点,不可能三者兼顾

  • 一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
  • 可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
  • 分区容忍性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择【分区由网络故障、机器故障导致,具有外部不可抗力性,无法控制,是必选项】。

即:设计分布式数据系统,就是在一致性和可用性之间取一个平衡

组件名 CAP 一致性算法
Eureka AP
Consul CP Raft
Zookeeper CP Paxos
etcd CP Raft
nacos AP Raft

二.常用服务治理框架

1. Eureka(*)

Eureka是Netflix开发的服务发现框架,本身是一个基于REST的服务,SpringCloud将它集成在其子项目spring-cloud-netflix中,以实现SpringCloud的服务发现功能。其也是目前使用最广泛的服务发现框架
Eureka包含两个组件:Eureka Server(即服务端,注册中心)和Eureka Client(即客户端,服务提供者/消费者)

  1. 服务提供者在启动时,向注册中心注册自己提供的服务。
  2. 服务消费者在启动时,向注册中心订阅自己所需的服务。
  3. 注册中心返回服务提供者地址给消费者。
  4. 服务消费者从提供者地址中调用消费者
    d810ebfac7c63d66515eec9ae47d5eae.png

2. Consul

Spring Cloud Consul项目是针对Consul的服务治理实现。Consul 是 HashiCorp 公司推出的开源工具,用于实现分布式系统的服务发现与配置,它包含多个组件,但是作为一个整体,在微服务架构中为我们的基础设施提供服务发现和服务配置的工具。

  • 使用Go语言编写,支持win、linux、mac全平台
  • Consul和Eureka不同,Eureka只需要在项目中加入服务端依赖,就可以作为服务端使用;Consul需要从官网下载,并单独安装
  • Consul服务器使用 Raft 协议(要求必须过半数的节点都写入成功才认为注册成功 Leader 挂掉时,重新选举期间整个 Consul 不可用)复制状态,保证了强一致性。相较于Eurake,高可用及效率均有损失。

d6a016b0ac5ee80914c24f1799beaafd.png

3. Dubbo+Zookeeper

Dubbo是一个由阿里巴巴开源的分布式服务框架,提供面向接口的远程方法调用(RPC),智能容错和负载均衡,以及服务自动注册和发现等功能,其直接使用socket通信。传输效率高。Dubbo主要作用是提供服务治理
ZooKeeper是一个开源的分布式协调服务,为分布式应用提供配置维护、域名服务、分布式同步、组服务等功能。ZooKeeper是Dubbo的推荐注册中心,提供服务注册功能(Dubbo也还可以搭配其他注册中心:Multicast注册中、Redis注册中心、Simple注册中心

PS:最新Spring社区孵化的Spring Cloud Alibaba,默认选用了Nacos作为注册中心,可以更加便捷地使用Ribbon或Feign来实现服务消费

4. Nacos

Nacos是由阿里巴巴开源的动态服务发现、配置管理和服务管理平台。Nacos提供了如下关键特性:服务发现和服务健康监测、动态配置服务、动态 DNS 服务、服务及其元数据管理。
和Consul一样,Nacos也需要从官网下载服务,进行单独启动
下图为Nacos官网提供的架构图:
352af1f2eb7fbf5f65e869893ac99633.png

5. etcd

etcd是一个采用http协议的分布式键值对存储系统,因其易用,简单。很多系统都采用或支持etcd作为服务发现的一部分,比如kubernetes。但其只是一个存储系统,如果想要提供完整的服务发现功能,必须搭配一些第三方的工具,搭建操作相对麻烦。

6. SpringCloud Alibaba

SpringCloud Alibaba作为一款旨在打造更符合中国国情的微服务体系的开源组件,目前主要提供了服务发现、配置管理、高可用防护、基于RocketMQ的消息中间件等功能。其既提供了基于Nacos+Ribbon/Feign的服务治理方案,又支持基于Nacos+Dubbo的rpc服务治理方案。
下图nacas启动后的管理界面:

三. Eureka项目实践

1. Eureka注册中心搭建(单机)

1). pom依赖

1
2
3
4
5
<!--eureka依赖【Springboot1.x中 artifactId 为 spring-cloud-starter-netflix-eureka-server,注意区分】-->
<dependency>
       <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

2). 启动类注解

通过在启动类上添加注解@EnableEurekaServer,为该应用开启注册中心功能

3). application.properties配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 服务名称
spring.application.name=NewsEditingSuitCenter
# 服务端口号
server.port=1111

#主机名
eureka.instance.hostname=eureka1
#服务名,默认取spring.application.name 配置值,没有则显示unknown
#eureka.instance.appname=NewsEditingSuitCenter

#注册时显示ip【即在eureka管理页面显示的格式为ip地址】
eureka.instance.prefer-ip-address=false
#注册时显示ip的配置方案【spring.cloud.client.ip-address即为本机ip】
#eureka.instance.prefer-ip-address=true
#eureka.instance.hostname=${spring.cloud.client.ip-address}
#eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}

#表示是否将自己注册在EurekaServer上,默认为true。由于当前应用就是EurekaServer,所以置为false
eureka.client.register-with-eureka=false
#表示表示是否从EurekaServer获取注册信息,默认为true。单节点不需要同步其他的EurekaServer节点的数据
eureka.client.fetch-registry=false

#eureka server地址
eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/

#关闭自我保护(生产时打开该选项)
#如果在15分钟内超过85%的客户端节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,Eureka Server自动进入自我保护机制
#1. 不再从注册列表中移除因为长时间没收到心跳而应该过期的服务、
#2. 仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点、
#3. 当网络稳定时,当前Eureka Server新的注册信息会被同步到其它节点中
eureka.server.enable-self-preservation=false

#扫描失效服务的间隔时间(缺省为60*1000ms)
eureka.server.eviction-interval-timer-in-ms=5000

2. Eureka注册中心实现高可用

运行多个Eureka server实例,并进行互相注册即可实现注册中心的高可用
Eureka不允许在单台主机(即同一个ip)上搭建高可用服务,可以利用本机hosts文件构造本地域名来模拟多机,下述过程即是采用该方式进行搭建

1). hosts文件配置

1
2
3
127.0.0.1 eureka1
127.0.0.1 eureka2
127.0.0.1 eureka3

2). 搭建多个Eureka server

将上文”1. Eureka注册中心搭建(单机)“中搭建的项目,复制多个即可

3). 修改Eureka server服务的application.properties配置

  • 节点1配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 服务名称
spring.application.name=NewsEditingSuitCenter
# 服务端口号
server.port=1111

#主机名
eureka.instance.hostname=eureka1

#是否将自己注册在EurekaServer上
eureka.client.register-with-eureka=true
#是否从EurekaServer获取注册信息,多节点需要同步其他的EurekaServer节点的数据
eureka.client.fetch-registry=true

#eureka server地址【指向其他节点地址】
eureka.client.serviceUrl.defaultZone=http://eureka2:1112/eureka/,http://eureka3:1113/eureka/
  • 节点2配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 服务名称
spring.application.name=NewsEditingSuitCenter
# 服务端口号
server.port=1112

#主机名
eureka.instance.hostname=eureka2

#是否将自己注册在EurekaServer上
eureka.client.register-with-eureka=true
#是否从EurekaServer获取注册信息,多节点需要同步其他的EurekaServer节点的数据
eureka.client.fetch-registry=true

#eureka server地址【指向其他节点地址】
eureka.client.serviceUrl.defaultZone=http://eureka2:1112/eureka/,http://eureka3:1113/eureka/
  • 节点3配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 服务名称
spring.application.name=NewsEditingSuitCenter
# 服务端口号
server.port=1113

#主机名
eureka.instance.hostname=eureka3

#是否将自己注册在EurekaServer上
eureka.client.register-with-eureka=true
#是否从EurekaServer获取注册信息,多节点需要同步其他的EurekaServer节点的数据
eureka.client.fetch-registry=true

#eureka server地址【指向其他节点地址】
eureka.client.serviceUrl.defaultZone=http://eureka1:1111/eureka/,http://eureka2:1112/eureka/

3. 注册服务至Eureka【构建生产者】

此步骤,即将现有的快融服务注册到Eureka注册中心

1). pom依赖

1
2
3
4
5
6
7
8
9
10
11
<!-- eureka client 适配springboot1.*-->
<!--<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency> -->

<!-- eureka client 适配springboot2*-->
<dependency>
        <groupId>org.springframework.cloud</groupId>  
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

2). 启动类注解

通过在启动类上添加注解`@EnableDiscoveryClient,激活Eureka中的DiscoveryClient实现,这样才能实现Controller中对服务信息的输出

1
2
3
4
5
6
7
8
9
@EnableDiscoveryClient
@EnableAsync
@SpringBootApplication
@ServletComponentScan
@Configuration
@MapperScan("com.dayang.portal.db.dao")
public class Application {
...
}

3). application.properties配置

1
2
3
4
5
6
# 服务名称
spring.application.name=dyportalserver
# 服务端口号
server.port=9001
#eureka server地址
eureka.client.serviceUrl.defaultZone=http://eureka1:1111/eureka/
  • 进阶配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#状态页面的URL,相对路径,默认值是/info【https需要使用绝对路径配置eureka.instance.status-page-url】
eureka.instance.status-page-url-path=${server.servlet.context-path}/actuator/info
#健康检查页面的URL,相对路径,默认值是/health【https需要使用绝对路径配置eureka.instance.health-check-url】
eureka.instance.health-check-url-path=${server.servlet.context-path}/acturtor/health


#该实例的主页url,相对路径【绝对路径:eureka.instance.home-page-url】
#eureka.instance.home-page-url-path=${server.servlet.context-path}
#是否注册为服务
eureka.client.register-with-eureka=true
#是否检索服务
eureka.client.fetch-registry=true
############# 续约配置  ##############
# 心跳时间,即服务续约间隔时间(缺省为30s)
eureka.instance.lease-renewal-interval-in-seconds=5
# 发呆时间,即服务续约到期时间(缺省为90s)
eureka.instance.lease-expiration-duration-in-seconds=10
# 开启健康检查(依赖spring-boot-starter-actuator)
eureka.client.healthcheck.enabled=true

4. 基于Ribbon实现服务间调用【构建消费者】

Spring Cloud Ribbon是基于基于HTTP和TCP的客户端负载均衡的工具(基于Netflix Ribbon实现),它被集成在springCloud的基础设施中,并不需要独立部署(简而言之,引入依赖开启配置即可使用)

客户端负载均衡:
区别于基于F5(硬件)、nginx(软件)等实现的服务端负载均衡,是将服务清单维护在服务端,按照算法进行请求分发; 客户端负载均衡,是由客户端(即服务消费者)自行维护服务节点清单【从Eureka server获取】,由客户端自行选择请求节点

Ribbon实现客户端负载均衡的方式,是通过在客户端中配置ribbonServerList来设置服务端列表去轮询访问以达到均衡负载的作用。Ribbon与Eureka联合使用时,ribbonServerList会被DiscoveryEnabledNIWSServerList重写,扩展成从Eureka注册中心中获取服务实例列表。同时它也会用NIWSDiscoveryPing来取代IPing,它将职责委托给Eureka来确定服务端是否已经启动。(相应的,当Ribbon与Consul联合使用时,ribbonServerList会被ConsulServerList来扩展成从Consul获取服务实例列表。同时由ConsulPing来作为IPing接口的实现)

1). pom依赖

1
2
3
4
<dependency>        
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>

2). 声明RestTemplate

通过显示地声明一个RestTemplate对象,并添加注解@LoadBalanced,开启客户端负载均衡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@EnableDiscoveryClient
@SpringBootApplicationpublic
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}
...

/**
     * @Author: ysy
     * @Description: Spring Boot >= 1.4版本,RestTemplate不再自动声明
     * 【@LoadBalanced用于开启LoadBalancerInterceptor,实现通过服务名实现调用】
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(RestTemplateBuilder
builder) {
       // Do any additional configuration here
       return builder.build();
    }
}

3). 利用RestTemplate发起请求

RestTemplate是Spring提供的用于访问Rest服务的客户端,RestTemplate提供了多种便捷访问远程Http服务的方法。相较于我们目前使用的HTTPClient,大大提高客户端的编写效率。

关于httpClient、OkHttpClient、RestTemplate三者的比较,可以参见HttpUrlConnection VS RestTemplate

RestTemplate常用API:

  • getForEntity(String url, Class responseType, Object… uriVariables)
  • getForEntity(URI url, Class responseType)
  • getForObject(String url, Class responseType, Object… uriVariables)
  • getForObject(URI url, Class responseType)
  • postForObject(URI url, @Nullable Object request, Class responseType)
  • postForObject(String url, @Nullable Object request, Class responseType, Object… uriVariables)
  • postForObject(String url, @Nullable Object request, Class responseType,Map<String, ?> uriVariables)
  • GET请求样例【手动获取实例地址】
1
2
3
4
5
6
7
8
9
10
11
12
13
// 获取restTemplate对象
@Autowired
LoadBalancerClient loadBalancerClient;
@Autowired
RestTemplate restTemplate;

// 获取服务地址
ServiceInstance serviceInstance = loadBalancerClient.choose("eureka-client");
String url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort();
//第一个参数表示服务地址:dyportalserver为在Eureka中注册的服务名
//第二个参数表示返回值类型
//后续为可变参数,分别替换请求路径中的占位符变量
String name = restTemplate.getForObject(url + "/test?name={1}", String.class, "智新测试");
  • POST请求样例【自动获取实例地址】
1
2
3
4
5
6
@Autowired    
RestTemplate restTemplate;

//第一个参数表示服务地址:dyportalserver为在Eureka中注册的服务名
//第二个参数表示返回值类型
CloudType cloudType = S.getForObject("http://dyportalserver/UserInfoController/getCloudType", CloudType.class);
  • POST请求样例 【上面是理想转态,下面才是现实】
1
2
3
4
5
6
7
8
9
10
11
12
@Autowired    
RestTemplate restTemplate;

@Test
    public void getCloudType() {
// 返回json封装的数据
      String res = restTemplate.postForObject("http://" + serviceName + contextPath +Constants_portal.CLOUDTYPE_ADDRESS_SUFFIX, null, String.class);
// 利用TypeToken对带泛型的对象进行反序列化
      Type typeToken = new TypeToken<XtcpCommonReponse<CloudType>>(){}.getType();
      XtcpCommonReponse<CloudType> response = new Gson().fromJson(res, typeToken);
      logger.debug("CloudType: " + response.getData());
    }
  • POST请求样例【带参数】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Autowired    
RestTemplate restTemplate;

@Test
    public void getParameter() {
      //构造请求头
      HttpHeaders headers = new HttpHeaders();
      headers.add("userId", "admin");
      headers.add("tenantId", "dayang.com");
      headers.add("token", "token");
      headers.setContentType(MediaType.MULTIPART_FORM_DATA);
//构造请求体
      MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
      map.add("parameterCode","PUB_CRECRE");
      HttpEntity<MultiValueMap<String, String>> formEntity = new HttpEntity<>(map, headers);
//发起请求
      String res = restTemplate.postForObject("http://" + serviceName + contextPath + Constants_portal.PARAMETER_QUERY_ADDRESS_SUFFIX, formEntity, String.class);
// 处理返回值
      Type typeToken = new
TypeToken<XtcpCommonReponse<List<CcsParameter>>>(){}.getType();
      XtcpCommonReponse<List<CcsParameter>> response = new Gson().fromJson(res, typeToken);
      List<CcsParameter> parameterList = response.getData();

5. 基于Feign实现服务间调用(*)

Spring Cloud Feign是一套基于Netflix Feign实现的声明式服务调用客户端,只需要通过创建接口并用注解来配置它既可完成对Web服务接口的绑定。同时还整合了Ribbon和Eureka来提供均衡负载的HTTP客户端实现

另外,Feign同时整合了Hystrix功能,支持服务容错保护

1). pom依赖

1
2
3
4
<dependency>        
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>

2). 启动类注解

通过在启动类上添加注解@EnableFeignClients,开启扫描Spring Cloud Feign客户端的功能

1
2
3
4
5
6
7
8
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplicationpublic
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}
}

3). 创建Feign的客户端接口定义

Feign提供的是声明式的服务绑定功能,即使用@FeignClient注解即可实现对服务的绑定

  • 构造Feign客户端
1
2
3
4
5
6
7
8
9
10
11
12
// 通过注解将该接口与门户后端服务进行绑定【服务名不区分大小写】
@FeignClient("dyportalserver")
public interface DypotalClient {

// 使用mvc注解绑定具体的REST接口【不带参数】
     @PostMapping("dyportalserver/UserInfoController/getCloudType")
     public String getCloudType();

// 使用mvc注解绑定具体的REST接口【带参数】
@PostMapping("dyportalserver/ParameterController/getParameter")
     public String getParameter(@RequestHeader("tenantId") String tenantId, @RequestHeader("userId") String userId, @RequestHeader("token") String token, @RequestParam("parameterCode") String parameterCode);
}
  • 调用Feign客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 注入Feign客户端
@Autowired
private FeignClientUtil feignClientUtil;

@Test
public void getCloudTypeFeign() {
   String res = feignClientUtil.getCloudType();
   Type typeToken = new TypeToken<XtcpCommonReponse<CloudType>>(){}.getType();
   XtcpCommonReponse<CloudType> response = new Gson().fromJson(res, typeToken);
}

@Test
public void getParameterFeign() {
String res = feignClientUtil.getParameter("dayang.com", "admin", "token", "PUB_CRECAS");
Type typeToken = new TypeToken<XtcpCommonReponse<List<CcsParameter>>>(){}.getType();
XtcpCommonReponse<List<CcsParameter>> response = new Gson().fromJson(res, typeToken);
List<CcsParameter> parameterList = response.getData();
}
坚持原创技术分享,您的支持将鼓励我继续创作!