大家知道,将一个普通的springboot 应用注册到Eureka Server只需要两步
- 在应用主类添加@EnableDiscoveryClient注解
- 在application.properties中配置eureka.client.serviceUrl.defaultZone
顺着这条思路,首先看下@EnableDiscoveryClient有什么类容
/**
* Annotation to enable a DiscoveryClient implementation.
* @author Spencer Gibb
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(EnableDiscoveryClientImportSelector.class)
public @interface EnableDiscoveryClient {
/**
* If true, the ServiceRegistry will automatically register the local server.
*/
boolean autoRegister() default true;
}
从注释就能知道,该注解主要是开启DiscoveryClient的实例。梳理关系后可得到下图
其中,左边的org.springframework.cloud.client.discovery.DiscoveryClient是spring cloud的接口,定义了用来发现服务的常用抽象方法。org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient是对接口的实现,从命名就可以知道,是对Eureka的发现服务的封装,所以依赖了com.netflix.discovery.EurekaClient接口,EurekaClient又继承于LookupService,主要是定义了发现服务的抽象方法,而真正去实现发现服务功能的类是com.netflix.discovery.DiscoveryClient。
要看一个类,首先就看类头部定义的注释
/**
* The class that is instrumental for interactions with <tt>Eureka Server</tt>.
*
* <p>
* <tt>Eureka Client</tt> is responsible for a) <em>Registering</em> the
* instance with <tt>Eureka Server</tt> b) <em>Renewal</em>of the lease with
* <tt>Eureka Server</tt> c) <em>Cancellation</em> of the lease from
* <tt>Eureka Server</tt> during shutdown
* <p>
* d) <em>Querying</em> the list of services/instances registered with
* <tt>Eureka Server</tt>
* <p>
*
* <p>
* <tt>Eureka Client</tt> needs a configured list of <tt>Eureka Server</tt>
* {@link java.net.URL}s to talk to.These {@link java.net.URL}s are typically amazon elastic eips
* which do not change. All of the functions defined above fail-over to other
* {@link java.net.URL}s specified in the list in the case of failure.
* </p>
*
* @author Karthik Ranganathan, Greg Kim
* @author Spencer Gibb
*
*/
@Singleton
这个类用于Eureka Server相互协作;
Eureka Client主要负责下面的任务:
- 向Eureka Server注册服务实例
- 向Eureka Server获取服务租约
- 在服务关闭时,向Eureka Server取消租约
- 查询Eureka Server中的服务实例列表
在DiscoveryClient的构造方法里可以看到,会调用initScheduledTasks(),首先看看initScheduledTasks
/**
* Initializes all scheduled tasks.
*/
private void initScheduledTasks() {
if (clientConfig.shouldFetchRegistry()) {
// registry cache refresh timer
int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
scheduler.schedule(
new TimedSupervisorTask(
"cacheRefresh",
scheduler,
cacheRefreshExecutor,
registryFetchIntervalSeconds,
TimeUnit.SECONDS,
expBackOffBound,
new CacheRefreshThread()
),
registryFetchIntervalSeconds, TimeUnit.SECONDS);
}
if (clientConfig.shouldRegisterWithEureka()) {
int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
logger.info("Starting heartbeat executor: " + "renew interval is: " + renewalIntervalInSecs);
// Heartbeat timer
scheduler.schedule(
new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs,
TimeUnit.SECONDS,
expBackOffBound,
new HeartbeatThread()
),
renewalIntervalInSecs, TimeUnit.SECONDS);
// InstanceInfo replicator
instanceInfoReplicator = new InstanceInfoReplicator(
this,
instanceInfo,
clientConfig.getInstanceInfoReplicationIntervalSeconds(),
2); // burstSize
statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
@Override
public String getId() {
return "statusChangeListener";
}
@Override
public void notify(StatusChangeEvent statusChangeEvent) {
if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
// log at warn level if DOWN was involved
logger.warn("Saw local status change event {}", statusChangeEvent);
} else {
logger.info("Saw local status change event {}", statusChangeEvent);
}
instanceInfoReplicator.onDemandUpdate();
}
};
if (clientConfig.shouldOnDemandUpdateStatusChange()) {
applicationInfoManager.registerStatusChangeListener(statusChangeListener);
}
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else {
logger.info("Not registering with Eureka server per configuration");
}
}
可以看到,它有个判断clientConfig.shouldRegisterWithEureka(),默认是true,在if里,会创建一个InstanceInfoReplicator的实例对象,它会执行一个定时任务,默认周期是30秒,点进去查看InstanceInfoReplicator的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);
}
}
可以看到discoveryClient.register();这一行就是真正触发调用注册的地方了。继续查看register方法
boolean register() throws Throwable {
logger.info(PREFIX + appPathIdentifier + ": registering service...");
EurekaHttpResponse<Void> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == 204;
}
可以看到,注册操作也是通融过REST请求的方式进行的,同时,注册的时候要传入一个instanceInfo对象,就是客户端的元数据,当服务端返回204时,就代表注册成功。
上面分析了Eureka Client是如何注册的,下面来看一些额外的东西
- 配置Eureka Server的URL列表是如何获取的。根据配置的参数名eureka.client.serviceUrl.dedfaultZone,在DiscoveryClient搜索serviceUrl可以找到相关属性的加载,但在SR5版本已经被标记为@Deprecated,从注释上可以看到替代的类com.netflix.discovery.endpoint.EndpointUtils,在EndpointUtils中可以找到这个 方法getServiceUrlsFromConfig
public static List<String> getServiceUrlsFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) { List<String> orderedUrls = new ArrayList<String>(); String region = getRegion(clientConfig); String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion()); if (availZones == null || availZones.length == 0) { availZones = new String[1]; availZones[0] = DEFAULT_ZONE; } logger.debug("The availability zone for the given region {} are {}", region, Arrays.toString(availZones)); int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones); List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[myZoneOffset]); if (serviceUrls != null) { orderedUrls.addAll(serviceUrls); } int currentOffset = myZoneOffset == (availZones.length - 1) ? 0 : (myZoneOffset + 1); while (currentOffset != myZoneOffset) { serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[currentOffset]); if (serviceUrls != null) { orderedUrls.addAll(serviceUrls); } if (currentOffset == (availZones.length - 1)) { currentOffset = 0; } else { currentOffset++; } } if (orderedUrls.size() < 1) { throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!"); } return orderedUrls; }
从getServiceUrlsFromConfig方法中可以看到,依次加载了两个东西Region和Zone
-
通过getRegion方法可以看到,从配置里读取了一个Region,默认是default,自定义的话是通过eureka.client.region
public static String getRegion(EurekaClientConfig clientConfig) { String region = clientConfig.getRegion(); if (region == null) { region = DEFAULT_REGION; } region = region.trim().toLowerCase(); return region; }
-
通过getAvailabilityZones来获取Zone,默认是defaultZone,这也是配置注册url的属性 eureka.client.region.defaultZone的由来了。若要自定义,配置名是eureka.client.availability-zones,要配置多个,则以逗号隔开
public String[] getAvailabilityZones(String region) { String value = this.availabilityZones.get(region); if (value == null) { value = DEFAULT_ZONE; } return value.split(","); }
-
在获取到Region和Zone后,就会加载具体的Eureka Server地址,方法是clientConfig.getEurekaServerServiceUrls,查看实现
public List<String> getEurekaServerServiceUrls(String myZone) {
String serviceUrls = this.serviceUrl.get(myZone);
if (serviceUrls == null || serviceUrls.isEmpty()) {
serviceUrls = this.serviceUrl.get(DEFAULT_ZONE);
}
if (!StringUtils.isEmpty(serviceUrls)) {
final String[] serviceUrlsSplit = StringUtils.commaDelimitedListToStringArray(serviceUrls);
List<String> eurekaServiceUrls = new ArrayList<>(serviceUrlsSplit.length);
for (String eurekaServiceUrl : serviceUrlsSplit) {
if (!endsWithSlash(eurekaServiceUrl)) {
eurekaServiceUrl += "/";
}
eurekaServiceUrls.add(eurekaServiceUrl);
}
return eurekaServiceUrls;
}
return new ArrayList<>();
}
这里之所以要有个Zone参数,是因为在使用Ribbon调用服务的时候,Ribbon的默认策略会优先访问和客户端处于同一个Zone的服务端实例,只有在同一个Zone没有可用服务端实例的时候才会访问其他Zone的服务端实例。
-----------------------------------------------------------------------------------------------------------------------------