精华内容
下载资源
问答
  • Eureka Eureka is a REST (Representational State Transfer) based service that is primarily used in the AWS cloud for locating services for the purpose of load balancing and failover of middle-tier ...
  • Eureka服务发现原理 eureka:多个eureka server组成一个高可用的eureka集群,集群内的eureka节点之间通过同步复制的方式更新注册信息,保持服务注册信息的一致性。 Application Service: 是一个服务的提供者,为...

    Eureka服务发现原理

    在这里插入图片描述

    • eureka:多个eureka server组成一个高可用的eureka集群,集群内的eureka节点之间通过同步复制的方式更新注册信息,保持服务注册信息的一致性。
    • Application Service: 是一个服务的提供者,为其他服务提供特定的业务服务;它也是一个eureka client,会将自己注册到eureka server中,同时获取注册表缓存在本地。
    • Application Client:服务调用者,会去调用application service提供的服务,通过http api 的方式调用。application client将自己注册到eureka server中,同时获取注册表信息,从信息表中找到所需的服务发起远程调用。
    • Replicate:eureka server之间注册表信息的同步机制,使得eureka集群中不同注册表中服务实例信息保持一致。

    Eureka注册原理解析:

    Eureka Client

    Eureka Client 将很多与Eureka Server的交互工作隐藏,开发人员是不用管这部分工作的。Eureka Client工作流程:

    在这里插入图片描述

    图片来自SpringCloud微服务架构进阶

    client注册的流程其实很简单,无非就是以下几个步骤:

    • 先读eureka server的配置信息,从而知道eureka server在哪,以便后面进行注册
    • 接着再读取自己的配置信息,然后将自己的信息封装在InstanceInfo实例中,等下将实例发送到eureka server中
    • 通过上面步骤已经知道eureka server的地址了,此时先把注册拉取到本地缓存起来
    • 将上面封装的InstanceInfo实例发送到eureka server进行注册,然后初始化心跳检测以及缓存刷新(这些都是通过开启后台线程完成的)
    • 再次拉取注册表更新本地注册表信息


    InstanceInfo实例部分代码:可以看到几个主要的信息比如:instanceId、ipAddr、port、appName、appGroupName、lastUpdatedTimestamp等

    
    @ProvidedBy(EurekaConfigBasedInstanceInfoProvider.class)
    @Serializer("com.netflix.discovery.converters.EntityBodyConverter")
    @XStreamAlias("instance")
    @JsonRootName("instance")
    public class InstanceInfo {
        private static final String VERSION_UNKNOWN = "unknown";
        private static final Logger logger = LoggerFactory.getLogger(InstanceInfo.class);
        public static final int DEFAULT_PORT = 7001;
        public static final int DEFAULT_SECURE_PORT = 7002;
        public static final int DEFAULT_COUNTRY_ID = 1;
        private volatile String instanceId;
        private volatile String appName;
        @Auto
        private volatile String appGroupName;
        private volatile String ipAddr;
        private static final String SID_DEFAULT = "na";
        /** @deprecated */
        @Deprecated
        private volatile String sid;
        private volatile int port;
        private volatile int securePort;
        ...
        @Auto
        private volatile Long lastUpdatedTimestamp;
        
        ...
        }
    
    Eureka Server

    Eureka Server是一个开箱即用的服务注册中心,开发人员只需要导入相关的依赖即可,它提供以下功能:

    • 服务注册
    • 接受eureka client发送过来的心跳检测
    • 服务剔除(当一个client心跳超时)
    • 服务下线(client请求关闭)
    • 集群同步(不同eureka server中注册表信息同步)
    • 获取注册表中服务实例信息(每个eureka server同时也是一个eureka client,eureka server可以把自己注册到eureka集群中)

    服务注册时eureka client会将服务实例InstanceInfo发送到eureka server。

    • 服务实例通过ConcurrentHashMap保存在内存中,在服务注册的过程中会先获取一个锁,防止其他线程对registry注册表进行数据操作,避免数据不一致。
      eureka server接收到client发送过来的InstanceInfo实例时,会先根据唯一的instanceId检查注册表中是否已存在该实例。
    • 如果没有该实例,说明这是一次新的注册服务,server会将InstanceInfo信息保存到注册表中
    • 如果存在该实例,说明这是一次心跳检测或者实例信息更新操作,会比较lastUpdatedTimestamp字段保留最新的InstanceInfo实例信息。
    集群同步

    为了保持注册表的一致性,集群中的eureka server需要一个同步机制来维护注册表。

    集群同步分为两个部分,我个人将其理解为pull和push:

    • pull:eureka server启动时从集群中其他节点pull注册表信息进行本地注册表的初始化
    • push:eureka server对本地的注册表进行更新操作后(包括增删改)会将操作push(也就是同步)到其他节点中

    集群模式下的eureka server,多个eureka server之间互相注册并同步注册表信息,即使集群中个别节点出现故障或宕机,集群还是能够稳定提供服务。

    缺陷:Eureka Server集群的节点之间是通过http的方式进行同步的,网络存在不可靠性,为了保持高可用性,eureka server 牺牲了数据一致性,eureka server不满足 CAP找那个C(数据一致性)。

    展开全文
  • 第二章Eureka注册发现原理解析笔记

    千次阅读 2020-03-11 21:25:10
    一、EurekaClient运行流程分析 EurekaClient为了简化开发人员的工作量,将很多与EurekaServer交互的工作隐藏起来,自主完成,具体完成的工作如下,代码见spring-cloud-netflix-eureka-client-2.0.0.RELEASE.jar。 ...

    一、EurekaClient运行流程分析

    EurekaClient为了简化开发人员的工作量,将很多与EurekaServer交互的工作隐藏起来,自主完成,具体完成的工作如下,代码见spring-cloud-netflix-eureka-client-2.0.0.RELEASE.jar。

    代码Git地址:https://gitee.com/hankin_chj/springcloud-micro-service.git

    1、应用启动阶段:

    • 读取与Eureka Server交互的配置信息,封装成EurekaClientConfig;
    • 读取自身服务实例配置信息,封装成EurekalnstanceConfig;
    • 从Eureka server拉取注册表信息并缓存到本地;
    • 服务注册;
    • 通过定时任务初始化发送心跳、缓存刷新(拉取注册表信息更新本地缓存)和按需注册(监控服务实例信息变化,决定是否重新发起注册,更新注册表中的服务实例元数据)定时任务。

    2、应用执行阶段

    • 定时发送心跳到Eureka Server中维持在注册表的租约;
    • 定时从Eureka Server中拉取注册表信息,更新本地注册表缓存;
    • 监控应用自身信息变化,若发生变化,需要重新发起服务注册。

    3、应用销毁阶段

    从Eureka Server注销自身服务实例。

    4、应用启动与运行阶段分析

    Eureka Client通过Starter的方式引人依赖,Spring Boot将会为项目使用以下的自动配置类:

    • EurekaClientAutoConfiguration:EurekeClient自动配置类,负责Eureka关键Beans的配置和初始化,如AppplicationInfoManager和EurekaClientConfig等。
    • RibbonEurekaAutoConfiguration:Ribbon负载均衡相关配置。
    • EurekaDiscoveryClientConfiguration:配置自动注册和应用的健康检查器。

    这些注解对应的配置信息:

    spring-cloud-netflix-eureka-client-2.0.0.RELEASE.jar/META-INF/spring.factories

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration,\
    org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceAutoConfiguration,\
    org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,\
    org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,\
    org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration
    org.springframework.cloud.bootstrap.BootstrapConfiguration=\
    org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration

    4.1、读取应用自身配置

    通过EurekaClientAutoConfiguration配置类,Spring boot帮助Eureka Client完成很多必要Bean的属性读取和配置。

    1)EurekaClientConfig封装Eureka Client与Eureka Server交互所需要的配置信息。Spring Cloud为其提供了一个默认配置类的EurekaClientConfigBean,可以在配置文件中通过前缀eureka.client属性名进行属性覆盖。源码见org.springframework.cloud.netflix.eureka.EurekaClientConfigBean:

    @ConfigurationProperties(EurekaClientConfigBean.PREFIX)
    public class EurekaClientConfigBean implements EurekaClientConfig {
       public static final String PREFIX = "eureka.client";

    2)ApplicationInfoManager作为应用信息管理器,管理服务实例的信息类InstanceInfo和服务实例的配置信息类EurekaInstanceConfig。

    它的构造方法如下:

    @Inject
    public ApplicationInfoManager(EurekaInstanceConfig config, InstanceInfo instanceInfo, OptionalArgs optionalArgs) {
        this.config = config;
        this.instanceInfo = instanceInfo;
        this.listeners = new ConcurrentHashMap<String, StatusChangeListener>();
        if (optionalArgs != null) {
            this.instanceStatusMapper = optionalArgs.getInstanceStatusMapper();
        } else {
            this.instanceStatusMapper = NO_OP_MAPPER;
        }
        instance = this;
    }

    3)InstanceInfo封装将被发送到EurekaServer进行服务注册的服务实例元数据。它在EurekServer的注册表中代表一个服务实例,其他服务实例可以通过Instancelnfo了解该服的相关信息从而发起服务请求。

    4)EurekaInstanceConfig封装EurekaClient自身服务实例的配置信息,主要用于构建InstanceInfo通常这些信息在配置文件中的eureka.instance前缀下进行设置, SpringCloud通过EurekalnstanceConfigBean配置类提供了默认配置。

    5)DiscoveryClientSpring Cloud中定义用来服务发现的客户端接口,对于DiscoveryClient可以具体查看EurekaDiscoveryClient,EurekaDiscoveryClient又借助EurekaClient来实现。

    另外netflix包里面还有一个DiscoveryClient,按名字翻译其实就是服务发现客户端,他是整个EurekaClient的核心,是与EurekaServer进行交互的核心所在。

     

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

    @Singleton
    public class DiscoveryClient implements EurekaClient {

    public class EurekaDiscoveryClient implements DiscoveryClient {
       public static final String DESCRIPTION = "Spring Cloud Eureka Discovery Client";
       private final EurekaInstanceConfig config;
       private final EurekaClient eurekaClient;
       public EurekaDiscoveryClient(EurekaInstanceConfig config, EurekaClient eurekaClient) {
          this.config = config;
          this.eurekaClient = eurekaClient;
       }

    4.2、运行服务

    DiscoveryClient核心方法代码如下所示:

    DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config,
                    AbstractDiscoveryClientOptionalArgs args,Provider<BackupRegistry> backupRegistryProvider) {
        if (args != null) {
            this.healthCheckHandlerProvider = args.healthCheckHandlerProvider;
            this.healthCheckCallbackProvider = args.healthCheckCallbackProvider;
            this.eventListeners.addAll(args.getEventListeners());
            this.preRegistrationHandler = args.preRegistrationHandler;
        } else {
            this.healthCheckCallbackProvider = null;
            this.healthCheckHandlerProvider = null;
            this.preRegistrationHandler = null;
        }
        this.applicationInfoManager = applicationInfoManager;
        InstanceInfo myInfo = applicationInfoManager.getInfo();
        clientConfig = config;
        staticClientConfig = clientConfig;
        transportConfig = config.getTransportConfig();
        instanceInfo = myInfo;
        if (myInfo != null) {
            appPathIdentifier = instanceInfo.getAppName() + "/" + instanceInfo.getId();
        } else {
            logger.warn("Setting instanceInfo to a passed in null value");
        }
        //传入BackupRegistry(NotImplementedRegistryImpl)备份注册中心
        this.backupRegistryProvider = backupRegistryProvider;
        this.urlRandomizer = new EndpointUtils.InstanceInfoBasedUrlRandomizer(instanceInfo);
        localRegionApps.set(new Applications());
        fetchRegistryGeneration = new AtomicLong(0);
        remoteRegionsToFetch = new AtomicReference<String>(clientConfig.fetchRegistryForRemoteRegions());
        remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(","));
        //从eureka server拉起注册表信息  eureka.client.fetch-register
        if (config.shouldFetchRegistry()) {
            this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX

    + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
        } else {
            this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
        }
        // 当前的客户端是否应该注册到erueka中eureka.client.register-with-eureka  
        if (config.shouldRegisterWithEureka()) {
            this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX

     + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
        } else {
            this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
        }
        logger.info("Initializing Eureka in region {}", clientConfig.getRegion());
        //如果既不需要注册,也不需要拉去数据,直接返回,初始结束
        if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
            logger.info("Client configured to neither register nor query for data.");
            scheduler = null;
            heartbeatExecutor = null;
            cacheRefreshExecutor = null;
            eurekaTransport = null;
            instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), clientConfig.getRegion());
            // This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
            // to work with DI'd DiscoveryClient
            DiscoveryManager.getInstance().setDiscoveryClient(this);
            DiscoveryManager.getInstance().setEurekaClientConfig(config);
            initTimestampMs = System.currentTimeMillis();
            logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",
                    initTimestampMs, this.getApplications().size());
            return;  // no need to setup up an network tasks and we are done
        }
        try {
            //线程池大小为2,一个用户发送心跳,另外1个缓存刷新
            // default size of 2 - 1 each for heartbeat and cacheRefresh
            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
            //初始化client与server交互的jersey客户端
            eurekaTransport = new EurekaTransport();
            scheduleServerEndpointTask(eurekaTransport, args);
            AzToRegionMapper azToRegionMapper;
            if (clientConfig.shouldUseDnsForFetchingServiceUrls()) {
                azToRegionMapper = new DNSBasedAzToRegionMapper(clientConfig);
            } else {
                azToRegionMapper = new PropertyBasedAzToRegionMapper(clientConfig);
            }
            if (null != remoteRegionsToFetch.get()) {
                azToRegionMapper.setRegionsToFetch(remoteRegionsToFetch.get().split(","));
            }
            instanceRegionChecker = new InstanceRegionChecker(azToRegionMapper, clientConfig.getRegion());
        } catch (Throwable e) {
            throw new RuntimeException("Failed to initialize DiscoveryClient!", e);
        }
        //拉取注册表的信息
        if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
            fetchRegistryFromBackup();
        }
        //将服务实例进行注册
        // call and execute the pre registration handler before all background tasks (inc registration) is started
        if (this.preRegistrationHandler != null) {
            this.preRegistrationHandler.beforeRegistration();
        }
        if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
            try {
                if (!register() ) {
                    throw new IllegalStateException("Registration error at startup. Invalid server response.");
                }
            } catch (Throwable th) {
                logger.error("Registration error at startup: {}", th.getMessage());
                throw new IllegalStateException(th);
            }
        }
        // finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
        //初始心跳定时任务,缓存刷新
        initScheduledTasks();
        try {
            Monitors.registerObject(this);
        } catch (Throwable e) {
            logger.warn("Cannot register timers", e);
        }
        DiscoveryManager.getInstance().setDiscoveryClient(this);
        DiscoveryManager.getInstance().setEurekaClientConfig(config);
        initTimestampMs = System.currentTimeMillis();
        logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",
        initTimestampMs, this.getApplications().size());
    }

    4.3、总结

    DiscoveryClient构造函数做的事情:

    • 相关配置赋值
    • 备份注册中心的初始化,实际什么事都没做
    • 拉取Server注册表中的信息
    • 注册前的预处理
    • 向Server注册自身
    • 初始心跳定时任务,缓存刷新等定时任务

    5、Eureka客户端流程源码分析

    5.1、拉取Server注册表中的信息代码分析(重要步骤)

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

    判断是否全量拉去方法源码fetchRegistry(false):

    //是否全量拉去
    private boolean fetchRegistry(boolean forceFullRegistryFetch) {
        Stopwatch tracer = FETCH_REGISTRY_TIMER.start();
        try {  // If the delta is disabled or if it is the first time, get all applications
            //如果增量拉取被禁止全量拉去
            Applications applications = getApplications();
            if (clientConfig.shouldDisableDelta()
                    || (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress()))
                    || forceFullRegistryFetch
                    || (applications == null)
                    || (applications.getRegisteredApplications().size() == 0)
                    || (applications.getVersion() == -1))  {
                //全量拉取
                getAndStoreFullRegistry();
            } else {
                //增量拉取
                getAndUpdateDelta(applications);
            }
            applications.setAppsHashCode(applications.getReconcileHashCode());
            //打印注册表上所有服务实例信息
            logTotalInstances();
        } catch (Throwable e) {
     logger.error(PREFIX + "{} - was unable to refresh its cache! status = {}", appPathIdentifier, e.getMessage(), e);
            return false;
        } finally {
            if (tracer != null) {
                tracer.stop();
            }
        }
        onCacheRefreshed();
        // Update remote status based on refreshed data held in the cache
        updateInstanceRemoteStatus();
        // registry was fetched successfully, so return true
        return true;
    }

    5.2、全量拉取源码分析

    一般只有在第一次拉去注册表信息的时候,全量拉取调用 getAndStoreFullRegistry()方法:

    private void getAndStoreFullRegistry() throws Throwable {
        //拉取注册表的版本信息
        long currentUpdateGeneration = fetchRegistryGeneration.get();
        logger.info("Getting all instance registry info from the eureka server");
        Applications apps = null;
        EurekaHttpResponse<Applications> httpResponse = clientConfig.getRegistryRefreshSingleVipAddress() == null
                ? eurekaTransport.queryClient.getApplications(remoteRegionsRef.get())
                : eurekaTransport.queryClient.getVip(clientConfig.getRegistryRefreshSingleVipAddress(), remoteRegionsRef.get());
        //拉取成功
        if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
            apps = httpResponse.getEntity();
        }
        logger.info("The response status is {}", httpResponse.getStatusCode());
    }

    5.3、增量拉取代码分析

    增量拉取,首先根据拉去信息进行判断是否为空,如果为空则进行全量拉去,反之更新本地缓存,getAndUpdateDelta(applications)代码如下:

    private void getAndUpdateDelta(Applications applications) throws Throwable {
        long currentUpdateGeneration = fetchRegistryGeneration.get();
        //拉取信息
        Applications delta = null;
        EurekaHttpResponse<Applications> httpResponse = eurekaTransport.queryClient.getDelta(remoteRegionsRef.get());
        if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
            delta = httpResponse.getEntity();
        }
        //如果拉取失败进行全量拉取
        if (delta == null) {
            logger.warn("The server does not allow the delta revision to be applied because it is not safe. "
                    + "Hence got the full registry.");
            getAndStoreFullRegistry();
        } else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
            logger.debug("Got delta update with apps hashcode {}", delta.getAppsHashCode());
            String reconcileHashCode = "";
            if (fetchRegistryUpdateLock.tryLock()) {
                try {
                    //跟新本地缓存
                    updateDelta(delta);
                    reconcileHashCode = getReconcileHashCode(applications);
                } finally {
                    fetchRegistryUpdateLock.unlock();
                }
            } else {
                logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta");
            }
            // There is a diff in number of instances for some reason
            if (!reconcileHashCode.equals(delta.getAppsHashCode()) || clientConfig.shouldLogDeltaDiff()) {
                reconcileAndLogDifference(delta, reconcileHashCode);  // this makes a remoteCall
            }
        } else {
            logger.warn("Not updating application delta as another thread is updating it already");
            logger.debug("Ignoring delta update with apps hashcode {}, as another thread is updating it already", delta.getAppsHashCode());
        }
    }

    5.4、服务注册(重要步骤)

    if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
        try {
            if (!register() ) { // 注册方法
                throw new IllegalStateException("Registration error at startup. Invalid server response.");
            }
        } catch (Throwable th) {
            logger.error("Registration error at startup: {}", th.getMessage());
            throw new IllegalStateException(th);
        }
    }

    register()方法负责服务的注册源码分析:

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

    5.5、定时任务(重要步骤)

    initScheduledTasks()是负责定时任务的相关方法,具体源码实现如下:

    private void initScheduledTasks() {
        if (clientConfig.shouldFetchRegistry()) {
             // 拉取服务默认30秒,eureka.client.register-fetch-interval-seconds
            int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
            int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
            scheduler.schedule( new TimedSupervisorTask(  "cacheRefresh", scheduler, cacheRefreshExecutor,
                     registryFetchIntervalSeconds, TimeUnit.SECONDS,expBackOffBound, new CacheRefreshThread()),
                    registryFetchIntervalSeconds, TimeUnit.SECONDS);
        }
        if (clientConfig.shouldRegisterWithEureka()) {
            int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
            int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
            logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);
            // Heartbeat timer 心跳服务,默认30秒
            scheduler.schedule(new TimedSupervisorTask( "heartbeat", scheduler, heartbeatExecutor,
                            renewalIntervalInSecs,TimeUnit.SECONDS,expBackOffBound,new HeartbeatThread()),
                    renewalIntervalInSecs, TimeUnit.SECONDS);
            // 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");
        }
    }

    5.6、服务下线

    com.netflix.discovery.DiscoveryClient #shutdown

    @PreDestroy
    @Override
    public synchronized void shutdown() {
        if (isShutdown.compareAndSet(false, true)) {
            logger.info("Shutting down DiscoveryClient ...");
            if (statusChangeListener != null && applicationInfoManager != null) {
                //注销状态监听器
                applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId());
            }
            //取消定时任务
            cancelScheduledTasks();
            // If APPINFO was registered
            if (applicationInfoManager != null && clientConfig.shouldRegisterWithEureka()
                    && clientConfig.shouldUnregisterOnShutdown()) {
                applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
                unregister();
            }
            //关闭与server连接的客户端
            if (eurekaTransport != null) {
                eurekaTransport.shutdown();
            }
            //关闭相关监控
            heartbeatStalenessMonitor.shutdown();
            registryStalenessMonitor.shutdown();
            logger.info("Completed shut down of DiscoveryClient");
        }
    }

    二、EurekaServer运行流程分析

    1、流程总览

    EurekaServer是服务的注册中心,负责Eureka Client的相关信息注册,主要职责:

    • 服务注册
    • 接受心跳服务(client端通过定时任务初始化发送心跳)
    • 服务剔除
    • 服务下线
    • 集群同步

    1.1、服务配置启动信息:

    spring-cloud-netflix-eureka-server-2.0.0.RELEASE.jar!\META-INF\spring.factories

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration

    EurekaServerAutoConfiguration类代码:

    @Configuration
    @Import(EurekaServerInitializerConfiguration.class)
    @ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
    @EnableConfigurationProperties({ EurekaDashboardProperties.class,InstanceRegistryProperties.class })
    @PropertySource("classpath:/eureka/server.properties")
    public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter {
       private static final String[] EUREKA_PACKAGES = new String[] { "com.netflix.discovery","com.netflix.eureka" };
       @Autowired
       private ApplicationInfoManager applicationInfoManager;
       @Autowired
       private EurekaServerConfig eurekaServerConfig;
       @Autowired
       private EurekaClientConfig eurekaClientConfig;
       @Autowired
       private EurekaClient eurekaClient;
       @Autowired
       private InstanceRegistryProperties instanceRegistryProperties;

    EurekaServerAutoConfiguration 是通过配置文件注册的,注册方法入口:

    @Bean
    public PeerAwareInstanceRegistry peerAwareInstanceRegistry( ServerCodecs serverCodecs) {
       this.eurekaClient.getApplications(); // force initialization
       return new InstanceRegistry(this.eurekaServerConfig, this.eurekaClientConfig,
             serverCodecs, this.eurekaClient,
             this.instanceRegistryProperties.getExpectedNumberOfRenewsPerMin(),
             this.instanceRegistryProperties.getDefaultOpenForTrafficCount());
    }

    这里面有个InstanceRegistry()方法就是重点需要关注的了。

    1.2、整个接口关系如下如所示:

     

    所有业务的核心方式实现主要集中在PeerAwareInstanceRegistryImpl与AbstractInstanceRegistry这两个类中,其中服务注册、接受心跳服务、服务剔除都是在AbstractInstanceRegistry中处理的,服务下线与集群同步是在PeerAwareInstanceRegistryImpl类中处理的。

    1.3、首先看下最上层的接口

    public interface LeaseManager<T> {
        //注册
        void register(T var1, int var2, boolean var3);
        //下线
        boolean cancel(String var1, String var2, boolean var3);
        //跟新
        boolean renew(String var1, String var2, boolean var3);
        //服务剔除
        void evict();
    }

    PeerAwareInstanceRegistryImpl是一个子类的实现,在上面的基础上扩展对集群的同步操作,使Eureka Server集群信息保持一致。

    2、服务注册

    com.netflix.eureka.registry.AbstractInstanceRegistry#register(); 这方法是负责服务的注册的:

    public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
        try {
            //获取读锁
            this.read.lock();
            // TODO gMap  其实可以发现,这里注册中心其实是个ConcurrentHashMap
            Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(registrant.getAppName());
            EurekaMonitors.REGISTER.increment(isReplication);
            if(gMap == null) {
                ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap();
                //TODO  key为appName,如果存在,返回存在的值,否则添加,返回null
                gMap = (Map)this.registry.putIfAbsent(registrant.getAppName(), gNewMap);
                if(gMap == null) {
                    gMap = gNewMap;
                }
            }
            //TODO 根据instanceId获取实例的租约
            Lease<InstanceInfo> existingLease = (Lease)((Map)gMap).get(registrant.getId());
            if(existingLease != null && existingLease.getHolder() != null) {
                Long existingLastDirtyTimestamp = ((InstanceInfo)existingLease.getHolder()).getLastDirtyTimestamp();
                Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
                // TODO 如果该实例的租约已经存在,比较最后的更新时间戳大小,取最大值的注册信息信息
                if(existingLastDirtyTimestamp.longValue() > registrationLastDirtyTimestamp.longValue()) {
                    registrant = (InstanceInfo)existingLease.getHolder();
                }
            } else {
                Object var6 = this.lock;
                //TODO 如果租约不存在,注册一个新的实例
                synchronized(this.lock) {
                    if(this.expectedNumberOfRenewsPerMin > 0) {
                        this.expectedNumberOfRenewsPerMin += 2;
                        this.numberOfRenewsPerMinThreshold = (int)((double)this.expectedNumberOfRenewsPerMin

    * this.serverConfig.getRenewalPercentThreshold());
                    }
                }
                logger.debug("No previous lease information found; it is new registration");
            }
            //TODO 创建新的租约
            Lease<InstanceInfo> lease = new Lease(registrant, leaseDuration);
            if(existingLease != null) {
                lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
            }
            //TODO 保存租约到map中
            ((Map)gMap).put(registrant.getId(), lease);
            //TODO 获得最近注册队列
            AbstractInstanceRegistry.CircularQueue var20 = this.recentRegisteredQueue;
            synchronized(this.recentRegisteredQueue) {
                this.recentRegisteredQueue.add(new Pair(Long.valueOf(System.currentTimeMillis()), registrant.getAppName() + "(" + registrant.getId() + ")"));
            }
            if(!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
                logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the overrides", registrant.getOverriddenStatus(), registrant.getId());
                if(!this.overriddenInstanceStatusMap.containsKey(registrant.getId())) {
                    logger.info("Not found overridden id {} and hence adding it", registrant.getId());
                    this.overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
                }
            }
            InstanceStatus overriddenStatusFromMap = (InstanceStatus)this.overriddenInstanceStatusMap.get(registrant.getId());
            if(overriddenStatusFromMap != null) {
                logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
                registrant.setOverriddenStatus(overriddenStatusFromMap);
            }
            InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(registrant, existingLease, isReplication);
            registrant.setStatusWithoutDirty(overriddenInstanceStatus);
            if(InstanceStatus.UP.equals(registrant.getStatus())) {
                lease.serviceUp();
            }
            registrant.setActionType(ActionType.ADDED);
            this.recentlyChangedQueue.add(new AbstractInstanceRegistry.RecentlyChangedItem(lease));
            registrant.setLastUpdatedTimestamp();
            this.invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
        } finally { //释放锁
            this.read.unlock();
        }
    }

    3、接受心跳服务

    在Eureka Client完成服务的注册后,需要定时向Eureka Server发送心跳请求(默认30s),维持自己在EurekaServer的租约有效性com.netflix.eureka.registry.AbstractInstanceRegistry.renew(appName, id,

    isReplication);方法源码如下所示:

    public boolean renew(String appName, String id, boolean isReplication) {
        EurekaMonitors.RENEW.increment(isReplication);
        //TODO 根据appName获取服务集群租约集合
        Map<String, Lease<InstanceInfo>> gMap = (Map) this.registry.get(appName);
        Lease<InstanceInfo> leaseToRenew = null;
        if (gMap != null) {
            leaseToRenew = (Lease) gMap.get(id);
        }
        //TODO 如果租约不存在,直接返回false
        if (leaseToRenew == null) {
            EurekaMonitors.RENEW_NOT_FOUND.increment(isReplication);
            logger.warn("DS: Registry: lease doesn't exist, registering resource: {} - {}", appName, id);
            return false;
        } else {
            InstanceInfo instanceInfo = (InstanceInfo) leaseToRenew.getHolder();
            if (instanceInfo != null) {
                //TODO 得到服务的最终状态
                InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(instanceInfo, leaseToRenew, isReplication);
                if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
                    //TODO 如果状态为UNKNOWN,取消续约
                    logger.info("Instance status UNKNOWN possibly due to deleted override for instance {}; re-register required", instanceInfo.getId());
                    EurekaMonitors.RENEW_NOT_FOUND.increment(isReplication);
                    return false;
                }
                if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
                    logger.info("The instance status {} is different from overridden instance status {} for instance {}. Hence setting the status to overridden status", new Object[]{instanceInfo.getStatus().name(), instanceInfo.getOverriddenStatus().name(), instanceInfo.getId()});
                    instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
                }
            }
            this.renewsLastMin.increment();
            //TODO 跟新续约有效时间
            leaseToRenew.renew();
            return true;
        }
    }

    4、服务剔除

    如果Eureka Client在注册后,由于服务的崩溃或网络异常导致既没有续约,也没有下线,那么服务就处于不可知的状态,需要剔除这些服务。

    代码见com.netflix.eureka.registry.AbstractInstanceRegistry#evict(long)方法。

    这是个定时任务调用的方法:com.netflix.eureka.registry.AbstractInstanceRegistry#postInit()中使用

    AbstractInstanceRegistry.EvictionTask()负责调用(默认60s)。

    1)AbstractInstanceRegistry#evict(long)剔除方法源码:

    public void evict(long additionalLeaseMs) {
        logger.debug("Running the evict task");
        //TODO 如果自我保护状态,不允许剔除服务前面配置关系自我保护机制代码判断
        if(!this.isLeaseExpirationEnabled()) {
            logger.debug("DS: lease expiration is currently disabled.");
        } else {
            List<Lease<InstanceInfo>> expiredLeases = new ArrayList();
            //TODO 遍历注册表registry,获取所有过期的租约
            Iterator var4 = this.registry.entrySet().iterator();
            while(true) {
                Map leaseMap;
                do {
                    if(!var4.hasNext()) {
                        //TODO 获取注册表租约总数
                        int registrySize = (int)this.getLocalRegistrySize();
                        int registrySizeThreshold = (int)((double)registrySize * this.serverConfig.getRenewalPercentThreshold());
                        //TODO 计算最多允许剔除的阈值
                        int evictionLimit = registrySize - registrySizeThreshold;
                        //TODO 两者中取小的值,为本常剔除的数量
                        int toEvict = Math.min(expiredLeases.size(), evictionLimit);
                        if(toEvict > 0) {
                            logger.info("Evicting {} items (expired={}, evictionLimit={})", new Object[]{Integer.valueOf(toEvict), Integer.valueOf(expiredLeases.size()), Integer.valueOf(evictionLimit)});
                            Random random = new Random(System.currentTimeMillis());
                            //TODO 逐个剔除
                            for(int i = 0; i < toEvict; ++i) {
                                int next = i + random.nextInt(expiredLeases.size() - i);
                                Collections.swap(expiredLeases, i, next);
                                Lease<InstanceInfo> lease = (Lease)expiredLeases.get(i);
                                String appName = ((InstanceInfo)lease.getHolder()).getAppName();
                                String id = ((InstanceInfo)lease.getHolder()).getId();
                                EurekaMonitors.EXPIRED.increment();
                                logger.warn("DS: Registry: expired lease for {}/{}", appName, id);
                                //TODO 剔除
                                this.internalCancel(appName, id, false);
                            }
                        }
                        return;
                    }
                    Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry = (Entry)var4.next();
                    leaseMap = (Map)groupEntry.getValue();
                } while(leaseMap == null);
                Iterator var7 = leaseMap.entrySet().iterator();
                while(var7.hasNext()) {
                    Entry<String, Lease<InstanceInfo>> leaseEntry = (Entry)var7.next();
                    Lease<InstanceInfo> lease = (Lease)leaseEntry.getValue();
                    if(lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
                        expiredLeases.add(lease);
                    }
                }
            }
        }
    }

    5、服务下线

    EurekaClient在应用销毁时候,会向Eureka Server发送下线请求,对于服务端的服务下线,其主要代码对应在com.netflix.eureka.registry.AbstractInstanceRegistry#cancel()方法中

    PeerAwareInstanceRegistryImpl#cancel()方法调用父类方法AbstractInstanceRegistry#cancel()方法实现:

    @Override
    public boolean cancel(final String appName, final String id, final boolean isReplication) {
        if (super.cancel(appName, id, isReplication)) {
            replicateToPeers(Action.Cancel, appName, id, null, null, isReplication);
            synchronized (lock) {
                if (this.expectedNumberOfRenewsPerMin > 0) {
                    // Since the client wants to cancel it, reduce the threshold (1 for 30 seconds, 2 for a minute)
                    this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin - 2;
                    this.numberOfRenewsPerMinThreshold =
                            (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
                }
            }
            return true;
        }
        return false;
    }

    com.netflix.eureka.registry.AbstractInstanceRegistry#cancel方法源码:

    public boolean cancel(String appName, String id, boolean isReplication) {
        return this.internalCancel(appName, id, isReplication);
    }

    protected boolean internalCancel(String appName, String id, boolean isReplication) {
        boolean var10;
        try {
            //读锁,防止被其他线程进行修改
            this.read.lock();
            EurekaMonitors.CANCEL.increment(isReplication);
            //根据appName获取服务实例集群
            Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(appName);
            Lease<InstanceInfo> leaseToCancel = null;
            //TODO 移除服务实例租约
            if(gMap != null) {
                leaseToCancel = (Lease)gMap.remove(id);
            }
            AbstractInstanceRegistry.CircularQueue var6 = this.recentCanceledQueue;
            synchronized(this.recentCanceledQueue) {
                this.recentCanceledQueue.add(new Pair(Long.valueOf(System.currentTimeMillis()), appName + "(" + id + ")"));
            }
            InstanceStatus instanceStatus = (InstanceStatus)this.overriddenInstanceStatusMap.remove(id);
            if(instanceStatus != null) {
                logger.debug("Removed instance id {} from the overridden map which has value {}", id, instanceStatus.name());
            }
            //TODO 租约不存在,返回false
            if(leaseToCancel == null) {
                EurekaMonitors.CANCEL_NOT_FOUND.increment(isReplication);
                logger.warn("DS: Registry: cancel failed because Lease is not registered for: {}/{}", appName, id);
                boolean var17 = false;
                return var17;
            }
            //TODO 设置租约的下线时间
            leaseToCancel.cancel();
            InstanceInfo instanceInfo = (InstanceInfo)leaseToCancel.getHolder();
            String vip = null;
            String svip = null;
            if(instanceInfo != null) {
                instanceInfo.setActionType(ActionType.DELETED);
                this.recentlyChangedQueue.add(new AbstractInstanceRegistry.RecentlyChangedItem(leaseToCancel));
                instanceInfo.setLastUpdatedTimestamp();
                vip = instanceInfo.getVIPAddress();
                svip = instanceInfo.getSecureVipAddress();
            }
            //TODO 设置缓存过期
            this.invalidateCache(appName, vip, svip);
            logger.info("Cancelled instance {}/{} (replication={})", new Object[]{appName, id, Boolean.valueOf(isReplication)});
            var10 = true;
        } finally {//释放锁
            this.read.unlock();
        }
        return var10;
    }

    6、集群同步

    如果Eureka Server是通过集群方式进行部署,为了为维护整个集群中注册表数据一致性所以集群同步也是非常重要得事情。

    集群同步分为两部分:

    • EurekaServer在启动过程中从他的peer节点中拉取注册表信息,并讲这些服务实例注册到本地注册表中;
    • 另一部分是eureka server每次对本地注册表进行操作时,同时会讲操作同步到他的peer节点中,达到数据一致;

    6.1、Eureka Server初始化本地注册表信息

    在eureka server启动过程中,会从它的peer节点中拉取注册表来初始化本地注册表,这部分主要通过

    com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#syncUp()方法实现,他从可能存在的peer节点中,拉取peer节点中的注册表信息,并将其中的服务实例的信息注册到本地注册表中。

    public int syncUp() {
        // Copy entire entry from neighboring DS node
        int count = 0;
        for (int i = 0; ((i < serverConfig.getRegistrySyncRetries()) && (count == 0)); i++) {
            if (i > 0) {
                try {
                    //TODO 根据配置休停下
                    Thread.sleep(serverConfig.getRegistrySyncRetryWaitMs());
                } catch (InterruptedException e) {
                    logger.warn("Interrupted during registry transfer..");
                    break;
                }
            }
            //TODO 获取所有的服务实例
            Applications apps = eurekaClient.getApplications();
            for (Application app : apps.getRegisteredApplications()) {
                for (InstanceInfo instance : app.getInstances()) {
                    try {
                        //TODO 判断是否可以注册
                        if (isRegisterable(instance)) {
                            //TODO 注册到自身的注册表中
                            register(instance, instance.getLeaseInfo().getDurationInSecs(), true);
                            count++;
                        }
                    } catch (Throwable t) {
                        logger.error("During DS init copy", t);
                    }
                }
            }
        }
        return count;
    }

    通过这一步保证了eureka启动时的数据一致性。

    6.2、Eureka Server之间注册表信息同步复制

    为了保证Eureka Server集群运行时候注册表的信息一致性,每个eureka server在对本地注册表进行管理操作时,会将相应的信息同步到peer节点中。其中以下几个方法:

    com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#cancel();

    com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#register();

    com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#renew();

    等方法中,都回调用com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateToPeers()方法:

    private void replicateToPeers(Action action, String appName, String id,InstanceInfo info /* optional */,
                                  InstanceStatus newStatus /* optional */, boolean isReplication) {
        Stopwatch tracer = action.getTimer().start();
        try {
            if (isReplication) {
                numberOfReplicationsLastMin.increment();
            }
            // If it is a replication already, do not replicate again as this will create a poison replication
            if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
                return;
            }
            //TODO 向peer集群中的每一个peer进行同步
            for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
                // If the url represents this host, do not replicate to yourself.
                if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
                    continue;
                }
                //TODO 根据action调用不同的同步请求
                replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
            }
        } finally {
            tracer.stop();
        }
    }

    根据action调用不同的同步请求:

    private void replicateInstanceActionsToPeers(Action action, String appName, String id,
                    InstanceInfo info, InstanceStatus newStatus,PeerEurekaNode node) {
        try {
            InstanceInfo infoFromRegistry = null;
            CurrentRequestVersion.set(Version.V2);
            switch (action) {
                case Cancel:
                    node.cancel(appName, id);
                    break;
                case Heartbeat:
                    InstanceStatus overriddenStatus = overriddenInstanceStatusMap.get(id);
                    infoFromRegistry = getInstanceByAppAndId(appName, id, false);
                    node.heartbeat(appName, id, infoFromRegistry, overriddenStatus, false);
                    break;
                case Register:
                    node.register(info);
                    break;
                case StatusUpdate:
                    infoFromRegistry = getInstanceByAppAndId(appName, id, false);
                    node.statusUpdate(appName, id, newStatus, infoFromRegistry);
                    break;
                case DeleteStatusOverride:
                    infoFromRegistry = getInstanceByAppAndId(appName, id, false);
                    node.deleteStatusOverride(appName, id, infoFromRegistry);
                    break;
            }
        } catch (Throwable t) {
            logger.error("Cannot replicate information to {} for action {}", node.getServiceUrl(), action.name(), t);
        }
    }

     

    public void heartbeat(final String appName, final String id,final InstanceInfo info,
                           final InstanceStatus overriddenStatus, boolean primeConnection) throws Throwable {
        if (primeConnection) {
            // We do not care about the result for priming request.
            replicationClient.sendHeartBeat(appName, id, info, overriddenStatus);
            return;
        }
        ReplicationTask replicationTask = new InstanceReplicationTask(targetHost, Action.Heartbeat, info, overriddenStatus, false) {
            @Override
            public EurekaHttpResponse<InstanceInfo> execute() throws Throwable {
                return replicationClient.sendHeartBeat(appName, id, info, overriddenStatus);
            }
            @Override
            public void handleFailure(int statusCode, Object responseEntity) throws Throwable {
                super.handleFailure(statusCode, responseEntity);
                if (statusCode == 404) {
                    logger.warn("{}: missing entry.", getTaskName());
                    if (info != null) {
                        logger.warn("{}: cannot find instance id {} and hence replicating the instance with status {}",
                                getTaskName(), info.getId(), info.getStatus());
                        register(info);
                    }
                } else if (config.shouldSyncWhenTimestampDiffers()) {
                    InstanceInfo peerInstanceInfo = (InstanceInfo) responseEntity;
                    if (peerInstanceInfo != null) {
                        syncInstancesIfTimestampDiffers(appName, id, info, peerInstanceInfo);
                    }
                }
            }
        };
        long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
        batchingDispatcher.process(taskId("heartbeat", info), replicationTask, expiryTime);
    }

    展开全文
  • Eureka注册中心原理

    2021-04-07 09:52:40
     Eureka是Netflix开发的服务发现框架,本身是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的。SpringCloud将它集成在其子项目spring-cloud-netflix中,以...

    一、Eureka简介

      Eureka是Netflix开发的服务发现框架,本身是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的。SpringCloud将它集成在其子项目spring-cloud-netflix中,以实现SpringCloud的服务发现功能。

      1、Eureka组件

      Eureka包含两个组件:Eureka Server和Eureka Client。

      1.1 Eureka Server

      Eureka Server提供服务注册服务,各个节点启动后,会在Eureka Server中进行注册,这样Eureka Server中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。
      Eureka Server本身也是一个服务,默认情况下会自动注册到Eureka注册中心。
      如果搭建单机版的Eureka Server注册中心,则需要配置取消Eureka Server的自动注册逻辑。毕竟当前服务注册到当前服务代表的注册中心中是一个说不通的逻辑。
      Eureka Server通过Register、Get、Renew等接口提供服务的注册、发现和心跳检测等服务。

      2.1 Eureka Client

      Eureka Client是一个java客户端,用于简化与Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳,默认周期为30秒,如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除(默认90秒)
      Eureka Client分为两个角色,分别是:Application Service(Service Provider)和Application Client(Service Consumer)

      2.1.1 Application Service

      服务提供方,是注册到Eureka Server中的服务。

      2.1.2 Application Client

      服务消费方,通过Eureka Server发现服务,并消费。

      在这里,Application Service和Application Client不是绝对上的定义,因为Provider在提供服务的同时,也可以消费其他Provider提供的服务;Consumer在消费服务的同时,也可以提供对外服务。

     2、Eureka Server架构原理简介

    Register(服务注册):把自己的IP和端口注册给Eureka。
    Renew(服务续约):发送心跳包,每30秒发送一次。告诉Eureka自己还活着。
    Cancel(服务下线):当provider关闭时会向Eureka发送消息,把自己从服务列表中删除。防止consumer调用到不存在的服务。
    Get Registry(获取服务注册列表):获取其他服务列表。
    Replicate(集群中数据同步):eureka集群中的数据复制与同步。
    Make Remote Call(远程调用):完成服务的远程调用。

    三、集群版Eureka Server

      注册中心作为微服务架构中的核心功能,其重要性不言而喻。所以单机版的Eureka Server在可靠性上并不符合现在的互联网开发环境。集群版的Eureka Server才是商业开发中的选择。

      Eureka Server注册中心的集群和Dubbo的ZooKeeper注册中心集群在结构上有很大的不同。

      ZooKeeper注册中心集群搭建后,集群中各节点呈现主从关系,集群中只有主节点对外提供服务的注册和发现功能,从节点相当于备份节点,只有主节点宕机时,从节点会选举出一个新的主节点,继续提供服务的注册和发现功能。  而Eureka Server注册中心集群中每个节点都是平等的,集群中的所有节点同时对外提供服务的发现和注册等功能。同时集群中每个Eureka Server节点又是一个微服务,也就是说,每个节点都可以在集群中的其他节点上注册当前服务。又因为每个节点都是注册中心,所以节点之间又可以相互注册当前节点中已注册的服务,并发现其他节点中已注册的服务。所以Eureka Server注册中心集群版在搭建过程中有很多的方式,找到一个最合适最可靠的搭建方式才能称为一个称职的程序员。

    四、Eureka Server安全认证

      Eureka Server作为Spring Cloud中的服务注册中心,如果可以任意访问的话,那么其安全性太低。所以Spring Cloud中也有为Eureka Server提供安全认证的方式。可以使用spring-boot-starter-security组件来为Eureka Server增加安全认证。

      POM依赖:

    <!-- spring boot security安全认证启动器 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

      修改全局配置文件,在全局配置文件中,开启基于http basic的安全认证。

    
    eureka.instance.hostname=eurekaserver1
    # 使用http basic安全认证语法,在集群通信中增加认证信息。  http://用户名:密码@地址:端口/eureka/
    eureka.client.serviceUrl.defaultZone=http://test:123456@eurekaserver2:8761/eureka/

    五、CAP定理

      CAP原则又称CAP定理,指的是在一个分布式系统中,Consistency(数据一致性)、 Availability(服务可用性)、Partition tolerance(分区容错性),三者不可兼得。CAP由Eric Brewer在2000年PODC会议上提出。该猜想在提出两年后被证明成立,成为我们熟知的CAP定理。

    分布式系统CAP定理

    数据一致性

    (Consistency)

    数据一致性(Consistency)

    也叫做数据原子性系统在执行某项操作后仍然处于一致的状态。在分布式系统中,更新操作执行成功后所有的用户都应该读到最新的值,这样的系统被认为是具有强一致性的。等同于所有节点访问同一份最新的数据副本。

    优点: 数据一致,没有数据错误可能。

    缺点: 相对效率降低。

    服务可用性

    (Availablity)

    每一个操作总是能够在一定的时间内返回结果,这里需要注意的是"一定时间内"和"返回结果"。一定时间内指的是,在可以容忍的范围内返回结果,结果可以是成功或者是失败。

    分区容错性

    (Partition-torlerance)

    在网络分区的情况下,被分隔的节点仍能正常对外提供服务(分布式集群,数据被分布存储在不同的服务器上,无论什么情况,服务器都能正常被访问)
    定律:任何分布式系统只可同时满足二点,没法三者兼顾。
    CA,放弃P 如果想避免分区容错性问题的发生,一种做法是将所有的数据(与事务相关的)/服务都放在一台机器上。虽然无法100%保证系统不会出错,但不会碰到由分区带来的负面效果。当然这个选择会严重的影响系统的扩展性。
    CP,放弃A 相对于放弃"分区容错性"来说,其反面就是放弃可用性。一旦遇到分区容错故障,那么受到影响的服务需要等待一定时间,因此在等待时间内系统无法对外提供服务。
    AP,放弃C 这里所说的放弃一致性,并不是完全放弃数据一致性,而是放弃数据的强一致性,而保留数据的最终一致性。以网络购物为例,对只剩下一件库存的商品,如果同时接受了两个订单,那么较晚的订单将被告知商品告罄。

      Eureka和ZooKeeper的特性

     

    六、服务保护

      1 服务保护模式

      服务保护模式(自我保护模式):一般情况下,微服务在Eureka上注册后,会每30秒发送心跳包,Eureka通过心跳来判断服务时候健康,同时会定期删除超过90秒没有发送心跳服务。

      导致Eureka Server接收不到心跳包的可能:一是微服务自身的原因,二是微服务与Eureka之间的网络故障。通常微服务的自身的故障只会导致个别服务出现故障,一般不会出现大面积故障,而网络故障通常会导致Eureka Server在短时间内无法收到大批心跳。虑到这个区别,Eureka设置了一个阀值,当判断挂掉的服务的数量超过阀值时,Eureka Server认为很大程度上出现了网络故障,将不再删除心跳过期的服务。

      那么这个阀值是多少呢?Eureka Server在运行期间,会统计心跳失败的比例在15分钟内是否低于85%,如果低于85%,Eureka Server则任务是网络故障,不会删除心跳过期服务。

      这种服务保护算法叫做Eureka Server的服务保护模式。

      这种不删除的,90秒没有心跳的服务,称为无效服务,但是还是保存在服务列表中。如果Consumer到注册中心发现服务,则Eureka Server会将所有好的数据(有效服务数据)和坏的数据(无效服务数据)都返回给Consumer。

      2 服务保护模式的存在必要性

      因为同时保留"好数据"与"坏数据"总比丢掉任何数据要更好,当网络故障恢复后,Eureka Server会退出"自我保护模式"。

      Eureka还有客户端缓存功能(也就是微服务的缓存功能)。即便Eureka Server集群中所有节点都宕机失效,微服务的Provider和Consumer都能正常通信。

      微服务的负载均衡策略会自动剔除死亡的微服务节点(Robbin)。

      只要Consumer不关闭,缓存始终有效,直到一个应用下的所有Provider访问都无效的时候,才会访问Eureka Server重新获取服务列表。

      3 关闭服务保护模式

      可以通过全局配置文件来关闭服务保护模式,商业项目中不推荐关闭服务保护,因为网络不可靠很容易造成网络波动、延迟、断线的可能。如果关闭了服务保护,可能导致大量的服务反复注册、删除、再注册。导致效率降低。在商业项目中,服务的数量一般都是几十个,大型的商业项目中服务的数量可能上百、数百,甚至上千:

    # 关闭自我保护:true为开启自我保护,false为关闭自我保护
    eureka.server.enableSelfPreservation=false
    # 清理间隔(单位:毫秒,默认是60*1000),当服务心跳失效后多久,删除服务。
    eureka.server.eviction.interval-timer-in-ms=60000

      4 优雅关闭服务(优雅停服)

      在Spring Cloud中,可以通过HTTP请求的方式,通知Eureka Client优雅停服,这个请求一旦发送到Eureka Client,那么Eureka Client会发送一个shutdown请求到Eureka Server,Eureka Server接收到这个shutdown请求后,会在服务列表中标记这个服务的状态为down,同时Eureka Client应用自动关闭。这个过程就是优雅停服。

      如果使用了优雅停服,则不需要再关闭Eureka Server的服务保护模式。

      POM依赖:
      优雅停服是通过Eureka Client发起的,所以需要在Eureka Client中增加新的依赖,这个依赖是autuator组件,添加下述依赖即可。

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-actuator</artifactId>
    </dependency>

      修改全局配置文件:

      Eureka Client默认不开启优雅停服功能,需要在全局配置文件中新增如下内容:

    # 启用shutdown,优雅停服功能
    endpoints.shutdown.enabled=true
    # 禁用密码验证
    endpoints.shutdown.sensitive=false

      发起shutdown请求:

      必须通过POST请求向Eureka Client发起一个shutdown请求。请求路径为:http://ip:port/shutdown。可以通过任意技术实现,如:HTTPClient、form表单,AJAX等。

      建议使用优雅停服方式来关闭Application Service/Application Client服务。

    展开全文
  • 注册中心是分布式开发的核心组件之一,而eureka是spring cloud推荐的注册中心实现,因此对于Java开发同学来说,还是有必要学习eureka的,特别是其架构及设计思想。 官方文档定义是:Eureka is a REST ...

    注册中心是分布式开发的核心组件之一,而eureka是spring cloud推荐的注册中心实现,因此对于Java开发同学来说,还是有必要学习eureka的,特别是其架构及设计思想。

    官方文档定义是:Eureka is a REST (Representational State Transfer) based service that is primarily used in the AWS cloud for locating services for the purpose of load balancing and failover of middle-tier servers. We call this service, the Eureka Server. Eureka also comes with a Java-based client component,the Eureka Client, which makes interactions with the service much easier. The client also has a built-in load balancer that does basic round-robin load balancing.

    Eureka是一个REST (Representational State Transfer)服务,它主要用于AWS云,用于定位服务,以实现中间层服务器的负载平衡和故障转移,我们称此服务为Eureka服务器。Eureka也有一个基于java的客户端组件,Eureka客户端,这使得与服务的交互更加容易,同时客户端也有一个内置的负载平衡器,它执行基本的循环负载均衡

    Eureka提供了完整的Service Registry和Service Discovery实现,并且也经受住了Netflix自己的生产环境考验,相对使用起来会比较省心(同时Spring Cloud还有一套非常完善的开源代码来整合Eureka,所以使用起来非常方便)

    本文主要内容有:eureka基础概念及架构、服务发现原理、eureka server/client流程分析及优缺点分析,最后做个小结。由于本文侧重于原理分析,因此eureka(结合spring cloud)的使用就不再赘述了,感兴趣的小伙伴可以看下 程序猿DD[1] 关于spring cloud的相关教程。

    eureka基础

    eureka架构图

     

    Eureka Server:提供服务注册和发现,多个Eureka Server之间会同步数据,做到状态一致(最终一致性)

    Service Provider:服务提供方,将自身服务注册到Eureka,从而使服务消费方能够找到•Service Consumer:服务消费方,从Eureka获取注册服务列表,从而能够消费服务

    注意,上图中的3个角色都是逻辑角色,在实际运行中,这几个角色甚至可以是同一个项目(JVM进程)中。

    自我保护机制

    自我保护机制主要在Eureka Client和Eureka Server之间存在网络分区的情况下发挥保护作用,在服务器端和客户端都有对应实现。假设在某种特定的情况下(如网络故障), Eureka Client和Eureka Server无法进行通信,此时Eureka Client无法向Eureka Server发起注册和续约请求,Eureka Server中就可能因注册表中的服务实例租约出现大量过期而面临被剔除的危险,然而此时的Eureka Client可能是处于健康状态的(可接受服务访问),如果直接将注册表中大量过期的服务实例租约剔除显然是不合理的,自我保护机制提高了eureka的服务可用性。

    当自我保护机制触发时,Eureka不再从注册列表中移除因为长时间没收到心跳而应该过期的服务,仍能查询服务信息并且接受新服务注册请求,也就是其他功能是正常的。这里思考下,如果eureka节点A触发自我保护机制过程中,有新服务注册了然后网络回复后,其他peer节点能收到A节点的新服务信息,数据同步到peer过程中是有网络异常重试的,也就是说,是能保证最终一致性的。

    服务发现原理

    eureka server可以集群部署,多个节点之间会进行(异步方式)数据同步,保证数据最终一致性,Eureka Server作为一个开箱即用的服务注册中心,提供的功能包括:服务注册、接收服务心跳、服务剔除、服务下线等。需要注意的是,Eureka Server同时也是一个Eureka Client,在不禁止Eureka Server的客户端行为时,它会向它配置文件中的其他Eureka Server进行拉取注册表、服务注册和发送心跳等操作。

    eureka server端通过appNameinstanceInfoId来唯一区分一个服务实例,服务实例信息是保存在哪里呢?其实就是一个Map中:

    // 第一层的key是appName,第二层的key是instanceInfoId
    private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry 
        = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();

    服务注册

    Service Provider启动时会将服务信息(InstanceInfo)发送给eureka server,eureka server接收到之后会写入registry中,服务注册默认过期时间DEFAULT_DURATION_IN_SECS = 90秒。InstanceInfo写入到本地registry之后,然后同步给其他peer节点,对应方法com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateToPeers

    写入本地registry

    服务信息(InstanceInfo)保存在Lease中,写入本地registry对应方法com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#register,Lease统一保存在内存的ConcurrentHashMap中,在服务注册过程中,首先加个读锁,然后从registry中判断该Lease是否已存在,如果已存在则比较lastDirtyTimestamp时间戳,取二者最大的服务信息,避免发生数据覆盖。使用InstanceInfo创建一个新的InstanceInfo:

    if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
        // 已存在Lease则比较时间戳,取二者最大值
        registrant = existingLease.getHolder();
    }
    Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
    if (existingLease != null) {
        // 已存在Lease则取上次up时间戳
        lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
    }
    
    public Lease(T r, int durationInSecs) {
        holder = r;
        registrationTimestamp = System.currentTimeMillis(); // 当前时间
        lastUpdateTimestamp = registrationTimestamp;
        duration = (durationInSecs * 1000);
    }

    不知道小伙伴看了上述方法的代码有没有这样的疑问?

    通过读锁并且 registry 的读取和写入不是原子的,那么在并发时其实是有可能发生数据覆盖的,如果发生数据覆盖岂不是有问题了!猛一看会以为脏数据不就是有问题么?换个角度想,脏数据就一定有问题么? 其实针对这个问题,eureka的处理方式是没有问题的,该方法并发时,针对InstanceInfo Lease的构造,二者的信息是基本一致的,因为registrationTimestamp取的就是当前时间,所以并并发的数据不会产生问题。

    同步给其他peer

    InstanceInfo写入到本地registry之后,然后同步给其他peer节点,对应方法com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateToPeers。如果当前节点接收到的InstanceInfo本身就是另一个节点同步来的,则不会继续同步给其他节点,避免形成“广播效应”;InstanceInfo同步时会排除当前节点。

    InstanceInfo的状态有依以下几种:Heartbeat, Register, Cancel, StatusUpdate, DeleteStatusOverride,默认情况下同步操作时批量异步执行的,同步请求首先缓存到Map中,key为requestType+appName+id,然后由发送线程将请求发送到peer节点。

    Peer之间的状态是采用异步的方式同步的,所以不保证节点间的状态一定是一致的,不过基本能保证最终状态是一致的。结合服务发现的场景,实际上也并不需要节点间的状态强一致。在一段时间内(比如30秒),节点A比节点B多一个服务实例或少一个服务实例,在业务上也是完全可以接受的(Service Consumer侧一般也会实现错误重试和负载均衡机制)。所以按照CAP理论,Eureka的选择就是放弃C,选择AP。 如果同步过程中,出现了异常怎么办呢,这时会根据异常信息做对应的处理,如果是读取超时或者网络连接异常,则稍后重试;如果其他异常则打印错误日志不再后续处理。

    服务续约

    Renew(服务续约)操作由Service Provider定期调用,类似于heartbeat。主要是用来告诉Eureka Server Service Provider还活着,避免服务被剔除掉。renew接口实现方式和register基本一致:首先更新自身状态,再同步到其它Peer,服务续约也就是把过期时间设置为当前时间加上duration的值。

    注意:服务注册如果InstanceInfo不存在则加入,存在则更新;而服务预约只是进行更新,如果InstanceInfo不存在直接返回false。

    服务下线

    Cancel(服务下线)一般在Service Provider shutdown的时候调用,用来把自身的服务从Eureka Server中删除,以防客户端调用不存在的服务,eureka从本地”删除“(设置为删除状态)之后会同步给其他peer,对应方法com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#cancel

    服务失效剔除

    Eureka Server中有一个EvictionTask,用于检查服务是否失效。Eviction(失效服务剔除)用来定期(默认为每60秒)在Eureka Server检测失效的服务,检测标准就是超过一定时间没有Renew的服务。默认失效时间为90秒,也就是如果有服务超过90秒没有向Eureka Server发起Renew请求的话,就会被当做失效服务剔除掉。失效时间可以通过eureka.instance.leaseExpirationDurationInSeconds进行配置,定期扫描时间可以通过eureka.server.evictionIntervalTimerInMs进行配置。

    服务剔除#evict方法中有很多限制,都是为了保证Eureka Server的可用性:比如自我保护时期不能进行服务剔除操作、过期操作是分批进行、服务剔除是随机逐个剔除,剔除均匀分布在所有应用中,防止在同一时间内同一服务集群中的服务全部过期被剔除,以致大量剔除发生时,在未进行自我保护前促使了程序的崩溃。

    eureka server/client流程

    服务信息拉取

    Eureka consumer服务信息的拉取分为全量式拉取和增量式拉取,eureka consumer启动时进行全量拉取,运行过程中由定时任务进行增量式拉取,如果网络出现异常,可能导致先拉取的数据被旧数据覆盖(比如上一次拉取线程获取结果较慢,数据已更新情况下使用返回结果再次更新,导致数据版本落后),产生脏数据。对此,eureka通过类型AtomicLong的fetchRegistryGeneration对数据版本进行跟踪,版本不一致则表示此次拉取到的数据已过期。

    fetchRegistryGeneration过程是在拉取数据之前,执行fetchRegistryGeneration.get获取当前版本号,获取到数据之后,通过fetchRegistryGeneration.compareAndSet来判断当前版本号是否已更新。 注意:如果增量式更新出现意外,会再次进行一次全量拉取更新。

    Eureka server的伸缩容

    Eureka Server是怎么知道有多少Peer的呢?Eureka Server在启动后会调用EurekaClientConfig.getEurekaServerServiceUrls来获取所有的Peer节点,并且会定期更新。定期更新频率可以通过eureka.server.peerEurekaNodesUpdateIntervalMs配置。

    这个方法的默认实现是从配置文件读取,所以如果Eureka Server节点相对固定的话,可以通过在配置文件中配置来实现。如果希望能更灵活的控制Eureka Server节点,比如动态扩容/缩容,那么可以override getEurekaServerServiceUrls方法,提供自己的实现,比如我们的项目中会通过数据库读取Eureka Server列表。

    eureka server启动时把自己当做是Service Consumer从其它Peer Eureka获取所有服务的注册信息。然后对每个服务信息,在自己这里执行Register,isReplication=true,从而完成初始化。

    Service Provider

    Service Provider启动时首先时注册到Eureka Service上,这样其他消费者才能进行服务调用,除了在启动时之外,只要实例状态信息有变化,也会注册到Eureka Service。需要注意的是,需要确保配置eureka.client.registerWithEureka=true。register逻辑在方法AbstractJerseyEurekaHttpClient.register中,Service Provider会依次注册到配置的Eureka Server Url上,如果注册出现异常,则会继续注册其他的url。

    Renew操作会在Service Provider端定期发起,用来通知Eureka Server自己还活着。这里instance.leaseRenewalIntervalInSeconds属性表示Renew频率。默认是30秒,也就是每30秒会向Eureka Server发起Renew操作。这部分逻辑在HeartbeatThread类中。在Service Provider服务shutdown的时候,需要及时通知Eureka Server把自己剔除,从而避免客户端调用已经下线的服务,逻辑本身比较简单,通过对方法标记@PreDestroy,从而在服务shutdown的时候会被触发。

    Service Consumer

    Service Consumer这块的实现相对就简单一些,因为它只涉及到从Eureka Server获取服务列表和更新服务列表。Service Consumer在启动时会从Eureka Server获取所有服务列表,并在本地缓存。需要注意的是,需要确保配置eureka.client.shouldFetchRegistry=true。由于在本地有一份Service Registries缓存,所以需要定期更新,定期更新频率可以通过eureka.client.registryFetchIntervalSeconds配置。

    小结

    为什么要用eureka呢,因为分布式开发架构中,任何单点的服务都不能保证不会中断,因此需要服务发现机制,某个节点中断后,服务消费者能及时感知到保证服务高可用。从eureka的设计与实现上来说还是容易理解的,SpringCloud将它集成在自己的子项目spring-cloud-netflix中,实现SpringCloud的服务发现功能。

    注册中心除了用eureka之外,还有zookeeper、consul、nacos等解决方案,他们实现原理不同,各自适用于不同的场景,可按需使用。

    Eureka比ZooKeeper相比优势是什么

    Zookeeper保证CP 当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接受服务直接down掉不可用。也就是说,服务注册功能对可用性的要求要高于一致性。但是zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举leader的时间太长,30 ~ 120s, 且选举期间整个zk集群都是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够最终恢复,但是漫长的选举时间导致的注册长期不可用是不能容忍的。

    Eureka保证AP Eureka看明白了这一点,因此在设计时就优先保证可用性。Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册或时如果发现连接失败,则会自动切换至其它节点,只要有一台Eureka还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。除此之外,Eureka还有一种自我保护机制,如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障。

    eureka有哪些不足: eureka consumer本身有缓存,服务状态更新滞后,最常见的状况就是,服务下线了但是服务消费者还未及时感知,此时调用到已下线服务会导致请求失败,只能依靠consumer端的容错机制来保证。

    展开全文
  • 在前面的文章介绍了,如何使用服务注册发现组件:Eureka,并给出使用示例。本文在此基础上,将会讲解 Eureka 客户端实现的内幕,结合源码深入实现的细节,知其所以然。客户端需要重点关注以下几点:从Eureka Server...
  • 服务发现原理 原理:如下图 1.1、发现原理 1.1.1、服务部署情况 注册中心:部署在上海机房,北京机房,深圳机房 服务提供者:部署在上海机房,深圳机房 消费者:部署在上海机房,北京机房 1.1.2、服务注册情况 ...
  • 在前面的文章介绍了,如何使用服务注册发现组件: Eureka,并给出使用示例。本文在此基础上,将会讲解 Eureka 客户端实现的内幕,结合源码深入实现的细节,知其所以然。客户端需要重点关注以下几点: 从Eureka ...
  • Eureka是Netflix开发的服务发现框架,SpringCloud将它集成在自己的子项目spring-cloud-netflix中,实现SpringCloud的服务发现功能。Eureka包含两个组件:Eureka Server 和Eureka Client。 (1) Eureka Server ...
  • Eureka 服务注册发现原理剖析

    千次阅读 2020-03-27 15:33:38
    1、介绍 Eureka是Netflix开发的服务发现框架,本身是...SpringCloud将它集成在其子项目spring-cloud-netflix中,以实现SpringCloud的服务注册发现的功能。 Eureka包含两个组件:Eureka Server和Eureka Client。 Eu...
  • 一. Eureka简介 Eureka是Netflix开发的服务发现框架,本身是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的。...Eureka Server提供服务注册服务,各个节点启
  •  Eureka是Netflix开发的服务发现框架,本身是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的。SpringCloud将它集成在其子项目spring-cloud-netflix中,以...
  • 解决什么问题➟阐述微服务以及服务注册发现的部分概念➟阐述Eureka服务注册与发现的部分原理及细节为什么需要服务中心过去,每个应用都是一个CPU,一个主机上的单一系统。然而今天,随着大数据和云计算时代的到来,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 590
精华内容 236
关键字:

eureka注册发现原理