Eureka
是 Spring Netflix OSS
中服务发现与注册的核心组件,在 Spring Cloud 2022.0.x
版本开始,Spring Netflix OSS
组件(例如 Hystrix、Zuul
)被正式移除。Spring 团队逐渐停止了对这些组件的支持,转而支持基于 Spring Cloud 的其他解决方案,比如 Spring Cloud Gateway、Resilience4j
等。但 Eureka
仍然支持,说明在设计上 Eureka
作为服务注册与发现仍占有一席之地。Eureka Server:作为注册中心,维护所有服务的注册信息;
Eureka Client:服务提供者或消费者,通过 Eureka Server 注册或获取服务信息。
pom.xml
文件<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>eureka-server</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
</parent>
<properties>
<spring-cloud.version>2021.0.1</spring-cloud.version>
</properties>
<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>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
instance:
hostname: localhost # Eureka Server 的主机名,其他服务会通过这个地址注册
instance-id: ${spring.application.name}:${server.port} # 实例 ID,唯一标识一个服务实例
client:
register-with-eureka: true # 是否将 Eureka Server 本身作为客户端注册到注册中心
fetch-registry: false # Eureka Server 不拉取服务注册表(只提供服务注册功能)
service-url:
defaultZone: http://localhost:8761/eureka/ # Eureka Server 地址,客户端注册所用
server:
enable-self-preservation: false # 启用自我保护模式,防止服务实例因心跳丢失被剔除
eviction-interval-timer-in-ms: 60000 # 清理失效服务的时间间隔,单位为毫秒
package org.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
pom.xml
文件<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>service-demo1</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
</parent>
<properties>
<spring-cloud.version>2021.0.1</spring-cloud.version>
</properties>
<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>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 8081
spring:
application:
name: service-demo1
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/ # Eureka Server 地址,客户端注册所用
fetch-registry: true # 是否从 Eureka Server 拉取服务注册表,服务提供者通常设置为 true
register-with-eureka: true # 是否将自己注册到 Eureka Server,服务提供者需要设置为 true
registry-fetch-interval-seconds: 30 # 注册表拉取间隔
instance:
hostname: localhost
instance-id: ${spring.application.name}:${server.port} # 实例 ID,唯一标识一个服务实例
prefer-ip-address: true # 使用 IP 地址注册服务,通常设置为 true
lease-renewal-interval-in-seconds: 30 # 心跳间隔
lease-expiration-duration-in-seconds: 90 # 过期时间
package org.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
报错 com.netflix.discovery.AbstractDiscoveryClientOptionalArgs that could not be found. 解决方法:需要引入 spring-boot-starter-web
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Eureka
注册中心添加认证添加 Spring Security 模块
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
application.yml
spring:
application:
name: eureka-security-server
security: # 配置 Spring Security 登录用户名和密码
user:
name: admin
password: 123456
添加 Java 配置 WebSecurityConfig
默认情况下添加SpringSecurity依赖的应用每个请求都需要添加CSRF token才能访问。
Eureka客户端注册时并不会添加,所以需要配置/eureka/**路径不需要CSRF token。
package org.example.config;
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().ignoringAntMatchers("/eureka/**");
super.configure(http);
}
}
搭建 Eureka 集群实现高可用
搭建 Eureka 集群实现高可用
,集群一方面可以实现高可用,另一方面也可以分担服务注册和发现的压力。application.yml
实现相互注册# 配置 host 文件
127.0.0.1 localhost1
127.0.0.1 localhost2
# eureka-server1
server:
port: 8761
eureka:
instance:
hostname: localhost1 # Eureka Server 的主机名,其他服务会通过这个地址注册
prefer-ip-address: false # 使用 IP 地址注册服务,通常设置为 true
instance-id: ${spring.application.name}:${server.port} # 实例 ID,唯一标识一个服务实例
client:
register-with-eureka: true # 将 Eureka Server 本身作为客户端注册到注册中心
fetch-registry: false # Eureka Server 不拉取服务注册表(只提供服务注册功能)
service-url:
defaultZone: http://admin:123456@localhost2:8762/eureka/
# eureka-server2
server:
port: 8762
eureka:
instance:
hostname: localhost2 # Eureka Server 的主机名,其他服务会通过这个地址注册
prefer-ip-address: false # 使用 IP 地址注册服务,通常设置为 true
instance-id: ${spring.application.name}:${server.port} # 实例 ID,唯一标识一个服务实例
client:
register-with-eureka: true # 将 Eureka Server 本身作为客户端注册到注册中心
fetch-registry: false # Eureka Server 不拉取服务注册表(只提供服务注册功能)
service-url:
defaultZone: http://admin:123456@localhost1:8761/eureka/
# 上面两个注册中心实现相互注册,并修改 eureka-client 配置
eureka:
client:
service-url:
defaultZone: http://admin:123456@localhost:8761/eureka/,http://admin:123456@localhost:8762/eureka/
不同host
eureka
底层使用 isThisMyUrl
方法去重,如果获取到相同的 host
会被当做一个主机被去重,无法实现集群同步。 /**
* Checks if the given service url contains the current host which is trying
* to replicate. Only after the EIP binding is done the host has a chance to
* identify itself in the list of replica nodes and needs to take itself out
* of replication traffic.
*
* @param url the service url of the replica node that the check is made.
* @return true, if the url represents the current node which is trying to
* replicate, false otherwise.
*/
public boolean isThisMyUrl(String url) {
final String myUrlConfigured = serverConfig.getMyUrl();
if (myUrlConfigured != null) {
return myUrlConfigured.equals(url);
}
return isInstanceURL(url, applicationInfoManager.getInfo());
}
Eureka Client
在启动完成后实例状态为变更 UP
状态,并尝试进行客户端注册,注册成功后定时进行心跳请求保持客户端状态;若第一次注册失败,后续定时心跳续期请求会返回 204 ,并重新尝试注册。Eureka Client
在发送注册、心跳等请求时,会向 Eureka Server
集群节点 serviceUrlList
顺序逐个去尝试,如果有一个请求成功了,就不再去向其他节点请求,最多只重试3次,超过3次直接抛出异常。 # com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient#execute
@Override
protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
List<EurekaEndpoint> candidateHosts = null;
int endpointIdx = 0;
// 顺序尝试前 numberOfRetries 可以注册中心实例
for (int retry = 0; retry < numberOfRetries; retry++) {
EurekaHttpClient currentHttpClient = delegate.get();
EurekaEndpoint currentEndpoint = null;
if (currentHttpClient == null) {
if (candidateHosts == null) {
candidateHosts = getHostCandidates();
if (candidateHosts.isEmpty()) {
throw new TransportException("There is no known eureka server; cluster server list is empty");
}
}
if (endpointIdx >= candidateHosts.size()) {
throw new TransportException("Cannot execute request on any known server");
}
currentEndpoint = candidateHosts.get(endpointIdx++);
currentHttpClient = clientFactory.newClient(currentEndpoint);
}
try {
EurekaHttpResponse<R> response = requestExecutor.execute(currentHttpClient);
if (serverStatusEvaluator.accept(response.getStatusCode(), requestExecutor.getRequestType())) {
delegate.set(currentHttpClient);
if (retry > 0) {
logger.info("Request execution succeeded on retry #{}", retry);
}
return response;
}
logger.warn("Request execution failure with status code {}; retrying on another server if available", response.getStatusCode());
} catch (Exception e) {
logger.warn("Request execution failed with message: {}", e.getMessage()); // just log message as the underlying client should log the stacktrace
}
// Connection error or 5xx from the server that must be retried on another server
delegate.compareAndSet(currentHttpClient, null);
if (currentEndpoint != null) {
quarantineSet.add(currentEndpoint);
}
}
throw new TransportException("Retry limit reached; giving up on completing the request");
}
# org.springframework.cloud.netflix.eureka.http.RestTemplateEurekaHttpClient#register
@Override
public EurekaHttpResponse<Void> register(InstanceInfo info) {
String urlPath = serviceUrl + "apps/" + info.getAppName();
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.ACCEPT_ENCODING, "gzip");
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
ResponseEntity<Void> response = restTemplate.exchange(urlPath, HttpMethod.POST, new HttpEntity<>(info, headers),
Void.class);
return anEurekaHttpResponse(response.getStatusCodeValue()).headers(headersOf(response)).build();
}
service-url
应该尽可能把所有地址都配置上,且顺序应该保持随机。Eureka Client
失效驱逐Eureka Service
会定时遍历遍历注册表中的实例,找出超过租约期的实例并将其从注册表中移除。若已启用自我保护模式,则停止驱逐,直到恢复心跳,自我保护模式关闭。 # com.netflix.eureka.registry.AbstractInstanceRegistry#evict(long)
public void evict(long additionalLeaseMs) {
logger.debug("Running the evict task");
// 判断自我保护模式是否启动:防止由于网络分区或临时的网络中断等非正常情况导致 Eureka 大规模地将实例误认为已失效并驱逐,避免影响系统的整体可用性。
if (!isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
return;
}
// We collect first all expired items, to evict them in random order. For large eviction sets,
// if we do not that, we might wipe out whole apps before self preservation kicks in. By randomizing it,
// the impact should be evenly distributed across all applications.
List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) {
Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue();
if (leaseMap != null) {
for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) {
Lease<InstanceInfo> lease = leaseEntry.getValue();
if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
expiredLeases.add(lease);
}
}
}
}
// To compensate for GC pauses or drifting local time, we need to use current registry size as a base for
// triggering self-preservation. Without that we would wipe out full registry.
// 即使关闭自我保护模式,若不将 renewalPercentThreshold 设置为 0 ,实例也会分批过期,避免网络原因造成服务难以恢复
int registrySize = (int) getLocalRegistrySize();
int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
int evictionLimit = registrySize - registrySizeThreshold;
int toEvict = Math.min(expiredLeases.size(), evictionLimit);
if (toEvict > 0) {
logger.info("Evicting {} items (expired={}, evictionLimit={})", toEvict, expiredLeases.size(), evictionLimit);
// 随机驱逐的方式将过期实例的驱逐影响分布在不同应用之间,避免某一应用实例被全部驱逐。
Random random = new Random(System.currentTimeMillis());
for (int i = 0; i < toEvict; i++) {
// Pick a random item (Knuth shuffle algorithm)
int next = i + random.nextInt(expiredLeases.size() - i);
Collections.swap(expiredLeases, i, next);
Lease<InstanceInfo> lease = expiredLeases.get(i);
String appName = lease.getHolder().getAppName();
String id = lease.getHolder().getId();
EXPIRED.increment();
logger.warn("DS: Registry: expired lease for {}/{}", appName, id);
internalCancel(appName, id, false);
}
}
}
假设 20 个租约,其中有 10 个租约过期。
第一轮执行开始
int registrySize = 20;
int registrySizeThreshold = (int) (20 * 0.85) = 17;
int evictionLimit = 20 - 17 = 3;
int toEvict = Math.min(10, 3) = 3;
第一轮执行结束,剩余 17 个租约,其中有 7 个租约过期。
第二轮执行开始
int registrySize = 17;
int registrySizeThreshold = (int) (17 * 0.85) = 14;
int evictionLimit = 17 - 14 = 3;
int toEvict = Math.min(7, 3) = 3;
第二轮执行结束,剩余 14 个租约,其中有 4 个租约过期。
...以此类推,或者将 renewalPercentThreshold 设置为0 ,但不建议
Eureka
属于 AP 设计,注册中心是完全平等和独立,且状态并不完全一致,当 Eureka Client
请求注册、续期、下线到某一个注册中心实例时,该实例会将这些信息同步到集群中其它注册中心,以注册的代码为例: # com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#register
/**
* Registers the information about the {@link InstanceInfo} and replicates
* this information to all peer eureka nodes. If this is replication event
* from other replica nodes then it is not replicated.
*
* @param info
* the {@link InstanceInfo} to be registered and replicated.
* @param isReplication
* true if this is a replication event from other replica nodes,
* false otherwise.
*/
@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);
}
# com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateToPeers
/**
* Replicates all eureka actions to peer eureka nodes except for replication
* traffic to this node.
*
*/
private void replicateToPeers(Action action, String appName, String id,
InstanceInfo info /* optional */,
InstanceStatus newStatus /* optional */, boolean isReplication) {
Stopwatch tracer = action.getTimer().start();
try {
if (isReplication) {
numberOfReplicationsLastMin.increment();
}
// If it is a replication already, do not replicate again as this will create a poison replication
if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
return;
}
for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
// If the url represents this host, do not replicate to yourself.
if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
continue;
}
replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
}
} finally {
tracer.stop();
}
}
避免大量下线:当心跳数量突然下降时,停止过快地移除实例。
提高系统可用性:在网络抖动或短暂的连接问题下,保证系统中的服务实例尽可能保持在线。
protected void updateRenewsPerMinThreshold() {
this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfClientsSendingRenews
* (60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds())
* serverConfig.getRenewalPercentThreshold());
}
# 以上面的实例为例
int(3 (实例数) * (60.0 / 30(续期时间))* 0.85) = 5
👋 你好,我是 Lorin 洛林,一位 Java 后端技术开发者!座右铭:Technology has the power to make the world a better place.
🚀 我对技术的热情是我不断学习和分享的动力。我的博客是一个关于Java生态系统、后端开发和最新技术趋势的地方。
🧠 作为一个 Java 后端技术爱好者,我不仅热衷于探索语言的新特性和技术的深度,还热衷于分享我的见解和最佳实践。我相信知识的分享和社区合作可以帮助我们共同成长。
💡 在我的博客上,你将找到关于Java核心概念、JVM 底层技术、常用框架如Spring和Mybatis 、MySQL等数据库管理、RabbitMQ、Rocketmq等消息中间件、性能优化等内容的深入文章。我也将分享一些编程技巧和解决问题的方法,以帮助你更好地掌握Java编程。
🌐 我鼓励互动和建立社区,因此请留下你的问题、建议或主题请求,让我知道你感兴趣的内容。此外,我将分享最新的互联网和技术资讯,以确保你与技术世界的最新发展保持联系。我期待与你一起在技术之路上前进,一起探讨技术世界的无限可能性。
📖 保持关注我的博客,让我们共同追求技术卓越。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。