目录
为什么需要Redis集群
为什么需要集群?
性能
扩展
可用性
Redis主从复制(replication)
主从复制配置
主从复制原理
连接阶段
数据同步阶段
命令传播阶段
主从复制的不足
可用性保证Sentinel(哨兵)
Sentinel原理
服务下线
故障转移
Sentinel的功能总结
Sentinel实战
Sentinel配置
Sentinel验证
Sentinel连接使用
哨兵机制的不足
Redis分布式方案
客户端Sharding
ShardedJedis
代理Proxy
Twemproxy
Codis
Redis Cluster
架构
数据分布
客户端重定向
数据迁移
高可用和主从切换原理
总结
Redis本身的QPS已经很高了,但是如果在一些并发量非常高的情况下,性能还是会受到影响。这个时候我们希望有更多的Redis服务来完成工作。
第二个是处于存储的考虑。因为Redis所有的数据都放在内存中,如果数据量大,很容易受到硬件的限制。升级硬件收效和成本比太低,所以我们需要有一种横向扩展的方法。
第三点是可用性和安全的问题。如果只有一个Redis服务,一旦服务宕机,那么所有的客户端都无法访问,会对业务造成很大影响。另一个,如果硬件发生故障,而单机的数据无法恢复的话,带来的影响也是灾难性的。
可用性、数据安全、性能都可以通过启动多个Redis服务实现。其中有一个是主节点(master),可以有多个从节点(slave)。主从之间通过数据同步,存储完成相同的数据。如果主节点发生故障,则把某个从节点改成主节点,访问新的主节点。
假设一主多从,203是主节点,在每个slave节点的redis.conf配置文件增加一行
slaveof 192.168.8.203 6379
在主从切换的时候,这个配置会被重写成:
replicatof 192.168.8.203 6379
或者在启动服务时通过参数指定master节点:
./redis-server --slaveof 192.168.8.203 6379
或在客户端直接执行slaveof ip port,使该Redis示例称为xx的从节点。
启动后,查看集群状态:
#进入客户端
info replication
从节点不能写入数据(只读),只能从master节点同步数据,get成功,set失败。
set king test
#(error) READONLY You can't write against a read only replica.
主节点写入后,slave会自动从master同步数据。
断开复制:
slaveof no one
此时从节点会变成自己的主节点,不再复制数据。
当从节点变成了主节点的一个客户端之后,会给主节点发送ping请求。
延迟时不可避免地,只能通过优化网络。
repl-disable-tcp-nodelay no
当设置为yes时,TCP会对包进行合并从而减少带宽,但是发送的频率会降低,从节点数据延迟增加,一致性变差;具体发送频率与Linux内核的配置有关,默认配置为40ms。当设置为no时,TCP会立马将主节点的数据发送给从节点,带宽增加但延迟变小。
一般来说,只有当应用对Redis数据不一致的容忍度较高,且主从节点之间网络状态不好时,才会设置为yes;多数情况使用默认值no。
如果从节点有一段时间断开了与主节点的连接是否需要全量复制?增量,如何处理?
通过master_repl_offset记录的偏移量
#通过命令查看master_repl_offset
info replication
主从复制模式解决了数据备份和性能(通过读写分离)的问题,但是还是存在一些不足:
如何实现主从的自动切换呢?思路:
创建一台监控服务来监控所有Redis服务节点的状态,比如:master节点超过一定时间没有给监控服务器发送心跳报文,就把master标记为下线,然后把某一个slave变成master。应用每一次都是从这个监控服务器拿到master的地址。
问题:如果监控服务本身出问题了怎么办?
于是Redis引入了Sentinel的设计思路:通过运行监控服务器来保证服务的可用性。
从Redis2.8版本起,Redis提供了一个稳定版本的Sentinel(哨兵),用来解决高可用的问题。它是一个特殊状态的redis实例。
我们会启动一个或者多个Sentinel服务
src/redis-sentinel
它本质上只是一个运行在特殊模式之下的Redis服务,Sentinel通过info命令得到被监听Redis机器的master,slave等信息。
为了保证监控服务器的可用性,我们会对Sentinel做集群的部署。Sentinel既监控所有的Redis服务,也相互监控。
Sentinel本身没有主从之分,只有Redis服务节点有主从之分。
Sentinel默认以每秒钟1次的频率向Redis服务发送ping命令。如果在down-after-milliseconds内都没有收到有效回复,Sentinel会将该服务标记为下线(主观下线)。
#sentinel.conf
sentinel down-after-milliseconds
这个时候Sentinel节点会继续询问其他的Sentinel系欸但,确认主观下线的节点是否下线,如果多数Sentinel节点都认为其下线,此时该节点被确认下线(客观下线),这个时候就需要重新选举master。
如果master被标记为下线,就会开始故障转移流程。
既然有这么多的Sentinel节点,由谁来主导故障转移呢?
故障转移流程的第一步就是在Sentinel集群选出一个Leader,由Leader完成故障转移流程。Sentinel通过Raft算法,实现Sentinel集群的Leader选举。
Raft算法
在分布式存储系统中,通常通过维护多个副本来提高系统的可用性,那么多个节点之间必须要面对数据一致性的问题。Raft的目的就是通过复制的方式,使所有节点达成一致,但是这么多节点,以哪个节点的数据为准?所以必须选出一个Leader。
大体上有两个步骤:Leader选举,数据复制。
Raft是一个共识算法(consensus algorithm)。Spring Cloud的注册中心解决方案Consul也用到了Raft协议。
Raft的核心思想:先到先得,少数服从多数。
Raft算法演示:http://thesecretlivesofdata.com/raft/
总结:
Sentinel的Raft算法和Raft论文略有不同。
故障转移
关于从节点选举,一共有四个因素影响选举的结果,分别是断开连接时长、优先级排序、复制数量、进程id。
如果与Sentinel连接断开比较久,超过了一个阈值,就直接失去了选举权。如果拥有选举权,那就看谁的优先级高,这个在配置文件里可以设置(replica-priority 100),数值越小优先级越高。
如果优先级相同,就看谁从master中复制的数据最多(复制偏移量最大),选最多的那个,如果复制数量也相同,就选择进程id最小的那个。
不是我懒,是这样更准确,更权威。咳咳......
四大功能:
为了保证Sentinel的高可用,Sentinel也需要做集群部署,集群中至少需要三个Sentinel实例。
hostname | IP | 节点角色&端口 |
master | 192.168.8.203 | Master:6379/Sentinel:26379 |
slave1 | 192.168.8.204 | Slave:6379/Sentinel:26379 |
slave2 | 192.168.8.205 | Slave:6379/Sentinel:26379 |
以Redis安装路径/home/soft/redis-5.0.5/为例
在204和205的src/redis.conf配置文件中添加
slaveof 192.168.8.203 6379
在203、204、205创建Sentinel配置文件(安装后根目录下默认有sentinel.conf):
cd /home/soft/redis-5.0.5
mkdir logs
mkdir rdbs
mkdir sentinel-tmp
vim sentinel.conf
三台服务器内容相同,如下:
daemonize yes
port 26379
protected-mode no
dir "/home/soft/redis-5.0.5/sentinel-tmp"
sentinel monitor redis-master 192.168.8.203 6379 2
sentinel down-after-milliseconds redis-master 30000
sentinel failover-timeout redis-master 180000
sentinel parallel-syncs redis-master 1
#上面四个redis-master名称需要统一
参数解释:
启动Redis服务和Sentinel服务
cd /home/soft/redis-5.0.5/src
#启动Redis服务
./redis-server ../redis.conf
#启动Sentinel服务
./redis-sentinel ../sentinel.conf
#或者通过如下命令
./redis-server ../sentinel.conf --sentinel
查看集群状态:
info replication
模拟master宕机,在master服务端执行命令:
shutdown
观察其他节点集群状态查看role:是否从slave变成master
Jedis连接Sentinel
master name来自于sentinel.conf的配置
private static JedisSentinelPool createJedisPool() {
String masterName = "redis-master";
Set sentinels = new HashSet();
sentinels.add("192.168.8.203:26379");
sentinels.add("192.168.8.204:26379");
sentinels.add("192.168.8.205:26379");
pool = new JedisSentinelPool(masterName, sentinels);
return pool;
}
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
无论是Jedis还是Spring Boot(2.x版本默认是Lettuce),都只需要配置全部哨兵的地址,由哨兵返回当前的master节点地址。
主从切换的过程中会丢失数据,因为只有一个master。
只能单点写,没有解决水平扩容的问题。
如果数据量非常大,这个时候我们需要多个master-slave的group,把数据分布到不同的group中。
问题来了,数据怎么分片?分片之后,怎么实现路由?
如果要实现Redis数据的分片,我们有三种方案。第一种是在客户端实现相关的逻辑,例如用取模或者一致性哈希对key进行分片,查询和修改都先判断key的路由。
第二种是把分片处理的逻辑抽取出来,运行一个独立的代理服务,客户端连接到这个代理服务,代理服务做请求的转发。
第三种就是基于服务端实现。
Jedis客户端提供了Redis Sharding的方案,并且支持连接池。
public class ShardingTest {
public static void main(String[] args) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
// Redis 服务器
JedisShardInfo shardInfo1 = new JedisShardInfo("127.0.0.1", 6379);
JedisShardInfo shardInfo2 = new JedisShardInfo("192.168.8.205", 6379);
// 连接池
List infoList = Arrays.asList(shardInfo1, shardInfo2);
ShardedJedisPool jedisPool = new ShardedJedisPool(poolConfig, infoList);
ShardedJedis jedis = null;
try{
jedis = jedisPool.getResource();
for(int i=0; i<100; i++){
jedis.set("k"+i, ""+i);
}
for(int i=0; i<100; i++){
System.out.println(jedis.get("k"+i));
}
}finally{
if(jedis!=null) {
jedis.close();
}
}
}
}
使用ShardedJedis之类的客户端分片代码的优势是配置简单,不依赖于其他中间件,分区的逻辑可以自定义,比较灵活。但是基于客户端的方案,不能实现动态的服务增减,每个客户端需要自行维护分片策略,存在重复代码。
第二种思路就是把分片的代码抽取出来,做成一个公共服务,所有的客户端都连接到这个代理层。由代理层来实现请求和转发。
典型的代理分区方案有Twitter开源的Twemproxy和国内的豌豆荚开源的Codis。
Twemproxy的优点:比较稳定,可用性高。
不足:
Codis是一个代理中间件,用Go语言开发的。
功能:客户端连接Codis跟连接Redis没有区别。
Codis | Tewmproxy | Redis Cluster | |
重新分片不需要重启 | YES | NO | YES |
pipeline | YES | YES | |
多key操作的hash tags{} | YES | YES | YES |
重新分片时的多key操作 | YES | - | NO |
客户端支持 | 所有 | 所有 | 支持cluster协议的客户端 |
分片原理:Codis把所有的key分成N个槽(例如1024),每个槽对应一个分组,一个分组对应于一个或者一组Redis实例。Codis对key进行CRC32运算,得到一个32位的数字,然后模拟N(槽的个数),得到余数,这个就是key对应的槽,槽后面就是Redis的实例。比如4个槽:
Codis的槽位映射关系是保存在Proxy中的,如果要解决单点的问题,Codis也要做集群部署,多个Codis节点怎么同步槽和实例的关系呢?需要运行一个Zookeeper(或者etcd/本地文件)。
在新增节点的时候,可以为节点指定特定的槽位。Codis也提供了自动均衡策略。Codis不支持事务,其他的一些命令也不支持。
获取数据原理(mget):在Redis中的各个实例里获取到符合的key,然后再汇总到Codis中。
Codis是第三方提供的分布式解决方案,在官方的集群功能稳定之前,Codis也得到了大量的应用。
Codis是第三方提供的分布式解决方案,在官方的集群功能稳定之前,Codis也得到了大量的应用。
Redis Cluster是在Redis 3.0的版本正式推出的,用来解决分布式的需求,同时也可以实现高可用。跟Codis不一样,它是去中心化的,客户端可以连接到任意一个可用节点。
数据分片有几个关键的问题需要解决:
Redis Cluster可以看成是由多个Redis实例组成的数据集合。客户端不需要关注数据的子集到底存储在哪个节点,只需要关注这个集合整体。
以3主3从为例,节点之间的两两交互,共享数据分片、节点状态等信息。
如果是希望数据分布相对均匀的话,我们首先可以考虑哈希后取模。
哈希后取模
例如,hash(key)%N,根据余数,决定映射到哪一个节点。这种方式比较简单,属于静态的分片规则。但是一旦节点数量变化,新增或者减少,由于取模的N发生变化,数据需要重新分布。
为了解决这个问题,我们又有了一致性哈希算法。
一致性哈希
把所有的哈希值空间组织成一个虚拟的圆环(哈希环),整个空间按顺时针方向组织。因为是环形空间,0和2^32-1是重叠的。
假设我们有四台机器要哈希环来实现映射(分布数据),我们先根据机器的名称或者IP计算哈希值,然后纷纷不到哈希环中(红色圆圈)。
现在有4条数据或者4个访问请求,对key计算后,得到哈希环中的位置(绿色圆圈)。沿哈希环顺时针找到的第一个Node,就是数据存储的节点。
在这种情况下,新增了一个Node5节点,不影响数据的分布。
删除了一个节点Node4,只影响相邻的一个节点。
谷歌的MurmurHash就是一致性哈希算法。在分布式系统中,负载均衡、分库分表等场景中都有应用。
一致性哈希解决了动态增减节点时,所有数据都需要重新分布的问题,它只会影响到下一个相邻的节点,对其他节点没有影响。
但是这样的一致性哈希算法有一个缺点,因为节点不一定是均匀地分布的,特别是在节点数比较少的情况下,所以数据不能得到均匀分布。解决这个问题的办法是引入虚拟节点(Virtual Node)。
比如:2个节点,5条数据,只有1条分布到Node2,4条分布到Node1,不均匀。
Node1设置了两个虚拟节点,Node2也设置了两个虚拟节点(虚线圆圈)。
这时候有3条数据分布到Node1,1条数据分布到Node2.
Redis虚拟槽分区
Redis既没有用哈希取模,也没有用一致性哈希,而是用虚拟槽来实现的。
Redis创建了16384个槽(slot),每个节点负责一定区间的slot。比如Node1负责0-5460,Node2负责5461-10922,Node3负责10923-16383 。
Redis的每个master节点维护一个16384位(2048bytes=2KB)的位序列,比如:序列的第0位是1,就代表第一个slot是它负责;序列的第一位是0,代表第二个slot不归它负责。
对象分布到Redis节点上时,对key用CRC16算法计算再%16384,得到一个slot的值,数据落到负责这个slot的Redis节点上。
查看key属于哪个slot:
cluster keyslot king
注意:key与slot的关系是永远不会变的,会变得只有slot和Redis节点得关系。
怎么让相关得数据落到同一个节点上
比如有些multi key操作是不能跨节点得,如果要让某些数据分布到一个节点上,例如用户2673得基本信息和金融信息,怎么办?
在key里面加入{hash tag}即可。Redis在计算槽编号得时候只会获取{}之间得字符串进行槽编号计算,这样由于上面两个不同得键,{}里面得字符串是相同得,因此它们可以被计算出相同得槽。
比如在7291端口得Redis得redis-cli客户端操作:
set king 1
#(error) MOVED 13724 127.0.0.1:7293
服务端返回MOVED,也就是根据key计算出来的slot不归7191端口管理,而是归9293端口管理,服务端返回MOVED告诉客户端去7293端口操作。
这个时候更换端口,用redis-cli -p 7293操作,才会返回OK。或者用
./redis-cli -c -p port
-c代表cluster。这样客户端需要连续两次。
Jedis等客户端会在本地维护一份slot——node的映射关系,大部分时候不需要重定向,所以叫做smart jedis(需要客户端支持)。
新增或下线了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节点的过程,其过程如下:
Redis Cluster既能够实现主从的角色分配,又能够实现主从切换,相当于集成了Replication和Sentinel的功能。
优势:
不足: