Eureka之注册表缓存机制

附:SpringCloud之系列汇总跳转地址

一、问题提出

现在咱们假设手头有一套大型的分布式系统,一共100个服务,每个服务部署在20台机器上,机器是4核8G的标准配置。

也就是说,相当于你一共部署了100 * 20 = 2000个服务实例,有2000台机器。

每台机器上的服务实例内部都有一个Eureka Client组件,它会每隔30秒请求一次Eureka Server,拉取变化的注册表。

此外,每个服务实例上的Eureka Client都会每隔30秒发送一次心跳请求给Eureka Server。

那么大家算算,Eureka Server作为一个微服务注册中心,每秒钟要被请求多少次?一天要被请求多少次?

  • 按标准的算法,每个服务实例每分钟请求2次拉取注册表,每分钟请求2次发送心跳

  • 这样一个服务实例每分钟会请求4次,2000个服务实例每分钟请求8000次

  • 换算到每秒,则是8000 / 60 = 133次左右,我们就大概估算为Eureka Server每秒会被请求150次

  • 那一天的话,就是8000 * 60 * 24 = 1152万,也就是每天千万级访问量

好!经过这么一个测算,大家是否发现这里的奥秘了?

  • 首先,对于微服务注册中心这种组件,在一开始设计它的拉取频率以及心跳发送频率时,就已经考虑到了一个大型系统的各个服务请求时的压力,每秒会承载多大的请求量。

  • 所以各服务实例每隔30秒发起请求拉取变化的注册表,以及每隔30秒发送心跳给Eureka Server,其实这个时间安排是有其用意的。

按照我们的测算,一个上百个服务,几千台机器的系统,按照这样的频率请求Eureka Server,日请求量在千万级,每秒的访问量在150次左右。

即使算上其他一些额外操作,我们姑且就算每秒钟请求Eureka Server在200次~300次吧。

所以通过设置一个适当的拉取注册表以及发送心跳的频率,可以保证大规模系统里对Eureka Server的请求压力不会太大。

关键问题来了,Eureka Server是如何保证轻松抗住这每秒数百次请求,每天千万级请求的呢?

要搞清楚这个,首先得清楚Eureka Server到底是用什么来存储注册表的?三个字,看源码

接下来咱们就一起进入Eureka源码里一探究竟:

public abstract class AbstractInstanceRegistry implements InstanceRegistry {
    private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
            = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
    ......
}
  • 如上图所示,图中的这个名字叫做registryCocurrentHashMap,就是注册表的核心结构。
  • 从代码中可以看到,Eureka Server的注册表直接基于纯内存,即在内存里维护了一个数据结构。

  • 各个服务的注册、服务下线、服务故障,全部会在内存里维护和更新这个注册表。

  • 各个服务每隔30秒拉取注册表的时候,Eureka Server就是直接提供内存里存储的有变化的注册表数据给他们就可以了。

  • 同样,每隔30秒发起心跳时,也是在这个纯内存的Map数据结构里更新心跳时间。

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

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

  • 首先,这个ConcurrentHashMap的key就是服务名称,比如“inventory-service”,就是一个服务名称。

  • value则代表了一个服务的多个服务实例。

  • 举例:比如“inventory-service”是可以有3个服务实例的,每个服务实例部署在一台机器上。

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

    Map<String, Lease<InstanceInfo>>

  • 这个Map的key就是服务实例的id

  • value是一个叫做Lease的类,它的泛型是一个叫做InstanceInfo的东东,你可能会问,这俩又是什么鬼?

  • 首先说下InstanceInfo,其实啊,我们见名知义,这个InstanceInfo就代表了服务实例的具体信息,比如机器的ip地址、hostname以及端口号。

  • 而这个Lease,里面则会维护每个服务最近一次发送心跳的时间

二、AP特性

从CAP理论看,Eureka是一个AP系统,优先保证可用性(A)和分区容错性(P),不保证强一致性(C),只保证最终一致性,因此在架构中设计了较多缓存。

三、服务状态

Eureka服务状态enum类:com.netflix.appinfo.InstanceInfo.InstanceStatus

状态说明状态说明
UP在线OUT_OF_SERVICE失效
DOWN下线UNKNOWN未知
STARTING正在启动

四、Eureka Server

在Eureka高可用架构中,Eureka Server也可以作为Client向其他server注册,多节点相互注册组成Eureka集群,集群间相互视为peer。Eureka Client向Server注册、续约、更新状态时,接受节点更新自己的服务注册信息后,逐个同步至其他peer节点。

【注意】如果server-A向server-B节点单向注册,则server-A视server-B为peer节点,server-A接受的数据会同步给server-B,但server-B接受的数据不会同步给server-A。

4.1 缓存机制

Eureka Server存在三个变量:(registry、readWriteCacheMap、readOnlyCacheMap)保存服务注册信息,默认情况下定时任务每30s将readWriteCacheMap同步至readOnlyCacheMap,每60s清理超过90s未续约的节点,Eureka Client每30s从readOnlyCacheMap更新服务注册信息,而UI则从registry更新服务注册信息。

三级缓存

缓存类型说明
registryConcurrentHashMap实时更新,类AbstractInstanceRegistry成员变量,UI端请求的是这里的服务注册信息
readWriteCacheMapGuava Cache/LoadingCache实时更新,类ResponseCacheImpl成员变量,缓存时间180秒
readOnlyCacheMapConcurrentHashMap周期更新,类ResponseCacheImpl成员变量,默认每30s从readWriteCacheMap更新,Eureka client默认从这里更新服务注册信息,可配置直接从readWriteCacheMap更新

缓存相关配置

配置默认说明
eureka.server.useReadOnlyResponseCachetrueClient从readOnlyCacheMap更新数据,false则跳过readOnlyCacheMap直接从readWriteCacheMap更新
eureka.server.responsecCacheUpdateIntervalMs30000readWriteCacheMap更新至readOnlyCacheMap周期,默认30s
eureka.server.evictionIntervalTimerInMs60000清理未续约节点(evict)周期,默认60s
eureka.instance.leaseExpirationDurationInSeconds90清理未续约节点超时时间,默认90s

关键类

类名说明
com.netflix.eureka.registry.AbstractInstanceRegistry保存服务注册信息,持有registry和responseCache成员变量
com.netflix.eureka.registry.ResponseCacheImpl持有readWriteCacheMap和readOnlyCacheMap成员变量

五、Eureka Client

Eureka Client存在两种角色:服务提供者服务消费者,作为服务消费者一般配合Ribbon或Feign(Feign内部使用Ribbon)使用。Eureka Client启动后,作为服务提供者立即向Server注册,默认情况下每30s续约(renew);作为服务消费者立即向Server全量更新服务注册信息,默认情况下每30s增量更新服务注册信息;Ribbon延时1s向Client获取使用的服务注册信息,默认每30s更新使用的服务注册信息,只保存状态为UP的服务。

二级缓存

缓存类型说明
localRegionAppsAtomicReference周期更新,类DiscoveryClient成员变量,Eureka Client保存服务注册信息,启动后立即向Server全量更新,默认每30s增量更新
upServerListZoneMapConcurrentHashMap周期更新,类LoadBalancerStats成员变量,Ribbon保存使用且状态为UP的服务注册信息,启动后延时1s向Client更新,默认每30s更新

缓存相关配置

配置默认说明
eureka.instance.leaseRenewalIntervalInSeconds30Eureka Client 续约周期,默认30s
eureka.client.registryFetchIntervalSeconds30Eureka Client 增量更新周期,默认30s(正常情况下增量更新,超时或与Server端不一致等情况则全量更新)
ribbon.ServerListRefreshInterval30000Ribbon 更新周期,默认30s

关键类

类名说明
com.netflix.discovery.DiscoveryClientEureka Client 负责注册、续约和更新,方法initScheduledTasks()分别初始化续约和更新定时任务
com.netflix.loadbalancer.PollingServerListUpdaterRibbon 更新使用的服务注册信息,start初始化更新定时任务
com.netflix.loadbalancer.LoadBalancerStatsRibbon,保存使用且状态为UP的服务注册信息

六、默认配置下服务消费者最长感知时间

Eureka Client时间说明
上线30(readOnly)+30(Client)+30(Ribbon)=90sreadWrite -> readOnly -> Client -> Ribbon 各30s
正常下线30(readonly)+30(Client)+30(Ribbon)=90s服务正常下线(kill或kill -15杀死进程)会给进程善后机会,DiscoveryClient.shutdown()将向Server更新自身状态为DOWN,然后发送DELETE请求注销自己,registry和readWriteCacheMap实时更新,故UI将不再显示该服务实例
非正常下线30+60(evict)*2+30+30+30= 240s服务非正常下线(kill -9杀死进程或进程崩溃)不会触发DiscoveryClient.shutdown()方法,Eureka Server将依赖每60s清理超过90s未续约服务从registry和readWriteCacheMap中删除该服务实例

考虑如下情况

  • 0s时服务未通知Eureka Client直接下线;
  • 29s时第一次过期检查evict未超过90s;
  • 89s时第二次过期检查evict未超过90s;
  • 149s时第三次过期检查evict未续约时间超过了90s,故将该服务实例从registry和readWriteCacheMap中删除;
  • 179s时定时任务从readWriteCacheMap更新至readOnlyCacheMap;
  • 209s时Eureka Client从Eureka Server的readOnlyCacheMap更新;
  • 239s时Ribbon从Eureka Client更新。

因此,极限情况下服务消费者最长感知时间将无限趋近240s。

七、应对措施

服务注册中心在选择使用Eureka时说明已经接受了其优先保证可用性(A)和分区容错性(P)、不保证强一致性(C)的特点。如果需要优先保证强一致性(C),则应该考虑使用ZooKeeper等CP系统作为服务注册中心。分布式系统中一般配置多节点,单个节点服务上线的状态更新滞后并没有什么影响,这里主要考虑服务下线后状态更新滞后的应对措施。

Eureka Server

  • 1.缩短readOnlyCacheMap更新周期。缩短该定时任务周期可减少滞后时间。

    eureka.server.responsecCacheUpdateIntervalMs: 10000  # Eureka Server readOnlyCacheMap更新周期
  • 2.关闭readOnlyCacheMap。中小型系统可以考虑该方案,Eureka Client直接从readWriteCacheMap更新服务注册信息。

    eureka.server.useReadOnlyResponseCache: false        # 是否使用readOnlyCacheMap

Eureka Client

  • 1.服务消费者使用容错机制。如Spring Cloud Retry和Hystrix,Ribbon、Feign、Zuul都可以配置Retry,服务消费者访问某个已下线节点时一般报ConnectTimeout,这时可以通过Retry机制重试下一个节点。

  • 2.服务消费者缩短更新周期。Eureka Client和Ribbon二级缓存影响状态更新,缩短这两个定时任务周期可减少滞后时间,例如配置:

    eureka.client.registryFetchIntervalSeconds: 5        # Eureka Client更新周期
    ribbon.ServerListRefreshInterval: 2000               # Ribbon更新周期
  • 3.服务提供者保证服务正常下线。服务下线时使用kill或kill -15命令,避免使用kill -9命令,kill或kill -15命令杀死进程时将触发Eureka Client的shutdown()方法,主动删除Server的registry和readWriteCacheMap中的注册信息,不必依赖Server的evict清除。

  • 4.服务提供者延迟下线。服务下线之前先调用接口使Eureka Server中保存的服务状态为DOWN或OUT_OF_SERVICE后再下线,二者时间差根据缓存机制和配置决定,比如默认情况下调用接口后延迟90s再下线服务即可保证服务消费者不会调用已下线服务实例。

七、网关实现服务下线实时感知

在软件工程中,没有一个问题是中间层解决不了的,而网关是服务提供者和服务消费者的中间层。以Spring Cloud Zuul网关为例,网关作为Eureka Client保存了服务注册信息,服务消费者通过网关将请求转发给服务提供者,只需要做到服务提供者下线时通知网关在自己保存的服务列表中使该服务失效。为了保持网关的独立性,可实现一个独立服务接收下线通知并协调网关集群。

附:SpringCloud之系列汇总跳转地址

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值