话不多说,直接上
server端需要实现的功能:
1.接受注册
2.接受心跳
3.服务剔除
4.服务下线
5.集群同步
6.获取注册表中服务实例信息
Ⅰ、启动server 注册相关bean
通过pom依赖和springboot的自动装配,注册外部配置类,在spring-cloud-netflix-eureka-server-2.1.2.REALEASE.jar中的META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration
启动时会自动加载EurekaServerAutoConfiguration类,功能:向spring的bean工厂添加eureka-server相关功能的bean。
但是EurekaServerAutoConfiguration的生效时有条件的;@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
意思是:只有在Spring容器里有Marker这个类的实例时,才会加载EurekaServerAutoConfiguration,这个就是控制是否开启Eureka Server的关键。
Ⅱ、开启eureka server
在主启动类上加入注解@EnableEurekaServer,包含@Import(EurekaServerMarkerConfiguration.class);
意思是:动态注入此bean到spring 容器。引入了EurekaServerMarkerConfiguration.class。此配置类什么也没做,就创建了一个空的Marker类,所以开启了Server服 务。注册了前面说的:EurekaServerAutoConfiguration
Ⅲ、开启注册
在EurekaServerMarkerConfiguration上有@Import(EurekaServerInitializerConfiguration.class),导入了EurekaServerInitializerConfiguration,它实现了 SmartLifeccle接口,SmartLifecycle的作用是:初始化完之后,执行public void start()方法。
在public void start()中,启动一个线程。发布事件:publish(new EurekaRegistryAvailableEvent(getEurekaServerConfig())),告诉client,可以来注册了
上面提到的 log.info("Started Eureka Server") 的上面一行。
1. eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);
2. 点contextInitialized进去,看到initEurekaServerContext,初始化eureka 上下文,
3. 点initEurekaServerContext进去,看到 // Copy registry from neighboring eureka node int registryCount = this.registry.syncUp();从相邻的eureka 节点复制注册表,
4. 下一行openForTraffic(主要是和client 交换信息,traffic),查看实现,PeerAwareInstanceRegistryImpl,
5. 开启任务postInit,进去之后发现剔除功能(剔除 没有续约的服务)。
6. postInit,点进去,发现new EvictionTask(),
7. 点进去,看到run方法中,evict(compensationTimeMs),
8. 点进去就到了,具体剔除逻辑
Ⅳ、PeerAwareInstanceRegistry接口
在EurekaServerAutoConfiguration中 有 public EurekaServerContext eurekaServerContext,中有DefaultEurekaServerContext,点进去找到
@PostConstruct
@Override
public void initialize() {
logger.info("Initializing ...");
peerEurekaNodes.start();
try {
registry.init(peerEurekaNodes);
} catch (Exception e) {
throw new RuntimeException(e);
}
logger.info("Initialized");
}
其中peerEurekaNodes.start();启动一个只拥有一个线程的线程池,第一次进去会更新一下集群其他节点信息。
registry.init(peerEurekaNodes);鼠标放在registry上,发现是PeerAwareInstanceRegistryImpl , 的 注册信息管理类里面的init方法。
PeerAwareInstanceRegistry是个接口,实现类是:PeerAwareInstanceRegistryImpl。
PeerAwareInstanceRegistry接口,实现了com.netflix.eureka.registry.InstanceRegistry
Ⅴ、服务实例注册表
Server是围绕注册表管理的。有两个InstanceRegistry。
一、com.netflix.eureka.registry.InstanceRegistry是euraka server中注册表管理的核心接口。职责是在内存中管理注册到Eureka Server中的服务实例信息。实现类有PeerAwareInstanceRegistryImpl。
二、org.springframework.cloud.netflix.eureka.server.InstanceRegistry对PeerAwareInstanceRegistryImpl进行了继承和扩展,使其适配Spring cloud的使用环境,主要的实现由PeerAwareInstanceRegistryImpl提供。
三、com.netflix.eureka.registry.InstanceRegistry extends LeaseManager<InstanceInfo>, LookupService<String> 。
LeaseManager<InstanceInfo>是对注册到server中的服务实例租约进行管理。
LookupService<String>是提供服务实例的检索查询功能。
四、LeaseManager<InstanceInfo>接口的作用是对注册到Eureka Server中的服务实例租约进行管理,方法有:服务注册,下线,续约,剔除。
此接口管理的类目前是InstanceInfo。InstanceInfo代表服务实例信息。
五、PeerAwareInstanceRegistryImpl 增加了对peer节点的同步复制操作。使得eureka server集群中注册表信息保持一致。
Ⅵ、接受服务注册
Eureka Client在发起服务注册时会将自身的服务实例元数据封装在InstanceInfo中,然后将InstanceInfo发送到Eureka Server。
Eureka Server在接收到Eureka Client发送的InstanceInfo后将会尝试将其放到本地注册表中以供其他Eureka Client进行服务发现。
我们学过:通过 eureka/apps/{服务名}注册
在EurekaServerAutoConfiguration中定义了 public FilterRegistrationBean jerseyFilterRegistration ,表明eureka-server使用了Jersey实现 对外的 restFull接口。注册一个 Jersey 的 filter ,配置好相应的Filter 和 url映射。
public javax.ws.rs.core.Application jerseyApplication()方法:中。
provider.addIncludeFilter(new AnnotationTypeFilter(Path.class));
provider.addIncludeFilter(new AnnotationTypeFilter(Provider.class));
添加一些过滤器,类似于过滤请求地址,Path类似于@RequestMapping,Provider类似于@Controller
在com.netflix.eureka.resources包下,是Eureka Server对于Eureka client的REST请求的定义。
看ApplicationResource类(这是一类请求,应用类的请求),类似于应用@Controller注解:@Produces({"application/xml", "application/json"}),接受xml和json。
见名识意 public Response addInstance。添加实例instanceinfo。
方法中,有一句: registry.register(info, "true".equals(isReplication));
鼠标放在registry上PeerAwareInstanceRegistry接口,点击void register方法。
发现 是PeerAwareInstanceRegistryImpl 的方法:
public void register(final InstanceInfo info, final boolean isReplication) ,中有一句:super.register(info, leaseDuration, isReplication);
进入下面正题: com.netflix.eureka.registry.AbstractInstanceRegistry---> register方法
1、在register中,服务实例的InstanceInfo保存在Lease中,Lease在AbstractInstanceRegistry中统一通过ConcurrentHashMap保存在内存中。
2、在服务注册过程中,会先获取一个读锁,防止其他线程对registry注册表进行数据操作,避免数据的不一致。
3、然后从resgitry查询对应的InstanceInfo租约是否已经存在注册表中,根据appName划分服务集群,使用InstanceId唯一标记服务实例。
4、如果租约存在,比较两个租约中的InstanceInfo的最后更新时间lastDirtyTimestamp,保留时间戳大的服务实例信息InstanceInfo。
5、如果租约不存在,意味这是一次全新的服务注册,将会进行自我保护的统计,创建新的租约保存InstanceInfo。
6、接着将租约放到resgitry注册表中。
7、之后将进行一系列缓存操作并根据覆盖状态规则设置服务实例的状态,缓存操作包括将InstanceInfo加入用于统计Eureka Client增量式获取注册表信息的 recentlyChangedQueue和失效responseCache中对应的缓存。
8、最后设置服务实例租约的上线时间用于计算租约的有效时间,释放读锁并完成服务注册。
Ⅶ、接受心跳 续租,renew
在Eureka Client完成服务注册之后,它需要定时向Eureka Server发送心跳请求(默认30秒一次),维持自己在Eureka Server中租约的有效性。
看另一类请求com.netflix.eureka.resources.InstanceResource。
下public Response renewLease()方法。
看到一行boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode); 点击renew的实现。
进入下面正题:
Eureka Server处理心跳请求的核心逻辑位于AbstractInstanceRegistry#renew方法中。
renew方法是对Eureka Client位于注册表中的租约的续租操作,
不像register方法需要服务实例信息,仅根据服务实例的服务名和服务实例id即可更新对应租约的有效时间。
com.netflix.eureka.registry.AbstractInstanceRegistry
renew //根据appName获取服务集群的租约集合
Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
//查看服务实例状态
InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus( instanceInfo, leaseToRenew, isReplication);
if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
//统计每分钟续租次数
renewsLastMin.increment();
//更新租约
leaseToRenew.renew();
此方法中不关注InstanceInfo,仅关注于租约本身以及租约的服务实例状态。
如果根据服务实例的appName和instanceInfoId查询出服务实例的租约,
并且根据#getOverriddenInstanceStatus方法得到的instanceStatus不为InstanceStatus.UNKNOWN,
那么更新租约中的有效时间,即更新租约Lease中的lastUpdateTimestamp,达到续约的目的;
如果租约不存在,那么返回续租失败的结果。
Ⅷ、服务剔除
如果Eureka Client在注册后,既没有续约,也没有下线(服务崩溃或者网络异常等原因),
那么服务的状态就处于不可知的状态,不能保证能够从该服务实例中获取到回馈,
所以需要服务剔除此方法定时清理这些不稳定的服务,该方法会批量将注册表中所有过期租约剔除。
剔除是定时任务,默认60秒执行一次。延时60秒,间隔60秒
evictionTimer.schedule(evictionTaskRef.get(),
serverConfig.getEvictionIntervalTimerInMs(),
serverConfig.getEvictionIntervalTimerInMs());
从上面eureka server启动来看,剔除的任务,是线程启动的,执行的是下面的方法。
com.netflix.eureka.registry.AbstractInstanceRegistry---> evict
判断是否开启自我保护
if (!isLeaseExpirationEnabled()) { 如果开启自我保护,不剔除。
点进去isLeaseExpirationEnabled,查看实现类,有一个isSelfPreservationModeEnabled,
点进去 @Override public boolean isSelfPreservationModeEnabled() { return serverConfig.shouldEnableSelfPreservation(); },
发现EurekaServerConfig,的方法shouldEnableSelfPreservation,看其实现中有EurekaServerConfigBean,发现属性:enableSelfPreservation。
紧接着一个大的for循环,遍历注册表register,依次判断租约是否过期。一次性获取所有的过期租约。
//获取注册表租约总数
int registrySize = (int) getLocalRegistrySize();
计算注册表租约的阈值 (总数乘以 续租百分比),得出要续租的数量
int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
总数减去要续租的数量,就是理论要剔除的数量
int evictionLimit = registrySize - registrySizeThreshold;
//求上面理论剔除数量,和过期租约总数的最小值。就是最终要提出的数量。
int toEvict = Math.min(expiredLeases.size(), evictionLimit);
然后剔除。用internalCancel(appName, id, false);执行
服务下线将服务从注册表清除掉。
剔除的限制:
1.自我保护期间不清除。
2.分批次清除。
3.服务是逐个随机剔除,剔除均匀分布在所有应用中,防止在同一时间内同一服务集群中的服务全部过期被剔除,造成在大量剔除服务时,并在进行自我保护时,促使程序崩溃。
EurekaServerInitializerConfiguration的 eurekaServerBootstrap.contextInitialized()方法中initEurekaServerContext();
点进去this.registry.openForTraffic(this.applicationInfoManager, registryCount);
点进去,super.postInit();
点进去,evictionTaskRef.set(new EvictionTask());
evictionTimer.schedule(evictionTaskRef.get(),
serverConfig.getEvictionIntervalTimerInMs(),
serverConfig.getEvictionIntervalTimerInMs());
发现定时任务。
剔除服务是个定时任务,用EvictionTask执行,默认60秒执行一次,延时60秒执行。定时剔除过期服务。
服务剔除将会遍历registry注册表,找出其中所有的过期租约,
然后根据配置文件中续租百分比阀值和当前注册表的租约总数量计算出最大允许的剔除租约的数量(当前注册表中租约总数量减去当前注册表租约阀值),
分批次剔除过期的服务实例租约。对过期的服务实例租约调用AbstractInstanceRegistry#internalCancel服务下线的方法将其从注册表中清除掉。
自我保护机制主要在Eureka Client和Eureka Server之间存在网络分区的情况下发挥保护作用,在服务器端和客户端都有对应实现。
假设在某种特定的情况下(如网络故障),Eureka Client和Eureka Server无法进行通信,此时Eureka Client无法向Eureka Server发起注册和续约请求,Eureka Server中就可能因注册表中的服务实例租约出现大量过期而面临被剔除的危险,然而此时的Eureka Client可能是处于健康状态的(可接受服务访问),如果直接将注册表中大量过期的服务实例租约剔除显然是不合理的。
针对这种情况,Eureka设计了“自我保护机制”。在Eureka Server处,如果出现大量的服务实例过期被剔除的现象,那么该Server节点将进入自我保护模式,保护注册表中的信息不再被剔除,在通信稳定后再退出该模式;在Eureka Client处,如果向Eureka Server注册失败,将快速超时并尝试与其他的Eureka Server进行通信。“自我保护机制”的设计大大提高了Eureka的可用性。
|
Ⅸ、服务下线
Eureka Client在应用销毁时,会向Eureka Server发送服务下线请求,清除注册表中关于本应用的租约,避免无效的服务调用。
在服务剔除的过程中,也是通过服务下线的逻辑完成对单个服务实例过期租约的清除工作。
在InstanceResource中,
public Response cancelLease( @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication)
一行代码:boolean isSuccess = registry.cancel(app.getName(), id, "true".equals(isReplication));
点进去cancel,发现:internalCancel(appName, id, isReplication);
查看实现:
先获取读锁,防止被其他线程修改
read.lock();
根据appName获取服务实力集群。
Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
在内存中取消实例 id的服务
if (gMap != null) {
leaseToCancel = gMap.remove(id);
}
添加到最近下线服务的统计队列
synchronized (recentCanceledQueue) {
recentCanceledQueue.add(new Pair<Long, String>(System.currentTimeMillis(), appName + "(" + id + ")"));
}
往下判断leaseToCancel是否为空,租约不存在,返回false, 如果存在, 设置租约下线时间。
leaseToCancel.cancel();
InstanceInfo instanceInfo = leaseToCancel.getHolder();
获取持有租约的服务信息,标记服务实例为instanceInfo.setActionType(ActionType.DELETED);
添加到租约变更记录队列 recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));
用于eureka client的增量拉取注册表信息。
释放锁。
首先通过registry根据服务名和服务实例id查询关于服务实例的租约Lease是否存在,统计最近请求下线的服务实例用于Eureka Server主页展示。
如果租约不存在,返回下线失败;如果租约存在,从registry注册表中移除,设置租约的下线时间,
同时在最近租约变更记录队列中添加新的下线记录,以用于Eureka Client的增量式获取注册表信息。
Ⅹ、集群同步
如果Eureka Server是通过集群的方式进行部署,那么为了维护整个集群中Eureka Server注册表数据的一致性,势必需要一个机制同步Eureka Server集群中的注册表数据。
Eureka Server集群同步包含两个部分,
一部分是Eureka Server在启动过程中从它的peer节点中拉取注册表信息,并将这些服务实例的信息注册到本地注册表中;
另一部分是Eureka Server每次对本地注册表进行操作时,同时会将操作同步到它的peer节点中,达到集群注册表数据统一的目的。
1.启动拉取别的peer
在Eureka Server启动类中:EurekaServerInitializerConfiguration位于EurekaServerAutoConfiguration 的import注解中。
一行:eurekaServerBootstrap.contextInitialized( )
进去:initEurekaServerContext();
点进去,一行:int registryCount = this.registry.syncUp(); 看注释:拉取注册表从邻近节点。
点击syncUp()的实现方法进去: 看循环:
意思是,如果是i第一次进来,为0,不够等待的代码,直接执行下面的拉取服务实例。 将自己作为一个eureka client,拉取注册表。
并通过register(instance, instance.getLeaseInfo().getDurationInSecs(), true)注册到自身的注册表中。
Eureka Server也是一个Eureka Client,在启动的时候也会进行DiscoveryClient的初始化,会从其对应的Eureka Server中拉取全量的注册表信息。在Eureka Server集群部署的情况下,Eureka Server从它的peer节点中拉取到注册表信息后,将遍历这个Applications,将所有的服务实例通过AbstractRegistry#register方法注册到自身注册表中。
int registryCount = this.registry.syncUp();
this.registry.openForTraffic(this.applicationInfoManager, registryCount);
当执行完上面的syncUp逻辑后,在下面的openForTraffic,开启此server接受别的client注册,拉取注册表等操作。
而在它首次拉取其他peer节点时,是不允许client的通信请求的。
在openForTraffic中,
初始化期望client发送过来的服务数量,即上面获取到的服务数量
this.expectedNumberOfClientsSendingRenews = count; updateRenewsPerMinThreshold
点进去,是计算自我保护的统计参数:
this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfClientsSendingRenews * (60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds()) * serverConfig.getRenewalPercentThreshold());
服务数(每个服务每分钟续约次数)阈值
if (count > 0) { this.peerInstancesTransferEmptyOnStartup = false; }
如果count=0,没有拉取到注册表信息,将此值设为true,表示其他peer来取空的实例信息,意味着,将不允许client从此server获取注册表信息。
如果count>0,将此值设置为false,允许client来获取注册表。
后面将服务置为上线,并开启剔除的定时任务。
当Server的状态不为UP时,将拒绝所有的请求。
在Client请求获取注册表信息时,Server会判断此时是否允许获取注册表中的信息。
上述做法是为了避免Eureka Server在#syncUp方法中没有获取到任何服务实例信息时(Eureka Server集群部署的情况下),
Eureka Server注册表中的信息影响到Eureka Client缓存的注册表中的信息。
因为是全量同步,如果server什么也没同步过来,会导致client清空注册表。导致服务调用出问题。
2.Server之间注册表信息的同步复制
为了保证Eureka Server集群运行时注册表信息的一致性,每个Eureka Server在对本地注册表进行管理操作时,会将相应的操作同步到所有peer节点中。
在外部调用server的restful方法时,在com.netflix.eureka.resources包下的ApplicationResource资源中,查看每个服务的操作。
比如服务注册public Response addInstance(),此方法中有 registry.register(info, "true".equals(isReplication));
点进去实现类:replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);这是一种情况。
在PeerAwareInstanceRegistryImpl类中,看其他操作,cancel,renew等中都有replicateToPeers,
此方法中有个peerEurekaNodes,代表一个可同步数据的eureka Server的集合,如果注册表有变化,向此中的peer节点同步。
replicateToPeers方法,它将遍历Eureka Server中peer节点,向每个peer节点发送同步请求。
for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
// If the url represents this host, do not replicate to yourself.
if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
continue;
}
replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
}
此replicateInstanceActionsToPeers方法中,类PeerEurekaNode的实例node的各种方法,cancel,register,等,
用了batchingDispatcher.process(),作用是将同一时间段内,相同服务实例的相同操作将使用相同的任务编号,在进行同步复制的时候,将根据任务编号合并操作,减少同步操作的数量和网络消耗,但是同时也造成了同步复制的延时性,不满足CAP中的C(强一致性)。
所以Eureka,只满足AP。
通过Eureka Server在启动过程中初始化本地注册表信息和Eureka Server集群间的同步复制操作,最终达到了集群中Eureka Server注册表信息一致的目的。
XI、获取注册表中服务实例信息
Eureka Server中获取注册表的服务实例信息主要通过两个方法实现:
AbstractInstanceRegistry#getApplicationsFromMultipleRegions 从多地区获取全量注册表数据,AbstractInstanceRegistry#getApplicationDeltasFromMultipleRegions 从多地区获取增量式注册表数据。
1、全量:
上面讲到从节点复制注册信息的时候,用方法public int syncUp() ,一行
Applications apps = eurekaClient.getApplications();
点进去实现类,有一行 getApplicationsFromAllRemoteRegions();
下面getApplicationsFromMultipleRegions,作用从多个地区中获取全量注册表信息,并封装成Applications返回,
它首先会将本地注册表registry中的所有服务实例信息提取出来封装到Applications中,
再根据是否需要拉取Region的注册信息,将远程拉取过来的Application放到上面的Applications中。
最后得到一个全量的Applications。
2、在前面提到接受服务注册,接受心跳等方法中,都有recentlyChangedQueue.add(new RecentlyChangedItem(lease));
作用是将新变动的服务放到最近变化的服务实例信息队列中,用于记录增量是注册表信息。
getApplicationDeltasFromMultipleRegions,实现了从远处eureka server中获取增量式注册表信息的能力。
在EurekaServer对外restful中,在com.netflix.eureka.resources下,
@GET
public Response getApplication(@PathParam("version") String version,
@HeaderParam("Accept") final String acceptHeader,
@HeaderParam(EurekaAccept.HTTP_X_EUREKA_ACCEPT) String eurekaAccept) {
其中有一句:String payLoad = responseCache.get(cacheKey);
在responseCache初始化的时候,它的构造方法
ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {}中,
Value value = generatePayload(key);
点进去有一句:registry.getApplicationDeltasFromMultipleRegions(key.getRegions()));
从远程获取delta增量注册信息。但是这个只是向client提供,不向server提供,因为server可以通过每次变更自动同步到peer。
获取增量式注册表信息将会从recentlyChangedQueue中获取最近变化的服务实例信息。
recentlyChangedQueue中统计了近3分钟内进行注册、修改和剔除的服务实例信息,
在服务注册AbstractInstanceRegistry#registry、
接受心跳请求AbstractInstanceRegistry#renew和
服务下线AbstractInstanceRegistry#internalCancel等方法中
均可见到recentlyChangedQueue对这些服务实例进行登记,用于记录增量式注册表信息。
#getApplicationsFromMultipleRegions方法同样提供了从远程Region的Eureka Server获取增量式注册表信息的能力。