Spring Cloud Eureka

Eureka是服务注册发现中心。Eureka 来源于古希腊词汇,意为“发现了”。在软件领域, Eureka是Netflix公司开源的一个服务注册与发现的组件,和Netflix公司其它的一些服务组件(例如:负载均衡、 熔断器、网关等) 一起被Spring Cloud社区整合为Spring Cloud Netflix模块。 Eureka是Netflix贡献给Spring Cloud的一个开源框架。

同为注册中心,相比动物管理员Zookeeper,Spring Cloud Eureka有什么区别呢?

核心点在于分布式微服务中的CAP定理!

问:为什么 Zookeeper 不适合做注册中心?

Zookeeper 注重数据的一致性,Eureka 则侧重服务的可用性。

  • 在 Zookeeper中,若Leader挂了,则Zookeeper集群整体不对外提供服务了,直到重新选举出一个新的Leader出来(选举时间大约120s 左右),才能继续对外提供服务。
  • Eureka注重服务的可用性,当Eureka集群只要有一台活着,它就能对外提供服务。

Zookeeper是CP,而Eureka是AP

CAP理论相关请见博文:浅析CAP理论&Base理论

Eureka Quick Start Demo

单Eureka Server

设计一个Eureka的快速开始Demo,结果如下图所示:

Eureka Server作为服务注册中心,向所有Eureka Client提供服务注册的能力(Eureka Server自身具备Eureka Client一样的注册能力,单机部署Eureka Server时体现不出来,集群模式部署Eureka Server的时候就体现出来了,因为各个Eureka Server相互之间也是需要进行注册),所有注册的Eureka Client都可以和Eureka Server进行通信,请求获取注册到Eureka Server上的Eureka Client的服务列表,这就是服务发现。

在实际开发中,如果使用Eureka进行服务治理,一般都是单独创建一个Eureka Server应用作为服务注册中心独立部署。不过由于Eureka版本的停更,同时其他注册中心组件Nacos、Consul等出现,可能项目开发中使用Eureka的场景越来越少了,但有些旧的微服务代码中还是有Eureka的痕迹,可以作为学习了解的对象。

Eureka Demo地址:Eureka Quick Start Demo

Eureka的UI

代码运行执行,访问:http://localhost:8761/,如下图:

其中:

Status表示服务节点的状态:

  • UP: 服务是上线的,括号里面是具体服务实例的个数,提供服务的最小单元 ;
  • DOWN: 服务是下线的;
  • UN_KONW: 服务的状态未知;

Eureka Server的配置

常用的配置项如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
server:
port: 8761
# 如果Eureka的client配置中没有指定注册中心的地址service-url,并且自己注册自己配置没有关闭,那么应用的端口号就必须设置为8761,否则Eureka
# 启动之后会抛出异常,并且无法成功注册到Eureka Server上.

spring:
application:
name: eureka-server-1 # 应用名称,不要使用特殊字符

# eureka的配置分为三类: server配置, client配置, instance实例配置
eureka:
server: # server配置,eureka-server既是服务端又是客户端,也就是说,它不仅可以提供客户端注册,同时本身也可以注册到其他server上.
eviction-interval-timer-in-ms: 10000 # 服务端定时剔除下线的实例信息的间隔时间/ms
renewal-percent-threshold: 0.85 # 续约百分比,超过85%的应用没有发送心跳续约,那么eureka会保护服务,不会剔除任何一个实例(eureka会认为是自己的网络问题,AP)
enable-self-preservation: true # Eureka Server的自我保护机制,避免因为网络原因造成误删除,生产环境建议打开

instance: # instance实例配置
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port} # 实例ID名称: 主机名:应用名:端口号
hostname: localhost # 主机名称或者服务节点IP
prefer-ip-address: true # 服务列表以IP形式展示
lease-renewal-interval-in-seconds: 5 # 服务实例的心跳续约时间间隔,需要比上面的eviction-interval-timer-in-ms配置值小
lease-expiration-duration-in-seconds: 20 # Eureka Server至上一次收到Eureka Client心跳之后,等待下一次心跳的超时时间,这个时间内若没有收到下一次心跳,就剔除该客户端实例.

client:
service-url: # 指定注册中心的地址
defaultZone: ${EUREKA_SERVER_URL:http://localhost:8761/eureka}
register-with-eureka: ${REGISTER_WITH_EUREKA:false} # 先将server自己注册自己关掉|默认是开启的(集群模式需要开启,单机一般关闭)
fetch-registry: true # 应用是否去拉取服务列表到本地

关于端口port的配置需要注意,如果Eureka的client配置中没有指定注册中心的地址service-url,并且自己注册自己配置没有关闭,那么应用的端口号就必须设置为8761,否则Eureka启动之后会抛出异常,并且无法成功注册到Eureka Server上。异常信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
2023-05-16 18:13:00.233  INFO 54488 --- [extShutdownHook] o.s.c.n.e.s.EurekaServiceRegistry        : Unregistering application EUREKA-SERVER-1 with eureka with status DOWN
2023-05-16 18:13:00.233 INFO 54488 --- [extShutdownHook] com.netflix.discovery.DiscoveryClient : Saw local status change event StatusChangeEvent [timestamp=1684231980233, current=DOWN, previous=UP]
2023-05-16 18:13:00.234 INFO 54488 --- [nfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_EUREKA-SERVER-1/localhost:eureka-server-1:8762: registering service...
2023-05-16 18:13:00.235 INFO 54488 --- [nfoReplicator-0] c.n.d.s.t.d.RedirectingEurekaHttpClient : Request execution error. endpoint=DefaultEndpoint{ serviceUrl='http://localhost:8761/eureka/}, exception=java.net.ConnectException: Connection refused (Connection refused) stacktrace=com.sun.jersey.api.client.ClientHandlerException: java.net.ConnectException: Connection refused (Connection refused)
at com.sun.jersey.client.apache4.ApacheHttpClient4Handler.handle(ApacheHttpClient4Handler.java:187)
at com.sun.jersey.api.client.filter.GZIPContentEncodingFilter.handle(GZIPContentEncodingFilter.java:123)
at com.netflix.discovery.EurekaIdentityHeaderFilter.handle(EurekaIdentityHeaderFilter.java:27)
at com.sun.jersey.api.client.Client.handle(Client.java:652)
at com.sun.jersey.api.client.WebResource.handle(WebResource.java:682)
at com.sun.jersey.api.client.WebResource.access$200(WebResource.java:74)
at com.sun.jersey.api.client.WebResource$Builder.post(WebResource.java:570)
at com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient.register(AbstractJerseyEurekaHttpClient.java:57)
at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator$1.execute(EurekaHttpClientDecorator.java:59)
at com.netflix.discovery.shared.transport.decorator.MetricsCollectingEurekaHttpClient.execute(MetricsCollectingEurekaHttpClient.java:73)
at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.register(EurekaHttpClientDecorator.java:56)
at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator$1.execute(EurekaHttpClientDecorator.java:59)
at com.netflix.discovery.shared.transport.decorator.RedirectingEurekaHttpClient.executeOnNewServer(RedirectingEurekaHttpClient.java:121)
at com.netflix.discovery.shared.transport.decorator.RedirectingEurekaHttpClient.execute(RedirectingEurekaHttpClient.java:80)
at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.register(EurekaHttpClientDecorator.java:56)
at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator$1.execute(EurekaHttpClientDecorator.java:59)
at com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient.execute(RetryableEurekaHttpClient.java:120)
at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.register(EurekaHttpClientDecorator.java:56)
at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator$1.execute(EurekaHttpClientDecorator.java:59)
at com.netflix.discovery.shared.transport.decorator.SessionedEurekaHttpClient.execute(SessionedEurekaHttpClient.java:77)
at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.register(EurekaHttpClientDecorator.java:56)
at com.netflix.discovery.DiscoveryClient.register(DiscoveryClient.java:876)
at com.netflix.discovery.InstanceInfoReplicator.run(InstanceInfoReplicator.java:121)
at com.netflix.discovery.InstanceInfoReplicator$1.run(InstanceInfoReplicator.java:101)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.net.ConnectException: Connection refused (Connection refused)
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
at org.apache.http.conn.scheme.PlainSocketFactory.connectSocket(PlainSocketFactory.java:121)
at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:180)
at org.apache.http.impl.conn.AbstractPoolEntry.open(AbstractPoolEntry.java:144)
at org.apache.http.impl.conn.AbstractPooledConnAdapter.open(AbstractPooledConnAdapter.java:134)
at org.apache.http.impl.client.DefaultRequestDirector.tryConnect(DefaultRequestDirector.java:605)
at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:440)
at org.apache.http.impl.client.AbstractHttpClient.doExecute(AbstractHttpClient.java:835)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:118)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:56)
at com.sun.jersey.client.apache4.ApacheHttpClient4Handler.handle(ApacheHttpClient4Handler.java:173)
... 30 more

2023-05-16 18:13:00.235 WARN 54488 --- [nfoReplicator-0] c.n.d.s.t.d.RetryableEurekaHttpClient : Request execution failed with message: java.net.ConnectException: Connection refused (Connection refused)
2023-05-16 18:13:00.236 WARN 54488 --- [nfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_EUREKA-SERVER-1/localhost:eureka-server-1:8762 - registration failed Cannot execute request on any known server

com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server
at com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient.execute(RetryableEurekaHttpClient.java:112) ~[eureka-client-1.10.14.jar:1.10.14]
at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.register(EurekaHttpClientDecorator.java:56) ~[eureka-client-1.10.14.jar:1.10.14]
at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator$1.execute(EurekaHttpClientDecorator.java:59) ~[eureka-client-1.10.14.jar:1.10.14]
at com.netflix.discovery.shared.transport.decorator.SessionedEurekaHttpClient.execute(SessionedEurekaHttpClient.java:77) ~[eureka-client-1.10.14.jar:1.10.14]
at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.register(EurekaHttpClientDecorator.java:56) ~[eureka-client-1.10.14.jar:1.10.14]
at com.netflix.discovery.DiscoveryClient.register(DiscoveryClient.java:876) ~[eureka-client-1.10.14.jar:1.10.14]
at com.netflix.discovery.InstanceInfoReplicator.run(InstanceInfoReplicator.java:121) [eureka-client-1.10.14.jar:1.10.14]
at com.netflix.discovery.InstanceInfoReplicator$1.run(InstanceInfoReplicator.java:101) [eureka-client-1.10.14.jar:1.10.14]
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_201]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_201]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_201]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_201]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_201]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_201]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_201]

2023-05-16 18:13:00.236 WARN 54488 --- [nfoReplicator-0] c.n.discovery.InstanceInfoReplicator : There was a problem with the instance info replicator

com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server
at com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient.execute(RetryableEurekaHttpClient.java:112) ~[eureka-client-1.10.14.jar:1.10.14]
at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.register(EurekaHttpClientDecorator.java:56) ~[eureka-client-1.10.14.jar:1.10.14]
at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator$1.execute(EurekaHttpClientDecorator.java:59) ~[eureka-client-1.10.14.jar:1.10.14]
at com.netflix.discovery.shared.transport.decorator.SessionedEurekaHttpClient.execute(SessionedEurekaHttpClient.java:77) ~[eureka-client-1.10.14.jar:1.10.14]
at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.register(EurekaHttpClientDecorator.java:56) ~[eureka-client-1.10.14.jar:1.10.14]
at com.netflix.discovery.DiscoveryClient.register(DiscoveryClient.java:876) ~[eureka-client-1.10.14.jar:1.10.14]
at com.netflix.discovery.InstanceInfoReplicator.run(InstanceInfoReplicator.java:121) ~[eureka-client-1.10.14.jar:1.10.14]
at com.netflix.discovery.InstanceInfoReplicator$1.run(InstanceInfoReplicator.java:101) [eureka-client-1.10.14.jar:1.10.14]
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_201]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_201]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_201]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_201]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_201]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_201]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_201]

原因分析:

因为Eureka Server中如果没有配置注册中心的地址,并且Eureka Server会注册的本身时,Eureka Sever在启动之后就会使用源码中默认的硬编码写的URL地址为:http://localhost:8761,尝试对本身进行注册。从上述异常中的代码调用链中追溯可以找到源码中默认配置类为:

org.springframework.cloud.netflix.eureka.EurekaClientConfigBean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Default Eureka URL.
*/
public static final String DEFAULT_URL = "http://localhost:8761" + DEFAULT_PREFIX+ "/";

/**
* Default availability zone if none is resolved based on region.
*/
public static final String DEFAULT_ZONE = "defaultZone";

// serviceUrl初始化时会放入默认的Map集合:
private Map<String, String> serviceUrl = new HashMap<>();{
this.serviceUrl.put(DEFAULT_ZONE, DEFAULT_URL);
}

// 该方法就是配置文件中的,eureka:client:service-url配置项,如果没有配置,其中就只有默认的URL,
// Eureka Server注册自身的时候就会向http://localhost:8761发送注册请求,如果端口号不是8761,
// 就会抛出上述异常信息。
public void setServiceUrl(Map<String, String> serviceUrl) {
this.serviceUrl = serviceUrl;
}

集群Eureka Server

集群Demo架构图

Eureka Demo地址:Eureka Quick Start Demo

在本地部署Eureka Server集群时需要注意,如果所有的Eureka Server的IP都是本机,Eureka会认为只是同一个服务启动了多台,其间并没有数据交互,不是集群,需要修改操作系统的配置文件自定义IP地址别名,然后在Eureka Server配置文件中利用别名以示区分即可。

Eureka Server集群是去中心化设计,没有主从的概念,所有的节点都是对等的,每个节点互相注册,只要集群中各有一个节点存活,就能对外提供服务注册和服务发现能力,保证服务的可用性。

Eureka是AP,没有主从,一旦有主从就会涉及到主从的选举,同步会造成一定的时间内不可用。

Eureka Server集群内部数据流图:


参考文档: