写点什么

记一次 redis 主从切换导致的数据丢失与陷入只读状态故障

作者:Java你猿哥
  • 2023-05-23
    湖南
  • 本文字数:3414 字

    阅读完需:约 11 分钟

背景

最近一组业务 redis 数据不断增长需要扩容内存,而扩容内存则需要重启云主机,在按计划扩容升级执行主从切换时意外发生了数据丢失与 master 进入只读状态的故障,这里记录分享一下。

业务 redis 高可用架构

该组业务 redis 使用的是一主一从,通过 sentinel 集群实现故障时的自动主从切换,这套架构已经平稳运行数年,经历住了多次实战的考验。高可用架构大体如下图所示:


简单说一下 sentinel 实现高可用的原理:集群的多个(2n+1,N>1)哨兵会定期轮询 redis 的所有 master/slave 节点,如果 sentinel 集群中超过一半的哨兵判定 redis 某个节点已主观下线,就会将其判定为客观下线进行相应处理:

  1. 如果下线节点是 master,选定一个正常 work 的 slave 将其选定为新的 master 节点。

  2. 如果下线节点是 slave,将其从 slave 节点中移除。

如果已经被客观下线的节点恢复了正常,sentinel 中超过一半哨兵确认后则将其加回可用的 slave 节点。所有需要读写 redis 的 server 并不需要直接写死 redis 主从配置,而是通过访问 sentinel 获取当前 redis 的主从可用状态,具体实现方式可以定时查询 sentinel 询问更新,也可以通过订阅机制让 sentinel 在主从变动时主动通知订阅方更新。sentinel 实现高可用的详细原理这里不做过多赘述,有兴趣的小伙伴可以移步参考文献中的相关资料。

具体内存扩容流程

sentinel 可以在检测到故障时自动切换 redis 主从,也可以主动执行 sentinel failover mastername 命令实现手动切换主从,所以这次的内存扩容重启流程设计如下(A 代表初始 master 所在云主机,B 代表初始 slave 所在云主机):

  1. 升级主机 B 内存配置,重启主机 B

  2. 检查 B 重启后其上的 redis slave 是否重新同步 master 数据完成,包括:


    2.1 查看 slave redis log 是否异常,无异常 pass2.2 使用 info keyspace 命令 check master、slave 各 db key 数量是否一致,无异常 pass2.3 在 master 写入一个测试 key,在 slave 上 check 是否同步成功 2.4 观察依赖 server log 是否有异常

  3. 使用 sentinel failover mastername 命令手动主从切换,主机 A 变成新 slave,主机 B 变成新 master,根据以前手动切换的经验走到这一步基本上就稳了--因为这里本质上和一次普通主从切换已经没有区别了。

  4. 升级主机 A 内存配置,重启主机 A,执行以下 check:


    4.1 查看新 slave redis log 是否异常 4.2 使用 info keyspace 命令 check 新 master、新 slave 各 db key 数量是否一致,无异常 pass4.3 在新 master 写入测试 key,在新 slave 上 check 是否同步成功 4.4 观察依赖 server log 是否有异常

至此,若以上步骤都正常通过,一个完美的 redis 内存升级工作就完成了。

主从切换后数据丢失

结果正是没有想过可能会出问题的步骤 3 反而出现了问题,直接导致了主从切换后丢掉了部分数据,并且新 master 进入只读状态将近十分钟。

当时的情况是这样的:

在执行完步骤 3 后,check 新 slave redis log 无异常,正在考虑观察一会儿后执行主机 A 的升级重启操作,api 的分钟级别异常监控触发了一小波 redis 相关报警。第一反应在新 master 与新 slave 上执行了 info keyspace 查看 key 数量是否已经不一致,结果发现 master/slave 的 key 数量是一致的--但是再仔细一看:和切换前的 key 总数百万级相比切换后 key 总数降到了十万级--大部分 key 数据被丢失了。查看新 master、新 slave log 都没有发现明显 log 可以解释为什么主从切换后会丢失一大半数据这一现象,这时小伙伴第一次提到了是不是内存不够了,当时自己略一思考马上回复到:新 master 刚升级了内存,不可能内容扩大后反而内存不足的,所以应该不是这个问题。

n 分钟后...

小伙伴再一次提出了是不是 maxmemory 问题,这一下子点中了关键点,马上想到主机 B 升级了内存是不会有系统层面内存不足的问题,但是 redis 的内存使用实际上还会受到 maxmemory 参数限制,马上在新 master 上执行 config get maxmemory, 只有 3GB,而升级前数据实际使用内存超过了 6GB!立刻调大了新 master 的 maxmemory 参数,redis 很快恢复了可读写正常状态,一大波 redis 只读引发的告警通知开始快速下降。

原因定位

紧张又刺激的故障处理就这么过去了,在优先处理完丢失 key 数据恢复工作之后,开始回顾整理故障的详细原因,总共有如下几个疑问:

  1. 明确记得上个月给主机 A、B 上的 redis 都通过 config set maxmemory 设置为了 7GB,为什么出现问题时查询 B 上 redis 的 maxmemory 配置却变成了 3GB?

  2. 如果主机 B 的 maxmemory 是 3GB,其作为 slave 时为什么从 master 同步超过 6GB 的数据时不会有问题?--在主从切换前无论是查看 info keyspace 还是在 master 上写入测试 key 同步 check 都是 OK 的。

  3. 为什么主从切换后主机 B 上的 key 数据会丢失?这个是因为 maxmemory 设置过小,是故障的直接原因。

  4. 为什么新 master 由于 maxmemory 参数超限进入只读状态且删除部分数据后,新 master 中实际数据占用的大小依然超过>3GB?

如上四个疑问除了问题 3 已经明确了,剩下三个问题都让人疑惑--事出诡异必有妖,经过一番探寻得出其答案:

  1. 上个月修改 redis maxmemory 时,只通过 config set 命令修改了其运行时配置,而没有修改对应配置 redis.conf 上 maxmemory 的值,主机 B 上 redis 在重启后就会从 redis.conf 上载入该 maxmemory,该配置正是 3GB,同时 maxmemory 参数是 redis 节点独立的配置,slave 并不会从 master 同步该值。

  2. 在 redis5.0 版本之后,redis 引入了一个新的参数 replica-ignore-maxmemory,其官方文档定义如下:

Maxmemory on replicasBy default, a replica will ignore maxmemory (unless it is promoted to master after a failover or manually). It means that the eviction of keys will be handled by the master, sending the DEL commands to the replica as keys evict in the master side.This behavior ensures that masters and replicas stay consistent, which is usually what you want. However, if your replica is writable, or you want the replica to have a different memory setting, and you are sure all the writes performed to the replica are idempotent, then you may change this default (but be sure to understand what you are doing).Note that since the replica by default does not evict, it may end up using more memory than what is set via maxmemory (since there are certain buffers that may be larger on the replica, or data structures may sometimes take more memory and so forth). Make sure you monitor your replicas, and make sure they have enough memory to never hit a real out-of-memory condition before the master hits the configured maxmemory setting.To change this behavior, you can allow a replica to not ignore the maxmemory. The configuration directives to use is:replica-ignore-maxmemory no
复制代码

大意是 redis 作为 slave 时默认会无视 maxmemory 参数,这样可以保证主从的数据始终保持一致。当 master/slave 实际数据大小均小于其 maxmemory 设置时,这个参数没有任何影响,而这次丢失数据的原因正是因为主机 B 重启后作为 slave 时 maxmemory(3GB)小于实际数据大小(6GB+),此时 replica-ignore-maxmemory 默认开启保证作为 slave 时直接无视 maxmemory 的限制,而当执行 sentinel failover mastername 将主机 B 切换为新 master 后,新 master 不会受 replica-ignore-maxmemory 影响,发现自身 maxmemory<实际数据大小后直接开始主动淘汰 key,从而导致了数据丢失。3. 至于主机 B 作为 master 执行淘汰 key 策略并最终进入只读状态后,其实际数据大小依然>3GB 的原因,则是由于线上 redis 配置的策略是 volatile-lru 策略,该策略只会淘汰有过期时间的 key,对于不过期的 key 是不淘汰的。

总结

总的来看这次故障的根本原因还是个人对于 redis 的配置、操作经验不足,如果在调整运行时 maxmemory 时能做到以下二者之一,这次故障就不会出现了:

  1. 调整运行时 maxmemory 时同时调整配置文件 maxmemory 保持一致。

  2. 将配置文件 maxmemory 设置为 0--表示不限制内存使用。

正是因为对 redis 的认识和经验不足,没有想过到运行时配置与静态配置不一致可能导致的问题,这次不可避免的踩坑了。但是,作为一个本职 RD,半路接手基本靠自学的兼职运维,要考虑到 maxmemory 的运行配置与静态配置一致性问题好像也确实不是那么的理所当然。处理完这次故障后,特意在网上搜索了一番 redis 主从切换的注意事项、踩坑文章,想看看有没有人提到类似的坑,但是并无所获,难道这个坑真的没其他人踩(分享)过?陷入思考...

最后

如果有经验丰富的小伙伴看到这里,也欢迎不吝赐教指导一下 redis 主从的切换的各类常识与常见大坑!

用户头像

Java你猿哥

关注

一只在编程路上渐行渐远的程序猿 2023-03-09 加入

关注我,了解更多Java、架构、Spring等知识

评论

发布
暂无评论
记一次redis主从切换导致的数据丢失与陷入只读状态故障_redis_Java你猿哥_InfoQ写作社区