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 会重启 Redis
    • config: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 进行相应的处理和报警,这种方式是比较推荐的方式。

如何删除

  • 首先,无论是什么数据结构,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,无法实现规模化运维统计。
  • 代理端
    • 像 Twemproxy、Codis 这些基于代理的 Redis 分布式架构,所有客户端的请求都是通过代理端完成的。此架构是最适合做热点 key 统计的,因为代理是所有 Redis 客户端和服务端的桥梁。但并不是所有Redis都是采用此种架构。
  • Redis 服务端
    • 使用 monitor 命令统计热点 key 是很多开发和运维人员首先想到,monitor 命令可以监控到 Redis 执行的所有命令
    • 利用 monitor 命令就可以统计出一段时间内的热点 key 排行榜、命令排行榜、客户端分布等数据。
    • 但是此种方法会有两个问题:
      • monitor 命令在高并发条件下,会存在内存暴增和影响 Redis 性能的隐患,所以此种方法适合在短时间内使用。
      • 只能统计一个 Redis 节点的热点 key,对于 Redis 集群需要进行汇总统计。
  • 机器
    • Redis 客户端使用 TCP 协议与服务端进行交互,通信协议采用的是 RESP。如果站在机器的角度,可以通过对机器上所有 Redis 端口的 TCP 数据包进行抓取完成热点 key 的统计

解决热点 key 问题

  • 拆分复杂数据结构:如果当前 key 的类型是一个二级数据结构,例如哈希类型。如果该哈希元素个数较多,可以考虑将当前 hash 进行拆分,这样该热点 key 可以拆分为若干个新的 key 分布到不同 Redis 节点上,从而减轻压力。
  • 迁移热点key:以 Redis Cluster 为例,可以将热点 key 所在的 slot 单独迁移到一个新的 Redis 节点上,但此操作会增加运维成本。
  • 本地缓存加通知机制:可以将热点 key 放在业务端的本地缓存中,因为是在业务端的本地内存中,处理能力要高出 Redis 数十倍,但当数据更新时,此种模式会造成各个业务端和 Redis 数据不一致,通常会使用发布订阅机制来解决类似问题。

来源:《Redis 开发与运维》第 12 章 开发运维的“陷阱”

以上是关于Redis 开发与运维开发运维的“陷阱”的主要内容,如果未能解决你的问题,请参考以下文章

Redis 开发与运维开发运维的“陷阱”

Redis 开发与运维Redis Sentinel 哨兵

Redis 开发与运维Redis Sentinel 哨兵

Redis开发与运维

《Redis开发与运维》

Redis 开发与运维复制