Skip to content

Redis 集群代理基本原理与使用

Neuron Teckid edited this page Feb 11, 2015 · 4 revisions

Redis 集群的特性与问题

在 Redis 3.0 中支持的集群功能面向的基本存储单位是一个槽 (slot). 一个集群会含有总计 2^14=16384 个槽位. 每个槽位会被集群中的唯一一个 master 节点所控制.

向 Redis 发送的任何含 key 指令 (包括单 key 指令如 get, set, llen, hset, hmset 等, 以及多 key 指令 mget, mset, rename, rpoplpush 等) 时, 须先对 key 计算一次 hash (具体算法为 crc16 对总槽位数取模) 作为其槽位编号, 将指令发送给对应槽位的持有节点. 如果指令发送到了错误的节点, 该节点并不会处理请求, 而是会返回标识为 MOVED 的错误信息.

当 Redis 集群发生扩容时, 会将现有节点上的槽迁移到新增节点上, 实现数据负载均衡, 如此一来, 槽位分布在集群中可能并不是固定的. 并且当集群发生扩容 (或缩减) 时, 正在迁移的槽位会临时锁定无法读写, 直到该槽位的数据全部迁移到新节点上时才会恢复可访问性.

另外, 在集群中一个主节点瘫痪时 (可能是进程意外退出, 或网络无法连接等), 集群可以选举其一个从节点替换该主节点 (需要除瘫痪的主节点外至少另有 2 个主节点). 这时也需要修改数据路由.

综上, 对于 Redis 集群的客户端必须要解决的问题包括

  1. 计算 key 的槽位
  2. 通过与集群的通信获知槽位分布
  3. 根据 key 的槽位选择集群节点并传输指令
  4. 在节点返回 MOVED 等错误时能够重复步骤 2 并再次发送指令
  5. 在主从切换时能重复步骤 2 并再次发送指令

代理方案

由于以上问题, 以及目前各语言的 Redis 库对这些问题的解决参差不齐, 所以决定使用构建一个通用代理的方式一并处理.

通过代理方式, 所有应用程序以传统 Redis 连接方式连至代理, 然后代理计算槽位, 分发和重试.

目前已经解决的问题包括

  • 获取并计算出槽位-节点映射表
  • 所有单 key 的请求会正确计算槽位并分发至对应节点
  • 当节点返回 MOVED, ASK, CLUSTERDOWN 时, 或当节点关闭连接时 (可认为是节点瘫痪) 时, 请求会被暂存入缓存区, 然后, 代理会同时发起更新槽位分布的请求, 待此请求完毕后, 尝试依次重试缓存区的请求

目前绕过的问题

  • mset, mget 要求所有的 key 均在统一槽位, 但在随机情况下这几乎是不可能的. 因此, mset, mget 被拆分为了多个请求, 以管道方式分别发往对应的节点, 在所有节点全部返回结果后, 再汇总返回给客户端. 当 key 有多个时, 原子性失效.
  • rename 要求原 key 和改名后的 key 在同一槽位中. 如果经计算指令中两者确实在同一槽位, 那么将此请求直接转发至对应的节点, 此时保证原子性; 否则, 会先向原 key 所在的节点发送 get 指令获取值, 然后向目标 key 所在的节点发送 set 设定值. 如果这一步没有错误, 则继续 del 原 key, 此过程原子性失效.

目前没有解决的

  • rpoplpush, eval 等脚本系列的指令, 事务系列的指令没有实现

其他操作的实现

订阅长连接

订阅连接在代理中单独处理.

目前支持 [p]subscribe.

当客户段发送一个订阅指令后, 代理不再读取客户端传来的数据, 而是将此连接转换为长连接. 并且, 订阅长连接过程中不再对客户端发送的任何消息进行响应. 只有客户端主动关闭连接, 或服务器关闭了订阅连接, 此连接才会被回收.

一旦服务器出错或瘫痪, 此连接即中断. 不再重试.

订阅指令的参数并不是 key, 因此订阅并不是根据参数定向到对应的节点, 而是随机选择节点.

发布

目前支持 publish.

发布指令的参数并不是 key, 因此发布并不是根据参数定向到对应的节点, 而是随机选择节点转发指令.

屏蔽的操作

以下操作被屏蔽

  • info 若统计整个集群的信息, 代理的功能变得较为复杂, 因此暂不支持
  • cluster 由于代理可以认为是一个非集群 Redis 程序, 因此不支持特有集群指令
  • keys 过于复杂且影响效率
  • [Z|F]INTER 等集合操作, 若操作 key 不在同一个槽位甚至不在一个节点则可能需要代理来进行集合操作, 增加复杂性
  • RENAMENX 难以保证原子性
  • B[?]POP 系列暂不支持, 也许以后会支持
  • 其他非数据操作

代理信息

  • proxy 指令, 可以获取代理信息; 包括线程数 (threads:X), 每个线程的客户端连接数 (clients_count:X,Y,...), 每个线程的缓冲区分配字节数 (mem_buffer_alloc:X,Y,...), 代理程序版本 (version:X.Y.Z-yyyy-mm-dd)