首先我们用EurekaClient 就必须用到图上的两个注解之一。这两个注解有什么关系呢?
两个注解
这边很明显的 EnableEurekaClient 注解里面包含了EnableDiscoveryClient注解
其他的注解和EnableDiscoveryClient 中的一样, 这样我是否可以理解为只要看EnableDiscoveryClient 就行了。
这里网上看过上面两个注解在使用eureka 作为注册中心是没什么区别的,EnableEurekaClient只能用于eureka。
CAP定理,eureka的优点,与dubbo 之间的一些差异
EnableDiscoveryClient 可以用eureka(ap),zk(cp),consoul(ca)等等。zk 具有强一致性, 但是当master死掉的时候需要等重新选举出一个master 才能继续工作。eureka具有高可用性, 每一个eurekaserver 都可以单独使用的,他们之间是靠copy replicate 来进行数据更新的,当一个server挂掉之后,我们可以直接从别的server拿取信息。这里我想写一下dubbo 使用zk和 sb 集成eureka的心跳区别。dubbo 是cp之间的心跳具体我们看源码中netty中的url 指向的是provoid ,然后有个心跳。eureka 是c 和s 之间的心跳, 好像是在续租那块。
dubbo的服务在注册中心死亡后还是能够调用,这是因为dubbo 会将zk 上的信息存一份在本地, 调用的时候也是调用的本地的,当有一个新服务注册到zk上的时候,此服务先从zk 上拉出各个client 的信息, 而zk会将此服务的接口推送给其他已经注册到zk上的服务。注册中心挂掉还能调用的解析
为什么会加载@EnableDiscoveryClient到ioc 中呢?具体网上搜索spring.factories 文件的作用
一般看注解的会执行那个类 我们只需要忽略掉Enable 就行。
源码解析
所以我们crtl+shift+t搜索 DiscoveryClient 。这时会搜索到两个 一个是类一个是接口, 我们选择哪个呢? 看两个类的报名发现接口类的包名和注解是一样的,所以我们选择接口。
crtl+T我们看到下图所示:
我们看的是Eureka啊 , 直接无脑点EurekaDiscoveryClient, 进入里面我们会看到可爱的EurekaClient 作为EurekaDiscoveryClient的一个属性,这个时候crtl+t 会来到DiscoveryClient 这个类中, 注意不要和上面同名的接口搞混。当DiscoveryClient被new出来的时候,找到Discovery的构造方法,我打的断点里面 会首先走到下图的代码大概是DiscoveryClient的第371行左右:
// default size of 2 - 1 each for heartbeat and cacheRefresh
scheduler = Executors.newScheduledThreadPool(2,
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-%d")
.setDaemon(true)
.build());
heartbeatExecutor = new ThreadPoolExecutor(
1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
.setDaemon(true)
.build()
); // use direct handoff
cacheRefreshExecutor = new ThreadPoolExecutor(
1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
.setDaemon(true)
.build()
); // use direct handoff
首先这边会新建个调度器核心线程数为2,后面的参数为构建模式建立。后面新建了两个Executor执行器。顾名思义一个心跳,一个清缓存。后面的很多方法我没有去看,最主要的是这个方法initScheduledTasks() (code6);
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");
}
}
registryFetchIntervalSeconds这个是每过多长时间去EurekaServer 上去拿实例 ,expBackOffBound为缓存刷新重试延迟时间的最大乘数值,具体怎么使用我们进入TimedSupervisorTask,这个类的初始化参数的意思 我们可以将鼠标放置上面查看:
当我们进入之后(code7):
public TimedSupervisorTask(String name, ScheduledExecutorService scheduler, ThreadPoolExecutor executor,
int timeout, TimeUnit timeUnit, int expBackOffBound, Runnable task) {
this.scheduler = scheduler;
this.executor = executor;
this.timeoutMillis = timeUnit.toMillis(timeout);
this.task = task;
this.delay = new AtomicLong(timeoutMillis);
this.maxDelay = timeoutMillis * expBackOffBound;
// Initialize the counters and register.
timeoutCounter = Monitors.newCounter("timeouts");
rejectedCounter = Monitors.newCounter("rejectedExecutions");
throwableCounter = Monitors.newCounter("throwables");
threadPoolLevelGauge = new LongGauge(MonitorConfig.builder("threadPoolUsed").build());
Monitors.registerObject(name, this);
}
maxDelay是10*毫秒, 还有Monitors 来监控超时,异常等的数量,delay 是AtomicLong 说明这个是原子的,在同一个时间只能有一个线程对他进行操作。我们再看看他的run()方法就特别的有意思:
public void run() {
Future future = null;
try {
future = executor.submit(task);
threadPoolLevelGauge.set((long) executor.getActiveCount());
future.get(timeoutMillis, TimeUnit.MILLISECONDS); // block until done or timeout
delay.set(timeoutMillis);
threadPoolLevelGauge.set((long) executor.getActiveCount());
} catch (TimeoutException e) {
logger.error("task supervisor timed out", e);
timeoutCounter.increment();
long currentDelay = delay.get();
long newDelay = Math.min(maxDelay, currentDelay * 2);
delay.compareAndSet(currentDelay, newDelay);
} catch (RejectedExecutionException e) {
if (executor.isShutdown() || scheduler.isShutdown()) {
logger.warn("task supervisor shutting down, reject the task", e);
} else {
logger.error("task supervisor rejected the task", e);
}
rejectedCounter.increment();
} catch (Throwable e) {
if (executor.isShutdown() || scheduler.isShutdown()) {
logger.warn("task supervisor shutting down, can't accept the task");
} else {
logger.error("task supervisor threw an exception", e);
}
throwableCounter.increment();
} finally {
if (future != null) {
future.cancel(true);
}
if (!scheduler.isShutdown()) {
scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS);
}
}
}
future 说明是异步的执行任务,如果出现异常我们就会加大延迟时间 最大不会草果maxDelay,并且Counter类型会+1,最有意思的是这个finally 他继续的调用 调度器,使得调度器能够不停的执行下去。
getDiscoveryServiceUrls()这个方法,继续走会调到EndpointUtil中的getServiceUrlsFromConfig()方法,进入之后(code1)
ist<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;
}
首先 我们看到getRegion()方法,clientConfig他是根据application.yml 配置来获取配置信息的如果我们配置了就取出来, 如果没有我们就返回一个默认值具体看如下代码(code2):
public static String getRegion(EurekaClientConfig clientConfig) {
String region = clientConfig.getRegion();
if (region == null) {
region = DEFAULT_REGION;
}
region = region.trim().toLowerCase();
return region;
}
我们在看看code1代码块中的这句话:
String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
点进去(code3):
public String[] getAvailabilityZones(String region) {
String value = this.availabilityZones.get(region);
if (value == null) {
value = DEFAULT_ZONE;
}
return value.split(",");
}
@Override
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<>();
}
我们看到如果配置文件里面不配置的话,会返回值为DEFAULT_ZONE,如果赋值了, 这边会用,分割成一个数组, 说明我们可以配置多个地址并且用逗号分割。如果返回Default_zone的时候 serviceUrls会根据这个key 取值 我找到了 这一串代码(code4)
private Map<String, String> serviceUrl = new HashMap<>();
{
this.serviceUrl.put(DEFAULT_ZONE, DEFAULT_URL);
}
所以这个Default_url是什么呢?我们继续跟踪得到(code5);
public static final String DEFAULT_URL = "http://localhost:8761" + DEFAULT_PREFIX
+ "/";
public static final String DEFAULT_PREFIX = "/eureka";
这边也就很好的解释了。为什么不去配置 eurekaClient 为什么能够连接本地的8761端口的EurekaServer.(先去跑步有空再写)