网关组件 Zuul 性能一般,未来将退出 Spring Cloud 生态圈,所以直接使用 GateWay,把 GateWay 划分到第一代 Spring Cloud 核心组件中。
各组件整体结构如下:
Gateway:所有服务的入口;日志、黑白名单、鉴权。
Ribbon:负载均衡。
Hystrix:熔断器,服务熔断,服务降级。
Feign:远程调用。
Eureka:服务注册与发现;微服务名称、IP、端口号。
Config:搭建配置中心微服务;实现对配置文件的统一管理,配置自动刷新 - bus。
Actor
---> Gateway 网关
--转发-->
{
[搜索微服务,搜索微服务],
[商品微服务,商品微服务],
[支付微服务,支付微服务],
[秒杀微服务,秒杀微服务],
RestTemplate + Ribbon + Hystrix 或 Feign
}
---->
{
服务注册中心 Eureka,
配置中心 Config
}
从形式上来说,Feign 一个顶三,Feign = RestTemplate + Ribbon + Hystrix
。
Eureka 服务注册中心
常用的服务注册中心:Eureka、Nacos、Zookeeper、Consul。
关于服务注册中心
注意:服务注册中心本质上是为了解耦服务提供者和服务消费者。
服务消费者 -> 服务注册中心 -> 服务提供者。
对于任何一个微服务,原则上都应存在或者支持多个提供者(比如商品微服务部署多个实例),这是由微服务的分布式属性决定的。
更进一步,为了支持弹性扩、缩容特性,一个微服务的提供者的数量和分布往往是动态变化的,也是无法预先确定的。因此,原本在单体应用阶段常用的静态 LoadBalance 机制就不再适用了,需要引入额外的组件来管理微服务提供者的注册与发现,而这个组件就是服务注册中心。
注册中心实现原理
1. 启动:
容器 --> 服务注册中心
容器 --> 服务提供者
容器 --> 服务消费者
2. 注册:
服务消费者 --> 服务注册中心
服务提供者 --> 服务注册中心
3. 获取服务信息:
服务消费者 <--获取服务信息--> 服务注册中心
4. Invoke:
服务消费者 --负载均衡-熔断机制--> 服务提供者
分布式微服务架构中,服务注册中心用于存储服务提供者地址信息、服务发布相关的属性信息,消费者通过主动查询和被动通知的方式获取服务提供者的地址信息,而不再需要通过硬编码方式得到提供者的地址信息。消费者只需要知道当前系统发布了那些服务,而不需要知道服务具体存在于什么位置,这就是透明化路由。
1)服务提供者启动。
2)服务提供者将相关服务信息主动注册到注册中心。
3)服务消费者获取服务注册信息:Pull 模式 - 服务消费者可以主动拉取可用的服务提供者清单;Push 模式 - 服务消费者订阅服务,当服务提供者有变化时,注册中心也会主动推送更新后的服务清单给消费者。
4)服务消费者直接调用服务提供者。
另外,注册中心也需要完成服务提供者的健康监控,当发现服务提供者失效时需要及时剔除。
主流服务中心对比
Zookeeper:
Dubbo + Zookeeper
zookeeper = 存储 + 监听通知
Zookeeper 是一个分布式服务框架,是 Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
Zookeeper 用来做服务注册中心,主要是因为它具有节点变更通知功能,只要客户端监听相关服务节点,服务节点的所有变更,都能及时的通知到监听客户端,这样作为调用方只要使用 Zookeeper 的客户端就能实现服务节点的订阅和变更通知功能了,非常方便。另外,Zookeeper 可用性也可以,因为只要半数以上的选举节点存活,整个集群就是可用的,最少节点数为 3。
Eureka:
Eureka 由 Netflix 开源,并被 Pivatal 集成到 Spring Cloud 体系中,它是基于 RestfulAPI 风格开发的服务注册与发现组件。
Consul:
Consul 是由 HashiCorp 基于 Go 语言开发的支持多数据中心分布式高可用的服务发布和注册服务软件, 采用 Raft 算法保证服务的一致性,且支持健康检查。
Nacos:
Nacos 是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。简单来说 Nacos 就是注册中心 + 配置中心的组合,帮助我们解决微服务开发必会涉及到的服务注册与发现,服务配置,服务管理等问题。Nacos 是 Spring Cloud Alibaba 核心组件之一,负责服务注册与发现,还有配置。
CAP 定理:
CAP 定理又称 CAP 原则,指的是在一个分布式系统中,Consistency 一致性、 Availability 可用性、Partition tolerance 分区容错性,最多只能同时三个特性中的两个,三者不可兼得。
P:分区容错性 - 分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性或可用性的服务(一定的要满足的)。
C:数据一致性 - all nodes see the same data at the same time.
A:高可用 - Reads and writes always succeed.
CAP 不可能同时满足三个,要么是 AP,要么是 CP。
对比:
- Eureka - Java -
AP
-HTTP
- Consul - Go -
CP
-HTTP / DNS
- Zookeeper - Java -
CP
-客户端
- Nacos - Java -
支持 AP / CP 切换
-HTTP
服务注册中心组件 Eureka
服务注册中心的一般原理、对比了主流的服务注册中心方案,目光聚焦 Eureka。
Eureka 基础架构:
Eureka Server 需要手动搭建一个工程,并引入相关依赖,进行对应的配置文件设置。
Renew 心跳 / 心跳检测:服务注册默认 30s 续约,默认 90s 没有收到续约就会将 Client 实例从服务列表中剔除。
ApplicationService 服务提供者
----> Eureka Client
--注册服务--> Eureka Server 注册中心
Eureka Server 注册中心
--服务列表-缓存--> Eureka Client
----> ApplicationClient 客户端消费者
ApplicationClient 客户端消费者 --调用服务--> ApplicationService 服务提供者
Eureka 交互流程及原理:
不同的 Eureka Server 会互相同步复制(Replicate)服务实例列表;
每个 Eureka Server 是一个集群;
Eureka Server 搭建集群来保持高可用服务注册中心;
每个 Eureka Server 可能处于不同地域不同的机房。
Eureka 服务注册中心:[Eureka Server 1, Eureka Server 2, Eureka Server 3]
Appllication Service 服务提供者 - 含有 Eureka Client
Application Client 服务消费者 - 含有 Eureka Client
服务提供者可以进行 Register, Renew, Cancel, Get Registry 于服务注册中心。
服务消费者可以进行 Get Registry 于服务注册中心。
服务消费者 Make Remote Call 于服务提供者。
Eureka 包含两个组件 - Eureka Server 和 Eureka Client:
-
Eureka Client 是一个 Java 客户端,用于简化与 Eureka Server 的交互;Eureka Server 提供服务发现的能力。
-
各个微服务启动时,会通过 Eureka Client 向 Eureka Server 进行注册自己的信息(如网络信息),Eureka Server 会存储该服务的信息。
-
Application Service 作为服务提供者向 Eureka Server 中注册服务,Eureka Server 接受到注册事件会在集群和分区中进行数据同步,Application Client 作为消费端(服务消费者)可以从 Eureka Server 中获取到服务注册信息,进行服务调用。
-
微服务启动后,会周期性地向 Eureka Server 发送心跳以续约自己的信息; Eureka Server 默认心跳续约周期为 30s,默认 90s 后会将还没有续约的 Client 给剔除。
-
如果 Eureka Server 在一定时间内没有接收到某个微服务节点的心跳,将会注销该微服务节点(默认 90 秒)。
-
每个 Eureka Server 同时也是 Eureka Client,多个 Eureka Server 之间通过复制的方式完成服务注册列表的同步。
-
Eureka Client 会缓存 Eureka Server 中的信息。即使所有的 Eureka Server 节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者。
Eureka 通过心跳检测、健康检查和客户端缓存等机制,提高系统的灵活性、可伸缩性和高可用性。
搭建单例 Eureka Server 服务注册中心
实现过程:
- 单实例 Eureka Server -> 访问管理界面。
- 服务提供者(商品微服务注册到集群)。
- 服务消费者(页面静态化微服务注册到 Eureka / 从 Eureka Server 获取服务信息)。
- 完成调用 。
1)搭建 Eureka Server 服务 lagou-cloud-eureka
lagou-parent 中增加 Spring Cloud 版本号依赖管理;Spring Cloud 是一个综合的项目,下面有很多子项目,比如 eureka 子项目;Greemwich 对应的 Spring Boot 是 2.0.x 版本。
<dependencyManagement>
<dependencies>
<!-- SCN -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2)lagou-cloud-eureka 工程 pom.xml 中引入依赖
<dependencies>
<!-- Eureka server 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
注意:在父工程的 pom 文件中手动引入 jaxb 的 jar,因为 Jdk 9 之后默认没有加载该模块,但是 Eureka Server 使用到,所以需要手动导入,否则 Eureka Server 服务无法启动。
父工程 lagou-parent 的 pom 中增加依赖:
<!-- 引入 Jaxb,开始 -->
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.2.11</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.2.11</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.2.10-b140310.1920</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- 引入 Jaxb,结束 -->
3)在 application.yml 文件中配置 Eureka server 服务端口,服务名等信息:
server:
port: 9200
spring:
application:
name: lagou-cloud-eureka
eureka:
# Eureka server 本身也是 eureka 的一个客户端,因为在集群下需要与其他 eureka server 进行数据的同步
client:
# 定义 eureka server url, 如果是集群情况下 defaultZone 设置为集群下的别的 Eureka Server 的地址,多个地址使用逗号隔开
service-url:
defaultZone: http://${
eureka.instance.hostname}:${
server.port}/eureka
# 自己就是服务不需要注册自己
register-with-eureka: false
# 自己就是服务不需要从 Eureka Server 获取服务信息, 默认为 true
fetch-registry: false
instance:
# 当前 eureka 实例的主机名
hostname: localhost
# 使用 ip 注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是 ip)
prefer-ip-address: true
# 自定义实例显示格式,加上版本号,便于多版本管理,注意是 ip-address,早期版本是 ipAddress
instance-id: ${
spring.cloud.client.ip-address}:${
spring.application.name}:${
server.port}:@project.version@
4)编写启动类,声明当前服务为 Eureka 注册中心
package com.renda.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
/**
* @author Renda Zhang
* @since 2020-11-01 16:36
*/
@SpringBootApplication
@EnableEurekaServer // 表示当前项目为 Eureka Server
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
5)访问 http://localhost:9200/
,如果出现 Eureka 注册中心后台页面,则表明 Eureka Server 发布成功。
6)商品微服务和页面静态化微服务注册到 Eureka。
两个微服务的 POM 文件中都添加 Eureka Client 依赖:
<!-- Eureka client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
两个微服务的 application.yml 都配置 Eureka 服务端信息:
eureka:
client:
service-url:
defaultZone: http://localhost:9200/eureka/
instance:
# 使用 ip 注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是 ip)
prefer-ip-address: true
# 自定义实例显示格式,加上版本号,便于多版本管理,注意是 ip-address,早期版本是 ipAddress
instance-id: ${
spring.cloud.client.ip-address}:${
spring.application.name}:${
server.port}:@project.version@
修改两个微服务的启动类,加上注解:
// 将当前项目作为 Eureka Client 注册到 Eureka Server, 只能在 Eureka 环境中使用
@EnableEurekaClient
或者使用可以应用在所有服务注册中心环境的注解:
// 将当前项目表示为注册中心的客户端,向注册中心进行注册,可以在所有的服务注册中心环境下使用
@EnableDiscoveryClient
7)刷新 Eureka 注册中心后台页面,发现新增了两个微服务信息。
搭建 Eureka Server 高可用集群
在互联网应用中,服务实例很少有单个的。
如果 EurekaServer 只有一个实例,该实例挂掉,正好微服务消费者本地缓存列表中的服务实例也不可用,那么这个时候整个系统都受影响。
在生产环境中,会配置 Eureka Server 集群实现高可用。
Eureka Server 集群之中的节点通过点对点(P2P)通信的方式共享服务注册表。
开启两台 Eureka Server 以搭建集群。
修改个人电脑中 host 地址:
127.0.0.1 LagouCloudEurekaServerA
127.0.0.1 LagouCloudEurekaServerB
将 lagou-cloud-eureka
复制一份为 lagou-cloud-eureka2
lagou-cloud-eureka
的 application.yml:
server:
port: 9200
spring:
application:
name: lagou-cloud-eureka
eureka:
# Eureka server 本身也是 eureka 的一个客户端,因为在集群下需要与其他 eureka server 进行数据的同步
client:
# 定义 eureka server url, 如果是集群情况下 defaultZone 设置为集群下的别的 Eureka Server 的地址,多个地址使用逗号隔开
service-url:
defaultZone: http://LagouCloudEurekaServerB:9201/eureka
# 表示是否向 Eureka 中心注册自己的信息,因为自己就是 Eureka Server 所以不进行注册, 默认为 true
register-with-eureka: true
# 是否查询 / 拉取 Eureka Server 服务注册列表,默认为 true
fetch-registry: true
instance:
# 使用 ip 注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是 ip)
prefer-ip-address: true
# 自定义实例显示格式,加上版本号,便于多版本管理,注意是 ip-address,早期版本是 ipAddress
instance-id: ${
spring.cloud.client.ip-address}:${
spring.application.name}:${
server.port}:@project.version@
lagou-cloud-eureka2
的 application.yml:
server:
port: 9201
spring:
application:
name: lagou-cloud-eureka
eureka:
# Eureka server 本身也是 eureka 的一个客户端,因为在集群下需要与其他 eureka server 进行数据的同步
client:
# 定义 eureka server url, 如果是集群情况下 defaultZone 设置为集群下的别的 Eureka Server 的地址,多个地址使用逗号隔开
service-url:
defaultZone: http://LagouCloudEurekaServerA:9200/eureka
# 表示是否向 Eureka 中心注册自己的信息,因为自己就是 Eureka Server 所以不进行注册, 默认为 true
register-with-eureka: true
# 是否查询 / 拉取 Eureka Server 服务注册列表,默认为 true
fetch-registry: true
instance:
# 使用 ip 注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是 ip)
prefer-ip-address: true
# 自定义实例显示格式,加上版本号,便于多版本管理,注意是 ip-address,早期版本是 ipAddress
instance-id: ${
spring.cloud.client.ip-address}:${
spring.application.name}:${
server.port}:@project.version@
商品微服务的 application.xml:
server:
# 微服务的集群环境中,通常会为每一个微服务叠加。
port: 9000
spring:
application:
name: lagou-service-product
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/renda01?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: password
eureka:
client:
service-url:
# 把 eureka 集群中的所有 url 都填写了进来,也可以只写一台,因为各个 eureka server 可以同步注册表
defaultZone: http://LagouCloudEurekaServerA:9200/eureka, http://LagouCloudEurekaServerB:9201/eureka
instance:
# 使用 ip 注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是 ip)
prefer-ip-address: true
# 自定义实例显示格式,加上版本号,便于多版本管理,注意是 ip-address,早期版本是 ipAddress
instance-id: ${
spring.cloud.client.ip-address}:${
spring.application.name}:${
server.port}:@project.version@
页面静态化微服务 application.xml:
server:
# 后期该微服务多实例,端口从 9100 递增(10 个以内)
port: 9100
Spring:
application:
name: lagou-service-page
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/renda01?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: password
eureka:
client:
service-url:
# 把 eureka 集群中的所有 url 都填写了进来,也可以只写一台,因为各个 eureka server 可以同步注册表
defaultZone: http://LagouCloudEurekaServerA:9200/eureka, http://LagouCloudEurekaServerB:9201/eureka
instance:
# 使用 ip 注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是 ip)
prefer-ip-address: true
# 自定义实例显示格式,加上版本号,便于多版本管理,注意是 ip-address,早期版本是 ipAddress
instance-id: ${
spring.cloud.client.ip-address}:${
spring.application.name}:${
server.port}:@project.version@
服务消费者调用服务提供者
改造页面静态化微服务:之前是直接通过 RestTemplate 写死 URL 进行调用,现在通过 Eureka 方式进行调用。
@RestController
@RequestMapping("/page")
public class PageController {
@Autowired
RestTemplate restTemplate;
@Autowired
DiscoveryClient discoveryClient;
@GetMapping("/getProduct/{id}")
public Products getProduct(@PathVariable Integer id) {
// 1.获得 Eureka 中注册的 lagou-service-product 实例集合
List<ServiceInstance> instances = discoveryClient.getInstances("lagou-service-product");
// 2.获得实例集合中的第一个
ServiceInstance instance = instances.get(0);
// 3.根据实例信息拼接 IP 地址
String host = instance.getHost();
int port = instance.getPort();
String url = "https://" + host + ":" + port + "/product/query/" + id;
// 调用并返回
return restTemplate.getForObject(url, Products.class);
}
}
启动注册中心集群和微服务并使用 Postman 进行测试:
GET http://localhost:9100/page/getProduct/1
Eureka 细节详解
Eureka 元数据详解
Eureka 的元数据有两种:标准元数据和自定义元数据。
标准元数据:主机名、IP 地址、端口号等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。
自定义元数据:可以使用 eureka.instance.metadata-map
配置,符合 KEY / VALUE
的存储格式;这些元数据可以在远程客户端中访问。
eureka:
instance:
# 使用 ip 注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是 ip)
prefer-ip-address: true
# 自定义实例显示格式,加上版本号,便于多版本管理,注意是 ip-address,早期版本是 ipAddress
instance-id: ${
spring.cl