-
Notifications
You must be signed in to change notification settings - Fork 82
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 集群的客户端必须要解决的问题包括
- 计算 key 的槽位
- 通过与集群的通信获知槽位分布
- 根据 key 的槽位选择集群节点并传输指令
- 在节点返回
MOVED
等错误时能够重复步骤 2 并再次发送指令 - 在主从切换时能重复步骤 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
)