本文为eureka学习笔记,错误之处请指正。

-----------------------------------------------------------

1、服务生产者是怎么注册到配置中心的
  a、第一步,构造实例信息,用于接下来的注册;
  这个过程是通过EurekaClientConfiguration类中的方法eurekaApplicationInfoManager来实现的,其中的InstanceInfoFactory.create(config)返回了一个实例,这个实例就是创建后的实例信息对象。通过查看create方法的源码可以看到该方法就是将读取的eureka的配置信息放入一个EurekaInstanceConfig,然后用这些配置创建了一个InstanceInfo,注意,这时候是没有注册到服务端的。
  EurekaClientConfiguration的部分代码:

@Configuration
@EurekaClientAutoConfiguration.ConditionalOnMissingRefreshScope
protected static class EurekaClientConfiguration {
//省略部分代码
@Bean(destroyMethod = "shutdown")
@ConditionalOnMissingBean(value = {EurekaClient.class},search = SearchStrategy.CURRENT)
public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientConfig config) {//用于跟服务器交互的客户端实例,实际是DiscoveryClient的一个子类(spring cloud对eureka自己进行了一些封装)
return new CloudEurekaClient(manager, config, this.optionalArgs, this.context);
}
@Bean
@ConditionalOnMissingBean( value = {ApplicationInfoManager.class}, search = SearchStrategy.CURRENT ) //在没有ApplicationInfoManager的实例的时候创建
public ApplicationInfoManager eurekaApplicationInfoManager(EurekaInstanceConfig config) {
InstanceInfo instanceInfo = (new InstanceInfoFactory()).create(config); //在这里创建了instanceInfo
return new ApplicationInfoManager(config, instanceInfo);
}
}
  创建过程:
public InstanceInfo create(EurekaInstanceConfig config) {
Builder leaseInfoBuilder = Builder.newBuilder().setRenewalIntervalInSecs(config.getLeaseRenewalIntervalInSeconds()).setDurationInSecs(config.getLeaseExpirationDurationInSeconds());
com.netflix.appinfo.InstanceInfo.Builder builder = com.netflix.appinfo.InstanceInfo.Builder.newBuilder();
builder.setNamespace(namespace).setAppName(config.getAppname()) // 这里用config的信息进行了大量的设置
.setInstanceId(config.getInstanceId())
.setAppGroupName(config.getAppGroupName())
.setDataCenterInfo(config.getDataCenterInfo())
.setIPAddr(config.getIpAddress()).setHostName(config.getHostName(false))
.setPort(config.getNonSecurePort())
.enablePort(InstanceInfo.PortType.UNSECURE,
config.isNonSecurePortEnabled())
.setSecurePort(config.getSecurePort())
.enablePort(InstanceInfo.PortType.SECURE, config.getSecurePortEnabled())
.setVIPAddress(config.getVirtualHostName())
.setSecureVIPAddress(config.getSecureVirtualHostName())
.setHomePageUrl(config.getHomePageUrlPath(), config.getHomePageUrl())
.setStatusPageUrl(config.getStatusPageUrlPath(),
config.getStatusPageUrl())
.setHealthCheckUrls(config.getHealthCheckUrlPath(),
config.getHealthCheckUrl(), config.getSecureHealthCheckUrl())
.setASGName(config.getASGName());
// 省略代码若干...
return instanceInfo;
}

  其中,方法参数EurekaInstanceConfig的实际实现是EurekaInstanceConfigBean,类注解如下:

@ConfigurationProperties("eureka.instance")//读取配置文件中前缀为eureka.instance的内容
public class EurekaInstanceConfigBean implements CloudEurekaInstanceConfig, EnvironmentAware {
private static final String UNKNOWN = "unknown";
private HostInfo hostInfo;
private InetUtils inetUtils;
......
}
  类似的配置信息类还有:EurekaClientConfigBean(客户端配置信息),EurekaServerConfigBean(服务端配置信息)等;
  b、利用初始化的discoveryClient,将实例信息注册到server端(如果配置文件中声明要注册的话)
  DiscoveryClient类在创建的时候会执行initScheduledTasks()方法进行初始化,显然这个方法是构建了一系列定时任务,其中一段如下:
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);
//省略代码若干......
if (clientConfig.shouldOnDemandUpdateStatusChange()) {
applicationInfoManager.registerStatusChangeListener(statusChangeListener);
}
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
}

  我们看到该方法启动了一个线程,new HeartbeatThread的run方法代码如下:

private class HeartbeatThread implements Runnable {
public void run() {
if (renew()) { //这个renew实际就是心跳检测的过程
lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
}
}
}

  renew方法代码:

boolean renew() {
EurekaHttpResponse<InstanceInfo> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);//发送心跳请求
logger.debug("{} - Heartbeat status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
if (httpResponse.getStatusCode() == 404) {//返回状态码400,不存在,说明服务端没有信息,要进行注册操作
REREGISTER_COUNTER.increment();
logger.info("{} - Re-registering apps/{}", PREFIX + appPathIdentifier, instanceInfo.getAppName());
return register();//进行注册
}
return httpResponse.getStatusCode() == 200;//服务端正常返回,心跳检测成功
} catch (Throwable e) {
logger.error("{} - was unable to send heartbeat!", PREFIX + appPathIdentifier, e);
return false;
}
}
2、服务端注册过程

  spring cloud中eureka的client跟server端都使用jersey进行交互,在server端,EurekaServerAutoConfiguration中的jerseyFilterRegistration方法动态注册了一个Jersey filter,用来拦截所有请求。具体对请求的处理是由ApplicationResource类的addInstance方法来完成的。

@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info, @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
// validate that the instanceinfo contains all the necessary required fields
//参数校验代码 // handle cases where clients may be registering with bad DataCenterInfo with missing data
//高可用部分代码 registry.register(info, "true".equals(isReplication));
return Response.status(204).build(); // 204 to be backwards compatible
}

  父类InstanceRegistry的register是具体的执行代码,这个register方法干了两件事儿:1、发布了一个事件,2、进行了一个注册操作。再父类PeerAwareInstanceRegistryImpl的主要功能是将注册操作分发到eureka server集群的其它节点,以保持数据的一致性。继续一步步点击进入,最终的注册由AbstractInstanceRegistry类的register完成,部分代码如下:

private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry  = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>(); //这个是map里边套了个map,键为服务名称
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
read.lock();
Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
REGISTER.increment(isReplication);
if (gMap == null) {
final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();//这个是内部的map,键为示例名称,值为实例的具体信息
gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
if (gMap == null) {
gMap = gNewMap;
}
}
//其它代码
}

  可以看到,最终服务端维护了一个ConcurrentHashMap,key为服务名值为该服务的实例组成的一个map,子map中key为实例名。

3、集群节点间信息同步

  eureka中没有主从的概念,所有节点都是平等的。其数据传播依赖于“对等机制”(peer2peer)。
  EurekaServerAutoConfiguration类中有一个上下文初始化的bean--eurekaServerContext,该bean的初始化方法如下:
@PostConstruct
@Override
public void initialize() throws Exception {
logger.info("Initializing ...");
peerEurekaNodes.start(); //服务启动,刷新peer集群列表并启动更新任务
registry.init(peerEurekaNodes);
logger.info("Initialized");
}

  其中start方法主要是启动了一个线程,每隔一段时间执行一次updatePeerEurekaNodes(List<String> newPeerUrls)方法,该方法主要作用是更新集群节点信息,添加新节点,删除关闭的节点。

  在服务端完成注册之后,PeerAwareInstanceRegistryImpl的register方法中:
public void register(final InstanceInfo info, final boolean isReplication) {
int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
leaseDuration = info.getLeaseInfo().getDurationInSecs();
}
super.register(info, leaseDuration, isReplication);
replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication); //复制注册信息到其它节点,isReplication用于防止复制自身,造成集群间节点相互复制
}

  当然,信息的同步过程,不止在注册发生,在服务的续约(renew),下架(cancle)均会进行同步。  

  续约:从服务提供者发起的定时任务,类似于心跳机制,隔一段时间调用一次服务端接口,告诉服务端它还活着,不要把它T了。
  下架:服务提供者自己shut down的时候,通知服务端把自己剔除,避免客户端拿到不可用的服务。
  剔除:服务端判断服务不可用,将服务从服务端删除。
4、服务的剔除及下架
  服务剔除是指eureka server认为服务不可用,进行剔除;下架指客户端关闭服务时,向服务端发送关闭请求。下架主要是client端调用DiscoveryClient的shutdown方法以及unregister方法。以下是服务剔除内容:
  EurekaServerInitializerConfiguration的start方法中有一行eurekaServerBootstrap.contextInitialized,其中方法内的initEurekaServerContext有registry.openForTraffic,具体代码实现者peerAwareInstanceRegistryImpl的openForTraffic最后一行super.postInit(),实现中new EvictionTask中的run中的evict(),,,,,,,终于到了剔除服务的代码了!

public void evict(long additionalLeaseMs) {
//其它代码
// We collect first all expired items, to evict them in random order. For large eviction sets,
// if we do not that, we might wipe out whole apps before self preservation kicks in. By randomizing it,
// the impact should be evenly distributed across all applications.
List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) {
Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue();
if (leaseMap != null) {
for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) {
Lease<InstanceInfo> lease = leaseEntry.getValue();
if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) { //判断服务失效
expiredLeases.add(lease);
}
}
}
}
//其它代码
}
  isExpired代码:
public boolean isExpired(long additionalLeaseMs) {
return (evictionTimestamp > 0 || System.currentTimeMillis() > (lastUpdateTimestamp + duration + additionalLeaseMs)); //最后更新时间+lease的存活时间+补偿时间< 当前时间
}

5、自我保护机制

  自我保护机制不会剔除服务。
  三个数值:
  期望值:正常每分钟心跳数量,所有服务的实例数*2(因为默认30秒心跳一次)
  保护值:进入自我保护机制的数值,心跳数少于该数值则进入保护机制。期望值*阈值百分比
  阈值:进入保护机制的一个设置值
  源码在PeerAwareInstanceRegistryImpl#isLeaseExpirationEnabled
6、消费者获取信息
  实际就是客户端有个定时任务去服务端获取信息并进行缓存的过程;
  DiscoveryClient类的initScheduledTasks的new CacheRefreshThread---->refreshRegistry---->EurekaHttpClient类的getApplications:
  其中,refreshRegistry中有拉取服务信息的操作fetchRegistry,该操作会根据客户端的配置决定是拉取全量信息还是增量信息
private boolean fetchRegistry(boolean forceFullRegistryFetch) {
Stopwatch tracer = FETCH_REGISTRY_TIMER.start();
try {
// If the delta is disabled or if it is the first time, get all applications
Applications applications = getApplications();
if (clientConfig.shouldDisableDelta() || (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress())) || forceFullRegistryFetch || (applications == null) || (applications.getRegisteredApplications().size() == 0) || (applications.getVersion() == -1)) {//Client application does not have latest library supporting delta
logger.info("Disable delta property : {}", clientConfig.shouldDisableDelta());
logger.info("Single vip registry refresh property : {}", clientConfig.getRegistryRefreshSingleVipAddress());
logger.info("Force full registry fetch : {}", forceFullRegistryFetch);
logger.info("Application is null : {}", (applications == null));
logger.info("Registered Applications size is zero : {}", (applications.getRegisteredApplications().size() == 0));
logger.info("Application version is -1: {}", (applications.getVersion() == -1));
getAndStoreFullRegistry();//全量数据
} else {
getAndUpdateDelta(applications);//增量数据
}
applications.setAppsHashCode(applications.getReconcileHashCode());
logTotalInstances();
} catch (Throwable e) {
logger.error(PREFIX + appPathIdentifier + " - was unable to refresh its cache! status = " + e.getMessage(), e);
return false;
} finally {
if (tracer != null) {
tracer.stop();
}
}
// Notify about cache refresh before updating the instance remote status
onCacheRefreshed();
// Update remote status based on refreshed data held in the cache
updateInstanceRemoteStatus();
// registry was fetched successfully, so return true
return true;
}

代码看出,根据disable-delta的值以及是否是初次获取数据来判断是否全量获取数据,如果disable-delta为true(禁用delta增量更新),则以后每次获取均为全量获取,默认为false,也就是除了第一次,以后都是增量获取数据。进入getAndStoreFullRegistry,再进入获取httpResponse的eurekaTransport.queryClient.getApplications(remoteRegionsRef.get(),跟踪可进入SessionedEurekaHttpClient的execute方法:

protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
long now = System.currentTimeMillis();
long delay = now - lastReconnectTimeStamp;
if (delay >= currentSessionDurationMs) { //这里判断超时就创建新的session,实际是定期创建session,目的是防止一个客户端一直连接某个server,在server增加时不利于负载均衡的处理
logger.debug("Ending a session and starting anew");
lastReconnectTimeStamp = now;
currentSessionDurationMs = randomizeSessionDuration(sessionDurationMs);
TransportUtils.shutdown(eurekaHttpClientRef.getAndSet(null));
}
//省略代码
return requestExecutor.execute(eurekaHttpClient); //还得进入看实现
}
        继续跟踪,进入RetryableEurekaHttpClient的execute方法,在这里可以看到读取了配置文件内容,拿到了eureka-server的url,经过一系列跳转最终进入了AbstractJerseyEurekaHttpClient的getApplicationsInternal方法,在这里发送了请求并获取了返回结果。

        返回DiscoveryClient的getAndStoreFullRegistry方法,在拿到httpResponse并获取返回结果后执行了filterAndShuffle:
private Applications filterAndShuffle(Applications apps) {
if (apps != null) {
if (isFetchingRemoteRegionRegistries()) {
Map<String, Applications> remoteRegionVsApps = new ConcurrentHashMap<String, Applications>();
apps.shuffleAndIndexInstances(remoteRegionVsApps, clientConfig, instanceRegionChecker);
for (Applications applications : remoteRegionVsApps.values()) {
applications.shuffleInstances(clientConfig.shouldFilterOnlyUpInstances());
}
this.remoteRegionVsApps = remoteRegionVsApps;
} else {
apps.shuffleInstances(clientConfig.shouldFilterOnlyUpInstances());
}
}
return apps;
} 
  这里看名字就知道,过滤并洗牌操作,就是把获取的实例顺序打乱,目的是防止同一个实例总是出现在某个位置造成负载的时候偏重不均匀;过滤这个操作在嵌套的方法中,最终代码在Applications的shuffleAndFilterInstances方法,可以看到只保留了状态为UP的实例。
  洗牌之后,通过addInstancesToVIPMaps调用addInstanceToMap将实例添加到本地缓存map: appNameApplicationMap中。

7、心跳机制
   DiscoveryClient 内部类HeartBeatThread:
private class HeartbeatThread implements Runnable {
public void run() {
if (renew()) {//可以看到,就是调用了renew方法
lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
}
}
}

----------------------------------------------

 
 

eureka 源码的更多相关文章

  1. Eureka源码探索(一)-客户端服务端的启动和负载均衡

    1. Eureka源码探索(一)-客户端服务端的启动和负载均衡 1.1. 服务端 1.1.1. 找起始点 目前唯一知道的,就是启动Eureka服务需要添加注解@EnableEurekaServer,但 ...

  2. Eureka 源码编译 部署

    Netflix开源的Eureka 是使用Gradle 构建的,所以我们也使用Gradle来编译它   所需环境 Eclipse , Gradle , Tomcat ,git 这些插件如果己经安装可直接 ...

  3. 微服务之SpringCloud实战(四):SpringCloud Eureka源码分析

    Eureka源码解析: 搭建Eureka服务的时候,我们会再SpringBoot启动类加上@EnableEurekaServer的注解,这个注解做了一些什么,我们一起来看. 点进@EnableEure ...

  4. 【一起学源码-微服务】Netflix Eureka 源码一:Netflix Eureka 源码初探,我们为什么要读源码?

    前言 最近发现 网上好多自己的博客,很多朋友转载了文章却不加下 原载地址,本文欢迎转载一起学习,请在目录出加上原出处,感谢.转载来自:博客(一枝花算不算浪漫) 看了前面几篇文章的小伙伴知道,前几天在学 ...

  5. 【一起学源码-微服务】Nexflix Eureka 源码二:EurekaServer启动之配置文件加载以及面向接口的配置项读取

    前言 上篇文章已经介绍了 为何要读netflix eureka源码了,这里就不再概述,下面开始正式源码解读的内容. 如若转载 请标明来源:一枝花算不算浪漫 代码总览 还记得上文中,我们通过web.xm ...

  6. Eureka 源码分析之 Eureka Server

    文章首发于公众号<程序员果果> 地址 : https://mp.weixin.qq.com/s/FfJrAGQuHyVrsedtbr0Ihw 简介 上一篇文章<Eureka 源码分析 ...

  7. 【一起学源码-微服务】Nexflix Eureka 源码六:在眼花缭乱的代码中,EurekaClient是如何注册的?

    前言 上一讲已经讲解了EurekaClient的启动流程,到了这里已经有6篇Eureka源码分析的文章了,看了下之前的文章,感觉代码成分太多,会影响阅读,后面会只截取主要的代码,加上注释讲解. 这一讲 ...

  8. 【一起学源码-微服务】Nexflix Eureka 源码十:服务下线及实例摘除,一个client下线到底多久才会被其他实例感知?

    前言 前情回顾 上一讲我们讲了 client端向server端发送心跳检查,也是默认每30钟发送一次,server端接收后会更新注册表的一个时间戳属性,然后一次心跳(续约)也就完成了. 本讲目录 这一 ...

  9. 【一起学源码-微服务】Nexflix Eureka 源码十二:EurekaServer集群模式源码分析

    前言 前情回顾 上一讲看了Eureka 注册中心的自我保护机制,以及里面提到的bug问题. 哈哈 转眼间都2020年了,这个系列的文章从12.17 一直写到现在,也是不容易哈,每天持续不断学习,输出博 ...

  10. 【一起学源码-微服务】Nexflix Eureka 源码十三:Eureka源码解读完结撒花篇~!

    前言 想说的话 [一起学源码-微服务-Netflix Eureka]专栏到这里就已经全部结束了. 实话实说,从最开始Eureka Server和Eureka Client初始化的流程还是一脸闷逼,到现 ...

随机推荐

  1. css笔记-1

    id 优先级大于 class 行间 style  优先级大于id class 和属性是并行的 !important > 行间样式 >id >class|属性 >标签选择器 &g ...

  2. 关于C#中的算术运算

    使用中间变量交换两个int型变量的值: ; ; a = a+b; b = a-b; a = a-b; 相信大家很容易写出来,但考虑到边界值情况时会有一些有趣的事情. 我们知道有一个int.MaxVal ...

  3. WSMBT Modbus & WSMBS Modbus 控件及注册机

    先上注册机 点击下载 How to add the WSMBT control to the toolbox: On the Tools menu, click Choose Toolbox Item ...

  4. CentOS6.5上Zabbix3.0的RPM安装【一】-安装并配置Server

    一.Environment OS:CentOS6.5 64bit [桌面版安装] Server端:192.168.201.109 ServerName Clinet端:192.168.201.199 ...

  5. AB二进制

    Description 若将一个正整数化为二进制数,在此二进制数中,我们将数字1的个数多于数字0的个数的这类二进制数称为A类数,否则就称其为B类数. 例如: (13)10=(1101)2        ...

  6. P2866 [USACO06NOV]糟糕的一天Bad Hair Day

    题意:给你一个序列,问将序列倒过来后,对于每个点,在再碰到第一个比它大的点之前,有多少比它小的? 求出比它小的个数的和 样例: 610374122 output: 5 倒序后:2    12    4 ...

  7. 2019年GPLT L2-3 深入虎穴 比赛题解 中国高校计算机大赛-团体程序设计天梯赛题解

    著名的王牌间谍 007 需要执行一次任务,获取敌方的机密情报.已知情报藏在一个地下迷宫里,迷宫只有一个入口,里面有很多条通路,每条路通向一扇门.每一扇门背后或者是一个房间,或者又有很多条路,同样是每条 ...

  8. clang -O3 for循环的LLVM IR

    O3都是怪物,这里分析的是CLANG怪物,示例程序遍历数组每个元素然后放大. void foreach_scale(int arr[],int elem){ for(int i=0;i<elem ...

  9. php暂停函数sleep()和usleep的区别

    在PHP中暂停代码执行一定时间,有两个函数可以实现,一个是sleep(),另一个是usleep(),它们参数都是一个整数值.sleep()是暂停多少秒,usleep()是暂停多少微秒. 注意:usle ...

  10. Android studio 混淆打包

    AndroidStudio中的项目可以用compile的形式引入github上的开源项目,可以引用module,而不一定都要用libs文件夹中添加jar包的形式. 在最终realease打包时,混淆的 ...