本章主要内容:
1.源码分析
2.配置详解
源码分析
我们从Eureka的客户端看它如何完成通信行为的。
我们将一个普通的Spring Boot应用注册到Eureka Server 或者是 从Eureka Server 中获取服务列表时,主要做了两个事情:
- 在应用主类中配置了@EnableDiscoveryClient 注解
- 在application.properties中用eureka.client.serviceUrl.defaultZone参数指定了服务注册中心的位置。
我们来看一下@EnableDiscoveryClient 注解的源码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(EnableDiscoveryClientImportSelector.class)
public @interface EnableDiscoveryClient {
}
该注解主要是用来开启DiscoveryClient的实例
org.springframework.cloud.client.discovery.DiscoveryClient是Spring Cloud 的接口,定义了用来发现服务的常用抽象方法,通过该接口可以有效地屏蔽服务治理的实现细节,所以使用Spring Cloud构建的微服务应用可以方便切换不同服务治理框架,不用改动程序代码,只需要添加一些针对服务治理框架的配置即可。
package org.springframework.cloud.client.discovery;
import java.util.List;
import org.springframework.cloud.client.ServiceInstance;
public interface DiscoveryClient
{
public abstract String description();
public abstract ServiceInstance getLocalServiceInstance();
public abstract List getInstances(String s);
public abstract List getServices();
}
org.springframework.cloud.netflix.eureka.EnableDiscoveryClient是对DiscoveryClient接口的实现,实现的是对Eureka发现服务的封装。真正实现发现服务的是com.netflix.discovery.DiscoveryClient类
DiscoveryClient类主要用于帮助与Eureka Server互相协作。
Eureka Client 负责下面的任务:
- 向Eureka Server注册服务实例
- 向Eureka Server 服务租约
- 当服务关闭期间,向Eureka Server 取消租约
- 查询Eureka Server中的服务实例列表
Eureka Cient 还需要配置一个Eureka Server的URL列表
先分析一下Eureka Server的URL列表:
com.netflix.discovery.endpoint.EndpointUtils
public static List getServiceUrlsFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone)
{
List orderedUrls = new ArrayList();
String region = getRegion(clientConfig);
String availZones[] = clientConfig.getAvailabilityZones(clientConfig.getRegion());
if(availZones == null || availZones.length == 0)
{
availZones = new String[1];
availZones[0] = "default";
}
logger.debug("The availability zone for the given region {} are {}", region, Arrays.toString(availZones));
int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);
List serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[myZoneOffset]);
if(serviceUrls != null)
orderedUrls.addAll(serviceUrls);
for(int currentOffset = myZoneOffset != availZones.length - 1 ? myZoneOffset + 1 : 0; 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!");
else
return orderedUrls;
}
从上面的函数中发现,客户端一共加载了两个内容,一个是Region,一个是Zone。
public static String getRegion(EurekaClientConfig clientConfig)
{
String region = clientConfig.getRegion();
if(region == null)
region = "default";
region = region.trim().toLowerCase();
return region;
}
getRegion函数从配置中读取了一个Region返回,所以一个微服务应用只可以属于一个Region。默认是default。通过eureka.client.region属性定义region。
public String[] getAvailabilityZones(String region)
{
String value = (String)availabilityZones.get(region);
if(value == null)
value = "defaultZone";
return value.split(",");
}
getAvailabilityZones函数,默认是defaultZone,可以看到Region与ZOne是一对多的关系,Zone可以设置多个,用逗号分隔。
在获取了Region和Zone的信息后,才开始真正加载Eureka Server的具体地址。根据传入的参数按一定算法加载位于哪一个Zone配置的serviceUrls。
int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);
List serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[myZoneOffset]);
看一下clientConfig.getEurekaServerServiceUrls的实现:
public List getEurekaServerServiceUrls(String myZone)
{
String serviceUrls = (String)serviceUrl.get(myZone);
if(serviceUrls == null || serviceUrls.isEmpty())
serviceUrls = (String)serviceUrl.get("defaultZone");
if(!StringUtils.isEmpty(serviceUrls))
{
String serviceUrlsSplit[] = StringUtils.commaDelimitedListToStringArray(serviceUrls);
List eurekaServiceUrls = new ArrayList(serviceUrlsSplit.length);
String as[] = serviceUrlsSplit;
int i = as.length;
for(int j = 0; j < i; j++)
{
String eurekaServiceUrl = as[j];
if(!endsWithSlash(eurekaServiceUrl))
eurekaServiceUrl = (new StringBuilder()).append(eurekaServiceUrl).append("/").toString();
eurekaServiceUrls.add(eurekaServiceUrl);
}
return eurekaServiceUrls;
} else
{
return new ArrayList();
}
}
当在微服务应用中使用Ribbon实现服务调用时,对于Zone的设置可以在负载均衡时实现区域亲和特性:Ribbon的默认策略会优先访问客户端处于同一个Zone的服务端实例,只有当同一个Zone中没有可用服务端实例的时候才会访问其他Zone中的实例。
服务注册
接着看DiscoveryClient如何实现服务注册的:
private void initScheduledTasks()
{
if(clientConfig.shouldFetchRegistry())
{
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((new StringBuilder()).append("Starting heartbeat executor: renew interval is: ").append(renewalIntervalInSecs).toString());
scheduler.schedule(new TimedSupervisorTask("heartbeat", scheduler, heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new HeartbeatThread()), renewalIntervalInSecs, TimeUnit.SECONDS);
instanceInfoReplicator = new InstanceInfoReplicator(this, instanceInfo, clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2);
statusChangeListener = new com.netflix.appinfo.ApplicationInfoManager.StatusChangeListener() {
public String getId()
{
return "statusChangeListener";
}
public void notify(StatusChangeEvent statusChangeEvent)
{
if(com.netflix.appinfo.InstanceInfo.InstanceStatus.DOWN == statusChangeEvent.getStatus() || com.netflix.appinfo.InstanceInfo.InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus())
DiscoveryClient.logger.warn("Saw local status change event {}", statusChangeEvent);
else
DiscoveryClient.logger.info("Saw local status change event {}", statusChangeEvent);
instanceInfoReplicator.onDemandUpdate();
}
final DiscoveryClient this$0;
{
this.this$0 = DiscoveryClient.this;
super();
}
};
if(clientConfig.shouldOnDemandUpdateStatusChange())
applicationInfoManager.registerStatusChangeListener(statusChangeListener);
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else
{
logger.info("Not registering with Eureka server per configuration");
}
}
可以看到在if(clientConfig.shouldRegisterWithEureka())里有一个InstanceInfoReplicator的实例,它会执行一个定时任务,该类的run()函数如下:
public void run()
{
discoveryClient.refreshInstanceInfo();
Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
if(dirtyTimestamp != null)
{
discoveryClient.register();
instanceInfo.unsetIsDirty(dirtyTimestamp.longValue());
}
Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
break MISSING_BLOCK_LABEL_140;
Throwable t;
t;
logger.warn("There was a problem with the instance info replicator", t);
Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
break MISSING_BLOCK_LABEL_140;
Exception exception;
exception;
Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
throw exception;
}
真正触发调用注册的地方就在discoveryClient.register();内容如下:
boolean register()
throws Throwable
{
logger.info((new StringBuilder()).append("DiscoveryClient_").append(appPathIdentifier).append(": registering service...").toString());
EurekaHttpResponse httpResponse;
try
{
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
}
catch(Exception e)
{
logger.warn("{} - registration failed {}", new Object[] {
(new StringBuilder()).append("DiscoveryClient_").append(appPathIdentifier).toString(), e.getMessage(), e
});
throw e;
}
if(logger.isInfoEnabled())
logger.info("{} - registration status: {}", (new StringBuilder()).append("DiscoveryClient_").append(appPathIdentifier).toString(), Integer.valueOf(httpResponse.getStatusCode()));
return httpResponse.getStatusCode() == 204;
}
注册操作是通过REST请求的方式进行的。同时可以看到发起注册请求的时候,传入了一个com.netflix.appinfo.InstanceInfo对象,该对象就是注册时客户端给服务端的元数据。
服务获取与服务续约
DiscoveryClient的initScheduledTasks函数中,还有两个定时任务,分别是服务获取和服务续约:
cacheRefresh和heartbeat
服务注册中心处理
Eureka Server对于各类REST请求的定义都位于com.netflix.eureka.resources包下
配置详解
Eureka客户端的配置主要分为两个方面:
1.服务注册相关的配置信息,包括服务注册中心的地址、服务获取的间隔时间、可用区域等。
2.服务实例相关的配置信息,包括服务实例的名称、IP地址、端口号、健康检查路径等。
服务注册类配置
指定注册中心
通过eureka.client.serviceUrl参数实现。它的配置值存储在HashMap中,并且设置有一组默认值,默认值的key为defaultZone、Value为http://localhost:8761/eureka/
通常配置为:eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
为了服务注册中心的安全考虑,可以加入安全校验,配置如下:
http://<username>:<password>@localhost:1111/eureka/
username为安全校验信息的用户名,password为该用户的密码。
其他配置
EurekaClientConfigBean定义了常用的配置参数,这些参数都以eureka.client为前缀
参数名 | 说明 | 默认值 |
---|---|---|
enabled | 启用Enable客户端 | true |
registryFetchIntervalSeconds | 从Eureka服务器获取注册信息的间隔时间,单位是秒 | 30 |
instanceInfoReplicationIntervalSeconds | 更新实例信息的变化到Eureka服务器的间隔时间,单位是秒 | 30 |
initialInstanceInfoReplicationIntervalSeconds | 初始化实例信息到Eureka服务端的间隔时间,单位是秒 | 40 |
eurekaServiceUrlPollIntervalSeconds | 轮询Eureka服务端地址更改的间隔时间,单位是秒 | 300 |
eurekaServerReadTimeoutSeconds | 读取Eureka Server信息的超时时间,单位是秒 | 8 |
eurekaServerConnectTimeoutSeconds | 连接Eureka Server的超时时间,单位是秒 | 5 |
eurekaServerTotalConnections | 从Eureka客户端到所有Eureka服务端主机的连接总数 | 200 |
eurekaServerTotalConnectionsPerHost | 从Eureka 客户端到每个Eureka服务端主机的连接总数 | 50 |
eurekaConnectionIdleTimeoutSeconds | Eureka服务端链接的空闲关闭时间,单位是秒 | 30 |
heartbeatExecutorThreadPoolSize | 心跳连接池的初始化线程数 | 2 |
heartbeatExecutorExponentialBackOffBound | 心跳超时重试延迟时间的最大乘数值 | 10 |
cacheRefreshExecutorThreadPoolSize | 刷新缓存线程池的初始化线程数 | 2 |
cacheRefreshExecutorExponentialBackOffBound | 缓存刷新重试延迟时间的最大乘数值 | 10 |
useDnsForFetchingServiceUrls | 使用DNS来获取Eureka服务端的serviceURL | false |
registerWithEureka | 是否要将自身的实例信息注册到Eureka服务端 | true |
preferSameZoneEureka | 是否偏好使用处于相同Zone的Eureka服务端 | true |
filterOnlyUpInstances | 获取实例时是否过滤, 仅保留UP状态的实例 | true |
fetchRegistry | 是否从Eureka服务端获取注册信息 | true |
服务实例类配置
实例名配置:
默认使用的主机名,可以通过spring.application.name 或者spring.application.instance_id设置