redis故障诊断和优化

8. redis的故障诊断与优化

8.1 常见缓存问题

8.1.1 缓存穿透

  • 什么叫缓存穿透?

一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。如果key对应的value是一定不存在的,并且对该key并发请求量很大,就会对后端系统造成很大的压力。

也就是说,对不存在的key进行高并发访问,导致数据库压力瞬间增大,这就叫做【缓存穿透】。

  • 如何解决?

1:对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。

2:对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过 滤。(布隆表达式)

8.1.2 缓存雪崩

  • 什么叫缓存雪崩

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压 力。

  • 如何解决

1:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和 写缓存,其他线程等待。

2:不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

3:做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长 期(此点为补充)

8.1.3 缓存击穿

  • 什么叫做缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这 个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。

缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会 从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

  • 如何解决

使用redis的setnx互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去操作数据库。

<code>if(redis.sexnx()==1){
 //先查询缓存
//查询数据库
//加入缓存
}/<code>

8.2 启动redis日志功能

redis默认不记录log文件, 需要在redis.conf文件修改logfile参数.如下:

  • 日志级别 loglevel notic
  • 日志路劲 logfile "/usr/local/redis/log/redis.log"

8.3 redis的监控状态

8.3.1 RDB状态监控

<code>bin/redis-cli info |grep rdb_/<code>
  • rdb_changes_since_last_save表明上次RDB保存以后改变的key次数
  • rdb_bgsave_in_progress 表示当前是否在进行 bgsave 操作,1 表示正在进行;0 表示没有进行
  • rdb_last_save_time 上次保存 RDB 文件的时间戳
  • rdb_last_bgsave_time_sec 上次保存的耗时
  • rdb_last_bgsave_status 上次保存的状态
  • rdb_current_bgsave_time_sec 目前保存 RDB 文件已花费的时间

8.3.2 aof的监控状态

<code>bin/redis-cli info |grep aof_/<code>
  • aof_enabledAOF文件是否启用
  • aof_rewrite_in_progress 表示当前是否在进行 AOF 日志的重写
  • aof_rewrite_scheduled
  • aof_last_rewrite_time_sec 上次写入的时间戳
  • aof_current_rewrite_time_sec:-1
  • aof_last_bgrewrite_status:ok 上次写入状态
  • aof_last_write_status:ok 上次写入状态

8.3.3 内存监控

<code>bin/redis-cli info |grep mem/<code>
  • used_memory:13490096 //数据占用了多少内存(字节)
  • used_memory_human:12.87M //数据占用了多少内存(带单位的,可读性好)
  • used_memory_rss:13490096 //redis 占用了多少内存
  • used_memory_peak:15301192 //占用内存的峰值(字节)
  • used_memory_peak_human:14.59M //占用内存的峰值(带单位的,可读性好)
  • used_memory_lua:31744 //lua 引擎所占用的内存大小(字节)
  • mem_fragmentation_ratio:1.00 //内存碎片率
  • mem_allocator:libc //redis 内存分配器版本,在编译时指定的。有 libc、jemalloc、tcmalloc 这 3 种

8.4 redis慢查询

8.4.1 慢查询日志

慢查询日志帮助开发和运维人员定位系统存在的慢操作。慢查询日志就是系统

在命令执行前后计算每条命令的执行时间,当超过预设阀值,就将这条命令的相关 信息(慢查询 ID,发生时间戳,耗时,命令的详细信息)记录下来。

Redis 客户端一条命令分为如下四部分执行:

redis故障诊断和优化

需要注意的是,慢查询日志只是统计步骤 3)执行命令的时间,所以慢查询并不代 表客户端没有超时问题。需要注意的是,慢查询日志只是统计步骤 3)执行命令的时间, 所以慢查询并不代表客户端没有超时问题。


8.4.2 慢查询参数

  • 慢查询的预设阀值 slowlog-log-slower-than
    • slowlog-log-slower-than参数就是预设阀值,1000,如果一条命令的执行时间超过 10000 微妙,那么它将被记录 在慢查询日志中。
    • 如果slowlog-log-slower-than的值是0,则会记录所有命令。
    • 如果slowlog-log-slower-than的值小于0,则任何命令都不会记录日志
    • 对于高流量的场景,如果执行命令的时间在 1 毫秒以上,那 么 redis 最多可支撑 OPS(每秒操作次数)不到 1000,因此高 OPS 场景的 REDIS 建议设置为 1 毫秒.
  • 慢查询日志的长度slowlog-max-len
    • slowlog-max-len只是说明了慢查询日志最多存储多少条。Redis使用一个列表来存储慢查询日志,showlog-max-len 就是列表的最大长度。 当慢查询日志已经到达列表的最大长度时,又有慢查询日志要进入列 表,则最早插入列表的日志将会被移出列表,新日志被插入列表的末 尾。
    • slowlog-max-len的设置建议, 线上环境建议调大慢查询日志的列表,记录慢查询日志时 Redis 会对长命令做截断操作,并不会占用大量内存。增大慢查询列表可 以减缓慢查询被剔除出列表的可能性。例如线上可以设置为 1000 以 上

8.4.3 慢查询日志的组成

慢查询日志由以下四个属性组成:标识 ID,发生时间戳,命令耗时,执行命令和参数

8.5 redis的pipeline(管道)

简单来说,PipeLine(管道)就是“批处理”操作。 由于网络开销延迟,就算 redis server 端有很强的处理能力,也会由于收到的client 消息少,而造成吞吐量小。当 client 使用 pipelining 发送命令时,redis server 必须将部分请求放到队列中(使用内存),执行完毕后一次性发送结果;如果发送 的命令很多的话,建议对返回的结果加标签,当然这也会增加使用的内存。

Pipeline 在某些场景下非常有用,比如有多个 command 需要被“及时的”提 交,而且他们对相应结果没有互相依赖,对结果响应也无需立即获得,那么 pipeline 就可以充当这种“批处理”的工具;而且在一定程度上,可以较大的提升性能,性 能提升的原因主要是 TCP 连接中减少了“交互往返”的时间。

应用场景: 特别是有for循环取值时, 建议使用pipeline

代码示例:

<code># 自定义一个自己需要的类
List<response>> resAll = new ArrayList();

// 获取一个管道对象
Pipeline pipeline = jedisUtil.getReis().pipelined();
List<string> ids = new ArrayList();
ids.add("str1");
ids.add("str2");
ids.add("str3");

ids.forEach(id->{
 Response<string> res = pipeline.get(id);
 // 管道的对象结果都存在resAll中
 resAll.add(res);
});
//pipeline.sync(); close内部有sync()方法调用
pipeline.close();
resAll.forEach(item -> {
 System.out.println(Integer.valueof(res) + 100);
});/<string>/<string>/<response>/<code>


8.6 redis的噩耗: 阻塞

8.6.1 耗时长命令造成阻塞

  • keys, sort等命令

当redis的数据量达到一定级别后(比如20G),阻塞操作对性能的影响尤为严重;keys命令用于查找所有符合给定模式 pattern 的 key,时间复杂度为O(N), N 为数据库中 key 的数量。当数据库中的个数达到千万时,这个命令会造成读写线程阻塞数秒;类似的命令有sunion sort等操作;如果业务需求中一定要使用keys、sort等操作怎么办?

解决方案

在架构设计中,有“分流”一招,说的是将处理快的请求和处理慢的请求分离开来,否则,慢的影响到了快的,让快的也快不起来;这在redis的设计中体现的非常明显,redis的纯内存操作,epoll非阻塞IO事件处理,这些快的放在一个线程中搞定,而持久化,AOF重写、Master-slave同步数据这些耗时的操作就单开一个进程来处理,不要慢的影响到快的;同样,既然需要使用keys这些耗时的操作,那么我们就将它们剥离出去,比如单开一个redis slave结点,专门用于keys、sort等耗时的操作,这些查询一般不会是线上的实时业务,查询慢点就慢点,主要是能完成任务,而对于线上的耗时快的任务没有影响

  • smembers命令

smembers命令用于获取集合全集,时间复杂度为O(N),N为集合中的数量;如果一个集合中保存了千万量级的数据,一次取回也会造成事件处理线程的长时间阻塞;

解决方案

和sort,keys等命令不一样,smembers可能是线上实时应用场景中使用频率非常高的一个命令,这里分流一招并不适合,我们更多的需要从设计层面来考虑;在设计时,我们可以控制集合的数量,将集合数一般保持在500个以内;比如原来使用一个键来存储一年的记录,数据量大,我们可以使用12个键来分别保存12个月的记录,或者365个键来保存每一天的记录,将集合的规模控制在可接受的范围;

如果不容易将集合划分为多个子集合,而坚持用一个大集合来存储,那么在取集合的时候可以考虑使用SRANDMEMBER key [count];随机返回集合中的指定数量,当然,如果要遍历集合中的所有元素,这个命令就不适合了;

  • save命令

save命令使用事件处理线程进行数据的持久化;当数据量大的时候,会造成线程长时间阻塞(我们的生产上,reids内存中1个G保存需要12s左右),整个redis被block;save阻塞了事件处理的线程,我们甚至无法使用redis-cli查看当前的系统状态,造成“何时保存结束,目前保存了多少”这样的信息都无从得知;

解决方案

我没有想到需要用到save命令的场景,任何时候需要持久化的时候使用bgsave都是合理的选择(当然,这个命令也会带来问题,后面聊到);

推介两篇redis阻塞的文章:

https://blog.csdn.net/linbiaorui/article/details/79822318

http://colin115.iteye.com/blog/2263351

8.7 redis持久化故障诊断

  1. 使用 Java 客户端,循环插入 20 个 200M 大小的数据,程序如下:
  2. 检查 RDB 的状态信息
<code>bin/redis-cli info |grep rdb_/<code>
  1. 检查日志文件: redis.log
redis故障诊断和优化


  1. 解决问题:修改 vm.overcommit_memory 参数。关于 vm.overcommit_memory 不同的值说明:由于 RDB 文件写的时候 fork 一个子进程。相当于复制了一个内存镜像。 当时系统的内存是 4G,而 redis 占用了近 3G 的内存,因此肯定会报内存无法 分配。如果 「vm.overcommit_memory」设置为 0,在可用内存不足的情况下, 就无法分配新的内存。如果 「vm.overcommit_memory」设置为 1。 那么 redis将使用交换内存。
  2. 方法一: 修改内核参数 vi /etc/sysctl。设置 vm.overcommit_memory = 1 然后执行 sysctl -p
  3. 方法二: 使用交换内存并不是一个完美的方案。最好的办法是扩大物 理内存。
  4. 0 表示检查是否有足够的内存可用,如果是,允许分配;如果内存不够,拒绝该请求,并返回一个错误给应用程序。
  5. 1 允许分配超出物理内存加上交换内存的请求
  6. 2 内核总是返回 true

8.8 redis内存淘汰策略

8.8.1 最大缓存

在 redis 中,允许用户设置最大使用内存大小maxmemory,默认为0,没有指定最大缓存,如果有新的数据添 加,超过最大内存,则会使redis崩溃,所以一定要设置.

redis 内存数据集大小上升到一定大小的时候,就会实行数据淘汰策略

8.8.2 淘汰策略

redis淘汰策略配置: maxmemory-policy voltile-lru,支持热配置

Redis提供6种数据淘汰策略:

  1. voltile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  4. allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  6. no-enviction(驱逐):禁止驱逐数据

8.8.3 LRU原理

LRU( ,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如 果数据最近被访问过,那么将来被访问的几率也更高”。


分享到:


相關文章: