Eureka 集群

  • 使用了注册中心之后,所有的服务都要通过服务注册中心来进行信息交换。
  • 那么服务注册中心的稳定性就变得非常重要了,一旦服务注册中心掉线,那么就会影响到整个系统的稳定性。
  • 所以,在实际开发中,Eureka一般都是以集群的形式出现。

Eureka集群,实际上就是启动多个Eureka示例,多个Eureka实例之间,相互注册,相互同步数据,共同组成一个Eureka集群。


Eureka 集群搭建

  1. 首先需要修改电脑的 hosts文件

    127.0.0.1 eureka-a eureka-b
  2. 在resources中添加两个环境模拟集群:

    application-a.yml
    application-b.yml
    

    打成jar包流程:跳过test测试,package打包

  3. 启动Eureka实例

打包完成后,在命令行启动实例

  • 打开term终端

    cd target/
  • 启动实例:

    1
    2
    java -jar eureka-0.0.1-SNAPSHOT.jar --spring.profiles.active=a
    java -jar eureka-0.0.1-SNAPSHOT.jar --spring.profiles.active=b

Eureka 细节

Eureka 本身可以分为两大部分

  • Eureka Server
  • Eureka client

Eureka Server

Eureka Server主要提供了三个功能:

  1. 服务注册,所有的服务都注册到Eureka Server上面
  2. 提供了注册表,注册表就是所有注册上来的服务的一个列表,Eureka Client在调用服务的时候,需要获取这个注册表,一般来说,这个注册表会被缓存下来,如果缓存失效了,则直接获取最新的注册表
  3. 同步状态,Eureka Client通过注册、心跳等机制,和Eureka Server同步当前客户端的状态

Eureka Client

  • Eureka Client主要是用来简化每一个服务和Eureka Server之间的交互
  • Eureka Client会自动拉取、更新以及缓存Eureka Server的信息,这样的话,即使Eureka Server所有的节点都宕机了
  • Eureka Client依然可以在缓存中获取到想要调用服务的地址(但是地址有可能会发生改变,从而不准确)。

Eureka 服务注册

  • 服务提供者将自己注册到服务注册中心(Eureka Server)
  • 需要注意的是,所谓的服务提供者,只是一个业务上的划分,本质上它就是一个Eureka Client
  • 当Eureka Client向Eureka Server注册时,他需要提供一些自身的元数据信息,例如 IP地址、端口号、名称、运行状态等等。

Eureka 服务续约

  • Eureka Client注册到Eureka Server上之后,事情才刚刚开始运行
  • 注册成功之后,默认的情况下,Eureka Client每隔30秒便会向Eureka Server发送一条心跳消息,来告诉Eureka Server我还在运行中
  • 如果Eureka Server连续90秒都没有收到Eureka Client的续约消息(连续三次没发送回复),Eureka Server就会认为Eureka Client已经掉线了,便会将掉线的Eureka Client从当前的服务注册列表中剔除出去。

关于服务续约的两个相关属性(一般不建议修改):

1
2
3
4
## 表示服务的续约时间,默认时间为30秒
eureka.instance.lease-renewal-interval-in-seconds=30
## 表示服务的失效时间,默认时间为90秒
eureka.instance.lease-expiration-duration-in-seconds=90

服务下线

当Eureka Client下线的时候,会主动发送一条信息,告诉Eureka Server,我已经下线。


获取注册表信息

  • Eureka Client会从Eureka Server上获取服务的注册信息,并且将其缓存到本地
  • 那么本地服务端在需要调用远程服务时,会从该缓存信息中查找远程服务所对应的 IP地址、端口号等信息
  • Eureka Client上缓存的服务注册信息也会定期从Eureka Server上更新(30秒)
  • 如果Eureka Server返回的注册表信息与本地缓存的注册表信息不一致的话,Eureka Client会自动更改同步处理。

这里涉及到两个属性:

1
2
3
4
5
6
7
## 表示是否允许从 Eureka Server中获取信息 默认为true
eureka:
client:
fetch-registry: true
instance:
## 表示Eureka Client上缓存的服务注册信息,定时更新的时间间隔,默认为30秒
lease-renewal-interval-in-seconds:30

Eureka 集群原理

  • 在Eureka集群架构中,Eureka Server之间通过 Replicate进行数据同步,不同的Eureka Server之间不区分主从节点,所有的节点都是平等的。在节点之间,通过指定serviceUrl来相互注册,形成一个集群,从而提高节点的可用性。
  • 在Eureka Server集群中,如果有某一个节点宕机,Eureka Client会自动切换到新的Eureka Server上。每一个Eureka Server节点,都会相互同步数据。
  • Eureka Server的连接方式,也可以是单线的,也就是a–>b–>c,此时虽然a的数据也会和c之间互相同步,但是一般不建议这种写法。

在我们配置serviceUrl时,可以指定多个注册地址,也就是a可以注册到b上,也可以同时注册到c上,一般推荐使用这种写法。
Eureka分区:

  1. region: 根据地理上的不同区域分区
  2. zone: 根据具体的机房分区

服务注册与消费

服务注册

  • 服务注册就是把一个微服务注册到Eureka Server上,这样的话,当其他服务需要调用该服务的时候,只需要从Eureka Server上查询该服务的信息即可。

以下我们创建一个 provider,作为我们的服务提供者。

  • 创建一个spring boot项目
  • 选择Eureka Client, Spring Web依赖
  • 这样的话,当服务创建成功之后,简单的配置一下,就可以被注册到Eureka Server上了。

只需要在 application.yml配置一下项目的注册地址即可

1
2
3
4
5
6
7
8
9
10
spring:
application:
name: provider
server:
port: 1114
eureka:
client:
## 注册地址
service-url:
defaultZone: http://localhost:1111/eureka
  • 先启动Eureka Server,等到服务注册中心Eureka Server启动成功之后,再启动 provider项目注册。
  • 检验是否成功注册:等到两者均成功启动
  • 打开 http://localhost:1111 ,就可以查看 provider的注册信息是否存在。

服务消费

  • 首先在 provider中提供一个接口
  • 然后创建一个新的 consumer项目
  • 去消费这个接口(/hello)
  1. 在 provider中,提供了一个 hello接口。
  2. 创建一个 consumer项目,在 consumer项目中,去消费 provider提供的接口
  3. 因为 consumer想要能够获取到 provider这个接口的地址,它就需要去Eureka Server中查询。
  4. 如果直接在consumer中写死 provider的地址,便意味着这两个服务之间的耦合度太高了,所以我们需要降低他们之间的耦合度

写死的调用:

(具体代码参考 provider中HelloController、consumer中UserHelloController(/hello1))

  1. 首先也在 application.yml中配置一下注册信息
  2. 假设我们现在想在 consumer中调用 provider提供的服务,我们可以直接将调用写死,也就是说整个调用过程中不会涉及到Eureka Server。
  • 写死的方法是利用了 HttpUrlConnection来发起的请求,请求中 provider的地址写死了
  • 意味着 provider和 consumer高度的绑定在一起,这个不符合微服务的思想。

灵活调用方法:

(具体代码参考 provider中HelloController、consumer中UserHelloController(/hello2))

我们可以借助Eureka Client提供的 DiscoveryClient工具

利用了这个工具,我们可以根据服务名从Eureka Server上查询到一个服务的详细信息。

注意需要注入下面的工具 DiscoveryClient

1
2
@Autowired
DiscoveryClient discoveryClient;

注意:DiscoveryClient 查询到的服务列表是一个集合,因为服务在部署的过程中,有可能是集群化部署,集合中的每一项就是一个实例。


展示集群化部署

如果要同时启动多个 provider实例,多个 provider实例的端口不同,为了区分调用时到底是哪一个 provider提供的服务,这里可以在接口返回值中返回端口。

修改完成后,对项目进行打包。打包成功之后,在命令行启动两个 provider实例:

1
2
java -jar provider-0.0.1-SNAPSHOT.jar --server.port=1117
java -jar provider-0.0.1-SNAPSHOT.jar --server.port=1118
  • 启动完成后检查Eureka Server,这两个 provider实例是否注册上来。
  • 这个时候注册成功之后,在 consumer中再去调用 provider,DiscoveryClient集合之中,获取到的就不再是一个实例,而是两个实例了。

手动实现线性负载均衡

(具体代码参考 provider中HelloController、consumer中UserHelloController(/hello3))

// todo 一步步简化 手动负载均衡改自动

  • 在从集合中获取数据时,通过小小的改变来实现线性负载均衡。
  • 当请求 http://localhost:1115/hello3 的时候,观察到 port端口号不断变化,则表示负载均衡成功启动。

升级改造集群化部署

从两个方面进行改造:

1. Http调用
  • Http调用,我们可以使用 Spring提供的 RestTemplate来实现。
  • 首先,在当前服务中,提供一个 RestTemplate的实例。 (具体代码参考/consumer/ConsumerApplication)方法:restTemplateOne()
  • 然后在 Http调用时,不再使用 HttpUrlConnection,而是直接使用 RestTemplate。
  • (具体代码参考 provider中HelloController、consumer中UserHelloController(/hello4))
  • 用 RestTemplate一行代码就可以实现了 Http调用。

2. 负载均衡
  • 使用 Ribbon来快速实现负载均衡。
  • 在 RestTemplate上使用 @LoadBalanced注解开启负载均衡
  • (具体代码参考/consumer/ConsumerApplication)方法:restTemplate)
  • 此时的 RestTemplate就自动具备了负载均衡的功能。
  • (具体代码参考 provider中HelloController、consumer中UserHelloController(/hello5))

RestTemplate

  • RestTemplate 是从 Spring 3.0开始支持的一个 Http请求工具
  • RestTemplate和 Spring Boot无关, 更加和Spring Cloud无关。
  • RestTemplate 提供了常见的 REST请求方法模版,例如 GET、 POST、 PUT、 DELETE请求,以及一些通用的请求执行方法 exchange 和 execute 方法。
  • RestTemplate 本身实现了 RestOperations接口,而在 RestOperations接口中,定义了常见的 RESTful操作,这些操作在 RestTemplate中都得到了很好的实现。

GET

(具体代码参考 provider中HelloController(/hello2)、consumer中UserHelloController(/hello6))

  • 首先我们在 provider中定义一个 hello2 接口。
  • 然后在 consumer中去访问这个接口,因为这个接口是一个 GET请求,所以访问方式就是调用 RestTemplate中的 GET请求去访问。

getForObject()、getForEntity() 区别
  • 在 RestTemplate中,关于 GET请求,一共有两大类:getForObject()、getForEntity()
  • 这两大类方法实际上是重载的,唯一的不同就是,返回的值类型不同。
  • getForObject(): 返回的是一个对象,这个对象就是服务端返回的具体值。
  • getForEntity(): 返回的是一个 ResponseEntity,这个 ResponseEntity中除了服务端返回的具体数据外,另外还保留了 Http响应头的数据。
  • 通过 getForObject()可以看到直接拿到了服务的返回值,getForEntity()不仅仅拿到了服务的返回值,还拿到了 http响应的具体信息。
  • 然后启动 Eureka Server、 provider 以及 consumer,访问 consumer中的 http://localhost:1115/hello6 接口,即可以查看到请求结果。

getForObject()、getForEntity() 重载方法
  • getForObject()、getForEntity() 分别有三个重载方法,两者的三个重载方法基本上是一致的。
  • 所以,主要弄清楚一种方法即可。三个重载方法其实代表了三种不同的传参方式。
  • (具体代码参考 provider中HelloController(/hello2)、consumer中UserHelloController(/hello7))
  • 重启 consumer中的 http://localhost:1115/hello7 接口,即可以查看到请求结果。

POST

  • 首先我们在 provider中提供两个 POST接口
  • 同时,因为 POST请求可能需要传递 JSON,所以这里我们创建一个普通的 Maven项目作为 commons模块
  • 然后让这个 commons模块被 provider 和 consumer 共同引用,这样就可以方便我们传递 JSON 了。
  • commons 模块创建成功后,在 commons模块中创建 User对象,分别被 provider 和 consumer 引用。
  • 引入完成后,我们在 provider中提供两个 POST请求接口。
  • (具体代码参考 provider中HelloController(/hello3、/hello4)、consumer中UserHelloController(/hello7))
  • 定义完成后,接下来在 consumer中调用这两个接口。
  • RestTemplate 中的 POST请求和 GET请求很像,只是多出来三个方法
  • 就是 postForLocation,另外两个 postForObject 和 postForEntity 和前面的 get基本是一致的。
  • 所以我们主要看 postForObject 和额外的 postForLocation。

postForObject()

(具体代码参考 provider中HelloController(/user、/user1)、consumer中UserHelloController(/hello8、/hello9))


postForLocation()
  • 有的时候,当我们执行完一个 post请求之后,立马就要进行重定向,
  • 例如一个非常常见的场景就是注册,注册是一个 post请求,注册完成之后,立马重定向到登陆页面去登陆,对于这种场景,我们就可以使用到 postForLocation。
  • 首先我们在 provider上提供一个 RegisterController 用户注册接口。
  • (参考代码 provider/controller/RegisterController.java)
  • post 接口,响应一定要为302,否则 postForLocation无效。
  • (具体代码参考 provider中RegisterController/register、consumer中UserHelloController(/hello10)
  • 注意:重定向的地址,一定要写成绝对路径,不要写成相对路径,否则会在 consumer 中调用时候出问题。
  • postForLocation,调用该方法返回的是一个 uri,这个 uri 就是重定向的地址(里面包含了重定向的参数),拿到了 uri 之后,就可以直接发送新的请求了。

PUT

  • PUT 请求比较简单,重载的方法也比较少,一般用于做更新操作
  • 我们首先在 provider 中提供一个 PUT 接口。 /provider/update1 /provider/update2
  • 注意: PUT 接口传参其实和 POST 很像,也接受两种类型的参数, key-value 形式以及 JSON 形式。
  • 然后,我们在 consumer 中调用该接口。 key-value:/consumer/hello11 json:/consumer/hello12
  • consumer 中的写法基本和 post 类似,也是两种方式,可以传递两种不同类型的参数。

DELETE

  • DELETE 也比较容易,我们有两种方式来传递参数,key-value形式,或者 PathVariable(参数放在路径中)
  • 首先我们在 provider 中定义两个 DELETE 方法。
  • key-value:/provider/delete1 PathVariable:/provider/delete2
  • 然后在 consumer 中调用这两个删除的接口。
  • (具体代码参考 consumer中UserHelloController(/hello13))
  • delete 参数的传递也支持 map, 这块实际上和 get 也是一样的。

客户端负载均衡

  • 客户端负载均衡就是相对服务端负载均衡而言的。
  • 服务端负载均衡,就是传统的 Nginx 的方式,用 Nginx 做负载均衡,我们称之为服务端负载均衡
  • 服务端负载均衡:它的一个特点是,就是调用的客户端并不知道自己具体是由哪一个 Server 提供的服务,它也不会关心,反正请求发送给 Nginx, Nginx再将请求转发给 Tomcat(上游服务器、service)。客户端只需要记住 Nginx的地址就可以了。
  • 客户端负载均衡:它的特点是,调用的客户端本身它是知道所有 Server 的详细信息的,当你需要调用 Server 上的接口的时候,客户端会从自身所维护的 Server 列表中,根据提前配置好的负载均衡策略,自己挑选一个 Server 来调用,此时,客户端是知道它所调用的是哪一个 Server。
  • 在 RestTemplate 中,如果想使用负载均衡功能,只需要给 RestTemplate 实例上添加一个 @LoadBalanced 注解即可,此时,RestTemplate 就会自动具备负载均衡功能,这个负载均衡就是客户端负载均衡。

负载均衡原理

  • 在 Spring Cloud 中,实现负载均衡非常容易,只需要添加 @LoadBalanced 注解即可。
  • 只要添加了该注解,一个原本普普通通的 Rest 请求的工具 RestTemplate 就会自动具备负载均衡功能

整体上来说的话,这个功能的实现就是三个核心点:

  1. 从 Eureka Client 本地缓存的服务注册信息中,选择一个可以调用的服务
  2. 根据 1 中所选择的服务,重构请求 URL 地址(不可以直接写域名+端口号,只能写服务名)
  3. 将 1、2 步的功能嵌入到 RestTemplate 中