如何正确架构Redis?

无论物理机还是云主机,内存往往有限制,scale up不是一个好办法,需要scale out横向可伸缩扩展

硬件资源成本降低,多核CPU,几十G内存主机普遍,主进程单线程工作的Redis,只运行一个实例浪费

同时,管理一个巨大内存不如管理相对较小的内存高效。通常一机多Redis实例

方案

1.Redis官方集群方案 Redis Cluster

服务器Sharding技术,3.0版本开始正式提供

Sharding采用slot概念,分成16384个槽,类似pre sharding思路

每个进入Redis的键值对,根据key进行散列,分配到这16384个slot中的某一个中。hash算法简单,CRC16后16384取模

集群中的每个node负责分摊这16384个slot中的一部分,即每个slot都对应一个node负责处理

加减node节点时,16384个槽需再分配,槽中的键值也要迁移。目前实现处于半自动状态,需要人工介入


Redis集群,要保证16384个槽对应的node都正常工作,如果某个node发生故障,那它负责的slots也就失效,整个集群将不能工作

增加集群可访问性,官方推荐将node配成主从,即一个master,挂n个slave

如果主失效,Redis Cluster从slave选举一个上升为主节点,类似服务器节点通过Sentinel监控架构成主从结构,只是Redis Cluster本身提供了故障转移容错的能力

Redis Cluster的新节点识别能力、故障判断及故障转移能力是通过集群中的每个node都在和其它nodes进行通信,称为集群总线(cluster bus)。使用对外服务端口号加10000。例6379与其它nodes通信端口是16379。nodes间通信用二进制协议

对客户端来说,整个cluster看做一个整体,可连任意node进行操作,当key没有分配到该node上时,Redis会返回转向指令,指向正确的node

Redis Cluster可以说是服务端Sharding分片技术的体现,将键值按照一定算法合理分配到各个实例分片上,同时各个实例节点协调沟通,共同对外承担一致服务

2.Redis Sharding集群

多Redis实例服务,涉及到定位、协同、容错、扩容等技术难题

轻量级的客户端Redis Sharding技术,可以说是Redis Cluster出来之前,业界普遍使用的多Redis实例集群方法

采用哈希算法将key散列,通过hash函数,特定key映射到特定Redis节点上,客户端就知道该向哪个Redis节点操作数据

jedis已支持Redis Sharding功能,即ShardedJedis及结合缓存池的ShardedJedisPool

Jedis Sharding特点: 一致性哈希算法(consistent hashing),key和节点name同时hashing,然后映射匹配,算法用MURMUR_HASH

1.用一致性哈希而不是简单类似哈希求模映射主要原因是加减节点时不会产生由于重新匹配造成的rehashing。一致性哈希只影响相邻节点key分配,影响量小

2.为了避免一致性哈希只影响相邻节点造成节点分配压力,ShardedJedis对每个Redis节点根据名字(没有,Jedis会赋予缺省名字)虚拟化出160个虚拟节点进行散列 根据权重weight,也可虚拟化出160倍数的虚拟节点。用虚拟节点做映射匹配,可以在增加或减少Redis节点时,key在各Redis节点移动再分配更均匀,而不是只有相邻节点受影响

3.ShardedJedis支持keyTagPattern模式,即抽取key的一部分keyTag做sharding,这样通过合理命名key,可以将一组相关联的key放入同一个Redis节点,这在避免跨节点访问相关数据时很重要

扩容问题

Redis Sharding 采用客户端 Sharding 方式,服务端 Redis 还是一个个相对独立的 Redis 实例节点,没有做任何变动 同时也不需要增加额外的中间处理组件,这是一种非常轻量、灵活的 Redis 多实例集群方法

这种轻量灵活方式必然在集群其它能力方面做出妥协。比如扩容,增加 Redis 节点时,尽管采用一致性哈希,毕竟还是会有 key 匹配不到而丢失,这时需要键值迁移

轻量级客户端 sharding,处理 Redis 键值迁移是不现实的,这就要求应用层面允许 Redis 中数据丢失或从后端数据库重新加载数据。但有些时候,击穿缓存层,直接访问数据库层,会对系统访问造成很大压力

改善这种情况?

Redis 作者给出了一个比较讨巧的办法 –presharding,即预先根据系统规模尽量部署好多个 Redis 实例,这些实例占用系统资源很小,一台物理机可部署多个,让他们都参与 sharding,当需要扩容时,选中一个实例作为主节点,新加入的 Redis 节点作为从节点进行数据复制

数据同步后,修改 sharding 配置,让指向原实例的 Shard 指向新机器上扩容后的 Redis 节点,同时调整新 Redis 节点为主节点,原实例可不再使用

presharding 是预先分配好足够的分片,扩容时只是将属于某一分片的原 Redis 实例替换成新的容量更大的 Redis 实例。参与 sharding 的分片没有改变,所以也就不存在 key 值从一个区转移到另一个分片区的现象,只是将属于同分片区的键值从原 Redis 实例同步到新 Redis 实例

并不是只有增删 Redis 节点引起键值丢失问题,更大的障碍来自 Redis 节点突然宕机 为不影响 Redis 性能,尽量不开启 AOF 和 RDB 文件保存功能,可架构 Redis 主备模式,主 Redis 宕机,数据不会丢失,备 Redis 留有备份

这样架构模式变成一个 Redis 节点切片包含一个主 Redis 和一个备 Redis。在主 Redis 宕机时,备 Redis 接管过来,上升为主 Redis,继续提供服务。主备共同组成一个 Redis 节点,通过自动故障转移,保证了节点的高可用性

Redis Sentinel 提供了主备模式下 Redis 监控、故障转移功能达到系统的高可用性

高访问量下,即使采用 Sharding 分片,一个单独节点还是承担了很大的访问压力,这时还需要进一步分解。通常情况下,应用访问 Redis 读操作量和写操作量差异很大,读常常是写的数倍,可以将读写分离,而且读提供更多的实例数

可以利用主从模式实现读写分离,主负责写,从负责只读,同时一主挂多个从。在 Sentinel 监控下,还可以保障节点故障的自动监测

3. 利用代理中间件实现大规模 Redis 集群

上2种是多 Redis 服务器集群方式基于客户端 sharding 的 Redis Sharding 和基于服务端 sharding 的 Redis Cluster

客户端 sharding优势在于服务端的 Redis 实例彼此独立,相互无关联,每个 Redis 实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强

不足之处在于:

  • sharding 处理放到客户端,规模进步扩大时给运维带来挑战
  • 服务端 Redis 实例群拓扑结构有变化时,每个客户端都需要更新调整
  • 连接不能共享,当应用规模增大时,资源浪费制约优化

服务端 sharding 的 Redis Cluster 其优势在于服务端 Redis 集群拓扑结构变化时,客户端不需要感知,客户端像使用单 Redis 服务器一样使用 Redis 集群,运维管理也比较方便

不过 Redis Cluster 正式版推出时间不长,系统稳定性、性能等都需要时间检验,尤其在大规模使用场合

能不能结合二者优势?即能使服务端各实例彼此独立,支持线性可伸缩,同时 sharding 又能集中处理,方便统一管理?

用中间件做 sharding 的技术twemproxy

处于客户端和服务器的中间,将客户端发来的请求,进行一定的处理后 (如 sharding),再转发给后端真正的 Redis 服务器。即客户端不直接访问 Redis 服务器,而通过 twemproxy 代理中间件间接访问

twemproxy 中间件的内部处理是无状态的,它本身可以很轻松地集群,这样可避免单点压力或故障

twemproxy 用 C 开发 twemproxy 后端不仅支持 redis,同时也支持 memcached,这是 twitter 系统具体环境造成的

由于使用了中间件,twemproxy 可以通过共享与后端系统的连接,降低客户端直接连接后端服务器的连接数量。同时,它也提供 sharding 功能,支持后端服务器集群水平扩展。统一运维管理也带来了方便

当然,也是由于使用了中间件代理,相比客户端直连服务器方式,性能上会有所损耗,实测结果大约降低了 20% 左右

https://www.zhihu.com/question/21419897


2.x 时代

多个 Redis 实例,分布在多台机器上,客户端缓存所有 Redis 实例连接(配置文件或者 zk 中心服务管理所有 Redis 配置),自己对 key 做 hash,确定每个 key 的分布,找到对应的 Redis 实例,再做操作

为了 key 的均匀分布和集群的扩展性、稳定性,一般用一致性哈希算法

每次扩容新增实例的时候需要更新配置信息,客户端重新加载配置信息并更新本地缓存的连接

自建集群的问题

一、主从无法自动切换导致出故障时服务不可用,需要运维工程师手动切,实际有 keepalived 方案

二、扩容实例时无法灵活控制新增数量,而且需要手工操作,耗费时间人力

业务关系 key 的量以亿计,为扩容时清理数据方便,设计分布策略时,每个实例附加了一个整型区段比如 实例 A: [0,99], 实例 B: [100, 199],然后每个 key 都默默带上了一个 hash 出来的整数前缀

扩容时为了保持均匀,不得不 double 一下原来实例的数量增加 A’ 和 B’,将两个区间分别均分

A’完整拷贝 A 的数据,B’ 完整拷贝 B 的数据,再在新增实例内遍历一遍 key,根据整数前缀删掉不对的数据

整个过程都放在凌晨做,因为数据同步和清理过程会影响 Redis 的服务性能。清理完成后,更新 Redis 的配置文件,在客户端重新加载后开始生效

总结一下就是为了自建集群使用中有两个东西需要自己处理

  • 实例出问题时的节点切换
  • 实例扩展时的数据再分配

这两点处理起来都不是十分优雅

3.0 集群支持

节点间互相建立连接,互相发现通过配置文件来实现

集群会自动将不可用 master 节点切换为它的一个 slave,保持了很好的高可用性。基本上是第一个问题的内置解决方案

第二个问题看上去似乎没什么革新的地方

数据分布没有使用一致性哈希,引入了一个 hash slot概念,总共有 16384(固定的)个 slots。所有 master Redis 实例分摊这些 slots

key 做 CRC16 映射到整数,然后对 16384 取模,定位到对应的 Redis 实例上

加入节点时还是需要手动将一部分老 Redis 的 hash slot 重新分配到新的上

尽管由于节点相互之间存在连接,slot 重新分配后数据访问和存储可以自动重定向,但是 key 的自动迁移会阻塞两个节点,对于 key 非常多的应用场景,仍然显得有些力不从心

而且 slot 得重新分配该使用什么策略能保证 key 得分配均匀呢?和我们的处理方式一样均分么?

客户端方面,新的 Jedis已经不再需要自己做一致性哈希,并且节点实例的缓存都已经控制好

总的来说,Redis Cluster 通过节点互联支持了内部的数据访问的重定向,内部的数据迁移,也做到了在节点 fail 时的主从自动切换,相比自己处理集群来说方便了许多