如何为多租户架构缓存应用程序

发布于:2020-12-31 14:11:02

0

158

0

架构缓存 应用程序 缓存

所以…缓存。它是什么?这是一种快速获得收益的方法,无需反复计算或获取内容,从而获得性能和成本优势。这也是它的名字来源,它是“cache -ching!”收银机的声音来自2014年的黑暗时代,那时还没有Apple Pay,实物货币还很流行。我现在是父亲了,接受现实吧。

假设我们需要调用一个API或查询数据库服务器,或者只是取一大堆数字(谷歌表示这是一个实际的单词,我检查过了),然后把它们加起来。这些都相当昂贵。所以我们缓存结果——我们让它便于重用。

我们为什么要缓存?

我认为讨论这个问题在这里重要的 是多么昂贵 上面的一些东西。现代计算机中已经有几层缓存在起作用。作为一个具体的例子,我们将使用我们的一个Web服务器,该服务器目前装有一对 Intel Xeon E5-2960 v3 CPU 和2133MHz DIMM。高速缓存访问是处理器的“多少个周期”功能,因此通过知道我们始终以3.06GHz(性能功耗模式)运行,我们可以得出延迟(此处为Intel体系结构参考 –这些处理器属于Haswell一代):

  • L1(每个内核):4个周期或 〜1.3ns 延迟– 12x 32KB + 32KB

  • L2(每个内核):12个周期或 〜3.92ns 延迟– 12x 256KB

  • L3(共享):34个周期或 〜11.11ns 延迟– 30MB

  • 系统内存:  〜100ns 延迟– 8倍8GB

每个缓存层都可以存储更多,但是距离更远。在处理器设计中要权衡取舍。例如,平均每个内核更多的内存意味着(几乎可以肯定)意味着将其与内核之间的距离更远,这会增加延迟,机会成本和功耗。在此范围内,电子必须行进多远会产生重大影响。请记住,距离 每秒乘以 数十亿。

而且我没有涉及上面的磁盘延迟,因为我们很少接触磁盘。为什么?好吧,我想解释一下,我们需要……看一下磁盘。哦,闪闪发光的磁盘!但请不要在袜子跑动后触摸它们。在Stack Overflow上,任何不是备份或日志记录服务器的产品都位于SSD上。本地存储通常分为以下几层:

  • NVMe SSD:〜120μs(来源)

  • SATA或SAS SSD:〜400–600μs(来源)

  • 旋转硬盘:2–6ms(源)

这些数字一直在变化,所以不要过多地关注确切的数字。我们试图评估的是这些存储层之间差异的大小。让我们看一下列表(假设每个数字的下限,这是最好的数字):

  • L1:1.3ns

  • L2:3.92ns(慢3倍)

  • L3:11.11ns(慢8.5倍)

  • DDR4 RAM:100ns(慢77倍)

  • NVMe SSD:120,000ns(慢92,307倍)

  • SATA / SAS SSD:400,000ns(慢307,692x)

  • 旋转硬盘:2–6ms(慢1,538,461x)

  • Microsoft Live登录:12次重定向和5秒(慢3,846,153,846倍,大约)

如果数字不是您的事, 这是Colin Scott提供的简洁的开源可视化效果 (使用滑块!)   (您甚至可以查看它们随着时间的推移是如何演变的-确实很简洁):

{xunruicms_img_title}

考虑到这些绩效数字并考虑规模感,让我们添加一些每天都很重要的数字。假设我们的数据源是 X, X 无关紧要。它可能是SQL,或者是微服务,或者是宏服务,或者是左垫服务,或者是Redis,或者是磁盘上的文件等。这里的关键是我们正在将源的性能与RAM的性能进行比较。假设我们的来源需要……

  • 100ns(从RAM –快速!)

  • 1ms(慢10,000倍)

  • 100ms(慢100,000倍)

  • 1秒(慢1,000,000倍)

我不认为我们需要去进一步说明这一点: 即只需要1毫秒,即使事情的方式, 方法 比本地内存要慢。请记住:毫秒,微秒,纳秒–以防万一其他人忘记了1000ns!= 1ms,就像我有时做的那样……

但是并不是所有的缓存都是本地的。例如,我们使用Redis进行Web层背后的共享缓存(我们将在稍后介绍))。假设我们要遍及整个网络来获取它。对于我们来说,这是一个0.17毫秒的往返,您还需要发送一些数据。对于小物件(通常),总共约为0.2-0.5ms。仍然比本地RAM慢2000–5,000倍,但也比大多数来源快很多。请记住,这些数字是因为我们位于小型局域网中。云延迟通常会更高,因此请查看您的延迟。

当我们获得数据时,也许我们也想以某种方式对其进行按摩。可能是瑞典人。也许我们需要总计,也许我们需要过滤,也许我们需要对其进行编码,也许我们需要随机捏弄它以欺骗您。那是一项测试,看您是否还在阅读。你通过了!不管原因是什么,通用性通常是 我们想做 <x> 一次,而不是 每次我们服务时。

有时我们节省延迟,有时我们节省CPU。通常,其中一个或两个都是为什么要引入缓存。现在,让我们涵盖另一面...

为什么我们不缓存?

对于每个讨厌缓存的人,这是给您的部分!是的,我完全扮演双方的角色。

鉴于上述情况以及获胜的激烈程度, 我们为什么 不缓存一些东西?好吧,因为 每个决策都需要权衡取舍。每一个 单。一。它可以很简单,只需花费时间或机会成本,但仍然需要权衡取舍。

当涉及到缓存时,添加缓存会带来一些成本:

  • 在需要时以及在需要时清除值(缓存无效- 我们将在少数几个中介绍)

  • 缓存使用的内存

  • 访问缓存的延迟(权衡了对源的访问)

  • 花费更多的时间和精力在调试更复杂的事情上

每当出现缓存候选对象(通常具有新功能)时,我们都需要评估这些内容……这并不总是一件容易的事。尽管缓存是一门精确的科学,就像占星术一样,它仍然很棘手。

在Stack Overflow,我们的体系结构具有一个总体主题:使其尽可能简单。简单易于评估,推理,调试和根据需要进行更改。仅在需要时和更复杂的情况  下使其更复杂。这包括缓存。仅在需要时缓存。它增加了更多的工作并 增加了错误的机会,因此除非需要,否则:不需要。至少还没有。

  • 让我们开始问一些问题。

  • 命中缓存快得多吗?

  • 我们要保存什么?

  • 值得储存吗?

  • 是否值得清理上述存储(例如垃圾收集)?

  • 它会立即在大对象堆上继续吗?

  • 我们必须多久使它失效一次?

  • 我们认为每个缓存条目会有多少次点击?

  • 它会与其他使无效化复杂化的事物发生相互作用吗?

  • 会有多少个变体?

  • 我们是否仅需分配就可以计算密钥?

  • 它是本地还是远程缓存?

  • 用户之间共享吗?

  • 站点之间共享吗?

  • 它是依靠量子纠缠还是调试它只是让您认为呢?

  • 缓存是什么颜色?

所有这些都是提出来并影响缓存决策的问题。我将尝试通过这篇文章覆盖它们。

堆栈溢出时的缓存层

我们在Stack Overflow上拥有自己的“ L1” /“ L2”缓存,但是为了避免与上述CPU缓存混淆,我将避免以这种方式引用它们。我们拥有几种类型的缓存。在深入探究它们使用的通用位之前,让我们首先在此处快速介绍本地和内存缓存的术语。

  • “全局缓存”:内存中的缓存(全局,每个Web服务器,并在未命中时由Redis支持)

  • 通常情况下,诸如用户的最高收入限额之类的东西在网络上共享

  • 这将命中本地内存(共享密钥空间),然后是Redis(共享密钥空间,使用Redis数据库0)

  • “站点缓存”:内存中缓存(每个站点,每个Web服务器,并在未命中时由Redis支持)

  • 通常,每个站点的问题列表或用户列表之类的内容

  • 这会命中本地内存(使用前缀的每个站点密钥空间),然后是Redis(使用Redis数据库的每个站点密钥空间)

  • “本地缓存”:内存缓存(每个站点,每个Web服务器,无任何支持 )

  • 通常情况下,价格便宜但可串流播放且Redis跃点不值得

  • 这只会影响本地内存(每个站点的键空间,使用前缀)

我们所说的“每站点”是什么意思?堆栈溢出和站点的堆栈交换网络是 一个多租户架构。堆栈溢出只是 数百个站点之一。这意味着Web服务器上的一个进程将托管所有站点,因此我们需要在需要的地方拆分缓存。而且我们必须清除它(我们还将介绍它的工作原理)。

Redis

在讨论服务器和共享缓存的工作方式之前,让我们快速介绍一下共享位的基础:Redis。那么,Redis是什么 ?这是一个开放源代码键/值数据存储,具有许多有用的数据结构,附加的发布/订阅者机制以及坚如磐石的稳定性。

为什么不使用Redis  <something else>?好吧,因为它有效。而且效果很好。当我们需要共享缓存时,这似乎是一个好主意。这 真是令人难以置信的 坚如磐石。我们没有等待-它的速度非常快。我们知道它是如何工作的。我们对此非常熟悉。我们知道如何监视它。我们知道如何拼写。我们为此维护了最常用的开源库之一。如果需要,我们可以调整该库。

这是我们的基础设施 只是不用担心。我们基本上认为这是理所当然的(尽管我们仍然有HA副本设置–我们并不 完全 疯狂)。在选择基础架构时,您不只是为了获得可能的价值而进行更改。改变需要付出努力,花费时间并带来风险。如果您拥有的东西运作良好并且需要您做什么,为什么还要花时间和精力去冒险?好吧...你不知道。随着时间的流逝,您可以做数千件更好的事情。就像辩论哪个缓存服务器是最好的一样!

我们有一些Redis实例来区分应用程序的关注点(但在同一组服务器上),这是一个看起来像这样的示例:

{xunruicms_img_title}

出于好奇,上周二(2019-07-30)的一些快速统计信息涵盖了主框上的所有实例(因为我们将它们拆分用于组织工作,而不是性能……一个实例可以轻松完成我们的所有工作):

  • 我们的Redis物理服务器具有256GB内存,但使用的内存少于96GB。

  • 每天处理1,586,553,473条命令(由于副本,在所有实例中,每秒3,726,580,897条命令和每秒86,982个峰值)

  • 整个服务器的平均CPU利用率为2.01%(峰值为3.04%)(即使是最活跃的实例,均<1%)

  • 124,415,398个活动密钥(包括副本的422,818,481个)

  • 这些数字遍及308,065,226个HTTP命中(其中64,717,337个是问题页面)

注意:这些都不是Redis的限制-我们还没有任何限制。只是我们的实例上有多少活动。

还有其他非缓存原因,我们使用Redis,即:我们还对Websocket使用pub / sub机制 , 以提供有关乐谱,代表等的实时更新。Redis5.0 添加了Streams  ,这非常适合我们的Websocket,我们当其他一些基础架构就绪时(可能主要受Stack Overflow Enterprise版本的限制),它们可能会迁移到它们。