Eureka原理解析

一、Eureka简介

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

Eureka原理解析_第1张图片

 

大概意思是: Eureka 2.0 的开源工作已经停止,依赖于开源库里面的 Eureka 2.x 分支构建的项目或者相关代码,风险自负!

      1、Eureka组件

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

  Eureka Server

   Eureka Server提供服务注册服务,各个节点启动后,会在Eureka Server中进行注册,这样Eureka Server中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。
   注册中心服务端主要对外提供了三个功能:

          1、服务注册: 服务提供者启动时,会通过 Eureka Client 向 Eureka Server 注册信息,Eureka Server 会存储该服务的信息,Eureka Server 内部有二层缓存机制来维护整个注册表Eureka Server 端服务注册接口逻辑图如下:


Eureka原理解析_第2张图片

 

      2、提供注册表:服务消费者在调用服务时,如果 Eureka Client 没有缓存注册表的话,会从 Eureka Server 获取最新的注册表

      3、同步状态:Eureka Client 通过注册、心跳机制和 Eureka Server 同步当前客户端的状态

 Eureka Client

 Eureka Client是一个java客户端,用于简化与Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳,默认周期为30秒,如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka         Server将会从服务注册表中把这个服务节点移除(默认90秒),Eureka Client 会拉取、更新和缓存 Eureka Server 中的信息。因此当所有的 Eureka Server 节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者,但是当服务有更改的时候会出现信息不一致。

    Register: 服务注册
    服务的提供者,将自身注册到注册中心,服务提供者也是一个 Eureka Client。当 Eureka Client 向 Eureka Server 注册时,它提供自身的元数据,比如 IP 地址、端口,运行状况指示符 URL,主页等。

    Renew: 服务续约
    Eureka Client服务注册后, 会每隔 30 秒发送一次心跳来续约。 通过续约来告知 Eureka Server 该 Eureka Client 运行正常,没有出现问题。 默认情况下,如果 Eureka Server 在 180秒内没有收到 Eureka Client 的续约,Server 端会将实例从其注册表中删除,此时间可配置,一般情况不建议更 改(实现逻辑是将服务信息的注册信中的过期时间设置为当前时间加上90秒)。
服务续约的两个重要属性 服务续约任务的调用间隔时间,默认为30秒,服务失效的时间,默认为90秒(此处服务剔除Eureka Server端存在一个Bug,实际是180s才会剔除)
eureka.instance.lease-renewal-interval-in-seconds=30 eureka.instance.lease-expiration-duration-in-seconds=90

    Eviction 服务剔除
   Eureka Server启动时会创建一个定时任务,每隔60s执行一次,把当前注册表中超时(90s)没有续约的Eureka Client服务剔除。Eureka官网介绍的超时时间默认值是90s 没有续约就会剔除,但是实际是超过180s才会剔除。
原因:
1. 在服务续约的时候将服务的过期时间修改为当前时间加90s,
2. 后台线程检测线程(60s检测一次)判断注册信息是否需要过期剔除的时候,又将过期时间增加了 90s 然后和当前时间校对,相当于一共延后了180s。
但是这个bug对实际影响较小,鉴于Eureka已被大量在线上应用,考虑到影响现有服务,这个bug不再修复。

    Cancel: 服务下线
Eureka Client 在程序关闭时向 Eureka Server 发送取消请求。 发送请求后,该客户端实例信息将从 Eureka Server 的实例注册表中删除。该下线请求不会自动完成,它需要调用以下内容:
DiscoveryManager.getInstance().shutdownComponent();

    GetRegisty: 获取注册列表信息
Eureka Client 初始化的时候回初始化一个 拉取Eureka Server的 TimedSupervisorTask 任务,通过finally 中任务自己调用自己,重复执行完成 相当于 每隔30s 拉取Eureka Server注册实例信息。TimedSupervisorTask 固定周期性任务,一但超时就会将下一个任务的执行时间间隔增大一倍,直到超过最大任务执行间隔的限制,一旦任务不在超时,就会将任务的执行间隔恢复为默认值。且任务的间隔修改为CAS操作。

public class TimedSupervisorTask extends TimerTask {

    private static final Logger logger = LoggerFactory.getLogger(TimedSupervisorTask.class);

    private final Counter timeoutCounter;

    private final Counter rejectedCounter;

    private final Counter throwableCounter;

    private final LongGauge threadPoolLevelGauge;

    private final ScheduledExecutorService scheduler;

    private final ThreadPoolExecutor executor;

    private final long timeoutMillis;

    private final Runnable task;

    private final AtomicLong delay;

    private final long maxDelay;

    public TimedSupervisorTask(String name, ScheduledExecutorService scheduler, ThreadPoolExecutor executor,

                               int timeout, TimeUnit timeUnit, int expBackOffBound, Runnable task) {

        this.scheduler = scheduler;

        this.executor = executor;

        this.timeoutMillis = timeUnit.toMillis(timeout);

        this.task = task;

        this.delay = new AtomicLong(timeoutMillis);

        this.maxDelay = timeoutMillis * expBackOffBound;

        // Initialize the counters and register.

        timeoutCounter = Monitors.newCounter("timeouts");

        rejectedCounter = Monitors.newCounter("rejectedExecutions");

        throwableCounter = Monitors.newCounter("throwables");

        threadPoolLevelGauge = new LongGauge(MonitorConfig.builder("threadPoolUsed").build());

        Monitors.registerObject(name, this);

    }

    @Override

    public void run() {

        Future future = null;

        try {

            future = executor.submit(task);

            threadPoolLevelGauge.set((long) executor.getActiveCount());

            future.get(timeoutMillis, TimeUnit.MILLISECONDS);  // block until done or timeout

            delay.set(timeoutMillis);

            threadPoolLevelGauge.set((long) executor.getActiveCount());

        catch (TimeoutException e) {

            logger.warn("task supervisor timed out", e);

            timeoutCounter.increment();

            long currentDelay = delay.get();

            long newDelay = Math.min(maxDelay, currentDelay * 2);

            delay.compareAndSet(currentDelay, newDelay);

        catch (RejectedExecutionException e) {

            if (executor.isShutdown() || scheduler.isShutdown()) {

                logger.warn("task supervisor shutting down, reject the task", e);

            else {

                logger.warn("task supervisor rejected the task", e);

            }

            rejectedCounter.increment();

        catch (Throwable e) {

            if (executor.isShutdown() || scheduler.isShutdown()) {

                logger.warn("task supervisor shutting down, can't accept the task");

            else {

                logger.warn("task supervisor threw an exception", e);

            }

            throwableCounter.increment();

        finally {

            if (future != null) {

                future.cancel(true);

            }

            if (!scheduler.isShutdown()) {

                scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS);

            }

        }

    }

}

Eureka Client 从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。Eureka Client 初始化时,首次拉取注册表信息时会全量拉取,如果没有关闭增量拉取,Eureka Client 定期(每30秒钟)向Eureka Server端拉取服务的注册列表增量缓存信息,然后和Client端本地的服务注册缓存信息合并,然后根据服务注册缓存信息计算出对应的哈希值和Eureka Server服务端返回的hashCode校对,如果校对失败,那么Eureka Client 则会重新向Eureka Server端全量拉取服务注册表信息。 Eureka Server 缓存注册列表信息,整个注册表以及每个应用程序的信息进行了压缩,压缩内容和没有压缩的内容完全相同。Eureka Client 和 Eureka Server 可以使用 JSON/XML 格式进行通讯。在默认情况下 Eureka Client 使用压缩 JSON 格式来获取注册列表的信息。

获取服务是服务消费者的基础,所以必有两个重要参数需要注意:

# 启用服务消费者从注册中心拉取服务列表的功能

eureka.client.fetch-registry=true

# 设置服务消费者从注册中心拉取服务列表的间隔

eureka.client.registry-fetch-interval-seconds=30

Remote Call: 远程调用
当 Eureka Client 从注册中心获取到服务提供者信息后,就可以通过 Http 请求调用对应的服务;服务提供者有多个时,Eureka Client 客户端会通过 Ribbon 自动进行负载均衡。


二、Eureka 集群原理

Eureka原理解析_第3张图片

 

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

  

从图中可以看出 Eureka Server 集群相互之间通过 Replicate 来同步数据,相互之间不区分主节点和从节点,所有的节点都是平等的。在这种架构中,节点通过彼此互相注册来提高可用性,每个节点需要添加一个或多个有效的 serviceUrl 指向其他节点。

如果某台 Eureka Server 宕机,Eureka Client 的请求会自动切换到新的 Eureka Server 节点。当宕机的服务器重新恢复后,Eureka 会再次将其纳入到服务器集群管理之中。当节点开始接受客户端请求时,所有的操作都会进行节点间复制,将请求复制到其它 Eureka Server 当前所知的所有节点中。

另外 Eureka Server 的同步遵循着一个非常简单的原则:只要有一条边将节点连接,就可以进行信息传播与同步。所以,如果存在多个节点,只需要将节点之间两两连接起来形成通路,那么其它注册中心都可以共享信息。每个 Eureka Server 同时也是 Eureka Client,多个 Eureka Server 之间通过 P2P 的方式完成服务注册表的同步。

Eureka Server 集群之间的状态是采用异步方式同步的,所以不保证节点间的状态一定是一致的,不过基本能保证最终状态是一致的。

Eureka 分区
Eureka 提供了 Region 和 Zone 两个概念来进行分区,这两个概念均来自于亚马逊的 AWS:
region:可以理解为地理上的不同区域,比如亚洲地区,中国区或者深圳等等。没有具体大小的限制。根据项目具体的情况,可以自行合理划分 region。
zone:可以简单理解为 region 内的具体机房,比如说 region 划分为深圳,然后深圳有两个机房,就可以在此 region 之下划分出 zone1、zone2 两个 zone。

上图中的 us-east-1c、us-east-1d、us-east-1e 就代表了不同的 Zone。Zone 内的 Eureka Client 优先和 Zone 内的 Eureka Server 进行心跳同步,同样调用端优先在 Zone 内的 Eureka Server 获取服务列表,当 Zone 内的 Eureka Server 挂掉之后,才会从别的 Zone 中获取信息。

Eureka Server端优秀的多级缓存机制

假设Eureka Server部署在4核8G的普通机器上,那么基于内存来承载各个服务的请求,每秒钟最多可以处理多少请求呢?

  • 根据之前的测试,单台4核8G的机器,处理纯内存操作,哪怕加上一些网络的开销,每秒处理几百请求也是轻松加愉快的。
  • 而且Eureka Server为了避免同时读写内存数据结构造成的并发冲突问题,还采用了多级缓存机制来进一步提升服务请求的响应速度。
  • 在拉取注册表的时候:
    • 首先从ReadOnlyCacheMap(默认30秒自动过期)里查缓存的注册表。
    • 若没有,就找ReadWriteCacheMap(默认180秒自动过期)里缓存的注册表。
    • 如果还没有,就从内存中获取实际的注册表数据。
  • 在注册表发生变更的时候:
    • 会在内存中更新变更的注册表数据,同时过期掉ReadWriteCacheMap。
    • 此过程不会影响ReadOnlyCacheMap提供人家查询注册表,这个时候就造成了和真实服务注册表信息不一致的情况。
    • 一段时间内(最多30秒),各服务拉取注册表会直接读ReadOnlyCacheMap
    • 30秒过后,Eureka Server的后台线程发现ReadWriteCacheMap已经清空了,也会清空ReadOnlyCacheMap中的缓存
    • 下次有服务拉取注册表,又会从内存中获取最新的数据了,同时填充各个缓存。

多级缓存机制的优点是什么?

  • 尽可能保证了内存注册表数据不会出现频繁的读写冲突问题。
  • 并且进一步保证对Eureka Server的大量请求,都是快速从纯内存走,性能极高。

多级缓存机制的缺点是什么?

  • 造成短暂的数据不一致情况(注册表发生变更时清空ReadWriteCacheMap,其他服务读取ReadOnlyCacheMap中过期的服务注册表信息)

为方便大家更好的理解,同样来一张图,大家跟着图再来回顾一下这整个过程:

Eureka原理解析_第4张图片

 

Eurka 保证 AP

Eureka Server 各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而 Eureka Client 在向某个 Eureka 注册时,如果发现连接失败,则会自动切换至其它节点。只要有一台Eureka Server 还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。
AP体现在:
1. Eureka Client 本地缓存有服务注册的全量信息,即使与Eureka Server网络中断也可以根据本地缓存服务注册信息发起请求
2. Eureka Client 在向某个 Eureka Server 注册时,如果发现连接失败,则会自动切换至其它Eureka Server节点
3. Eureka Server 各个节点都是平等的,相互之间通过异步 Replicate 来同步数据,达到最终一致性。
4. Eureka Server 的两级缓存

Eurka 工作流程

了解完 Eureka 核心概念,自我保护机制,以及集群内的工作原理后,我们来整体梳理一下 Eureka 的工作流程:
1、Eureka Server 启动成功,等待服务端注册。在启动过程中如果配置了集群,集群之间定时通过 Replicate 同步注册表,每个 Eureka Server 都存在独立完整的服务注册表信息
2、Eureka Client 启动时根据配置的 Eureka Server 地址去注册中心注册服务
3、Eureka Client 会每 30s 向 Eureka Server 发送一次心跳请求,证明客户端服务正常
4、当 Eureka Server 90s 内没有收到 Eureka Client 的心跳,注册中心则认为该节点失效,会注销该实例
5、单位时间内 Eureka Server 统计到有大量的 Eureka Client 没有上送心跳,则认为可能为网络异常,进入自我保护机制,不再剔除没有上送心跳的客户端
6、当 Eureka Client 心跳请求恢复正常之后,Eureka Server 自动退出自我保护模式
7、Eureka Client 定时全量或者增量从注册中心获取服务注册表,并且将获取到的信息缓存到本地
8、服务调用时,Eureka Client 会先从本地缓存找寻调取的服务。如果获取不到,先从注册中心刷新注册表,再同步到本地缓存
9、Eureka Client 获取到目标服务器信息,发起服务调用
10、Eureka Client 程序关闭时向 Eureka Server 发送取消请求,Eureka Server 将实例从注册表中删除

这就是Eurka基本工作流程

Eureka Server设计精妙的注册表存储结构
Eureka原理解析_第5张图片
Eureka原理解析_第6张图片

 

 

  • 如上图所示,图中的这个名字叫做registry的CocurrentHashMap,就是注册表的核心结构。看完之后忍不住先赞叹一下,精妙的设计!
  • 从代码中可以看到,Eureka Server的注册表直接基于纯内存,即在内存里维护了一个数据结构。
  • 各个服务的注册、服务下线、服务故障,全部会在内存里维护和更新这个注册表。
  • 各个服务每隔30秒拉取注册表的时候,Eureka Server就是直接提供内存里存储的有变化的注册表数据给他们就可以了。
  • 同样,每隔30秒发起心跳时,也是在这个纯内存的Map数据结构里更新心跳时间。

一句话概括:维护注册表、拉取注册表、更新心跳时间,全部发生在内存里!这是Eureka Server非常核心的一个点。

搞清楚了这个,咱们再来分析一下registry这个东西的数据结构,大家千万别被它复杂的外表唬住了,沉下心来,一层层的分析!

  • 首先,这个ConcurrentHashMap的key就是服务名称,比如“inventory-service”,就是一个服务名称。
  • value则代表了一个服务的多个服务实例。
  • 举例:比如“inventory-service”是可以有3个服务实例的,每个服务实例部署在一台机器上。

再来看看作为value的这个Map:
Map>>

  • 这个Map的key就是服务实例的id
  • value是一个叫做Lease的类,它的泛型是一个叫做InstanceInfo的东东,你可能会问,这俩又是什么鬼?
  • 首先说下InstanceInfo,其实啊,我们见名知义,这个InstanceInfo就代表了服务实例的具体信息,比如机器的ip地址、hostname以及端口号。
  • 而这个Lease,里面则会维护每个服务最近一次发送心跳的时间

你可能感兴趣的:(eureka,spring,cloud,java)