"Eureka"来源于古希腊词汇,意为“发现了”。在软件领域,Eureka是Netflix在线影片公司开源的一个服务注册和发现组件,和其他的Netflix公司的服务组件(例如负载均衡,熔断器,网关等)一起,被Spring Cloud社区整合为Spring Cloud Netflix模块。
2.1 Eureka简介
和Zookeeper类似,Eureka是一个用于服务注册和发现的组件,最开始主要应用与亚马逊公司的云计算服务平台AWS,Eureka分为Eureka Server和Eureka Client,Eureka Server为Eureka服务注册中心,Eureka Client为Eureka客户端。
举个例子:Eureka好比滴滴网约车平台,没有滴滴时,人们出门叫车只能叫出租车。一些私家车想做出租却没有资格,被称为黑车。而很多人想要约车,但是无奈出租车太少,不方便。私家车很多却不敢拦,而且满大街的车,谁知道哪个才是愿意载人的。一个想要,一个愿意给,就是缺少引子,缺乏管理啊。
此时滴滴这样的网约车平台出现了,所有想载客的私家车全部到滴滴注册,记录你的车型(服务类型),身份信息(联系方式)。这样提供服务的私家车,在滴滴那里都能找到,一目了然。
此时要叫车的人,只需要打开APP,输入你的目的地,选择车型(服务类型),滴滴自动安排一个符合需求的车到你面前,为你服务,完美!
Eureka相当于微服务架构中的“滴滴”。负责微服务的注册和发现工作,它记录了服务和服务地址的映射关系。在分布式架构中,服务会注册到Eureka注册中心,当服务需要调用其它服务时,就从Eureka找到服务的地址,进行调用。Eureka在Spring Cloud中的作用是用来作为服务治理实现服务注册和发现。Eureka主要涉及到三大角色:服务提供者、服务消费者、注册中心。
服务注册是指,各个微服务在启动时,将自己的网络地址等信息注册到Eureka,服务提供者将自己的服务信息,如服务名、IP等告知服务注册中心。
服务发现是指当一个服务消费者需要调用另外一个服务时,服务消费者从Eureka查询服务提供者的地址,并通过该地址调用服务提供者的接口。一个服务既可以是服务消费者,也可以是服务发现者。
各个微服务与注册中心使用一定机制(例如心跳)通信。如果Eureka与某微服务长时间无法通信,Eureka会将该服务实例从服务注册中心中剔除,如果剔除掉这个服务实例过了一段时间,此服务恢复心跳,那么服务注册中心将该实例重新纳入到服务列表中,Eureka架构图,如图2-1所示。
图2-1 Eureka原理图
\
注意:Eureka2.x已经停更,解决方案推荐使用Nacos作为替换方案,Nacos在Spring Cloud Alibaba中讲解。
2.2 Eureka入门
\
本节介绍Eureka的基本使用,创建Eureka Server,让后将上面支付微服务,和订单微服务注册到Eureka Server中。Eureka基本机构主要包括以下3个角色。
- Eureka Server:服务注册中心,提供服务注册和发现功能。
- Provider Service:服务提供者,案例中就是支付微服务。
- Consumer Service:服务消费者,案例中就是订单微服务。
2.2.1 EurekaServer
\
- 选择依赖
选择下面依赖,如图2-2所示。
- Spring Boot 2.4.8
- Spring Boot DevTools
- Lombok
- Eureka Server
图2-2 依赖选择
\
pom.xml配置文件代码如下。
<?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>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.8</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.lxs.demo</groupId>
<artifactId>04_cloud_eureka</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>04_cloud_eureka</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2020.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</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>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<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>
- 启动器
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
注:@EnableEurekaServer,声明当前应用为Eureka Server
- 配置文件
server:
port: 9004
spring:
application:
name: eureka-server
eureka:
client:
service-url:
# eureka 服务地址,如果是集群的话;需要指定其它集群eureka地址
defaultZone: http://127.0.0.1:9004/eureka
# 不注册自己
register-with-eureka: false
# 不拉取服务
fetch-registry: false
- 启动并测试
启动应用访问http://localhost:9004/,效果如图2-3所示。
图2-3 Eureka Server运行效果
\
2.2.2 服务提供者
\
改造支付服务作为服务提供者,提供支付服务,注册到Eureka Server。
- 添加依赖
在支付微服务工程pom.xml中添加如下依赖。
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2020.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- 配置文件
application.yml配置如下。
server:
port: 9001
spring:
application:
name: cloud-payment-service
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:9004/eureka
fetch-registry: true
register-with-eureka: true
注意:
- 这里我们添加了spring.application.name属性来指定应用名称,将来会作为服务的id使用。
- 不用指定register-with-eureka和fetch-registry,因为默认是true
- 启动器
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentApplication {
public static void main(String[] args) {
SpringApplication.run(PaymentApplication.class, args);
}
}
- 启动并测试
启动应用,效果如图2-4所示
图2-4 注册服务到Eureka Server
2.2.3 服务消费者
\
改造订单微服务,订单服务调用支付服务,订单微服务作为服务消费者,当然订单服务既可以作为消费者,调用支付服务,也可以作为被其他服务调用,作为服务提供者,所以订单服务也可以注册到Eureka Server
- 添加依赖
在支付微服务工程pom.xml中添加如下依赖。
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2020.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- 配置文件
application.yml配置如下。
server:
port: 9002
spring:
application:
name: cloud-order-service
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:9004/eureka
注意:
- 这里我们添加了spring.application.name属性来指定应用名称,将来会作为服务的id使用。
- 不用指定register-with-eureka和fetch-registry,因为默认是true
- 启动器
@SpringBootApplication
@EnableDiscoveryClient
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
- OrderController
修改OrderController,代码如下。
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/payment/{id}")
public ResponseEntity<Payment> getPaymentById(@PathVariable("id") Integer id) {
String url = "http://localhost:9001/payment/" + id;
List<ServiceInstance> serviceInstances = discoveryClient.getInstances("cloud-payment-service");
ServiceInstance serviceInstance = serviceInstances.get(0);
url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/payment/" + id;
Payment payment = restTemplate.getForObject(url, Payment.class);
return ResponseEntity.ok(payment);
}
}
- 启动并测试
启动应用,测试效果如图2-5所示。
图2-5 调用订单服务
\
2.3 源码解析
2.3.1 Eureka的一些概念
\
- Register --- 服务注册
当Eureka Client向Eureka Server注册时,Eureka Client提供自身的元数据,比如IP地址、端口、运行状况指标的URL,主页地址等信息。
- Renew --- 服务续约
Eureka Client在默认情况下会每隔30秒发送一次心跳来进行服务续约,通过服务续约来告知Eureka Server该Eureka Client依然可用,正常情况下,如果Eureka Server在90秒内没有收到Eureka Client的心跳,Eureka Server会将Eureka Client实例从注册列表中删除,注意:官网建议不要更爱服务续约的间隔时间。
- Fetch Registries --- 获取服务注册列表信息
\
Eureka Client从Eureka Server获取服务注册表信息,并将其缓存到本地。Eureka Client 会使用服务注册列表信息查找其他服务的信息,从而进行远程调用,改注册列表信息定时(每隔30秒)更新一次,每次返回的注册列表信息可能与Eureka Client的缓存信息不同,Erueka Client会重新获取整个注册表信息。Eureka Server缓存了所有的服务注册表信息,并且进行了压缩。Eureka Client和Eureka Server可以使用json和xml的数据格式进行通信,默认,Eureka Client使用JSON格式方式来获取服务器注册列表信息。
- Cancel --- 服务下线
Eureka Client在程序关闭时可以向Eureka Server发送下线请求,发送请求后,该客户端的实例信息将从Eureka Server的服务注册列表信息中删除。改下线请求不会自动完成,需要在程序关闭时调用以下代码
DiscoveryManager.getInstance().shutdownComponent();
\
- Eviction --- 服务
在默认情况下,Eureka Client连续90秒没有想Eureka Server发送服务续约(心跳)时,Eureka Server会将该服务实例从服务列表中删除。即服务剔除。
2.3.2 Register 服务注册
\
- Eureka Client源码
\
Eureka Client向Eureka Server提交自己的服务信息,包括IP、端口、ServiceId等信息。如果Eureka Client没有配置ServiceId,则默认为配置文件中的配置的服务名,即${spring.application.name}的值。
(1)DiscoveryClient初始化方法initScheduledTasks方法
该方法主要开启了获取服务注册列表的信息,如果需要向Eureka Server注册,则开启注册,同时开启了定时任务向Eureka Server服务续约,代码如下。
/**
* Initializes all scheduled tasks.
*/
private void initScheduledTasks() {
if (clientConfig.shouldFetchRegistry()) {
...//省略了任务调度获取注册列表的代码。
}
if (clientConfig.shouldRegisterWithEureka()) {
...
// Heartbeat timer
heartbeatTask = new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs,
TimeUnit.SECONDS,
expBackOffBound,
new HeartbeatThread()
);
scheduler.schedule(
heartbeatTask,
renewalIntervalInSecs, TimeUnit.SECONDS);
// InstanceInfo replicator
instanceInfoReplicator = new InstanceInfoReplicator(
this,
instanceInfo,
clientConfig.getInstanceInfoReplicationIntervalSeconds(),
2); // burstSize
...
}
}
\
(2) instanceInfoReplicator类
\
initScheduledTasks方法中,定时任务调用instanceInfoReplicator类,instanceInfoReplicator类继承Runable接口,run方法代码如下。
public void run() {
try {
discoveryClient.refreshInstanceInfo();
Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
if (dirtyTimestamp != null) {
discoveryClient.register();
instanceInfo.unsetIsDirty(dirtyTimestamp);
}
} catch (Throwable t) {
logger.warn("There was a problem with the instance info replicator", t);
} finally {
Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
(3)DiscoveryClient的register方法
在com.netflix.discovery包下的DiscoveryClient类中有一个register()方法,该方法通过Http请求向Eureka Server注册,代码如下所示。
boolean register() throws Throwable {
logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
EurekaHttpResponse<Void> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}
\
- Eureka Server端源码
(1)ApplicationResource类
ApplicationResource类的addInstance方法,接收Eureka Client客户端注册请求,完成注册,代码如下。
/**
* Registers information about a particular instance for an
* {@link com.netflix.discovery.shared.Application}.
*
* @param info
* {@link InstanceInfo} information of the instance.
* @param isReplication
* a header parameter containing information whether this is
* replicated from other nodes.
*/
@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info,
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
...
registry.register(info, "true".equals(isReplication));
return Response.status(204).build(); // 204 to be backwards compatible
}
(2) PeerAwareInstanceRegistryImpl类
上面addInstance方法调用PeerAwareInstanceRegistryImpl类的register方法进行注册,代码如下。
@Override
public void register(final InstanceInfo info, final boolean isReplication) {
int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
leaseDuration = info.getLeaseInfo().getDurationInSecs();
}
super.register(info, leaseDuration, isReplication);
//高可用,多节点同步数据
replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}
\
2.3.3 Renew 服务续约
\
服务续约和服务注册非常类似,通过前文中的分析可以知道,服务注册在Eureka Client程序启动后开启,并且同时开启服务续约定时任务。
- Eureka Client端
在DiscoveryClient类下又renew()方法,完成续约,代码如下。
/**
* Renew with the eureka service by making the appropriate REST call
*/
boolean renew() {
EurekaHttpResponse<InstanceInfo> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
logger.debug(PREFIX + "{} - Heartbeat status: {}", appPathIdentifier, httpResponse.getStatusCode());
if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) {
REREGISTER_COUNTER.increment();
logger.info(PREFIX + "{} - Re-registering apps/{}", appPathIdentifier, instanceInfo.getAppName());
long timestamp = instanceInfo.setIsDirtyWithTime();
boolean success = register();
if (success) {
instanceInfo.unsetIsDirty(timestamp);
}
return success;
}
return httpResponse.getStatusCode() == Status.OK.getStatusCode();
} catch (Throwable e) {
logger.error(PREFIX + "{} - was unable to send heartbeat!", appPathIdentifier, e);
return false;
}
}
- Eureka Server端
在com.netflix.eureka.InstanceResource类下,接口方法renewLease(),它是一个RESTful API接口。完成服务器断续,代码如下。
@PUT
public Response renewLease(
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
@QueryParam("overriddenstatus") String overriddenStatus,
@QueryParam("status") String status,
@QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
...
boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);
...
}
此外,服务续约的两个参数是可以配置的,即Eureka Client发送续约心跳间隔时间参数,和Eureka Server多长时间内没有收到心跳将实例剔除的时间参数,默认情况下这两个参数分别是30秒和90秒
eureka:
instance:
# 心跳间隔时间
lease-renewal-interval-in-seconds: 30
# 没收到心跳多长时间剔除
lease-expiration-duration-in-seconds: 90
2.4 Eureka的自我保护
\
当有一个新的Eureka Server出现时,他尝试从相邻的Peer几点获取所有服务实例注册信息。如果从相邻的Peer节点获取信息时出现了故障,Eureka Server会尝试其他的Peer节点。如果Eureka Server能够成功获取所有的服务实例信息。则根据配置信息设置服务续约的阈值。在任何时间,如果Eureka Server接收到的服务续约低于为该值配置的百分比(默认为15分钟内低于85%),则服务器开启自我保护模式,即不再剔除注册列表的信息。
这样做的好处在于,如果Eureka Server资深的网络问题而导致Eureka Client无法续约,Eureka Client的注册列表信息不再被删除,也就是Eureka Client还可以被其他服务消费。自我保护开启后,效果如图2-6所示。
图2-6 Eureka 自我保护开启效果
在默认情况下,Eureka Server的自我保护模式是开启的,生产环境下这很有效,保证了大多数服务依然可用,但是这给我们的开发带来了麻烦, 因此开发阶段我们都会关闭自我保护模式。代码如下
eureka:
server:
enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间(缺省为60*1000ms)
2.5 Eureka Server集群
\
Eureka Server不但需要接收服务的心跳,用来检测服务是否可用,而且每个服务会定期会去Eureka申请服务列表的信息,当服务实例很多时,Eureka中的负载就会很大,所以必须实现Eureka服务注册中心的高可用,一般的做法是将Eureka Server集群化。
- 配置文件
\
更改Eureka Server的配置文件 application.yml,在改配置文件中,采用多profile格式,代码如下。
---
spring:
config:
activate:
on-profile: peer1
server:
port: 9003
eureka:
instance:
hostname: peer1
client:
service-url:
defaultZone: http://peer2:9004/eureka
---
spring:
config:
activate:
on-profile: peer2
server:
port: 9004
eureka:
instance:
hostname: peer2
client:
service-url:
defaultZone: http://peer1:9003/eureka
注:
- 在yaml单一配置文件中,可用连续三个连字号(---)区分多个文件。
- Spring Boot2.4.x使用spring.config.activate.on-profile代替原来的spring.profiles
- 域名解析
因为本地搭建Eureka Server集群,所以需要修改本地的host文件,c:\Windows\System32\drivers\etc\hosts,代码如下。
127.0.0.1 peer1
127.0.0.1 peer2
- 启动并测试
通过mvn package编译后,使用java -jar方式启动,并通过--spring.profiles.active指定配置文件,本案例中需要启动2个Eureka Server实例,他们的配置文件分别是peer1和peer2,命令如下
java -jar 04_cloud_eureka-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer1
java -jar 04_cloud_eureka-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer2
启动支付服务,支付微服务仅向9004的Eureka Server注册,代码如下。
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:9004/eureka
此时,支付服务并没有向9003注册,访问Eureka Server的节点的peer1管控台界面,可见9004的注册列表信息已经,同步到了9003节点,效果如图2-7所示
图2-7 高可用Eureka Server效果