redis内存优化
Redis和客户端通常受IO约束,并且相对于请求/应答序列的其余部分,IO成本通常至少为2个数量级。默认情况下,Redis不会压缩存储在其中的任何值,因此在存储到Redis中之前压缩数据变得非常重要。这有助于减少有效负载,从而为你带来更高的吞吐量、更低的延迟和更高的成本节省。
有多种压缩算法可供选择,每种算法都有其自身的权衡。
GZIP压缩比Snappy或LZO使用更多的CPU资源,但提供了更高的压缩比。对应不经常访问的冷数据,GZip通常是一个不错选择。对于经常访问的热数据,Snappy或LZO是更好的选择。
压缩字符串需要更改代码。有些库可以透明地压缩对象,你只需要配置库即可。在其它情况下,你可能必须手动压缩数据。
压缩字符串可以节省30-50%的内存。通过压缩字符串,还可以减少应用程序和Redis数据库之间的网络带宽。
压缩/解压缩需要你的应用程序执行额外的工作。这种权衡通常是值得的。如果你担心额外的CPU负载,请切换到一个更快的算法,例如snappy或LZO。
压缩不应盲目地遵循,有时压缩不能帮助你减少内存,反而会提高CPU利用率。在少数情况下应避免压缩:
Redis键在增加Redis实例的内存消耗方面起到了魔鬼般的作用。通常,你应该始终偏爱描述性键,但是如果你拥有一个包含数百万个键的大型数据集,那么这些大键可能会花费你的大量金钱。
在编写良好的应用程序中,切换到较短的键通常涉及更新应用程序代码中的几个常量字符串。
你必须识别Redis实例中的所有大键,并通过删除其中的额外字符来缩短它。你可以通过两种方式实现:
redis-cli --bigkeys
使用RedisInsight的优势在于,它为你提供了整个数据集中的大键,而bigkeys
命令运行在特定的记录集上,并从该记录集中返回大键,因此,使用bigkeys
很难从整个数据集中识别大键。
举个例子:假设你有100,000,000个键名称,例如
my-descriptive-large-keyname (28个字符)
现在,如果你将键名称缩短为
my-des-lg-kn (12个字符)
通过缩短键来节省16个字符,即16个字节,这可以节省1,000,000,000*16 = 1.6GB的RAM内存!
大键比短键更具描述性,因此,在读取数据库时,你可能会发现键的相关性较差,但与这种痛苦相比,节省内存和成本更高效。
Redis为你提供了有关64位计算机的以下统计信息。
与32位计算机相比,64位有更多可用内存。但是,如果你确定数据大小不超过3 GB,那么存储在32位是一个不错的选择。
64位系统比32位系统使用更多的内存来存储相同的键,尤其是在键和值较小的情况下。这是因为为小键分配了完整的64位,导致浪费了未使用的位。
从64位计算机切换到32位可以大大降低所用计算机的成本,并可以优化内存的使用。
对于32位Redis变体,任何大于32位的键名都要求该键跨越多个字节,从而增加了内存使用量。
如果你的数据大小预计会增加超过3 GB,则应避免切换。
Redis 4.0是已发包的最新版本。与以前的版本相比,它包含各种重大改进。
RDB+AOF
混合格式。Redis 4.0仍然不是一个稳定的发行版,而是经过严格测试的发行版,因此Redis 3.2是关键应用程序的更好选择,直到Redis 4.0在接下来的几个月中更加成熟为止。
Redis没有任何特定的数据类型来存储序列化对象,它们作为字节数组存储在Redis中。如果我们使用常规的方法来序列化java、python和PHP对象,则它们的大小可能会更大,从而影响内存消耗和延迟。
代替你的编程语言的默认序列化程序(java序列化对象、python pickle、PHP序列化等),切换到更好的库。有各种库,例如——协议缓冲区Protocol Buffers,消息包MessagePack等。
MessagePack是一种有效的二进制序列化格式。它使你可以在多种语言(如JSON)之间交换数据。但是它更快,更小。小整数被编码为一个字节,典型的短字符串除字符串本身外仅需要一个额外的字节。
正如Redis的创建者Salvatore Sanfilippo所说:
Redis scripting has support for MessagePack because it is a fast and compact serialization format with a simple to implement specification. I liked it so much that I implemented a MessagePack C extension for Lua just to include it into Redis.
Redis脚本支持MessagePack,因为它是一种快速紧凑的序列化格式,具有易于实现的规范。我非常喜欢它,所以我为Lua实现了一个MessagePack C扩展,只是将其包含在Redis中。
Protocol buffers(通常称为Protobuf)是Google开发的一种协议,用于允许对结构化数据进行序列化和反序列化。Google开发它的目的是提供一种比XML更好的方式来使系统进行通信。因此,他们专注于使其比XML更简单、更小、更快和更可维护。
在64位计算机上,字符串数据类型的开销约为90字节。换句话说,调用set foo bar
使用大约96个字节,其中90个字节是开销。只有在以下情况下才应使用String数据类型:
如果你没有执行上述任何操作,请使用哈希。
假设我们必须在用户的帖子上存储评论数,我们可以有一个键名,例如user:{userId}:post:{postId}:comments
。
这样,我们每个用户的每个帖子都有一个键。所以现在,如果我们需要查找整个应用程序的评论总数,我们可以做
Redis::mget("user:{
$userId}:post:1", "user:{
$userId}:post:2", ...);
要将其转换为哈希,你可以执行以下操作:
Redis::hmset("user:{
$userId}:comments", "post:1", 20, "post:2", 50);
这将构建一个Redis哈希,其中包含两个字段post:1
和post:2
,其值分别为20和50。
将小字符串组合为哈希可以减少使用的内存,从而节省成本。
哈希可以在一个非常小的内存空间中有效地编码,因此Redis制作者建议我们尽可能使用哈希,因为“几个键比一个包含几个字段的哈希的单个键使用更多的内存”,一个键表示一个Redis对象包含的信息比它的值要多的多,另一方面,一个哈希字段只包含指定的值,这就是为什么它更高效的原因。
性能是有代价的。通过将字符串转换为哈希,我们节省了内存,因为它只保存字符串值,而没有额外的信息,例如:空闲时间idle time
、过期时间expiration
、对象引用计数object reference count
以及与之相关的编码encoding
。但是,如果我们希望键具有过期值,则不能将其与哈希结构相关联,因为过期不可用。
这个决定取决于字符串的数量,如果少于100万且内存消耗不高,则转换不会受到很大影响,并且没有必要增加代码的复杂性。
但是,如果字符串超过100万,并且内存消耗很高,那么肯定应该遵循这种方法。
仅包含整数的Set集合在内存方面非常节省。如果你的Set集合包含字符串,请尝试通过将字符串标识符映射到整数来使用整数。
你可以在编程语言中使用枚举,也可以使用redis哈希数据结构将值映射为整数。切换为整数后,Redis将在内部使用IntSet编码。
这种编码非常节省内存。默认情况下,set-max-intset-entries
的值为512,但是你可以在redis.conf中设置此值。
通过增加set-max-intset-entries
的值,Set集合操作的延迟会增加,并且Redis服务器上的CPU利用率也会增加。你可以在进行此更改之前和之后运行此命令来进行检查。
运行 `info commandstats`
唯一项可能难以计数。通常,这意味着存储每个唯一项,然后以某种方式调用这些信息。使用Redis,可以通过使用set和单个命令来完成此操作,但是对于非常大的set,存储和时间复杂性都是令人望而却步的。HyperLogLog提供了一种概率替代方法。
如果你的set集合包含大量元素,并且只使用集合进行存在性检查或消除重复项,那么使用布隆过滤器bloom filter将使你受益。
Bloom过滤器本身不受支持,但是你可以在redis之上找到几种解决方案。如果你只使用该set集合来计算唯一元素的数量(例如,唯一IP地址、用户访问的唯一页面等),那么切换到hyperloglog可以节省大量内存。
以下是使用HyperLogLog的权衡:
例如,如果你想维护今天有多少个唯一的ipaddress进行了一次API调用。HyperLogLog告诉你今天有46966个唯一IPs
。
但是,如果你想显示这些46966个IP地址
——它就不能显示。为此,你需要在一个set集合中维护所有IP地址。
如果哈希中包含大量键值对,并且每个键值对都足够小,则将其分解为较小的哈希以节省内存。要切分HASH表,我们需要选择一种对数据进行分区的方法。
哈希本身有键,可用于将键划分为不同的分片。分片的数量取决于我们要存储的键总数和分片的大小。使用分片数量和哈希值,我们可以确定键所在的分片ID。
数字键——对于数字键,将根据键的数字键值将键分配给分片ID(在同一分片中保持数字相似的键)。
非数字键——对于非数字键,使用CRC32校验和。在这种情况下,使用CRC32是因为它返回一个简单的整数而无需进行额外工作,并且计算速度很快(比MD5或SHA1哈希算法快得多)。
在进行分片时,你应该对预期元素的总数
和分片大小
保持一致,因为这两个信息是减低分片数量说必需的。理想情况下,你不应该更改这些值,因为这会更改分片的数量。
如果你要更改任何一个值,则应该有一个将数据从旧数据分片移动到新数据分片的过程(通常称为重新分片resharding)。
将大哈希转换为小哈希的唯一权衡是,这会增加代码的复杂性。
哈希有两种编码类型:HashTable
和Ziplist
。根据Redis提供的两种配置——hash-max-ziplist-entries
和hash-max-ziplist-values
,完成存储哪种数据结构的决定。
默认情况下,redis.conf具有以下设置:
因此,如果一个键的任何值超过这两个配置,它将自动存储为Hashtable
而不是Ziplist
。可以观察到,与Ziplist
相比,HashTable
消耗几乎两倍的的内存,因此为了节省内存,你可以增加这两个配置并将哈希表转换为ziplist。
Redis中的ziplist实现通过每个条目只存储三段数据来实现其较小的内存大小; 第一段是前一个条目的长度,第二段是当前条目的长度,第三段是存储的数据。因此,ziplist消耗更少的内存。
简洁是有代价的,因为更改大小和检索条目需要更多时间。因此,redis服务器上的延迟增加了,CPU使用率也可能增加了。
注意: 同样,对于sorted set,也可以将其转换为ziplist,但是唯一的区别是zset-max-ziplist-entries
是128,这比哈希的条目少。
Redis哈希存储字段名和值。如果你有成千上万个具有相似字段名的小型哈希对象,则字段名使用的内存将累加起来。为避免这种情况,请考虑使用List而不是Hash。字段名成为List列表的索引。
虽然这样可以节省内存,但是仅当你具有成千上万个哈希并且每个哈希有相似的字段时,才应使用此方法。压缩字段名是减少字段名使用的内存的另一种方法。
举个例子。假设你要在Redis中设置用户详细信息。你像这样做:
hmset user:123 id 123 firstname Bob lastname Lee location CA twitter bob_lee
现在,Redis 2.6将其内部存储为Zip List;你可以通过运行调试对象user:123
并查看encoding编码字段进行确认。在这种编码中,键值对是按顺序存储的,因此我们在上面创建的用户对象将大致如下所示:["firstname", "Bob", "lastname", "Lee", "location", "CA", "twitter", "bob_lee"]
现在,如果你创建第二个用户,则键将被复制。如果你有一百万个用户,那么,再次重复这些键是很大的浪费。为了解决这个问题,我们可以从Python——NamedTuples中借用一个概念。
NamedTuple只是一个只读列表,但是它有一些魔术使该列表看起来像字典。你的应用程序需要维护从字段名到索引的映射,例如"firstname" => 0, "lastname" => 1
,依此类推。
然后,你只需创建一个List列表而不是一个Hash哈希,例如lpush user:123 Bob Lee CA bob_lee
。在应用程序中使用正确的抽象,可以节省大量内存。
唯一的权衡与代码复杂性有关。对于小哈希和小列表,Redis内部使用相同的编码(ziplist),因此切换到列表时不会对性能造成影响。 但是,如果你的哈希中的字段超过512个,则不建议使用此方法。
以下是应避免将哈希转换为列表的情况:
Redis哈希由字段及其值组成。与值一样,字段名也消耗内存,因此在分配字段名时需要牢记。如果你有大量具有相似字段名的哈希,则内存将显着增加。为了减少内存使用,可以使用较小的字段名称。
参考前面的将哈希转换为列表的示例,我们有一个包含用户详细信息的哈希。
hmset user:123 id 123 firstname Bob lastname Lee location CA twitter bob_lee
在这种情况下,firstname, lastname, location, twitter
是全字段名,可以缩写为:fn, ln, loc, twi
。这样做可以节省字段名使用的一些内存。
不要生成动态脚本,这会导致Lua缓存增长并失去控制。加载脚本会消耗内存。内存消耗是由于以下因素造成的。
List只是数组的链接列表,其中没有一个数组被压缩。默认情况下,redis不会压缩列表中的元素。但是,如果使用长列表,并且大多数情况下只从头和尾访问元素,则可以启用压缩。
我们有两种配置:list-max-ziplist-size
:-2(8Kb大小,默认);list-compression-depth
:0,1,2(默认为0)
redis.conf
文件list-compression-depth
=1中的配置更改可帮助你实现压缩。
压缩深度是在开始压缩内部节点之前,列表两端保持不变的列表节点的数量。
例:
对于较小的值(例如,每个列表条目为40个字节),压缩对性能的影响很小。当使用最大ziplist大小为8k的40个字节值时,每个ziplist大约包含200个单独的元素。创建新的ziplist时,你只需支付额外的“压缩开销”费用(在这种情况下,每200个插入一次)。
对于较大的值(例如,每个列表条目为1024字节),压缩确实会对性能产生明显影响,但是对于ziplist大小的所有良好值(-2),Redis仍以每秒150,000次操作以上的速度运行。当使用1024字节值且最大ziplist大小为8k时,每个ziplist最多可以包含7个元素。在这种情况下,每7个插入一次,将支付额外的压缩开销。这就是为什么在1024字节元素的情况下性能略有下降的原因。
当你在一个键上设置了过期时间时,redis不会在该瞬间使它过期。相反,它使用随机算法来找出应该过期的键。由于此算法是随机的,因此键可能没有过期。这意味着redis消耗内存来保存已经过期的键。一旦访问键,就将其删除。
如果只有几个键已过期并且Redis还没有删除它们——它是好的。只有当大量键还没有过期时,它是一个问题。
INFO
命令,找到所有数据库的total_memory_used以及所有键的总和。查看不同之处,你可以清楚地指出,已过期的键仍然没有回收大量内存。
你可以按照以下三个步骤之一来回收内存:
如果我们增加maxmemory-samples配置,它将使键更快地过期,但是会花费更多的CPU周期,从而增加了命令的延迟。其次,增加键的过期时间有所帮助,但这需要对应用程序逻辑进行重大更改。
本文参考:
memory-optimizations