SpringCloud Eureka Client 源码

这里主要学习一下EurekaClient,通过上一篇学习了解到,Client的功能主要有这么几个:

  • 服务注册
  • 服务续约
  • 服务发现
  • 服务下线
  • 维护集群节点(未写完)

而这些功能主要由netflix eureka实现,其客户端实现类为DiscoveryClient.java,它实现了EurekaClient接口,而EurekaClient又继承了LookupService接口。

数据结构

我们首先来看看这个最上层的接口都声明了些什么?

LookupService:

Application getApplication(String appName);

Applications getApplications();

List<InstanceInfo> getInstancesById(String id);

InstanceInfo getNextServerFromEureka(String virtualHostname, boolean secure);

在这里从返回类型可以看出此接口的主要任务为获取Application和InstanceInfo,可是这2个货是干什么的呢?

  • Application:其属性包含一个name和一个InstanceInfo类型的Set,其name为服务名,为配置文件中的spring.application.name,其Set为一个InstanceInfo列表,其中记述了注册了此服务名的Eureka节点列表。
  • InstanceInfo:主要记述了Eureka节点的信息,包括ip、port等等。

而这四个接口分别是获取application、application列表,还有获取InstanceInfo列表,而从DiscoveryClient.java的实现来看,getInstancesById()也是从applications对象里取出的。

    @Override
    public List<InstanceInfo> getInstancesById(String id) {
        List<InstanceInfo> instancesList = new ArrayList<InstanceInfo>();
        for (Application app : this.getApplications()
                .getRegisteredApplications()) {
            InstanceInfo instanceInfo = app.getByInstanceId(id);
            if (instanceInfo != null) {
                instancesList.add(instanceInfo);
            }
        }
        return instancesList;
    }

说明在客户端里,是以服务名为单位,保存着注册此服务的服务提供者列表。这个服务提供者保存在InstanceInfo里。

类结构

作为主要工作的类DiscoveryClient.java,它有几个内部类:

  • EurekaTransport.java:用于注册、续约等与服务器通信
  • HeartbeatThread.java:续约用
  • CacheRefreshThread.java:刷新服务列表用

注册服务

在客户端启动后初始化了DiscoveryClient,在这里启动了3个定时任务:

  • InstanceInfoReplicator线程:用来向注册中心注册自己(最初的注册不是由这个定时任务完成,是由springcloud里的逻辑直接调用这个线程的run方法完成)。
  • HearBeat线程:定时发送心跳,进行服务续约,如果服务被注册中心剔除,则重新注册。
  • CacheRefresh线程:定时刷新服务列表

由此可见,netflix eureka 的客户端组件DiscoveryClient启动后,服务的注册是依靠这个心跳线程来实现的。
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();
        }
    }
}

其run方法,调用了renew()方法:

    /**
     * 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);
            logger.debug("{} - Heartbeat status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
            if (httpResponse.getStatusCode() == 404) {
                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;
        }
    }

registrationClient.sendHeartBeat向注册中心续约,如果返回404,则说明已失效,需要重新调用register()注册。

而此线程是在一个定时任务里调用:

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);
        ..........
}

renewalIntervalInSecs为心跳续约的时间间隔,默认为30秒,则意味着服务启动30秒后,才能注册到注册中心。可是我们在实际情况中,服务启动后,服务立刻就注册到注册中心了,这是为什么?

原因是Spring Cloud Eureka在封装的时候,自己调用了register()方法,启动同时注册了服务。
EurekaServiceRegistry.java

public class EurekaServiceRegistry implements ServiceRegistry<EurekaRegistration> {

    private static final Log log = LogFactory.getLog(EurekaServiceRegistry.class);

    @Override
    public void register(EurekaRegistration reg) {
        maybeInitializeClient(reg);

        if (log.isInfoEnabled()) {
            log.info("Registering application " + reg.getInstanceConfig().getAppname()
                    + " with eureka with status "
                    + reg.getInstanceConfig().getInitialStatus());
        }

        reg.getApplicationInfoManager()
                .setInstanceStatus(reg.getInstanceConfig().getInitialStatus());

        if (reg.getHealthCheckHandler() != null) {
            reg.getEurekaClient().registerHealthCheck(reg.getHealthCheckHandler());
        }
    }
    ........
}

通过调用setInstanceStatus(),立刻触发DiscoveryClient中的statusChangeListener,

public synchronized void setInstanceStatus(InstanceStatus status) {
        InstanceStatus next = instanceStatusMapper.map(status);
        if (next == null) {
            return;
        }

        InstanceStatus prev = instanceInfo.setStatus(next);
        if (prev != null) {
            for (StatusChangeListener listener : listeners.values()) {
                try {
                    listener.notify(new StatusChangeEvent(prev, next));
                } catch (Exception e) {
                    logger.warn("failed to notify listener: {}", listener.getId(), e);
                }
            }
        }
    }

listener.notify()最终调用回DiscoveryClient.java中的register(),此段代码在com.netflix.discovery.InstanceInfoReplicator中:

    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);
        }

由此可见,Spring Cloud EurekaClient在启动时会向服务端注册自己的服务信息,并告知注册中心自己的过期时间(lease-expiration-duration-in-seconds),并且根据配置的续约间隔时间(lease-renewal-interval-in-seconds 默认30秒)定时向服务端发送心跳包进行服务续约动作。

这个注册的注册中心的地址是通过eureka.client.serviceUrl来指定的,那么在集群中,这个地址是多个,我们是向其中一个注册,还是每个都注册一遍呢?
在RetryableEurekaHttpClient中,有如下代码段:

     @Override
    protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
        List<EurekaEndpoint> candidateHosts = null;
        int endpointIdx = 0;
        for (int retry = 0; retry < numberOfRetries; retry++) {
            EurekaHttpClient currentHttpClient = delegate.get();
            EurekaEndpoint currentEndpoint = null;
            if (currentHttpClient == null) {
                if (candidateHosts == null) {
                    candidateHosts = getHostCandidates();
                    if (candidateHosts.isEmpty()) {
                        throw new TransportException("There is no known eureka server; cluster server list is empty");
                    }
                }
                if (endpointIdx >= candidateHosts.size()) {
                    throw new TransportException("Cannot execute request on any known server");
                }

                currentEndpoint = candidateHosts.get(endpointIdx++);
                currentHttpClient = clientFactory.newClient(currentEndpoint);
            }

            try {
                EurekaHttpResponse<R> response = requestExecutor.execute(currentHttpClient);
                if (serverStatusEvaluator.accept(response.getStatusCode(), requestExecutor.getRequestType())) {
                    delegate.set(currentHttpClient);
                    if (retry > 0) {
                        logger.info("Request execution succeeded on retry #{}", retry);
                    }
                    return response;
                }
                logger.warn("Request execution failure with status code {}; retrying on another server if available", response.getStatusCode());
            } catch (Exception e) {
                logger.warn("Request execution failed with message: {}", e.getMessage());  // just log message as the underlying client should log the stacktrace
            }

            // Connection error or 5xx from the server that must be retried on another server
            delegate.compareAndSet(currentHttpClient, null);
            if (currentEndpoint != null) {
                quarantineSet.add(currentEndpoint);
            }
        }
        throw new TransportException("Retry limit reached; giving up on completing the request");
    }

这里的getHostCandidates()方法,是从配置文件中取得配置得serviceUrl列表,之后,只取了第一个:

currentEndpoint = candidateHosts.get(endpointIdx++);

如果注册失败,则循环重试,用serviceUrl列表的下一个。
至于注册信息怎么在注册中心传播,在Eureka Server再进行学习。

发现服务

前面已经注册了服务,服务是用来消费的,所以Eureka还提供了从注册中心发现服务的功能。

这个发现服务的功能就是由上面所说的CacheRefresh线程来实现的。

    DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,

        .....................

            cacheRefreshExecutor = new ThreadPoolExecutor(
                    1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
                    new SynchronousQueue<Runnable>(),
                    new ThreadFactoryBuilder()
                            .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
                            .setDaemon(true)
                            .build()
            );  // use direct handoff

        ..........................

        if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
            fetchRegistryFromBackup();
        }

        ..........................

        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);
        }

registryFetchIntervalSeconds为刷新服务列表的间隔时间。这样如果注册中心服务列表有变化,就会被同步到客户端。
在掉用initScheduledTasks()方法之前,和刷新服务列表信息定时任务之间,代码还掉用了fetchRegistry(boolean forceFullRegistryFetch)方法,立刻拉取了服务端的信息。这就是为什么定时任务为30秒,但是客户端启动后,立刻就能获取到服务列表的原因。

若初始拉取注册信息失败,从备份注册中心获取。备份注册中心为构造函数的最后一个参数。

服务注册节点与服务发现节点的关系

由前面的介绍可以发现每个客户端节点在启动时,都需要连接注册中心,通过instanceInfoReplicator(间隔30秒),这个过程也是注册服务的过程。
所以每个Eureka节点既可以是服务生产者,也可以是服务消费者。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值