Eureka

2021-07-20 14:23:40 浏览数 (1)

Eureka 是AP服务, 无 master/slave 之分,每一个 Peer 都是对等的。只要有一台Eureka还在,就能保证注册服务可用, 只不过每个Server的注册表信息可能不一致。为了保障注册中心的高可用性,容忍了数据的非强一致性。在集群环境中如果某台 EurekaServer 宕机,EurekaClient 的请求会自动切换到新的 EurekaServer 节点上,服务提供者有多个时,Eureka Client 客户端会通过 Ribbon 自动进行负载均衡。

代码语言:yaml复制
eureka:
  instance:
    hostname: localhost
    prefer-ip-address: false
    lease-renewal-interval-in-seconds: 10 #每10S给其他服务发次请求,监测心跳
    lease-expiration-duration-in-seconds: 30 #如果其他服务没心跳,30秒后剔除该服务
  client:
    registerWithEureka: true
    fetchRegistry: true
    healthcheck.enabled: true
    serviceUrl:
        defaultZone:  http://localhost:2100/eureka/,http://localhost:2000/eureka/
  1. @EnableEurekaServer : 表明当前服务是用来做注册中心的。 通过application.yml配置“eureka.client.registerWithEureka” 为true(默认) 表示否将自己注册到EurekaServer。
  2. @EnableEurekaClient : 表明当前服务是会注册到EurekaServer上。还有个@EnableDiscoveryClient可以使用其他注册中心
  3. 高可用的实现是: 在配置“eureka.client.service-url.defaultZone” 指定多个eurekaServer的地址互相注册

这里的Server和Client的概念是针对于Eureka注册中心,没有类似dubbo里具体的consumer、provider角色之分。provider提供了哪些接口Api是不体现的,所以需要通过Swagger等外部文档来记录接口方法和参数。

代码语言:javascript复制
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
	<version>2.2.3.RELEASE</version>
</dependency>

组件的工作过程

客户端

  1. Client向Server进行服务注册; DiscoveryClient#register
  2. 每隔30s进行续期renew() , Server服务返回NOT_FOUND会重新注册 ;   DiscoveryClient#HeartbeatThread
  3. 每隔30s拉取Server的注册表 ,分全量和增量, 增量数据应用后计算出的hashCode不匹配Server响应回的hash值,则发起新的全量拉取;DiscoveryClient#CacheRefreshThread

服务端

  1. renew() 续期,app注册信息有效时间向后延长90s 。 AbstractInstanceRegistry#renew
  2. register、cancel、statusUpdate 等会产生注册表信息变动的操作, 使用的是读锁控制。 直接操作注册表底层Map后,将变动的情况放到一个变动队列, 该队列被异步任务30s一次清理3分钟前的数据 。 最后会删除二级缓存中的指定key数据。
  3. 读请求:  使用写锁来控制一致性。 增量请求会将变动队列数据返回,同时还会返回按规则生产一个全量注册表的hashCode. 优先从只读缓存取, 取不到再从loadingCache拿, 最后才是底层注册表 (请求的key是 “查询类型”)
  4. 默认60s一次清除90s内还没有renew()的注册信息(但最长可能要经过2*90s才能剔除该服务)。 删除过程是: 筛选出已经过期的appList,随机删除直到留下的实例数不低于总的85%(min(expired, total - 85% * total) ) , 这个是单次任务执行的逻辑, 如果下次任务又开始了, 没有开启“失效保护”, 又会如此处理。 按javadoc上的说法: (为了补偿GC暂停或本地时间漂移导致不这么做会情况注册表, 要保留一定的基准) AbstractInstanceRegistry#EvictionTask.evict
  5. 自我保护特性: 在每次renew后都会计数(统计一分钟内的 ), 在EvictionTask任务逻辑一开始就来判定: 如果开启了自我保护,则判定: 该续约数小于期望续约数(注册实例数 * (60s /续约间隔30s) * 0.85)就不再删除注册信息。

三级缓存结构

最底层是在org.springframework.cloud.netflix.eureka.server.InstanceRegistry (AbstractInstanceRegistry):

代码语言:txt复制
// 第一层的key是spring.application.name,第二层的key是instanceInfoId,value是服务详情和服务治理相关的属性
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();

其他两层在响应处理类:com.netflix.eureka.registry.ResponseCacheImpl

代码语言:txt复制
// 只读缓存
private final ConcurrentMap<Key, Value> readOnlyCacheMap = new ConcurrentHashMap<Key, Value>(); 
//google的一个缓存框架,key不存在值则调用CacheLoader从InstanceRegistry拿并保存
private final LoadingCache<Key, Value> readWriteCacheMap; 
  1. readWriteCacheMap : 这是一个guava提供的缓存框架。180s过期, key不存在值则执行指定的CacheLoader从InstanceRegistry拿并缓存。
  2. 在ResponseCacheImpl的构造函数里定义了一个Timer,每 30s一次以readOnlyCacheMap为锚定,从readWriteCacheMap匹配相同的key刷新value到readOnlyCacheMap

在 AbstractInstanceRegistry 提供的 注册register、取消Cancel、状态更新statusUpdate 等方法里都是

  1. 先去:直接操作最底层的注册表“registry”。
  2. 然后:将改变的实例信息存放到一个队列中recentlyChangedQueue,这个队列只会存储最近三分钟有变化的节点信息(标记了变化类型 ActionType)。(AbstractInstanceRegistry.getDeltaRetentionTask()会每30s一次执行任务去删除队列里超过3分钟的数据)
  3. 最后:也会 ResponseCache.invalidate 失效 readWriterCacheMap。

这个过程用的是读锁

ResponseCacheImpl.getValue 获取过程:

  1. 优先从readOnlyCacheMap获取指定key的值,
  2. 没有则从readWriteCacheMap获取并写入readOnlyCacheMap;
  3. 还是没有再执行CacheLoader从InstanceRegistry 拿并封装为Value对象(含null和“”的情况)保存。这里会执行方法AbstractInstanceRegistry.getApplicationDeltasFromMultipleRegions,它逻辑里用的是写锁

Q1: 为什么要用三级缓存结构?

因为对于最底层的ConcurrentHashMap使用了读写锁,三级缓存结构也是为了减少并发下的锁竞争,增强查询性能。


Q2: 为什么写时用读锁,读时用写锁

首先,这是一个典型的读多写少的场景。

因为是要从保存最近3分钟的增量队列recentlyChangedQueue里获取增量(变动)Delta数据记录,还要计算全量注册apps的hashCode一起响应回客户端,用读写锁来保证查询的这2步能数据一致性。

其次 ,变化队列实现是ConcurrentLinkedQueue、最底层注册表实现ConcurrentHashMap ,它两是支持并发写的, 那如果更新用的写锁, 没必要。

renew续约操作没有使用锁,那是因为它不会向最近更新队列中添加元素的,不会影响增量更新数据的拉取。

客户端注册信息同步过程

  • 从server端拿服务注册列表时,也就是AbstractInstanceRegistry.getApplicationDeltasFromMultipleRegions方法的执行结果有两部分
    1. recentlyChangedQueue里变化的增量数据;
    2. Applications.getReconcileHashCode()得到一个所有注册实例数据按规则计算的hasCode。
使用写锁,返回的是变化队列里的数据使用写锁,返回的是变化队列里的数据

在最后,根据全量apps的信息TreeMap排序后得到一个hashCode

  • 在eurekaClient端的处理逻辑是在DiscoveryClient.fetchRegistry,分全量拉取getAndStoreFullRegistry() 和增量拉取getAndUpdateDelta(localRegionApps),这里主要看增量部分。

DiscoveryClient.getAndUpdateDelta的逻辑里:

拿回了delta数据后,根据变化的类型来更新本地原有的localRegionApps数据,接着也会按相同规则计算本地全量注册信息allApps的hashCode,拿来和delta返回的hashCode 比对;若不一样,会执行方法reconcileAndLogDifference再次请求eurekaServer端全量拉取来过滤打散并覆盖本地缓存。

app信息过期最长需要 2*duration 才会被剔除

代码语言:yaml复制
# 服务续约任务的调用间隔时间,默认为30秒
eureka.instance.lease-renewal-interval-in-seconds=30  
# 服务续约的时间,默认为90秒。
eureka.instance.lease-expiration-duration-in-seconds=90
 # 清理无效节点的时间间隔(单位毫秒,默认是60*1000)
eureka.server.eviction-interval-timer-in-ms=60
# 关闭自我保护
eureka.server.enable-self-preservation=false
  1. renew 续约自己: DiscoveryClient#HeartbeatThread 执行renew()的间隔时间默认30s 。 EurekaServer处理new时,注册app信息InstanceInfo以当前时间为锚定,向后默认续期90s 。
  2. 更新注册表信息: DiscoveryClient#CacheRefreshThread 默认30s一次 调用方法DiscoveryClient.fetchRegistry从server拉取注册信息(全量 or 增量)。 "client.refresh.interval"
  3. AbstractInstanceRegistry#EvictionTask 默认60s一次删除无效服务。

这里来看个过期问题:

EvictionTask 每隔60s来删除无效服务, 它的判定逻辑是Lease.isExpired

代码语言:java复制
    public boolean isExpired(long additionalLeaseMs) {
       // 当前时间要大于 上次更新时间   90s
        return (evictionTimestamp > 0 || System.currentTimeMillis() > (lastUpdateTimestamp   duration   additionalLeaseMs));
    }

AbstractInstanceRegistry#renew | statusUpdate 时执行Lease.renew():

代码语言:javascript复制
    public void renew() {
        lastUpdateTimestamp = System.currentTimeMillis()   duration; // 更新时间是:以当前时间延长了90s
    }

所以,如果1个服务在续约之后立马宕机了,这个判定过期的时间是 2*duration

自我保护机制

renew 在续约成功后会给一个 “MeasuredRate renewsLastMin” 计数值 1 , 它会统计当前1分钟内的续约成功数。

Renews thresshold > Renews(last min): 当续约阀值大于当前最后一分钟的续约数。此时Eureka将进入自我保护机制。

代码语言:java复制
// AbstractInstanceRegistry
    protected void updateRenewsPerMinThreshold() {
        this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfClientsSendingRenews
                * (60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds())
                * serverConfig.getRenewalPercentThreshold());
    }
expectedNumberOfClientsSendingRenews: 注册的实例数
getExpectedClientRenewalIntervalSeconds:服务端期望的客户端续约间隔,既服务端每分钟期望接收的心跳间隔
getRenewalPercentThreshold(): 阀值系数 默认为0.85

// PeerAwareInstanceRegistryImpl
  public boolean isLeaseExpirationEnabled() {
        if (!isSelfPreservationModeEnabled()) { 
            return true;
        }
        // 开启失效保护后, 最近1分钟的续约值要大于阈值,则不会进入自我保护
        return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
    }

即使有过期,也不是把所有过期的都删除,而是取 过期列表size 和 最大可删除(25%)的最小值, 从过期列表里随机删除。

使用方式

代码语言:txt复制
server.port: 2000
spring.application.name: eureka_server  # 其他服务指定Service-Id是需要用大写, 控制台显示的也是大写 
eureka:
  instance:
    hostname: localhost
    prefer-ip-address: false #Eureka默认使用 hostname 进行服务注册: eg."DESKTOP-G3BHFUS:eureka_server:2000", 以IP地址注册到服务中心,相互注册使用IP地址
    instance-id:  186.198.7.24:2300   #指定向注册中心注册的服务ip地址。 ip地址也可以是"http://域名地址"(http://eureka.com). eureka后台在status处会显示这个,但访问还是取的真实IP
    lease-renewal-interval-in-seconds: 30 #  服务续约任务的调用间隔时间,默认为30秒
    lease-expiration-duration-in-seconds: 90 # renew续约时更新lastUpdateTimestamp = currentTimeMillis   这个duration时间
  client:
    registerWithEureka: true  #将自己注册到EurekaServer
    fetchRegistry: true
    healthcheck.enabled: true
    serviceUrl:
        defaultZone:  http://localhost:2100/eureka/,http://localhost:2000/eureka/ #高可用 互相注册
  server:
    enable-self-preservation: true #自我保护,
    eviction-interval-timer-in-ms: 10000 # 失效实例检测任务调度间隔

#开启@FeignClient的fallback   
feign:
    hystrix:
        enabled: true
#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
  ReadTimeout: 15000
  ConnectTimeout: 15000  
访问http://localhost:2000 访问http://localhost:2000

访问: http://localhost:2000/eureka/apps 这里会有服务者真正的ipAddr (还可以访问:localhost:2000/eureka/status )

代码语言:txt复制
<applications> 
  <versions__delta>1</versions__delta>  
  <apps__hashcode>UP_1_</apps__hashcode>  
  <application> 
    <name>EUREKA_SERVER</name>  
    <instance> 
      <instanceId>186.198.7.24:2300</instanceId>  
      <hostName>localhost</hostName>  
      <app>EUREKA_SERVER</app>  
      <ipAddr>192.168.99.1</ipAddr>  <!--真正的访问ip地址-->
      <status>UP</status>  
      <overriddenstatus>UNKNOWN</overriddenstatus>  
      <port enabled="true">2000</port>  
      <securePort enabled="false">443</securePort>  
      <countryId>1</countryId>  
      <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo"> 
        <name>MyOwn</name> 
      </dataCenterInfo>  
      <leaseInfo> 
        <renewalIntervalInSecs>30</renewalIntervalInSecs>  
        <durationInSecs>90</durationInSecs>  
        <registrationTimestamp>1625580417331</registrationTimestamp>  
        <lastRenewalTimestamp>1625580626801</lastRenewalTimestamp>  
        <evictionTimestamp>0</evictionTimestamp>  
        <serviceUpTimestamp>1625580361035</serviceUpTimestamp> 
      </leaseInfo>  
      <metadata> 
        <management.port>2000</management.port> 
      </metadata>  
      <homePageUrl>http://localhost:2000/</homePageUrl>  
      <statusPageUrl>http://localhost:2000/actuator/info</statusPageUrl>
      <healthCheckUrl>http://localhost:2000/actuator/health</healthCheckUrl>   <!--探活检测url-->
      <vipAddress>eureka_server</vipAddress>  
      <secureVipAddress>eureka_server</secureVipAddress>  
      <isCoordinatingDiscoveryServer>true</isCoordinatingDiscoveryServer>  
      <lastUpdatedTimestamp>1625580417331</lastUpdatedTimestamp>  
      <lastDirtyTimestamp>1625580356703</lastDirtyTimestamp>  
      <actionType>ADDED</actionType> 
    </instance> 
  </application> 
</applications>

这些个访问入口在代码:org.springframework.cloud.netflix.eureka.server.EurekaController

访问: http://localhost:2000/lastn

数据来源自:AbstractInstanceRegistry里的成员变量recentRegisteredQueue、recentCanceledQueue数据来源自:AbstractInstanceRegistry里的成员变量recentRegisteredQueue、recentCanceledQueue

手动删除实例:

代码语言:txt复制
curl -XDELETE http://eurekaserver地址/eureka/apps/要删除的服务名app/要删除的instanceId

postman执行 http://localhost:2000/eureka/apps/EUREKA_SERVER/186.198.7.24:2300 后 :

在 cancel列表出现了操作日志。在 cancel列表出现了操作日志。
代码语言:txt复制
2021-07-06 23:25:09.566 [http-nio-2000-exec-9] INFO  [AbstractInstanceRegistry.java:internalCancel:328] [trace=20109255f7fd4e40,span=20109255f7fd4e40,parent=] c.n.e.r.AbstractInstanceRegistry - Cancelled instance EUREKA_SERVER/186.198.7.24:2300 (replication=false) 
2021-07-06 23:25:18.025 [Eureka-EvictionTimer] INFO  [AbstractInstanceRegistry.java:run:1247] [trace=,span=,parent=] c.n.e.r.AbstractInstanceRegistry - Running the evict task with compensationTime 0ms
2021-07-06 23:25:28.026 [Eureka-EvictionTimer] INFO  [AbstractInstanceRegistry.java:run:1247] [trace=,span=,parent=] c.n.e.r.AbstractInstanceRegistry - Running the evict task with compensationTime 0ms
2021-07-06 23:25:37.413 [http-nio-2000-exec-3] WARN  [AbstractInstanceRegistry.java:renew:360] [trace=95a0152e6a038ce0,span=95a0152e6a038ce0,parent=] c.n.e.r.AbstractInstanceRegistry - DS: Registry: lease doesn't exist, registering resource: EUREKA_SERVER - 186.198.7.24:2300
2021-07-06 23:25:37.413 [http-nio-2000-exec-3] WARN  [InstanceResource.java:renewLease:116] [trace=95a0152e6a038ce0,span=95a0152e6a038ce0,parent=] c.n.e.r.InstanceResource - Not Found (Renew): EUREKA_SERVER - 186.198.7.24:2300
2021-07-06 23:25:37.414 [DiscoveryClient-HeartbeatExecutor-0] INFO  [DiscoveryClient.java:renew:878] [trace=,span=,parent=] c.n.d.DiscoveryClient - DiscoveryClient_EUREKA_SERVER/186.198.7.24:2300 - Re-registering apps/EUREKA_SERVER
2021-07-06 23:25:37.414 [DiscoveryClient-HeartbeatExecutor-0] INFO  [DiscoveryClient.java:register:854] [trace=,span=,parent=] c.n.d.DiscoveryClient - DiscoveryClient_EUREKA_SERVER/186.198.7.24:2300: registering service...
2021-07-06 23:25:37.420 [http-nio-2000-exec-4] INFO  [AbstractInstanceRegistry.java:register:266] [trace=c3080a2e264ec1cd,span=c3080a2e264ec1cd,parent=] c.n.e.r.AbstractInstanceRegistry - Registered instance EUREKA_SERVER/186.198.7.24:2300 with status UP (replication=false)
2021-07-06 23:25:37.422 [DiscoveryClient-HeartbeatExecutor-0] INFO  [DiscoveryClient.java:register:863] [trace=,span=,parent=] c.n.d.DiscoveryClient - DiscoveryClient_EUREKA_SERVER/186.198.7.24:2300 - registration status: 204
2021-07-06 23:25:38.027 [Eureka-EvictionTimer] INFO  [AbstractInstanceRegistry.java:run:1247] [trace=,span=,parent=] c.n.e.r.AbstractInstanceRegistry - Running the evict task with compensationTime 0ms

从执行日志里可以看到:

  1. Cancel完成了, 但该服务又被Renew是注册回来了。
  2. AbstractInstanceRegistry 执行的EvictionTask每隔10s一次,和配置文件中的配置值是一致的。

0 人点赞