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

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

我的博客地址,原文出处

先直接给出配置让你尝鲜

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服务端详解

服务端缓存

如图所示

file

服务注册到注册中心后,服务实例信息是存储在注册表中的,也就是内存中。但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之间无法正常通信,而微服务本身是正常运行的,此时不应该移除这个微服务,
      所以引入了自我保护机制。

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

file

当处于自我保护模式时

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345