Redis最佳实践 | 青训营

94 阅读11分钟

redis已经成为十分常用且好用的中间件,但使用仍需要注意一些问题

Redis问题

  • 它的内存增长快——内存
  • Redis操作延迟变大——性能
  • 故障发生频率增多——高可靠
  • 运维需要注意什么——日常运维
  • 部署Redis的环境参数——资源规划
  • 监控Redis关注什么指标——监控与安全

方案

1 内存

  • Redis性能高是因为Redis是部署在内存上的数据库,所以访问Redis中数据速度很快
  • 相对于磁盘来说,内存是比较珍贵的,当业务增大时Redis数据会越来越多
  • 因此需要内存优化策略,防止因业务增长导致Redis占用的内存开始膨胀
    1. 控制key长度:保证 key 在简单、清晰的前提下,尽可能把 key 定义得短一些,节省过长的key内存内存 user:book:123优化成u:bk:123
    2. 避免存储大key:String 10KB以下,List/Hash/Set/Zset 元素数量控制在1w以下
    3. 选择业务合适的数据类型:String、Set尽量int类型(整数编码存储),Hash、ZSet存储元素数量控制在转换阈值以下,以压缩列表(ziplist)存储,节约内存,多数据才会转化成哈希表和跳表
    4. 把Redis当作缓存使用:尽可能设置过期时间,只保留经常访问的热数据,内存利用率比较高
    5. 实例设置maxmemory+淘汰策略
      • 控制内存上限,提前预估业务数据量,设置maxmemory控制上限,避免膨胀
        • volatile-lru / allkeys-lru:优先保留最近访问过的数据
        • volatile-lfu / allkeys-lfu:优先保留访问次数最频繁的数据(4.0+版本支持)
        • volatile-ttl :优先淘汰即将过期的数据
        • volatile-random / allkeys-random:随机淘汰数据
    6. 数据压缩后写入Redis:进一步优化,采用snappy、gzip等压缩算法,但需要权衡,客户端去解压缩会使得CPU资源消耗增多

2 高性能

  • 单机Redis可以达到10w的QPS,如果发生延迟情况,那就不对了,需要尽可能避免操作延迟
    1. 避免存储大key
      • 由于 Redis 处理请求是单线程的,在写入一个 bigkey 时,更多时间将消耗在「内存分配」上,同样在删除一个bigkey时,释放内存这个耗时更多,它们都会使得操作延迟
      • 同时有网络数据传输耗时增加的可能,导致后续请求排队
      • 若确实有需求读取,应当拆分成多个小key存储
    2. 开始lazy-free 机制
      • 4.0+支持,机制在删除一个bigkey时,主线程只删除bigkey,释放内存的耗时操作会放导后台去执行,最大程度避免对主线程的影响
      • 无法避免bigkey的情况开启
    3. 不使用复杂度过高的命令
      • 如 SORT、SINTER、SINTERSTORE、ZUNIONSTORE、ZINTERSTORE 等聚合类命令,建议放在客户端执行。
      • Redis是单线程模型处理请求,执行复杂度过高的命令会让其他请求等待,发生排队延迟,同时也会消耗更多的CPU资源
    4. 执行O(N)命令,要关注N的大小
      • 一次查询过多数据,传输过程耗时会很长,操作延迟增大
      • 在查询数据时,你要遵循以下原则:
        1. 先查询数据元素的数量(LLEN/HLEN/SCARD/ZCARD)
        2. 元素数量较少,可一次性查询全量数据
        3. 元素数量非常多,分批查询数据(LRANGE/HASCAN/SSCAN/ZSCAN)
      • 对于容器类型(List/Hash/Set/ZSet),在元素数量未知的情况下,一定不要无脑执行 LRANGE key 0 -1 / HGETALL / SMEMBERS / ZRANGE key 0 -1。
    5. 关注DEL时间复杂度
      • O(1)一般,但是不一定
      • string类型是O(1)
      • List/Hash/Zset/Set O(n),即删除一个key,元素个数越多del越慢
      • 删除大量元素时,需要依次回收每个元素的内存,越多越慢
      • 建议:分批删除
        • List:多次LPOP或RPOP,直到元素删除完
        • Hash/Set/ZSet:先执行HSCAN/SSCAN/SCAN查询元素,再执行 HDEL/SREM/ZREM 依次删除每个元素
    6. 批量命令代替单个命令
      • 可以显著减少客户端、服务端的来回网络 IO 次数
      • String / Hash 使用 MGET/MSET 替代 GET/SET,HMGET/HMSET 替代 HGET/HSET
      • 其它数据类型使用 Pipeline,打包一次性发送多个命令服务端执行
    7. 避免集中过期key
      • 清理按照采用定时 + 懒惰的方式在主线程执行
      • 存在大量key集中过期,在清理过期key时会有阻塞主线程的风险
      • 设置过期时间时,再增加一个随机时间,打散它们,降低集中过期的风险
    8. 使用长连接操作Redis,合理配置连接池
      • 每次建立TCP连接都需要经过三次握手、四次挥手,这个过程也相当耗时
      • 连接池访问Redis,设置合理的参数,长时间不操作Redis时,需及时释放资源
    9. 建议只用db0
    • 有16个db(默认)
      • 在一个连接上操作多个db数据,每次都需要先执行SELECT,会给Redis带来额外的压力

      • 多个db目的是按不同业务线存储数据,但其实可以拆分多个实例存储,拆分多个实例不但不会相互影响,还能提高Redis访问性能

      • Cluster只支持db0,后期想迁移到Cluster,成本高

    1. 读写分离+分片集群
    • 如果业务读请求量很大,那么可以采用部署多个从库的方式,实现读写分离,让 Redis 的从库分担读压力,进而提升性能。
    • image.png
    • 如果你的业务写请求量很大,单个 Redis 实例已无法支撑这么大的写流量,那么此时你需要使用分片集群,分担写压力。
    • image.png
    1. 不开启AOF或者AOF配置为每秒刷盘
    • 如果对于丢失数据不敏感的业务,建议不开启 AOF,避免 AOF 写磁盘拖慢 Redis 的性能。

    • 如果确实需要开启 AOF,建议配置为 appendfsync everysec,把数据持久化的刷盘操作,放到后台线程中去执行,尽量降低 Redis 写磁盘对性能的影响

    1. 使用物理机部署Redis
    • 持久化是创建子进程的方式进行,虚拟环境执行fork的耗时会大的多

    1. 关闭操作系统内存大页机制
      • Linux 操作系统提供了内存大页机制,其特点在于,每次应用程序向操作系统申请内存时,申请单位由之前的 4KB 变为了 2MB。
      • 我们想想持久化:当 Redis 在做数据持久化时,会先 fork 一个子进程,此时主进程和子进程共享相同的内存地址空间。
        • 当主进程需要修改现有数据时,会采用写时复制(Copy On Write)的方式进行操作,在这个过程中,需要重新申请内存
        • 如果申请内存单位变为了 2MB,那么势必会增加内存申请的耗时,如果此时主进程有大量写操作,需要修改原有的数据,那么在此期间,操作延迟就会变大。
      • image.png

3 可靠性

  • 关键在于持续性,从三大维度包装
    • 资源隔离、多副本、故障恢复
    1. 按业务线部署实例

      • 按不同业务线部署Redis实例,一个实例崩了不会影响其他 2.部署主从集群
      • 多副本实例
      • 主库宕机,从库仍然可以使用,避免了数据丢失风险,降低了服务不可用的时间
      • 主从库要分布到不同机器,且不要交叉部署
        • 主库会承担所有的读写流量,优先保证主库的稳定性,从库机器异常也不能影响到主库
        • 定时备份只在从库机器执行,消耗从库机器的资源,避免对主库的影响
    2. 合理配置主从复制参数

      • 参数不合理
        • 赋值中断
        • 从库发起全量复制导致主库性能收到影响
      • 设置合理的 repl-backlog 参数:过小的 repl-backlog 在写流量比较大的场景下,主从复制中断会引发全量复制数据的风险
      • 设置合理的 slave client-output-buffer-limit:当从库复制发生问题时,过小的 buffer 会导致从库缓冲区溢出,从而导致复制中断
    3. 部署哨兵模式,实现故障自动切换

      • 只部署了主从节点,但故障发生时是无法自动切换的,所以,你还需要部署哨兵集群,实现故障的自动切换
      • 多个哨兵节点需要分布在不同机器上,实例为奇数个,防止哨兵选举失败,影响切换时间。

4 安全

  • Redis被注入可执行脚本的问题,root权限问题
    • 不要把 Redis 部署在公网可访问的服务器上
    • 部署时不使用默认端口 6379
    • 以普通用户启动 Redis 进程,禁止 root 用户启动
    • 限制 Redis 配置文件的目录访问权限
    • 推荐开启密码认证
    • 禁用/重命名危险命令(KEYS/FLUSHALL/FLUSHDB/CONFIG/EVAL)

运维

  1. 禁止使用 KEYS/FLUSHALL/FLUSHDB 命令

    • 这些命令会长时间阻塞Redis主线程
    • 建议:SCAN替换成KEYS,在4.0+上可以使用FLUSHALL/FLUSHDB ASYNC,清空数据的操作放在后台线程执行
  2. 扫描线上实例时,设置休眠时间

    • 对实例做大key的统计分析或者SCAN扫描线上实例,都建议在扫描时设置休眠时间,防止扫描过程中实例中的OPS(每秒操作次数)过高对Redis产生性能抖动
  3. 慎用MONITOR命令

    • 排查问题会通过这个命令查看正在执行的命令
    • 但是如果OPS高,会导致Redis输出缓冲区的内存持续增长,内存资源严重消耗,可能会导致内存超过maxmemory而引发内存淘汰
  4. 从库必须设置为slave-read-only

    • 从库要避免写入数据,导致主从数据不一致
    • 4.0以下,从库若不是read-only,写入有过期时间的数据,不会做定时清理和释放内存
  5. 合理分配timeout和tcp-keepalive参数

    • 网络问题导致意外的连接中断,maxclients参数又很小,导致客户端无法于服务端建立新的连接(服务端认为超过了maxclients),原因是没建立一个连接,都会为客户端分配一个client fd,网络发生问题时,服务端并不会立即释放这个client fd
    • 内部有个定时任务,会定时检测所有client的空闲时间是否超过配置的timeout值
      • 若没有开启tcp-keepalive,直到timeout时间后才会清理释放client fd
    • 没有清理之前,如果还有大量新连接进来,会导致Redis内部持有的client fd超过了maxclients,新连接会被拒绝
    • 优化
      • 不要配置过高的timeout:让服务器端尽快把无效的client fd清理
      • Redis开启tcp-keepalive:服务端会定时给客户端发送TCP心跳包,检测连接连通性,当网络异常时,可以尽快清理僵尸client fd
  6. 调整maxmemory时,注意主从库的调整顺序

    • 5.0以下,从库内存超过maxmemory,会触发数据淘汰
      • 某些场景下从库可能更早达到maxmemory(MONITOR导致输出缓冲区占用大量内存),从库开始淘汰数据,主从库就会产生不一致
      • 主从库修改顺序
        • 调大maxmemory:先修改从库,再修改主库
        • 调小maxmemory:先修改主库,再修改从库
    • 5.0 增加了一个配置replica-ignore-maxmemory,默认从库超过maxmemory不会淘汰数据,就解决了这个问题

额外说明

  • 合理资源规划和完善监控预警非常重要

    • 首先保证CPU、内存、磁盘资源、带宽都要足够
    • 规划好容量,主库机器预留一半的内存空间,防止主从机器网络故障,引发大面积全量同步,导致主库机器内存不足的问题
    • 单个实例内存建议10G以下,大实例在主从全量同步、RDB备份时有阻塞风险
  • 监控

    • 资源不足要及时抱紧
    • 设置合理的slowlog阈值,slowlog过多时要报警
    • 监控组件才气Redis INFO信息时要用长连接,避免频繁的短连接
    • 做好实例运行时监控,重点关注expired_keys、evicted_keys、latest_fork_usec指标,这些指标短时突增可能有阻塞风险

总结

  • 知识点很多,但都很重要
  • 加油写文章吧