1, Redis Sharding集群原理,使用的条件,优劣点以及对应的解决方案
2, Redis官方集群方案 Redis Cluster介绍
3,中间件的方式实现Redis集群
4,选择
redis 集群的解决的方案和原理分析
1, Redis Sharding集群
Redis Sharding采用客户端Sharding方式,服务端Redis还是一个个相对独立的Redis实例节点,没有做任何变动。理解就是客户端的连接的特定的服务器,但是在增加节点或者删除节点的时候,有一定的兼容的算法,使得原来打在某一个redis实例,还打在这个实例上面。
主要的思想就是根据一定的算法,例如 哈希算法,对Redis的key值进行散列,然后将key值散列到特定Redis实例上面。以后的设置,获取等关于该key值的操作,都会打到这个特定的redis实例上面进行操作。
值得庆幸的是,java redis的客户端驱动jedis,已经支持 Redis Sharding 功能。即是ShardedJedis以及结合缓存池ShardedJedisPool。
Jedis的Redis Sharding实现具有如下特点:
1.采用一致性哈希算法(consistent hashing),将key和节点name同时hashing,然后进行映射匹配,采用的算法是MURMUR_HASH。采用一致性哈希而不是采用简单类似哈希求模映射的主要原因是当增加或减少节点时,不会产生由于重新匹配造成的rehashing。一致性哈希只影响相邻节点key分配,影响量小。
即是:ShardedJedis 在操作的时候,都会调用的getShard方法,来获得key对应的redis实例:
public R getShard(String key) {
return resources.get(getShardInfo(key));
}
public S getShardInfo(String key) {
return getShardInfo(SafeEncoder.encode(getKeyTag(key)));
}
public String getKeyTag(String key) {
// 默认的情况下,tagPattern是NULL
if (tagPattern != null) {
Matcher m = tagPattern.matcher(key);
if (m.find()) return m.group(1);
}
return key;
}
public static byte[] encode(final String str) {
return str.getBytes(Protocol.CHARSET);
}
public S getShardInfo(byte[] key) {
// 最关键的treeMap的数据结构
SortedMap<Long, S> tail = nodes.tailMap(algo.hash(key));
if (tail.isEmpty()) {
return nodes.get(nodes.firstKey());
}
return tail.get(tail.firstKey());
}
//nodes 是ShardedJedisPool 初始化的时候,hash Redis的实例的配置信息后组成的TreeMap,声明是:
private TreeMap<Long, S> nodes;//S extends ShardInfo<R>
2.为了避免一致性哈希只影响相邻节点造成节点分配压力,ShardedJedis会对每个Redis节点根据名字(没有,Jedis会赋予缺省名字)会虚拟化出160个虚拟节点进行散列。根据权重weight,也可虚拟化出160倍数的虚拟节点。用虚拟节点做映射匹配,可以在增加或减少Redis节点时,key在各Redis节点移动再分配更均匀,而不是只有相邻节点受影响。
private void initialize(List<S> shards) {
nodes = new TreeMap<Long, S>();
for (int i = 0; i != shards.size(); ++i) {
final S shardInfo = shards.get(i);
if (shardInfo.getName() == null)
for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
nodes.put(this.algo.hash("SHARD-" + i + "-NODE-" + n), shardInfo);
}
else
for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
nodes.put(this.algo.hash(shardInfo.getName() + "*" + shardInfo.getWeight() + n),
shardInfo);
}
resources.put(shardInfo, shardInfo.createResource());
}
}
3.ShardedJedis支持keyTagPattern模式,即抽取key的一部分keyTag做sharding,这样通过合理命名key,可以将一组相关联的key放入同一个Redis节点,这在避免跨节点访问相关数据时很重要。(默认的情况下为NULL)
KeyTag这个可以在初始化ShardedJedisPool 设置。
public ShardedJedisPool(final GenericObjectPoolConfig poolConfig,
List<JedisShardInfo> shards, Pattern keyTagPattern) {
this(poolConfig, shards, Hashing.MURMUR_HASH, keyTagPattern);
}
当然,Redis Sharding这种轻量灵活方式必然在集群其它能力方面做出妥协,但是针对每一种不好的地方,我们又都准备了不同的解决方案。
1 扩容,当想要增加Redis节点时,尽管采用一致性哈希,毕竟还是会有key匹配不到而丢失,这时需要键值迁移
这就要求:应用层面允许Redis中数据丢失或从后端数据库重新加载数据。但有些时候,击穿缓存层,直接访问数据库层,会对系统访问造成很大压力。
2 轻量级客户端sharding,处理Redis键值迁移比较的麻烦。
Redis作者给出了一个比较讨巧的办法–presharding,即预先根据系统规模尽量部署好多个Redis实例,这些实例占用系统资源很小,一台物理机可部署多个,让他们都参与sharding,当需要扩容时,选中一个实例作为主节点,新加入的Redis节点作为从节点进行数据复制。数据同步后,修改sharding配置,让指向原实例的Shard指向新机器上扩容后的Redis节点,同时调整新Redis节点为主节点,原实例可不再使用。
3 应从redis宕机的风险
为不影响Redis性能,尽量不开启AOF和RDB文件保存功能,可采用Redis主备模式,主Redis宕机,数据不会丢失,因为备Redis留有备份。这样,架构模式变成一个Redis节点切片包含一个主Redis和一个备Redis。在主Redis宕机时,备Redis接管过来,上升为主Redis,继续提供服务。主备共同组成一个Redis节点,通过自动故障转移,保证了节点的高可用性。
可以利用主从模式实现读写分离,主负责写,从负责只读,同时一主挂多个从。在Sentinel监控下,还可以保障节点故障的自动监测。
2, Redis官方集群方案 Redis Cluster
Redis Cluster是一种服务器Sharding技术,3.0版本开始正式提供。
Redis Cluster中,Sharding采用slot(槽)的概念,一共分成16384个槽。对于每个进入Redis的键值对,根据key进行散列,分配到这16384个slot中的某一个中。使用的hash算法也比较简单,就是CRC16后16384取模。
Redis集群中的每个node(节点)负责分摊这16384个slot中的一部分,也就是说,每个slot都对应一个node负责处理。 这个和我们搭建的集群的日志相吻合:
当动态添加或减少node节点时,需要将16384个槽做个再分配,槽中的键值也要迁移。当然,这一过程,在目前实现中,还处于半自动状态,需要人工介入。Redis集群,要保证16384个槽对应的node都正常工作,如果某个node发生故障,那它负责的slots也就失效,整个集群将不能工作。
为了增加集群的可访问性,官方推荐的方案是将node配置成主从结构,即一个master主节点,挂n个slave从节点。这时,如果主节点失效,Redis Cluster会根据选举算法从slave节点中选择一个上升为主节点,整个集群继续对外提供服务。这非常类似前篇文章提到的Redis Sharding场景下服务器节点通过Sentinel监控架构成主从结构,只是Redis Cluster本身提供了故障转移容错的能力。
Redis Cluster的新节点识别能力、故障判断及故障转移能力是通过集群中的每个node都在和其它nodes进行通信,这被称为集群总线(cluster bus)。它们使用特殊的端口号,即对外服务端口号加10000。例如如果某个node的端口号是6379,那么它与其它nodes通信的端口号是16379。nodes之间的通信采用特殊的二进制协议。
对客户端来说,整个cluster被看做是一个整体,客户端可以连接任意一个node进行操作,就像操作单一Redis实例一样,当客户端操作的key没有分配到该node上时,Redis会返回转向指令,指向正确的node,这有点儿像浏览器页面的302 redirect跳转。
3.利用代理中间件实现大规模Redis集群
上面分别介绍了多Redis服务器集群的两种方式,它们是基于客户端sharding的Redis Sharding和基于服务端sharding的Redis Cluster。
客户端sharding技术其优势在于服务端的Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强。其不足之处在于:
由于sharding处理放到客户端,规模进步扩大时给运维带来挑战。 服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。 连接不能共享,当应用规模增大时,资源浪费制约优化。
服务端sharding的Redis Cluster其优势在于服务端Redis集群拓扑结构变化时,客户端不需要感知,客户端像使用单Redis服务器一样使用Redis集群,运维管理也比较方便。
不过Redis Cluster正式版推出时间不长,系统稳定性、性能等都需要时间检验,尤其在大规模使用场合。
能不能结合二者优势?即能使服务端各实例彼此独立,支持线性可伸缩,同时sharding又能集中处理,方便统一管理: Redis代理中间件twemproxy就是这样一种利用中间件做sharding的技术。
twemproxy处于客户端和服务器的中间,将客户端发来的请求,进行一定的处理后(如sharding),再转发给后端真正的Redis服务器。也就是说,客户端不直接访问Redis服务器,而是通过twemproxy代理中间件间接访问。
twemproxy中间件的内部处理是无状态的,它本身可以很轻松地集群,这样可避免单点压力或故障。
twemproxy又叫nutcracker,起源于twitter系统中redis/memcached集群开发实践,运行效果良好,后代码奉献给开源社区。其轻量高效,采用C语言开发,工程网址是:GitHub - twitter/twemproxy: A fast, light-weight proxy for memcached and redis
twemproxy后端不仅支持redis,同时也支持memcached,这是twitter系统具体环境造成的。
由于使用了中间件,twemproxy可以通过共享与后端系统的连接,降低客户端直接连接后端服务器的连接数量。同时,它也提供sharding功能,支持后端服务器集群水平扩展。统一运维管理也带来了方便。
当然,也是由于使用了中间件代理,相比客户端直连服务器方式,性能上会有所损耗,实测结果大约降低了20%左右。
那么,上面几种方案如何选择?
显然在客户端做分片是自定义能力最高的,优势在于,在不需要客户端服务端协作,以及没有中间层的条件下,每个请求的 roundtrip 时间是相对更小的,搭配良好的客户端分片策略,可以让整个集群获得很好的扩展性。当然劣势也很明显,用户需要自己对付 Redis 节点宕机的情况,需要采用更复杂的策略来做 replica,以及需要保证每个客户端看到的集群“视图”是一致的。
中间件的方案对客户端实现的要求是最低的,客户端只要支持基本的 Redis 通信协议即可,至于扩容、多副本、主从切换等机制客户端都不必操心,因此这种方案也很适合用来做“缓存服务”。
官方推出的协作方案也完整地支持了分片和多副本,相对于各种 proxy,这种方案假设了客户端实现是可以与服务端“协作”的,事实上主流语言的 SDK 都已经支持了。