## Redis Cluster 探索与思考
文 / 张冬洪
### Redis Cluster 的基本原理和架构
Redis Cluster 是分布式 Redis 的实现。随着 Redis 版本的更替,以及各种已知 bug 的 fixed,在稳定性和高可用性上有了很大的提升和进步,越来越多的企业将 Redis Cluster 实际应用到线上业务中,通过从社区获取到反馈社区的迭代,为 Redis Cluster 成为一个可靠的企业级开源产品,在简化业务架构和业务逻辑方面都起着积极重要的作用。下面从 Redis Cluster 的基本原理为起点开启
Redis Cluster 在业界的分析与思考之旅。
#### 基本原理
Redis Cluster 的基本原理可以从数据分片、数据迁移、集群通讯、故障检测以及故障转移等方面进行了解,Cluster 相关的代码也不是很多,注释也很详细,可自行查看,[地址如下。](https://github.com/antirez/redis/blob/unstable/src/cluster.c)这里由于篇幅的原因,主要从数据分片和数据迁移两方面进行详细介绍:
##### 数据分片
Redis Cluster 在设计中没有使用一致性哈希(Consistency Hashing),而是使用数据分片(Sharding)引入哈希槽(hash slot)来实现;一个 Redis Cluster 包含16384(0~16383)个哈希槽,存储在 Redis Cluster 中的所有键都会被映射到这些
slot 中,集群中的每个键都属于这16384个哈希槽中的一个,集群使用公式 slot=CRC16(key)/16384来计算 key 属于哪个槽,其中 CRC16(key)语句用于计算 key 的 CRC16 校验和。
集群中的每个主节点(Master)都负责处理16384个哈希槽中的一部分,当集群处于稳定状态时,每个哈希槽都只由一个主节点进行处理,每个主节点可以有一个到 N 个从节点(Slave),当主节点出现宕机或网络断线等不可用时,从节点能自动提升为主节点进行处理。
如图1,ClusterNode 数据结构中的 slots 和 numslots 属性记录了节点负责处理哪些槽。其中,slot 属性是一个二进制位数组(bitarray),其长度为16384/8=2048 Byte,共包含16384个二进制位。集群中的 Master 节点用 bit(0和1)来标识对于某个槽是否拥有。比如,对于编号为1的槽,Master 只要判断序列第二位(索引从0开始)的值是不是1即可,时间复杂度为O(1)。
图1 ClusterNode 数据结构
集群中所有槽的分配信息都保存在 ClusterState 数据结构的
slots 数组中,程序要检查槽i是否已经被分配或者找出处理槽 i 的节点,只需要访问 clusterState.slots[i] 的值即可,复杂度也为O(1)。ClusterState 数据结构如图2所示。
图2 ClusterState 数据结构
查找关系如图3所示。
图3 查找关系图
##### 数据迁移
数据迁移可以理解为 slot 和 key 的迁移,这个功能很重要,极大地方便了集群做线性扩展,以及实现平滑的扩容或缩容。那么它是一个怎样的实现过程?下面举个例子:现在要将 Master A节点中编号为1、2、3的 slot 迁移到 Master B 节点中,在 slot 迁移的中间状态下,slot 1、2、3在 Master A 节点的状态表现为
MIGRATING,在 Master B 节点的状态表现为 IMPORTING。
##### MIGRATING 状态
这个状态如图4所示是被迁移 slot 在当前所在 Master A 节点中出现的一种状态,预备迁移 slot 从 Mater A 到 Master B 的时候,被迁移 slot 的状态首先变为 MIGRATING 状态,当客户端请求的某个 key 所属的 slot 的状态处于 MIGRATING 状态时,会出现以下几种情况:
图4 slot 迁移的中间状态
- 如果 key 存在则成功处理。
- 如果 key 不存在,则返回客户端 ASK,客户端根据 ASK 首先发送ASKING 命令到目标节点,然后发送请求的命令到目标节点。
- 当 key 包含多个命令时:
- 如果都存在则成功处理
- 如果都不存在,则返回客户端 ASK
- 如果一部分存在,则返回客户端 TRYAGAIN,通知客户端稍后重试,这样当所有的 key 都迁移完毕,客户端重试请求时会得到 ASK,然后经过一次重定向就可以获取这批键
- 此时并不刷新客户端中 node 的映射关系
##### IMPORTING 状态
这个状态如图2所示是被迁移 slot 在目标 Master B 节点中出现的一种状态,预备迁移 slot 从 Mater A 到 Master B 的时候,被迁移 slot 的状态首先变为 IMPORTING 状态。在这种状态下的 slot 对客户端的请求可能会有下面几种影响:
- 如果 key 不存在则新建。
- 如果 key 不在该节点上,命令会被 MOVED 重定向,刷新客户端中 node 的映射关系。
- 如果是 ASKING 命令则命令会被执行,从而 key 没在被迁移的节点,已经被迁移到目标节点的情况命令可以被顺利执行。
##### 键空间迁移
这是完成数据迁移的重要一步,键空间迁移是指当满足了 slot 迁移前提的情况下,通过相关命令将 slot 1、2、3中的键空间从 Master A 节点转移到 Master B 节点,这个过程由 MIGRATE 命令经过3步真正完成数据转移。步骤示意如图5。
图5 表空间迁移步骤
经过上面三步可以完成键空间数据迁移,然后再将处于 MIGRATING 和 IMPORTING 状态的槽变为常态即可,从而完成整个重新分片的过程。
#### 架构
实现细节:
- Redis Cluster 中节点负责存储数据,记录集群状态,集群节点能自动发现其他节点,检测出节点的状态,并在需要时剔除故障节点,提升新的主节点。
- Redis Cluster 中所有节点通过 PING-PONG 机制彼此互联,使用一个二级制协议(Cluster Bus) 进行通信,优化传输速度和带宽。发现新的节点、发送 PING 包、特定情况下发送集群消息,集群连接能够发布与订阅消息。
- 客户端和集群中的节点直连,不需要中间的 Proxy 层。理论上而言,客户端可以自由地向集群中的所有节点发送请求,但是每次不需要连接集群中的所有节点,只需要连接集群中任何一个可用节点即可。当客户端发起请求后,接收到重定向(MOVED\ASK)错误,会自动重定向到其他节点,所以客户端无需保存集群状态。不过客户端可以缓存键值和节点之间的映射关系,这样能明显提高命令执行的效率。
- Redis Cluster 中节点之间使用异步复制,在分区过程中存在窗口,容易导致丢失写入的数据,集群即使努力尝试所有写入,但是以下两种情况可能丢失数据:
- 命令操作已经到达主节点,但在主节点回复的时候,写入可能还没有通过主节点复制到从节点那里。如果这时主节点宕机了,这条命令将永久丢失。以防主节点长时间不可达而它的一个从节点已经被提升为主节点。
- 分区导致一个主节点不可达,然而集群发送故障转移(failover),提升从节点为主节点,原来的主节点再次恢复。一个没有更新路由表(routing table)的客户端或许会在集群把这个主节点变成一个从节点(新主节点的从节点)之前对它进行写入操作,导致数据彻底丢失。
- Redis 集群的节点不可用后,在经过集群半数以上 Master 节点与故障节点通信超过 cluster-node-timeout 时间后,认为该节点故障,从而集群根据自动故障机制,将从节点提升为主节点。这时集群恢复可用。
### Redis Cluster 的优势和不足
#### 优势
- 无中心架构。
- 数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布。
- 可扩展性,可线性扩展到1000个节点,节点可动态添加或删除。
- 高可用性,部分节点不可用时,集群仍可用。通过增加 Slave 做
standby 数据副本,能够实现故障自动 failover,节点之间通过
gossip 协议交换状态信息,用投票机制完成 Slave 到 Master 的角色提升。
- 降低运维成本,提高系统的扩展性和可用性。
#### 不足
- Client 实现复杂,驱动要求实现 Smart Client,缓存 slots mapping 信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。目前仅J edisCluster 相对成熟,异常处理部分还不完善,比如常见的“max redirect exception”。
- 节点会因为某些原因发生阻塞(阻塞时间大于 clutser-node-timeout),被判断下线,这种 failover 是没有必要的。
- 数据通过异步复制,不保证数据的强一致性。
- 多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况。
- Slave 在集群中充当“冷备”,不能缓解读压力,当然可以通过 SDK 的合理设计来提高 Slave 资源的利用率。
### Redis Cluster 在业界有哪些探索
通过调研了解,目前业界使用 Redis Cluster 大致可以总结为4类:
#### 直连型
直连型,又可以称之为经典型或者传统型,是官方的默认使用方式,架构图见图6。这种使用方式的优缺点在上面的介绍中已经有所说明,这里不再过多重复赘述。但值得一提的是,这种方式使用 Redis Cluster 需要依赖 Smart Client,诸如连接维护、缓存路由表、MultiOp 和 Pipeline 的支持都需要在 Client 上实现,而且很多语言的 Client 目前都还是没有的(关于 Clients 的更多介绍请参考https://redis.io/clients)。虽然 Client 能够进行定制化,但有一定的开发难度,客户端的不成熟将直接影响到线上业务的稳定性。
图6 Redis Cluster 架构(来自互联网)
#### 带 Proxy 型
在 Redis Cluster 还没有那么稳定的时候,很多公司都已经开始探索分布式 Redis 的实现了,比如有基于 Twemproxy 或者 Codis
的实现,下面举一个唯品会基于 Twemproxy 架构的例子(不少公司分布式 Redis 的集群架构都经历过这个阶段),如图7所示。
图7 Redis 基于 Twemproxy 的架构实现(来自互联网)
这种架构的优点和缺点也比较明显。
优点:
- 后端 Sharding 逻辑对业务透明,业务方的读写方式和操作单个
Redis 一致;
- 可以作为 Cache 和 Storage 的 Proxy,Proxy 的逻辑和
Redis 资源层的逻辑是隔离的;
- Proxy 层可以用来兼容那些目前还不支持的 Clients。
缺点:
- 结构复杂,运维成本高;
- 可扩展性差,进行扩缩容都需要手动干预;
- failover 逻辑需要自己实现,其本身不能支持故障的自动转移;
- Proxy 层多了一次转发,性能有所损耗。
正是因此,我们知道 Redis Cluster 和基于 Twemproxy 结构使用中各自的优缺点,于是就出现了下面的这种架构,糅合了二者的优点,尽量规避二者的缺点,架构如图8。
图8 Smart Proxy 方案架构
目前业界 Smart Proxy 的方案了解到的有基于 Nginx Proxy 和自研的,自研的如饿了么开源部分功能的 Corvus,优酷土豆是则通过
Nginx 来实现,滴滴也在展开基于这种方式的探索。选用 Nginx Proxy 主要是考虑到 Nginx 的高性能,包括异步非阻塞处理方式、高效的内存管理、和 Redis 一样都是基于 epoll 事件驱动模式等优点。优酷土豆的 Redis 服务化就是采用这种结构。
优点:
- 提供一套 HTTP Restful 接口,隔离底层资源,对客户端完全透明,跨语言调用变得简单;
- 升级维护较为容易,维护 Redis Cluster,只需平滑升级
Proxy;
- 层次化存储,底层存储做冷热异构存储;
- 权限控制,Proxy 可以通过密钥管理白名单,把一些不合法的请求都过滤掉,并且也可以对用户请求的超大 value 进行控制和过滤;
- 安全性,可以屏蔽掉一些危险命令,比如 keys *、save、flushall 等,当然这些也可以在 Redis 上进行设置;
- 资源逻辑隔离,根据不同用户的 key 加上前缀,来实现动态路由和资源隔离;
- 监控埋点,对于不同的接口进行埋点监控。
缺点:
- Proxy 层做了一次转发,性能有所损耗;
- 增加了运维成本和管理成本,需要对架构和 Nginx Proxy 的实现细节足够了解,因为 Nginx Proxy 在批量接口调用高并发下可能会瞬间向 Redis Cluster 发起几百甚至上千的协程去访问,导致
Redis 的连接数或系统负载的不稳定,进而影响集群整体的稳定性。
#### 云服务型
这种类型典型的案例就是企业级的 PaaS 产品,如亚马逊和阿里云提供的 Redis Cluster 服务,用户无需知道内部的实现细节,只管使用即可,降低了运维和开发成本。当然也有开源的产品,国内如搜狐的CacheCloud,它提供一个 Redis 云管理平台,实现多种类型(Redis Standalone、Redis Sentinel、Redis Cluster)自动部署,解决 Redis 实例碎片化现象,提供完善统计、监控、运维功能,减少开发人员的运维成本和误操作,提高机器的利用率,提供灵活的伸缩性,提供方便的接入客户端,[更多细节请参考](https://cachecloud.github.io)。尽管这还不错,如果是一个新业务,到可以尝试一下,但若对于一个稳定的业务而言,要迁移到
CacheCloud 上则需要谨慎。如果对分布式框架感兴趣的可以看下
Twitter 开源的一个实现 Memcached 和 Redis的 分布式缓存框架 Pelikan,目前国内并没有看到这样的应用案例,[它的官网](http://twitter.github.io/pelikan/)。
图9 CacheCloud 平台架构(来自互联网)
#### 自研型
这种类型在众多类型中更显得孤独,因为这种类型的方案更多是现象级,仅仅存在于为数不多的具有自研能力的公司中,或者说这种方案都是各公司根据自己的业务模型来进行定制化的。这类产品的一个共同特点是没有使用 Redis Cluster 的全部功能,只是借鉴了 Redis Cluster 的某些核心功能,比如说 failover 和 slot 的迁移。作为国内使用 Redis 较早的公司之一,新浪微博就基于内部定制化的
Redis 版本研发出了微博 Redis 服务化系统 Tribe。它支持动态路由、读写分离(从节点能够处理读请求)、负载均衡、配置更新、数据聚集(相同前缀的数据落到同一个 slot 中)、动态扩缩容,以及数据落地存储。同类型的还有百度的 BDRP 系统。
图10 Tribe 系统架构图
### Redis Cluster 运维开发最佳实践经验
- 根据公司的业务模型选择合适的架构,适合自己的才是最好的;
- 做好容错机制,当连接或者请求异常时进行连接 retry 或
reconnect;
- 重试时间可设置大于 cluster-node-time (默认15s),增强容错性,减少不必要的 failover;
- 避免产生 hot-key,导致节点成为系统的短板;
- 避免产生 big-key,导致网卡打爆和慢查询;
- 设置合理的 TTL,释放内存。避免大量 key 在同一时间段过期,虽然 Redis 已经做了很多优化,仍然会导致请求变慢;
- 避免使用阻塞操作(如 save、flushall、flushdb、keys *等),不建议使用事务;
- Redis Cluster 不建议使用 pipeline 和 multi-keys 操作(如 mset/mget. multi-key 操作),减少 max redirect 的产生;
- 当数据量很大时,由于复制积压缓冲区大小的限制,主从节点做一次全量复制导致网络流量暴增,建议单实例容量不要分配过大或者借鉴微博的优化采用增量复制的方式来规避;
- 数据持久化建议在业务低峰期操作,关闭 aofrewrite 机制,aof
的写入操作放到 bio 线程中完成,解决磁盘压力较大时Redis阻塞的问题。设置系统参数 vm.overcommit_memory=1,也可以避免
bgsave/aofrewrite 的失败;
- client buffer 参数调整
client-output-buffer-limit normal 256mb 128mb 60
client-output-buffer-limit slave 512mb 256mb 180
- 对于版本升级的问题,修改源码,将 Redis 的核心处理逻辑封装到动态库,内存中的数据保存在全局变量里,通过外部程序来调用动态库里的相应函数来读写数据。版本升级时只需要替换成新的动态库文件即可,无须重新载入数据,可毫秒级完成;
- 对于实现异地多活或实现数据中心级灾备的要求(即实现集群间数据的实时同步),可以参考搜狐的实现:Redis Cluster => Redis-Port => Smart proxy => Redis Cluster;
[从 Redis 4.2 的 Roadmap 来看,更值得期待](https://gist.github.com/antirez/a3787d538eec3db381a41654e214b31d):
- 加速 key->hashslot 的分配
- 更好更多的数据中心存储
- redis-trib 的 C 代码将移植到 redis-cli,瘦身包体积
- 集群的备份/恢复
- 非阻塞的 Migrate
- 更快的 resharding
- 隐藏一个只 Cache 模式,当没有 Slave 时,Masters 当在有一个失败后能够自动重新分配 slot
- Cluster API 和 Redis Modules 的改进,并且 Disque
分布式消息队列将作为 Redis Module 加入 Redis。
致谢:
感谢好友陈群、李航和刘东辉的协助审稿和宝贵建议。