(二)微服务注册与发现
微服务注册与发现
1 服务调用
1.1 需求场景
学生查询已下单股票列表时,需要去股票服务中获取股票详情,补全股票信息。

1.2 单体应用服务间调用
所有接口在同一容器同一应用上下文中,可能直接调用。

1.3 微服务化后服务间调用
服务相互独立,发布在不同容器中,需要通过远程调用。

1.4 微服务化带来的问题
1.服务消费者配置依赖了服务提供者的网络地址(ip和端口),当服务提供者网路地址发生变化,会导致服务消费者修改配置,重新发布。
2.消费者只能配置单点服务提供者网络地址,导致单点问题,哪怕是通过nginx代理之后,服务提供者集群节点发生变化,也需要修改nginx配置,并重新发布
2 服务注册与发现
要想解决这些问题,服务消费者需要一个强大的服务注册和发现机制,服务消费者使用这种机制获取服务提供者的网络信息,服务消费者可以动态感知到服务提供者网络变化,无需修改配置。
服务注册和发现结构大致如下:

微服务的网路地址都有服务注册中心管理,提供一下功能:
1.微服务信息存储:用来记录每个微服务的信息,列入微服务的唯一标示,网络信息(ip和端口);
2.提供服务的注册,注销和发现功能,注册和注销用于每个微服务提供者信息变化管理,发现功能用于服务消费者获取可用的微服务提供者信息;
3.服务检查机制,服务的注册和注销是由服务提供者主动发起变更请求,服务提供者正常情况下没问题,当出现宕机,网络问题导致服务提供者不能发起请求或者请求丢失,导致已注册服务信息无法,所以服务注册中心需要对已注册的服务做定时的检查机制。当某个微服务实例长时间无法访问,需要移除该实例。
spring cloud 给我们提供了多个注册中心组件的支持,例如Eureka,zk等,下面就来讲讲官方推荐的Eureka。
2.1 Eureka介绍
Eureka是Netflix开源的服务发现组件,本身是一个基于REST的服务。它包含Server和Client两部分。SpringCloud将它集成在子项目Spring Cloud Netflix中,从而实现微服务的注册与发现。
2.2 Eureka 高可用架构

官方提供的Eureka 高可用架构是基于AWS(Amazon Web Services)服务架构设计的。通常在我们这种非AWS环境下,us-east-1c、us-east-1d和us-east-1e可理解成不同的机房。
Eureka 高可用架构中:
- Application Service相当于服务提供者
- Application Client相当于服务消费者
- Application Client 通过发现Eureka中 Application Service注册的网络信息实现远程调用(make remote call)
Eureka包含两个组件:Eureka Server和Eureka Client。
- Eureka Server提供服务注册(Register)、续费(Renew)、注销(Cancel)、发现(Get Register)和同步(Replicate)的RESTful API,供Eureka Client 调用。
- Eureka Client是一个Java客户端,用于简化与Eureka Server的交互。
- 如果Eureka Server在一定时间内没有接收到某个微服务实例的续约(Renew)心跳(默认30秒),Eureka Server将会注销该实例(默认90秒)。
- 在高可用Eureka 架构中,Eureka Server同时默认也是Eureka Client,多个Eureka Server通过复制同步(Replicate)服务注册信息
- Eureka Client会缓存服务注册表中的信息。这种方式有一定的优势——首先,微服务无须每次请求都查询Eureka Server,从而降低了Eureka Server的压力;其次,即使Eureka Server所有节点都宕掉,服务消费者
3 Eureka 实战
结合前面服务调用中学生和股票服务之间的业务场景集成Eureka,并将股票服务(服务提供者)注册到Eureka Server中
3.1 编写一个Eureka Server
1.创建一个ArtifactId是finace-training-eureka-server的Maven工程,并为项目添加以下依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
2.在配置文件application.yml中添加如下内容。
server:
port: 8761
eureka:
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://localhost:8761/eureka/
简要讲解一下 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交互的地址,查询服务与注册服务都需要依赖
3.编写启动类,在启动类上添加@EnableEurekaServer注解,声明这是一个Eureka Server。
package com.myhexin.finace.training.eureka.server;
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);
}
}
启动测试
启动Eureka Server,访问http://localhost:8761/,可看到如下图所示的界面:

3.2 将微服务注册到Eureka Server中
1.创建一个ArtifactId是finace-training-stock的Maven工程,并为项目添加以下依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
2.在配置文件application.yml中添加如下内容。
server:
port: 8000
spring:
application:
name: finace-training-stock
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
3.编写启动类,在启动类上添加@EnableDiscoveryClient注解,声明这是一个Eureka Client
package com.myhexin.finace.training.server.stock.main;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = "com.myhexin")
@EnableDiscoveryClient
public class ServierApplication {
public static void main(String[] args) {
SpringApplication.run(ServierApplication.class, args);
}
}
也可以使用@EnableEurekaClient注解替代@EnableDiscoveryClient。在SpringCloud中,服务发现组件有多种选择,例如ZooKeeper、Consul等。@EnableDiscoveryClient为各种服务组件提供了支持,该注解是spring-cloud-commons项目的注解,是一个高度的抽象;而@EnableEurekaClient表明是Eureka的Client,该注解是spring-cloud-netfix项目中的注解,只能与Eureka一起工作。当Eureka在项目的classpath中时,两个注解没有区别。
启动测试
启动服务,访问http://localhost:8761/,可看到如下图所示的界面:

3.3 实现Eureka Server的高可用
Eureka Client会定时连接Eureka Server,获取服务注册表中的信息并缓存到本地。微服务在消费远程API时总是使用本地缓存中的数据。因此一般来说,即使Eureka Server发生宕机,也不会影响到服务之间的调用。但如果Eureka Server宕机时,某些微服务也出现了不可用的情况,Eureka Client中的缓存若不被更新,就可能会影响到微服务的调用,甚至影响到整个应用系统的高可用性。因此,在生产环境中,通常会部署一个高可用的Eureka Server集群。
Eureka Server可以通过运行多个实例并相互注册的方式实现高可用部署,Eureka Server实例会彼此增量地同步信息,从而确保所有节点数据一致。事实上,节点之间相互注册是Eureka Server的默认行为。
一.改造finace-training-eureka-server高可用
启动8761 finace-training-eureka-server
1、配置文件application.yml修改如下
spring:
application:
name: finace-training-eureka-server
server:
port: 8761
eureka:
client:
serviceUrl:
#将自己注册到localhost:8672这个Eureka上面去
defaultZone: http://localhost:8762/eureka/
去除eureka.client.registerWithEureka=false 和 eureka.client.fetchRegistry=false配置项
2.启动服务
启动8762 finace-training-eureka-server
1.讲finace-training-eureka-server项目打包,部署到本机任意目录
2.将该jar包中的application.yml修改如下
spring:
application:
name: finace-training-eureka-server
server:
port: 8762
eureka:
client:
serviceUrl:
#将自己注册到localhost:8671这个Eureka上面去
defaultZone: http://localhost:8761/eureka/
3.使用java -jar finace-training-eureka-server-0.0.1-SNAPSHOT.jar 启动
启动测试
启动服务,分别访问http://localhost:8761/和http://localhost:8762/,可看到如下图所示的界面:

二.将应用注册到Eureka Server集群上
1.修改finace-training-stock 的application.yml配置文件eureka.client.serviceUrl.defaultZone配置项
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
微服务即使只配置Eureka Server集群中的某个节点,也能正常注册到Eureka Server集群,因为多个Eureka Server之间的数据会相互同步。例如(之前的配置文件不做修改):
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
启动测试
启动服务,分别访问http://localhost:8761/和http://localhost:8762/,可看到如下图所示的界面:

4 深入剖析 Eureka
4.1 数据存储结构
Eureka 的数据存储结构:

-
Eureka Server 将服务信息放到内存,没有持久化。
-
Eureka Server为了避免同时读写内存数据结构造成的并发冲突问题,采用了多级缓存机制。ureka Client在发现(Get Register)服务信息时,先从二级缓存层中获取数据,如果获取不到,先将数据存储层中服务信息加载到缓存中,再从缓存中获取信息(具体实现:ResponseCacheImpl.getValue)。
-
在从数据存储层加到服务信息到缓存的过程中,将服务信息数据处理成可以直接返回Eureka Client的数据放到缓存中,这样在缓存层就不需要再处理数据(具体实现:ResponseCacheImpl.readWriteCacheMap.load)。
以上实现提高了Eureka Server的处理能力和响应速度,保证了最关键的微服务注册中心的性能和可靠性。
数据存储层
rigistry 本质上是一个双层的 ConcurrentHashMap,存储在内存中的。
- 第一层是服务应用层, key 是spring.application.name(股票服务:finace-training-stock),value 是第二层 ConcurrentHashMap;
- 第二层服务应用实例层, key 是服务每个实例的 InstanceId,value 是 Lease 对象;
- Lease 对象包含了实例的服务详情和服务治理相关的属性(lastRenewalTimestamp:最近一次续约(Renew)或者心跳时间)。
将finace-training-stock发布port=8000和port=8010两个服务,访问http://localhost:8761/eureka/apps (参考其他:Eureka 提供的REST端点)获取8761 Eureka 下所有的服务信息:

缓存一致性
既然是缓存,那必然要有更新机制,来保证数据的一致性。下面是缓存的更新机制(Eureka 缓存实现ResponseCacheImpl.java):

更新机制包含删除和加载两个部分,上图黑色箭头表示删除缓存的动作,绿色表示加载或触发加载的动作。
删除二级缓存时机:
- Eureka Client 发送 register、renew 和 cancel 请求并更新 registry 注册表之后,删除二级缓存;
- Eureka Server 自身的 Evict Task 剔除服务后,删除二级缓存;
- 二级缓存本身设置了 guava 的失效机制,隔一段时间后自己自动失效;
加载二级缓存时机:
- Eureka Client 发送 getRegistry 请求后,如果二级缓存中没有,就触发 guava 的 load,即从 registry 中获取原始服务信息后进行处理加工,再加载到二级缓存中。
- Eureka Server 更新一级缓存的时候,如果二级缓存没有数据,也会触发 guava 的 load。
更新一级缓存时机:
- Eureka Server 内置了一个 TimerTask,定时将二级缓存中的数据同步到一级缓存(这个动作包括了删除和加载)。
4.2 服务注册(Register)
当服务提供者启动后,Eureka Client 会启动一个循环定时(默认30s)向Eureka Server注册(同步)自己的服务信息。
Eureka Client :

1.服务注册(默认30s)定时任务:com.netflix.discovery.InstanceInfoReplicator

2.调用 Eureka Server 注册接口(POST /eureka/apps/{appID}):com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient.register()

Eureka Server:

1.注册逻辑解析com.netflix.eureka.registry.AbstractInstanceRegistry.register()

4.3 服务续约(Renew)
Eureka Client 注册服务之后,会启动一个循环定时任务(默认30s)向Eureka Server发送续约请求,告诉注册中心 " awms ";
Eureka Client :

1.定时任务(默认 30s):com.netflix.discovery.DiscoveryClient.initScheduledTasks()

2.心跳线程:com.netflix.discovery.DiscoveryClient.HeartbeatThread

3.调用 Eureka Server 续约(心跳)接口(PUT /eureka/apps/{appID}/{instanceID}):
com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient.register()

**Eureka Server ** :

1.续约操作:com.netflix.eureka.lease.Lease.renew()

2.遍历集群类节点,同步续约请求:com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.replicateToPeers()

4.4 服务注销(Cancel)
服务正常停止之前会向注册中心发送注销请求,告诉注册中心"AWSL"。
Eureka Client :

1.调用 Eureka Server 注销接口(DELETE /eureka/apps/{appID}/{instanceID}):
com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient.register()

Eureka Server :

1.register中存在该服务注册信息,会删除服务实例信息
2.清理缓存
4.5 服务获取(Get Register)
-
Eureka Client 会缓存获取到的服务信息,微服务首先从Eureka Client中获取缓存的服务信息,
Eureka Client 定时(默认30s)去Eureka Server获取服务信息,更新本地缓存。
-
Eureka Client 获取服务有两种方式:全量获取和增量获取,可以通过配置eureka.client.disableDeltaForRemoteRegions,默认false,全量获取。
Eureka Client

1.获取服务信息(默认30s)任务,com.netflix.eureka.registry.RemoteRegionRegistry

2.获取服务信息代码:

Eureka Server
-
全量获取
先从缓存中获取,取不到从registry中查询到缓存再从缓存中取出来返回。
-
增量获取
1.在Registery 里面维护了一个ConcurrentLinkedQueue
recentlyChangedQueue ;当出现服务注册(register)和注销(cancel)会将最新的服务信息保存到该队列中。 2.定时(默认30s)任务定时清理recentlyChangedQueue 中过期的RecentlyChangedItem
过期算法实现(默认3分钟,180s过期):

3.从recentlyChangedQueue 获取增量变化的服务信息
4.6 服务检查
正常停止的服务可以发送服务注册(cancel)请求注销服务,但是服务由于宕机或者网络问题导致无法发送服务注销请求,可能导致注册中心中的服务不可用,这个时候就要一种服务检查机制,去找出这些问题服务,将他们剔除。
- Eureka Server 在启动时开启了一个服务检查任务
- 检查任务分析

1.自我保护检查
当Eureka Server节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。一旦进入该模式,Eureka Server就会保护服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。当网络故障恢复后,该Eureka Server节点会自动退出自我保护模式。
自我保护开始依据:

-
getNumOfRenewsInLastMin :最近一分所有服务续约总数,在Renew请求中统计
-
numberOfRenewsPerMinThreshold : 自我保护阈值,期望每分钟最少服务续约最少总数
自我保护阈值 = 服务总数 * (60S/ 客户端续约间隔(默认30s)) * 自我保护阈值因子(默认0.85)
举例:如果有 100 个服务,续约间隔是 30S,自我保护因子 0.85
自我保护阈值 =100 * 60 / 30 * 0.85 = 170。
如果最近一分所有服务续约总数 = 180 > 170,则说明大量服务可用,是服务问题,需要剔除过期服务
如果最近一分所有服务续约总数 = 150 > 170,则说明大量服务不可用,是注册中心自己的问题,进入自我
保护模式,不进入剔除服务流程。
2.过期服务
服务默认30s续约一次,90s过期时间,也就是说超过连续三次不续约就将认为是过期服务
3.计算需要剔除的服务数
并不是所有过期的过期的服务都需要剔除,只有当过期服务数超过剔除阈值时才剔除超过阈值的服务数,
服务阈值 = 服务总数 * 自我保护因子;
需要剔除服务数 = 过期服务数 - 服务阈值
4.剔除服务
通过Knuth shuffle algorithm(洗牌算法)找到需要剔除的服务,走Eurka Server的服务注销流程。
4.7 服务同步(Replicate)
Eureka Server之间通过同步机制来保证节点之间的数据一致性。分为Eureka Server 启动时同步和运行是同步。
启动时同步
- Eureka Server 启动时会作为Client向配置文件中配置的Eureka Server注册中心注册自己
- Eureka Server作为Client 向Eureka Server注册中心获取服务注册信息,获取成功后走自身的注册流程将获取到的服务自身的注册表中。

运行时同步
Eureka Server 在收到服务注册(register),续约(renew)和注销(cancel)请求时,在完成操作向配置文件中配置的Eureka Server 转发这些请求,以做到服务在每个Eureka Server上的一致性。
com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.relicateToPeers():

5 其他
5.1 为Eureka Server添加用户认证
在前面的Eureka实战中,Eureka Server是匿名访问的,也可以为Eureka Server添加基于HTTP basic的认证,这样需要登录才能访问Eureka Server。
1.在finace-training-eureka-server 添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.在application.yml中添加以下内容:
security:
basic:
#开启基于HTTP basic的认证
enabled: true
user:
#配置登录账号
name: user
#配置登录密码
password: password123
3.启动服务
启动测试
启动服务,访问http://localhost:8761/,需要输入以上用户密码才可访问。
将微服务注册到需认证的Eureka Server
只须将eureka.client.serviceUrl.defaultZone配置为http://user:password@EUREKA_HOST:EUREKA_PORT/eureka/ 这种形式
例如:
eureka:
client:
serviceUrl:
defaultZone: http://user:password123@localhost:8761/eureka/
5.2 多网卡环境下的IP选择
对于多网卡的服务器,各个微服务注册到Eureka Server上的IP要如何指定呢?
指定IP在某些场景下很有用。例如某台服务器有eth0、eth1、eth2三块网卡,但是只有eth1可以被其他的服务器访问;如果Eureka Client将eth0或者eth2注册到Eureka Server上,其他微服务就无法通过这个IP调用该微服务的接口。
SpringCloud提供了按需选择IP的能力,从而避免以上的问题。下面来详细讨论。
1.忽略指定名称的网卡
spring:
cloud:
inetutils:
ignored-interfaces:
- docker0
- veth.*
eureka:
instance:
prefer-ip-address: true
2.使用正则表达式,指定使用的网络地址
spring:
cloud:
inetutils:
preferredNetworks:
- 192.168
- 10.0
eureka:
instance:
prefer-ip-address: true
3.只使用站点本地地址,这样就可强制使用站点本地地址。
spring:
cloud:
inetutils:
useOnlySiteLocalInterfaces: true
eureka:
instance:
prefer-ip-address: true
4.手动指定IP地址
在某些极端场景下,可以手动指定注册到Eureka Server的微服务IP。示例:
eureka:
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
5.3 Eureka 提供的REST端点
Eurka Server主要接口:
-
应用相关(/eureka/apps/):com.netflix.eureka.resources.ApplicationResource
-
实例相关(/eureka/apps/{appID}/):com.netflix.eureka.resources.InstanceResource
可以使用XML或者JSON与这些端点通信,默认是XML
| 请求名称 | 请求方式 | HTTP地址 | 请求描述 |
|---|---|---|---|
| 注册新服务 | POST | /eureka/apps/ | 传递JSON或者XML格式参数内容,HTTP code为204时表示成功 |
| 取消注册服务 | DELETE | /eureka/apps/{appID}/ | HTTP code为200时表示成功 |
| 发送服务心跳 | PUT | /eureka/apps/{appID}/ | HTTP code为200时表示成功 |
| 查询所有服务 | GET | /eureka/apps | HTTP code为200时表示成功,返回XML/JSON数据内容 |
| 查询指定appID的服务列表 | GET | /eureka/apps/ | HTTP code为200时表示成功,返回XML/JSON数据内容 |
| 查询指定appID&instanceID | GET | /eureka/apps/{appID}/ | 获取指定appID以及InstanceId的服务信息,HTTP code为200时表示成功,返回XML/JSON数据内容 |
| 查询指定instanceID服务列表 | GET | /eureka/apps/instances/ | 获取指定instanceID的服务列表,HTTP code为200时表示成功,返回XML/JSON数据内容 |
| 变更服务状态 | PUT | /eureka/apps/{appID}/{instanceID}/status?value=DOWN | 服务上线、服务下线等状态变动,HTTP code为200时表示成功 |
| 变更元数据 | PUT | /eureka/apps/{appID}/{instanceID}/metadata?key=value | HTTP code为200时表示成功 |
| 查询指定IP下的服务列表 | GET | /eureka/vips/ | HTTP code为200时表示成功 |
| 查询指定安全IP下的服务列表 | GET | /eureka/svips/ | HTTP code为200时表示成功 |
声明
本博客所有内容仅供学习,不为商用,如有侵权,请联系博主,谢谢。
参考文献
[1] Eureka的GitHub : https://github.com/Netflix/eureka
[2] 周立. Spring Cloud与Docker微服务架构实战
[3] 马军伟. 微服务注册中心 Eureka 架构深入解读.https://www.infoq.cn/article/jlDJQ*3wtN2PcqTDyokh

浙公网安备 33010602011771号