关于java:一文给你道破Eureka服务发现慢的原因深度剖析Eureka客户端服务发现原理以及-Eureka服务端服务剔除原理

53次阅读

共计 7514 个字符,预计需要花费 19 分钟才能阅读完成。

我的博客,原文出处 http://www.deepinblog.com/eur…

我的博客地址,原文出处

先间接给出配置让你尝鲜

EurekaServer 端配置

eureka:
  server:
    #Eureka Server 会定时 (距离值是 eureka.server.eviction-interval-timer-in-ms,默认 60s) 进行查看, 如果发现实例在在肯定工夫
    #(此值由客户端设置的 eureka.instance.lease-expiration-duration-in-seconds 定义,默认值为 90s)内没有收到心跳,则会登记此实例。#咱们这里配置每秒钟去检测一次,驱除生效的实例
    eviction-interval-timer-in-ms: 1000
    #敞开一级缓存,让客户端间接从二级缓存去读取,省去各缓存之间的同步的工夫
    use-read-only-response-cache: false

EurekaClient 端 (利用端) 配置

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
    # EurekaClient 每隔多久从 EurekaServer 拉取一次服务列表,默认 30 秒,这里批改为 2 秒钟从注册核心拉取一次
    registry-fetch-interval-seconds: 2
  #租约期限以及续约期限的配置
  instance:
    #租约到期,服务生效工夫,默认值 90 秒,服务超过 90 秒没有产生心跳,EurekaServer 会将服务从列表移除
    #这里批改为 6 秒
    lease-expiration-duration-in-seconds: 6
    #租约续约间隔时间,默认 30 秒,这里批改为 3 秒钟
    lease-renewal-interval-in-seconds: 3

#这里是 Ribbon 缓存实例列表的刷新距离,默认 30 秒钟,这里批改为每秒钟刷新一次实例信息
ribbon:
  ServerListRefreshInterval: 1000

上面给你分析原理

Eureka 服务端详解

服务端缓存

如图所示

服务注册到注册核心后,服务实例信息是存储在注册表中的,也就是内存中。但 Eureka 为了进步响应速度,在外部做了优化,退出了两层的缓存构造,将 Client 须要的实例信息,间接缓存起来,获取的时候 间接从缓存中拿数据而后响应给 Client。

第一层缓存是 readOnlyCacheMap,readOnlyCacheMap 是采纳 ConcurrentHashMap 来存储数据的,次要负责定时与 readWriteCacheMap 进行数据同步,默认同 步工夫为 30 秒一次。

第二层缓存是 readWriteCacheMap,readWriteCacheMap 采纳 Guava 来实现缓存。缓存过期工夫默认为 180 秒,当服务下线、过期、注册、状态变更等操作都会革除此缓存中的数据。

第三层是数据存储层。

Client 获取服务实例数据时,会先从一级缓存中获取,如果一级缓存中不存在,再从二级缓存中获取,如果二级缓存也不存在,会触发缓存的加载,从存储层拉取数据到缓存中,而后再返回给 Client。

Eureka 之所以设计二级缓存机制,也是为了进步 Eureka Server 的响应速度,毛病是缓存会导致 Client 获取不到最新的服务实例信息,而后导致无奈疾速发现新的服务和已下线的服务。

理解了服务端的实现后,想要解决这个问题就变得很简略了,咱们能够缩短只读缓存的更新工夫 (eureka.server.response-cache-update-interval-ms)让服务发现变得更加及时,或者间接将只读缓 存敞开 (eureka.server.use-read-only-response-cache=false),多级缓存也导致 Client 层面(数据一致性) 很单薄。

客户端缓存

客户端缓存次要分为两块内容,一块是 Eureka Client 缓存,一块是 Ribbon 缓存。

Eureka Client 缓存,EurekaClient 负责跟 EurekaServer 进行交互,在 EurekaClient 中的 com.netflix.discovery.DiscoveryClient.initScheduledTasks() 办法中,初始化了一个 CacheRefreshThread 定时工作专⻔用来拉取 Eureka Server 的实例信息到本地。所以咱们须要缩短这个定时拉取服务信息的工夫距离(此值在客户端配置 eureka.client.registryFetchIntervalSeconds) 来疾速发现新的服务

Ribbon 缓存,Ribbon 会从 EurekaClient 中获取服务信息,ServerListUpdater 是 Ribbon 中负责服务实例 更新的组件,默认的实现是 PollingServerListUpdater,通过线程定时去更新实例信息。定时刷新的时 间距离默认是 30 秒,当服务进行或者上线后,这边最快也须要 30 秒能力将实例信息更新成最新的。咱们能够将这个工夫调短一点,比方 3 秒。

刷新距离的参数是通过 getRefreshIntervalMs 办法来获取的,办法中的逻辑也是从 Ribbon 的配置中进行取值的。所以咱们须要缩短这个更新距离(此值在客户端配置 ribbon.ServerListRefreshInterval)来疾速的更新 Ribbon 缓存实例列表

将这些服务端缓存和客户端缓存的工夫全副缩短后,跟默认的配置工夫相比,快了很多。咱们通过调整 参数的形式来尽量放慢服务发现的速度,然而还是不能齐全解决报错的问题,间隔时间设置为 3 秒,也还是会有距离。所以咱们个别都会开启重试性能,当路由的服务呈现问题时,能够重试到另一个服务来 保障这次申请的胜利。

服务端缓存局部源码如下:

/**
 *The class that is responsible for caching registry information that will be
 *queried by the clients.
 */
public class ResponseCacheImpl implements ResponseCache {
    // 一级缓存
      private final ConcurrentMap<Key, Value> readOnlyCacheMap = new ConcurrentHashMap<Key, Value>();
    // 二级缓存(Guava 实现)private final LoadingCache<Key, Value> readWriteCacheMap;
    // 数据存储层
    private final AbstractInstanceRegistry registry;
}

客户端缓存更新局部源码如下:

Eureka Client 缓存刷新局部源码

    //Eureka Client 缓存刷新局部源码
    /**
     * Initializes all scheduled tasks.
     * 在实例化 com.netflix.discovery.DiscoveryClient 被调用
     */
    private void initScheduledTasks() {if (clientConfig.shouldFetchRegistry()) {
            // registry cache refresh timer
            int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
            int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "cacheRefresh",
                            scheduler,
                            cacheRefreshExecutor,
                                  // 从哪个 Eureka 客户端拉取实例列表的间隔时间
                                          // 通过 eureka.client.registryFetchIntervalSeconds 可配置
                            registryFetchIntervalSeconds,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                                  // 执行刷新的 Runable 定时工作
                            new CacheRefreshThread()),
                          // 从哪个 Eureka 客户端拉取实例列表的间隔时间
                          // 通过 eureka.client.registryFetchIntervalSeconds 可配置
                    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
            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");
        }
    }    

Ribbon 缓存刷新局部源码

   //PollingServerListUpdater,Ribbon 缓存刷新局部源码
   @Override
    public synchronized void start(final UpdateAction updateAction) {if (isActive.compareAndSet(false, true)) {final Runnable wrapperRunnable = new Runnable() {
                @Override
                public void run() {if (!isActive.get()) {if (scheduledFuture != null) {scheduledFuture.cancel(true);
                        }
                        return;
                    }
                    try {updateAction.doUpdate();
                        lastUpdated = System.currentTimeMillis();} catch (Exception e) {logger.warn("Failed one update cycle", e);
                    }
                }
            };

            scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
                    wrapperRunnable,
                    initialDelayMs,
                    refreshIntervalMs,// 默认 30 秒,通过 ribbon.ServerListRefreshInterval 来配置更小的值来疾速更新 Ribbon 实例缓存
                    TimeUnit.MILLISECONDS
            );
        } else {logger.info("Already active, no-op");
        }
    }

服务下线

  1. 当服务失常敞开操作时,会发送服务下线的 REST 申请给 EurekaServer。
  2. 服务中心承受到申请后,将该服务置为下线状态

生效剔除

Eureka Server 中会有定时工作去检测生效的服务,将服务实例信息从注册表中移除,也能够将这个生效检测的工夫缩短,这样服务下线后就可能及时从注册表中革除。

  1. Eureka Server 会定时 (距离值是 eureka.server.eviction-interval-timer-in-ms,默认 60s) 进行查看,
    如果发现实例在在肯定工夫
    (此值由客户端设置的 eureka.instance.lease-expiration-duration-in-seconds 定义,默认值为 90s) 内没有收到心跳,
    则会登记此实例。

Eureka 客户端

服务提供者(也是 Eureka 客户端)

  • 服务提供者 (也是 Eureka 客户端) 要向 EurekaServer 注册服务,并实现服务续约等工作
  • 服务注册详解(服务提供者)

    1. 当咱们导入了 eureka-client 依赖坐标,配置 Eureka 服务注册核心地址
    2. 服务在启动时会向注册核心发动注册申请,携带服务元数据信息
    3. Eureka 注册核心会把服务的信息保留在 Map 中。
  • 服务续约详解(服务提供者)

    1. 服务每隔 30 秒会向注册核心续约 (心跳) 一次 (也称为报活),如果没有续约,租约在 90 秒后到期,而后服务会被生效。
      每隔 30 秒的续约操作咱们称之为心跳检测往往不须要咱们调整这两个配置
# 向 Eureka 服务中心集群注册服务 
eureka:
  #租约期限以及续约期限的配置
  instance:
    #租约到期,服务生效工夫,默认值 90 秒,服务超过 90 秒没有产生心跳,EurekaServer 会将服务从列表移除
    #这里批改为 6 秒
    lease-expiration-duration-in-seconds: 6
    #租约续约间隔时间,默认 30 秒,这里批改为 3 秒钟
    lease-renewal-interval-in-seconds: 3

服务消费者(也是 Eureka 客户端)

  • 服务消费者每隔 30 秒服务会从注册核心中拉取一份服务列表,这个工夫能够通过配置批改。往往不须要咱们调整

    1. 服务消费者启动时,从 EurekaServer 服务列表获取只读备份,缓存到本地;
    2. 默认每隔 30 秒,会从新获取并更新数据;
    3. 每隔 30 秒的工夫能够通过配置 eureka.client.registry-fetch-interval-seconds 批改,如下。
# 向 Eureka 服务中心集群注册服务 
eureka:
    client:
      # EurekaClient 每隔多久从 EurekaServer 拉取一次服务列表,默认 30 秒,这里批改为 2 秒钟从注册核心拉取一次
        registry-fetch-interval-seconds: 2

客户端缓存见 Eureka 服务端详情章节

至此您应该明确 Eureka 的服务发现机制了吧

最初再说说 Eureka 自我爱护机制

服务提供者 —> 注册核心

  • 定期的续约 (服务提供者和注册核心通信),如果服务提供者和注册核心之间的网络有点问题,
    不代表 服务提供者不可用,不代表服务消费者无法访问服务提供者,所以有自我爱护的机制
  • 如果在 15 分钟内超过 85% 的客户端节点都没有失常的心跳,那么 Eureka 就认为客户端与注册核心呈现了网络故障,
    Eureka Server 主动进入自我爱护机制。
  • 为什么会有自我爱护机制?

    • 默认状况下,如果 Eureka Server 在肯定工夫内 (默认 90 秒) 没有接管到某个微服务实例的心跳,Eureka Server 将会移除该实例。
      然而当网络分区故障产生时,微服务与 Eureka Server 之间无奈失常通信,而微服务自身是失常运行的,此时不应该移除这个微服务,
      所以引入了自我爱护机制。

服务中心⻚面会显示如下提示信息

当处于自我保护模式时

  1. 不会剔除任何服务实例(可能是服务提供者和 EurekaServer 之间网络问题),保障了大多数服务依 然可用
  2. Eureka Server 依然可能承受新服务的注册和查问申请,然而不会被同步到其它节点上,保障以后节点仍然可用,
    当网络稳固时,以后 Eureka Server 新的注册信息会被同步到其它节点中。
  3. 在 Eureka Server 工程中通过 eureka.server.enable-self-preservation 配置可用关停自我爱护,默认值是关上
eureka:
  server:
    enable-self-preservation: false # 敞开自我保护模式(缺省为关上)

如果有大批量的集群且存在网络分区,强烈建议开启自我爱护机制

正文完
 0