您的位置:  首页 > 技术杂谈 > 正文

12 张图 | 硬刚了一波,Eureka 三层缓存架构

2021-12-23 13:00 https://my.oschina.net/u/4499317/blog/5376953 悟空聊架构 次阅读 条评论

大家好,我是悟空。

先说下哈,这篇文章画原理图用了很多时间,求个三连!

Eureka 注册中心系列文章已经写到第七篇了,这里汇总下:

领导让我研究 Eureka 源码 | 启动过程

领导“叕”让我研究 Eureka 源码:注册过程

值得收藏的 Eureka 控制台详解

原来一个 Map 就能搞定注册表了

6 张图 | 剖析客户端首次同步注册表

11 张图 | 讲透原理,最细的增量拉取

> 本文已收录到我的 github: https://github.com/Jackson0714/PassJava-Learning

一、前言

上一讲我们讲到了 Eureka 注册中心的 Server 端有三级缓存来保存注册信息,可以利用缓存的快速读取来提高系统性能。我们再来细看下:

一级缓存:只读缓存 readOnlyCacheMap,数据结构 ConcurrentHashMap。相当于数据库。

二级缓存:读写缓存 readOnlyCacheMap,Guava Cache。相当于 Redis 主从架构中主节点,既可以进行读也可以进行写。

三级缓存:本地注册表 registry,数据结构 ConcurentHashMap。相当于 Redis 主从架构的从节点,只负责读。

看图更清晰,如下图所示:

三种缓存

另外 ConcurrenthashMap 也是一种 map 结构,也就是以键值对的方式进行存储,如下图所示:

Map 结构

本篇悟空哥会带着大家来看下 Eureka 的缓存架构是怎么样,通过学习这篇,我们也可以借鉴 Eureka 的缓存设计思想,将其运用到项目当中。

二、引发的几个思考

我们再来看下 Eureka 源码,其实不难看懂,下面会做解释。

  • 默认会先从只读缓存里面找。
  • 没有的话,再从读写缓存里面找。
  • 找到了的话就更新只读缓存,并返回找到的缓存。
  • 还找不到的话,就从本地缓存 registry 中加载进来。

带来了三个问题:

> (1)三级缓存数据怎么来的? > > (2)缓存数据如何更新的? > > (3)缓存如何过期?

三、本地缓存

我们先来看下本地缓存 registry,它是一种定义为 ConcurrentHashMap 的数据结构,之前也详细讲解过。

当客户端发起注册请求的时候,就会把注册信息放到 registry 中。如下代码所示:

registry.putIfAbsent(app)

putIfAbsent 表示如果存在重复的 key,就不会放入值,如果传入的 key 对应的 value 已经存在,就返回存在的 value,不进行替换。

经过 putIfAbsent 操作就把客户端的注册信息放到 registry 中了。

我们再来看下其中的一种缓存结构:读写缓存。

四、读写缓存

读写缓存,顾名思义,就是既可以进行读,也可以进行写的缓存。读主要是给只读缓存来读取的。写主要是将缓存更新到自己的 Map 中。

下面分别从写缓存的原理、写缓存的源码、过期时机的原理、过期时机的源码几个方面来分别解答。

3.1 写缓存的原理和源码

我开始以为当我们读缓存读不到的时候,就会去数据库查了。找了半天,没找到读数据库的地方。

然后我就用 IDEA 工具查找 readOnlyCacheMap 被使用的地方,终于让我找到了。

读写缓存用的是 Guava Cache工具类,这篇不会深究。简单来说就是当访问读写缓存时,如果这个 key 在缓存中不存在,则从本地去查,查到后再放回缓存。

然后又实现抽象方法 load(key),这个方法的作用就是当读写缓存中没有,则从本地 registry 缓存中拿。

读写缓存过期的时候其实分两种:定时过期和实时过期。由于上面的源码已经定义了定时过期的时间间隔,所以我们先来看定时过期。

3.2 定时过期

当构建这个读写缓存时,就会定义间隔多久过期整个读写缓存。如下代码所示,180 s 会定时过期读写缓存。

expireAfterWrite(180s)

3.3 实时过期

当有新的服务实例进行注册或者下线、发生故障时,就会把这个对应的服务实例的缓存给过期掉。

如下图所示,最上面的时注册中心,下面三个是服务实例。服务实例发生注册、下线、发生故障,注册中心都是可以感知到的,然后就会主动过期读写缓存对应的服务实例。

3.4 实时过期源码

从源码层面我们再来看下读写缓存过期的源码。调用了 invalidateCache 方法,进行过期。

文件路径:com/netflix/eureka/registry/AbstractInstanceRegistry.java

五、只读缓存

5.1 定时更新

只读缓存 readOnlyCacheMap,有一个定时更新的机制,每隔 30 秒就会更新一次只读缓存中的某些 key。

它其实是遍历自己的所有注册信息,然后和读写缓存进行比对,如果注册信息不一致,则替换为读写缓存的数据。

源码如下,有一个定时调度任务,每隔 30 秒调度一次。

5.2 更新

另外当客户端获取注册信息时,也会先读只读缓存,如果只读缓存中没有,则会从读写缓存中找,找到后就放到只读缓存中。如果读写缓存中没有,则从本地注册表 registry 中加载到读写缓存中,然后将注册表信息返回。

> 这里大家是否有个疑问:既然这个缓存叫做只读缓存,怎么还能被更新,不应该是不变的吗?

其实这里的不变是相对于客户端来说的,客户端获取注册表信息时,最开始访问的就是只读缓存,类似数据库或 Redis 的主从架构,主负责读写,从负责读。然后系统内部会把主节点的信息同步给从节点。大家明白了吗?

六、缓存相关配置

下面我们来看下 Eureka Server 对于缓存有哪些配置呢?

6.1 是否开启只读缓存

> eureka.server.useReadOnlyResponseCache

当客户端获取注册信息时,是否先从只读缓存获取。如果为 false,则直接从读写缓存获取。默认为 true。

6.2 定时更新只读缓存的间隔时间

> eureka.server.responseCacheUpdateIntervalMs

默认每隔 30 秒将读写缓存更新的缓存同步到只读缓存。

七、缓存带来的问题

三级缓存看似可以带来性能的提升。但是也会引入其他问题,比如缓存不一致问题。

只读缓存每隔 30s 才会刷新一次,和读写缓存会造成数据的不一致,客户端在 30s 内获取的注册表信息是滞后的。

当使用 Eureka 集群时,这种缓存不一致的问题会更明显,不同的节点之间也会出现只读缓存的数据不一致,所以 Eureka 只能保证高可用,并不能保证强一致性,也就是保证了 AP,不保证 CP,另外我们可以选用强一致性的注册中心,比如 Zookeeper、Nacos,这是后续要讲的内容了。

> 如何缓解不一致的问题呢?

(1)在服务端,我们可以设置更新只读缓存的时间间隔,默认是 30 秒,缩短一点,比如 15 秒,频率太高,可能对 Eureka 造成性能问题。

(2)服务端,我们也可以考虑关闭从只读缓存读注册表信息,Eureka Client 直接从读写缓存读取。

八、总结

Eureka Server 注册表三级缓存架构

本篇学习了 Eureka 注册中心 Server 端的三层缓存架构,分为 registry、readOnlyCacheMap、readWriteCacheMap,用来保存服务注册信息。

  • 默认情况下,每隔 30 秒从读写缓存将注册信息更新到只读缓存。
  • 默认情况下,客户端读取注册表时,先从只读缓存读,如果没有,则从读写缓存中读取,如果还是没有,则从本地注册表 registry 读取。
  • 默认情况下,每隔 180 秒定时过期读写缓存。
  • 服务实例注册、下线、故障时,会实时过期读写缓存。

参考资料: www.passjava.cn 《微服务架构深度解析》 Eureka 源码

> 作者简介:悟空,8年一线互联网开发和架构经验,用故事讲解分布式、架构设计、Java 核心技术。《JVM性能优化实战》专栏作者,开源了《Spring Cloud 实战 PassJava》项目,公众号:悟空聊架构。本文已收录至 www.passjava.cn

展开阅读全文
  • 0
    感动
  • 0
    路过
  • 0
    高兴
  • 0
    难过
  • 0
    搞笑
  • 0
    无聊
  • 0
    愤怒
  • 0
    同情
热度排行
友情链接