Eureka 是 Netflix 公司开发的一款开源的服务注册与发现组件。
Spring Cloud 将 Eureka 与 Netflix 中的其他开源服务组件(例如 Ribbon、Feign 以及 Hystrix 等)一起整合进 Spring Cloud Netflix 模块中,整合后的组件全称为 Spring Cloud Netflix Eureka,主要负责 Spring Cloud 的服务注册与发现功能。 Spring Cloud 使用 Spring Boot 思想为 Eureka 增加了自动化配置,开发人员只需要引入相关依赖和注解,就能将 Spring Boot 构建的微服务轻松地与 Eureka 进行整合。Eureka基本概念
Eureka 到目前为止已经停更了,但是仍然有很多老项目在使用它,所以我们还是要学习下的。目前主流的代替方案就是 Spring Cloud Alibaba 所属的 Nacos,超级强大后面会学习。
Eureka 的两大组件
Eureka 采用 CS(Client/Server,客户端/服务器) 架构,它包括以下两大组件:
- Eureka Server
各个微服务节点通过配置启动后,会在 EurekaServer 中进行注册,这样 EurekaServer 中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在 UI 界面中直观看到。
- Eureka Client
Client 用于在 Java 开发中简化 Eureka Server 的交互,在应用启动后,Client 将会向 Eureka Server发送心跳(默认周期为30秒)。如果 Eureka Server 在多个心跳周期内没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除(默认90秒)
同时 Client 也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器,在下面的代码演示作用可以清晰的看到它的效果。
注:“心跳”指的是一段定时发送的自定义信息,让对方知道自己“存活”,以确保连接的有效性。大部分 CS 架构的应用程序都采用了心跳机制,服务端和客户端都可以发心跳。通常情况下是客户端向服务器端发送心跳包,服务端用于判断客户端是否在线。
Eureka 服务注册与发现
Eureka架构图
- 服务注册中心(Register Service):它是一个 Eureka Server,用于提供服务注册和发现功能。
- 服务提供者(Provider Service):它是一个 Eureka Client,用于提供服务。它将自己提供的服务注册到服务注册中心,以供服务消费者发现。
- 服务消费者(Consumer Service):它是一个 Eureka Client,用于消费服务。它可以从服务注册中心获取服务列表,调用所需的服务。
Eureka使用流程
Eureka 实现服务注册与发现的流程如下:- 搭建一个 Eureka Server 作为服务注册中心;
- 服务提供者 Eureka Client 启动时,会把当前服务器的信息以服务名(spring.application.name)的方式注册到服务注册中心;
- 服务消费者 Eureka Client 启动时,也会向服务注册中心注册;
- 服务消费者还会获取一份可用服务列表,该列表中包含了所有注册到服务注册中心的服务信息(包括服务提供者和自身的信息);
- 在获得了可用服务列表后,服务消费者通过 HTTP 或消息中间件远程调用服务提供者提供的服务。
Eureka单机模式
先来记录下单机情况下Eureka的使用步骤
创建 EurekaServer
创建出一个注册中心,用来管理等会的生产者和消费者等服务
- 创建名字为
spring-cloud-eureka-server-9001
的 Maven模块 - 修改pom.xml,eureka-server 不写版本号就是新版的,目前是 3.1.4,最新的版本 查询地址:Just a moment…
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>spring-cloud-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.chen</groupId>
<artifactId>spring-cloud-eureka-server-9001</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud-eureka-server-9001</name>
<description>spring-cloud-eureka-server-9001</description>
<properties>
<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-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- eureka-server 不写版本号就是新版的 暂时是 3.1.4 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
- 修改配置文件
server:
port: 9001
eureka:
instance:
#eureka服务端的实例名称
hostname: 127.0.0.1
client:
# false 表示不向注册中心注册自己,注册自己肯定没什么意义
register-with-eureka: false
# false 表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
fetch-registry: false
service-url:
# 设置与Eureka server交互的地址查询服务和注册服务都需要依赖这个地址。
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
- 修改主启动类
通过 @EnableEurekaServer 注解,标识这是一个 EurekaServer 即 Eurek 注册中心
@SpringBootApplication
@EnableEurekaServer
public class SpringCloudEurekaServer9001Application {
public static void main(String[] args) {
SpringApplication.run(SpringCloudEurekaServer9001Application.class, args);
}
}
搭建服务提供者
创建Module spring-cloud-eureka-provider-book-9002
。服务的提供者,它负责具体的增删改查等业务,注册进入刚配置好的 EurekaServer 中去
- 修改
pom.xml
<!--引入 Eureka Client 的依赖,将服务注册到 Eureka Server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 创建修改配置文件
server:
port: 9002 #服务端口号
spring:
application:
name: spring-cloud-eureka-provider-9002 #微服务名称,对外暴漏的微服务名称,十分重要
datasource:
username: root #数据库登陆用户名
password: cj123456789 #数据库登陆密码
url: jdbc:mysql://127.0.0.1:3306/spring-cloud?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC #数据库url
driver-class-name: com.mysql.cj.jdbc.Driver #数据库驱动,如果是8.0以下使用 com.mysql.jdbc.Driver
mybatis:
# 指定 mapper.xml 的位置
mapper-locations: classpath:mybatis/mapper/*.xml
#扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名
type-aliases-package: com.chen.springcloudeurekaproviderbook9002.entity
configuration:
#默认开启驼峰命名法,可以不用设置该属性
map-underscore-to-camel-case: true
eureka:
client: #将客户端注册到 eureka 服务列表内
service-url:
defaultZone: http://127.0.0.1:9001/eureka #这个地址是9001注册中心在 application.yml 中暴露出来额注册地址 (单机版)
instance:
instance-id: spring-cloud-eureka-provider-book-9002 #自定义服务名称信息
prefer-ip-address: true #显示访问路径的 ip 地址
- 建表
CREATE TABLE `book` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '图书名称',
`author` varchar(10) DEFAULT NULL COMMENT '作者',
`price` decimal(10,0) DEFAULT NULL COMMENT '价格',
`publish_date` datetime DEFAULT NULL COMMENT '上架日期',
`count` int DEFAULT NULL COMMENT '库存数量',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3;
- 创建entity、mapper、service、controller
package com.chen.springcloudeurekaproviderbook9002.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Book {
private String id;
private String name;
private String author;
private String price;
private String publishDate;
}
package com.chen.springcloudeurekaproviderbook9002.mapper;
import com.chen.springcloudeurekaproviderbook9002.entity.Book;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface BookMapper {
//根据主键获取数据
Book selectByPrimaryKey(Integer deptNo);
//获取表中的全部数据
List<Book> getAll();
}
在resources下创建文件夹\mybatis\mapper\
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.chen.springcloudeurekaproviderbook9002.mapper.BookMapper">
<resultMap id="BaseResultMap" type="com.chen.springcloudeurekaproviderbook9002.entity.Book">
<id column="id" jdbcType="INTEGER" property="id"/>
<result column="name" jdbcType="VARCHAR" property="name"/>
<result column="auther" jdbcType="VARCHAR" property="auther"/>
<result column="price" jdbcType="VARCHAR" property="price"/>
<result column="publish_date" jdbcType="VARCHAR" property="publishDate"/>
<result column="publish_date" jdbcType="VARCHAR" property="publishDate"/>
<result column="count" jdbcType="VARCHAR" property="count"/>
</resultMap>
<sql id="Base_Column_List">
id,name,auther,price,publish_date,count
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from book
where id = #{id}
</select>
<select id="getAll" resultType="com.chen.springcloudeurekaproviderbook9002.entity.Book">
select * from book;
</select>
</mapper>
package com.chen.springcloudeurekaproviderbook9002.service;
import com.chen.springcloudeurekaproviderbook9002.entity.Book;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public interface BookService {
Book get(Integer id);
List<Book> selectAll();
}
package com.chen.springcloudeurekaproviderbook9002.service.impl;
import com.chen.springcloudeurekaproviderbook9002.entity.Book;
import com.chen.springcloudeurekaproviderbook9002.mapper.BookMapper;
import com.chen.springcloudeurekaproviderbook9002.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service("bookService")
public class BookServiceImpl implements BookService {
@Autowired
private BookMapper bookMapper;
@Override
public Book get(Integer id) {
return bookMapper.selectByPrimaryKey(1);
}
@Override
public List<Book> selectAll() {
return bookMapper.getAll();
}
}
package com.chen.springcloudeurekaproviderbook9002.controller;
import com.chen.springcloudeurekaproviderbook9002.entity.Book;
import com.chen.springcloudeurekaproviderbook9002.service.BookService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@Slf4j
public class BookController {
@Autowired
private BookService bookService;
@GetMapping(value = "/book/get/{id}")
public Book get(@PathVariable("id") int id) {
return bookService.get(id);
}
@GetMapping(value = "/book/list")
public List<Book> list() {
return bookService.selectAll();
}
}
- 修改主启动类
通过 @EnableEurekaClient 注解,标识这是一个 Eureka客户端
@SpringBootApplication
@EnableEurekaClient
public class SpringCloudEurekaProviderBook9002Application {
public static void main(String[] args) {
SpringApplication.run(SpringCloudEurekaProviderBook9002Application.class, args);
}
}
- 依次启动Eureka服务,启动客户端。访问EurekaServer http://127.0.0.1:9001/ 看到如下页面代表部署OK的
搭建服务消费者
创建订单服务消费者 spring-cloud-eureka-consumer-order-9003
。
- 修改
pom.xml
添加Eureka Client依赖
<!--引入 Eureka Client 的依赖,将服务注册到 Eureka Server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 修改配置文件
server:
port: 9003
spring:
application:
name: spring-cloud-eureka-consumer-order-9003
datasource:
username: root #数据库登陆用户名
password: cj123456789 #数据库登陆密码
url: jdbc:mysql://127.0.0.1:3306/spring-cloud?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC #数据库url
driver-class-name: com.mysql.cj.jdbc.Driver #数据库驱动,如果是8.0以下使用 com.mysql.jdbc.Driver
eureka:
client:
#表示是否将自己注册进 EurekaServer默认为true。
register-with-eureka: true
#是否从 EurekaServer 抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
defaultZone: http://127.0.0.1:9001/eureka
- 修改主启动类
通过 @EnableEurekaClientr 注解,标识这是一个 Eureka 的客户端
@SpringBootApplication
@EnableEurekaClient
public class SpringCloudEurekaConsumerOrder9003Application {
public static void main(String[] args) {
SpringApplication.run(SpringCloudEurekaConsumerOrder9003Application.class, args);
}
}
检验是否搭建完毕
在 Eureka Server 中查看是否 9002 和 9003 服务都注册了
依次启动
spring-cloud-eureka-server-9001
spring-cloud-eureka-provider-book-9002
spring-cloud-eureka-consumer-order-9003
浏览器输入 http://127.0.0.1:9001/ , 在主页的Instances currently registered with Eureka
将会看到 9002、9003已经注册OK成功。
Eureka 自我保护
Client 用于在 Java 开发中简化 Eureka Server 的交互,在应用启动后,Client 将会向 Eureka Server 发送心跳(默认周期为30秒)。
如果 Eureka Server 在多个心跳周期内没有接收到某个节点的心跳,EurekaServer 将会从服务注册表中把这个服务节点移除(默认90秒)
但是有时候,是因为网络分区发生了故障,比如:延时、卡顿、拥挤等,微服务与 EurekaServer 之间无法正常通信,这个服务节点就会被超时后移除,但是此时微服务本身其实是健康的,此时本不应该注销这个微服务。
Eureka 通过“自我保护模式”来解决这个问题——当 EurekaServer 节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。
一旦进入保护模式,Eureka Server 将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。
这也就是我们前面看到的红字,也就代表进入了保护模式
保护模式的设计哲学就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。
如何禁止自我保护
例如在 Eureka Server 7001 中关闭自我保护机制(默认是开启的)
- 配置 eureka.server.enable-self-preservation = false 可以禁用自我保护模式
eureka:
# ...
server:
# 关闭自我保护机制,保证不可用服务被及时踢除
enable-self-preservation: false
eviction-interval-timer-in-ms: 2000
- 默认是 30 s 发一次心跳,超过 90s 为超时,但是为了测试可以把服务这些时间改短一点
比如临时改成 1s 和 2s
eureka:
# ...
instance:
instance-id: payment8001
prefer-ip-address: true
# 心跳检测与续约时间
# 开发时没置小些,保证服务关闭后注册中心能即使剔除服务
# Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)
lease-renewal-interval-in-seconds: 1
# Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务
lease-expiration-duration-in-seconds: 2
- 效果如下:
代表这个 Eureka Server 的自我保护关闭
THE SELF PRESERVATION MODE IS TURNED OFF. THIS MAY NOT PROTECT INSTANCE EXPIRY IN CASE OF NETWORK/OTHER PROBLEMS.
- 测试:在 IDEA 中先关闭 9002,9001 的监控中马上 9002 删除了
Eureka集群模式
集群原理
在微服务架构中,一个系统往往由几十个服务组成,如果将这些服务全部注册到同一个 Eureka Server 中,就极有可能导致 Eureka Server 因不堪重负而崩溃,最终导致整个系统崩溃。
解决办法就是部署多台 Eureka Server,也就是我们常说的集群。
在 Eureka 实现服务注册与发现时一共涉及了 3 个角色:服务注册中心、服务提供者以及服务消费者,这三个角色分工明确,各司其职。
其实在 Eureka 中,所有服务都既是服务消费者也是服务提供者,服务注册中心 Eureka Server 也不例外。
我们在搭建服务注册中心时,在 application.yml 中涉及了这样的配置:
eureka:
client:
# false 表示不向注册中心注册自己,注册自己肯定没什么意义
register-with-eureka: false
# false 表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
fetch-registry: false
这样设置的原因是 spring-cloud-eureka-server-9001
本身自己就是服务注册中心,服务注册中心是不能将自己注册到自己身上的,但服务注册中心是可以将自己作为服务向其他的服务注册中心注册自己的。
举个例子,有两个 Eureka Server 分别为 A 和 B,虽然 A 不能将自己注册到 A 上,B 也不能将自己注册到 B 上,但 A 是可以作为一个服务把自己注册到 B 上的,同理 B 也可以将自己注册到 A 上。
这样就可以形成一组互相注册的 Eureka Server 集群,当服务提供者发送注册请求到 Eureka Server 时,Eureka Server 会将请求转发给集群中所有与之相连的 Eureka Server 上,以实现 Eureka Server 之间的服务同步。
通过服务同步,服务消费者可以在集群中的任意一台 Eureka Server 上获取服务提供者提供的服务。这样,即使集群中的某个服务注册中心发生故障,服务消费者仍然可以从集群中的其他 Eureka Server 中获取服务信息并调用,而不会导致系统的整体瘫痪,这就是 Eureka Server 集群的高可用性。
搭建集群环境
- 参照上方 EurekaServer搭建步骤,创建
spring-cloud-eureka-server-9000
,spring-cloud-eureka-server-8999
, - 修改本地hsots文件
C:\Windows\System32\drivers\etc
文件,将<font style="color:rgb(18, 18, 18);">localhost</font>
映射成别的自定义域名
#测试Eureka集群
127.0.0.1 eureka8999.com
127.0.0.1 eureka9000.com
127.0.0.1 eureka9001.com
这样访问域名+端口号就能访问到本地的集群环境。
- 唯一不同的是
application.yaml
内容如下:
server:
port: 8999
eureka:
instance:
#eureka服务端的实例名称
hostname: 127.0.0.1
client:
# false 表示不向注册中心注册自己,注册自己肯定没什么意义
register-with-eureka: false
# false 表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
fetch-registry: false
service-url:
# 设置与Eureka server交互的地址查询服务和注册服务都需要依赖这个地址。
# 集群版 将当前的 Eureka Server 注册到 9000 和 9001 上,形成一组互相注册的 Eureka Server 集群
defaultZone: http://eureka9000.com:9000/eureka/,http://eureka9001.com:9001/eureka/
server:
port: 9000
eureka:
instance:
#eureka服务端的实例名称
hostname: 127.0.0.1
client:
# false 表示不向注册中心注册自己,注册自己肯定没什么意义
register-with-eureka: false
# false 表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
fetch-registry: false
service-url:
# 设置与Eureka server交互的地址查询服务和注册服务都需要依赖这个地址。
# 集群版 将当前的 Eureka Server 注册到 8999 和 9001 上,形成一组互相注册的 Eureka Server 集群
defaultZone: http://eureka8999.com:8999/eureka/,http://eureka9001.com:9001/eureka/
server:
port: 9001
eureka:
instance:
#eureka服务端的实例名称
hostname: 127.0.0.1
client:
# false 表示不向注册中心注册自己,注册自己肯定没什么意义
register-with-eureka: false
# false 表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
fetch-registry: false
service-url:
# 设置与Eureka server交互的地址查询服务和注册服务都需要依赖这个地址。
# 集群版 将当前的 Eureka Server 注册到 9000 和 8999 上,形成一组互相注册的 Eureka Server 集群
defaultZone: http://eureka9000.com:9000/eureka/,http://eureka8999.com:8999/eureka/
- 修改服务提供者配置
eureka:
client: #将客户端注册到 eureka 服务列表内
service-url:
#defaultZone: http://127.0.0.1:9001/eureka #这个地址是9001注册中心在 application.yml 中暴露出来额注册地址 (单机版)
defaultZone: http://eureka8999.com:8999/eureka/,http://eureka9000.com:9000/eureka/,http://eureka9001.com:9001/eureka/ #将服务注册到 Eureka Server 集群
instance:
instance-id: spring-cloud-eureka-provider-book-9002 #自定义服务名称信息
prefer-ip-address: true #显示访问路径的 ip 地址
- 启动集群环境
(1)启动 spring-cloud-eureka-server-8999,访问 http://eureka8999.com:8999/
(2)启动 spring-cloud-eureka-server-9000,访问 http://eureka9000.com:9000/
(3)启动 spring-cloud-eureka-server-9001,访问 http://eureka9001.com:9001/
可以看到上方 DS Replicas,展示了另外的集群环境,此时说明集群配置OK。