Redis(八) 集群

Posted by ZhouJ000 on November 17, 2018
最后更新于:2018-11-21

Redis(一) 基础与api
Redis(二) 小功能
Redis(三) 阻塞与内存
Redis(四) 缓存设计
Redis(五) 客户端调用
Redis(六) 持久化与复制
Redis(七) 哨兵
Redis(八) 集群

Cluster集群

数据分布

分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据画风到多个节点上,每个节点负责整体数据的一个子集

常见的分区规则有哈希分区和顺序分区:

  • 哈希分区
    • 特点:离散度好、数据分布业务无关、无法顺序访问
    • 代表:Redis Cluster、Cassandra、Dynamo
  • 顺序分区
    • 特点:离散度容易倾斜、数据分布业务相关、可顺序访问
    • 代表:Bigtable、HBase、Hypertable

常见的哈希分区规则:

1、节点取余分区:使用特定的数据,如redis的键或用户ID,再根据节点数量N使用公式:hash(key)%N 计算出哈希值,用来决定数据映射到哪个节点上。这种方案优点是简单,常用于数据库的分库分表规则,一般采用预分区的方式,提前根据数据量规划好分区数,保证可以支撑未来一段时间的数据量,再根据负载情况将表迁移到其他数据库中。扩容时采用翻倍扩容,避免数据映射全部被打乱导致全量迁移的情况

2、一致性哈希分区:实现思路是为系统中每个节点分配一个token,范围一般在0~2^32,这些token构成一个哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点。这种方案比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。但一致性哈希分区存在几个问题:加减节点会造成哈希环中部分数据无法命中,需要手动处理或忽略这部分数据,因此一致性哈希常用与缓存场景;当使用少量节点时,节点变化将大范围影响哈希环中数据映射,因此这种方案不适合少量数据节点的分布式方案;普通的一致性哈希分区在增减节点时需要增加一倍或减去一半的节点才能保证数据和负载的均衡。因此一些分布式系统采用虚拟槽对一致性哈希进行改进,比如Dynamo系统

3、虚拟槽分区:巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。这个范围一般远远大于节点数,比如Redis Cluster槽范围是0~16383。槽是集群内数据管理和迁移的基本单位,采用大范围槽的主要目的是为了方便数据拆分和集群扩展。每个节点会负责一定数量的槽

Redis Cluser就采用了虚拟槽分区,所有的键根据哈希函数映射到0~16383整数槽内,计算公式:slot=CRC16(key)&16383。每个节点负责维护一部分槽以及槽所映射的键值数据

redis虚拟槽分区特点
1、解耦数据与节点之间的关系,简化了节点扩容和收缩难度
2、节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据
3、支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩灯场景

redis集群功能限制
1、key批量操作支持有限。如mset、mget,目前只支持具有相同slot值的key执行批量操作
2、key事务操作支持有限,同理只支持多key在同一节点上的事务操作
3、key作为数据分区的最小粒度,因此不能将一个大的键值对象如hash、list等映射到不同的节点
4、不支持多数据库空间
5、复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构

搭建集群

1、准备节点

redis集群一般由多个节点组成,节点数量至少6个才能保证组成高可用的集群。每个节点需要开启配置cluster-enabled yes,让redis运行在集群模式下。建议为集群内所有节点统一目录,一般划分为三个目录:conf、data、log,分别存放配置、数据和日志相关文件

prot 6379
cluster-enabled yes
cluster-node-timeout 15000
cluster-confile-file "nodes-6379.conf"

分别按端口号启动6个服务节点,但是每个节点彼此并不知道对方的存在

2、节点握手

让一批运行在集群模式下的节点通过Gossip协议彼此通信,到达感知对方

由客户端发起命令:cluster meet {ip} {port}
cluster meet命令是一个异步命令,执行之后立即返回。内部发起与目标节点进行握手通信:
1、节点6379本地创建6380节点信息对象,并发送meet消息
2、节点6380接收到meet消息后,保存6379节点信息并回复pong消息
3、之后节点6379与6380彼此定期通过ping/pong消息进行正常的节点通信

只需要在集群内任意节点上执行cluster meet命令加入新节点,握手状态会通过消息在集群内传播,这样其他节点会自动发现新节点并发起握手流程

节点建立握手之后集群还不能正常工作,这时集群处于下线状态,所有的数据读写都被禁止。由于目前所有的槽没有分配到节点,因此集群无法完成槽到节点的映射。只有当16384个槽全部分配给节点后,集群才进入在线状态

3、分配槽

redis集群把所有数据映射到16384个槽中。每个key会映射为一个固定的槽,只有当前节点分配了槽,才能相应和这些槽关联的键命令。通过cluster addslots命令为节点分配槽:redis-cli -h 127.0.0.1 -p 6379 cluster addslots {0...5461}

分配3个节点后,通过执行cluster info查看集群状态,可见集群状态是OK,集群进入在线状态

目前还有3个节点没有被使用,作为一个完整的集群,每个负责处理槽的节点应该具有从节点,保证当它出现故障时可以自动进行故障转移。集群模式下,redis节点角色分为主节点和从节点。首次启动的节点和被分配槽的节点都是主节点,从节点负责复制主节点槽信息和相关数据

使用cluster replicate {noedId}命令让一个节点成为从节点,其中命令必须在对应从节点上执行,nodeId是要复制主节点的节点ID

使用cluster nodes命令查看集群状态和复制关系

使用redis-trib.rb创建集群

redis-trib.rb是采用ruby实现的redis集群管理工具。内部通过cluster相关命令简化集群创建、检查、槽转移和均衡等常见运维操作

节点通信

在分布式存储中需要提供维护节点元数据信息的机制,所谓元数据是指:节点负责哪些数据,是否出现故障转移等状态信息。常见的元数据维护方式分为:集中式和P2P方式。redis集群采用P2P的Gossip(流言)协议,Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有节点都知道集群完整的信息

常见Gossip消息可分为:ping消息、pong消息、meet消息、fail消息等

集群内所有的消息都采用相同的消息头结构clusterMsg,它包含了发送节点关键信息,如节点ID、槽映射、节点表示(主从角色、是否下线)等

集群伸缩

集群伸缩 = 槽和数据在节点之间的移动

扩容集群

1、准备新节点
2、加入集群
3、迁移槽和数据

1,2步骤和之前一样,启动后通过cluster meet加入集群

新节点刚开始都是主节点状态,但是由于没有负责的槽,所以不能接受任何读写操作。对于新节点后续一般有两种选择:
1、为它迁移槽和数据实现扩容
2、作为其他主节点的从节点负责故障转移

槽在迁移过程中集群可以正常提供读写服务,迁移过程是集群扩容最核心的环节:
1) 槽迁移计划
首先需要为新节点指定槽的迁移计划,确定原有节点的哪些槽需要迁移到新节点,需要保证每个节点负责相似数量的槽,从而保证各节点的数据均匀

2) 迁移数据
数据迁移过程是逐个槽进行的,每个槽数据迁移过程:
1、对目标节点发送cluster setslot {slot} importing {sourceNodeId}命令,让目标节点准备导入槽的数据
2、对源节点发送cluster setslot {slot} migrating {targetNodeId}命令,让源节点准备迁出槽准备
3、源节点循环执行cluster getkeysinslot {slot} {count}命令,获取count个属于槽{slot}的键
4、在源节点上执行migrate {targetIp} {targetPort} “” 0 {timeout} keys {keys…}命令,把获取的键通过流水线(pipeline)机制批量迁移到目标节点(批量迁移命令在redis 3.0.6以上版本提供)
5、重复步骤3与4,直到槽下所有的键值数据迁移到目标节点
6、向集群内所有主节点发送cluster setslot {slot} node {targetNodeId}命令,通知槽分配给目标节点。为了保证槽节点映射变更及时传播,需要遍历发送给所有主节点更新被迁移的槽指向新节点

redis-trib.rb提供了槽分片功能(reshard命令)

3) 添加从节点
使用cluster replicate {masterNodeId}命令为主节点添加从节点,具备故障转移功能

收缩集群

1) 首先需要确认下线节点是否有负责的槽,如果是需要把槽转移到其他节点,保证节点下线后整个集群槽节点映射的完整性
2) 当下线节点不再负责槽或者本身是从节点时,可以通知集群内其他节点忘记下线节点,当所有节点忘记后该节点可以正常关闭

收缩正好和扩容转移方向相反,下线节点变为源节点,其他主节点变为目标节点,源节点需要把自身负责的槽均匀分配到其他主节点上

使用cluster forget {nodeId}命令,当节点收到该命令后,会把nodeId指定的节点加入禁用列表中,在禁用列表中的节点不再发送Gossip消息。禁用列表有效期是60秒,也就是说当一次forget命令发出后,我们有60秒的时间让集群内的所有节点忘记下线节点

线上操作不建议直接使用cluster forget命令下线节点,需要和大量节点命令交互,操作复杂且容易遗漏节点。建议使用redis-trib.rb del-node {host:port} {downNodeId}命令

请求路由

redis集群对客户端通信协议做了比较大的修改,为了追求性能最大化,并没有采用代理的方式而是采用客户端直连节点的方式。因此对希望从单机切换到集群环境的应用需要修改客户端代码

请求重定向

在集群模式下,redis接受任何键相关命令时首先计算键对应的槽,再根据槽找出所对应的节点,如果节点是自身处理键命令,如果不是回复MOVED重定向错误通知客户端请求正确的节点。这个过程称为MOVED重定向

借助cluster keyslot {key}命令返回key所对应的槽。使用redis-cli命令时,可以加入-c参数支持自动重定向,简化手动发起重定向操作

节点对不属于它的键命令只回复重定向响应,并不负责转发

正因为集群模式下把解析发起重定向的过程放到客户端完成,所以集群客户端协议相对于单机有了非常大的变化。键命令执行步骤主要分为两步:计算槽,查找槽所对应的节点

Smart客户端

Smart客户端通过在内部维护slot-node的映射关系,本地就可以实现键到节点的查找,从而保证IO效率最大化,而MOVED重定向负责协助Smart客户端更新slot-node映射

JedisCluster执行键命令(JedisClusterCommand):
1、计算slot并根据slots缓存获取目标节点连接,发送命令
2、如果出现连接错误,使用随机连接重新执行键命令,每次命令重试对redirections参数减1
3、捕获到MOVED重定向错误,使用cluster slots命令更新slots缓存(renewSlotCache方法)
4、重复执行1~3步骤,直到命令执行成功,或者当redirections小于等于0时抛出异常

1、客户端内部维护slots缓存表,并且针对每个节点维护连接池,当集群规模非常大时,客户端会维护非常多的连接并消耗更多的内存
2、使用jedis操作集群常见错误是JedisClusterMaxRedirectionsException,原因是节点宕机或请求超时会抛出JedisConnectionException,导致触发了随机重试,当重试次数耗尽抛出这个异常
3、当出现JedisConnectionException时,Jedis认为可能是集群节点故障需要随机重试来更新slots缓存
4、redis集群支持自动故障转移,但从故障到发现到完成转移需要一定时间,节点宕机期间所有指向这个节点的命令都会触发重试机制,每次收到MOVED重定向后会调用JedisClusterInfoCache类的renewSlotCache方法。获取写锁后再执行sluster slots命令初始化缓存,由于集群所有键命令都会执行getSlotPool方法计算槽对应节点,它内部要求读锁。ReentrantReadWriteLock是读锁共享且读写锁互斥,从而导致所有请求都会造成阻塞。对于并发高场景将极大影响集群吞吐量,这个现象称为cluster slots风暴:
— 1 ) 重试机制导致IO通信放大问题
— 2 ) 个别节点操作异常导致频繁的更新slots缓存,多次调用cluster slots命令,高并发时过渡消耗redis节点资源
— 3 ) 频繁更新本地slots缓存操作,内部使用写锁,阻塞对集群所有的键命令调用
针对以上问题Jedis2.8.2进行改造,当接受JedisConnectionException时不再轻易初始化slots缓存,大幅减低内部IO次数;更新slots缓存时,不再使用ping命令检测节点活跃度,并且使用redis covering变量保证同一时刻只有一个线程更新slots缓存,其他线程忽略,优化了写锁阻塞和cluster slots调用次数

1、JedisCluster定义

Set<HostAndPort> jedisClusterNode = new HashSet<>();
// add HostAndPort...
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
JedisCluster jedisCluster = new JedisCluster(jedisClusterNode, 1000, 1000, 5, poolConfig);

JedisCluster包含了所有节点的连接池,所以建议使用单例;JedisCluster每次操作完成后,不需要管理连接池的借还,它在内部已经完成;JedisCluster一般不要执行close操作,它会将所有JedisPool执行destroy操作

2、多节点命令和操作

redis cluser虽然提供了分布式特性,但是有些命令或操作,诸如keys,flushall,删除指定模式的键,需要遍历所有节点才能完成

public void delRedisClusterByPattern(JedisCluster jedisCluster, String pattern, int scanCounter) {
	Map<Stirng, JedisPool> jmap = jedisCluster.getClusterNodes();
	for (Entry<String, JedisPool> entry : jedisPoolMap.entrySet()) {
		Jedis jedis = entry.getValue().getResource();
		if (!isMaster(jedis)) {
			continue;
		}
		Pipeline pipeline = jedis.pipelined();
		String cursor = "0";
		ScanParams params = new ScanParams().count(scanCounter).match(pattern);
		while(true) {
			ScanResult<String> result = jedis.scan(cursor, params);
			List<Stirng> keyList = result.getResult();
			if (keyList != null && keyList.size() > 0) {
				for (String key: keyList) {
					pipeline.del(key);
				}
				pipeline.syncAndReturnAll();
			}
			cursor = scanResult.getStringCursor();
			if ("0".equals(cursor)) {
				break;
			}
		}
	}
}

private boolean isMaster(Jedis jedis) {
	String[] data = jedis.info("Replication").split("\r\n");
	for (Stirng line : data) {
		if ("role:master".equals(line.trim())) {
			return true;
		}
	}
	return false;
}

3、批量操作方法

由于key分布在各个节点上,会造成无法实现mget、mset等功能。但可以利用CRC16算法计算出key对应的slot,以及Smart客户端保存了slot和节点对应关系的特性,将属于同一个redis节点的key进行归档,然后分别对每个节点对应的子key列表执行mget或者pipeline操作

4、使用Lua、事务等特性

redis cluster提供了hashtag,可以将所需要操作的key使用一个hashtag

ASK重定向

redis集群支持在线迁移槽和数据来完成水平伸缩,当slot对应的数据从源节点到目标节点迁移过程中,客户端需要做到智能识别,保证键命令可以正常执行。

1、客户端根据本地slots缓存发送命令到源节点,如果存在键对象则直接执行并返回结果
2、如果键对象不在,则可能存在与目标节点,这时源节点会回复ASK重定向异常。格式(error) ASK {slot} {targetIp}:{targetPort}
3、客户端从ASK重定向异常提取出目标节点信息,发送asking命令到目标节点打开客户端连接标识,再执行键命令。如果存在则执行,不存在返回不存在信息

使用smart客户端批量操作集群时,需要评估mget/mset,Pipeline等方式在slot迁移场景下的容错性,防止集群迁移造成大量错误和数据丢失情况。建议优先使用Pipeline方式,在客户端实现对ASK重定向的正确处理,这样既可以受益于批量操作的IO优化,又可以兼容slot迁移场景

1) Pipeline严格按照键发送顺序返回结果,即使出现异常也是如此
2) 理解ASK重定向后,可以手动发起ASK流程保证Pipeline的结果正确性

故障转移

主观下线(pfail)、客观下线(fail)

故障恢复:
1、资格检查
2、准备选举时间
3、发起选举
4、选举选票
5、替换主节点

集群运维

集群完整性

带宽消耗

Pub/Sub广播问题

集群倾斜

集群读写分离

手动故障转移

数据迁移

分布式Redis服务的解决方案

redis_compared

codis和twemproxy最大的区别有两个:
1、codis支持动态水平扩展,对client完全透明不影响服务的情况下可以完成增减redis实例的操作
2、codis是用go语言写的并支持多线程,twemproxy用C并只用单线程。 后者又意味着:codis在多核机器上的性能会好于twemproxy;codis的最坏响应时间可能会因为GC的STW而变大,不过go1.5发布后会显著降低STW的时间;如果只用一个CPU的话go语言的性能不如C,因此在一些短连接而非长连接的场景中,整个系统的瓶颈可能变成accept新tcp连接的速度,这时codis的性能可能会差于twemproxy

codis和redis cluster的区别:
redis cluster基于smart client和无中心的设计,client必须按key的哈希将请求直接发送到对应的节点。Redis Cluster的成员管理(节点名称、IP、端口、状态、角色)等,都通过节点之间两两通讯,定期交换并更新,这是一种非常“重”的方案。而codis因其有中心节点、基于proxy的设计,对client来说可以像对单机redis一样去操作proxy(除了一些命令不支持),Codis引入了Group的概念,每个Group包括1个Redis Master及至少1个Redis Slave。采用预先分片(Pre-Sharding)机制,事先规定好了,分成1024个slots,这些路由信息保存在ZooKeeper中,ZooKeeper还维护Codis Server Group信息,并提供分布式锁等服务

参考:
codis
Codis——分布式Redis服务的解决方案
Codis 3集群搭建详解

监控云平台

redis私有云平台CacheCloud