内存优化

本页面是进程内的一个工作。目前它仅是一个当你的内存出现问题时的检查清单。

小型聚合数据类型的特殊编码

从Redis2.2开始,许多数据类型被优化为在一定大小下使用更少的空间。哈希,列表,仅由整数组成的集合,和有序集合,当元素数量小于一个给定数字,并且到元素最大尺寸,使用一个非常有效率的内存编码方式可以达到最高减少10倍内存(平均节省5倍内存)。

从用户和API的视角这是完全是透明的。因为这是CPU/memory权衡,它可能会使用下面的redis.conf指令调优特殊编码的元素最大数字和最大尺寸。

hash-max-ziplist-entries 512
hash-max-ziplist-value 64
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
set-max-intset-entries 512

如果特殊编码的值超过了配置中的最大尺寸,Redis将会自动将其转换为普通编码。这个操作对于小的值非常快,但如果你为了特别大的聚合类型使用特殊编码更改配置,建议是运行一些基准测试,并且测试检查下转换时间。

使用32位实例

Redis为了每个键使用更少的内存而使用32位编译,因为指针式小的,但是一个实例的最大内存使用将会限制在4GB。为了以32位编译Redis,使用make 32bit。RDB和AOF文件兼容32位和64位实例(同样也兼容小端字节序和大端字节序),因此你可以从32切换到64,或者相反,都没有问题。

位和字节层级的操作

Redis2.2引入新的bit和byte等级的操作:GETRANGE, SETRANGE, GETBIT 和 SETBIT。使用这些命令,你可以将Redis字符串类型看成是随机访问的数组。例如,如果你有一个应用,使用唯一渐增的证书标识,你可以使用位图保存用户的邮件列表订阅信息,在用户订阅和取消订阅时设置位,或者相反方向。保存1亿用户的这种信息仅需要占用12M的Redis RAM空间。你同样可以使用GETRANGE 和 SETRANGE为每位用户保存单字节的信息。这仅仅是一个例子,但是它确实可以使用新的基元在非常小的空间内去建模一系列的问题。

尽可能使用哈希

小的哈希被编码在非常小的空间内,因此你需要在可能的情况下去尝试使用哈希来代表你的数据。例如,如果在网页应用中使用对象代表用户,使用包含所有必须字段的一个哈希取代分别为name,surname,email,password字段设置一个不同的key。

如果你想要知道更多,阅读下面的部分。

使用哈希将存储在Redis上的普通键-值抽象成非常有内存效率的

我知道这个部分的标题很吓人,但是我将会详细解释它们包含什么。

基本上可以使用Redis建模一个普通的键-值存储,其中值仅允许是字符串,它不仅比普通的Redis键更有存储效率,同时也比memcached更有存储效率。

让我们从一些事实开始:几个键比一个包含一些字段哈希的键占用更多的内存。这怎么可能呢?我们使用了一个技巧。在理论上,为了确保能实现我们在常量时间内执行查找(在大O记法中也称为O(1)),需要使用一个在平均情况下时间复杂度是常量的数据结构,例如哈希表。

但是很多时候哈希仅包含少量字段。当一个哈希很小的时,我们可以使用一个O(N)的数据结构编码它,像一个使用长度前缀的键值对的线性数组一样。因为我们只有在N很小时才会这么使用,因此HGET和HSET命令的均摊时间依旧是O(1):当它包含的元素的数量增长到很大时,我们会将它转换成一个真正的哈希(你可以在redis.conf里配置限制)。

这不仅仅从时间复杂度视角运行的很好,但也从常量时间视角看,因为一个键值对的线性数组能很好的处理CPU缓存(相比哈希表,它是一个更好的缓存地方)。

无论如何,因为哈希字段和值不总是代表所有特性的Redis对象,哈希字段不能像一个真实的键一样关联一个生存时间(live),并且只能包含字符串。但我们是没问题的,这是我们设计哈希这种数据类型API的意图(我们相信简单比特性重要,所以嵌套的数据结构不被允许,就像单个字段的过期时间不被允许一样)。

所以,哈希是有内存效率的。当我们使用哈希代表一个对象或建模其他有关联的字段分组的问题时,这是非常有用。如果我们使用普通的键值业务时呢?

想象一下我们想要使用Redis来缓存很多小的对象,那些可以使用JSON编码的对象,小的HTML片段,简单的键key->布尔值等等。基本上任何事物都是一个小的键和值的string->string映射。

现在,让我们假设我们想要缓存的对象是有序的,例如:

  • object:102393
  • object:1234
  • object:5

这是我们能做的。每次我们只行一个SET操作来设置一个新的值,我们实际上将键拆分为两部分,一部分像key一样使用,另外一部分作为哈希的字段名。例如,名为"object:1234"的对象实际上拆分为:

  • 一个名为 object:12 的键
  • 一个名为34的字段

所以,我们使用所有的的字符,但是最后两位是键,并且最后两个字符是哈希字段的名字。我们使用下面的命令设置键。

HSET object:12 34 somevalue

就像你看到的,每个哈希将包含100个字段结束,那是一个在CPU和内存节省之间最优的折衷。

有另外一件非常重要的事情需要注意,使用这个模式,不管我们缓存的对象的数字,每个哈希将会拥有100个左右的字段。这是因为,我们的对象总将以一个数字结尾,不是一个随机字符串。从某种意义上说,最后的数字可以被认为是隐式预分片的形式。

小数字呢?像对象:2?我们只使用"object:"来处理这个案例,像一个键名,并且整个数字作为哈希字段名。因此object:2和object:10都将在"object:"键内结束,但是一个以键名”2",另外一个以”10"。

这个方法我们节省了多少内存?

我用下面的Ruby程序来测试这是如何工作的:

require 'rubygems'
require 'redis'

UseOptimization = true

def hash_get_key_field(key)
    s = key.split(":")
    if s[1].length > 2
        {:key => s[0]+":"+s[1][0..-3], :field => s[1][-2..-1]}
    else
        {:key => s[0]+":", :field => s[1]}
    end
end

def hash_set(r,key,value)
    kf = hash_get_key_field(key)
    r.hset(kf[:key],kf[:field],value)
end

def hash_get(r,key,value)
    kf = hash_get_key_field(key)
    r.hget(kf[:key],kf[:field],value)
end

r = Redis.new
(0..100000).each{|id|
    key = "object:#{id}"
    if UseOptimization
        hash_set(r,key,"val")
    else
        r.set(key,"val")
    end
}

这是一个使用Redis2.2的64位实例的结果:

  • UseOptimization set to true: 1.7 MB of used memory
  • UseOptimization设置为真(true):使用1.7MB
  • UseOptimization set to false; 11 MB of used memory
  • UseOptimization设置为假(false):使用11MB内存

这个是一个数量级,我认为,这使Redis或多或少以内存效率最高的存储普通键值。

注意:为了使这个能工作,确认在你的redis.conf里面包含像这样的内容:

hash-max-zipmap-entries 256

也要记得根据你的键和值的最大大小设置的下面的字段:

hash-max-zipmap-value 1024

每次一个哈希超过指定的元素数量或元素尺寸,它将会转化为一个真正的哈希表,并且内存节省将会丢失。

你可能会问了,为什么你不在正常键空间隐式的做呢?这样我就不必担心了。有两个原因:其一是我们倾向于做一个显式的权衡,并且这是一个在很多事情之间清晰的权衡:CPU,内存,最大元素尺寸。其二是顶级的键空间必须支持大量有趣的事情,比如过期时间,LRU数据等等,因此使用一般的方式做这些是不太现实的。

但是Redis的做法是,用户必须理解事物如何运作,这样他能够挑选最好的这种方案,并且理解系统将如何精确的运转。

内存分配

为了保存用户的键,Redis分配了maxmemory设置中允许的尽可能多的内存(然而还有少量的额外分配可能性)。

精确的值可以在配置文件里面设置,或者稍后通过CONFIG SET设置(参见Using memory as an LRU cache for more info)。关于Redis如何管理内存还有一些事情需要注意。

  • 当键被移除时,Redis并不总是会释放(返回)内存给OS。这并不是Redis特有的,但这是大多数malloc()实现的工作方式。例如,如果你将5GB的数据填充到一个实例,然后移除2GB的数据,驻留集(也被称为RSS,进程消耗的内存页数)将可能仍然是5GB左右,甚至当Redis宣称用户内存是3GB左右。发生此事的原因是底层的分配器不能够轻易的释放内存。例如,通常当大部分被移除的键被分配在与其他仍然存在的键相同的页上。

  • 前一点意味着,你需要在峰值内存的基础上提供内存。如果你的工作负载时不时需要10GB内存,即使大部分时间5GB就可以了,你仍需要提供10GB内存。

  • 然而,分配器是聪明的并且能够复用空闲的内存碎片,因此在你释放5GB数据集中的2GB后,当你再次开始添加更多的键时,你将会看到RSS(驻留集)保持稳定,当你添加最多2GB额外的键时,不会增加更多。内存分配器基本上会尝试复用之前(逻辑上)释放的2GB内存。

  • 由于这些原因,当你内存使用量峰值远大于当前已使用的内存时碎片率是不可靠的。内存碎片是计算实际使用的物理内存(RSS值)除以当前在用的内存总量(Redis分配执行的总和)。因为RSS反应的是峰值内存,当(虚拟的)已使用内存低的时候,是因为大量的键/值被释放,但是RSS是高的,RSS/mem_used率将会非常高。

如果maxmemory没有设置,Redis将会一直分配内存直到发现合适,因此它可以(阶梯式)吃掉你所有的空闲内存。因此,通常建议配置一些限制。你可能也想设置maxmemory-policynoeviction(在一些老版本的Redis中,这是一个默认值)。

当Redis的写命令达到内存限制时,它会返回一个内存不足错误--从而会引起应用程序错误,但是不会因为内存耗尽而导致整个机器挂掉。

工作进展

工作进展...更多提示很快就会添加。

你可能感兴趣的:(内存优化)