主要是安全性和可用性的考虑,如果只有一个redis服务,一旦服务宕机,那么所有的客户端无法访问,会对业务造成很大影响,另一个是一旦硬件损坏,单机无法恢复,会对数据带来灾难性影响。另一个考虑是性能的提升,主从复制模式,写请求只能由master节点来处理,但是读请求可能用从节点来分担请求,提高效率。
可用性、数据安全、性能都可以通过搭建多个 Reids 服务实现。其中有一个是主节点(master),可以有多个从节点(slave)。主从之间通过数据同步,存储完全相同的数据。如果主节点发生故障,则把某个从节点改成主节点,访问新的主节点
使用哨兵模式来实现主从复制架构是为了主从节点的切换故障转移能够自动进行。
Redis 本身的 QPS 已经很高了,但是如果在一些并发量非常高的情况下,性能还是会受到影响。这个时候我们希望有更多的 Redis 服务来完成工作。
第二个是出于存储的考虑。因为 Redis 所有的数据都放在内存中,如果数据量大,很容易受到硬件的限制。升级硬件收效和成本比太低,所以我们需要有一种横向扩展的方法,使用分片技术,把数据分配到一个个集群redis组中。
实现主从复制架构的手段:
一主多从:
info replication 查看主从架构的状态。
从节点不能写入数据(只读),只能从 master 节点同步数据。get 成功,set 失败。
从节点取消指定主节点,自己成为主节点,也就是从节点断开slave模式:
slave no one 命令。
连接阶段:
数据同步阶段:
4. slave节点第一次会执行全量复制,master节点通过bgsave命令在本地生成一个RDB数据快照文件,然后将RDB文件通过Socket连接传送给从节点,然后从节点先清空自身数据,然后使用接收到的RDB文件加载数据。 传送文件会有个超时时间,如果超时了会进行重连,可以设置大一点,防止超时循环重连,配置文件修改repl-timeout配置项设置。
5. 在开始生成RDB文件时,master节点还会接收新的写命令,会把新的写命令缓存在内存中,在slave 节点通过RDB文件加载数据成功后,再把新的写命令发送给slave节点。
6. 命令传播阶段:从节点数据同步完成后,可以开始对外提供读服务,主节点还是会继续执行写命令,然后异步将命令发送给slave 节点用于同步数据。是异步的,所以数据一致性必然会存在延时,与Zookeeper采用CP模式保证数据分布式一致性不一样,redis使用的是AP保证服务可用性。
7. 如果从节点由于某些原因,比如宕机啥的,与主节点断开连接了一段时间,在重新连接后会使用增量复制来同步数据,主节点和从节点都有master_repl_offset命令来记录数据的偏移量,偏移量越大,数据越新,一般主节点都要大于等于从节点的偏移量,可以使用该偏移量来进行增量复制,增量的数据就是主节点与从节点直接的数据偏移量之差。
数据一致性延迟是不可避免的,只能通过优化网络来尽可能减少数据延时时间。
repl-disable-tcp-nodelay no
上面配置的作用是:当设置为yes时,TCP会对多个写命令TCP包进行合并一次性发送从而减少带宽,但是发送的频率会降低,从节点的数据延时会增加,一致性变差,具体发送频率与linux内核参数有关,默认为40ms,当设置为no时,TCP会立马把主节点的数据发送给从节点,带宽增加但延时变小。
一般来说,只有当应用对 Redis 数据不一致的容忍度较高,且主从节点之间网络状况不好时,才会设置为 yes;多数情况使用默认值 no。
主从复制的不足:
哨兵Sentinel模式的大致思路是创建一台服务器来监控所有的Redis服务节点状态,比如master节点超过一定时间没有给监控服务器发送心跳报文,就把master标记为下线,然后把某个slave节点变成master节点,重写进行数据的同步。应用程序(客户端)每一次都是从这个监控服务器拿到master的地址。
如果监控服务器本身出问题了怎么办?那我们就拿不到 master 的地址了,应用也没有办法访问。可以对sentinel服务器进行集群。
从 Redis2.8 版本起,提供了一个稳定版本的 Sentinel(哨兵),用来解决高可用的问题。它是一个特殊状态的 redis 实例。
我们会启动一个或者多个 Sentinel 的服务(通过 src/redis-sentinel),它本质上只是一个运行在特殊模式之下的 Redis,Sentinel 通过 info 命令得到被监听 Redis 机器的master,slave 等信息。
为了保证监控服务器的可用性,我们会对 Sentinel 做集群的部署。Sentinel 既监控所有的 Redis 服务,Sentinel 之间也相互监控。
注意:Sentinel 本身没有主从之分,只有 Redis 服务节点有主从之分。
此时就要以下角色
服务下线:
Sentinel默认以每秒一次的频率向各Redis服务节点发送PING命令,如果在down-after-milliseconds 内都没有收到有效回复,Sentinel 会将该服务器标记为主观下线。
sentinel down-after-milliseconds <master-name> <milliseconds> #sentinel.conf
这个时候 Sentinel 节点会继续询问其他的 Sentinel 节点,确认这个节点是否下线,如果多数(超过半数) Sentinel 节点都认为 master 下线,master 才真正确认被下线(客观下线),这个时候就需要重新选举 master。
由于Sentinel实现了集群,所以首先需要通过Raft算法在Sentinel集群中选取出一个临时Leader来对下线的master进行故障转移。
故障转移:
如何从多个slave节点中选择一个成为新的master节点:
关于从节点选举,一共有四个因素影响选举的结果,分别是断开连接时长、优先级排序、复制数量、进程 id。
如果与哨兵连接断开的比较久,超过了某个阈值,就直接失去了选举权。如果拥有选举权,那就看谁的优先级高,这个在配置文件redis.conf里可以设置(replica-priority 100),数值越小优先级越高。
如果优先级相同,就看谁从 master 中复制的数据最多(复制偏移量最大),选最多的那个,如果复制数量也相同,就选择进程 id 最小的那个。
环境:
需要三个服务节点一主二从和三个sentinel集群节点,由于虚拟机不够,这里就只用三台虚拟机,每台虚拟机部署一个服务节点和一个sentinel节点。
ip | 节点角色/端口 |
---|---|
192.168.18.140 | master 6379 / sentinel 26379 |
192.168.18.141 | salve 6379 / sentinel 26379 |
192.168.18.142 | slave 6379 / sentinel 26379 |
daemonize yes #以服务进程模式启动
port 26379 #端口
protected-mode no # 是否允许外部网络访问
dir "/usr/local/redis-5.0.5/sentinel-tmp" #sentinel的工作目录
sentinel monitor redis-master 192.168.18.140 6379 2 #要监控的服务节点 master节点的ip port 从节点个数
sentinel down-after-milliseconds redis-master 30000 #master宕机多久,才会被 Sentinel 主观认为下线。
sentinel failover-timeout redis-master 180000 #故障转移的超时时间
#指定了在进行主从切换故障转移时,最多可以有多少个salve同时对新的master进行数据同步,
#这个设置的越少,故障转移需要的时间越长,因为能够同时进行同步的slave越少,这个设置的越大,
#代表在故障转移期间能够对外提供读服务的slave节点就越少,因为在同步期间服务不可用。
#比如故障转移时有4个slave节点向新的master节点同步数据,如果设置为1,那么同步将串行执行,
#同一时间只有一台salve进行同步,另外三台还能对外提供服务,
#如果设置为4,那么会4台slave同时进行同步,那么此时就没有从服务器对外提供服务了。
sentinel parallel-syncs redis-master 1
先启动三台服务节点再启动三台sentinel节点:
./redis-server ../redis.conf
./redis-sentinel ../sentinel.conf
验证:
哨兵机制的不足:
通过对Redis进行分布式集群,可以达到以下效果:
有三种方案对数据进行分片:
以上两种方案都是对服务端透明的,也就是redis集群中的每个Group都不会知道其他Group的存在,只是有客户端或者中间件进行分片调配。
第三种方案是通过服务端实现集群,在Redis3.0之后,推出了Redis Cluster用来解决分布式的需求,同时也可以实现高可用。跟 Codis 不一样,它是去中心化的,客户端可以连接到任意一个可用节点。
数据分片的关键考虑问题:
Redis Cluster 可以看成是由多个 Redis 实例组成的数据集合。客户端不需要关注数据的子集到底存储在哪个节点,只需要关注这个集合整体。
下面是三主三从,三个Group组成的redis集群,集群中的所有节点都是两两间互相通信。
数据分布的策略:
如果想要数据分布相对均匀,可以考虑哈希后取模,hash(key)%N,N为Group的个数,根据余数决定把数据存储到哪个Group里面,取数据的时候也是使用该算法路由到指定Group取数,这种方式比较简单,属于静态分片,一旦Group数量发生变化,由于取模的N发生变化,所有数据需要全部重新分布。
把所以哈希值空间组成一个虚拟的圆环(哈希环),整个空间按顺时针方向组织,因为是环形空间,所以0和2^32-1是重叠的。
假设有四台机器要使用hash环来实现数据分布,我们可以先根据机器的名称、Ip等计算出一个哈希值,然后分布到哈希环中。
然后添加一个key,先把key进行哈希计算,得到一个哈希值,然后看看哈希值会落到圆环中的哪个位置,然后从那个位置开始顺时针遇到的第一个集群节点,该key就会落到该节点上。
如果新增了一个节点,只会影响新增节点顺时针方向遇到的第一个节点。
如上图,新增的Node5在Node1和Node4之间,只会影响Node1,假设Node4的哈希值是1,Node1的哈希值是100,Node5的哈希值是60,在没有Node5之前,对key进行哈希得到的哈希值为2-100的key会落到Node1上,新增了Node5之后,对key进行哈希得到的哈希值为2-60的key会落到Node5。61-100的key会落到Node1,因为之前有一些2-60的数据落到了Node1上,所以只需把Node1这部分key迁移到Node5,就完成了新增节点的数据重新分布,只影响了一个节点。
假如删除了一个节点,就把该节点的数据全部迁移到该节点顺时针方向遇到的第一个节点就完成了数据重新分布。
一致性哈希解决了动态增减节点时,所有数据都需要重新分布的问题,它只会影响到下一个相邻的节点,对其他节点没有影响
但是这样的一致性哈希算法有一个缺点,因为节点不一定是均匀地分布的,特别是在节点数比较少的情况下,所以数据不能得到均匀分布。解决这个问题的办法是引入虚拟节点(Virtual Node)。
比如没有使用虚拟节点时有两个节点:
极端的情况是Node1通过哈希计算得到值为1,而Node2通过哈希计算得到了2^32-3次方,两个节点在圆环的位置就会顺时针Node1到Node2之间大量的key,而Node2到Node1却只有一点点,大量的key就会落到Node2上。
Node1 设置了两个虚拟节点,Node2 也设置了两个虚拟节点(虚线圆圈)。
定位到Node1_1、1_2、1_3的key会落到Node1节点,定位到Node2_1、2、3key会落到Node2节点,使得分布比较均匀。
Redis 既没有用哈希取模,也没有用一致性哈希,而是用虚拟槽来实现分片的。
Redis 创建了 16384 个槽(slot),每个节点负责一定区间的 slot。比如 Node1 负责 0-5460,Node2 负责 5461-10922,Node3 负责 10923-16383。
然后通过CRC16算法对key计算再对16384进行取模,看计算的结果会落在哪个槽里面,然后这个槽由哪个节点负责,该key就落入到这个节点上。
Redis 的每个 master 节点维护一个 16384 位(2048bytes=2KB)的位序列,比如:序列的第 0 位是 1,就代表第一个 slot 是它负责;序列的第 1 位是 0,代表第二个 slot不归它负责。
对象分布到 Redis 节点上时,对 key 用 CRC16 算法计算再%16384,得到一个 slot的值,数据落到负责这个 slot 的 Redis 节点上。
key 与 slot 的关系是永远不会变的,会变的只有 slot 和 Redis 节点的关系。也就是同一个key计算得到的slot值是用于不会变的,变的只是Redis节点与它负责的slot之间的关系。
新增或者下面master节点,数据怎么迁移呢:
因为 key 和 slot 的关系是永远不会变的,当新增了节点的时候,需要把原有的 slot分配给新的节点负责,并且把相关的数据迁移过来。
比如 Node1 负责 0-5460,Node2 负责 5461-10922,Node3 负责 10923-16383。现在新增一个Node4,然他负责13000-16383槽的数据,只要把原本在Node3的相关slot分配给Node4负责,并把Node3这些槽的数据迁移到Node4即可。
节点下线后,把该节点的slot分配给其他在线的节点,并把对应数据迁移过去即可,只有所以slot都有节点负责的情况下,集群才能对外提供服务。
怎么让相关数据落到同一节点
在key里面假如{hash tag}可以使得相关key落到同一节点上,Redis在计算槽编号的时候只会获取{}之间的字符来进行槽编号计算,这样由于两个不同的键,{}里面的字符串是相同的,因此他们可以被计算出相同的槽。
比如下面两key会被计算出相同的槽,因为{bb}是一样的。
aaa{bb}cccc
pppp{bb}eeee
为了节省机器,我们直接把6个Redis实例安装在同一台机器上(3主3从),只是使用不同的端口号。
机器IP192.168.18.140
六个节点的端口分别为9001 9002 9003 9004 9005 9006
新版的cluster已经不需要通过ruby脚本创建,删掉了ruby相关依赖的安装
port 9001 #端口号
daemonize yes #以守护进程启动
protected-mode yes #不允许外网访问
dir /usr/local/redis5.0.9/redis-5.0.9/redis-cluster/9001 #工作目录
cluster-enabled yes #是否允许开启集群
cluster-config-file nodes-7291.conf #集群节点的配置文件,不用自己创建,redis会自动创建并更新。
cluster-node-timeout 5000 #集群节点超时后认为主观下线
appendonly yes #开启AOF持久化
pidfile /var/run/redis_7291.pid #PID文件
修改好后把该文件复制到9002 9003 9004 9005 9006目录,并做一些修改 有关9001的都改为对应的。
./redis-server ../redis-cluster/9001/redis.conf
./redis-server ../redis-cluster/9002/redis.conf
./redis-server ../redis-cluster/9003/redis.conf
./redis-server ../redis-cluster/9004/redis.conf
./redis-server ../redis-cluster/9005/redis.conf
./redis-server ../redis-cluster/9006/redis.conf
使用ps -ef|grep redis查看进程启动情况:
4. 使用命令创建集群
./redis-cli --cluster create 192.168.18.140:9001 192.168.18.140:9002 192.168.18.140:9003 192.168.18.140:9004 192.168.18.140:9005 192.168.18.140:9006 --cluster-replicas 1
#cluster-replicas 1 为每个master节点配置一个slave节点
然后会自动为你生成一套集群方案
M表示master节点,S表示Slave节点,后面的一串表示集群中的节点标志,是唯一的。再后面是该节点的IP和端口,replicates 一串Id 表示该从节点是这串Id的节点的从节点。
主节点上都有显示展示的负责的slot槽。
集群命令:
cluster info :打印集群的信息
cluster nodes :列出集群当前已知的所有节点(node),以及这些节点的相关信息。
cluster meet :将 ip 和 port 所指定的节点添加到集群当中,让它成为集群的一份子。
cluster forget <node_id> :从集群中移除 node_id 指定的节点(保证空槽道)。
cluster replicate <node_id> :将当前节点设置为 node_id 指定的节点的从节点。
cluster saveconfig :将节点的配置文件保存到硬盘里面。
slot命令:
cluster addslots [slot …] :将一个或多个槽(slot)指派(assign)给当前节点。
cluster delslots [slot …] :移除一个或多个槽对当前节点的指派。
cluster flushslots :移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点。
cluster setslot node <node_id> :将槽 slot 指派给 node_id 指定的节点,如果槽已经指派给另一个节点,那么先让另一个节点删除该槽>,然后再进行指派。
cluster setslot migrating <node_id> :将本节点的槽 slot 迁移到 node_id 指定的节点中。
cluster setslot importing <node_id> :从 node_id 指定的节点中导入槽 slot 到本节点。
cluster setslot stable :取消对槽 slot 的导入(import)或者迁移(migrate)。
key相关命令:
cluster keyslot :计算键 key 应该被放置在哪个槽上。
cluster countkeysinslot :返回槽 slot 目前包含的键值对数量。
cluster getkeysinslot :返回 count 个 slot 槽中的键