Redis优化
内存优化
内存消耗分为:程序内存、对象内存、缓冲内存、内存碎片
1)对象内存占内存消耗中最多;
2)优化内存主要操作对象内存和内存碎片;
(1)程序内存:运行Redis进程所需内存;
1)空的Redis进程所消耗内存可忽略不计;
(2)对象内存:Redis中每个键值对所占用内存
1)键和值都分别占用内存,应避免过长的键或值;
2)键类型均为string,可忽略其在对象内存中消耗内存;
3)值需根据不同的类型,尽可能调整其底层数据结构的实现;
(3)缓冲内存:Redis中使用各缓冲占用内存
1)缓冲内存分为:客户端缓冲区、复制积压缓冲区、AOF缓冲区
2)输入/输出缓冲区在流量较大下容易失控(造成内存不稳定),需重点监控;
(4)内存碎片:操作Redis键值对使部分内存不可再用
1)Redis默认内存分配器:jemalloc
2)Redis支持的内存分配器:glibc
、tcmalloc
3)尽量避免频繁更新键和删除过期键,会增加内存碎片率(导致内存不可用)
4)可通过数据对齐(避免碎片)和重启Redis(回收碎片)将内存碎片率降低
回收
内存回收分为:删除过期键值对、内存溢出控制策略
1)Redis中所有设置过期时间的键均保存在字典中
2)主节点的内存回收操作会同步至从节点;
//频繁执行内存回收的成本较高(应maxmemory
> used_memory
时才执行)
(1)删除过期键值对: 通过删除过期键值对实现内存的回收
1)删除过期键值对分为:惰性删除、定时任务删除
2)惰性删除:当客户端读取过期键时,才删除该键并返回空
3)定时任务删除:Redis定时任务根据扫描所有键并根据比列删除
//惰性删除可有效节省CPU
定时任务删除过期键流程:
1)随机检查每个数据库中20个键值对,并删除过期键;
2)若过期键比列超过1/4,则继续检查(直至比列低于1/4或超时);
3)若因超时停止,Redis将执行快模式回收过期间(默认慢模式,25毫秒);
//快模式的超时时间为1
毫秒(2秒内能只可执行1次)
如:内存回收流程:
(2)内存溢出控制策略:当内存超过参数maxmemory指定值时,触发溢出策略
1)由参数maxmemory-policy
指定内存溢出控制策略;
2)常用内存控制策略如下:
内存控制策略 | 说明 |
---|---|
noeviction (默认) | 拒绝所有写入操作并返回错误信息至客户端 (只响应读操作) |
volatile-lru | 根据LRU算法删除设置过期时间的键,直到满足内存 (若无键可删除,则回退到noeviction策略) |
allkeys-lru | 根据LRU算法删除键,直到满足内存 (可删除任意键) |
allkeys-random | 随机删除所有键,直到满足内存 |
volatile-random | 随机删除过期键,直到满足内存 |
volatile-ttl | 根据字段TTL,删除将要过期的键 (若无键可删除,则回退到noeviction策略) |
优化
Redis中所有值的底层结构均为redisObject结构体
1)每个redisObject结构体至少占用16字节;
2)redisObject结构体中常用字段如下:
字段 | 含义 |
---|---|
type | 值的类型 (type命令本质) |
encoding | 值的底层数据结构 |
lru | 值最后一次被访问时间 |
refcout | 值被引用的次数 |
*ptr | 值是整数,代表值数据 值是其他类型,代表指向数据的执行 |
内存优化的两个方向:
(1)采用压缩数据结构作为值的底层实现(如:ziplist、quicklist和intset等)
1)Redis只支持压缩编码格式向非压缩编码格式转化(重启可实现逆转换);
2)当使用压缩编码格式时不建议频繁增删操作,非常消耗CPU;
(2)降低Redis的外层键数量(使用多层数据结构的类型存储类似数据)
1)建议使用哈希表实现(底层应为ziplist,否则可能消耗更多的内存);
2)ziplist长度需控制在1000
以内(读取长列表非常消耗CPU);
3)仅适用于存储小对象;
ziplist
ziplist:采用线性连续的内存结构存储数据(节省内存)
1)增删操作设计内存重新分配和释放(相关操作较多时,不建议使用该类型);
2)读写操作设计复杂的指针移动(最坏时间复杂度为O(n²))
3)长度和每个值大小建议不超过1000个和512字节;
ziplist结构体中常用字段如下:
字段 | 含义 |
---|---|
zlbytes | 记录整个压缩列表所占字节长度 (int32类型,4字节) |
zltail | 记录距离尾节点的偏移量 (int32类型,4字节) |
zllen | 记录压缩列表节点数量 (int16类型,2字节) |
entry | 记录具体节点信息 |
zlend | 记录列表结尾 (占用1字节) |
//可模拟双向链表结构(以O(1)时间复杂度出入队列)
entry存储具体节点信息如下:
信息 | 含义 |
---|---|
prev_entry_bytes_length | 前一个节点所占空间 (快速定位上一个节点,实现反向迭代) |
encoding | 标识当前节点编码和长度 (格式:字符串/整数) |
contents | 存储节点代表的值 |
intset
intset:存储有序且不重复的整数集(自动排序和去重)
1)以O(n)
时间复杂度实现值的插入
2)以O(log(n))
的时间复杂度实现值的排序、去重和查询
3)当存储数据超出原有内存时,intset会申请新内存空间并将拷贝数据;
intset结构体中常用字段如下:
字段 | 含义 |
---|---|
encoding | 整数数据的类型 (int16、int32和int64) |
length | 集合中值的个数 |
contents | 整数数组 (默认从小到大存储) |
缓存优化
缓存优化:实现Redis在应用中充当缓存层的实时性、高效性和高可用性
1)实际应用中将Redis做为底层存储层(数据库)的缓存层;
2)缓存优化以外还有缓存预热和缓存降级(较简单);
如:实际应用中Redis做为缓存层
缓存更新
缓存更新:保证缓存中的数据的实时性
1)缓存更新分为:LRU/LFU/FIFO算法剔除、超时剔除、主动更新
2)通过数据实时性和维护成本选择缓存更新策略;
(1)LRU/LFU/FIFO算法剔除:根据算法策略删除特定键
1)适用场景:Redis使用内存易超出预分配值;
2)实时性:最差(无法人工干预键的保留/删除);
3)维护成本:低(配置参数即可)
(2)超时剔除:根据过期时间,定时定量删除键
1)适用场景:能够容忍一定时间内的数据不一致
2)实时性:差(缓存数据与真实数据存在窗口期)
3)维护成本:中(创建键时指定过期时间)
(3)主动更新:通过开发程序监控真实数据进行实时更新
1)适用场景:应用需数据
2)实时性:强(真实数据更新后立即更新缓存数据)
3)维护成本:高(需通过开发程序)
缓存更新 | 实时性 | 维护成本 |
---|---|---|
LRU/LFU/FIFO算法剔除 | 最差 | 低 |
超时剔除 | 差 | 中 |
主动更新 | 强 | 高 |
//实际应用中建议结合使用超时剔除和主动更新(提高容灾性)
缓存穿透
缓存穿透:数据请求在缓存层和存储层中均未命中,请求均转到存储层
1)缓存穿透导致后端存储层负载过大(可能宕机);
2)默认存储层查不到数据返回空时,不会在缓存层进行备份;
优化方案:
1)当存储层返回空时,缓存层进行备份(并设置较短的过期时间);
2)采用布隆过滤器,将过滤器认为不存在的数据请求全部拒绝;
布隆过滤器(Bloom Filter):将所有数据的hash_tag存储在Bitmap中用于过滤
1)布隆过滤器需部署在缓存层前;
2)Bitmap中值只能记录1bit信息;
优化方案 | 使用场景 | 维护成本 |
---|---|---|
缓存空对象 | 1)数据命中率低 2)数据实时性高 | 1)代码维护简单 2)需较多的缓存空间 |
布隆过滤器 | 1)数据命中率低 2)数据实时性低 | 1)代码维护复杂 2)占用较少的缓存空间 |
缓存雪崩
缓存雪崩(Stampeding Herd):缓存层宕机后,数据请求均转到存储层
1)缓存雪崩导致后端存储层负载过大(可能宕机);
优化方案:
1)使用多节点保证存储层的高可用性(哨兵和集群);
2)隔离每个应用组件使之互不影响(容器或线程池实现);
3)缓存数据的过期时间分散设计,防止同一时间大量数据过期;
缓存击穿/热点Key
缓存击穿/热点Key:特定键在一段时间内被多个客户端频繁请求
1)热点Key在缓存失效时,会有大量线程请求存储层以重建缓存;
2)缓存击穿:请求数据仅在存储层中,大量线程请求存储层以重建缓存;
优化方案:
1)互斥锁:限定仅由获得锁的线程可重建缓存;
2)热点Key永不过期:使之永久存储于缓存层中(单独线程更新);
//互斥锁中其他线程会一直尝试获取锁,直到获取数据为止
优化方案 | 优点 | 缺点 |
---|---|---|
互斥锁 | 1)实现简单; 2)实时性高; | 1)增加代码复杂度; 2)存在程序死锁风险; 3)存在线程池阻塞风险; |
永不过期 | 1)完全杜绝热点Key | 1)实时性低; 2)占用较多内存; |
配置优化
配置优化:通过配置Linux系统提升Redis性能
1)Redis是CPU密集型服务,不建议和其他多核CPU密集型服务部署在一起
2)若Redis开启持久化或参数赋值,则不建议绑定CPU(子进程消耗过大)
3)建议所有运行Redis节点的Linux运行NTP服务(确保时间一致性)
4)关闭swap或降低其优先级(同时保证系统有20~30%闲置内存)
//不和其他CPU密集型服务部署在一起是为避免CPU过度竞争
内存分配
(1)使Redis进程可获得足够内存运行
1)overcommit技术:Linux对大部分内存请求均允许,但不立刻分配
2)通过设置“vm.overcommit_memory
”保证fork等操作的顺利执行
3)overcommit_memory不同值的含义如下:
值 | 含义 |
---|---|
0 | 先检查是否有足够可用内存再分配 |
1 | 允许超量使用内存(直到用完为止) |
2 | 使用内存不能超过swap + overcommit_ratio |
优化流程:
echo "vm.overcommit_memory=1" \>\> /etc/sysctl.conf
sysctl vm.overcommit_memory=1
(2)使Redis不轻易使用swap
1)swappiness技术:根据参数值指定各进程使用swap的概率
2)通过设置“/proc/sys/vm/swappiness
”使Redis的swap优先级最低;
3)swappiness特殊值的含义如下(范围0~100):
值 | 含义 |
---|---|
0 | 宁愿被OOM,也不用swap |
1 | 宁愿用swap也不被OOM |
60 | 默认值 |
100 | 优先使用swap |
优化流程:
echo 0 \> /proc/sys/vm/swappiness
echo vm.swappiness=0 \>\> /etc/sysctl.conf
(3)避免创建子进程时过度使用内存
1)Transparent Huge Pages(THP):创建进程时分配大内存页(2MB)
2)写时复制期间赋值内存页单位从4KB变为2MB(导致大量写操作慢查询)
3)开启THP会增快fork子进程的速度,但会导致过度内存消耗
优化流程(关闭THP):
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo "echo never > /sys/kernel/mm/transparent_hugepage/enabled" >> /etc/rc.local
(4)避免Redis进程被关闭
1)OOM Killer:当内存不足时,系统选择性关闭用户进程
2)通过配置“/proc/进程ID/oom_adj”使Redis进程被关闭的优先级最低;
//其值范围为“-17 ~ -15”
优化流程(Shell脚本):
#!/bin/bash
for redis_pid in \$(pgrep -f "redis-server")
do
echo -17 \> /proc/%{redis_pid}/oom_adj
done
连接限制
(1)配置最多可连接用户数(确保客户端正常连接)
1)文件句柄:Linux默认限制进程最多可持有1024个文件句柄
2)客户端连接后需占有一个文件句柄,且服务端默认占有32个文件句柄
3)通过“ulimit”实现最多连接用户数和参数maxclients一致;
//若限定持有文件句柄和参数maxclients不一致时,以前者为准
优化流程:
ulimit -Sn 10032
(2)保证TCP正常连接
1)TCP backlog:Linux通过 backlog队列存储端口的TCP连接(默认值128)
2)Redis的backlog队列默认长度为511(两者不一致,以系统为准);
3)通过配置“/proc/sys/net/core/somaxconn”保证可连接数;
优化流程:
echo 511 \> /proc/sys/net/core/somaxconn