18

Redis Sharding 集群跟一致性哈希有什么瓜葛?

 3 years ago
source link: https://xie.infoq.cn/article/aa7f0c4c6538cf966a4cdac79
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一、前言

最近在所负责的某些系统上遇到了一些Redis相关的问题,刚好在朋友圈聊到Cluster和Sharding这方面的东西,发现有些地方比较模糊,考虑到之前也整理了关于Sentinel集群模式,趁着有点力气整理一下Sharding的一些相关资料。

Cluster模式后面有时间再补充吧。

二、Redis sharding集群

1、概念及优劣:

客户端分片技术,即客户端自己计算每个key应该要放到哪个Redis实例,其主要原理还是使用哈希算法对某个Key进行哈希后映射存储到对应的Redis实例上。(这里提前透露简单的哈希算法可以是通过简单的取余,但是这种办法有致命的弱点,稍后再讲。)

此方法的好处是1)降低服务器集群的复杂度,且2)每个实例都是独立没有关联的。缺点就是1)扩容问题,目前客户端实现在支持动态增删实例方面没有解决方案;2)单点故障:即如果某个分片宕机后,那个分片的数据就不能提供服务,每个实例本身的高可用需要自己想办法。

针对以上两个问题,一般有以下的解决方案:

单点故障 :一般的做法是通过一主N从的哨兵模式实现该分片的自动故障转移,具体的方案原理及搭建请参考我的另外一篇文章: Redis哨兵(Sentinel)模式

扩容问题 :一般只能通过重启的方法进行扩容,但这种办法在键值对数据迁移方面加大了运维侧的难度,且应用层需要做配置改动去支持新的实例。还有另外一种办法,Redis作者推荐可以使用一种PreSharding的办法,这里暂不做介绍,后面再补充。

2、数据倾斜问题

后面会提到Jedis的用法(因为Jedis就是客户端分片的其中一种实现方案),这里也先简单介绍一下一致性哈希可以解决的问题吧。但在开始介绍之前一定要接受一个前提,一致性哈希算法可以做到:

  • 均衡性:不同对象经哈希后的哈希值在哈希环内(即hash family)能保证尽量均匀分布确保均衡落入到不同节点;

  • 单调性:对于部分已经分配到具体节点的键值对,即使有新节点加入也能保证该部分键值对要么映射在原节点要么映射到新节点(即不会映射到其他旧有节点上);

  • 分散性:暂时还不是特别懂,翻译不了

  • 负载性:暂时还不是特别懂,翻译不了

具体上述4种特性的英文描述可以在下面的原始论文中的第6页可以找到,这里就不做展示了。

另:

1、关于缓存系统对于该算法运用的相关论文有兴趣的朋友可以参考哥伦比亚大学网站的资料:  http://www.cs.columbia.edu/~asherman/papers/cachePaper.pdf

2、关于算法原始论文有兴趣的可以参考普林斯顿大学资料: https://www.cs.princeton.edu/courses/archive/fall09/cos518/papers/chash.pdf

如果Redis平时放些用户session、参数这类偏静态数据问题不大,如果我们是存储千万或亿级级别的交易数据、埋点数据(且需要频发读写)的话,在Redis Sharding的方案中,例如我们上例因为资源成本的问题我不可能配几十或百台实例,因此只配了两个实例,但是两个实例会带来什么样的问题?对,也会因为物理实例节点(这里简称Node)的偏少而带来数据倾斜的问题。怎么办呢?如果使用一致性哈希算法的话(我这里画了个概念图方便理解),算法会针对每个物理节点(Node)虚拟出多个虚拟节点(这里简称Virtual Node或者VNode),这样在整个哈希环空间内就有多个互相交错的虚拟节点,那数据分布得更均衡了而避免对于某台服务器的读写压力,一般来说虚拟节点越多数据分布得约均匀(曾经在某个文章看过压测数据,当虚拟节点数达到   1000级别 的时候,每个节点存储的数据基本上接近   “平均数” )。

具体请看下图。在没有虚拟的情况下,差不多所有数据(K1/K2/K3/K4/K5)全在Node1,只有K6在Node2;在使用了一致性哈希算法后(虚拟化后),K1/K3/K5实际上在Node1对应的那些虚拟节点中,K2/K4/K6则在Node2里,这样数据就均衡的分布在两个物理节点。

Qv6zYnE.png!mobile

实际上,大家可以看看一下Jedis的源码,在Node的初始化的时候Jedis会自动将每个Node虚拟出160个VNode,这样的话上例中的2个实例实际上就有 2*160=320个虚拟节点 ,而且可以从上图类推出来,这320个节点是 纵横交错的而非顺序排列 。另外,具体键值对(K,V)存储的时候Jedis怎么选择对应的虚拟节点的话,大家有兴趣可以看看相应源码,这里不做具体展示。

Qf6Rruj.png!mobile

3、数据丢失问题

使用了一致性哈希算法后还有一个好处,就是在没有足够时间做数据迁移的前提下,动态扩容或突发宕机时候导致数据丢失或者打到后面数据库的流量减少,最大程度避免雪崩的发生,是高容错性的一个体现。

以下图的哈希环为例,在没有插入Node3节点前,如果我们通过客户端去读取这几个K1/K2/K3的键值对的时候会根据算法自动识别是存储在Node1并可以获取到;但是如果在服务器压力暴增情况下临时新增一台服务器的话(潜台词就是没有做数据迁移,因为一旦上量了迁移也需要很多时间),客户端再去获取K1/K2/K3的时候,通过算法计算认为它们应该存储在Node3的,但实际Node3是肯定没有啦(因为没做数据迁移),所以对客户端来说50%的数据是凭空消失了(  被黄圈圈起来部分,这里思考一下是不是跟该算法的单调性有点吻合,细品一下 ),这是何等悲壮啊,等着跑路吧兄dei。但是,如果是有良心的程序猿,在系统设计时候会做好容错性设计的(不知道在读的你有没有做到),一般在应用层逻辑上会兜底,就是允许把请求穿透到服务端通过查库获取并同步更新到最新的Node3上,那下次再有请求过来的时候直接从Node3获取即可而不需要打到数据库。

针对上面提到的问题,

  • 首先,如果采用了一致性哈希后,因为上面提到虚拟化节点很多,这样的话呢就算发生上面情况,所影响的数据范围也是相对较小的,起码不是整个物理节点的数据都得查库;

  • 其次,如果我们在应用层做了相应兜底处理(即穿透到数据库获取并同步到Redis节点),因为相应的数据范围较小,因此打到服务端的流量压力也没有那么大。

eQ3YNvi.png!mobile

4、应用

目前客户端Jedis能够支持Redis Sharding,即ShardedJedis 以及结合缓存池的ShardedJedisPool组合使用。而且,Jedis的Redis Sharding实现是采用一致性哈希算法(具体请参考以上第二点)。 具体客户端使用方法请参考以下(整个工程为 springboot工程   )。

pom.xml

<!--引入Jedis客户端-->

<dependency>

<groupId>redis.clients</groupId>

<artifactId>jedis</artifactId>

<version>2.8.0</version>

</dependency>

application.properties

#redis sharding instance config

redis_client_timeout=500

redis_one_host=192.168.32.101

redis_one_port=6379

redis_one_password=123

redis_two_host=192.168.32.102

redis_two_port=6380

redis_two_password=123

RedisConfiguration.java

@Configuration

public class RedisConfiguration {

//redis one host

@Value("${redis_one_host}")

private String redisOneHost;

//redis one port

@Value("${redis_one_port}")

private int redisOnePort;

//redis one password

@Value("${redis_one_password}")

private String redisOnePassword;

//redis two host

@Value("${redis_one_host}")

private String redisTwoHost;

//redis two port

@Value("${redis_one_port}")

private int redisTwoPort;

//redis two password

@Value("${redis_two_password}")

private String redisTwoPassword;

//redis client timeout

@Value("${redis_client_timeout}")

private int redisClientTimeout;

@Bean(name="redisPool")

public ShardedJedisPool createRedisPool () throws Exception {

//设置连接池的相关配置

JedisPoolConfig poolConfig = new JedisPoolConfig();

poolConfig.setMaxTotal( 5 );

poolConfig.setMaxIdle( 2 );

poolConfig.setMaxWaitMillis( 5000 );

poolConfig.setTestOnBorrow( false );

poolConfig.setTestOnReturn( false );

//设置Redis信息

JedisShardInfo shardInfo1 = new JedisShardInfo(redisOneHost,redisOnePort, redisClientTimeout);

shardInfo1.setPassword(redisOnePassword);

JedisShardInfo shardInfo2 = new JedisShardInfo(redisTwoHost, redisTwoPort, redisClientTimeout);

shardInfo2.setPassword(redisTwoPassword);

//初始化ShardedJedisPool

List<JedisShardInfo> infoList = Arrays.asList(shardInfo1, shardInfo2);

ShardedJedisPool jedisPool = new ShardedJedisPool(poolConfig, infoList);

return jedisPool;

}

public static void main (String[] args) {

ShardedJedis shardedJedis = null ;

try {

RedisConfiguration redisConfiguration = new RedisConfiguration();

ShardedJedisPool shardedJedisPool = redisConfiguration.createRedisPool();

shardedJedis = shardedJedisPool.getResource();

shardedJedis.set( "CSDN" , "56" );

shardedJedis.set( "InfoQ" , "44" );

shardedJedis.set( "CNBlog" , "13" );

shardedJedis.set( "SegmentFault" , "22" );

Client client1 = shardedJedis.getShard( "CSDN" ).getClient();

Client client2 = shardedJedis.getShard( "InfoQ" ).getClient();

Client client3 = shardedJedis.getShard( "CNBlog" ).getClient();

Client client4 = shardedJedis.getShard( "SegmentFault" ).getClient();

System.out.println( "CSDN 位于实例:" + client1.getHost() + "|" + client1.getPort());

System.out.println( "InfoQ 位于实例:" + client1.getHost() + "|" + client1.getPort());

System.out.println( "CNBlog 位于实例:" + client1.getHost() + "|" + client1.getPort());

System.out.println( "SegmentFault 位于实例:" + client1.getHost() + "|" + client1.getPort());

} catch (Exception e){

e.printStackTrace();

} finally {

shardedJedis.close();

}

}

}

运行后打印出来的日志看,这几个值是存放到 不同的Redis实例 中,但是在客户端使用的时候究竟如何分配到不同的分片具体有Jedis实现的一致性哈希算法所决定的;当然,它所支持的默认算法是64位的MURMUR_HASH算法,另外也支持MD5哈希算法。

"C:\Program Files\Java\jdk1.8.0_102\bin\java"...

CSDN 位于实例:192.168.32.101|6379

InfoQ 位于实例:192.168.32.102|6380

CNBlog 位于实例:192.168.32.101|6379

SegmentFault 位于实例:192.168.32.102|6380

三、后话

区别于Redis Sharding这种轻量方案,Redis Cluster是Redis官方于Redis 3.0发布后推出的一种服务端分片的解决方案,它解决了多Redis实例下的协同问题。这里的协同包含数据自动分片、不同哈希槽(slot)的故障自动转移、新节点扩容等功能。你看,数据分片以前是靠客户端自己解决的,哈希槽故障自动转移以前是靠额外的哨兵机制解决的,现在官方搞个整体解决方案以帮助客户端轻量化以便客户端能够更聚焦于业务逻辑的开发工作。

我的理解是Redis Cluster是一个去中心化的集群解决方案,集群中的每个节点都是平等的(因为每个节点都知道整个集群其他节点的信息,如IP、端口、状态等,每个节点间都是相互通过长链保持通讯的),它跟Sentinel的本质上的差异也在于这里。一个去中心化模式,另一个集中式的主从模式。

具体cluster如何做 数据自动分片、故障自动转移、节点扩缩容 等细节,后面通过另外一篇博客中整理再发布吧,今天先到这里。

四、参考

https://www.cs.princeton.edu/courses/archive/fall09/cos518/papers/chash.pdf http://www.cs.columbia.edu/~asherman/papers/cachePaper.pdf


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK