源码分析:
先从注解开始入手
1、org.springframework.cloud.client.discovery.EnableDiscoveryClient
@EnableDiscoveryClient注解 (将应用注册到Eureka Server或从Eureka Server中获取服务列表)
作用:用来开启DiscoveryClient的实例
/**
* Annotation to enable a DiscoveryClient implementation.
* @author Spencer Gibb
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(EnableDiscoveryClientImportSelector.class)
public @interface EnableDiscoveryClient {
}
2、接口 DiscoveryClient (org.springframework.cloud.client.discovery.DiscoveryClient)
作用:它定义了用来发现服务的常用抽象方法,通过该接口可以有效的屏蔽服务治理的实现细节。
package org.springframework.cloud.client.discovery; /** * Represents read operations commonly available to discovery services such as Netflix Eureka or consul.io. */ public interface DiscoveryClient extends Ordered { ...... } |
它的实现类 :
org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient org.springframework.cloud.kubernetes.discovery.KubernetesDiscoveryClient org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClient org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient |
3、EurekaDiscoveryClient
package org.springframework.cloud.netflix.eureka; import com.netflix.discovery.EurekaClient; import com.netflix.discovery.EurekaClientConfig; public class EurekaDiscoveryClient implements DiscoveryClient { public static final String DESCRIPTION = "Spring Cloud Eureka Discovery Client" ; private final EurekaClient eurekaClient; private final EurekaClientConfig clientConfig; ...... ...... public List<ServiceInstance> getInstances(String serviceId) {....} public List<String> getServices() {......} } |
通过看其源码,EurekaDiscoveryClient都是通过调用底层netflix包中的类来实现功能,下面看对应类的源码
4、EurekaClient (com.netflix.discovery.EurekaClient)
package com.netflix.discovery; /** * Define a simple interface over the current DiscoveryClient implementation. * EurekaClient API contracts are: * - provide the ability to get InstanceInfo(s) (in various different ways) * - provide the ability to get data about the local Client (known regions, own AZ etc) * - provide the ability to register and access the healthcheck handler for the client */ @ImplementedBy (DiscoveryClient. class ) public interface EurekaClient extends LookupService { } |
既然看到了这,当然接着看 DiscoveryClient了
5、DiscoveryClient (com.netflix.discovery.DiscoveryClient)(也可以叫 Eureka Client )
作用:这个类用于和Eureka Server相互协调,主要功能包括:
向Eureka Server注册服务实例
向Eureka Server服务租约
当服务关闭期间,向Eureka Server取消服务
查询Eureka Server中的服务实例列表
Eureka Client 还需要配置一个Eureka Server的URL列表
5.1 获取服务列表:
/** * @deprecated see replacement in {@link com.netflix.discovery.endpoint.EndpointUtils} * * Get the list of all eureka service urls from properties file for the eureka client to talk to. * * @param instanceZone The zone in which the client resides * @param preferSameZone true if we have to prefer the same zone as the client, false otherwise * @return The list of all eureka service urls for the eureka client to talk to */ @Deprecated @Override public List<String> getServiceUrlsFromConfig(String instanceZone, boolean preferSameZone) { return EndpointUtils.getServiceUrlsFromConfig(clientConfig, instanceZone, preferSameZone); } |
接着看:EndpointUtils.getServiceUrlsFromConfig
分析:客户端依次加载两个内容:Region 和 Zone
Region : 只有一个region,即通过getRegion函数从配置文件中读取一个Region返回。所以一个微服务应用只可以属于一个Region, 默认为default, 另外可以通过eureka.client.region设置
Zone : 可以多个zone, 即通过getAvailabilityZones函数返回的集合。默认是defaultZone, 可以通过eureka.client.availability-zones属性设置.
serviceUrls : 获取了Region和Zone后,才真正的加载Eureka Server的具体地址。getEurekaServerServiceUrls函数,根据传入的参数按照一定的算法确定加载位于哪个Zone配置的serviceUrls
/** * Get the list of all eureka service urls from properties file for the eureka client to talk to. */ 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; } // 按照一定的算法确定加使用哪个zone int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones); List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[myZoneOffset]); ........ ........ return orderedUrls; } |
通过看clientConfig.getEurekaServerServiceUrls函数,就可以知道,eureka.client.serviceUrl.defaultZone属性可以配置多个,并且已逗号分割
补充说明:clientConfig采用实现类应该是 DefaultEurekaClientConfig,可以返现它的getEurekaServerServiceUrls函数对配置的defaultZone进行了按照逗号分割,但是并没有对每个值进行加 / 的处理
5.2、服务注册
通过 initScheduledTask方法
/** * Initializes all scheduled tasks. */ private void initScheduledTasks() { if (clientConfig.shouldRegisterWithEureka()) { int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs(); int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound(); logger.info( "Starting heartbeat executor: " + "renew interval is: {}" , renewalIntervalInSecs); ...... // InstanceInfo replicator instanceInfoReplicator = new InstanceInfoReplicator( this , instanceInfo, clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2 ); // burstSize ...... ...... instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds()); } else { logger.info( "Not registering with Eureka server per configuration" ); } } |
然后看 InstanceInfoReplicator 中的run方法:
package com.netflix.discovery; /** * A task for updating and replicating the local instanceinfo to the remote server. Properties of this task are: * - configured with a single update thread to guarantee sequential update to the remote server * - update tasks can be scheduled on-demand via onDemandUpdate() * - task processing is rate limited by burstSize * - a new update task is always scheduled automatically after an earlier update task. However if an on-demand task * is started, the scheduled automatic update task is discarded (and a new one will be scheduled after the new * on-demand update). * * @author dliu */ class InstanceInfoReplicator implements Runnable { 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(); ok, 看看它是如何注册的
/** * Register with the eureka service by making the appropriate REST call. */ boolean register() throws Throwable { logger.info(PREFIX + "{}: registering service..." , appPathIdentifier); EurekaHttpResponse<Void> httpResponse; try { httpResponse = eurekaTransport.registrationClient.register(instanceInfo); } catch (Exception e) { logger.warn(PREFIX + "{} - registration failed {}" , appPathIdentifier, e.getMessage(), e); throw e; } if (logger.isInfoEnabled()) { logger.info(PREFIX + "{} - registration status: {}" , appPathIdentifier, httpResponse.getStatusCode()); } return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode(); } |
通过命名可以猜出,注册操作是通过REST请求的方式进行的。同时,发起注册请求的时候,传入的对象是com.netflix.appinfo.InstanceInfo对象,也就是该对象就是注册时,客户端给服务端的服务元数据。
5.3 服务获取与服务续约
服务获取、服务续约、以及刚才分析的服务注册都在方法 initScheduledTask中
initScheduledTask有两个if判断:
// 服务获取
if (clientConfig.shouldFetchRegistry()) {。。。}
// 服务注册 + 服务续约
if (clientConfig.shouldRegisterWithEureka()) {。。。}
上面已经分析了下面的if, 下面在分析上面这个if(服务获取)
5.3.1. 服务获取
private void initScheduledTasks() { if (clientConfig.shouldFetchRegistry()) { // 对应配置文件的属性 eureka.client.fetch-registry=true,默认为true // registry cache refresh timer(注册缓冲的刷新) // 对应配置文件的eureka.client.registry-fetch-interval-seconds=30 int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds(); int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound(); scheduler.schedule( new TimedSupervisorTask( "cacheRefresh" , scheduler, cacheRefreshExecutor, registryFetchIntervalSeconds, TimeUnit.SECONDS, expBackOffBound, new CacheRefreshThread() ), registryFetchIntervalSeconds, TimeUnit.SECONDS); } ....... ....... } |
服务获取:为了定期的更新客户端的服务清单,以保证客户端能够访问到健康的服务实例,『服务获取』的请求不仅只在服务启动的时候获取,而是有个定时任务
服务获取会根据是否是第一次获取发起不同的REST请求和相应的处理。(具体怎样的区别没看)
5.3.2 服务续约
(客户端周期性的 发送心跳 来更新它的服务租约)
private void 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); } } |
可以看到这有个心跳检测new HeartbeatThread(), 进入看看它都干了啥
/** * The heartbeat task that renews the lease in the given intervals. */ private class HeartbeatThread implements Runnable { public void run() { if (renew()) { lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis(); } } } ---------------------------------------------------------------------------- /** * Renew with the eureka service by making the appropriate REST call */ boolean renew() { EurekaHttpResponse<InstanceInfo> httpResponse; try { httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null ); ....... ....... } return httpResponse.getStatusCode() == Status.OK.getStatusCode(); } catch (Throwable e) { logger.error(PREFIX + "{} - was unable to send heartbeat!" , appPathIdentifier, e); return false ; } } |
Eureka客户端通过REST请求的方式与服务端进行交互,那么服务端是如何处理这些REST请求的呢?看6
6、 服务注册中心的处理
6.1 响应『服务注册』请求
服务端对请求进行一系列的校验后,调用方法进行注册。
调用com.netflix.eureka.registry.AbstractInstanceRegistry的register方法进行注册:registry.register(info, "true".equals(isReplication));
将InstanceInfo中的元数据信息存储在一个ConcurrentHashMap对象中。正如前面说到的注册中心存储了两层Map接口,第一层是key存储服务名,InstanceInfo中的appName属性,第二层的key存储实例名,对应InstanceInfo中的instanceId属性。
然后,将该新服务注册的信息广播出去
replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
/** * Registers a new instance with a given duration. * * @see com.netflix.eureka.lease.LeaseManager#register(java.lang.Object, int, boolean) */ 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; } } .... .... } |