Redis VM 相关阐述

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011487710/article/details/79175827

redis官网对于弃用VM的描述 English
下面是谷歌翻译后内容,肯定有出入,请高手指教。

更新:自从Redis 2.6以后,虚拟内存被弃用了,所以这里的文档只是出于历史的原因。

虚拟内存技术规范

本文档详细介绍了Redis虚拟内存子系统的内部结构。目标用户不是最终用户,而是愿意理解或修改虚拟内存实现的程序员。

键与值:什么是换出?
VM子系统的目标是释放将Redis对象从内存传输到磁盘的内存。这是一个非常通用的命令,但具体而言,Redis仅传输与值关联的对象。为了更好地理解这个概念,我们将使用DEBUG命令显示从Redis内部的角度来看,一个保存值的键是如何看起来的:
**这里写图片描述**
从上面的输出中可以看出,Redis顶级散列表将Redis对象(键)映射到其他Redis对象(值)。虚拟内存只能交换磁盘上的值,与密钥关联的对象总是被占用在内存中:这种交换保证了非常好的查找​​性能,因为Redis VM的主要设计目标之一是具有与Redis类似的性能当经常使用的数据集的部分适合RAM时禁用VM。

交换值在内部看起来如何
当一个对象被换出时,这就是哈希表项中发生的事情:

  • 密钥继续保持代表密钥的Redis对象。
  • 该值设置为NULL

所以你可能想知道我们在哪里存储给定的值(与给定的键相关联)被换出的信息。只是在关键的对象!
这就是Redis Object结构robj的外观:
这里写图片描述
正如你所看到的,有几个关于VM的领域。最重要的是存储,可以是这个价值之一:

  • REDISVMMEMORY:关联的值在内存中。
  • REDISVMSWAPPED:关联的值被交换,并且哈希表值项被设置为NULL。
  • REDISVMLOADING:该值在磁盘上交换,条目为NULL,但有一个作业将对象从交换加载到内存(此字段仅在线程VM处于活动状态时使用)。
  • REDISVMSWAPPING:该值在内存中,该条目是一个指向实际Redis对象的指针,但是有一个I / O作业将这个值传送到交换文件。

如果一个对象在磁盘上交换(REDISVMSWAPPED或REDISVMLOADING),我们怎么知道它存储在哪里,它是什么类型等等?这很简单:vtype字段设置为交换的Redis对象的原始类型,而vm字段(即redisObjectVM结构)保存关于对象位置的信息。这是这个额外结构的定义:
这里写图片描述
正如你所看到的,结构包含了交换文件中对象所在的页面,使用的页面数量和对象的最后访问时间(这对于选择什么对象是一个好候选的算法是非常有用的用于交换,因为我们想要在磁盘上传输很少访问的对象)。
正如你所看到的,虽然所有其他字段在旧的Redis对象结构中使用未使用的字节(由于自然内存对齐问题,我们有一些空闲位),但是vm字段是新的,并且实际上使用了额外的内存。即使VM被禁用,我们是否应该支付这样的内存成本?没有!这是创建一个新的Redis对象的代码:
这里写图片描述
正如你可以看到,如果虚拟机系统没有启用,我们只分配内存的sizeof(* o)-sizeof(struct redisObjectVM)。假设vm字段是对象结构中的最后一个字段,并且在禁用VM的情况下永远不会访问这些字段,所以我们是安全的,没有VM的Redis不会支付内存开销。

交换文件
下一步了解虚拟机子系统的工作原理是理解对象如何存储在交换文件中。好消息是,这不是某种特殊格式,我们只是使用相同格式来存储.rdb文件中的对象,这是Redis使用SAVE命令生成的常用转储文件。
交换文件由给定数量的页面组成,其中每个页面大小是给定的字节数。这个参数可以在redis.conf中修改,因为不同的Redis实例可能对不同的值有更好的效果:它取决于你存储在里面的实际数据。以下是默认值:
vm-page-size 32
vm-pages 134217728
Redis在内存中占用了一个“位图”(一个连续的位数组,设置为0或1),每一位代表磁盘上的交换文件的页面:如果给定位设置为1,则表示已经使用的页面(有一些Redis对象存储在那里),而如果相应的位是零,页面是空闲的。
在内存中使用这个位图(这将调用页表)在性能方面是一个巨大的胜利,而且使用的内存很小:我们只需要1位磁盘上的每个页面。例如,在下面的例子中,每个32字节的134217728页(4GB交换文件)仅使用16 MB的RAM作为页表。

将内存中的对象传输到交换
为了将一个对象从内存传递到磁盘,我们需要执行以下步骤(假设非线程虚拟机,只是一个简单的阻塞方法):

  • 查找需要多少页才能将此对象存储在交换文件上。这只是简单地完成调用函数rdbSavedObjectPages返回磁盘上的对象使用的页面的数量。请注意,这个函数不会复制.rdb保存代码,只是为了理解在磁盘上保存一个对象之后的长度,我们使用打开/dev /null并在那里写对象的技巧,最后按顺序调用ftello检查所需的字节数量。我们所做的基本上是将对象保存在虚拟的非常快的文件上,即/dev / null。
  • 现在我们知道了交换文件中需要多少页面,我们需要在交换文件中找到这个数量的连续空闲页面。这个任务是由vmFindContiguousPages函数完成的。正如你所猜测的,如果交换已满,这个函数可能会失败,或者如此分散,以至于我们无法轻易找到所需数量的连续空闲页面。当发生这种情况时,我们只是放弃对象的交换,这将继续存在于内存中。
  • 最后,我们可以将对象写在磁盘上的指定位置,只需调用函数vmWriteObjectOnSwap即可。

正如你可以猜测,一旦对象被正确写入交换文件,它将从内存中被释放,关联的键中的存储字段被设置为REDISVMSWAPPED,并且所使用的页面被标记为在页表中被使用。

将对象加载回内存
从交换到内存加载一个对象更简单,因为我们已经知道对象的位置以及它使用了多少页面。我们也知道对象的类型(加载函数需要知道这个信息,因为在磁盘上没有标题或任何其他关于对象类型的信息),但是它被存储在相关的键的vtype字段中见上面。
调用函数vmLoadObject传递与我们想要加载的值对象关联的键对象就足够了。该函数还将负责修复键的存储类型(这将是REDISVMMEMORY),将页面标记为在页表中释放,等等。
函数的返回值是加载的Redis对象本身,我们将不得不在主哈希表中重新设置值(而不是在值最初被换出时替换为对象指针的NULL值) 。

如何阻止虚拟机工作
现在我们有了所有的构建块来描述阻塞虚拟机如何工作。首先,关于配置的一个重要细节。为了在Redis server.vm_max_threads中启用阻塞VM,必须将其设置为零。稍后我们将会看到在线程化虚拟机中如何使用这个最大数量的线程信息,因为现在所需要的就是当Redis设置为零时,Redis将恢复到完全阻止虚拟机。
我们还需要引入另一个重要的VM参数,即server.vm_max_memory。该参数非常重要,因为它用于触发交换:Redis将尝试仅在交换对象时使用比最大内存设置更多的内存,否则不需要交换,因为我们正在匹配用户请求的内存使用情况。

阻止VM交换
对象从内存交换到磁盘发生在cron函数中。这个函数每秒调用一次,而最近在git上的Redis版本中每100毫秒调用一次(即每秒10次)。如果这个函数检测到我们内存不足,也就是说,使用的内存大于vm-max-memory设置,它开始在调用函数vmSwapOneObect的循环中将对象从内存传输到磁盘。这个函数只需要一个参数,如果是0,它将以阻塞方式交换对象,否则如果是1,则使用I / O线程。在阻塞的情况下,我们只需要以零作为参数来调用它。
vmSwapOneObject执行以下步骤:

  • 在检查的关键空间,以找到一个很好的候选人交换(我们稍后会看到一个很好的候选人交换是)。
  • 关联的值以阻塞的方式传输到磁盘。
  • 密钥存储字段设置为REDISVMSWAPPED,而对象的vm字段设置为正确的值(交换对象的页面索引以及用于交换对象的页面数量)。
  • 最后,值对象被释放,并且哈希表的值条目被设置为NULL。

该函数被一次又一次地调用,直到发生以下情况之一:没有办法交换更多的对象,因为交换文件已满或者几乎所有的对象已经在磁盘上传输,或者只是内存使用已经在虚拟机最大内存参数。

内存不足时需要交换什么值?
了解什么是交换的好选择并不难。随机抽取几个对象,并且每个对象的可交换性都被换算为:
swappability = age*log(size_in_memory)
age是密钥未被请求的秒数,而sizeinmemory是对象在内存中使用的内存量(以字节为单位)的快速估计。所以我们试图换出很少被访问的对象,并且我们试图将较大的对象换成较小的对象,但是后者是不太重要的因素(因为使用了对数函数)。这是因为我们不希望更大的对象被换出,而且为了转移它而需要的I / O和CPU越多,对象越大。

阻止VM加载
如果请求对与被换出的对象关联的键的操作会发生什么?例如,Redis可能恰好处理以下命令:
GET foo
如果foo键的值对象被交换了,我们需要在处理操作之前把它加载回内存。在Redis中,关键查找过程集中在lookupKeyRead和lookupKeyWrite函数中,这两个函数用于实现访问密钥空间的所有Redis命令,所以我们在代码中有一个单独的点来处理从交换文件到内存。
所以这是发生了什么事情:

  • 用户调用一些具有交换键参数的命令
  • 命令实现调用查找函数
  • 查找功能搜索顶级散列表中的键。如果与所请求的键相关联的值被交换(我们可以看到检查键对象的存储字段),我们在返回给用户之前以阻塞的方式将其加载回内存中。

这是非常简单的,但事情会变得更有趣的线程。从阻塞虚拟机的角度来看,唯一真正的问题是使用另一个进程保存数据集,即处理BGSAVE和BGREWRITEAOF命令。

当VM处于活动状态时保存背景
在磁盘上保留的默认Redis方式是使用子进程创建.rdb文件。 Redis调用fork()系统调用来创建一个具有内存数据集的精确副本的子对象,因为fork重复了整个程序内存空间(实际上归功于一个称为Copy on Write内存页的技术在父进程和子进程,所以fork()调用将不需要太多的内存)。
在子进程中,我们有一个在给定时间点的数据集的副本。其他由客户端发出的命令将仅由父进程提供服务,不会修改子数据。
子进程只会将整个数据集存储到dump.rdb文件中,最后退出。但是当VM处于活动状态时会发生什么?值可以换出,所以我们没有在内存中的所有数据,我们需要访问交换文件,以检索交换值。虽然子进程正在保存交换文件在父进程和子进程之间共享,因为:

  • 父进程需要访问交换文件,以便在执行对换出的值的操作时将值加载回内存。
  • 子进程需要访问交换文件以检索完整的数据集,同时将数据集保存在磁盘上。

为了避免两个进程访问同一个交换文件时出现问题,我们做了一件简单的事情,那就是在后台保存过程中,不允许在父进程中将值交换出去。这样,这两个进程将以只读方式访问交换文件。这种方法存在的问题是,尽管子进程正在保存,但是即使Redis使用的内存大于最大内存参数指定的内存,也不能在交换文件上传输新值。这通常不是一个问题,因为后台保存将在很短的时间内终止,如果仍然需要一定百分比的值将在磁盘上尽快交换。
此方案的一种替代方法是启用只有在使用BGREWRITEAOF命令执行日志重写时才会出现此问题的“附加文件”。

阻塞虚拟机的问题
阻塞虚拟机的问题在于:阻塞:)在批处理活动中使用Redis时,这不是问题,但对于实时使用,Redis的一个优点是低延迟。当客户端正在访问换出的值,或者Redis需要换出值时,阻塞虚拟机的延迟行为将会变得很糟糕,同时其他客户端也不会被服务。
换出钥匙应该在后台进行。同样,当客户端正在访问换出的值时,其他访问内存值的客户端应该大致与VM被禁用时一样快。只有处理换出密钥的客户应该被延迟。
所有这些限制都要求实现一个无阻塞的虚拟机。

线程机制的虚拟机
基本上有三种主要方法可以将阻塞虚拟机变成非阻塞虚拟机。 * 1:一种方法是显而易见的,在我看来,根本不是一个好主意,就是把Redis本身变成一个线程服务器:如果每个请求都由另一个线程自动服务,其他客户端不需要等待被阻挡的Redis速度很快,导出原子操作,没有锁,只有一千行代码,因为它是单线程的,所以这不是我的选择。 * 2:对交换文件使用非阻塞I / O。毕竟,你可以认为Redis已经是基于事件循环的,为什么不以非阻塞的方式处理磁盘I / O呢?由于两个主要原因,我也抛弃了这种可能性。一个是非阻塞文件操作,不像套接字,是一个不兼容的噩梦。这不仅仅是调用select,你需要使用特定于操作系统的东西。另一个问题是I / O只是处理虚拟机所花费的时间的一部分,另一个重要部分是用于对交换文件进行编码/解码数据的CPU。这是我选择的选项三,即… * 3:使用I / O线程,即处理交换I / O操作的线程池。这是Redis虚拟机正在使用的,所以我们来详细说明这是如何工作的。

I / O线程
线程化虚拟机的设计目标在以下几个方面,按重要程度排列:

  • 简单的实现,竞争条件的小空间,简单的锁定,虚拟机系统或多或少地完全脱离其余的Redis代码。
  • 良好的性能,客户端访问内存中的值没有锁。
  • 能够在I / O线程中解码/编码对象。

上述目标导致Redis主线程(服务于实际客户端的线程)和I / O线程使用作业队列与单个互斥体进行通信。基本上,当主线程需要某些I / O线程在后台完成一些工作时,它会在server.io_newjobs队列中(即,仅链接列表)推送一个I / O作业结构。如果没有活动的I / O线程,则启动一个线程。此时某些I / O线程将处理I / O作业,并将处理结果压入server.io_processed队列。 I / O线程将使用UNIX管道向主线程发送一个字节,以表示已经处理了新的作业,并且结果已准备好进行处理。
这就是iojob结构的样子:
这里写图片描述
I / O线程只能执行三种类型的作业(类型由结构的类型字段指定):

  • REDISIOJOBLOAD:将与给定密钥关联的值从交换加载到内存。交换文件内的对象偏移量是页面,对象类型是key->
    vtype。此操作的结果将填充结构的val字段。
  • REDISIOJOBPREPARE_SWAP:计算为了将val指向的对象保存到交换中所需的页数。此操作的结果将填充页面字段。
  • REDISIOJOBDO_SWAP:将val指向的对象转移到页面偏移页面的交换文件。

主线程委托上述三个任务。其余的都由主线程自己处理,例如在交换文件页表中找到适当范围的空闲页面(这是一个快速操作),决定交换哪个对象,改变Redis对象的存储字段以反映一个值的当前状态。

非阻塞虚拟机作为阻塞虚拟机的概率增强
所以现在我们有办法请求处理慢VM操作的后台作业。如何将其添加到由主线程完成的其余工作的混合?虽然阻止虚拟机意识到一个对象被换出来只是当对象被查找时,这对我们来说已经太迟了:在C中,在命令中间启动一个后台作业并不重要,离开该函数并重新在I / O线程完成我们所要求的(也就是说,没有协同程序或延续或类似的)的情况下输入计算。
幸运的是,这样做有很多简单的方法。我们喜欢简单的东西:基本上把虚拟机的实现看成一个阻塞的虚拟机,但是增加一个优化(使用非阻塞虚拟机的操作,我们可以执行),使得阻塞的可能性很小。
这就是我们所做的:

  • 每次客户端向我们发送命令时,在命令执行之前,我们检查命令的参数向量以搜索交换的密钥。毕竟我们知道每个命令的参数是关键,因为Redis命令格式非常简单。
  • 如果我们检测到请求的命令中至少有一个密钥在磁盘上被交换,我们将阻塞客户端,而不是真的发出命令。对于与所请求的键关联的每个交换值,都会创建一个I/ O作业,以便将这些值返回到内存中。主线程继续执行事件循环,而不关心被阻塞的客户端。
  • 同时,I / O线程正在内存中加载值。每次I / O线程完成一个值的加载,它都会使用一个UNIX管道向主线程发送一个字节。管道文件描述符具有在主线程事件循环中相关的可读事件,即函数vmThreadedIOCompletedJob。如果此函数检测到阻塞客户端所需的所有值都已加载,则客户端将重新启动并调用原始命令。

所以你可以把它看作是一个阻塞的虚拟机,它几乎总是在内存中有正确的密钥,因为我们暂停了将要发出有关换出值的命令的客户机,直到这个值被加载。
如果检查什么参数是一个键的函数以某种方式失败,那么没有任何问题:查找函数将看到一个给定的键与一个换出的值相关联,并将阻止加载它。所以当我们无法预测哪些键被触摸的时候,我们的非阻塞虚拟机会回复到阻塞状态。
例如,在SORT命令与GET或BY选项一起使用的情况下,预先知道要请求的密钥并不是微不足道的,所以至少在第一个实现中,SORT BY / GET使用阻塞的VM实现。

在交换的密钥上阻塞客户端
如何阻止客户?暂停基于事件循环的服务器中的客户端是相当简单的。我们所做的只是取消它的读取处理程序。有时候我们会做一些不同的事情(例如BLPOP),只是将客户端标记为阻塞,而不处理新的数据(只是将新数据累加到输入缓冲区中)。

中止I / O作业
关于我们的阻塞和非阻塞虚拟机之间的交互有一些难以解决的问题,也就是说,如果一个阻塞操作从一个非阻塞操作同时“感兴趣”的密钥开始,会发生什么?
例如,当执行SORT BY时,通过排序命令以阻塞方式加载几个键。同时,另一个客户端可能会用简单的GET密钥命令请求相同的密钥,这将触发创建I / O作业以在后台加载密钥。
处理这个问题的唯一简单方法是能够终止主线程中的I / O作业,以便如果我们想要以阻塞方式加载或交换的密钥处于REDISVMLOADING或REDISVMSWAPPING状态(即,关于这个键有一个I / O的工作),我们可以杀死关于这个键的I / O作业,然后继续我们要执行的阻塞操作。
这并不像现在这样微不足道。在某个特定时刻,I / O作业可能处于以下三个队列之一:

  • server.io_newjobs:作业已经排队,但没有线程正在处理它。
  • server.io_processing:作业正在由I / O线程处理。
  • server.io_processed:作业已经被处理。能够杀死一个I / O作业的函数是vmCancelThreadedIOJob,这就是它所做的。
  • 如果作业在newjobs队列中,那很简单,从队列中移除iojob结构就足够了,因为没有线程仍在执行任何操作。
  • 如果作业在处理队列中,则线程正在搞乱我们的工作(可能还有相关的对象!)。我们唯一能做的就是等待物品以阻塞的方式移动到下一个队列。幸运的是,这种情况很少发生,所以这不是一个性能问题。
  • 如果作业正在处理队列中,我们将其标记为取消标记,将iojob结构中的已取消字段设置为1。函数处理完成的作业将被忽略并释放该作业而不是真正处理作业。

面临的问题
这个文档是不完整的,唯一的方法就是阅读源代码,但是为了使代码的审查/理解变得简单,应该是一个很好的介绍。
有什么不清楚这个网页?请留下评论,我会尽力解决这个问题可能整合在这个文件的答案。

猜你喜欢

转载自blog.csdn.net/u011487710/article/details/79175827
vm
今日推荐