HiSEN

Redis 开发与运维

零、背景

鉴于目前 Redis 使用广泛
虽然数据结构、API 比较简单
但是想使用好,还是有一定难度
建议了解下《Redis 开发与运维》
以达到知其然且知其所以然的境界

一、使用场景

大规模互联网在线应用
流量比较大,响应时间要求高
Redis 作为一款流行的高性能数据库

对于读多写少的场景,一般作为缓存使用
某大型电商订单系统读写比大概在 10:1
并且订单读取 90% 的流量都是创建订单当天查询

对于可容忍丢数据,但是对性能有极致要求,
比如优惠券发放,流量高,这种情况下当做 DB 用也挺好。

二、问题与建议

2.1 常见问题

  • 网络抖动导致 redis 操作失败
  • 定时任务清理过期 key 导致 IOPS 高
    • redis 做了二次开发,更激进的惰性删除( 针对大面积过期场景 )
  • 热 key 导致流量倾斜
    • 考虑 Server 端旁路监听,做统计,然后推送到客户端做内存缓存
    • 京东有开源热 Key 方案
  • redis slot 分配不均匀,导致某节点提前内存告警
    • 建议分配内存按 slot 分配,而不是节点

2.2 建议

  • 缓存时间动态可配
    • 上线后根据业务灵活调整,节约资源
    • 大促过程中,扩容来不及,可以根据趋势缩短缓存时间,避免淘汰
  • 使用 ProtoStuff 之类的高效序列化工具
  • 使用 snappy 等高效压缩算法

三、阅读摘抄

《Redis开发与运维》20 1121~1122

dbsize 获取当前数据库中键总数,复杂度 O(1)

keys * 复杂度 O(n)

type key 返回 redis 外部数据结构

object encoding key 返回 redis 内部编码实现

redis 为单线程模型,所有命令都进队列。

redis 单线程为何还这么快?

  1. 纯内存访问,内存响应约 100 ns,快的基础
  2. 非阻塞 I/O,事件驱动
  3. 单线程避免了线程切换和竞态产生的消耗

锁和线程切换通常是性能杀手。

单线程可以简化数据结构和算法。

由于是单线程,所以对每个命令执行时间有要求。
redis 是面向快速执行场景的数据库。

mget 结果是按照传入键的顺序返回。

redis 中每个中文占 3 个字节
随说:utf-8 汉字 3 ,ascii unicode 2

字符串类型的内部编码有 3 种:

  1. int:8 字节的长整形
  2. embstr:≤ 39 字节的字符串
  3. raw:> 39 字节的字符串

list 的 lrange 操作获取指定索引范围的元素。

  • 索引下标从左到右是 0~N-1
  • 索引下标从右到左是 -1~-N
  • 并且 end 包含了自身,很多编程语言不包含

对于字符串类型的键,再次执行 set 命令会将上次的过期时间去掉。

纯内存存储、I/O多路复用技术、单线程架构是 redis 高性能的三个因素。

了解每个命令的时间复杂度在

redis 命令真正执行的时间通常在微妙级别,所以才会有 redis 性能瓶颈是网络的说法。

原生批命令与 pipeline 的区别:

  1. 原生批命令是原子的,pipeline 是非原子的
  2. 原生一命令多 key,pipeline 支持多命令
  3. 原生是 redis 服务端实现,pipeline 客户端+服务端 实现

pipeline 只能操作一个 redis 实例,但即使在分布式 redis 场景中,也可以作为批量操作的重要优化手段。

redis 3.2 版本提供了 geo 功能,支持 LBS 服务。
geo 功能是 redis 借鉴了 ardb 功能实现,ardb 的作者来自中国。

输出缓冲区由两部分组成:

  1. 固定缓冲区 16KB,返回比较小的执行结果
  2. 动态缓冲区,列表实现,返回较大的结果

RESP(Redis Serialization Protocol Redis),协议。
保证客户端与服务端的正常通信,是各种编程语言开发客户端的基础。

jedis 没有内置序列化工具,需要自己选用。
page 121 有使用 protostuff (protobuf的Java客户端)进行操作的例子。
随说:这块 21 年出实际场景测试过,时间、空间复杂度优化幅度都蛮大

理解 redis 通信原理和建立完善的监控系统对快速定位客户端常见问题非常有帮助。

redis 默认采用 LZF 算法对生成的 RDB 文件做压缩处理,压缩后的文件远远小于内存大小。

正常情况下,fork 操作耗时应该是每 GB 消耗 20 毫秒左右。

redis 是 CPU 密集型服务,不要做绑定单核 CPU 操作。
由于子进程非常消耗 CPU,会和父进程产生单核资源竞争。

子进程通过 fork 操作产生,理论上需要两倍的内存来完成持久化,
但 Linux 有写时复制机制(copy-on-write),
父子进程会共享相同的物理内存页,
当父进程处理写请求时会把要修改的页创建副本,
而子进程在 fork 操作过程中共享整个父进程内存快照。

复制功能是高可用 redis 的基础。

主从复制延迟,redis 提供了 repl-disable-tcp-nodelay 参数控制是否关闭,
TCP_NODELAY,默认为 no,即开启 TCP_NODELAY 功能,
这时主节点会合并较小的 TCP 数据包节省带宽但是增大了主从之间的延迟,
适用于跨机房部署。如果为 yes,那么会立即发送,适合同机房网络。

对于写并发量较高的场景,
多个从节点会导致主节点写命令的多次发送从而过度消耗网络带宽,
同时也加重了主节点的负载影响服务稳定性。

psync 2.8 以上支持,有全量复制,部分复制
需要组件支持:

  1. 主从节点各自复制偏移量
  2. 主节点复制积压缓冲区
  3. 主节点运行 id

redis 支持无盘复制,repl-diskless-sync,试验阶段,
rdb 文件不保存到硬盘而直接通过网络发送给从节点。

为了降低主从延迟,
一般把 redis 主从部署在同机房 / 同城机房,
避免网络延迟和网络分区造成的心跳中断等情况。

对于读写分离,会造成从节点数据延迟,
可以编写外部监控程序监听主从节点的复制偏移量,
当延迟较大时出发报警或者通知客户端避免读取延迟过高的从节点。

为了保证复制一致性,从节点自身永远不会主动删除超时数据。

建议做读写分离之前,
可以考虑使用 redis cluster 等分布式解决方案,
这样不止扩展了读性能还可以扩展写性能和可支撑数据规模,
并且一致性和故障转移也可以得到保证,
对客户端的维护逻辑也相对容易。读写分离成本太高

redis-cli –bigkeys
把历史扫描过的最大对象统计出来,分析优化

带宽瓶颈通常出现在以下几个方面:

  1. 机器网卡带宽
  2. 机架交换机带宽
  3. 房之间专线带宽

网络快慢:同物理机 > 同机架 > 跨机架 > 同机房 > 同城机房 > 异地机房
随说:但它们的容灾性正好相反

redis 进程的内存消耗主要包括:自身内存 + 对象内存 + 缓冲内存 + 内存碎片。

对象内存是 redis 内存占用最大的一块,存储着用户所有的数据。

输入输出缓冲区在大流量的场景中容易失控,造成 redis 内存的不稳定,需要重点关注。

由于进程内保存大量的键,
维护每个键精准的过期删除机制会导致消耗大量的 CPU,
对于单线程的 redis 来说成本过高,
因此 redis 采用惰性删除和定时任务删除机制实现过期键的内存回收。

在高并发放场景下,建议字符串长度控制在 39 字节以内,
以减少 redisObject 内存分配次数,从而提高性能。

尽量减少字符串频繁修改操作如 append、setrange,
改为直接使用 set 修改字符串,降低预分配带来的内存浪费和内存碎片化。

redis sentinel 有一套合理的监控机来判断节点不可达,有三个定时任务:

  1. 每 10s 向主节点发送 info 信息获取最新拓扑
  2. 每 2s 向 sentinel 频道发送对主节点的判断以及自身的信息
  3. 每 1s sentinel 向主节点、从节点、其余 sentinel 发送 ping 做心跳检测,来确认节点是否可达。

redis 使用 raft 算法实现领导者的选举。

redis sentinel 是 redis 的高可用方案实现:故障发现、故障自动转移、配置中心、客户端通知

redis sentinel 模式下,客户端初始化连接的是 sentinel 节点集合,
不再是具体的 redis 节点,但 sentinel 只是配置中心不是代理。

redis cluster 是 redis 的分布式解决方案。当遇到单机内存、并发、流量等瓶颈时,
可以采用 cluster 架构方案达到负载均衡的目的。

redis cluster 之前的分布式方案:

  1. 客户端分片,客户端比较重,需要处理路由、高可用、故障转移等问题。
  2. 代理方案,部署麻烦,性能损耗。

数据分区是分布式存储的核心,
理解和灵活运用数据分区规则对于掌握 redis cluster 非常有帮助。

redis cluster 限制:

  1. 批量操作支持有限,目前 mget、mset 只支持具有相同 slot 值的 key 执行批量操作。
  2. key 事务操作支持有限
  3. key为数据分区最小力度,hash list 在单机
  4. 不支持多数据库空间
  5. 复制结构只支持一层,不支持嵌套树状

在分布式存储中需要提供维护节点元数据信息的机制,
所谓元数据是指:节点负责哪些数据,是否出现故障等状态信息。

常见的元数据维护方式分为:集中式、P2P方式

redis cluster 采用 P2P 的 Gossip(流言) 协议,
Gossip 协议工作原理就是节点彼此不断通信交换信息,
一段时间之后所有节点都会知道集群完整信息。

虽然 Gossip 协议的信息交换机制具有天然的分布式特性,但他是有成本的。
redis 集群内节点通信频率为每秒 10次。单次通信消息 > 2KB,
消息体携带的数据量根集群节点数息息相关,更大的集群代表更大的通信成本。
因此对于 redis 集群来说并不是大而全的集群更好,需要对集群的规模做限制。
随说:大规模集群可以考虑使用哨兵模式、或者拆分 redis cluster

集群内 Gossip 消息通信本身会消耗带宽,官方建议集群最大规模在 1000 以内。

redis cluster 可以实现对节点的灵活上下线控制。
其中原理可以抽象为槽和对应数据在不同节点之间的灵活移动。

集群伸缩=槽和数据在节点之间移动。

MOVED 重定向:在集群模式下 redis 接收 key 相关命令先计算对应的槽,再根据槽找到节点,
如果节点是自身,则处理命令,否则返回 MOVED 重定向错误,通知客户端请求正确的节点。

键使用大括号包含的内容又叫做 hash_tag,它提供不同的键可以具备相同的 slot 功能,
常用于 redis I/O 优化。在集群模式下实现 mget mset pipeline 等操作。

smart 客户端通过在内部维护
slot → node 的映射关系,本地就可以实现
key → node 的查找,从而保证 I/O 效率低最大化,而 MOVED 重定向负责协助 smart 客户端更新 slot → node 映射。

jediscluster 解析 cluster slots 结果缓存到本地,为每个节点创建唯一的 jedispool 连接池。

cluster slots 风暴:

  1. 重试机制导致 I/O 通信放大问题
  2. 个别节点异常导致频繁获取 slots 信息
  3. 频繁触发更新本地 slots 缓存操作,内部用了写锁,阻塞对集群所有的命令调用。

针对以上问题,jedis 2.8.2 版本做了改进:

  1. 当接收到 JedisConnectionException 时不再轻易初始化 slots 缓存,大幅降低内部 I/O 次数。
  2. 当更新 slots 缓存时,不再使用 ping 检测节点活跃度,并且使用 redis covering 变量保证同一时刻只有一个线程更新 slots 缓存,其它线程忽略,优化了写锁阻塞和 cluster slots 调用次数

jedis cluster 中由于 key 分布在不同的节点上,会造成无法实现 mget、mset 等功能。
但是可以利用 CRC16 计算出 key → slot,以及 smart 客户端保存 slot → node 的特性,
将属于同一个 redis 节点的 key 进行归类,
然后分别对每个节点对应的子 key 列表执行 mget 或者 pipeline 操作。

ASK 与 MOVED 都是对客户端重定向控制,ASK 表示集群正在进行 slot 迁移,
不知道什么时候完成。MOVED 明确表示 slot 对应的节点,因此需要更新 slot 缓存。

配置纪元的应用场景有:

  1. 新节点加入
  2. 槽节点映射冲突检测
  3. 从节点投票选举冲突检测

配置纪元的主要作用:

  1. 标示集群内每个主节点的不同版本和当前集群最大版本。
  2. 每次集群发生重要事件,都会递增纪元
  3. 大的纪元表示更新的状态

唯品会开发的 redis-migrate-tool 支持在线迁移,
采用多线程加速,提供数据校验和查看迁移状态等功能。
随说:在 github 开源了

缓存更新策略最佳实践

  1. 低一致性业务建议配置最大内存和淘汰策略
  2. 高一致性业务结合 expire 和主动更新

缓存粒度问题

  1. 全部字段缓存通用易维护,报文大性能低
  2. 部分字段缓存通用性低,维护复杂,高效
    随说:考虑使用 hash 全量存,按需取

使用布隆过滤器减少缓存击穿,page 351
随说:订单业务很难做到,特别是订单量大

无底洞:投入越多不一定产出越多

  1. 客户端一次批量操作会涉及多次网络开销,节点越多越耗时
  2. 网络连接数变多,对性能也会有影响

无底洞优化

  1. 命令本身的优化,例如优化 SQL 语句
  2. 减少网络通信次数
  3. 降低接入成本,使用连接池 / NIO 等
  4. 客户端 n 次 get,n 次网络 + n 次 get 命令
  5. 客户端 1 次 pipeline get,1 网络 + n get
  6. 客户端 1 次 mget,1 网络 + 1 mget
    随说:以上针对单个 redis 节点,非 cluster

NTP 是一种保证不同时钟一致性的服务。

redis cluster | sentinel 如果多个节点时间不一致,
会影响日志排查,但不会影响功能,节点依赖各自的时钟。

redis 公网无密码情况下,黑客可以写入公钥,通过设置 rdb 文件目录到 /root/.ssh 目录,
并且文件名设置为 authorized_keys 即可实现免密登录。爆破主机。

redis bind 参数指定的是 redis 和哪个网卡绑定(建议绑定内网),和客户端什么网段没有关系。

bigkey,一般字符串类型 value 超过 10KB 就是 bigkey 了,但这个值和具体的 OPS 相关。

redis 查看 key 的大小:debug object key
看其中的 serializedlength 即可。