微服务注册与发现
服务发现简介
通过前面的讲解,我们知道硬编码提供者地址的方式有不少问题。要想解决这些问题,服务消费者需要一个强大的服务发现机制,服务消费者使用这种机制获取服务提供者的网络信息。不仅如此,即使服务提供者的信息发生变化,服务消费者也无需修改配置文件。
服务发现组件提供这种能力。在微服务架构中,服务发现组件是一个非常关键的组件。
使用服务发现组件后的架构图,如下:
服务提供者、服务消费者、服务发现组件这三者之间的关系大致如下;
各个微服务在启动时,将自己的网络地址等信息注册到服务发现组件中,服务发现组件会存储这些信息。
服务消费者可从服务发现组件查询服务提供者的网络地址,并使用该地址调用服务提供者的接口。
各个微服务与服务发现组件使用一定机制(例如心跳)通信。服务发现组件如长时间无法与某微服务实例通信,就会注销该实例。
微服务网络地址发生变更(例如实例增减或者IP端口发生变化等)时,会重新注册到服务发现组件。是用这种方式,服务消费者就无须人工修改提供者的网络地址了。
综上,服务发现组件应具备以下功能:
服务注册表:是服务发现组件的核心,它用来记录各个微服务的信息,例如微服务的名称、IP、端口等。服务注册表提供查询API和管理API,查询API用于查询可用的微服务实例,管理API用于服务的注册和注销。
服务注册与服务发现:服务注册是指微服务在启动时,将自己的信息注册到服务发现组件上的过程。服务发现是指查询可用微服务列表及其网络地址的机制。
服务检查:服务发现组件使用一定机制定时检测已注册的服务,如发现某实例长时间无法访问,就会从服务注册表中移除该实例。
综上,使用服务发现的好处是显而易见的。SpringCloud提供了多种服务发现组件的支持,例如Eureka、Consul和ZooKeeper等。
Eureka简介
Eureka是Netflix开源的服务发现组件,本身是一个基于REST的服务。它包含Server和Client两部分。SpringCloud将它集成在子项目Spring Cloud Netflix中,从而实现微服务的注册与发现。
Eureka的GitHub:https://github.com/Netflix/Eureka。
Netflix是一家在线影片租赁提供商。
Eureka原理
在分析Eureka的原理之前,先来了解一下Region和Availability Zone,如下图所示:
Region和Availability Zone均是AWS的概念。其中,Region表示AWS中的地理位置,每个Region都有多个Availability Zone,各个Region之间完全隔离。AWS通过这种方式实现了最大的容错和稳定性。
SpringCloud默认使用的Region是us-east-1,在非AWS环境下,可以将Availability Zone理解成机房,将Region理解为跨机房的Eureka集群。
下面分析一下Eureka的原理,Eureka架构如下图所示。
Application Service相当于服务提供者
Application Client相当于服务消费者
Make Remote Call,可以理解成调用RESTful API的行为
us-east-1c、us-east-1d等都是zone,它们都属于us-east-1这个region
由上图可知,Eureka包含两个组件:Eureka Server和Eureka Client,它们的作用如下:
Eureka Server提供服务发现的能力,各个微服务启动时,会向Eureka Server注册自己的信息(例如IP、端口、微服务名称等),Eureka Server会存储这些信息。
Eureka Client是一个Java客户端,用于简化与Eureka Server的交互。
微服务启动后,会周期性(默认30秒)地向Eureka Server发送心跳以续约自己的“租期”。
如果Eureka Server在一定时间内没有接收到某个微服务实例的心跳,Eureka Server将会注销该实例(默认90秒)。
默认情况下,Eureka Server同时也是Eureka Client。多个Eureka Server实例,互相之间通过复制的方式,来实现服务注册表中数据的同步。
Eureka Client会缓存服务注册表中的信息。这种方式有一定的优势——首先,微服务无须每次请求都查询Eureka Server,从而降低了Eureka Server的压力;其次,即使Eureka Server所有节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者并完成调用。
综上,Eureka通过心跳检查、客户端缓存等机制,提高了系统的灵活性、可伸缩性和可用性。
编写Eureka Server
本节来编写一个Eureka Server。
创建一个ArtifactId是microservice-discovery-eureka的Maven工程,并为项目添加以下依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
1
2
3
4
编写启动类,在启动类上添加@EnableEurekaServer注解,声明这是一个Eureka Server。
package com.itmuch.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
1
2
3
4
5
6
7
8
9
10
11
在配置文件application.yml中添加如下内容。
server:
port: 8761
eureka:
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://localhost:8761/eureka/
1
2
3
4
5
6
7
8
简要讲解一下application.yml中的赔指数型:
eureka.client.registerWithEureka:表示是否将自己注册到Eureka Server,默认为true。由于当前应用就是Eureka Server,故而设为false。
eureka.client.fetchRegistry:表示是否从Eureka Server获取注册信息,默认为true。因为这是一个单点的Eureka Server,不需要同步其他的Eureka Server节点的数据,故而设为false。
eureka.client.serviceUrl.defaultZone:设置与Eureka Server交互的地址,查询服务与注册服务都需要依赖这个地址。默认是http://localhost:8761/eureka/;多个地址可使用‘,’分隔。
这样一个Eureka Server就编写完成了。
测试
启动Eureka Server,访问http://localhost:8761/,可看到如下图所示的界面。
由图可知,Eureka Server的首页展示了很多信息,例如当前实例的系统状态、注册到Eureka Server上的服务实例、常用信息、实例信息等。显然,当前还没有任何微服务实例被注册到Eureka Server上。
将微服务注册到Eureka Server上
本节将之前编写的用户微服务注册到Eureka Server上。
复制项目microservice-simple-provider-user,将ArtifactId修改为microservice-provider-user。
在pom.xml中添加以下依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
1
2
3
4
在配置文件application.ynl中添加以下配置。
spring:
application:
name: microservice-provider-user
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
1
2
3
4
5
6
7
8
9
其中,spring.application.name用于指定注册到Eureka Server上的应用名称;eureka.instance.prefer-ip-address = true表示将自己的IP注册到Eureka Server。如不配置该属性或将其设置为false,则表示注册微服务所在操作系统的hostname到Eureka Server。
编写启动类,在启动类上添加@EnableDiscoveryClient注解,声明这是一个Eureka Client。
package com.itmuch.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication
public class ProviderUserApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderUserApplication.class, args);
}
}
1
2
3
4
5
6
7
8
9
10
11
也可以使用@EnableEurekaClient注解替代@EnableDiscoveryClient。在SpringCloud中,服务发现组件有多种选择,例如ZooKeeper、Consul等。@EnableDiscoveryClient为各种服务组件提供了支持,该注解是spring-cloud-commons项目的注解,是一个高度的抽象;而@EnableEurekaClient表明是Eureka的Client,该注解是spring-cloud-netfix项目中的注解,只能与Eureka一起工作。当Eureka在项目的classpath中时,两个注解没有区别。
这样就可以将用户微服务注册到Eureka Server上了。同理,将电影微服务也注册到Eureka Server上,配置电影微服务的spring.application.name为microservice-consumer-movie。
相关文件如下:
MovieController.java
package com.itmuch.cloud.study.user.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import com.itmuch.cloud.study.user.entity.User;
@RestControllerpublic class MovieController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/user/{id}")
public User findById(@PathVariable Long id) {
return this.restTemplate.getForObject("http://localhost:8000/" + id, User.class);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
User.java
package com.itmuch.cloud.study.user.entity;
import java.math.BigDecimal;
public class User {
private Long id;
private String username;
private String name;
private Integer age;
private BigDecimal balance;
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return this.age;
}
public void setAge(Integer age) {
this.age = age;
}
public BigDecimal getBalance() {
return this.balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
}
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
35
36
37
38
39
40
ConsumerMovieApplication.java
package com.itmuch.cloud.study;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerMovieApplication {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerMovieApplication.class, args);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
application.yml
server:
port: 8010
spring:
application:
name: microservice-consumer-movie
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
1
2
3
4
5
6
7
8
9
10
11
pom.xml
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itmuch.cloud</groupId>
<artifactId>microservice-consumer-movie</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<!-- 引入spring boot的依赖 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.3.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
</dependencies>
<!-- 引入spring cloud的依赖 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Camden.SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 添加spring-boot的maven插件 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
测试
启动microservice-discovery-eureka。
启动microservice-provider-user。
启动microservice-consumer-movie。
访问http://localhost:8761/,可看到如图所示界面。
由图可知,此时用户微服务、电影微服务已经被注册到Eureka Server上了。
Eureka Server的高可用
有分布式应用开发经验的读者应该能够看出,前文编写的单节点的Eureka Server并不适合线上生产环境。Eureka Client会定时连接Eureka Server,获取服务注册表中的信息并缓存到本地。微服务在消费远程API时总是使用本地缓存中的数据。因此一般来说,即使Eureka Server发生宕机,也不会影响到服务之间的调用。但如果Eureka Server宕机时,某些微服务也出现了不可用的情况,Eureka Client中的缓存若不被更新,就可能会影响到微服务的调用,甚至影响到整个应用系统的高可用性。因此,在生产环境中,通常会部署一个高可用的Eureka Server集群。
Eureka Server可以通过运行多个实例并相互注册的方式实现高可用部署,Eureka Server实例会彼此增量地同步信息,从而确保所有节点数据一致。事实上,节点之间相互注册是Eureka Server的默认行为,还记得前文编写单节点Eureka Server时,额外配置了eureka.client.registerWithEureka=false、eureka.client.fetchRegistry=false吗?
本节在前文的基础上,构建一个双节点Eureka Server集群。
复制项目microservice-discovery-eureka,将ArtifactId修改为microservice-discovery-eureka-ha。
配置系统的hosts,Windows系统的hosts文件路径是C:\Windows\System32\drivers\etc\hosts;Linux及Mac OS等系统的文件路径是/etc/hosts。
127.0.0.1 peer1 peer2
将application.yml修改如下:让两个节点的Eureka Server相互注册。
spring:
application:
name: microservice-discovery-eureka-ha
---
spring:
# 指定profile=peer1
profiles: peer1
server:
port: 8761
eureka:
instance:
# 指定当profile=peer1时,主机名是peer1
hostname: peer1
client:
serviceUrl:
#将自己注册到peer2这个Eureka上面去
defaultZone: http://peer2:8762/eureka/
---
spring:
profiles: peer2
server:
port: 8762
eureka:
instance:
hostname: peer2
client:
serviceUrl:
defaultZone: http://peer1:8761/eureka/
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
注意:配置application.yml时,各节点的缩进不要使用tab,而应使用空格,否则可能解析配置信息失败
如上,使用连字符(—)将该application.yml文件分为三段。第二段和第三段分别为spring.profiles指定了一个值,该值表示它所在的那段内容应用在哪个Profile里。第一段由于并未指定spring.profiles,因此这段内容会对所有Profile生效。
经过以上分析,不难理解,我们定义了peer1和peer2这两个Profile。当应用以peer1这个Profile启动时,配置该Eureka Server的主机名为peer1,并将其注册到http://peer2:8762/eureka/;反之,当应用以profile=peer2时,Eureka Server会注册到peer1节点的Eureka Server。
测试
打包项目,并使用以下命令启动两个Eureka Server节点。
java -jar microservice-discovery-eureka-ha-1.0-SNAPSHOT.jar --spring.profiles.active=peer1
java -jar microservice-discovery-eureka-ha-1.0-SNAPSHOT.jar --spring.profiles.active=peer2
1
2
通过spring.profiles.active指定使用哪个profile启动。
访问http://peer1:8761/,会发现“registered-replicas”中已有peer2节点;同理,访问http://peer2:8762/,也能发现其中的“registered-replicas”有peer1节点。