本文将从防止阻塞和内存节约两个方面介绍如和高效使用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 对比测试
操作 | 数据量 | key大小 | value大小 | used_memory_human | used_memory_rss_human | mem_fragmentation_ratio | 说明 |
set | 100w | 20B | 100B | 176.66M | 180.19M | 1.02 | – |
set | 100w | 20B | 200B | 283.47M | 287.66M | 1.01 | |
set && append | 100w | 20B | 100B+100B | 497.10M | 503.19M | 1.01 | 先set,value大小为100B,随后append大小100B的数据 |
从上面的实验可以看出,同样存储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优化测试
数据量 | 数据结构 | 编码 | key | value | 配置 | used_memory_human | used_memory_rss_human | mem_fragmentation_ratio | 说明 |
100w | string | raw | 20B | json字符串 | 默认 | 252.95M | 258.04M | 1.02 | |
100w | hash | hashtbale | 20B | key-value | hash-max-ziplist-value 50 | 474.21M | 484.27M | 1.02 | |
100w | hash | ziplist | 20B | key-value | hash-max-ziplist-value 64 | 252.95M | 258.09M | 1.02 |
根据测试结构,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
How Twitter Uses Redis To Scale - 105TB RAM, 39MM QPS, 10,000+ Instances
Latency Numbers Every Programmer Should Know