1.为什么需要redis集群?
1.1 为什么需要集群?
1.1.1 性能
Redis本身的QPS已经很高了,但是如果在一些并发量非常高的情况下,性能还是会受到影响。这个时候我们希望有更多的Redis服务来完成工作。
1.1.2 扩展
第二个是出于存储的考虑。因为 Redis所有的数据都放在内存中,如果数据量大,很容易受到硬件的限制。升级硬件收效和成本比太低,所以我们需要有一种横向扩展的方法。
1.1.3 可用性
第三个是可用性和安全的问题。如果只有一个 Redis服务,一旦服务宕机,那么所有的客户端都无法访问,会对业务造成很大的影响。另一个,如果硬件发生故障,而单机的数据无法恢复的话,带来的影响也是灾难性的。
可用性,数据安全,性能都可以通过搭建多个Redis服务来实现,其中有一个主节点master,可以有多个从节点slave,主从之间数据同步,储存完全相同的数据,如果主节点发生故障,则把某个从节点改成主节点,访问新节点
Redis的主从复制(replication)
2.1主从复制配置
例如一台服务器主节点的是203,在每个从节点的redis.conf文件中增加一行
slaveof 192.xx.xx.203 6379;
//主从切换时,这个配置会被重写为
replicaof 192.xx.xx.203 6379;
或者在启动服务时通过参数的指定master节点
节点:
./redis-server–slaveof 192.xx.xx.203 6379
或在客户端直接执行slaveof xx xx,使该Redis实例成为从节点。
启动后,查看集群状态:
info replication
从节点不能写数据,只能读数据,只能从master节点上同步数据,get 成功,set失败;
主节点写入后,slave节点会自动从master同步数据
断开复制
slaveof no one;
2.2 主从复制原理
2.2.1 连接阶段
1.slave node 启动时(slaveof 命令),会在本地保存master node 的信息,包括master node的host和ip;
2.slave node 内部有个定时任务replicationCron,每隔1s中检查是否有新的master node 要连接和复制,如果发现,就跟master建立socket网络连接,如果连接成功,从该节点为socket建立一个专门处理复制工作的文件处理器,负责后续的复制工作,如接口RDB文件,传播广播等;
2.2.2 数据同步阶段
1.master node 第一次执行全量复制,通过bgsave命令在本地生成一份RDB快照,将快照文件发送给slave node(如果超时,会重连,可以调大repl-timeout的值),slave node 先要清除自己的旧数据,然后用RDB文件加载数据;
问题:生成RDB之间,master接受命令要怎么处理?
开始生成RDB时,master会把新的命令写到缓存在内存中,在slave node复制完成后,再将新的命令复制给从节点;
2.2.3 命令的传播阶段
1.master node持续写命令,异步复制给slave node;
repl-disable-tcp-nodelay no
当设置为yes,tcp会对包进行合并并减少带宽,但是发送频率会降低,从节点数据延迟降低,一致性变差;具体的发送频率与Linux内核有关,默认配置为40ms,当设置为no时,tcp立即将数据从主节点发送到从节点,带宽增加,延迟降低;一般来说,只有当应用对Redis数据不一致的容忍度比较高,且网络状态不好,才设置成yes,多数情况下设置为no。
问题:如果从节点一段时间断开了从主节点上复制数据,是不是要再复制一遍?
不用,通过master_repl_offset 记录偏移量;
2.3 主从复制的不足
主从复制解决了数据备份和性能(读写分离),但还存在一些问题:
1.RDB文件过大,同步非常消耗
2.在一主一从,一主多从的情况下,如果主服务器挂了,对外提供的服务器就不可用,单点问题没有解决,如果手动把之前的服务器切换成主服务器,这个比较费力,还会造成应用一定时间的不可用;
3.可用性保证之Sentinel
3.1Sentinel
创建一台监控服务器来监控redis服务节点的状态,比如,master节点超过一段时间没有给监控服务器发送心跳报文,就把master标记为下线,然后把某个slave节点变成master节点,应从每次都从监控器上拿到master系欸但信息;
问题:如果监控器挂掉了,就拿不到应用了。
那我们在创建一台监控来监控;
Redis的Sentinel就是从过运行监控服务器来保证服务的可用性;
从Redis2.8后,提供了稳定版本的Sentinel(哨兵),用来解决高可用的问题;
我们会启动一个或者多个sentinel服务,它的本质是运行在特殊模式下的redis服务,Sentinel通过info命令得到被监听的redis机器的master,slave等信息。
为了保证服务的可用性,会对sentinel做集群的部署,sentinel即监控了所有的redis服务,sentinel之间也相互监控;
Sentinel本身没有主从之分;
3.1.1 服务下线
sentinel 默认每秒中1频率向redis服务节点发送ping命令,如果在down-after-milliseconds 内没有收到有效回复,sentinel会将该服务器标记为下线(主观下线)
这个时候sentinel会继续询问其他的sentinel,确认这个节点是否可用,,如果多数节点认为这个master下线,master才会真正被下线(客观下线),这个时候就需要重新选举master;
3.1.2 故障转移
如果master被标记为下线,就会开始故障转移流程,故障转移的第一步就是在sentinel中选出一个leader,由leader完成故障转移流程,sentinel从过Raft算法,实现sentinel选举
Raft算法
在分布式存储系统中,通常通过维护多个副本来提高系统的高可用性,那么多节点面对的问题是数据一致性问题,raft算法就是从过复制的方式,使所有节点达成一致,这么多节点以哪个节点为准呢?必须选出leader;
大体上有两个步骤,领导选举,数据复制等;
核心思想:先到先得,少数服从多数;
Sentinle的Raft 算法和Raft论文略有不同。
1,master客观下线后触发选举,而不是从过election timeout时间开始选举。
2.leader并不会把自己称为leader的消息转发给多个sentinel,其他待leader从slave节点中选出master后,检测到新的master正常工作后,就会客观去掉下线标识,从而不需要故障转移流程;
故障转移
问题:怎么让原来的slave成为主节点?
1.选出sentinel Leader后,由它向某个节点发送slaveof on one命令,让它独立节点;
2.然后向其他节点发送slaveof X.x.x.x xxxx ,让他们成为本机的从节点。故障转移完成;
问题:这么多节点,选谁做主节点?
关于主节点选举,一般有4个因素,分别是断开连接时长,优先级,排序,复制数量,进程id;
如果断开的比较久,超过某个阈值,就直接失去选举权,如果拥有选举权,那就看看谁的优先级比较高,这个配置文件中可以配置(replica-priority 100) ,数量越小,优先级越高;如果优先级相同,则看谁从master复制的数据量大,选最多的那个;
sentinel实战
配置
为了保证 Sentinel 的高可用,Sentinel 也需要做集群部署,集群中至少需要三个
Sentinel实例(推荐奇数个,防止脑裂)。
以Redis安装路径/usr/local/soft/redis-5.0.5/为例。
在204和205的src/redis.conf配置文件中添加
slaveof192.xx.x.xxx 6379
在203、204、205创建sentinel配置文件(安装后根目录下默认有sentinel.conf) :
cd/usr/local/soft/redis-5.0.5
mkdir logs
mkdir rdbs
mkdir sentinel-tmp
daemonize yes
port 26379
protected-modeno dir"/usr/local/soft/redis-5.0.5/sentinel-tmp"
sentinel monitor redis-master192.xx.x.xxx 63792
sentinel down-after-milliseconds redis-master 30000
sentinel failover-timeoutredis-master 180000
sentinel parallel-syncsredis-master1
上面出现了4 个’redis-master’,这个名称要统一,并且使用客户端(比如 Jedis)连接的时候名称要正确。
cd/usr/local/soft/redis-5.0.5/src
#启动 Redis 节点 ./redis-server…/redis.conf
#启动 Sentinel 节点 ./redis-sentinel…/sentinel.conf # 或者 ./redis-server…/sentinel.conf–sentinel
查看集群状态
redis>info replication
3.3.3 Sentinel 连接使用
master name来自于sentinel.conf的配置。
private static JedisSentinelPool createJedisPool(){ StringmasterName="redis-master"; Set<String>sentinels=newHashSet<String>(); sentinels.add("192.168.8.203:26379"); sentinels.add("192.168.8.204:26379"); sentinels.add("192.168.8.205:26379"); pool=newJedisSentinelPool(masterName,sentinels); returnpool; }
Spring Boot连接Sentinel
spring.redis.sentinel.master=redis-master spring.redis.sentinel.nodes=192.168.8.203:26379,192.168.8.204:26379,192.168.8.205:26379
3.4 哨兵机制的不足
主从切换过程会丢失数据,因为只有一个master.
只能单点写,没有水平解决扩容问题;
如果数据量非常大,这个时候我们需要多个master组,把数据分布到不同的group中;
那么问题来了?数据怎么分片,分片后,怎样实现路由?
4.redis分布是方案
如果要实现分片,我们有三种不同的方案,第一种实在客户端,实现相关逻辑,例如取模或者一致性hash,查询和修改先判断key的路由;
第二种是把分片抽取出来单独做成一个代理,有客户端连接到这个代理,再由这个代理服务做出请求转发;
第三种就是基于服务端的实现
4.1 客户端sharing
publicclassShardingTest{ publicstaticvoidmain(String[]args){ JedisPoolConfigpoolConfig=newJedisPoolConfig();
//Redis 服务器 JedisShardInfoshardInfo1=newJedisShardInfo("127.0.0.1",6379); JedisShardInfoshardInfo2=newJedisShardInfo("192.168.8.205",6379);
// 连接池 List<JedisShardInfo>infoList=Arrays.asList(shardInfo1,shardInfo2); ShardedJedisPooljedisPool=newShardedJedisPool(poolConfig,infoList);
ShardedJedisjedis=null; try{ jedis=jedisPool.getResource(); for(inti=0;i<100;i++){ jedis.set("k"+i,""+i); } for(inti=0;i<100;i++){ System.out.println(jedis.get("k"+i)); }
}finally{ if(jedis!=null){ jedis.close();
4.2 代理
典型的代理分区方案有Twitter开源的Twemproxy和国内的豌豆荚开源的Codis。
优点:比较稳重,可用性高;
不足:
1.出现故障不能自动转移,架构复杂;
2.扩缩容需要修改配置,不能实现平滑的扩缩容;
4.2 codis
分片原理:codis把所有key分成n个槽,每个槽由对应的一个分组,一个分组对应有一个或者一组redis实例,codis对key进行了crc32运算,得到一个32为数字,然后模拟n个槽,得到的余数,就是这个key所对应的槽,槽后面就是redis实例,比如4个槽
codis的槽位映射关系是保存再proxy中,如果要解决单间问题,codies也要做集群,, 多个Codis节点怎么同步槽和实例的关系呢?需要运行一个Zookeeper (或者etcd/本地文件)。
4.3Redis Cluster
redis3.0版本后,用来解决分布式问题,同时也可以实现高可用,客户端可以连接到任意一个可用节点;
数据分片有几个关键的问题需要解决?
- 数据如何相对均匀的分布
- 客户端怎么样访问到相应的节点和数据
- 重新分配的过程中,如果保证正常服务
4.3.1 架构
redis cluster 可以看成是由多个Redis实例组成的数据集合,客户端不需要再关注数据子集到底存放到哪个节点,只需要关注集合整体
以三主三从为例,节点之间两两交互,共享数据分片,节点状态等;
4.3.2搭建
配置 启动 进入客户端: redis-cli-p7291 redis-cli-p7292 redis-cli-p7293 批量插入数据
4.3.3 数据分布
如果希望数据分布的均匀,可以考虑hash和取模
- 哈希后取模
例如,hash(key)%N,根据余数,决定映射到那一个节点。这种方式比较简单,属于静态的分片规则。但是一旦节点数量变化,新增或者减少,由于取模的N发生变化,数据需要重新分布。
为了解决这个问题,我们又有了一致性哈希算法。 - 一致性hash
把所有的哈希值空间组织成一个虚拟环,整个空间按照顺时针方向组织,因为是环空间,0和2^32-1是重叠的。
假设我们有4台集器要用哈希环来实现映射,我们根据集器的ip来计算哈希值,然后分布到哈希环中
现在有4条数据访问请求,对key进行计算后,得到的是哈希环中绿色的位置,沿哈希环顺时针找到的第一个Node,就是数据存储的节点。
在这种情况下,新增了一个Node5节点,不影响数据的分布。
删除了一个节点Node4,只影响相邻的一个节点。
一致性哈希解决了动态增减节点时,所有数据都需要重新分布的问题,它只会影响到下一个相邻的节点,对其他节点没有影响。
但是这样的一致性哈希算法有一个缺点,因为节点不一定是均匀地分布的,特别是在节点数比较少的情况下,所以数据不能得到均匀分布。解决这个问题的办法是引入虚拟节点(Virtual Node)。
Node1设置了两个虚拟节点,Node2也设置了两个虚拟节点(虚线圆圈)。
这时候有3条数据分布到Node1,1条数据分布到Node2。
redis虚拟槽分配
Redis创建了16384个槽,每个节点负责一定的空间,比如Node1负
责0-5460,Node2负责5461-10922,Node3负责10923-16383。
Redis的每一个master节点维护一个16384位(2048Kb==2kb)的位序列;
对象分布到Redis节点上,对key进行CRC16算法后再%16384,得到槽的位置,数据落到负责这个槽的节点上;
redis>cluster keyslot qingshan;
问题:怎么让相关数据落到一个槽上?
比如有些multikey操作是不能跨节点的,如果要让某些数据分布到一个节点上,例如用户2673的基本信息和金融信息,怎么办?
在key里面加入{hash tag}即可。Redis在计算槽编号的时候只会获取{}之间的字符串进行槽编号计算,这样由于上面两个不同的键,{}里面的字符串是相同的,因此他们可以被计算出相同的槽。
问题:客户连接到那台服务器上?访问数据不在该节点上,怎么办?
服务端返回moved,也就是根据key计算槽的位置,服务端会告诉你需要去那台机器上访问,这个时候 更换端口,用redis-cli -p 7239才会返回ok,或者用./redis-cli -c-p port 的命令(c代表cluster)。这样客户端需要连接两次。
jedis客户端会在本地维护一份slot–node的映射关系,大部分时候不需要定向;
问题:新增master或者下线了,数据怎么迁移和重新分配?
因为key和slot的关系永远不会变的,当新增节点,需要把原来相关slot的数据迁移过来就行;添加新节点(新增一个7297):
redis-cli–cluster add-node 127.0.0.1:7291 127.0.0.1:7297
新增的节点没有哈希槽,不能分布数据,在原来的任意一个节点上执行:
redis-cli–cluster reshard 127.0.0.1:7291
输入需要分配的哈希槽的数量(比如500),和哈希槽的来源节点(可以输入all或者id)。
问题:一个主节点挂了,从节点怎么变成主节点?
当slave发现自己的master变成fail时,便尝试惊醒FailOver,以期待成为行动master,由于挂掉的master有多个slave节点,从而slave竞争master节点的过程如下:
- slave发现自己的master变成FAIL;
- 将自己记录的集群currentEpoch加1,并广播FAILOVER_AUTH_REQUEST信息
- 其他节点收到该信息,只有master响应,判断请求的合法性,并发送一个FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack
- 尝试failover的slave收集FAILOVER_AUTH_ACK
- 超过半数后就变成新的Master
- 广播Pong通知其他集群节点
Redis cluster 既能实现主从分配的角色,又能实现主从切换,相当于继承了repilcation和sentinel;
4.7.3 优缺点
优点:
- 没有中心框架
- 数据按照slot分布多个数据节点,节点之间数据共享,可动态调整数据分布
- 可扩展性,可扩展到1000个节点,节点可动态的添加或者删除
- 高可用,部分节点不可用了,集群仍然可用,增加slave做数据副本,能够实现故障自动failover,节点间通过gossip协议交换状态信息,用投票机制完成slaver到master选举
- 降低运营成本,提高系统的高可用性和扩展性
不足: - Client实现太复杂,驱动要求实现Smart Client,缓存slotsmapping信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。
- 节点会因为某些原因发生阻塞,被判断位下线,这种failover没有必要
- 数据通过异步复制,不能保证数据的强一致性
- 多个业务使用同一套集群,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况。