本文将从防止阻塞和内存节约两个方面介绍如和高效使用Reids。
使用Redis时,我们需要结合具体业务和Redis特性两方面来考虑如何设计使用方案。需要两个从两个方面考虑:
-
防止阻塞 -
节约内存
下面,我们将就上面两个点展开说明如何高效合理使用Redis。
防止阻塞
从阻塞章节我们知道,引起Redis阻塞可能的原因有内因和外因两方面。
内因规避
减少复杂命令的使用,或者有节制的使用。下面这些命令可以看做复杂命令(时间复杂度为O(N)或者更高):SETRANGE, GETRANGE, MSET, MGET, MSETNX, HMSET, HMGET, HKEYS,HVALS, HGETALL, HSCAN, LTRIM, LINDEX, SMEMBERS, SUNION, SUNIONSTORE, SDIFF, SDIFFSTORE, ZUNIONSTORE, ZINTERSTORE, SINTER, SINTERSTORE
。这些命令当操作的key
或者field
过多时将会导致Redis进程阻塞。举例来说,对一个包含上十万甚至百万个field
的hash
执行hgetall
操作,hgetall
命令的时间复杂度为O(N),此时N页特别大(上十万甚至百万)必然耗时很长。从这个例子,我们可以发现至少两个不合理的地方:
-
这种有大量元素的数据不应该存在,因为,我们并不能确定什么时候我们对它执行了复杂命令。 -
如果真的不可规避超多元素的情况,在获取多个元素或者全量元素时,务必使用 scan
之类命令,且确保每次获取元素数量在一定范围,比如50等。
避免频繁生成RDB和AOF重写,尤其是高峰期。正常情况下,Redis比较时候缓存类型数据,当然为了保证数据不丢失,可以进行导出RDB和重新AOF。但需要确保一下几点:
-
不要执行 save
等同步命令; -
尽量不要在高峰期进行持久化操作; -
尽量在从实例上做持久化操作;
如果必须频繁持久化,需要确保如下几点:
-
保证CPU、内存充足,建议CPU和内存留出一定的buffer -
不要绑定CPU -
避免和CPU密集型服务混布 -
如果多个Redis实例部署在同一台机器,注意规划好系统资源,可以考虑错峰持久化,避免同时持久化导致系统资源开销瞬间突增 -
系统尽量不要开启 HugePage
,防止复制内存页过大而拖慢执行时间,且会导致持久化期间内存消耗增长
避免单Redis实例负载过高。Redis是单线程服务,当负载过大必然影响整体性能,可以通过如下方案提高读写能力:
-
可以通过读写分离,从实例承接部分读请求,来降低主实例压力; -
如果读写压力都很大的话,需要考虑集群方案。
外因规避
通常,引起服务的外因无外乎CPU、内存和网络,导致Redis阻塞的原因同样也需要从这几方面去考虑。CPU竞争导致Redis阻塞的问题原因在阻塞章节已经详细介绍过,关于解决方案,可以通过以下手段来规避:
-
进程CPU资源竞争,建议不要和其他多线程CPU密集型服务混布,尤其是线上环境。另外,如果流量趋势有波动的服务,比如有早晚高峰,建议不要把流量波动一致的服务混布。 -
绑定CPU,绑定CPU(设置CPU亲和力affinity)是为了降低Redis进程在不同CPU来回切换导致缓存命中率下降等引起的性能问题,但是,进程的CPU亲和力会继承给子进程,Redis进程 fork
出的子进程也共享该CPU。因此,如果需要频繁持久化的Redis不建议绑定CPU。
节约内存
系统优化
减少内存碎片,正常的碎片率(mem_fragmentation_ratio)在1.03左右。但是当存储的数据长短差异较大时,以下场景容易出现高内存碎片问题:
-
频繁做更新操作,例如频繁对已存在的键执行 append
、setrange
等更新操作。 -
大量过期键删除,键对象过期删除后,释放的空间无法得到充分利用,导致碎片率上升。
出现高内存碎片问题时常见的解决方式如下:
-
数据对齐:在条件允许的情况下尽量做数据对齐,比如数据尽量采用数字类型或者固定长度字符串等,但是这要视具体的业务而定,有些场景无法做到。 -
安全重启:重启节点可以做到内存碎片重新整理,因此可以利用高可用架构,如Sentinel或Cluster,将碎片率过高的主节点转换为从节点,进行安全重启。
RDB生成和AOF重写会fork
子进程,进而导致内存消耗。总结如下:
-
正常情况下Redis产生的子进程并不需要消耗1倍的父进程内存,实际消耗根据期间写入命令量决定,但是依然要预留出一些内存防止溢出。 -
需要设置sysctl vm.overcommit_memory=1允许内核可以分配所有的物理内存,防止Redis进程执行fork时因系统剩余内存不足而失败。 -
排查当前系统是否支持并开启THP,如果开启建议关闭,防止copy-onwrite期间内存过度消耗。
用户优化
减小键值字符串长度
-
key可以通过字符串缩减来减少长度 -
value可以通过序列化和压缩来减少存储,也可以可以通过业务侧优化减少不必要的字段
尽量使用set
而非append
因为字符串(SDS)存在预分配机制,日常开发中要小心预分配带来的内存浪费。
表-2 set & append 对比测试
从上面的实验可以看出,同样存储100w条key大小为20B,value大小为200B的数据,通过set
和append
操作实现的和直接使用set
实现多了近75% 的存储消耗。
字符串重构
字符串重构:指不一定把每份数据作为字符串整体存储,像json这样的数据可以使用hash结构,这样做有如下收益:
-
使用二级结构存储也能帮我们节省内存。 -
同时可以使用hmget、hmset命令支持字段的部分读取修改,而不用每次整体存取。
注意,这样样做的一个前提是json key-value
对中value相对较小,下面是一个测试例子。
{
"id" : "12345678",
"title" : "redis-memory-optimization",
"chinese_url" : "http://www.redis.cn/topics/memory-optimization.html",
"english_url" : "https://redis.io/topics/memory-optimization"
}
代码-2 一个json实例
表-3 hash优化测试
根据测试结构,hash-max-ziplist-value 50
配置下使用hash类型,内存消耗不但没有降低反而比字符串存储多出2倍,而调整hash-max-ziplist-value 64
之后内存降低为252.95M。因为json的chinese_url
属性长度是51,调整配置后hash类型内部编码方式变为ziplist,相比字符串在内存使用上至少持平且支持属性的部分操作。intset编码:intset编码是集合(set)类型编码的一种,内部表现为存储有序、不重复的整数集。当集合只包含整数且长度不超过set-max-intset-entries配置时被启用。
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
代码-3 intset结构
encoding:整数表示类型,根据集合内最长整数值确定类型,整数类型划分为三种:int-16、int-32、int-64。intset保存的整数类型根据长度划分,当保存的整数超出当前类型时,将会触发自动升级操作且升级后不再做回退。升级操作将会导致重新申请内存空间,把原有数据按转换类型后拷贝到新数组。使用intset编码的集合时,尽量保持整数范围一致,如都在int-16范围内。防止个别大整数触发集合升级操作,产生内存浪费。
控制键的数量
通过在客户端预估键规模,把大量键分组映射到多个hash结构中降低键的数量。简单的说就是复用key前缀。
总结
内存是相对宝贵的资源,通过合理的优化可以有效地降低内存的使用量,内存优化的思路包括:
-
精简键值对大小,键值字面量精简,使用高效二进制序列化工具。 -
使用对象共享池优化小整数对象。 -
数据优先使用整数,比字符串类型更节省空间。 -
优化字符串使用,避免预分配造成的内存浪费。 -
使用ziplist压缩编码优化hash、list等结构,注重效率和空间的平衡。 -
使用intset编码优化整数集合。 -
使用ziplist编码的hash结构降低小对象链规模。
reference
Redis官网
Redis开发与运维
How Twitter Uses Redis To Scale – 105TB RAM, 39MM QPS, 10,000+ Instances
Latency Numbers Every Programmer Should Know
原文始发于微信公众号(闲余说):Redis系列之如何高效使用
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/71132.html