Redis 开发与运维开发运维的“陷阱”
Posted 木兮同学
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis 开发与运维开发运维的“陷阱”相关的知识,希望对你有一定的参考价值。
文章目录
一、Linux 配置优化
内存分配控制
- Redis 设置合理的 maxmemory,保证机器有 20% ~ 30% 的闲置内存。
- 集中化管理 AOF 重写和 RDB 的 bgsave。
- 设置 vm.overcommit_memory=1,防止极端情况下会造成 fork 失败。
OOM killer
- OOM killer 会在可用内存不足时选择性地杀掉用户进程。
- 对于 Redis 所在的服务器来说,可以
将所有 Redis 的 oom_adj 设置为最低值或者稍小的值
,降低被 OOM killer 杀掉的概率。
使用 NTP
- NTP(Network Time Protocol,网络时间协议)是一种
保证不同机器时钟一致性
的服务。一般公司里都会有 NTP 服务用来提供标准时间服务,从而达到纠正时钟的效果。
二、flushall / flushdb 误操作
- Redis 的 flushall / flushdb 命令可以做数据清除,对于 Redis 的开发和运维人员有一定帮助,然而一旦误操作,它的破坏性也是很明显的。
缓存与存储
- 被误操作 flush 后,根据当前 Redis 是缓存还是存储使用策略有所不同:
- 缓存:对于业务数据的正确性可能造成损失还小一点,因为缓存中的数据可以从数据源重新进行构建。
- 存储:对业务方可能会造成巨大的影响,如果没有提前做业务降级,那么最终反馈到用户的应用可能就是报错或者空白页面等。
借助 AOF 机制恢复
- Redis 执行了 flush 操作后,AOF 持久化文件会受到什么影响呢,如下:
appendonly no
:对 AOF 持久化没有任何影响,因为根本不存在 AOF 文件。appendonly yes
:只不过在 AOF 文件中追加了一条 flushall 记录。
- 也就是说,虽然 Redis 中的数据被清掉了,但是
AOF 文件还保存着 flush 操作之前完整的数据
,这对恢复数据是很有帮助的。
RDB 有什么变化
Redis 执行了 flushall 操作后,RDB持久化文件会受到什么影响呢?
- 1)如果
没有开启 RDB 的自动策略
,也就是配置文件中没有类似如下配置
save 900 1
save 300 10
save 60 10000
-
那么除非手动执行过 save、bgsave 或者发生了主从的全量复制,否则 RDB 文件也会保存 flush 操作之前的数据,可以作为恢复数据的数据源。注意问题如下:
- 防止手动执行 save、bgsave,如果此时执行 save、bgsave,新的 RDB 文件就不会包含 flush 操作之前的数据,被老的RDB文件进行覆盖。
- RDB 文件中的数据可能没有 AOF 实时性高,也就是说,RDB 文件很可能很久以前主从全量复制生成的,或者之前用 save、bgsave 备份的。
-
2)如果
开启了 RDB 的自动策略
,由于 flush 涉及键值数量较多,RDB 文件会被清除意味着使用 RDB 恢复基本无望。
三、安全的 Redis
- 数据丢失对于很多 Redis 的开发者来说是致命的,经过相关机构的调查发现,被攻击的 Redis 有如下特点:
- Redis 所在的机器有外网 IP
- Redis 以默认端口 6379 为启动端口,并且是对外网开放的
- Redis 以 root 用户启动的 Redis 没有设置密码
- Redis 的 bind 设置为 0.0.0.0 或者 “”
- 将从下面几个方面介绍如何保证 Redis 的安全。
Redis 密码机制
- Redis 提供了 requirepass 配置为 Redis 提供密码功能,如果添加这个配置,客户端就不能通过
redis-cli -h {ip} -p {port}
来执行命令。
伪装危险命令
- Redis 中包含了很多“危险”命令,一旦生产上错误使用或者误操作,后果不堪设想,例如:
keys
:如果键值较多,存在阻塞 Redis 的可能性flushall/flushdb
:数据全部被清除save
:如果键值较多,存在阻塞 Redis 的可能性debug
:个例如 debug reload 会重启 Redisconfig
:config 应该交给管理员使用shutdown
:停止 Redis
- Redis 提供了
rename-command
配置解决了这个问题,例如添加如下配置:
rename-command flushall abcabcabc
- 那么再执行 flushall 的话,会收到 Redis 不认识 flushall 的错误提示,说明成功的对 flushall 进行了伪装。
- 而如果执行 abcabcabc ,那么就可以实现 flushall 功能了。
防火墙
- 可以使用防火墙限制输入和输出的 IP 或者 IP 范围、端口或者端口范围。
定期备份数据
- 定期备份数据能够在一定程度挽回一些损失,定期备份持久化数据是一个比较好的习惯。
不使用默认端口
- Redis 的默认端口是 6379,不使用默认端口从一定程度上可降低被入侵者发现的可能性,因为入侵者通常本身也是一些攻击程序。
使用非 root 用户启动
- root 用户作为管理员,权限非常大。如果被入侵者获取 root 权限后,就可以在这台机器以及相关机器所以操作。所以建议在启动 Redis 服务的时候使用非 root 用户启动。
四、处理 bigkey
bigkey 是指 key 对应的 value 所占的内存空间比较大
,例如一个字符串类型的 value 可以最大存到 512MB,一个列表类型的 value 最多可以存储 2^32 - 1 个元素。如果按照数据结构来细分的话,一般分为两种:- 字符串类型:体现在
单个 value 值很大
,一般认为超过 10 KB 就是 bigkey,但这个值和具体的 OPS 相关。 - 非字符串类型:哈希、列表、集合、有序集合,体现在
元素个数过多
。
- 字符串类型:体现在
bigkey 的危害
- 内存空间不均匀:例如在 Redis 集群中,bigkey 会造成节点的内存空间使用不均匀。
- 超时阻塞:由于 Redis 单线程的特性,操作 bigkey 比较耗时,也就意味着阻塞 Redis 可能性增大。
- 网络阻塞:每次获取 bigkey 产生的网络流量较大。
如何发现
redis-cli --bigkeys
可以命令统计 bigkey 的分布。但是生产环境中,开发和运维人员更希望自己可以定义 bigkey 的大小而且更希望找到真正的 bigkey 都有哪些
,这样才可以去定位、解决、优化问题。- 判断一个 key 是否为 bigkey,只需要
执行 debug object key 查看 serializedlength 属性
即可,它表示 key 对应的 value 序列化之后的字节数。 - 在实际的生产环境中发现 bigkey 的两种方式如下:
- 被动收集:许多开发人员确实可能对 bigkey 不了解或重视程度不够,但是这种 bigkey 一旦大量访问,很可能就会带来命令慢查询和网卡跑满问题,开发人员
通过对异常的分析通常能找到异常原因可能是 bigkey
,这种方式并不推荐,但是在实际生产环境中却大量存在,建议修改 Redis 客户端,当抛出异常时打印出所操作的 key,方便排查 bigkey 问题。 - 主动检测:
scan + debug object
,如果怀疑存在bigkey,可以使用scan命令渐进的扫描出所有的key,分别计算每个 key 的 serializedlength,找到对应 bigkey 进行相应的处理和报警,这种方式是比较推荐的方式。
- 被动收集:许多开发人员确实可能对 bigkey 不了解或重视程度不够,但是这种 bigkey 一旦大量访问,很可能就会带来命令慢查询和网卡跑满问题,开发人员
如何删除
- 首先,无论是什么数据结构,del 命令都能将其删除。但是经过上面的分析你一定不会这么做,
删除 bigkey 通常来说会阻塞 Redis 服务
。 - 这个时候就需要 scan 命令的若干类似命令拿出来:
sscan、hscan、zscan
。 - string
- 对于 string 类型使用 del 命令一般不会产生阻塞
- hash、list、set、sorted set
- 下面以 hash 为例子,使用 hscan 命令,每次获取部分(例如 100 个)field-value,再利用 hdel 删除每个 field(为了快速可以使用 Pipeline):
public void delBigHash(String bigKey) { // 游标 Jedis jedis = new Jedis("127.0.0.1", 6379); String cursor = "0"; while (true) { ScanResult<Map.Entry<String, String>> scanResult = jedis.hscan(bigKey, cursor, new ScanParams().count(100)); // 每次扫描后获取新的游标 cursor = scanResult.getStringCursor(); // 获取扫描结果 List<Map.Entry<String, String>> list = scanResult.getResult(); if (list == null || list.size() == 0) { continue; } String[] fields = getFieldsFrom(list); // 删除多个field jedis.hdel(bigkey, fields); // 游标为0时停止 if (cursor.equals("0")) { break; } } //最终删除key jedis.del(bigKey); } /** * 获取 field 数组 * @param list * @return */ private String[] getFieldsFrom(List<Map.Entry<String, String>> list) { List<String> fields = new ArrayList<>(); for (Map.Entry<String, String> entry : list) { fields.add(entry.getKey()); } return fields.toArray(new String[fields.size()]); }
- Redis 4.0 新增了非常实用的
惰性删除 lazy free 特性
,从根本上解决了 bigkey(主要指定元素较多集合类型key)删除的风险
五、寻找热点 key
热门新闻事件或商品通常会给系统带来巨大的流量,对存储这类信息的 Redis 来说却是一个巨大的挑战。以 Redis Cluster 为例,它会造成整体流量的不均衡,个别节点出现 OPS 过大的情况,极端情况下热点 key 甚至会超过 Redis 本身能够承受的 OPS,因此寻找热点 key 对于开发和运维人员非常重要。下面从以下几个方面分析热点 key。
统计热点 key
- 客户端
- 客户端其实是距离 key “最近” 的地方,因为 Redis 命令就是从客户端发出的,例如
在客户端设置全局字典(key 和调用次数)
,每次调用 Redis 命令时,使用这个字典进行记录。 - 使用客户端进行热点 key 的统计非常容易实现,但同时问题也非常多:
- 无法预知 key 的个数,存在内存泄露的危险。
- 对于客户端代码有侵入,各个语言的客户端都需要维护此逻辑,维护成本较高。
- 只能了解当前客户端的热点 key,无法实现规模化运维统计。
- 客户端其实是距离 key “最近” 的地方,因为 Redis 命令就是从客户端发出的,例如
- 代理端
- 像 Twemproxy、Codis 这些基于代理的 Redis 分布式架构,所有客户端的请求都是通过代理端完成的。此架构是最适合做热点 key 统计的,因为
代理是所有 Redis 客户端和服务端的桥梁
。但并不是所有Redis都是采用此种架构。
- 像 Twemproxy、Codis 这些基于代理的 Redis 分布式架构,所有客户端的请求都是通过代理端完成的。此架构是最适合做热点 key 统计的,因为
- Redis 服务端
- 使用 monitor 命令统计热点 key 是很多开发和运维人员首先想到,
monitor 命令可以监控到 Redis 执行的所有命令
。 - 利用 monitor 命令就可以统计出一段时间内的热点 key 排行榜、命令排行榜、客户端分布等数据。
- 但是此种方法会有两个问题:
- monitor 命令在高并发条件下,会存在
内存暴增和影响 Redis 性能
的隐患,所以此种方法适合在短时间内使用。 只能统计一个 Redis 节点的热点 key
,对于 Redis 集群需要进行汇总统计。
- monitor 命令在高并发条件下,会存在
- 使用 monitor 命令统计热点 key 是很多开发和运维人员首先想到,
- 机器
- Redis 客户端使用 TCP 协议与服务端进行交互,通信协议采用的是 RESP。如果站在机器的角度,可以通过
对机器上所有 Redis 端口的 TCP 数据包进行抓取完成热点 key 的统计
。
- Redis 客户端使用 TCP 协议与服务端进行交互,通信协议采用的是 RESP。如果站在机器的角度,可以通过
解决热点 key 问题
拆分复杂数据结构
:如果当前 key 的类型是一个二级数据结构,例如哈希类型。如果该哈希元素个数较多,可以考虑将当前 hash 进行拆分,这样该热点 key 可以拆分为若干个新的 key 分布到不同 Redis 节点上,从而减轻压力。迁移热点key
:以 Redis Cluster 为例,可以将热点 key 所在的 slot 单独迁移到一个新的 Redis 节点上,但此操作会增加运维成本。本地缓存加通知机制
:可以将热点 key 放在业务端的本地缓存中,因为是在业务端的本地内存中,处理能力要高出 Redis 数十倍,但当数据更新时,此种模式会造成各个业务端和 Redis 数据不一致,通常会使用发布订阅机制来解决类似问题。
来源:《Redis 开发与运维》第 12 章 开发运维的“陷阱”
以上是关于Redis 开发与运维开发运维的“陷阱”的主要内容,如果未能解决你的问题,请参考以下文章