Eureka源码分析之Eureka-Client(二)

一、前言

上篇文章提到EurekaInstanceConfig以及EurekaClientConfig两个配置接口通过硬编码+配置文件读取配置信息来创建配置对象,而这一切都是为了创建EurekaClient而生,本文主要是分析EurekaClient的创建。先上个EurekaClient的类结构图:

 

二、接口/类简单描述

1. LookupService

该接口一共有四个方法

package com.netflix.discovery.shared;
import java.util.List;
import com.netflix.appinfo.InstanceInfo;

public interface LookupService<T> {

    Application getApplication(String appName);

    Applications getApplications();

    List<InstanceInfo> getInstancesById(String id);

    InstanceInfo getNextServerFromEureka(String virtualHostname, boolean secure);

}

这个接口主要定义了一系列从eureka-server注册中心获取服务的接口,InstanceInfo对应的某个服务实例,Application对应的时某个App,该App下可能有多个InstanceInfo实例,而Applications则包含多个Application。从包含角度来说 Applications > Application > InstanceInfo

 

2. EurekaClient

定义了一系列EurekaClient的接口

package com.netflix.discovery;

import java.util.List;
import java.util.Set;

import javax.annotation.Nullable;

import com.google.inject.ImplementedBy;
import com.netflix.appinfo.ApplicationInfoManager;
import com.netflix.appinfo.HealthCheckCallback;
import com.netflix.appinfo.HealthCheckHandler;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.shared.Applications;
import com.netflix.discovery.shared.LookupService;


@ImplementedBy(DiscoveryClient.class)
public interface EurekaClient extends LookupService {

    // ========================
    // getters for InstanceInfo
    // ========================

    public Applications getApplicationsForARegion(@Nullable String region);

    public Applications getApplications(String serviceUrl);

    public List<InstanceInfo> getInstancesByVipAddress(String vipAddress, boolean secure);

    public List<InstanceInfo> getInstancesByVipAddress(String vipAddress, boolean secure, @Nullable String region);

    public List<InstanceInfo> getInstancesByVipAddressAndAppName(String vipAddress, String appName, boolean secure);

    // ==========================
    // getters for local metadata
    // ==========================

    public InstanceInfo.InstanceStatus getInstanceRemoteStatus();

    @Deprecated
    public List<String> getDiscoveryServiceUrls(String zone);

    @Deprecated
    public List<String> getServiceUrlsFromConfig(String instanceZone, boolean preferSameZone);

    @Deprecated
    public List<String> getServiceUrlsFromDNS(String instanceZone, boolean preferSameZone);

    // ===========================
    // healthcheck related methods
    // ===========================

    @Deprecated
    public void registerHealthCheckCallback(HealthCheckCallback callback);

    public void registerHealthCheck(HealthCheckHandler healthCheckHandler);

    public void registerEventListener(EurekaEventListener eventListener);

    public boolean unregisterEventListener(EurekaEventListener eventListener);

    public HealthCheckHandler getHealthCheckHandler();

    // =============
    // other methods
    // =============

    public void shutdown();

    public ApplicationInfoManager getApplicationInfoManager();
}

从注释中就可以看到,这些接口可分为以下四种类型:

1. 从注册中心获取其他服务实例InstanceInfo相关

2. 获取自身服务InstanceInfo元信息相关

3. 健康检查相关

4. 其他

 

3. DiscoveryClient

直接看构造方法

  1 // Eureka-Client初始化,后两个参数一般不会用到
  2     @Inject
  3     DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
  4                     Provider<BackupRegistry> backupRegistryProvider) {
  5         if (args != null) {
  6             this.healthCheckHandlerProvider = args.healthCheckHandlerProvider;
  7             this.healthCheckCallbackProvider = args.healthCheckCallbackProvider;
  8             this.eventListeners.addAll(args.getEventListeners());
  9             this.preRegistrationHandler = args.preRegistrationHandler;
 10         } else {
 11             this.healthCheckCallbackProvider = null;
 12             this.healthCheckHandlerProvider = null;
 13             this.preRegistrationHandler = null;
 14         }
 15         
 16         this.applicationInfoManager = applicationInfoManager;
 17         InstanceInfo myInfo = applicationInfoManager.getInfo();
 18 
 19         clientConfig = config;
 20         staticClientConfig = clientConfig;
 21         transportConfig = config.getTransportConfig();
 22         instanceInfo = myInfo;
 23         if (myInfo != null) {
 24             // 没实际用途,主要用于logger
 25             appPathIdentifier = instanceInfo.getAppName() + "/" + instanceInfo.getId();
 26         } else {
 27             logger.warn("Setting instanceInfo to a passed in null value");
 28         }
 29 
 30         // 备份注册中心,目前没有提供实现
 31         this.backupRegistryProvider = backupRegistryProvider;
 32 
 33         this.urlRandomizer = new EndpointUtils.InstanceInfoBasedUrlRandomizer(instanceInfo);
 34         // 初始化应用集合在本地的缓存,size=0
 35         localRegionApps.set(new Applications());
 36 
 37         fetchRegistryGeneration = new AtomicLong(0);
 38 
 39         // 和AWS有关,跳过
 40         remoteRegionsToFetch = new AtomicReference<String>(clientConfig.fetchRegistryForRemoteRegions());
 41         remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(","));
 42 
 43         if (config.shouldFetchRegistry()) { 
 44             this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
 45         } else {
 46             this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
 47         }
 48 
 49         if (config.shouldRegisterWithEureka()) {
 50             this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
 51         } else {
 52             this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
 53         }
 54 
 55         logger.info("Initializing Eureka in region {}", clientConfig.getRegion());
 56 
 57         if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
 58             logger.info("Client configured to neither register nor query for data.");
 59             scheduler = null;
 60             heartbeatExecutor = null;
 61             cacheRefreshExecutor = null;
 62             eurekaTransport = null;
 63             instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), clientConfig.getRegion());
 64 
 65             // This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
 66             // to work with DI'd DiscoveryClient
 67             DiscoveryManager.getInstance().setDiscoveryClient(this);
 68             DiscoveryManager.getInstance().setEurekaClientConfig(config);
 69 
 70             initTimestampMs = System.currentTimeMillis();
 71             logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",
 72                     initTimestampMs, this.getApplications().size());
 73 
 74             return;  // no need to setup up an network tasks and we are done
 75         }
 76 
 77         try {
 78             // default size of 2 - 1 each for heartbeat and cacheRefresh
 79             // 设置两个线程执行以下两个heartbeatExecutor
 80             scheduler = Executors.newScheduledThreadPool(2,
 81                     new ThreadFactoryBuilder()
 82                             .setNameFormat("DiscoveryClient-%d")
 83                             .setDaemon(true)
 84                             .build());
 85 
 86             // 1.初始化心跳线程池
 87             heartbeatExecutor = new ThreadPoolExecutor(
 88                     1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
 89                     new SynchronousQueue<Runnable>(),
 90                     new ThreadFactoryBuilder()
 91                             .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
 92                             .setDaemon(true)
 93                             .build()
 94             );  // use direct handoff
 95 
 96             // 2.初始化缓存刷新线程池
 97             cacheRefreshExecutor = new ThreadPoolExecutor(
 98                     1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
 99                     new SynchronousQueue<Runnable>(),
100                     new ThreadFactoryBuilder()
101                             .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
102                             .setDaemon(true)
103                             .build()
104             );  // use direct handoff
105 
106             // 初始化HttpClient,包括registerHttpClient和queryHttpClient
107             eurekaTransport = new EurekaTransport();
108             scheduleServerEndpointTask(eurekaTransport, args);
109 
110             // AWS相关,跳过
111             AzToRegionMapper azToRegionMapper;
112             if (clientConfig.shouldUseDnsForFetchingServiceUrls()) {
113                 azToRegionMapper = new DNSBasedAzToRegionMapper(clientConfig);
114             } else {
115                 azToRegionMapper = new PropertyBasedAzToRegionMapper(clientConfig);
116             }
117             if (null != remoteRegionsToFetch.get()) {
118                 azToRegionMapper.setRegionsToFetch(remoteRegionsToFetch.get().split(","));
119             }
120             instanceRegionChecker = new InstanceRegionChecker(azToRegionMapper, clientConfig.getRegion());
121         } catch (Throwable e) {
122             throw new RuntimeException("Failed to initialize DiscoveryClient!", e);
123         }
124 
125         // 从eureka-server拉取注册信息,fetchRegistry(false)->有全量和增量拉取,初始化时为全量拉取
126         if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
127             // 从备份注册中心拉取注册信息,目前暂时没有实现
128             fetchRegistryFromBackup();
129         }
130 
131         // call and execute the pre registration handler before all background tasks (inc registration) is started
132         if (this.preRegistrationHandler != null) {
133             // 注册前的操作,目前暂时没有实现
134             this.preRegistrationHandler.beforeRegistration();
135         }
136 
137         if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
138             try {
139                 // 注册到eureka-server,enforceRegistrationAtInit默认为false,所以不会马上注册
140                 // 后台开启线程注册
141                 if (!register() ) {
142                     throw new IllegalStateException("Registration error at startup. Invalid server response.");
143                 }
144             } catch (Throwable th) {
145                 logger.error("Registration error at startup: {}", th.getMessage());
146                 throw new IllegalStateException(th);
147             }
148         }
149 
150         // finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
151         // 开启定时任务
152         initScheduledTasks();
153 
154         try {
155             Monitors.registerObject(this);
156         } catch (Throwable e) {
157             logger.warn("Cannot register timers", e);
158         }
159 
160         // This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
161         // to work with DI'd DiscoveryClient
162         DiscoveryManager.getInstance().setDiscoveryClient(this);
163         DiscoveryManager.getInstance().setEurekaClientConfig(config);
164 
165         initTimestampMs = System.currentTimeMillis();
166         logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",
167                 initTimestampMs, this.getApplications().size());
168     }

 

这么长的构造函数我也不常见,现在大体下梳理整个流程:

5~53行:属性初始化

77~104行:初始化3个线程池,其中一个为主线程池,另外两个为子线程池,分别用于执行两个定时任务。

110~123行:和AWS(亚马逊云服务)相关,国内基本不会涉及,跳过。

126~129行:从eureka-server拉取服务,如果拉取服务失败,则从备份注册中心拉取(目前备份中心没有具体实现,可能以后版本才会有),其中拉取服务的逻辑在fetchRegistry(false)中。

132~134行:preRegistrationHandler.beforeRegistration(),进行eureka-client初始化前的操作,目前也没有实现,应该是留给用户扩展的方法。

137~148行:向eureka-client注册中心注册服务,因为clientConfig.shouldEnforceRegistrationAtInit()默认为false,再没修改该配置,是不会执行逻辑内的方法,也就是此时不会注册服务,服务注册会在定时任务那里后台注册。

152行:initScheduledTasks(),在这里开始了3个定时任务,分别为服务续约,缓存更新(拉取服务),实例复制(向注册中心注册或更新自身实例)。

 

拉取服务

 1 private boolean fetchRegistry(boolean forceFullRegistryFetch) {
 2         // TODO 监控相关
 3         Stopwatch tracer = FETCH_REGISTRY_TIMER.start();
 4 
 5         try {
 6             // If the delta is disabled or if it is the first time, get all
 7             // applications
 8             Applications applications = getApplications();
 9 
10             // 【全量】拉取 /apps
11             if (clientConfig.shouldDisableDelta() // 禁用增量 default false
12                     || (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress())) // default null
13                     || forceFullRegistryFetch
14                     || (applications == null)
15                     || (applications.getRegisteredApplications().size() == 0) // 初始化 size=0
16                     || (applications.getVersion() == -1)) //Client application does not have latest library supporting delta
17             {
18                 logger.info("Disable delta property : {}", clientConfig.shouldDisableDelta());
19                 logger.info("Single vip registry refresh property : {}", clientConfig.getRegistryRefreshSingleVipAddress());
20                 logger.info("Force full registry fetch : {}", forceFullRegistryFetch);
21                 logger.info("Application is null : {}", (applications == null));
22                 logger.info("Registered Applications size is zero : {}",
23                         (applications.getRegisteredApplications().size() == 0));
24                 logger.info("Application version is -1: {}", (applications.getVersion() == -1));
25 
26                 // 全量从eureka-server拉取注册信息
27                 getAndStoreFullRegistry();
28             } else {
29                 // 【增量】拉取 /apps/delta
30                 getAndUpdateDelta(applications);
31             }
32             applications.setAppsHashCode(applications.getReconcileHashCode());
33             logTotalInstances();
34         } catch (Throwable e) {
35             logger.error(PREFIX + "{} - was unable to refresh its cache! status = {}", appPathIdentifier, e.getMessage(), e);
36             return false;
37         } finally {
38             if (tracer != null) {
39                 tracer.stop();
40             }
41         }
42 
43         // Notify about cache refresh before updating the instance remote status
44         onCacheRefreshed();
45 
46         // Update remote status based on refreshed data held in the cache
47         updateInstanceRemoteStatus();
48 
49         // registry was fetched successfully, so return true
50         return true;
51     }

拉取实例信息有两种方式,分别为【全量拉取】和【增量拉取】。全量拉取即为把注册中心的所有实例信息一次性拉取到本地,这种方式网络占用大,故有第二种方式增量拉取,顾名思义就是拉取注册中心最近增加的实例信息,这种方式可以减少网络占用。在eureka-client初始化时(第一次)采用的是全量拉取,如果没有禁用增量的话,后续定时任务会进行增量拉取。

27行,全量拉取:getAndStoreFullRegistry(),调用了注册中心的 /apps接口。

30行,增量拉取:getAndUpdateDelta(applications),调用了注册中心的 /apps/delta接口。

44~47行:主要做一些缓存刷新等监听操作。

 

注册服务

调用了regitry()方法进行服务注册,但事实上按照默认设置的话,由于clientConfig.shouldEnforceRegistrationAtInit()默认为false,所以不会进入这个方法,注册由后台线程(定时任务)发起。

从下面的方法可以看到,注册信息就是InstanceInfo实例。

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

进入register(instanceInfo)方法:

@Override
    public EurekaHttpResponse<Void> register(InstanceInfo info) {
        String urlPath = "apps/" + info.getAppName();
        ClientResponse response = null;
        try {
            Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
            addExtraHeaders(resourceBuilder);
            response = resourceBuilder
                    .header("Accept-Encoding", "gzip")
                    .type(MediaType.APPLICATION_JSON_TYPE)
                    .accept(MediaType.APPLICATION_JSON)
                    .post(ClientResponse.class, info);
            return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
        } finally {
            if (logger.isDebugEnabled()) {
                logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
                        response == null ? "N/A" : response.getStatus());
            }
            if (response != null) {
                response.close();
            }
        }
    }

正是通过httpClient调用了注册中心 apps/AppName的接口,请求参数是InstanceInfo。

 

定时任务

前面说到定时任务initScheduledTasks()分别开启了服务续约、服务注册、服务拉取三个定时任务(可以理解为服务的增改查),下面来看看是怎么实现的:

private void initScheduledTasks() {
        if (clientConfig.shouldFetchRegistry()) {
            // registry cache refresh timer -> 服务缓存刷新定时器
            // 从server更新本地服务的频率,默认30s
            // 更新超时延迟重试时间,默认10s
            int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
            int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "cacheRefresh",
                            scheduler,
                            cacheRefreshExecutor,
                            registryFetchIntervalSeconds,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            // 【增量拉取任务】 -> registryFetch()
                            new CacheRefreshThread()
                    ),
                    registryFetchIntervalSeconds, TimeUnit.SECONDS);
        }

        // 允许客户端注册到eureka-server
        if (clientConfig.shouldRegisterWithEureka()) {
            // 心跳续约周期
            int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
            // 心跳执行超时后的延迟重试的时间
            int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
            logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);

            //1.(服务续租) Heartbeat timer 心跳定时任务
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "heartbeat",
                            scheduler,
                            heartbeatExecutor,
                            renewalIntervalInSecs,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            new HeartbeatThread()
                    ),
                    renewalIntervalInSecs, TimeUnit.SECONDS);

            //2.(服务注册) InstanceInfo replicator 实例复制定时任务
            instanceInfoReplicator = new InstanceInfoReplicator(
                    this,
                    instanceInfo,
                    clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                    2); // burstSize

            statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
                @Override
                public String getId() {
                    return "statusChangeListener";
                }

                @Override
                public void notify(StatusChangeEvent statusChangeEvent) {
                    if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
                            InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
                        // log at warn level if DOWN was involved
                        logger.warn("Saw local status change event {}", statusChangeEvent);
                    } else {
                        logger.info("Saw local status change event {}", statusChangeEvent);
                    }
                    instanceInfoReplicator.onDemandUpdate();
                }
            };

            if (clientConfig.shouldOnDemandUpdateStatusChange()) {
                applicationInfoManager.registerStatusChangeListener(statusChangeListener);
            }

            instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
        } else {
            logger.info("Not registering with Eureka server per configuration");
        }
    }

这三个定时任务核心是具体的任务实现,事实上大家也可以猜想到最后也就是调用了注册中心的某个接口,进行信息的更新交互,由于笔者水平以及时间有限(主要是前者),就不一一深入。

值得一提的时这些任务调度以一个很巧妙的方式try-catch-finally去实现了任务循环和取消,大家有兴趣的可以深入去看看。

 

三、总结

1.Eureka-Client是什么?我们可以理解为它就是向注册中心(相当于一个容器)进行服务实例的增删改查的一个实体。

2.注册中心经常会有变化(其他实例的增删改查),那么本地需要通过一系列的定时任务去和注册中心定时通信,维护和更新本地缓存的实例信息。

3.这么看来,注册中心其实就是一个Web应用(实时上确实是WEB应用),提供了各种Http接口来被其他eureka-client调用进行各种增删改查操作。

 

以上,都是笔者比较浅显的一个理解,事实上源码很多都没有去深入理解,毕竟深入每一个细节需要花费太多时间,笔者暂且先理一下大概脉络和大家分享。

由于水平有限,如果纰漏以及错误之处,希望大家不吝指点。

posted @ 2018-11-20 22:52  SingleDogs  阅读(136)  评论(0)    收藏  举报