缓存(cache)可以用来提升性能以及提高一个微服务系统的稳定性。

虽然,缓存这个名称应用在很多方面:局部,分布式,多层次;但是你是否知道在你的开发场景下需要使用哪一个?

本文提供一个深入的介绍关于一系列缓存策略的优缺点,从而帮助你在开发自己的系统时作出正确的决策。

1.我们为什么需要缓存(cache)?

在给出答案之前,让我们首先聚焦这个问题:缓存在微服务架构中解决了哪些问题?

1-1微服务简要分析

一个微服务架构,能够帮助一个组织更快的发展。复杂的商业流被分解为更为细小的领域,有不同的负责团队实现。他们通过APIs进行交互,只要保证API协议的稳定性,从而不需要严格的划定界限,不同领域团队可以实现更快速的开发。

然而,复杂度并没有消失,它仅仅是分散到其他部分。每当我们划分一个领域,那么就要添加一个或多个全新的微服务。我们之前使用单个应用解决的问题,需要额外在网络上添加需求。

这些众多需求中任何一个失败的话就会给整体系统的实现性带来巨大的负面影响,这就是复杂所在。

1-2远程调用程序(RPC)的代价

首先,让我们以下面的简单模型作为一个简单的微服务系统:

cache小技巧(你是否正确使用cache)(1)

图1

如上图所示,我们将Egde API暴露在网络上。每次,Edge API被终端用户调用来实施一次操作,Edge API在网络上实施两个额外的请求:一个对应于A,一个对应于B。A同样不是自我满足的:它通过调用C和D来最终满足Edge API。

现在,让我们看一下这些远程调用程序(RPC)是如何影响系统的性能和可利用性。

可利用性

首先,我们给出可利用性的定义:对于格式正确的需求被一个服务成功处理的比例:

cache小技巧(你是否正确使用cache)(2)

公式1

例如:如果接受100个请求,只有99个被成功处理,那么我们服务的可利用性就是0.99。一般情况下,我们会使用百分比来表示可利用性,在这个例子中,我们服务的可利用性就是99%。

接下来,我们探讨远程调用程序(RPCs)对于可用性的影响。即使在没有RPCs的情况下,我们仍然无法保证从一个API中获得100%的可利用性,因为,底层机器可能突然宕机,或者机器运行内存爆炸,也同样有可能机房中心意外失火等等。

这里,使用​表示我们上图给出模型中某个组件独立运行处理需求失败的概率,它的范围是(0<Pf​<1)。

对应的,​表示运行成功的概率,因此:

​Ps = 1 - Pf

如果一个服务没有依赖(例如上图中的C),那么​也就直接表示为此服务的可利用性。例如:如果​,也就是我们有99.99%的几率成功处理传入的需求。

让我们看一下有依赖的服务:A。A只有在如下的情况下成功处理一个传入请求:

那么关于Egde API呢?

显而易见,我们给出实例图中的服务的可利用性表示为:,其中,表示某个服务直接或间接依赖的数量。对于Edge API来说,就是。

对于这样的结果来说,到底是好还是坏呢?

让我们根据和不同的值给出不同的可利用性:

n=0

n=1

n=2

n=5

n=10

99.999%

99.998%

99.997%

99.994%

99.989%

99.99%

99.98%

99.97%

99.94%

99.89%

99.95%

99.90%

99.85%

99.70%

99.45%

99.90%

99.80%

99.70%

99.40%

98.91%

99.50%

99.00%

98.51%

97.04%

94.64%

如果我们假设请求率保持不变,则可以将这些可用性转换为宕机预算:您的服务可以停机多长时间?

如果我们运行一个可利用性达到99.99%的服务,那么每年宕机的时间就不会多于52分钟。如果你的Edge API有10个依赖,那么为了满足整体99.99%的可利用性,系统中每个组件需要具备99.999%的可利用性,因此,分担到内部API每年可宕机的时间仅有5分钟。

还有一个现象需要指出的是:只要有一个组件出现错误操作,都会破坏整体系统的可实用性。如果10个依赖中有一个提供99.5%的可利用性,而不能达到99.99%,那么整体系统的可利用性只能达到99.4%。

说了这么多,缓存(cache)到底怎么用,我们可以使用缓存来捕获依赖链的突发问题。那么,缓存到底有什么作用呢?如下图所示,让我们聚焦Edge API和A:

cache小技巧(你是否正确使用cache)(3)

图2

我们在Edge API中引入一个客户端缓存,来存储所对应于A的响应,用伪代码表示如下:

if cache_hit(request): return get_from_cache(request) else: response = call_a(request) set_cache_in_background(response) return response

if local_cache_hit(request): return get_from_local_cache(request) else: if remote_cache_hit(request): return get_from_remote_cache(request) else: response = call_a(request) set_local_cache_in_background(response) set_remote_cache_in_background(response) return response

如果你的系统最关心的是速度,那么一个局部缓存比较合适。如果你的系统最关心可利用性,那么考虑远处缓存。如果你想都考虑在内,那么选择多层次缓存。

总结

可观测性来说,一个操作可以检测远处缓存中的字段,而不是局部缓存中的。

可利用性来说,所有的客户端副本在本地缓存是冷缓存情况下,都转变为访问远处缓存。

速度来说,在较好的情况下,它和局部缓存一样快。如果局部缓存是冷缓存,但是在远处缓存击中的话,还是比查询服务器快。

操作易用性来说,你只需要操作本地和远程缓存。

这一设定类似于CPU缓存的操作过程。

每一个客户端副本保存一份局部缓存以及在本地运行寻求查询失败后,运行一个远程缓存。伪代码如下:

可观测性来说,一个操作可以简单连接到远程缓存来监测存储字段。

可利用性对于所有客户端副本来说,都是访问相同的远程缓存。如果远程缓存是热缓存,那么新的客户端副本就可以维持操作而不需要在服务器宕机的情况下导致运行失败。

速度在我们操作一个远程调用程序来访问或者设置一个缓存字段时,是很重要的属性。远程缓存可以从内存中读取数值,所以还是比直接从服务器端读取来得快。

操作易用性来说,你仅需要在应用程序之上运行另一个系统,该系统具有其自身的故障模式和特点。您可以依靠由项目维护者或公共云提供商运营的托管解决方案来降低风险。

在特殊的远程服务器上,传回了一份响应数据,由客户端缓存存储(例如:Redis,Memcached)。

可观测性不是很直观,尤其是没有将内存当作储存使用时,就无法监测一个局部缓存。

那么如果服务器宕机/性能下降,其对应的可利用性如何呢?如果缓存满足于所需要所有响应(也就是热缓存),那么没有问题,客户端不会收到服务器的影响。局部缓存,虽然是局部的,但是当我们发布一个全新的客户端应用时,它的局部缓存则是完全时空的(冷缓存)。新的客户端应用在服务器宕机时,不能利用缓存继续工作,因此有着失败的风险。

局部缓存中的速度是相对比较快的,也就是一个缓存击中的数量级比从服务器直接获取低很多。

局部缓存的操作易用性,可以简单举个例子,如:Java中ConcurrentHashMap或者Rust中的RwLock<HashMap<key, Value>>。

在同一台机器上运行的客户端,保存了一份服务器返回的响应;这一响应数据既可以被保存在内存中,也可被存放在磁盘上。

缓存类型

对于每种类型的缓存,我们关注于:

我们确定的是,热路径中的组件数量会对微服务系统的可用性和延迟产生负面影响。在客户端缓存,如果击中率足够高的话,那么是一个高效的策略:它可以允许我们跳过调用树的整个子集,有效提高了系统的可利用性和速度。下面,我们详细查看三种不同类型的缓存策略:局部、分布式、多层次。

2.我们应该使用哪种缓存(cache)?

那么,如何使用缓存来打破依赖链?当涉及到延迟,我们应用于可利用性的推理同样重要,一个缓存允许我们移除调用树中的一整个子集。我们假设每个服务延迟的中位数是50毫秒,包括缓存。没有缓存,Edge API的中位数延迟是250毫秒。如果我们缓存A的响应,对于Edge API的中位数延迟就变成150毫米(Edge API B 缓存)。如果缓存比一个RPC(也就是在内存中)快一个数量级,那么我们的延迟可以下降到100毫米,比我们开始的快将近两倍。

系统延迟的中位数可以被大约描述为:所有组件延迟中位数之和。如果我们模型中每个组件的延迟中位数是50毫秒(包括网络时耗),Edge API就会有一个中位数为250毫秒的延迟。对于长尾延迟有了一定的提高:它的P99比单个微服务架构的P99之和小。这给出了直观的理解:一个需求如果很不幸的失败了,那么对于微服务架构整个长尾延迟会被拉长。

从数学角度来看,有点复杂。我们可以借助一个长尾分布图,大致描述我们组件的延迟:

接下来,我们考虑远程调用程序在延迟上的影响。我们假设,对依赖项的请求未并行触发(也就是:Edge API调用B之前首先等待A的响应)。那么,我们Edge API的依赖数量是如何影响我们的系统延迟的呢?

每个需求都要花费不同的时间来完成,我们仅关心一些整体的因素来决定系统如果操作。我们常常使用中位数来衡量响应时间,以及长尾延迟的分布。如果我们系统的响应时间的中位数是67毫秒,那么50%的进入需求在少于67毫秒被完成。说到长尾延迟,我们将关注到延迟百分比----P90, P95,P99。如果说我们系统的P99是162毫秒,也就是99%以上的进入需求在少于162毫米被解决。

让我们回顾系统的另一个重要的属性----延迟,也就是,要多久之后,系统才能响应需求?

延迟

如果我们缓存了A的响应,我们不仅可以跳过对于A的调用,同时可以避免调用C和D。这样,就将Edge API的依赖数量从5减少到1个,此外,我们的可利用性就从99.94%提升到99.98%。

我们假设。如果我们没有使用一个缓存,那么Edge API的可利用性只有大概99.980%。假设有50%的可能性击中一个缓存响应,那么Edge API的可利用性就可以达到99.985%。如果有90%的可能性击中一个缓存响应,那么Edge API的可利用性就可以达到99.989%。换句话说,如果我们缓存命中率很高,Edge API的可利用性就能够得到很大的提高。让我们回到原始的图像:

我们用来表示缓存击中的概率,那么Edge API可利用性就说:

假设A是唯一的依赖,那么对于Edge API的成功调用,取决于:

那么这到底如何影响Edge API的可利用性的呢?

,