Springcloud - DiscoveryClient

605 阅读9分钟

0.本文重点:

  1. 服务启动注册、心跳、拉取服务流程

  2. 服务何时向server端更新状态

[DiscoveryClient-InstanceInfoReplicator-0] [com.netflix.discovery.DiscoveryClient]
[register] [879] : DiscoveryClient_xxxx/ xxx:xxx - registration status: 204

服务启动时我们经常可以看到上面这段日志,实则这是在调用 eureka server的api接口

Eureka -Api list

请求名称请求方式HTTP地址请求描述
注册服务POST/eureka/apps/{appID}传递JSON或者XML格式的参数内容,HTTP code为204表示成功
删除服务DELETE/eureka/apps/{appID}/{instanceID}HTTP code为200时表示成功
发起心跳PUT/eureka/apps/{appID}/{instanceID}HTTP code为200时表示成功
查询服务GET/eureka/appsHTTP code为200时表示成功,返回XML/JSON数据
查询指定appID的服务列表GET/eureka/apps/{appID}HTTP code为200时表示成功,返回XML/JSON数据
查询指定appID&instanceID的服务GET/eureka/apps/{appID}/{instanceID}获取指定appID以及instanceID的服务信息,HTTP code为200时表示成功,返回XML/JSON数据
查询指定instanceID服务列表GET/eureka/apps/instances/{instanceID}获取指定instanceID的服务信息,HTTP code为200时表示成功,返回XML/JSON数据
变更服务状态PUT/eureka/apps/{appID}/{instanceID}/status?value=DOWN服务上线、服务下线等状态改变,HTTP code为200时表示成功

① 服务注册(Register): Eureka Client会通过发送REST请求向Eureka Server注册自己的服务,提供自身IP、端口、微服务名称等信息。Eureka Server接收到注册请求后,就会把这些信息存储在一个双层的Map中。

  ② 服务续约(Renew): 在服务注册后,Eureka Client会维护一个心跳来持续通知Eureka Server,说明服务一直处于可用状态,防止被剔除。Eureka Client在默认的情况下会每隔30秒(eureka.instance.leaseRenewallIntervalInSeconds)发送一次心跳来进行服务续约。

  ③ 服务同步(Replicate): Eureka Server集群中多个Eureka Server之间会互相进行注册,不同Eureka Server之间会进行服务同步,用来保证Eureka Server集群内的所有实例中的数据一致性

  ④ 获取服务(Get Registry): 服务消费者(Eureka Client)在启动的时候,会发送一个REST请求给Eureka Server,获取上面注册的服务清单,并且缓存在Eureka Client本地,默认缓存30秒(eureka.client.registryFetchIntervalSeconds)。同时,为了性能考虑,Eureka Server也会维护一份只读的服务清单缓存,该缓存每隔30秒更新一次。

  ⑤ 服务调用(Make Remote Call): 服务消费者在获取到服务清单后,就可以根据清单中的服务列表信息,查找到其他服务的地址,从而进行远程调用。

  ⑥ 服务下线(Cancel): 当Eureka Client需要关闭或重启时,就不希望在这个时间段内再有请求进来,所以,就需要提前先发送REST请求给Eureka Server,告诉Eureka Server自己要下线了,Eureka Server在收到请求后,就会把该服务状态置为下线(DOWN),并把该下线事件传播出去。

  ⑦ 服务剔除(Evict): 服务实例可能会因为网络故障等原因导致不能提供服务,而此时该实例也没有发送请求给Eureka Server来进行服务下线,所以,还需要有服务剔除的机制。Eureka Server在启动的时候会创建一个定时任务,每隔一段时间(默认60秒):eureka.server.eviction-interval-timer-in-ms(毫秒数),从当前服务清单中把超时没有续约(客户端配置 默认90秒,eureka.instance.leaseExpirationDurationInSeconds )的服务剔除。

  ⑧ 自我保护: 既然Eureka Server会定时剔除超时没有续约的服务,那就有可能出现一种场景,网络一段时间内发生了异常,所有的服务都没能够进行续约,Eureka Server就把所有的服务都剔除了,这样显然不太合理。所以,就有了自我保护机制,当短时间内,统计续约失败的比例,如果达到一定阈值,则会触发自我保护的机制,在该机制下,Eureka Server不会剔除任何的微服务,等到正常后,再退出自我保护机制。自我保护开关(eureka.server.enableself-preservation: false)

服务状态、Applications

服务的状态

public enum InstanceStatus {
    UP, // Ready to receive traffic
    DOWN, // Do not send traffic- healthcheck callback failed
    STARTING, // Just about starting- initializations to be done - do not
    // send traffic
    OUT_OF_SERVICE, // Intentionally shutdown for traffic
    UNKNOWN;

客户端维护的服务关系,双层map,第一层是服务名与服务列表的关系,第二层是指定服务下,instanceId与服务实例的关系。

public class DiscoveryClient implements EurekaClient {
    private final AtomicReference<Applications> localRegionApps = new AtomicReference<Applications>();
}

public class Applications {
    
    private final AbstractQueue<Application> applications;
    private final Map<String, Application> appNameApplicationMap;
    private final Map<String, VipIndexSupport> virtualHostNameAppMap;
    private final Map<String, VipIndexSupport> secureVirtualHostNameAppMap;
 }
 
public class Application {
    private volatile boolean isDirty = false;
    private final Set<InstanceInfo> instances;
    private final AtomicReference<List<InstanceInfo>> shuffledInstances;
    private final Map<String, InstanceInfo> instancesMap;
}

DiscoveryClient

DiscoveryClient由 EurekaAutoServiceRegistration辅助注册,改类实现了SmartLiftcycle接口,在所有非lazy bean都实例化完成后,容器会自动回调其start方法,从而实例化DiscoveryClient 。应用进程即将结束时回调其stop方法,向eureka server通知自己即将下线,也就是将DOWN的状态告诉server

EurekaServiceRegistry#register

public void register(EurekaRegistration reg) {
   //在此处初始化DiscoveryClient
   maybeInitializeClient(reg);
   
   reg.getApplicationInfoManager()
     //更新状态 此出由 starting -> up 会触发StatusChangeEvent  立即向服务端更新自己的状态
         .setInstanceStatus(reg.getInstanceConfig().getInitialStatus());

   reg.getHealthCheckHandler().ifAvailable(healthCheckHandler -> reg
         .getEurekaClient().registerHealthCheck(healthCheckHandler));
}

EurekaServiceRegistry#deregister

public void deregister(EurekaRegistration reg) {
    if (reg.getApplicationInfoManager().getInfo() != null) {
           //更新状态  立即向服务端更新自己的状态为DOWN
           reg.getApplicationInfoManager()
                            .setInstanceStatus(InstanceInfo.InstanceStatus.DOWN);

    }
}

构造方法

DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
    ..
    
    this.applicationInfoManager = applicationInfoManager;
    //本服务信息  服务名 ip 端口 ,健康检查路径等
    InstanceInfo myInfo = applicationInfoManager.getInfo();
    ...
  
    if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
        //不注册 也不拉取服务
        ...
        return;  
    }

    try {
        // default size of 2 - 1 each for heartbeat and cacheRefresh
        //注释写的很清楚了 2个线程 1个是心跳 1个是缓存刷新
        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
        ...
    } catch (Throwable e) {
        throw new RuntimeException("Failed to initialize DiscoveryClient!", e);
    }

    if (clientConfig.shouldFetchRegistry()) {
        try {
            //fetchRegistry 这里调用是拉取全量,后续的定时任务是增量拉取
            boolean primaryFetchRegistryResult = fetchRegistry(false);
            ...
        } catch (Throwable th) {
            logger.error("Fetch registry error at startup: {}", th.getMessage());
            throw new IllegalStateException(th);
        }
    }
     ...
    // 初始化 心跳 注册 定时任务
    initScheduledTasks();
    ...
}

initScheduledTasks 初始化定时任务

private void initScheduledTasks() {
    //拉取服务
    if (clientConfig.shouldFetchRegistry()) {
        //默认30s 
        int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
        int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
        cacheRefreshTask = new TimedSupervisorTask(
                "cacheRefresh",
                scheduler,
                cacheRefreshExecutor,
                registryFetchIntervalSeconds,
                TimeUnit.SECONDS,
                expBackOffBound,
                //runnable 调用下方 DiscoveryClient#refreshRegistry
                new CacheRefreshThread()
        );
        scheduler.schedule(
                cacheRefreshTask,
                registryFetchIntervalSeconds, TimeUnit.SECONDS);
    }

   //注册服务
    if (clientConfig.shouldRegisterWithEureka()) {
       //默认30s
        int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
        int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
        logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);

        // Heartbeat timer
        heartbeatTask = new TimedSupervisorTask(
                "heartbeat",
                scheduler,
                heartbeatExecutor,
                renewalIntervalInSecs,
                TimeUnit.SECONDS,
                expBackOffBound,
                //runnable 调用下方 DiscoveryClient#renew
                new HeartbeatThread()
        );
        scheduler.schedule(
                heartbeatTask,
                renewalIntervalInSecs, TimeUnit.SECONDS);
        
         ...
        //注册状态变化事件监听器
        statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
            @Override
            public String getId() {
                return "statusChangeListener";
            }

            @Override
            public void notify(StatusChangeEvent statusChangeEvent) {
                //状态变化回调事件  调用下方InstanceInfoReplicator#onDemandUpdate,
                //本质上就是不再等待定时任务执行,立即更新状态相关变化
                instanceInfoReplicator.onDemandUpdate();
            }
        };

        if (clientConfig.shouldOnDemandUpdateStatusChange()) {
           //默认注册  代表是不仅仅只在定时任务中向服务端更新自己的状态 只要状态发生了变化,
           //会立即向server更新自己的状态
            applicationInfoManager.registerStatusChangeListener(statusChangeListener);
        }

 //默认40s后执行 下方InstanceInfoReplicator#run 
 instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
    } else {
        logger.info("Not registering with Eureka server per configuration");
    }
}

DiscoveryClient#refreshRegistry

void refreshRegistry() {
    try {
         ...
         //调用下方 DiscoveryClient#fetchRegistry  拉取全量/增量数据
        boolean success = fetchRegistry(remoteRegionsModified);
        if (success) {
            registrySize = localRegionApps.get().size();
            lastSuccessfulRegistryFetchTimestamp = System.currentTimeMillis();
        }
        ...
    } catch (Throwable e) {
        logger.error("Cannot fetch registry from server", e);
    }
}

DiscoveryClient#fetchRegistry 拉取服务

private boolean fetchRegistry(boolean forceFullRegistryFetch) {
    Stopwatch tracer = FETCH_REGISTRY_TIMER.start();
    try {
        Applications applications = getApplications();

        if (clientConfig.shouldDisableDelta()
                || (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress()))
                || forceFullRegistryFetch
                //首次全量拉取后 只有拉取到了  applications就不会为空,后续就会增量拉取
                || (applications == null)
                || (applications.getRegisteredApplications().size() == 0)
                || (applications.getVersion() == -1)) //Client application does not have latest library supporting delta
        {
            ...
            //默认会去掉状态非UP的服务
            getAndStoreFullRegistry();
        } else {
           //获取增量 分别就  ADDED、MODIFIED、DELETED 三种事件机制 更新维护的 appNameApplicationMap,
           //更新localRegionApps\applications的版本号,用于下次继续拉取增量
            getAndUpdateDelta(applications);
        }
        applications.setAppsHashCode(applications.getReconcileHashCode());
        logTotalInstances();
    } catch (Throwable e) {
       ....
    }

    //发布 CacheRefreshedEvent 此事件是EurekaEvent 是eureka自己实现的发布订阅机制;
    //增量拉取时,cacheRefreshedCount已完成初始化, 还会发布一个HeartbeatEvent事件(spring的applicationEvent)
        (spring gateway的路由刷新有用到这个事件)
    onCacheRefreshed();

  //拉取到的服务列表 更新到本地缓存,从本地缓存的服务列表,根据自身服务的instanceId找到相关的状态 
     第一次是获取不到的
    //获取不到的情况下将状态置为UNKNOWN,同时与上次维护自身服务状态对比,如果不一致就发布StatusChangeEvent事件
    //由于默认上次状态为 UNKNOWN 本次不处理。当然如果你是服务重启的话,有可能拉到DOWN的状态
    //由于首次拉取的时候 statusChangeListner还未注册,所以首次拉取的动作不会向server更新自己的状态
    updateInstanceRemoteStatus();

    // registry was fetched successfully, so return true
    return true;
}

DiscoveryClient#renew

就是简单的30s一次请求
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;
    }
}

InstanceInfoReplicator#onDemandUpdate

public boolean onDemandUpdate() {
    if (rateLimiter.acquire(burstSize, allowedRatePerMinute)) {
        if (!scheduler.isShutdown()) {
            scheduler.submit(new Runnable() {
                @Override
                public void run() {
                    Future latestPeriodic = scheduledPeriodicRef.get();
                    if (latestPeriodic != null && !latestPeriodic.isDone()) {
                        //取消定时任务状态检查
                        latestPeriodic.cancel(false);
                    }
                    //意思就是不等定时任务了,因为已经感知到状态发生变化了 ,马上更新
                    InstanceInfoReplicator.this.run();
                }
            });
            return true;
        } else {
            logger.warn("Ignoring onDemand update due to stopped scheduler");
            return false;
        }
    } else {
        logger.warn("Ignoring onDemand update due to rate limiter");
        return false;
    }
}

InstanceInfoReplicator#run

public void run() {
    try {
        discoveryClient.refreshInstanceInfo();
        //判断是否存在dirtyTime  dirtyTime就是当状态发生变化时 记录下的时间戳
        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 {
        //相当于30s一次校验一下自己的状态 如果状态变化就向服务端更新状态 ,服务启动时默认是40s后,
        //但是会被listener感知到变化 提前触发
        Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
        scheduledPeriodicRef.set(next);
    }
}

总结

通过源码的分析,我们可以知道DiscoverClient初始化时做了一下几件事

1.向eureka server拉取服务列表,更新到本地缓存中

2.初始化以下定时任务

  1. 初始化refreshRegistry 拉取服务更新缓存定时任务 默认30s执行一次
  2. 初始化renew 心跳定时任务 默认30s执行一次
  3. 初始化run 服务自检查定时任务 首次是40s执行,后续30s执行一次。

3.初始化任务结束,将自身状态由STARTING->UP,由于状态发生变化,记录dirtyTime(也就是状态发生变化而又未通知server的时间点)发布StatusChangeEvent,被StatusChangeListener监听到,中断第3个定时任务,不再等待,直接向server端更新自己的状态,这个体现在日志层面就是服务启动之初的204注册响应吗。然后更新第三个定时任务的时间周期为30s一次。

参考:

  1. www.cnblogs.com/toby-xu/p/1…