引语
随着使用Redis的深入,我们不可避免的需要深入了解优化Redis的内存,本章将重点讲解Redis的内存优化之道,同时推荐大家阅读memory-optimization一文。
想要高效的使用Redis,就需要充分理解计算机的内存技术及网络和硬盘延迟,以便追踪性能瓶颈、处理资源规划及分配。
配置优化
在Redis.io网站上的官方文档内存优化中,其中一条建议是在32位模式下编译Redis替代64位实例。
对于同样小于3GB的数据集,32位的比64位版本的要小,因为32位实例的指针大小只有64位版本的一半,它的内存空间占用要少些,但假若内存超过了4GB,但32位实例还是被限制在4GB一下,若使用不当很有可能造成Redis崩溃,数据丢失等情况。如果想要高效的运行Redis,我们首先需要了解的就是配置文件redis.conf中的指令,redis.conf中的大多数配置都做了详细说明,与内存相关的优化配置如下:
# 从RDB版本5开始,一个CRC64的校验文件就被放在了文件末尾。
# 这能够保证文件格式的完整性,但是当存储或加载rdb文件时会有约10%左右的性能下降
# 所以,为了保证性能最大化,可以关掉这个配置项。
rdbchecksum yes
# Redis 每100ms会使用1ms的cpu时间来对redis的hash进行rehash,这样可以降低内存的使用。
# 如果你的使用场景有严格实时性需要,不能接受redis有任何延迟的话,把这项配置为no。
activerehashing yes
首先讨论一下rdbchecksum,rdb是redis的持久化方式的一种(另外一种叫aof),在对于数据安全性要求不高时,可以将其设置为no(顺便普及一下redis备份的相关知识:传送门);activerehashing用于配置是否激活重置hash,可以通过配置此项来释放内存。
使用Hash
官网中推荐尽可能的使用Hash,因为Redis存储小于100个字段的Hash结构上,其存储效率非常高。
使用Key过期
Key过期算是最有效的一种手段之一了,并不是所有的Key都需要一直存在于内存中,当Key不大可能再被使用或长时间内不再使用,我们可以使用过期策略来让Redis自动删除它。
配置回收策略
当内存超出限制时,我们可能希望Redis自动进行GC,当我们配置了内存容量时,通过redis.conf中的
maxmemory 100mb
来实现,设置为0时表示没有内存限制,64位默认为0,32位则默认为3G,当达到阈值时,将触发回收策略:
# noveicition:当内存达到限制时返回错误;
# allkeys-lru:回收最近最少使用的键;
# volatile-lru:只回收有设置过期时间的最近最少使用的键;
# allkeys-random:回收随机键;
# volatile-random:随机回收设置过期时间的键;
# volatile-ttl:回收设置过期时间的键,优先回收离TLL时间最短的键。
maxmemory-policy noeviction
LRU是我们常见的淘汰算法,每次淘汰最近最少使用的元素,发散一下,贴一段LRU的简单实现:
/// <summary>
/// LRU 缓存。
/// </summary>
public class LRUCache
{
int _curSize = 0; //当前缓存大小
int _limit = 5; //元素上限
LRUNode _headNode; //数据头
Dictionary<string, LRUNode> _nodeDic; //缓存链表
public LRUCache()
{
_headNode = new LRUNode("", "");
_headNode.Prev = _headNode;
_headNode.Next = _headNode;
_nodeDic = new Dictionary<string, LRUNode>();
}
public string Get(string key)
{
if (_nodeDic.ContainsKey(key))
{
var curNode = _nodeDic[key];
MoveToHead(curNode); //更新节点的使用频率
return curNode.Val;
}
return "";
}
public void Set(string key, string val)
{
LRUNode curNode;
if (_nodeDic.ContainsKey(key))
{
curNode = _nodeDic[key];
MoveToHead(curNode);
}
else
{
curNode = new LRUNode(key, val);
AddToHead(curNode);
_curSize++;
if (_curSize > _limit)
{
RemoveLast(curNode);
_curSize--;
}
_nodeDic.Add(key, curNode);
}
}
public int GetTotalSize()
{
return _nodeDic.Count;
}
//新增节点
void AddToHead(LRUNode node)
{
node.Prev = _headNode;
node.Next = _headNode.Next;
_headNode.Next.Prev = node;
_headNode.Next = node;
}
//将节点移除
void RemoveFromList(LRUNode node)
{
node.Prev.Next = node.Next;
if (node.Next != null)
node.Next.Prev = node.Prev;
}
//移除最后一个
void RemoveLast(LRUNode node)
{
var deNode = _headNode.Prev;
RemoveFromList(deNode);
_nodeDic.Remove(deNode.Key);
}
//移动到表头
void MoveToHead(LRUNode node)
{
RemoveFromList(node);
AddToHead(node);
}
}
/// <summary>
/// LRU 链表。
/// </summary>
public class LRUNode
{
public LRUNode Prev; //前节点
public LRUNode Next; //后节点
public string Key;
public string Val;
public LRUNode(string key, string val)
{
Key = key;
Val = val;
}
}
然后我们运行:
static void Main(string[] args)
{
LRUCache _chache = new LRUCache();
for (int i = 0; i < 20; i++)
{
_chache.Get("13");
_chache.Set(i.ToString(), i.ToString());
}
for (int i = 0; i < 20; i++)
{
Console.WriteLine(_chache.Get(i.ToString()));
}
Console.ReadLine();
}
猜猜结果是什么?循环输出的是“13、16、17、18、19”,因为13一直在被使用,所以不会被删除掉,反而去删除了15。
硬件和网络延迟
在应用程序中,性能问题很容易被误认为是Redis数据库内存不足造成的。其实该问题很有可能是有关客户端应用程序和后端服务器之间硬件和网络延迟的问题。
当我们遇见Redis延迟时,一般可以从下面三个方向去查证:
- 命令延迟:前文中我们提到过不同命令的时间复杂度不同,时间复杂度为O(1)的执行起来就非常快,而为O(n)的就可能因为数据量比较大而延迟;
- 往返延迟:由于网络拥塞导致的命令收发延迟;
- 客户端延迟:例如多个客户端同时尝试连接到Redis而客户端连接数达到限制需要等待导致的并发延迟。
那么若是遇见了这些问题,该如何定位呢?接下来我将介绍一些性能问题排查是经常用到的命令:
info
info
info会输出和Redis相关的基本信息,如版本号、内存使用、CPU、集群等,大多数情况我们比较关心的是memory,所以你可以使用:
info memory
来简化输出,memory中的内容如下:
used_memory:812168 # Redis分配器分配的内存总量,以字节为单位
used_memory_human:793.13K # 以Kb为单位,为了方便阅读
used_memory_rss:7708672 # 操作系统上显示已分配的内存总量
used_memory_peak:813008 # Redis消耗内存的峰值
used_memory_peak_human:793.95K # 友好的显示
used_memory_lua:36864 # Lua引擎使用的内存大小
mem_fragmentation_ratio:9.49 # 内存碎片率
mem_allocator:jemalloc-3.6.0 # 在编译时Redis使用的内存分配器
理想情况下used_memory_rss的值应该只比used_memory稍微高一点儿,但是两者值相差较大是,表示存在内存碎片,可以通过mem_fragmentation_ratio看出来,内存大于1是合理的,但超过1.5则表示有较多的内存碎片,或许有人会问我的是9.38是不是要重启,我也发现了这个问题,新搭的虚拟机,完全没必要,猜测是系统给redis预先分配了固定内存导致的;目前最好的办法就是重启redis回收内存。当used_memory的值应该比used_memory_rss高时,则表示Redis的部分内存被操作系统换出到交换空间了,这种情况下,操作就可能会出现明显的延迟,还有可能出现数据丢失的危险。stats中的total_commands_processed显示了redis当前处理的命令总数,可以通过定期记录数目计算出QPS来分析性能是否下降。client中的connected_clients节点展示了当前有多少个链接,如果连接数超出预期,则表示客户端可能没有有效释放链接,Redis默认允许的客户端最大连接数为10000。
slowlog
有时候我们想知道到底是哪些操作导致了Redis堵塞,这个时候我们可以通过slowlog命令来定位,默认情况下若一个命令执行时间操作10ms就会被记录,示例中我们取前10条慢日志:
slowlog get 10
如果你想降低或提高10ms这个阈值的话,请使用如下命令:
config set slowlog-log-slower-than # 也可以通过config get slowlog-log-slower-than来查看当前的配置,
latency-monitor-threshold
Redis中提供了一个特殊模式来监控命令延迟,即“latency-monitor-threshold”指令,该指令设置了以毫秒为单位的限制,超过该限制的所有或部分命令及Redis示例的活动均会被记录下来。该指令默认为0,不自动监控,所以我们首先需要如下配置:
config set latency-monitor-threshold 100
通过latency latest命令我们可以查看到事件名、最近延迟的Unix时间戳、最近的延迟、最大延迟等。我们可以通过debug来人为制造一些慢命令来进行测试:
debug sleep 1
debug sleep .25
latency-monitor-threshold可以和slow-log配合使用,想详细了解可以去查看latency的相关命令。
网络优化
书中并未提及,但是既然前面涉及到了,这里也给出一些优化建议来减少网络延迟:
- 使用长链接、不要频繁的连接和断开客户端到服务器的连接;
- 相比较管道而言,可以优先使用多参数命令,如mset、mget、hmset、hmget等;
- 使用管道;
- 如果存在数据依赖而不方便使用管道时,可以考虑使用Lua脚本来进行优化。
对于优化这一块,是不是考虑搞个番外来单独讲呢?