Eureka源碼

先明了Eureka做了哪些事情:

  • 服務(wù)注冊
  • 自我保護
  • 心跳檢測
  • 狀態(tài)同步
  • ...

從這幾件事出發(fā) 看看Eureka如何實現(xiàn)的。

服務(wù)注冊<客戶端注冊>

服務(wù)注冊是由Eureka Client向Eureka Server上報自己的服務(wù)信息,是在服務(wù)啟動的時候就做了。那就從客戶端啟動入手。

但重點是如何找 哪個方法是做服務(wù)注冊的呢?說實話 很迷茫 ,于是從網(wǎng)上找到Spring Cloud的一些規(guī)范,ServiceRegistry接口是Spring Cloud定義的服務(wù)注冊的約定。那就從這個接口的實現(xiàn)類著手。也就是EurekaServiceRegistry.register,那這個方法是哪里調(diào)用的呢?最終找到了org.springframework.cloud.netflix.eureka.serviceregistry.EurekaAutoServiceRegistration#start。這個方法其實是IOC容器中Bean的生命周期函數(shù)。

所以,有兩個入手點,一是客戶端的注解EnableEurekaClient或者AutoConfiguration;一是EurekaAutoServiceRegistration。

聲明一下:在客戶端使用時 不論EnableEurekaClient還是EnableDiscoveryClient都是可選項。且EnableEurekaClient的注釋明確說了 注解為可選項。因此,第一個入手點僅剩下AutoConfiguration。

EurekaClientAutoConfiguration

EurekaClientAutoConfiguration本身就是個配置類,里面全是Bean的定義,下面截取的代碼片段 都是根據(jù)上面的分析而來,將相關(guān)Bean的初始化入口列出來。

// Spring Cloud的規(guī)范實現(xiàn)類
// 約定使用Service Registry注冊和注銷實例。
@Bean
public EurekaServiceRegistry eurekaServiceRegistry() {
    return new EurekaServiceRegistry();
}

// 服務(wù)自動注冊
@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled",
                       matchIfMissing = true)
public EurekaAutoServiceRegistration eurekaAutoServiceRegistration(
        ApplicationContext context, EurekaServiceRegistry registry,
        EurekaRegistration registration) {
  
    return new EurekaAutoServiceRegistration(context, registry, registration);
}

@Bean(destroyMethod = "shutdown")
@ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT)
@org.springframework.cloud.context.config.annotation.RefreshScope
@Lazy
public EurekaClient eurekaClient(ApplicationInfoManager manager,
        EurekaClientConfig config, EurekaInstanceConfig instance,
        @Autowired(required = false) HealthCheckHandler healthCheckHandler) {
    // If we use the proxy of the ApplicationInfoManager we could run into a
    // problem
    // when shutdown is called on the CloudEurekaClient where the
    // ApplicationInfoManager bean is
    // requested but wont be allowed because we are shutting down. To avoid this
    // we use the
    // object directly.
    ApplicationInfoManager appManager;
    if (AopUtils.isAopProxy(manager)) {
        appManager = ProxyUtils.getTargetObject(manager);
    } else {
        appManager = manager;
    }
    CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager,
            config, this.optionalArgs, this.context);
    cloudEurekaClient.registerHealthCheck(healthCheckHandler);
    return cloudEurekaClient;
}

@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled",
                       matchIfMissing = true)
public EurekaRegistration eurekaRegistration(EurekaClient eurekaClient,
        CloudEurekaInstanceConfig instanceConfig,
        ApplicationInfoManager applicationInfoManager, 
        @Autowired(required = false) ObjectProvider<HealthCheckHandler> healthCheckHandler) {
  
    return EurekaRegistration.builder(instanceConfig).with(applicationInfoManager)
            .with(eurekaClient).with(healthCheckHandler).build();
}

EurekaAutoServiceRegistration

AutoConfiguration中找到了實例化此類的地方,說明這個類必然是IoC容器中的一個Bean。同時,關(guān)注一下此類的接口實現(xiàn),重點關(guān)注一下SmartLifecycle 。點一下AutoServiceRegistration,這個其實也算是Spring Cloud提供的一個自動注冊的點。

public class EurekaAutoServiceRegistration implements AutoServiceRegistration,
      SmartLifecycle, Ordered, SmartApplicationListener {
        ....
}

這就表明,在IOC容器啟動時 會回調(diào)里面的生命周期方法,例如startstop

// 回調(diào)時觸發(fā)
public void start() {
    // only set the port if the nonSecurePort or securePort is 0 and this.port != 0
    if (this.port.get() != 0) {
        if (this.registration.getNonSecurePort() == 0) {
            this.registration.setNonSecurePort(this.port.get());
        }

        if (this.registration.getSecurePort() == 0 && this.registration.isSecure()) {
            this.registration.setSecurePort(this.port.get());
        }
    }

    // only initialize if nonSecurePort is greater than 0 and it isn't already running
    // because of containerPortInitializer below
    if (!this.running.get() && this.registration.getNonSecurePort() > 0) {
        // serviceRegistry = org.springframework.cloud.netflix.eureka.serviceregistry.EurekaServiceRegistry
        this.serviceRegistry.register(this.registration);

        this.context.publishEvent(new InstanceRegisteredEvent<>(this,
                this.registration.getInstanceConfig()));
        this.running.set(true);
    }
}

EurekaServiceRegistry#register

這里雖然是真正的注冊方法,但是Spring還是進行了一些其他的設(shè)計,是通過發(fā)布事件這種方式來表示狀態(tài)變更的。

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

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

    // 這里其實是發(fā)布了狀態(tài)變更的事件
    reg.getApplicationInfoManager() // =com.netflix.appinfo.ApplicationInfoManager
            .setInstanceStatus(reg.getInstanceConfig().getInitialStatus());

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

這里穿插一下com.netflix.appinfo.ApplicationInfoManager.setInstanceStatus

// 設(shè)置此實例的狀態(tài)。應(yīng)用程序可以使用它來指示是否準備接收通信流。在這里設(shè)置狀態(tài)還會將狀態(tài)更改事件通知所有注冊的偵聽器。
public synchronized void setInstanceStatus(InstanceStatus status) {
    InstanceStatus next = instanceStatusMapper.map(status);
    if (next == null) {return;}

    InstanceStatus prev = instanceInfo.setStatus(next);
    if (prev != null) {
        // listeners是從哪里添加的內(nèi)容呢 看方法-registerStatusChangeListener
        for (StatusChangeListener listener : listeners.values()) {
            try {
                // 狀態(tài)的變更 意味著 有服務(wù)進行注冊或者更新
                listener.notify(new StatusChangeEvent(prev, next));
            } catch (Exception e) {
                logger.warn("failed to notify listener: {}", listener.getId(), e);
            }
        }
    }
}

// 點擊alt+F7 找到調(diào)用這個方法的入口:com.netflix.discovery.DiscoveryClient#initScheduledTasks
public void registerStatusChangeListener(StatusChangeListener listener) {
    listeners.put(listener.getId(), listener);
}

DiscoveryClient#initScheduledTasks

DiscoveryClient的構(gòu)造函數(shù)里面調(diào)了initScheduledTasks,這里就可以找一下哪里對DiscoveryClient進行了初始化,Spring Cloud這里做了一個巧妙的設(shè)計,它定義了一個類CloudEurekaClient繼承了DiscoveryClient,而在EurekaClientAutoConfiguration里面 初始化了CloudEurekaClient

public class CloudEurekaClient extends DiscoveryClient {
    ....
    public CloudEurekaClient(ApplicationInfoManager applicationInfoManager,
            EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs<?> args,
            ApplicationEventPublisher publisher) {
        // 這里調(diào)用了DiscoveryClient的構(gòu)造
        super(applicationInfoManager, config, args);
        this.applicationInfoManager = applicationInfoManager;
        this.publisher = publisher;
        this.eurekaTransportField = ReflectionUtils.findField(DiscoveryClient.class,
                "eurekaTransport");
        ReflectionUtils.makeAccessible(this.eurekaTransportField);
    }
    ....
}

public class DiscoveryClient implements EurekaClient {
    ....
    @Inject
    DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config,
                    AbstractDiscoveryClientOptionalArgs args,
                    Provider<BackupRegistry> backupRegistryProvider, 
                    EndpointRandomizer endpointRandomizer) {
      
        // finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch 就在這里了
        initScheduledTasks();
    }
    ....
}

梳理前面的類的關(guān)系

到這里就全通了。

首先在AutoConfiguration中實例化了CloudEurekaClient,從而調(diào)用了DiscoveryClient#initScheduledTasks,初始化了StatusChangeListener,同時將其添加到了com.netflix.appinfo.ApplicationInfoManager#listeners里面。

之后IoC容器初始化完成后回調(diào)所有SmartLifecycle的實現(xiàn)類的start()方法,進而到org.springframework.cloud.netflix.eureka.serviceregistry.EurekaServiceRegistry#register,然后就是com.netflix.appinfo.ApplicationInfoManager#setInstanceStatus,之后com.netflix.appinfo.ApplicationInfoManager.StatusChangeListener#notify

StatusChangeListener

事件的監(jiān)聽 一個接口 只有一個匿名內(nèi)部類

以下代碼片段來自DiscoveryClient#initScheduledTasks,是事件監(jiān)聽的一個匿名內(nèi)部類。

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

    @Override
    public void notify(StatusChangeEvent statusChangeEvent) {
        if (statusChangeEvent.getStatus() == InstanceStatus.DOWN) {
            logger.error("Saw local status change event {}", statusChangeEvent);
        } else {
            logger.info("Saw local status change event {}", statusChangeEvent);
        }
        // 真正將實例信息更新到注冊中心
        instanceInfoReplicator.onDemandUpdate();
    }
};

com.netflix.discovery.InstanceInfoReplicator#onDemandUpdate 這個方法其實就是調(diào)用了com.netflix.discovery.InstanceInfoReplicator#run

com.netflix.discovery.InstanceInfoReplicator#run

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

com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient#register

從這里面 只能看出來是通過Post方式請求了http://{eureka.ip:port}/eureka/apps/{appName}地址。

這樣只需要找一下服務(wù)端是如何暴漏相關(guān)的接口 并如何處理注冊信息的即可。

String urlPath = "apps/" + info.getAppName();
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);

服務(wù)注冊<服務(wù)端接收>

Eureka使用的jersey框架,所以說 很難像對待Spring Web MVC的方式找暴漏的接口服務(wù),所以從網(wǎng)上找了一下對應(yīng)的方法。最后即使找到了方式 也沒搞明白是咋樣映射到url的。

ApplicationResource#addInstance

從網(wǎng)上找到的,接收注冊請求的方式是com.netflix.eureka.resources.ApplicationResource#addInstance,方法前面都是校驗參數(shù),真正執(zhí)行注冊操作的是registry.register(info, "true".equals(isReplication));,其中registry=org.springframework.cloud.netflix.eureka.server.InstanceRegistry

看一下關(guān)于InstanceRegistry的類圖

org.springframework.cloud.netflix.eureka.server.InstanceRegistry#register

public void register(final InstanceInfo info, final boolean isReplication) {
    // 等于 publishEvent(new EurekaInstanceRegisteredEvent(this, info, leaseDuration, isReplication));
    handleRegistration(info, resolveInstanceLeaseDuration(info), isReplication);
    // com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#register
    super.register(info, isReplication);
}

com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#register

public void register(final InstanceInfo info, final boolean isReplication) {
    // 心跳的超時時間 默認90s
    int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
    // 如果自定義配置了超時時間 這里重新賦值一下
    if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
        leaseDuration = info.getLeaseInfo().getDurationInSecs();
    }
    // com.netflix.eureka.registry.AbstractInstanceRegistry#register
    super.register(info, leaseDuration, isReplication);
    // 高可用情況下 節(jié)點間的同步復(fù)制
    replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}

com.netflix.eureka.registry.AbstractInstanceRegistry#register

存儲節(jié)點信息的容器:ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
外層 KEY = spring.application.name
內(nèi)層 KEY = instanceID
內(nèi)層 VALUE = 實例信息

代碼很長 把logger相關(guān)的內(nèi)容刪掉了

入?yún)?registrant 其實是遠程調(diào)用接口時傳入的 而 registry.get(registrant.getAppName()).get(registrant.getId()) 則是本地的副本

public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
    read.lock();
    try {
        // registry = ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>
        // 根據(jù)spring.application.name獲取服務(wù)的列表<集群情況下才會是列表>
        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>>();
            gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
            // 說明上一步成功進行了初始化 這里大概是考慮其他節(jié)點插入了同樣的服務(wù)并同步了過來這種情況
            if (gMap == null) {
                gMap = gNewMap;
            }
        }
        // 本地副本
        Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
        // Retain the last dirty timestamp without overwriting it, if there is already a lease
        // 如果不為空 說明服務(wù)器本地存在副本 這里需要保留下本地臟時間戳 而且不覆蓋它
        if (existingLease != null && (existingLease.getHolder() != null)) {
            // registry存在的這個臟時間戳
            Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
            // 遠程傳輸過來 的時間戳
            Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
            
            // 只有大于的情況下 使用服務(wù)器本地副本
            // 在小于等于時 使用遠程傳輸過來的
            if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
                
                registrant = existingLease.getHolder();
            }
        } else {
            // The lease does not exist and hence it is a new registration
            synchronized (lock) {
                if (this.expectedNumberOfClientsSendingRenews > 0) {
                    // 這個好像是記錄的客戶端的數(shù)量 提供給心跳續(xù)約那里用的
                    this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
                    // 更新一下 每分鐘心跳續(xù)約次數(shù)閾值 
                    // TODO 這個值啥用
                    updateRenewsPerMinThreshold();
                }
            }
        }
        // 重新構(gòu)造一個
        Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
        // 如果本地副本不為空 那就將重新構(gòu)造出來的更新 服務(wù)啟動的時間
        if (existingLease != null) {
            lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
        }
        // 這一步就已經(jīng)將服務(wù)信息加入到了注冊中心了
        gMap.put(registrant.getId(), lease);
        // 這一步?jīng)]啥用 recentRegisteredQueue好像是提供給Eureka的控制臺用的
        recentRegisteredQueue.add(new Pair<Long, String>(
                System.currentTimeMillis(),
                registrant.getAppName() + "(" + registrant.getId() + ")"));
        // 下面開始處理狀態(tài)的問題 不知道是干啥的
        if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
            
            if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
                
                overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
            }
        }
        InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
        if (overriddenStatusFromMap != null) {
            
            registrant.setOverriddenStatus(overriddenStatusFromMap);
        }

        // Set the status based on the overridden status rules
        InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
        registrant.setStatusWithoutDirty(overriddenInstanceStatus);

        // If the lease is registered with UP status, set lease service up timestamp
        if (InstanceStatus.UP.equals(registrant.getStatus())) {
            // 這一步等于 serviceUpTimestamp = System.currentTimeMillis();
            lease.serviceUp();
        }
        registrant.setActionType(ActionType.ADDED);
        // 這個應(yīng)該是被 readWriteCacheMap 用的
        recentlyChangedQueue.add(new RecentlyChangedItem(lease));
        registrant.setLastUpdatedTimestamp();
        // 更新緩存 也是更新 readWriteCacheMap 里面的內(nèi)容
        invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
    } finally {
        read.unlock();
    }
}

服務(wù)信息緩存

未能保證多級緩存的一致性

com.netflix.eureka.registry.ResponseCacheImpl

public class ResponseCacheImpl implements ResponseCache {
    
    ...
    private final ConcurrentMap<Key, Value> readOnlyCacheMap = new ConcurrentHashMap<Key,Value>();
    // guava的緩存
    private final LoadingCache<Key, Value> readWriteCacheMap;
    
    ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, 
                      AbstractInstanceRegistry registry) {
        ....
        this.readWriteCacheMap= CacheBuilder.newBuilder()
            .initialCapacity(serverConfig.getInitialCapacityOfResponseCache())
            // 默認180s
            .expireAfterWrite(serverConfig.getResponseCacheAutoExpirationInSeconds(),
                              TimeUnit.SECONDS)
            .removalListener(new RemovalListener<Key, Value>() {
                  @Override
                  // readWriteCacheMap.invalidate的時候 回調(diào)這個方法
                  public void onRemoval(RemovalNotification<Key, Value> notification) {
                      Key removedKey = notification.getKey();
                      if (removedKey.hasRegions()) {
                          Key cloneWithNoRegions = removedKey.cloneWithoutRegions();
                          regionSpecificKeys.remove(cloneWithNoRegions, removedKey);
                      }
                  }
              }).build(new CacheLoader<Key, Value>() {
                  @Override
                  // readWriteCacheMap.get的時候 回調(diào)這個方法
                  public Value load(Key key) throws Exception {
                      if (key.hasRegions()) {
                          Key cloneWithNoRegions = key.cloneWithoutRegions();
                          regionSpecificKeys.put(cloneWithNoRegions, key);
                      }
                      Value value = generatePayload(key);
                      return value;
                  }
              });
        // 定時任務(wù) 更新只讀緩存 默認30s
        // responseCacheUpdateIntervalMs = eureka.server.responseCacheUpdateIntervalMs
        if (shouldUseReadOnlyResponseCache) {
            timer.schedule(getCacheUpdateTask(),
                           new Date(
                               (
                               (System.currentTimeMillis() / responseCacheUpdateIntervalMs) 
                               * responseCacheUpdateIntervalMs
                               ) + responseCacheUpdateIntervalMs),
                    responseCacheUpdateIntervalMs);
        }

        try {
            Monitors.registerObject(this);
        } catch (Throwable e) {
            logger.warn("Cannot register the JMX monitor for the InstanceRegistry", e);
        }
    }
    ....
}

心跳續(xù)約<客戶端發(fā)送>

入口

心跳只可能在注冊之后開始,所以在看注冊相關(guān)的代碼的時候可以留意一下有關(guān)heartbeat相關(guān)的內(nèi)容。

這里就提兩個點

  1. DiscoveryClient#DiscoveryClient
  2. DiscoveryClient#initScheduledTasks

這兩個 都有和心跳相關(guān)的線程池 或者 定時任務(wù),那就從這里著手

DiscoveryClient#DiscoveryClient

構(gòu)造方法種創(chuàng)建了一個線程池

heartbeatExecutor = new ThreadPoolExecutor(
    1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
    new SynchronousQueue<Runnable>(),
    new ThreadFactoryBuilder()
    .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
    .setDaemon(true)
    .build()
);

DiscoveryClient#initScheduledTasks

這里構(gòu)建了一個Task 然后最終走到了com.netflix.discovery.TimedSupervisorTask#run,這個方法其實就是將HeartbeatThread作為一個task 交由 heartbeatExecutor里面執(zhí)行

heartbeatTask = new TimedSupervisorTask(
        "heartbeat",
        scheduler,
        heartbeatExecutor,
        renewalIntervalInSecs,
        TimeUnit.SECONDS,
        expBackOffBound,
        new HeartbeatThread()
);
// 線程調(diào)度
scheduler.schedule(heartbeatTask, renewalIntervalInSecs, TimeUnit.SECONDS);

有個小疑問:

為啥不直接heartbeatExecutor.submit(new HeartbeatThread()),為啥要費勁弄一個TimedSupervisorTask類 然后使用 ScheduledExecutorService.schedule呢?

其實看一眼邏輯 無非就是處理了一些異常情況

DiscoveryClient.HeartbeatThread

其實最終還是起一個HeartbeatThread的線程

private class HeartbeatThread implements Runnable {

    public void run() {
        if (renew()) {
            lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
        }
    }
}

DiscoveryClient#renew

這里無非還是一個遠程調(diào)用而已

EurekaHttpResponse<InstanceInfo> httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);

com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient#sendHeartBeat

這里只傳入了狀態(tài)和臟時間戳

String urlPath = "apps/" + appName + '/' + id;

WebResource webResource = jerseyClient.resource(serviceUrl)
        .path(urlPath)
        .queryParam("status", info.getStatus().toString())
        .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString());
if (overriddenStatus != null) {
    webResource = webResource.queryParam("overriddenstatus", overriddenStatus.name());
}
Builder requestBuilder = webResource.getRequestBuilder();
addExtraHeaders(requestBuilder);
ClientResponse response = requestBuilder.put(ClientResponse.class);

心跳續(xù)約<服務(wù)端接收>

InstanceResource#renewLease

服務(wù)端接收請求的地方 核心代碼就是 registry.renew(app.getName(), id, isFromReplicaNode);

org.springframework.cloud.netflix.eureka.server.InstanceRegistry#renew

public boolean renew(final String appName, final String serverId, boolean isReplication) {
    
    List<Application> applications = getSortedApplications();
    for (Application input : applications) {
        if (input.getName().equals(appName)) {
            InstanceInfo instance = null;
            for (InstanceInfo info : input.getInstances()) {
                if (info.getId().equals(serverId)) {
                    instance = info;
                    break;
                }
            }
            // 發(fā)布事件
            publishEvent(new EurekaInstanceRenewedEvent(this, appName, serverId,
                    instance, isReplication));
            break;
        }
    }
    // com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#renew
    return super.renew(appName, serverId, isReplication);
}

com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#renew

public boolean renew(final String appName, final String id, final boolean isReplication) {
    // 
    if (super.renew(appName, id, isReplication)) {
        // 同步其他節(jié)點
        replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);
        return true;
    }
    return false;
}

com.netflix.eureka.registry.AbstractInstanceRegistry#renew

public boolean renew(String appName, String id, boolean isReplication) {
    RENEW.increment(isReplication);
    Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
    Lease<InstanceInfo> leaseToRenew = null;
    if (gMap != null) {
        leaseToRenew = gMap.get(id);
    }
    // lease doesn't exist
    if (leaseToRenew == null) {
        RENEW_NOT_FOUND.increment(isReplication);
        return false;
    } else {
        InstanceInfo instanceInfo = leaseToRenew.getHolder();
        if (instanceInfo != null) {
            // touchASGCache(instanceInfo.getASGName());
            // 感覺這個方法比較重要
            InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(
                    instanceInfo, leaseToRenew, isReplication);
            if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
                RENEW_NOT_FOUND.increment(isReplication);
                return false;
            }
            if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
                
                instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
            }
        }
        // 計數(shù)器遞增
        renewsLastMin.increment();
        // 更新 最后更新時間戳
        leaseToRenew.renew();
        return true;
    }
}

自我保護機制

自我保護機制的觸發(fā)條件:在15分鐘內(nèi)超過85%的客戶端都沒有正常的心跳,那么Eureka就認為客戶端與注冊中心之間出現(xiàn)了網(wǎng)絡(luò)故障,Eureka Server自動進入自我保護機制。

進入自我保護機制后,Eureka Server會做以下的事情:

  1. Eureka Server不再剔除注冊表中失去心跳連接的服務(wù)
  2. Eureka Server可以繼續(xù)接受新服務(wù)的注冊和查詢請求,但不會將信息同步到其他節(jié)點中,保證當前節(jié)點依然可用
  3. 當網(wǎng)絡(luò)穩(wěn)定后,當前Eureka Server的新注冊信息才會被同步到其他節(jié)點中

EurekaServerBootstrap

這個類以及其中方法的由來:

  1. EurekaServerAutoConfiguration中進行了實例化 注入了IoC容器
  2. EurekaServerBootstrap#contextInitialized 這個方法是用來做自我保護機制的
  3. contextInitializedEurekaServerInitializerConfiguration#start 調(diào)用

調(diào)用鏈:org.springframework.cloud.netflix.eureka.server.EurekaServerBootstrap#contextInitialized

org.springframework.cloud.netflix.eureka.server.EurekaServerBootstrap#initEurekaServerContext

com.netflix.eureka.registry.InstanceRegistry#openForTraffic

InstanceRegistry#openForTraffic

單純調(diào)用了父類的方法 com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#openForTraffic

@Override
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
    // Renewals happen every 30 seconds and for a minute it should be a factor of 2.
    this.expectedNumberOfClientsSendingRenews = count;
    // 更新每分鐘的續(xù)約閾值
    updateRenewsPerMinThreshold();
    logger.info("Got {} instances from neighboring DS node", count);
    logger.info("Renew threshold is: {}", numberOfRenewsPerMinThreshold);
    this.startupTime = System.currentTimeMillis();
    if (count > 0) {
        this.peerInstancesTransferEmptyOnStartup = false;
    }
    DataCenterInfo.Name selfName = applicationInfoManager.getInfo().getDataCenterInfo().getName();
    boolean isAws = Name.Amazon == selfName;
    if (isAws && serverConfig.shouldPrimeAwsReplicaConnections()) {
        logger.info("Priming AWS connections for all replicas..");
        primeAwsReplicas(applicationInfoManager);
    }
    logger.info("Changing status to UP");
    applicationInfoManager.setInstanceStatus(InstanceStatus.UP);
    // 重點在這里
    super.postInit();
}

com.netflix.eureka.registry.AbstractInstanceRegistry#updateRenewsPerMinThreshold

  • expectedNumberOfClientsSendingRenews 這個值是預(yù)期的 有幾個客戶端就是幾個

  • serverConfig.getExpectedClientRenewalIntervalSeconds() 默認等于 30

  • serverConfig.getRenewalPercentThreshold() 默認等于 0.85

protected void updateRenewsPerMinThreshold() {
    this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfClientsSendingRenews
            * (60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds())
            * serverConfig.getRenewalPercentThreshold());
}

AbstractInstanceRegistry#postInit

protected void postInit() {
    // 開啟一個定時任務(wù) 統(tǒng)計 每分鐘 心跳續(xù)約的次數(shù)
    renewsLastMin.start();
    if (evictionTaskRef.get() != null) {
        evictionTaskRef.get().cancel();
    }
    // 定時器的定時任務(wù)提交 每60s執(zhí)行一次 serverConfig.getEvictionIntervalTimerInMs()=60*1000
    evictionTaskRef.set(new EvictionTask());
    evictionTimer.schedule(evictionTaskRef.get(), serverConfig.getEvictionIntervalTimerInMs(),
                           serverConfig.getEvictionIntervalTimerInMs());
}

EvictionTask里面的run方法 最終調(diào)用了com.netflix.eureka.registry.AbstractInstanceRegistry#evict(long)

AbstractInstanceRegistry#evict

public void evict(long additionalLeaseMs) {
    
    // 判斷是否需要開啟自我保護
    if (!isLeaseExpirationEnabled()) {
        logger.debug("DS: lease expiration is currently disabled.");
        return;
    }

    
    // 先收集所有的過期項,以隨機的順序驅(qū)逐他們。
    // 對于大規(guī)模的驅(qū)逐項集合,如果我們不隨機驅(qū)逐,我們可能會在自我保護開始之前把整個應(yīng)用程序抹去。
    // 通過隨機化,影響應(yīng)該均勻分布在所有應(yīng)用程序中。
    List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
    for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) {
        Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue();
        if (leaseMap != null) {
            for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) {
                Lease<InstanceInfo> lease = leaseEntry.getValue();
                if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
                    expiredLeases.add(lease);
                }
            }
        }
    }

    
    // 判斷是不是 超過85%的客戶端都沒有正常的心跳
    int registrySize = (int) getLocalRegistrySize();
    int registrySizeThreshold = registrySize * serverConfig.getRenewalPercentThreshold();
    int evictionLimit = registrySize - registrySizeThreshold;
    int toEvict = Math.min(expiredLeases.size(), evictionLimit);
    if (toEvict > 0) {
       
        Random random = new Random(System.currentTimeMillis());
        for (int i = 0; i < toEvict; i++) {
            // Pick a random item (Knuth shuffle algorithm)
            int next = i + random.nextInt(expiredLeases.size() - i);
            Collections.swap(expiredLeases, i, next);
            Lease<InstanceInfo> lease = expiredLeases.get(i);

            String appName = lease.getHolder().getAppName();
            String id = lease.getHolder().getId();
            EXPIRED.increment();
            logger.warn("DS: Registry: expired lease for {}/{}", appName, id);
            // 下線操作
            internalCancel(appName, id, false);
        }
    }
}

客戶端拉取服務(wù)節(jié)點信息

客戶端的配置eureka.client.fetch-registry在默認的情況下是true,這個配置表示是否從注冊中心拉取服務(wù)信息。在構(gòu)造方法DiscoveryClient#DiscoveryClient里面有一段判斷這個配置的內(nèi)容:

if (clientConfig.shouldFetchRegistry()) {
    try {
        // 這里就是拉取的入口
        boolean primaryFetchRegistryResult = fetchRegistry(false);
        ....
    } catch (Throwable th) {
        throw new IllegalStateException(th);
    }
}
private boolean fetchRegistry(boolean forceFullRegistryFetch) {
    Stopwatch tracer = FETCH_REGISTRY_TIMER.start();

    try {
        // 如果增量被禁用 或者是 第一次來 那就執(zhí)行這個
        Applications applications = getApplications();

        if (clientConfig.shouldDisableDelta()
                || (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress()))
                || forceFullRegistryFetch
                || (applications == null)
                || (applications.getRegisteredApplications().size() == 0)
                || (applications.getVersion() == -1)) //Client application does not have latest library supporting delta
        {
            // 全量拉取 調(diào)用接口 apps/
            getAndStoreFullRegistry();
        } else {
            // 增量拉取 調(diào)用接口 apps/delta
            getAndUpdateDelta(applications);
        }
        applications.setAppsHashCode(applications.getReconcileHashCode());
        logTotalInstances();
    } catch (Throwable e) {
        return false;
    } finally {
        if (tracer != null) {
            tracer.stop();
        }
    }

    // Notify about cache refresh before updating the instance remote status
    onCacheRefreshed();

    // Update remote status based on refreshed data held in the cache
    updateInstanceRemoteStatus();

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

客戶端保存服務(wù)節(jié)點信息

在上一步 已經(jīng)將服務(wù)信息通過遠程通信的方式請求而來,如何保存 其實就是如何處理響應(yīng)數(shù)據(jù)的問題。

這部分的代碼不想看了。

節(jié)點間數(shù)據(jù)同步

在上面的章節(jié) 注冊,有一點說了以下節(jié)點間的同步

PeerAwareInstanceRegistryImpl#replicateToPeers

與Ribbon的整合

Ribbon獲取服務(wù)列表的地方是com.netflix.loadbalancer.DynamicServerListLoadBalancer#updateListOfServers,在這個方法里面有一個ServerList.getUpdatedListOfServers(),如果是從配置文件的話,那么子類是ConfigurationBasedServerList,現(xiàn)在換成了Eureka,那子類是org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList

DomainExtractingServerList#getUpdatedListOfServers

public List<DiscoveryEnabledServer> getUpdatedListOfServers() {
    // this.list = com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList
    List<DiscoveryEnabledServer> servers = setZones(
            this.list.getUpdatedListOfServers());
    return servers;
}

DiscoveryEnabledNIWSServerList#getUpdatedListOfServers

public List<DiscoveryEnabledServer> getUpdatedListOfServers(){
    return obtainServersViaDiscovery();
}

private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
    List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();

    if (eurekaClientProvider == null || eurekaClientProvider.get() == null) {
        logger.warn("EurekaClient has not been initialized yet, returning an empty list");
        return new ArrayList<DiscoveryEnabledServer>();
    }

    EurekaClient eurekaClient = eurekaClientProvider.get();
    if (vipAddresses!=null){
        for (String vipAddress : vipAddresses.split(",")) {
            // if targetRegion is null, it will be interpreted as the same region of client
            List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
            for (InstanceInfo ii : listOfInstanceInfo) {
                if (ii.getStatus().equals(InstanceStatus.UP)) {

                    if(shouldUseOverridePort){
                        if(logger.isDebugEnabled()){
                            logger.debug("Overriding port on client name: " + clientName + " to " + overridePort);
                        }

                        // copy is necessary since the InstanceInfo builder just uses the original reference,
                        // and we don't want to corrupt the global eureka copy of the object which may be
                        // used by other clients in our system
                        InstanceInfo copy = new InstanceInfo(ii);

                        if(isSecure){
                            ii = new InstanceInfo.Builder(copy).setSecurePort(overridePort).build();
                        }else{
                            ii = new InstanceInfo.Builder(copy).setPort(overridePort).build();
                        }
                    }

                    DiscoveryEnabledServer des = createServer(ii, isSecure, shouldUseIpAddr);
                    serverList.add(des);
                }
            }
            if (serverList.size()>0 && prioritizeVipAddressBasedServers){
                break; // if the current vipAddress has servers, we dont use subsequent vipAddress based servers
            }
        }
    }
    return serverList;
}

Eureka Server小記

服務(wù)端的搭建過程很簡單,除了引入依賴與yml文件的配置之外,就是一個Spring Boot的項目,在啟動類上標注了@EnableEurekaServer,就完成了。因此入手點從這個注解開始。

@EnableEurekaServer

這個注解設(shè)計的極為巧妙,通過注釋也可以看出來,他的作用就是激活這個Eureka Server的配置的;但是他是怎么激活的呢?重點就在@Import(EurekaServerMarkerConfiguration.class) 導(dǎo)入的這個配置也極其簡單,聲明了一個Marker的Bean。這里就很奇怪了,憑什么一個簡單的Marker就能激活配置呢?

在Spring自己實現(xiàn)的類SPI的機制下,一個類配置在了spring.factories中時,Spring 啟動時就可以掃描到這里面配置的類,也就是EurekaServerAutoConfiguration。但是EurekaServerAutoConfiguration上的條件就是當前環(huán)境中必須有Marker這個Bean。這樣@EnableEurekaServer就可以實現(xiàn)他注釋所說的了。

為什么會這樣設(shè)計呢?個人理解,可能就是為了避免錯誤的引入依賴而導(dǎo)致項目加載很多不必要的類吧。

/**
 * Annotation to activate Eureka Server related configuration.
 * {@link EurekaServerAutoConfiguration}
 * 用以 <激活 Eureka Server 相關(guān)的配置> 的注解
 */
@Import(EurekaServerMarkerConfiguration.class)
public @interface EnableEurekaServer {

}

/**
 * Responsible for adding in a marker bean to activate
 * {@link EurekaServerAutoConfiguration}.
 * 負責添加一個標記Bean 來激活 配置類EurekaServerAutoConfiguration
 */
@Configuration(proxyBeanMethods = false)
public class EurekaServerMarkerConfiguration {

    @Bean
    public Marker eurekaServerMarkerBean() {
        return new Marker();
    }

    class Marker {}
}

// 重點就是這個條件
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
public class EurekaServerAutoConfiguration implements WebMvcConfigurer {
}

控制臺入口 EurekaController

這個是Spring提供的控制臺接口,按照Spring Web MVC的方式定義的。還是比較容易看懂。

http://localhost:8761 相關(guān)接口

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內(nèi)容