Eureka
Spring Cloud Eureka是基于Netflix Eureka做的二次封装,主要完成服务治理功能。
服务治理,从侠义的角度讲,就是解决服务注册与发现的问题。早期我们可以通过配置内网域名来访问服务,但随着服务数量越来越多,域名维护越来越繁琐,这个时候就可以上Eureka了。DNS的作用是把域名转换为IP地址,而Eureka的作用就是把服务名转换为IP地址,这样各个服务之间就可以通过服务名来调用。和使用DNS时必须为每个服务定义不同的内网域名一样,使用Eureka也必须为每个服务定义不同的服务名。
多说一句,其实Spring Cloud本身是支持多种服务注册中心,包括Eureka、Zookeeper和Consul,由于Eureka是Netflix采用的方案,Spring Cloud核心又是Netflix贡献的,所以一般使用Eureka多一些。
角色
使用Eureka时有三个角色:注册中心、服务提供者和服务发现者,Eureka起到注册中心的作用。服务提供者在启动时调用REST接口注册到Eureka上,之后通过心跳包告诉Eureka服务还活着,服务发现者可以通过REST接口从Eureka获取服务列表,服务发现者在本地进行了缓存,不必每一个请求都访问Eureka。服务注册和发现的唯一标识是服务名,在分布式环境下的多个服务成为服务实例。
服务端
首先,引入Eureka的包。加入dependencyManagement后每一个dependency都使用dependencyManagement的version。
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</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>
|
然后,添加@EnableEurekaServer注解,开启Eureka功能,除此之外不用再写一行代码。
@EnableEurekaServer public class ServiceRegistryApplication { }
|
最后,在application.properties里面配置注册中心属性
# 端口 server.port=6010 # 主机名 eureka.instance.hostname=localhost # 自己不用注册到自己 eureka.client.register-with-eureka=false # 自己不用从自己读取服务列表 eureka.client.fetch-registry=false # 关闭保护机制 eureka.server.enable-self-preservation=false # 注册中心地址 eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
|
启动ServiceRegistryApplication,访问http://localhost:6010 就能看到Eureka提供的页面了。

| Application |
AMIs |
Availability Zones |
Status |
| SERVICE-USER |
n/a (1) |
(1) |
UP (1) - service-user:172.168.0.24:3310 |
| SERVICE-ORDER |
n/a (1) |
(1) |
UP (1) - service-order:172.168.0.24:3330 |
客户端
首先,引入Eureka的包,和服务端的区别是把starter-eureka-server换成了starter-eureka。
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</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>
|
然后,添加@EnableDiscoveryClient注解,开启服务发现功能。
@EnableDiscoveryClient public class ServiceUserApplication { }
|
最后,在application.properties里面配置注册中心属性
# 服务名 spring.application.name=service-user # 主机名 eureka.instance.hostname=localhost # 注册到Eureka eureka.client.register-with-eureka=true # 从Eureka读取服务列表 eureka.client.fetch-registry=true # Eureka地址 eureka.client.service-url.defaultZone=http://localhost:6010/eureka/
|
默认服务实例名为主机名+服务名+端口,服务列表显示如下:
| Application |
AMIs |
Availability Zones |
Status |
| SERVICE-USER |
n/a (1) |
(1) |
UP (1) - lunovox:service-user:3310 |
注意:Status列里面显示的是服务的实例名,并非服务的访问地址。把鼠标移动到服务实例上,在左下角可以看到
显示的访问地址为:[http://localhost:3310/info],默认是根据主机名访问的。
通常我们更习惯使用IP地址而不是使用主机名访问服务,所以想到自定义instance-id,我习惯将服务实例名定义为服务名+IP地址+端口。
eureka.instance.instance-id=${spring.application.name}:${spring.cloud.client.ipAddress}:${server.port}
|
修改配置文件重启,服务列表显示如下:
| Application |
AMIs |
Availability Zones |
Status |
| SERVICE-USER |
n/a (1) |
(1) |
UP (1) - service-user:192.168.75.1:3310 |
实例名修改了,但是看一下访问地址,还是[http://localhost:3310/info]。
这说明instance-id仅仅定义了实例名,如果要修改访问地址我们应该设置prefer-ip-address
eureka.instance.prefer-ip-address=true
|
修改配置文件重启,服务列表显示如下:
| Application |
AMIs |
Availability Zones |
Status |
| SERVICE-USER |
n/a (1) |
(1) |
UP (1) - service-user:192.168.75.1:3310 |
访问地址是[http://192.168.75.1:3310/info],不再使用主机名访问了。
但是,还有问题。因为我的主机有多网卡,我想使用的IP地址是173.168.0.24,不是192.168.75.1, 后面的IP地址是安装虚拟机后虚拟的网卡。这个时候可以设置preferred-networks
spring.cloud.inetutils.preferred-networks=173.168.0
|
修改配置文件重启,服务列表显示如下:
| Application |
AMIs |
Availability Zones |
Status |
| SERVICE-USER |
n/a (1) |
(1) |
UP (1) - service-user:192.168.75.1:3310 |
访问地址是[http://173.168.0.24:3310/info],访问地址是正确的,但是实例名中的地址还是没变。
实例名中的地址仅用来显示和区分,所以并不影响使用,但是为什么没有改成173.168.0.24呢?我明明已经设置优先使用173.168.0段的IP了。
原因就在于实例名生成早于inetutils,所以preferred-networks没起作用。怎么办?
我们在启动参数中增加-Dspring.cloud.inetutils.preferred-networks=173.168.0 ,服务列表显示如下:
| Application |
AMIs |
Availability Zones |
Status |
| SERVICE-USER |
n/a (1) |
(1) |
UP (1) - service-user:173.168.0.24:3310 |
访问地址是[http://173.168.0.24:3310/info],终于对了。
高可用
生产环境中Eureka显然不能是单点了,Eureka集群采用了互相注册的办法,通过把自己注册到另外一个Eureka上实现互相发现。互通的Eureka服务器之间可以同步服务列表。
例如:部署在192.168.0.1的Eureka1配置如下
server.port=6000 eureka.instance.hostname=localhost eureka.client.service-url.defaultZone=http://192.168.0.2:6000/eureka
|
部署在192.168.0.2的Eureka2配置如下
server.port=6000 eureka.instance.hostname=localhost eureka.client.service-url.defaultZone=http://192.168.0.1:6000/eureka
|
单点情况下defaultZone指向自己,集群情况下defaultZone执行了其他Eureka服务器,相当于把自己也作为一个服务注册到其他Eureka上,这样就实现了互通。
自动注入
通过@EnableDiscoveryClient注解激活Eureka客户端以后就可以注入DiscoveryClient了,通过DiscoveryClient可以手动读取到Eureka上的服务信息。
@Autowired DiscoveryClient client;
public Object getServiceList() { List<ServiceInstance> list = client.getInstances("service-order"); return list; }
|
例如,上面的代码读取Eureka注册中心上service-order 服务的实例信息,返回结果如下:
[ { "host": "localhost", "port": 3330, "metadata": {}, "uri": "http://localhost:3330", "secure": false, "serviceId": "SERVICE-ORDER", "instanceInfo": { "instanceId": "localhost:service-order:3330", "app": "SERVICE-ORDER", "appGroupName": null, "ipAddr": "192.168.75.1", "sid": "na", "homePageUrl": "http://localhost:3330/", "statusPageUrl": "http://localhost:3330/info", "healthCheckUrl": "http://localhost:3330/health", "secureHealthCheckUrl": null, "vipAddress": "service-order", "secureVipAddress": "service-order", "countryId": 1, "dataCenterInfo": { "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", "name": "MyOwn" }, "hostName": "localhost", "status": "UP", "leaseInfo": { "renewalIntervalInSecs": 30, "durationInSecs": 90, "registrationTimestamp": 1528356532934, "lastRenewalTimestamp": 1528356532934, "evictionTimestamp": 0, "serviceUpTimestamp": 1528356532935 }, "isCoordinatingDiscoveryServer": false, "metadata": {}, "lastUpdatedTimestamp": 1528356532935, "lastDirtyTimestamp": 1528356501095, "actionType": "ADDED", "asgName": null, "overriddenStatus": "UNKNOWN" } } ]
|
优雅下线
我们发现,停止服务后在Eureka上面仍然能看到已经停止的服务,即使关闭了自我保护机制也是一样。一般要过了很长时间才能在Eureka上删除已经停止的服务。
产生延迟的原因是Eureka和服务之间通过Heartbeat包进行检测,默认的检测时间是比较长的,所以才会出现停止服务后Eureka长时间不刷新的问题。可以修改心跳频率。
# 服务发送心跳包给Eureka的时间间隔,时间单位为秒,默认值30 eureka.instance.lease-renewal-interval-in-seconds=1 # Eureka超过这个时间没有收到心跳包删除服务实例,时间单位为秒,默认值30 eureka.instance.lease-expiration-duration-in-seconds=3
|
默认服务每隔30秒给Eureka Server发送一个心跳包,Eureka Server超过90秒没有收到心跳包才会进行下线处理,所以感觉时间延迟比较长。
上面的例子中,我们减少了心跳时间间隔和检测时间,可以更快发现服务下线。但是减少心跳时间间隔会增加网络开销,不是好办法,更好的方案应该是服务停止时立即给Eureka发下线通知。
属性详解
server类
client类
| 参数 |
说明 |
| eureka.client.register-with-eureka |
=true,注册到Eureka;=false,不注册 |
|
|
|
|
|
|
|
|
|
|
|
###
源码分析
客户端
@EnableDiscoveryClient注解对应DiscoveryClient接口。
DiscoveryClient接口属于org.springframework.cloud:spring-cloud-commons JAR包,是抽象接口,具体实现可以是Eureka,也可以是Zookeeper或者Consul。
package org.springframework.cloud.client.discovery; public interface DiscoveryClient { List<ServiceInstance> getInstances(String serviceId); }
|
EurekaDiscoveryClient类是DiscoveryClient接口的Eureka实现,属于org.springframework.cloud:spring-cloud-netflix-eureka-clientJAR包。
package org.springframework.cloud.netflix.eureka; @RequiredArgsConstructor public class EurekaDiscoveryClient implements DiscoveryClient { private final EurekaInstanceConfig config; private final EurekaClient eurekaClient; @Override public List<ServiceInstance> getInstances(String serviceId) { List<InstanceInfo> infos; infos = this.eurekaClient.getInstancesByVipAddress(serviceId, false); List<ServiceInstance> instances = new ArrayList<>(); for (InstanceInfo info : infos) { instances.add(new EurekaServiceInstance(info)); } return instances; } }
|
EurekaDiscoveryClient实际上通过EurekaClient读取服务列表,只负责把InstanceInfo转换成EurekaServiceInstance。
EurekaDiscoveryClient注入的EurekaClient接口属于com.netflix.eureka:eureka-clientJAR包。
@ImplementedBy(DiscoveryClient.class) public interface EurekaClient extends LookupService { public List<InstanceInfo> getInstancesByVipAddress(String vipAddress, boolean secure); public void shutdown(); }
|
EurekaClient的实现类为DiscoveryClient
package com.netflix.discovery; @Singleton public class DiscoveryClient implements EurekaClient { private static final Logger logger = LoggerFactory.getLogger(DiscoveryClient.class); private final EurekaTransport eurekaTransport; private final InstanceInfo instanceInfo; private final AtomicBoolean isShutdown = new AtomicBoolean(false); boolean register() throws Throwable { EurekaHttpResponse<Void> httpResponse; try { httpResponse = eurekaTransport.registrationClient.register(instanceInfo); } catch (Exception e) { throw e; } return httpResponse.getStatusCode() == 204; } boolean renew() { EurekaHttpResponse<InstanceInfo> httpResponse; try { httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo); return httpResponse.getStatusCode() == 200; } catch (Throwable e) { return false; } } public synchronized void shutdown() { if (isShutdown.compareAndSet(false, true)) { if (eurekaTransport != null) { eurekaTransport.shutdown(); } } } }
|
服务端