2014 年 3 月,Martin Fowler 写的一篇文章 《Microservices》以通俗易懂的形式为大家定义了什么是微服务架构。
文章连接 https://martinfowler.com/articles/microservices.html
微服务架构是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值.每个服务运行在其独立的进程
中,服务与服务间采用轻量级的通信机制互相协作(通常是基于HTTP协议的RESTful API
).每个服务都围绕着具体业务进行构建,并且能够被独立的部署到生产环境、类生产环境等。
微服务架构强调的重点是业务系统需要彻底的组件化和服务化,原有的单个业务系统会拆分为多个可以独立开发、设计、运行和运维的小应用,这些小应用之间通过服务完成交互和集成。
优点
缺点
Spring Cloud 从设计之初就考虑了分布式架构演化所需的功能,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等。这些功能都是以插拔的形式提供出来,方便我们在系统架构演进的过程中,可以合理选择需要的组件进行集成,从而在架构演进的过程中会更加平滑、顺利。
Spring Cloud 提供的功能,完美地解决了微服务实施过中可能会遇到的困难。对比业内相关框架,在微服务领域 Spring Cloud 目前还没有真正的对手,从某种程度来讲 Spring Cloud 已经成为了微服务落地的技术标准。
SpringCloud F版本
之后,推荐使用 SpringCloud Alibaba 系列的技术栈(Nacos、Sentinel、Seata),不推荐使用Netflix的技术栈,比如 Eureka、Hystrix、ribbon、zuul等,因为这几个都进入了维护模式。
由于,现在依然有很多公司在使用 Eureka、ribbon、Hystrix 这几个技术,所以,后面的文章依然会讲解它们的使用。
发送 get请求 https://start.spring.io/actuator/info
因为后面的文章,我们会演示SpringCloud 和 SpringCloud Alibaba 的用法,所以这里我们版本统一使用 SpringBoot 2.2.2.RELEASE 、SpringCloud Hoxton.SR11、spring-cloud-alibaba 2.2.1.RELEASE
版本。
Eureka 就是 Netflix 开源的一款提供服务注册和发现的产品,Spring Cloud 封装了 Eureka 模块,在 Eureka 的基础上优化了一些配置,对一些不太合理的逻辑进行了优化,并提供了可视化界面,方便查看服务的运行状态。
从上图我们可以看出有三个角色:
Eureka Server
,担任注册中心的角色,提供了服务的注册和发现功能Service Provider,服务提供者
,将自身服务注册到 Eureka Server,同时通过心跳来检查服务的运行状态Service Consumer,服务消费者
,从 Eureka 获取注册服务列表,找到对应的服务地址再进行调用
服务提供者和服务消费者其实是 Eureka Client 角色。
注册中心服务端主要对外提供了三个功能:
服务提供者和服务消费者其实是 Eureka Client ,Eureka Client 是一个 Java 客户端,用于简化与 Eureka Server 的交互。Eureka Client 会拉取、更新和缓存 Eureka Server 中的信息。因此当所有的 Eureka Server 节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者,但是当服务有更改的时候会出现信息不一致。
服务的提供者,将自身注册到注册中心,服务提供者也是一个 Eureka Client。当 Eureka Client 向 Eureka Server 注册时,它提供自身的元数据,比如 IP 地址、端口,运行状况指示符 URL,主页等。
Eureka Client 会每隔 30 秒发送一次心跳来续约。 通过续约来告知 Eureka Server 该 Eureka Client 运行正常,没有出现问题。 默认情况下,如果 Eureka Server 在 90 秒内没有收到 Eureka Client 的续约,Server 端会将实例从其注册表中删除,此时间可配置,一般情况不建议更改。
服务续约的两个重要属性:
服务续约任务的调用间隔时间,默认为30秒
eureka.instance.lease-renewal-interval-in-seconds=30
服务失效的时间,默认为90秒。
eureka.instance.lease-expiration-duration-in-seconds=90
Eviction 服务剔除
当 Eureka Client 和 Eureka Server 不再有心跳时,Eureka Server 会将该服务实例从服务注册列表中删除,即服务剔除。
Eureka Client 在程序关闭时向 Eureka Server 发送取消请求。 发送请求后,该客户端实例信息将从 Eureka Server 的实例注册表中删除。该下线请求不会自动完成,它需要调用以下内容:
DiscoveryManager.getInstance().shutdownComponent();
Eureka Client 从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。该注册列表信息定期(每30秒钟)更新一次。每次返回注册列表信息可能与 Eureka Client 的缓存信息不同,Eureka Client 自动处理。
如果由于某种原因导致注册列表信息不能及时匹配,Eureka Client 则会重新获取整个注册表信息。 Eureka Server 缓存注册列表信息,整个注册表以及每个应用程序的信息进行了压缩,压缩内容和没有压缩的内容完全相同。Eureka Client 和 Eureka Server 可以使用 JSON/XML 格式进行通讯。在默认情况下 Eureka Client 使用压缩 JSON 格式来获取注册列表的信息。
获取服务是服务消费者的基础,所以必有两个重要参数需要注意:
# 启用服务消费者从注册中心拉取服务列表的功能
eureka.client.fetch-registry=true
# 设置服务消费者从注册中心拉取服务列表的间隔
eureka.client.registry-fetch-interval-seconds=30
当 Eureka Client 从注册中心获取到服务提供者信息后,就可以通过 Http 请求调用对应的服务;服务提供者有多个时,Eureka Client 客户端会通过 Ribbon 自动进行负载均衡。
006SpringCloud
pom.xml中管理 spring-boot、spring-cloud、spring-cloud-alibaba的版本
4.0.0
com.xander
006SpringCloud
1.0-SNAPSHOT
pom
8
8
UTF-8
org.springframework.boot
spring-boot-dependencies
2.2.2.RELEASE
pom
import
org.springframework.cloud
spring-cloud-dependencies
Hoxton.SR11
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
2.2.1.RELEASE
pom
import
org.springframework.boot
spring-boot-maven-plugin
true
true
eureka-server-7001
pom.xml
006SpringCloud
com.xander
1.0-SNAPSHOT
4.0.0
com.xander
eureka-server-7001
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-devtools
runtime
true
新建 EurekaServer7001.java
@SpringBootApplication
@EnableEurekaServer //启用EurekaServer注册中心功能
public class EurekaServer7001 {
public static void main(String[] args) {
SpringApplication.run(EurekaServer7001.class, args);
}
}
application.yml
server:
port: 7001
eureka:
instance:
hostname: eureka7001 #eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
# defaultZone 设置与 Eureka Server 交互的地址查询服务和注册服务都需要依赖这个地址
defaultZone: http://localhost:${server.port}/eureka/
EurekaServer7001
服务,并访问 http://localhost:7001/可以发现后台页面被分为了五大块:
理论上来讲,服务消费者本地缓存了服务提供者的地址。即使 Eureka Server 宕机,也不会影响服务之间的调用,但是一旦涉及到服务的上下线,本地的缓存信息将会出现偏差,从而影响到了整个微服务架构的稳定性,因此搭建 Eureka Server 集群来提高整个架构的高可用性,是非常有必要的。
Eureka Server 集群相互之间通过 Replicate 来同步数据,相互之间不区分主节点和从节点,所有的节点都是平等的。在这种架构中,节点通过彼此互相注册来提高可用性,每个节点需要添加一个或多个有效的 serviceUrl 指向其他节点。
如果某台 Eureka Server 宕机,Eureka Client 的请求会自动切换到新的 Eureka Server 节点。当宕机的服务器重新恢复后,Eureka 会再次将其纳入到服务器集群管理之中。当节点开始接受客户端请求时,所有的操作都会进行节点间复制,将请求复制到其它 Eureka Server 当前所知的所有节点中。
Eureka Server 集群之间的状态是采用异步方式同步的,所以不保证节点间的状态一定是一致的,不过基本能保证最终状态是一致的。
Eureka 保证 AP
Eureka Server 各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而 Eureka Client 在向某个 Eureka 注册时,如果发现连接失败,则会自动切换至其它节点。只要有一台 Eureka Server 还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。
新建 module eureka-server-7002
pom.xml
006SpringCloud
com.xander
1.0-SNAPSHOT
4.0.0
com.xander
eureka-server-7002
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-devtools
runtime
true
新建服务启动类 EurekaServer7002.java
@SpringBootApplication
@EnableEurekaServer //启用EurekaServer注册中心功能
public class EurekaServer7002 {
public static void main(String[] args) {
SpringApplication.run(EurekaServer7002.class, args);
}
}
application.yml
server:
port: 7002
eureka:
instance:
hostname: eureka7002.com #eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
# defaultZone 设置与 Eureka Server 交互的地址查询服务和注册服务都需要依赖这个地址
#集群指向其它eureka
defaultZone: http://eureka7001.com:7001/eureka/
修改eureka-server-7001
的 application.yml
server:
port: 7001
eureka:
instance:
hostname: eureka7001.com #eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
# defaultZone 设置与 Eureka Server 交互的地址查询服务和注册服务都需要依赖这个地址
#集群指向其它eureka
defaultZone: http://eureka7002.com:7002/eureka/
在 Windows 的 C:\Windows\System32\drivers\etc\hosts
或者 Linux 的 /etc/hosts
文件末添加以下信息:127.0.0.1 eureka7001.com eureka7002.com
启动 eureka-server-7001
和 eureka-server-7002
服务,在 DS Replicas 模块中可以看到另外注册中心的别名。
common-api
存放公共的实体和接口common-api 的pom.xml
006SpringCloud
com.xander
1.0-SNAPSHOT
4.0.0
common-api
新建相应实体封装 CommonResult.java
package com.xander.entities;
/**
* Description: 公共响应实体封装
*
* @author Xander
* datetime: 2021-05-10 22:34
*/
public class CommonResult {
private Integer code;
private String message;
private T data;
public static CommonResult newInstance() {
CommonResult instance = new CommonResult();
return instance;
}
public Integer getCode() {
return code;
}
public CommonResult setCode(Integer code) {
this.code = code;
return this;
}
public String getMessage() {
return message;
}
public CommonResult setMessage(String message) {
this.message = message;
return this;
}
public T getData() {
return data;
}
public CommonResult setData(T data) {
this.data = data;
return this;
}
}
provider-payment9001
pom.xml
006SpringCloud
com.xander
1.0-SNAPSHOT
4.0.0
provider-payment9001
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
com.xander
common-api
1.0-SNAPSHOT
application.yml
server:
port: 9001
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
# 集群版-
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
instance:
# 服务实例id
instance-id: payment9001
#访问路径可以显示IP地址
prefer-ip-address: true
spring:
application:
# 微服务名称
name: paymentService
服务启动类 ProviderPayment9001.java
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class ProviderPayment9001 {
public static void main(String[] args) {
SpringApplication.run(ProviderPayment9001.class, args);
}
}
PaymentController
@RestController
@RequestMapping("/payment")
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/get/{id}")
public CommonResult getPaymentById(@PathVariable("id") Long id) {
if (id != null && id % 2 == 0) {
Payment payment = Payment.newInstance().setSerial(UUID.randomUUID().toString()).setId(id);
return CommonResult.newInstance().setCode(200).setMessage("查询成功,serverPort:" + serverPort).setData(payment);
} else {
return CommonResult.newInstance().setCode(444).setMessage("没有对应记录,查询ID: " + id).setData(null);
}
}
}
启动服务,可以看到微服务 paymentService 已经注册进 Eureka 注册中心
consumer-order8001
pom.xml
006SpringCloud
com.xander
1.0-SNAPSHOT
4.0.0
consumer-order8001
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
com.xander
common-api
1.0-SNAPSHOT
application.yml
server:
port: 8001
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
# 集群版-
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
instance:
# 服务实例id
instance-id: order8001
#访问路径可以显示IP地址
prefer-ip-address: true
spring:
application:
# 微服务名称
name: orderService
服务启动类 ConsumerOrder8001.java
@SpringBootApplication
@EnableEurekaClient
public class ConsumerOrder8001 {
public static void main(String[] args) {
SpringApplication.run(ConsumerOrder8001.class, args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
OrderController.java
@RestController
@RequestMapping("/order")
public class OrderController {
public static final String PAYMENT_URL = "http://localhost:9001";
@Autowired
private RestTemplate restTemplate;
@GetMapping("/getPayment/{id}")
public CommonResult getPayment(@PathVariable("id") String id) {
return this.restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
}
}
启动服务,可以看到微服务 orderService 已经注册进 Eureka 注册中心
浏览器测试服务间调用,orderService 通过 http 请求调用 paymentService
默认情况下,如果 Eureka Server 在一定的 90s 内没有接收到某个微服务实例的心跳,会注销该实例。但是在微服务架构下服务之间通常都是跨进程调用,网络通信往往会面临着各种问题,比如微服务状态正常,网络分区故障,导致此实例被注销。
固定时间内大量实例被注销,可能会严重威胁整个微服务架构的可用性。为了解决这个问题,Eureka 开发了自我保护机制,那么什么是自我保护机制呢?
Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否超过85%,如果超过85%的客户端节点都没有正常的心跳,那么Eureka认为客户端与注册中心出现了网络故障,Eureka Server 即会进入自我保护机制。
Eureka Server 进入自我保护机制,会出现以下几种情况:
Eureka 自我保护机制是为了防止误杀服务而提供的一个机制。当个别客户端出现心跳失联时,则认为是客户端的问题,剔除掉客户端;当 Eureka 捕获到大量的心跳失败时,则认为可能是网络问题,进入自我保护机制;当客户端心跳恢复时,Eureka 会自动退出自我保护机制。
Eureka Server 的数据存储分了两层:数据存储层和缓存层。数据存储层记录注册到 Eureka Server 上的服务信息,缓存层是经过包装后的数据,可以直接在 Eureka Client 调用时返回。我们先来看看数据存储层的数据结构。
Eureka Server 的数据存储层是双层的 ConcurrentHashMap,我们知道 ConcurrentHashMap 是线程安全高效的 Map 集合。
private final ConcurrentHashMap>> registry= new ConcurrentHashMap>>();
第一层的 ConcurrentHashMap 的 key=spring.application.name,也就是客户端实例注册的应用名;value 为嵌套的 ConcurrentHashMap。
第二层嵌套的 ConcurrentHashMap 的 key=instanceId,也就是服务的唯一实例 ID,value 为 Lease 对象,Lease 对象存储着这个实例的所有注册信息,包括 ip 、端口、属性等。
根据这个存储结构我们可以发现,Eureka Server 第一层都是存储着所有的服务名,以及服务名对应的实例信息,也就是说第一层都是按照服务应用名这个维度来切分存储:
除过 Eureka Server 端存在缓存外,Eureka Client 也同样存在着缓存机制,Eureka Client 启动时会全量拉取服务列表,启动后每隔 30 秒从 Eureka Server 量获取服务列表信息,并保持在本地缓存中。
Eureka Client 增量拉取失败,或者增量拉取之后对比 hashcode 发现不一致,就会执行全量拉取,这样避免了网络某时段分片带来的问题,同样会更新到本地缓存。
同时对于服务调用,如果涉及到 ribbon 负载均衡,那么 ribbon 对于这个实例列表也有自己的缓存,这个缓存定时(默认30秒)从 Eureka Client 的缓存更新。
1、Eureka Server 启动成功,等待服务端注册。在启动过程中如果配置了集群,集群之间定时通过 Replicate 同步注册表,每个 Eureka Server 都存在独立完整的服务注册表信息;
2、Eureka Client 启动时根据配置的 Eureka Server 地址去注册中心注册服务;
3、Eureka Client 会每 30s 向 Eureka Server 发送一次心跳请求,证明客户端服务正常;
4、当 Eureka Server 90s 内没有收到 Eureka Client 的心跳,注册中心则认为该节点失效,会注销该实例;
5、单位时间内 Eureka Server 统计到有大量的 Eureka Client 没有上送心跳,则认为可能为网络异常,进入自我保护机制,不再剔除没有上送心跳的客户端;
6、当 Eureka Client 心跳请求恢复正常之后,Eureka Server 自动退出自我保护模式;
7、Eureka Client 定时全量或者增量从注册中心获取服务注册表,并且将获取到的信息缓存到本地;
8、服务调用时,Eureka Client 会先从本地缓存找寻调取的服务。如果获取不到,先从注册中心刷新注册表,再同步到本地缓存;
9、Eureka Client 获取到目标服务器信息,发起服务调用;
10、Eureka Client 程序关闭时向 Eureka Server 发送取消请求,Eureka Server 将实例从注册表中删除。
#服务端开启自我保护模式,前面有介绍
eureka.server.enable-self-preservation=true
#扫描失效服务的间隔时间(单位毫秒,默认是60*1000)即60秒
eureka.server.eviction-interval-timer-in-ms= 60000
#间隔多长时间,清除过期的 delta 数据
eureka.server.delta-retention-timer-interval-in-ms=0
#该客户端是否可用
eureka.client.enabled=true
#实例是否在eureka服务器上注册自己的信息以供其他服务发现,默认为true
eureka.client.register-with-eureka=false
#此客户端是否获取eureka服务器注册表上的注册信息,默认为true
eureka.client.fetch-registry=false
#是否过滤掉,非UP的实例。默认为true
eureka.client.filter-only-up-instances=true
#与Eureka注册服务中心的通信zone和url地址
eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
#client连接Eureka服务端后的空闲等待时间,默认为30 秒
eureka.client.eureka-connection-idle-timeout-seconds=30
#client连接eureka服务端的连接超时时间,默认为5秒
eureka.client.eureka-server-connect-timeout-seconds=5
#client对服务端的读超时时长
eureka.client.eureka-server-read-timeout-seconds=8
#服务注册中心实例的主机名
eureka.instance.hostname=localhost
#注册在Eureka服务中的应用组名
eureka.instance.app-group-name=
#注册在的Eureka服务中的应用名称
eureka.instance.appname=
#该实例注册到服务中心的唯一ID
eureka.instance.instance-id=
#该实例的IP地址
eureka.instance.ip-address=
#该实例,相较于hostname是否优先使用IP
eureka.instance.prefer-ip-address=false
代码:
https://github.com/wengxingxia/006SpringCloud.git
[慕课手记同步:SpringCloud-01-Eureka] https://www.imooc.com/article/317479
欢迎关注文章同步公众号"黑桃"