前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布

Eureka

原创
作者头像
莫闲得慌
修改2021-07-20 14:23:40
6980
修改2021-07-20 14:23:40
举报
文章被收录于专栏:芒格

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一次,和配置文件中的配置值是一致的。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 组件的工作过程
    • 客户端
      • 服务端
        • 三级缓存结构
          • 客户端注册信息同步过程
            • app信息过期最长需要 2*duration 才会被剔除
              • 自我保护机制
              • 使用方式
              相关产品与服务
              微服务引擎 TSE
              微服务引擎(Tencent Cloud Service Engine)提供开箱即用的云上全场景微服务解决方案。支持开源增强的云原生注册配置中心(Zookeeper、Nacos 和 Apollo),北极星网格(腾讯自研并开源的 PolarisMesh)、云原生 API 网关(Kong)以及微服务应用托管的弹性微服务平台。微服务引擎完全兼容开源版本的使用方式,在功能、可用性和可运维性等多个方面进行增强。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档