zoukankan      html  css  js  c++  java
  • Eureka重点原理解析

    前言

        带着问题学习,事半功倍。本文将对如下几个问题进行总结说明:

    1、EurekaServer端服务注册的流程和设计模式

    2、Eureka服务续约的bug

    3、EurekaClient的启动流程

    4、client启动后是往一个server注册还是多个server遍历注册

    5、EurekaServer的三级缓存

    6、一个EurekaClient宕机后,其他EurekaClient最晚多长时间后才会不再往这个宕机的服务发起请求?

        Eureka在Spring Cloud组件全家桶中,处于很核心的位置,从去年格林尼治版的更新说明中就能知道,更新日志截图如下,netflix的其他组件均进入维护状态,不再添加新特性,但Eureka不包括在内。个人观点,一方面Eureka的功能实现相对比较复杂,不好随便改动,再就是位置关键,改动后影响范围广。

    下面进入正文。注:Spring Cloud版本Hoxton SR1,eureka-core 1.9.13

    正文 

    一、EurekaServer端服务注册的流程和设计模式

    服务端的入口类如下所示,不带s的类中是对单个服务/实例的操作,带s的是集合操作。服务注册入口在ApplicationResource中。

     服务注册方法是ApplicationResource#addInstance,可以看到经过一些必要的判断后调用了注册方法,注意因为该请求是从客户端发起的,isReplication为空,所以register方法的第二个参数是false。

    1         registry.register(info, "true".equals(isReplication));
    2         return Response.status(204).build();  // 204 to be backwards compatible
    3     }

    追踪进入PeerAwareInstanceRegistryImpl类的register方法,如下,该方法先调用了父类的注册方法,然后调的往其他服务扩散注册信息的方法replicateToPeers。

    1     @Override
    2     public void register(final InstanceInfo info, final boolean isReplication) {
    3         int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
    4         if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
    5             leaseDuration = info.getLeaseInfo().getDurationInSecs();
    6         }
    7         super.register(info, leaseDuration, isReplication);
    8         replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
    9     }

    继续跟进到父类AbstractInstanceRegistry,在父类的register方法中完成了对真实服务列表ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry的维护 (方法太长就不贴出来了)。

    至此完成了服务注册,共涉及到三个类:ApplicationResource、PeerAwareInstanceRegistryImpl、AbstractInstanceRegistry。前两个类的register方法都是做了一些自己的事情外加调用父类的register,一个典型的责任链模式应用,一个类只负责自己的事情,然后调用上一层的方法,如果需加一个功能,只需要在对应位置加一层继承关系即可,对原有功能无侵入。

    二、Eureka服务续约的bug

    打开Lease租债器类,看到renew方法和isExpired方法:

    1 public void renew() {
    2         lastUpdateTimestamp = System.currentTimeMillis() + duration;
    3 
    4     }
    1 public boolean isExpired(long additionalLeaseMs) {
    2         return (evictionTimestamp > 0 || System.currentTimeMillis() > (lastUpdateTimestamp + duration + additionalLeaseMs));
    3     }

    续约方法每次调用都将最后修改时间变为当前时间+有效期(默认90s),而判断是否失效的方法比较的是当前时间和最后修改时间+有效期,这就导致有效期加了两次,即一个服务过了两倍的有效期时间之后才会被服务端判定为到期。其实这个事情在isExpired方法的注释中可以看到说明:

     1     /**
     2      * Checks if the lease of a given {@link com.netflix.appinfo.InstanceInfo} has expired or not.
     3      *
     4      * Note that due to renew() doing the 'wrong" thing and setting lastUpdateTimestamp to +duration more than
     5      * what it should be, the expiry will actually be 2 * duration. This is a minor bug and should only affect
     6      * instances that ungracefully shutdown. Due to possible wide ranging impact to existing usage, this will
     7      * not be fixed.
     8      *
     9      * @param additionalLeaseMs any additional lease time to add to the lease evaluation in ms.
    10      */

    三、EurekaClient的启动流程

    Eureka客户端只需要引入依赖加上配置,便可以自动实现服务注册。客户端的功能主要包括三部分:启动时的服务注册、服务定时续约、服务列表缓存定时更新。

    客户端启动的逻辑都在DiscoveryClient的构造方法中,com.netflix.discovery.DiscoveryClient#DiscoveryClient,方法代码太长,就不粘贴代码了,只描述下流程:

    初始化两个ThreadPoolExecutor:服务续约和更新缓存;

    从server拉取注册信息com.netflix.discovery.DiscoveryClient#fetchRegistry;

    com.netflix.discovery.DiscoveryClient#register服务注册;

    启动定时器。

    四、client启动后是往一个server注册还是多个server遍历注册

    客户端在执行register方法注册服务时,采用装饰器模式对httpClient进行处理,其中有一个是RetryableEurekaHttpClient,在该类的execute方法中对客户端配置文件中配置的serviceUrl进行了遍历,如果第一个注册请求处理成功了,则不再重试,否则遍历serviceUrl重试。具体可见com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient#execute方法。

    五、EurekaServer的三级缓存

    eureka的服务端为了提高服务列表维护和读取的一致性与可用性,对服务列表的查看设置了三级缓存,入口为com.netflix.eureka.resources.ApplicationsResource#getContainers。在该方法中调用了

    ResponseCacheImpl#getGZIP方法获取缓存,如下:

    1 public byte[] getGZIP(Key key) {
    2         Value payload = getValue(key, shouldUseReadOnlyResponseCache);
    3         if (payload == null) {
    4             return null;
    5         }
    6         return payload.getGzipped();
    7     }

    继续跟进getValue方法:

     1 Value getValue(final Key key, boolean useReadOnlyCache) {
     2         Value payload = null;
     3         try {
     4             if (useReadOnlyCache) {
     5                 final Value currentPayload = readOnlyCacheMap.get(key);
     6                 if (currentPayload != null) {
     7                     payload = currentPayload;
     8                 } else {
     9                     payload = readWriteCacheMap.get(key);
    10                     readOnlyCacheMap.put(key, payload);
    11                 }
    12             } else {
    13                 payload = readWriteCacheMap.get(key);
    14             }
    15         } catch (Throwable t) {
    16             logger.error("Cannot get value for key : {}", key, t);
    17         }
    18         return payload;
    19     }

    可以看到,这里有两个map:readOnlyCacheMap(只读缓存)、readWriteCacheMap(读写缓存),再加上AbstractInstanceRegistry#registry真实数据,总共三级map缓存。

    它们使用的规则如下:只读缓存每隔30s定时从读写缓存中更新最新数据,读写缓存与真实数据是同步的,它的存在是为了减少对真实数据的读取。额外要注意,在eureka server中,读取操作用的写锁,而注册修改下线操作用的读锁。

    通过三级缓存,Eureka在并发吞吐量的基础上做到了最大程度的数据一致性。这种设计思路值得学习。

    六、一个EurekaClient宕机后,其他EurekaClient最晚多长时间后才会不再往这个宕机的服务发起请求?

    先说下上面三级缓存场景可能产生的延迟:如果在服务端的真实服务列表中,一个服务已经被剔除了,此时最多过多长时间其他客户端才能得知到此消息?

    客户端每隔30s去服务端拉取一次缓存 + 服务端只读缓存每30s同步一次读写缓存的数据,即最长需要60s后客户端才能得到最新的服务端列表数据。

    再来看宕机的情况,即如果一个服务宕机,其他服务最多会经过多长时间才不会再往这个服务发送请求?

    先看服务端,因为有上面第二项说的bug存在,默认服务端经过90s*2才会剔除该宕机服务,该剔除的定时器每60s执行一次,再加上上面说的客户端更新缓存的60s延迟,再加上ribbon的60s缓存,所以总计是:

    90*2 + 60 + 60 + 60 = 360s,即最长可能需要6分钟。

    小结

    Eureka的定时器真TM多。。。

  • 相关阅读:
    memset 还可以这样用
    搜索(DFS)
    搜索(BFS)
    最大流之sap算法
    最大流之sap算法
    String VS Cstring(字符串)
    String VS Cstring(字符串)
    Set 与 Multiset
    deque(双端队列)
    枚举 TODO
  • 原文地址:https://www.cnblogs.com/zzq6032010/p/12817560.html
Copyright © 2011-2022 走看看