Redis(四)Redis内存

文章目录

    • 一、内存
      • 1.1 内存消耗
        • 1.1.1 内存使用统计
        • 1.1.2 内存消耗划分
      • 1.2 内存管理
        • 1.2.1 设置内存上限
        • 1.2.2 动态调整内存上限
        • 1.2.3 键过期删除策略
        • 1.2.4 内存淘汰策略
      • 1.3 内存优化
        • 1.3.1 缩减键值对象
        • 1.3.2 共享对象池
        • 1.3.3 字符串优化
        • 1.3.4 编码优化
        • 1.3.5 控制键的数量
      • 1.4 内存相关问题
        • 1.4.1 Mysql里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据
        • 1.4.2 如何获取当前最大内容,怎么动态设置
        • 1.4.3 Redis的内存用完了会发生什么
        • 1.4.4 LRU的实现思路
    • 二、阻塞问题
      • 2.1 内在原因
        • 2.1.1 API或数据结构使用不合理
        • 2.1.2 CPU饱和
        • 2.1.3 持久化阻塞
      • 2.2 外在原因
        • 2.2.1 CPU竞争
        • 2.2.2 内存交换
        • 2.2.3 网络问题

本系列文章:
  Redis(一)数据类型、常用命令
  Redis(二)Redis客户端的使用
  Redis(三)持久化、主从复制
  Redis(四)Redis内存
  Redis(五)哨兵、集群
  Redis(六)缓存、分布式锁
  Redis(七)Redis优化建议

一、内存

1.1 内存消耗

1.1.1 内存使用统计

  可通过info memory命令获取内存相关指标。示例:
Redis(四)Redis内存_第1张图片
  各个属性的含义:

属性 说明
used_memory Redis分配器分配的内存总量,也就是内部存储的所有数据内存占用量
used_memory_human 以可读的形式返回used_memory
used_memory_rss 从操作系统的角度显示Redis进程占用的物理内存总量
used_memory_peak 内存使用的最大值,表示used_memory的峰值
used_memory_peak_human 以可读的格式返回used_memory_peak
used_memory_lua Lua引擎所消耗的内存大小
mem_fragmentation_ratio used_memory_rss / used_memory比值,表示内存碎片率
mem_allocator Redis所使用的的内存分配器,默认为jemalloc

  当mem_fragmentation_ratio(used_memory_rss / used_memory)>1时,说明used_memory_rss-used_memory多出的部分内存并没有用于数据存储,而是被内存碎片所消耗。如果两者相差很大,说明碎片率严重。
  当mem_fragmentation_ratio<1时,这种情况一般出现在操作系统把Redis内存交换到硬盘导致,出现这种情况时要格外关注,由于硬盘速度远远慢于内存,Redis性能会变得很差。

1.1.2 内存消耗划分

  Redis进程内消耗主要包括:自身内存+对象内存+缓冲内存+内存碎片,Redis空进程自身内存消耗非常少,通常used_memory_rss(Redis进程占用的物理内存总量)在3MB左右,一个空的Redis进程消耗内存可以忽略不计。

  • 1、对象内存
      对象内存是Redis内存占用最大的一块,对象内存消耗可以简单理解为sizeof(keys)+sizeof(values)。键对象都是字符串,在使用Redis时应当避免使用过长的键。
      
  • 2、缓冲内存
      缓冲内存主要包括:客户端缓冲、复制积压缓冲区、AOF缓冲区。
      客户端缓冲指的是所有接入到Redis服务器TCP连接的输入输出缓冲。输入缓冲无法控制,最大空间为1G,如果超过将断开连接。输出缓冲通过
    参数client-output-buffer-limit控制,不同类型客户端的默认值:

      普通客户端:指除了复制和订阅的客户端之外的所有连接。一般情况下,普通客户端的内存消耗可以忽略不计,但是当有大量慢连接客户端接入时这部分内存消耗就不能忽略了,可以设置maxclients(最大连接数)做限制。
      从客户端:当主从节点之间网络延迟较高或主节点挂载大量从节点时这部分内存消耗将占用很大一部分,建议主节点挂载的从节点不要多于2个,主从节点不要部署在较差的网络环境下,如异地跨机房环境。
      订阅客户端:当订阅服务的消息生产快于消费速度时,输出缓冲区会产生积压造成输出缓冲区空间溢出。
      复制积压缓冲区:Redis在2.8版本之后提供了一个可重用的固定大小缓冲区用于实现部分复制功能,由repl-backlog-size参数表示,默认1M,示例:

      整个主节点只有一个复制积压缓冲区,所有的从节点共享此缓冲区,因此可以设置较大的缓冲区空间,如100MB。
      AOF缓冲区:这部分空间用于在Redis重写期间保存最近的写入命令,这部分空间占用通常很小。
  • 3、内存碎片
      Redis默认的内存分配器采用jemalloc,可选的分配器还有:glibc、tcmalloc。
      jemalloc在64位系统中将内存空间划分为:小、大、巨大三个范围。每个范围内又划分为多个小的内存块单位。

  :[8byte],[16byte,32byte,48byte,…,128byte],[192byte,256byte,…512byte],[768byte,1024byte,…,3840byte]
  :[4KB,8KB,12KB,…,4072KB]
  巨大:[4MB,8MB,12MB,…]

  比如当保存10KB对象时jemalloc可能会采用12KB的块存储,而剩下的3KB空间变为了内存碎片不能再分配给其他对象存储。
  jemalloc正常的碎片率(mem_fragmentation_ratio)在1.03左右。
  以下场景容易出现高内存碎片问题:

  1. 频繁做更新操作。
  2. 大量过期键删除,键对象过期删除后,释放的空间无法得到充分利用,导致碎片率上升。

  出现高内存碎片问题时常见的解决方式:

  1. 数据对齐:在条件允许的情况下尽量做数据对齐,比如数据尽量采用数字类型或者固定长度字符串等。
  2. 安全重启:重启节点可以做到内存碎片重新整理。

1.2 内存管理

1.2.1 设置内存上限

  Redis使用maxmemory参数限制最大可用内存。示例:

  如果不设置maxmemory或者设置为0,64位系统不限制内存,32位系统最多使用3GB内存
  maxmemory限制的是Redis实际使用的内存量,也就是used_memory统计项对应的内存。由于内存碎片的存在,实际消耗的内存可能会比maxmemory设置的更大,实际使用时要小心这部分内存溢出。
  比如一台24GB内存的服务器,为系统预留4GB内存,预留4GB空闲内存给其他进程或Redis fork进程,留给Redis16GB内存,这样可以部署4个maxmemory=4GB的Redis进程。
Redis(四)Redis内存_第2张图片

1.2.2 动态调整内存上限

  通过config set maxmemory可以设置一个Redis示例的内存上限,示例:

1.2.3 键过期删除策略

  Redis的内存回收机制主要体现在以下两个方面:

  1. 删除到达过期时间的键对象;
  2. 内存使用达到maxmemory上限时触发内存溢出控制策略。

  从库不会进行过期扫描,从库对过期的处理是被动的。主库在key到期时,会在AOF文件里增加一条del指令,同步到所有的从库,从库通过执行这条del指令来删除过期的key。

  Redis中有个设置时间过期的功能,即对存储在Redis 数据库中的值可以设置一个过期时间。
  我们执行set key命令 的时候,都可以指定expire time,即过期时间,以此来限定这个 key 可以存活的时间。

  对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。

  键过期策略通常有以下三种:定时过期、惰性过期和定期过期。

  • 1、定时删除
      在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作;

  定时删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放,但是对CPU是最不友好的。因为删除操作会占用CPU的时间,如果刚好碰上了CPU很忙的时候,比如正在做交集或排序等计算的时候,就会给CPU造成额外的压力。

  • 2、惰性删除
      放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,那就返回该键;
  • 3、定期删除
      每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于删除多少过期键,以及要检查多少个数据库,则由算法决定。

  由于维护每个键精准的过期删除机制会导致消耗大量的CPU,成本过高(不采用第1种策略的愿意),因此Redis采用惰性删除和定期删除机制实现过期键的内存回收

  惰性删除:当客户端读取超时的键时,会执行删除键操作并返回空。单独用这种方式,浪费内存,存在内存泄露的问题(当过期键一直没有访问将无法得到及时删除,从而导致内存不能及时释放)。正因为如此,Redis还提供另一种定时任务删除机制作为惰性删除的补充。
  定期删除:Redis内部维护一个定时任务,默认每秒运行10次,根据键的过期比例、使用快慢两种速率模式回收键。
  定时任务删除过期键流程:

  1. 定时任务(每10秒执行一次,在Redis配置文件中有默认配置:hz 10)在每个数据库空间随机检查20个键,当发现过期时删除对应的键。
  2. 如果超过检查数25%的键过期,循环执行回收逻辑直到不足25%或运行超时为止,慢模式下超时时间为25毫秒。
  3. 如果之前回收键逻辑超时,则在Redis触发内部事件之前再次以快模式运行回收过期键任务,快模式下超时时间为1毫秒且2秒内只能运行1次。

  此处再简单回顾一下Key的过期时间和永久有效分别怎么设置。
  setex命令可以为key设置value值,并且同时设置过期时间。示例:
Redis(四)Redis内存_第3张图片
  也可以用expire命令,直接为key设置过期时间。示例:
Redis(四)Redis内存_第4张图片

setex是一个原子操作,设置值,设置过期时间两个动作,会在同一时间完成。

  persist命令用于移除给定 key 的过期时间,使得 key 永不过期。示例:
Redis(四)Redis内存_第5张图片

1.2.4 内存淘汰策略

  当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略,由maxmemory-policy参数控制,示例:

  在Redis4.0以前,内存溢出策略有6种,默认值为noeviction,含义为:

  1、noeviction:默认策略,不会删除任何键,拒绝所有写入操作并返回客户端错误信息OOM command not allowed when used memory,只响应读操作。
  2、volatile-lru:根据LRU算法删除超时的键,直到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。
  3、allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。
  4、allkeys-random:随机删除所有键,直到腾出足够空间为止。
  5、volatile-random:随机删除过期键,直到腾出足够空间为止。
  6、volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。

  Redis4.0以后,又增加了以下两种:

  7、volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键(频率最少)。
  8、allkeys-lfu:从所有键中驱逐使用频率最少的键(频率最少)。

  从这些策略的名称可以看出:

volatile:表示过期的键
allkeys:表示所有键
lru:表示使用LRU算法,简单来说是删除过期时间最久的键
lfu:表示使用LFU算法,简单来说是删除使用频率最少的键
ttl:表示马上要过期

  内存淘汰策略可以通过配置文件来修改,redis.conf对应的配置项是maxmemory-policy 修改对应的值就行。要动态配置的话,可以用config set maxmemory-policy {policy}命令进行修改。
  频繁执行回收内存成本很高,主要包括查找可回收键和删除键的开销。

  Redis用的是一种近似LRU算法,它跟LRU算法还不太一样。之所以不使用LRU算法,是因为需要消耗大量的额外的内存,需要对现有的数据结构进行较大的改造。近似LRU算法则很简单,在现有数据结构的基础上使用随机采样法来淘汰元素,能达到和 LRU算法非常近似的效果。Redis为实现近似LRU算法,它给每个 key 增加了一个额外的小字段,这个字段的长度是24个bit,也就是最后一次被访问的时间戳。

  • 内存淘汰策略使用参考
      1)如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用allkeys-lru。
      2)如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用allkeys-random。

1.3 内存优化

  Redis存储的所有值对象在内部定义为redisObject结构体:
Redis(四)Redis内存_第6张图片
  Redis存储的数据都使用redisObject来封装,包括string、hash、list、set、zset在内的所有数据类型。
  type:表示当前对象使用的数据类型。
  encoding:表示Redis内部编码类型。
  lru:记录对象最后一次被访问的时间。
  refcount:记录当前对象被引用的次数,用于通过引用次数回收内存,当refcount=0时,可以安全回收当前对象空间。使用object refcount {key}命令可以获取当前对象引用。
  *ptr:如果是整数,直接存储数据;否则表示指向数据的指针。Redis在3.0之后对值对象是字符串且长度<=39字节的数据,内部编码为embstr类型,字符串和redisObject一起分配,从而只要一次内存操作即可。

  高并发写入场景中,在条件允许的情况下,建议字符串长度控制在39字节以内,减少创建redisObject内存分配次数,从而提高性能。

1.3.1 缩减键值对象

  降低Redis内存使用最直接的方式就是缩减键和值的长度
  在设计键时,在完整描述业务情况下,键值越短越好。
  值对象缩减比较复杂,常见需求是把业务对象序列化成二进制数据放入Redis。值对象缩减常见的做法分为两大类:

  1. 首先应该在业务上精简业务对象,去掉不必要的属性避免存储无效数据。
  2. 其次在序列化工具选择上,应该选择更高效的序列化工具来降低字节数组大小。

  Java常见序列化组件占用内存空间对比(单位字节):
Redis(四)Redis内存_第7张图片
  java-built-in-serializer表示Java内置序列化方式。

  值对象除了存储二进制数据之外,通常还会使用通用格式存储数据比如:json、xml等作为字符串存储在Redis中。这种方式优点是方便调试和跨语言,但是同样的数据相比字节数组所需的空间更大,在内存紧张的情况下,可以使用通用压缩算法压缩json、xml后再存入Redis,从而降低内存占用,例如使用GZIP压缩后的json可降低约60%的空间。

1.3.2 共享对象池

  共享对象池是指Redis内部维护[0-9999]的整数对象池,用于节约内存
  整数对象池在Redis源码中定义,不能修改。
  是否使用整数对象池内存对比:

  可见使用共享对象池后,相同的数据内存使用降低30%以上。
  当设置maxmemory并启用LRU相关淘汰策略如:volatile-lru,allkeys-lru时,Redis禁止使用共享对象池。
  LRU算法需要获取对象最后被访问时间,以便淘汰最长未访问数据,每个对象最后访问时间存储在redisObject对象的lru字段。对象共享意味着多个引用共享同一个redisObject,这时lru字段也会被共享,导致无法获取每个对象的最后访问时间。因此,使用LUR相关淘汰策略时,无法使用共享对象池
  如果没有设置maxmemory,直到内存被用尽Redis也不会触发内存回收,所以共享对象池可以正常工作。因此,设置了maxmemory后,无法使用共享对象池

1.3.3 字符串优化

  Redis没有采用原生C语言的字符串类型而是自己实现了字符串结构,内部简单动态字符串(simple dynamic string,SDS),图示:
Redis(四)Redis内存_第8张图片
  获取字符串长度、已用长度、未用长度的时间复杂度都是O(1)。
  内部实现空间预分配机制,字符串缩减后的空间不释放,作为预分配空间保留。
  字符串之所以采用预分配的方式是防止修改操作需要不断重分配内存和字节数据拷贝。但同样也会造成内存的浪费。字符串预分配每次并不都是翻倍扩容,空间预分配规则:

  1. 第一次创建len属性等于数据实际大小,free等于0,不做预分配。
  2. 修改后如果已有free空间不够且数据小于1M,每次预分配一倍容量。如原有len=60byte,free=0,再追加60byte,预分配120byte,总占用空间:60byte+60byte+120byte+1byte。
  3. 修改后如果已有free空间不够且数据大于1MB,每次预分配1MB数据。如原有len=30MB,free=0,当再追加100byte,预分配1MB,总占用空间:1MB+100byte+1MB+1byte。

  尽量减少字符串频繁修改操作如append、setrange,改为直接使用set修改字符串,降低预分配带来的内存浪费和内存碎片化

1.3.4 编码优化

  Redis对外提供了string、list、hash、set、zet等类型,对内也有不同的底层数据结构的实现,可以用object encoding {key}命令查看,示例:
Redis(四)Redis内存_第9张图片
  Redis针对每种对外数据类型可以采用至少两种编码方式来实现,示例:
Redis(四)Redis内存_第10张图片
  不同编码主要是为了实现效率和空间的平衡。
  编码类型转换在Redis写入数据时自动完成,这个转换过程是不可逆的,转换规则只能从小内存编码向大内存编码转换。示例:
Redis(四)Redis内存_第11张图片
  Redis之所以不支持编码回退,主要是数据增删频繁时,数据向压缩编码转换非常消耗CPU。
  示例中用到了list-max-ziplist-entries参数,这个参数用来决定列表长度在多少范围内使用ziplist编码。
  ziplist编码主要目的是为了节约内存,因此所有数据都是采用线性连续的内存结构。ziplist编码是应用范围最广的一种,可以分别作为hash、list、zset类型的底层数据结构实现。一个ziplist可以包含多个entry(元素),每个entry保存具体的数据(整数或者字节数组),内部结构:
Redis(四)Redis内存_第12张图片
  zlbytes:记录整个压缩列表所占字节长度,方便重新调整ziplist空间。类型是int-32,长度为4字节。
  zltail:记录距离尾节点的偏移量,方便尾节点弹出操作。类型是int-32,长度为4字节。
  zllen:记录压缩链表节点数量,当长度超过216-2时需要遍历整个列表获取长度,一般很少见。类型是int-16,长度为2字节。
  entry:记录具体的节点,长度根据实际存储的数据而定。
  zlend:记录列表结尾,占用一个字节。
  Entry的元素:

  prev_entry_bytes_length:记录前一个节点所占空间,用于快速定位上一个节点,可实现列表反向迭代。
  encoding:标示当前节点编码和长度,前两位表示编码类型:字符串/整数,其余位表示数据长度。
  contents:保存节点的值,针对实际数据长度做内存占用优化。

  ziplist的特点:

  1. 底层实现为数据紧凑排列的一块连续内存数组。
  2. 可以模拟双向链表结构,以O(1)时间复杂度入队和出队。
  3. 新增删除操作涉及内存重新分配或释放,加大了操作的复杂性。
  4. 读写操作涉及复杂的指针移动,最坏时间复杂度为O(n2)。
  5. 适合存储小对象和长度有限的数据。

  intset是集合类型编码的一种,内部表现为存储有序、不重复的整数集。当集合只包含整数且长度不超过set-max-intset-entries配置时被启用。示例:
Redis(四)Redis内存_第13张图片
  intset结构:
Redis(四)Redis内存_第14张图片
  字段含义:

  1. encoding:整数表示类型,根据集合内最长整数值确定类型,整数
    类型划分为三种:int-16、int-32、int-64。
  2. length:表示集合元素个数。
  3. contents:整数数组,按从小到大顺序保存。

  intset保存的整数类型根据长度划分,当保存的整数超出当前类型时,将会触发自动升级操作且升级后不再做回退。因此,使用intset编码的集合时,尽量保持整数范围一致,如都在int-16范围内。防止个别大整数触发集合升级操作,产生内存浪费。
  intset数据结构插入命令复杂度为O(n),查询命令为O(log(n)),由于整数占用空间非常小,所以在集合长度可控的基础上,写入命令执行速度也会非常快,因此当使用整数集合时尽量使用intset编码。

1.3.5 控制键的数量

  对于存储相同的数据内容利用Redis的数据结构降低外层键的数量,也可以节省大量内存。比如:通过在客户端预估键规模,把大量键分组映射到多个hash结构中降低键的数量。
Redis(四)Redis内存_第15张图片
  同样的数据使用ziplist编码的hash类型存储比string类型节约内存。当value越小(即小对象)越明显。
  hash-ziplist类型比string类型写入耗时,但随着value空间的减少,耗时逐渐降低。
  hash类型节省内存的原理是使用ziplist编码,如果使用hashtable编码方式反而会增加内存消耗。
  ziplist长度需要控制在1000以内,否则由于存取操作时间复杂度在O(n)到O(n2)之间,否则会导致CPU消耗严重。
  ziplist适合存储小对象,对于大对象不但内存优化效果不明显还会增加命令操作耗时。
  关于hash键和field键的设计:

  1. 当键离散度较高时,可以按字符串位截取,把后三位作为哈希的field,之前部分作为哈希的键。如:key=1948480哈希key=group:hash:1948,哈希field=480。
  2. 当键离散度较低时,可以使用哈希算法打散键,如:使用crc32(key)&10000函数把所有的键映射到“0-9999”整数范围内,哈希field存储键的原始值。
  3. 尽量减少hash键和field的长度,如使用部分键内容。

1.4 内存相关问题

1.4.1 Mysql里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据

  要考虑两方面内容:1.Redis的内存淘汰策略。2.Redis的最大内存设置。思路:首先计算出20w数据所需的内存空间,设置最大内存(maxmemory),然后选择合适的内存淘汰策略。
  当Redis内存数据集大小上升到一定大小的时候,就会执行内存淘汰策略。示例:

  至于最大内存设置,可以对最大内存进行设置。当使用的内存到达指定的限制时,Redis会根据内存淘汰策略删除键,以释放空间。示例:

1.4.2 如何获取当前最大内容,怎么动态设置

  获取最大内存命令:config get maxmemory
  社会最大内存命令示例:config set maxmemory 1GB

1.4.3 Redis的内存用完了会发生什么

  如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以将Redis当缓存来使用配置淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。

1.4.4 LRU的实现思路

  可以通过双向链表来实现:

  1. 通过双向链表来实现,新数据插入到链表头部。
  2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部。
  3. 当链表满的时候,将链表尾部的数据丢弃。

二、阻塞问题

  Redis是单线程架构,因此阻塞问题是需要非常重视的。导致阻塞问题的原因大致分为:

  1. 内在原因:不合理地使用API或数据结构、CPU饱和、持久化阻塞等。
  2. 外在原因:CPU竞争、内存交换、网络问题等。

  当Redis阻塞时,应用会收到大量Redis超时异常,比如Jedis客户端会抛出JedisConnectionException异常,这时一般会触发告警,何时触发报警一般根据应用的并发量决定,如1分钟内超过10个异常触发报警。

2.1 内在原因

  应该围绕这几个方面去排查:API或数据结构使用不合理;CPU饱和的问题;持久化相关的阻塞。

2.1.1 API或数据结构使用不合理

  典型的不合理使用API和数据结构,比如:对一个包含上万个元素的hash结构执行hgetall操作,由于数据量比较大且命令算法复杂度是O(n),这条命令执行速度必然很慢。

  • 1、慢查询
      所谓慢查询日志就是系统在命令执行前后计算每条命令的执行时间,当超过预设阀值,就将这条命令的相关信息(例如:发生时间,耗时,命令的详细信息)记录下来,Redis中也有这种功能。
      Redis客户端执行一条命令的过程:
    Redis(四)Redis内存_第16张图片
      慢查询本身只记录了命令执行时间,不包括数据网络传输时间和命令排队时间,因此客户端发生阻塞异常后,可能不是当前命令缓慢,而是在等待其他命令执行。
      Redis提供了slowlog-log-slower-thanslowlog-max-len配置慢查询阀值和慢查询记录。

  slowlog-log-slower-than单位是微秒(1秒=1000毫秒=1000000微秒),默认值是10000。
  如果slowlog-log-slower-than=0会记录所有的命令,slowlog-log-slower-than<0对于任何命令都不会进行记录。

  Redis使用了一个列表来存储慢查询日志,slowlog-max-len就是列表的最大长度。一个新的命令满足慢查询条件时被插入到这个列表中,当慢查询日志列表已处于其最大长度时,最早插入的一个命令将从列表中移出,例如slowlog-max-len设置为5,当有第6条慢查询插入的话,那么队头的第一条数据就出列,第6条慢查询就会入列。
  总的来说,在Redis中有两种修改配置的方法,一种是修改配置文件,另一种是使用config set命令动态修改。slowlog-log-slower-thanslowlog-max-len自然也可以用这两种方式进行修改。
  1、获取慢查询日志:slowlog get {n},示例:
Redis(四)Redis内存_第17张图片
  慢查询日志有4个属性组成,分别是慢查询日志的标识id、发生时间戳、命令耗时、执行命令和参数。
  2、获取慢查询日志列表当前的长度:slowlog len,示例:

  3、慢查询日志重置(即清空满查询日志列表):slowlog reset
  使用慢查询的一些建议:

  1. 对于·slowlog-max-len,并不会占用太大内存,因为Redis会对慢查询中的长命令做截断操作,线上可设置为1000以上。
  2. 对于slowlog-log-slower-than,建议设置为1毫秒。
  3. 由于慢查询日志是一个先进先出的队列,也就是说如果慢查询比较多的情况下,可能会丢失部分慢查询命令,为了防止这种情况发生,可以定期执行slow get命令将慢查询日志持久化到其他存储中(例如MySQL)。

  上面介绍了一些慢查询的简单使用和原理,接下来看下慢查询在项目中的运用。
  执行slowlog get {n}命令可以获取最近的n条慢查询命令,默认对于执行超过10毫秒的命令都会记录到一个定长队列中,线上实例建议设置为1毫秒便于及时发现毫秒级以上的命令。
  如果命令执行时间在毫秒级,则实例实际OPS只有1000左右。慢查询队列长度默认128,可适当调大。

  发现慢查询后,需要作出及时调整。参考:

  1. 修改为低复杂度的命令,如hgetall改为hmget等,禁用keys、sort等命令。
  2. 调整大对象,缩减大对象数据或把大对象拆分为多个小对象,防止一次命令操作过多的数据。大对象拆分过程需要视具体的业务决定。
  • 2、大对象
      查看大对象的命令:redis-cli -h {ip} -p {port} bigkeys。内部原理采用分段进行scan操作,把历史扫描过的最大对象统计出来便于分析优化,示例:
    Redis(四)Redis内存_第18张图片

2.1.2 CPU饱和

  单线程的Redis处理命令时只能使用一个CPU,CPU饱和是指Redis把单核CPU使用率跑到接近100%。使用top命令可以看出对应Redis进程的CPU使用率。
  当出现CPU饱和情况时,可以查看当前Redis的并发量是否达到极限,建议使用命令redis-cli -h {ip} -p {port} --stat获取当前Redis实时的使用情况,示例:

  如果只有几百或几千OPS的Redis实例就接近CPU饱和是很不正常的,可能使用了高算法复杂度的命令

2.1.3 持久化阻塞

  对于开启了持久化功能的Redis节点,需要排查是否是持久化导致的阻塞。持久化引起主线程阻塞的操作主要有:fork阻塞、AOF刷盘阻塞、HugePage写操作阻塞。

  • 1、fork阻塞
      在RDB和AOF重写时,Redis主线程调用fork操作产生共享内存的子进程,由子进程完成持久化文件重写工作。如果fork操作本身耗时过长,必然会导致主线程的阻塞。
      可以执行info stats命令获取到latest_fork_usec指标,表示Redis最近一次fork操作耗时,如果耗时很大,比如超过1秒,则需要做出优化调整,如避免使用过大的内存实例和规避fork缓慢的操作系统等。示例:
    Redis(四)Redis内存_第19张图片
  • 2、AOF刷盘阻塞
      开启AOF持久化功能时,文件刷盘的方式一般采用每秒一次,后台线程每秒对AOF文件做fsync操作。当硬盘压力过大时,fsync操作需要等待,直到写入完成。如果主线程发现距离上一次的fsync成功超过2秒,为了数据安全性它会阻塞直到后台线程执行fsync操作完成。这种阻塞行为主要是硬盘压力引起。此时的日志示例:

      也可以查看info persistence命令中的aof_delayed_fsync指标,每次发生fdatasync阻塞主线程时会累加。示例:
    Redis(四)Redis内存_第20张图片
  • 3、HugePage写操作阻塞
      子进程在执行重写期间利用Linux写时复制技术降低内存开销,因此只有写操作时Redis才复制要修改的内存页。对于开启THP的操作系统,每次写命令引起的复制内存页单位由4K变为2MB,会拖慢写操作的执行时间,导致大量写操作慢查询。

2.2 外在原因

  外在原因主要围绕三个方面进行排查:CPU竞争、内存交换、网络问题。

2.2.1 CPU竞争

  进程竞争:Redis是典型的CPU密集型应用。当其他进程过度消耗CPU时,将严重影响Redis吞吐量。可以通过top等命令定位到CPU消耗的时间点和具体进程,这个问题需要调整服务之间部署结构。
  绑定CPU:部署Redis时为了充分利用多核CPU,通常一台机器部署多个实例。常见的一种优化是把Redis进程绑定到CPU上,用于降低CPU频繁上下文切换的开销。这种优化存在意外情况:

  当Redis父进程创建子进程进行RDB/AOF重写时,如果做了CPU绑定,会与父进程共享使用一个CPU。子进程重写时对单核CPU使用率通常在90%以上,父进程与子进程将产生激烈CPU竞争,极大影响Redis稳定性。因此对于开启了持久化或参与复制的主节点不建议绑定CPU。

2.2.2 内存交换

  如果操作系统把Redis使用的部分内存换出到硬盘,由于内存与硬盘读写速度差几个数量级,会导致发生交换后的Redis性能急剧下降。
  识别Redis内存交换的检查方法:

  • 1、查询Redis进程号,示例:
  • 2、根据进程号查询内存交换信息,示例:
    Redis(四)Redis内存_第21张图片
      如果交换量都是0KB或者个别的是4KB,则是正常现象,说明Redis进程内存没有被交换。预防内存交换的方法有:

  1、保证机器充足的可用内存。
  2、确保所有Redis实例设置最大可用内存(maxmemory),防止极端情况
下Redis内存不可控的增长。
  3、降低系统使用swap优先级,如echo10>/proc/sys/vm/swappiness.

2.2.3 网络问题

  网络问题经常是引起Redis阻塞的问题点。常见的网络问题主要有:连接拒绝、网络延迟、网卡软中断等。

  • 1、连接拒绝
      当出现网络闪断或者连接数溢出时,客户端会出现无法连接Redis的情况。需要区分这三种情况:网络闪断、Redis连接拒绝、连接溢出。
      网络闪断:一般发生在网络割接或者带宽耗尽的情况。对于重要的Redis服务需要充分考虑部署架构的优化,尽量避免客户端与Redis之间异地跨机房调用。
      当Redis连接数大于maxclients时会拒绝新的连接进入,被拒绝的连接数可以用info stats命令查看,示例:

      客户端访问Redis时尽量采用NIO长连接或者连接池的方式。
      连接溢出的原因较多,其中两种:进程限制、backlog队列溢出。
      客户端想成功连接上Redis服务需要操作系统和Redis的限制都通过才可以。
      系统对于特定端口的TCP连接使用backlog队列保存。Redis默认的长度为511,通过tcp-backlog参数设置。系统的backlog默认值为128,使用echo511>/proc/sys/net/core/somaxconn命令进行修改。可以通过netstat-s命令获取因backlog队列溢出造成的连接拒绝统计,示例:
  • 2、网络延迟
      网络延迟取决于客户端到Redis服务器之间的网络环境。主要包括它们之间的物理拓扑和带宽占用情况。常见的物理拓扑按网络延迟由快到慢可分为:同物理机>同机架>跨机架>同机房>同城机房>异地机房。
      可以在redis-cli -h {host} -p {port}命令后面加入如下参数进行延迟测试:

  1、--latency:持续进行延迟测试,分别统计:最小值、最大值、平均值、采样次数。
  2、--latency-history:统计结果同–latency,默认每15秒完成一行统计。
  3、--latency-dist:使用统计图的形式展示延迟统计,每1秒采样一次。

  网络延迟问题经常出现在跨机房的部署结构上。
  带宽瓶颈通常出现在以下几个方面:机器网卡带宽、机架交换机带宽、机房之间专线带宽。
  带宽占用主要根据当时使用率是否达到瓶颈有关,如频繁操作Redis的大对象对于千兆网卡的机器很容易达到网卡瓶颈。

  • 3、网卡软中断
      网卡软中断是指由于单个网卡队列只能使用一个CPU,高并发下网卡数据交互都集中在同一个CPU,导致无法充分利用多核CPU的情况。网卡软中断瓶颈一般出现在网络高流量吞吐的场景。
      相关情况可以用top命令查看,示例:
    Redis(四)Redis内存_第22张图片
      可以很明显看到CPU1的软中断指标(si)过高。

你可能感兴趣的:(【Redis】,redis,数据库)