Redis实战(八)集群

15

是什么?

引入的原因是:由于数据量过大,单个master复制集难以承担,因此需要对多个复制集构建集群,形成水平扩展,每个复制集只负责存储整个数据集的一部分,这就是Redis集群,起作用是提供在多个Redis节点间共享数据的程序集。

Redis集群是一个提供在多个Redis节点间共享数据的程序集。Redis集群支持多个Master。

能干嘛?

  • Redis集群支持多个Master,每个Master又可以挂载多个Slave
    • 读写分离
    • 支持数据的高可用
    • 支持海量数据的读写存储操作
  • 由于Cluster自带的Sentinel的故障转移机制,内置了高可用的支持,无需再去使用哨兵功能
  • 客户端与Redis的节点连接,不再需要连接集群中所有的节点,只需要任意连接集群中的一个可用节点即可
  • 槽位slot负责分配到各个物理服务节点,由对应的集群来负责维护节点、插槽和数据之间的关系

Redis集群原理

Redis集群的数据分片

Redis集群没有使用一致性hash,而是引入了哈希槽的概念。
Redis集群有16384(2^14)个哈希槽,每个key通过CRC16计算后对16384取模来决定放置在哪个槽,集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点,那么hash槽的分配示意图如下:

分片是什么?

使用Redis集群时我们会将存储的数据分散到多台redis机器上,这称为分片。简言之,集群中的每个Redis实例都被认为是整个数据的一个分片。

如何找到给定key的分片?

为了找到给定key的分片,我们对key进行CRC16(key)算法处理并通过对总分片数量取模。然后,使用确定性哈希函数,这意味着给定的key将多次始终映射到同一个分片,我们可以推断将来读取特定key的位置。

优势
  • 方便扩容和数据分派查找
    这种结构很容易添加和删除节点,比如如果我想新添加个节点D,我需要从节点A,B,C中将部分槽移到D上。如果想移除节点A,需要将A中的槽移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可。由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以不论添加节点或者删除改变某个节点的哈希槽的数量都不会造成集群不可用的状态。
槽位映射的方案
普通hash算法

使用了固定的值来取模hash(key) mod n,n表示节点数量,该方法难以进行扩容和缩容,当某个节点宕机后,无法自动处理,容错能力低。优点是实现简单,容易操作。
缺点:

  • 扩容和缩容比较麻烦,需要重新分配数据
一致性哈希

参考资料:16 张图解带你掌握一致性哈希算法-华为开发者论坛 | 华为开发者联盟 (huawei.com)
详见参考资料。一致性哈希的哈希空间大小为2^32,取值范围0-(2^32)-1,该算法来自麻省理工大学,常用于分布式缓存中。可以将0看作2^32,这样它的取值空间将变成一个圆,称为哈希环。哈希环是封闭的,这意味着任何key通过哈希后都将落在环上。

在环上分配主机的一般做法是对主机的IP、域名或主机名称进行hash,将其映射到环上,沿顺时针方向遇到的第一个主机将对key进行处理。但是该方案有个缺点,通过hash将主机映射到环上大多数时候并不是均匀的(哈希环的数据倾斜问题),这时候有人提出了虚拟节点的概念,一台物理主机可以包含多个虚拟节点,将虚拟节点通过哈希映射到环上,落在虚拟节点上的key将由对应的物理节点进行处理。这里使用了双层映射(物理节点->虚拟节点->哈希环)。

一致性哈希的优点

  • 容错性:假设Node C宕机,可以看到此时对象A、B、D不会受到影响。一般的,在一致性Hash算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(沿着逆时针方向遇到的第一台服务器)之间的数据,其他不会受到影响。简单说,如果C挂了,受到影响的只是B、C之间的数据且这些数据会转移到D进行存储。
  • 扩展性:数据量增加了,需要增加一台节点Node X,X的位置在A和B之间,那受到影响的也仅仅是A到X之间的数据,重新把A到X之间的数据录入到X上即可,不会导致hash取余全部数据重写洗牌。
    缺点
  • 容易造成数据倾斜
哈希槽

是什么
哈希槽可以解决数据倾斜问题(虽然该问题一致性哈希也可以通过引入虚拟节点来解决)。哈希槽实质上就是一个数组,数组[0,2^14-1]形成hash slot空间。

能干嘛
解决均匀分配的问题,在数据和节点之间又加入了一层,把这层称为哈希槽(slot),用于管理数据和节点之间的关系,现在就相当于节点上放的是槽,槽里放的是数据。

槽解决的是粒度问题(槽代表了具有相同hash值的数据集合),相当于把粒度变大了,这样便于移动。哈希解决的是映射问题,使用key的哈希值来计算所在的槽,便于数据分配。

哈希槽的数量
一个集群只能有16384(2^14)个槽,编号0-16383。这些槽会分配给集群中的所有主节点,分配策略没有要求。

集群会记录节点和槽的对应关系,解决了节点和槽的关系后,接下来就需要对key求哈希值,然后对16384取模,余数是几key就落入对应的槽里。HASH_SLOT=CRC16(key) mod 16384。以槽为单位移动数据,因为槽的数目是固定的,处理起来比较容易,这样数据移动问题就解决了。

为什么槽位数量是16384?

  1. 如果槽位为65536,发送心跳信息的消息头达8K,发送的心跳包过于庞大。
    在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。当槽位为65536时,这块的大小是:65536/(8*1024)=8kb。当槽位为16384时,这块的大小为16384/(8*1024)=2kb
    因为每秒钟,redis节点需要发送一定数量的ping消息心跳包,如果槽位为65536,这个ping消息的消息头就太大了,浪费带宽。

  2. redis的集群主节点数量基本不太可能超过1000个。
    集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者不建议redis cluster节点数量超过1000个。那么对应节点数在1000以内的redis cluster集群,16384个槽位够用了,没必要扩展到65536个。

  3. 槽位越小,节点少的情况下,压缩比高,容易传输
    Redis主节点的配置信息中它所负责的哈希槽是通过一张bitmap的形式保存的,在传输过程中会对bitmap进行压缩,但是如果bitmap的填充率slots/N很高的话(N表示节点数),bitmap的压缩比就很低。如果节点数少,而哈希槽数量很多的话,压缩比就很低。

扩展——稀疏位图
redis使用的bitmap为稀疏位图,这意味着它只会分配足够的内存来存储已设置为1的位。

Redis集群不保证强一致性,这意味着在特定条件下,Redis可能会丢掉一些被系统接收的写请求。CAP理论:只能保证AP

Redis集群命令实操

集群信息查看命令

  • redis-cli -a 123456 --cluster check 节点ip:port:检查集群状态
  • info replication:查看节点的master和slave状态信息
  • cluster info:查看集群信息
  • cluster nodes:查看集群节点之间的关联关系和哈希槽的分配情况等

搭建3主3从Redis集群

本次案例中,我们使用3台虚拟机来构建集群,每台机器使用6379和6380端口。

集群中的节点的配置文件都是相同的,示例如下:


#[GENERAL]
bind 0.0.0.0
protected-mode no
requirepass 123456
port 6379
daemonize yes
pidfile /var/run/redis/redis-6379.pid
logfile /data/logs/redis/redis-6379.log
dbfilename dump-6379.rdb
appendonly yes                            
appendfilename "appendonly-6379.aof"
masterauth 123456
dir /data/redis
# 如果没有配置该项或该项为no,则slave节点仅作备份,不能读和写
replica-read-only yes

#[CLUSTER]
cluster-enabled yes
# 该文件是构建集群后自动生成的
cluster-config-file nodes-6379.conf
cluster-node-timeout 5000

6380端口的配置文件只需要按照上面的在vi中用命令:%s/6379/6380/g修改下端口号即可。

集群配置步骤

  1. 按上面的方式配置好配置文件
  2. 启动集群中所有的节点
  3. 在任意节点上的终端中,使用命令 redis-cli -a 123456 --cluster create 192.168.1.110:6379 192.168.1.112:6379 192.168.1.114:6379 192.168.1.110:6380 192.168.1.112:6380 192.168.1.114:6380 --cluster-replicas 1 构建集群。
  4. 上述命令中,--cluster-replicas 1表示我们希望为每个主节点提供一个副本(从节点,slave)
  5. 命令运行完毕后会进行master、slave节点的指定和哈希槽的分配,会提示是否确认分配,输入yes后,会将预分配更新到每个节点上。
  6. 使用redis-cli连接集群redis-cli -a 123456 -p 6380 -c,注意这里使用了-c来表明连接的是redis集群,如果没有该参数,在写入时,会发生错误。

注意事项

  • 使用redis-cli连接集群时,需要加-c参数,让redis-cli知道我们要连接的时redis集群
  • 对集群中的从节点执行写入命令时,redis-cli会自动将连接路由到该slave的master节点,然后再执行写入操作,类似于连接重定向。

==坑==:
通过JedisCluster连接部署在内网的Redis集群时,需要通过DefaultJedisClientConfig配置HostAndPortMapper。原因是:jedis是从redis集群中获取集群中的HostAndPort,获取到的主机地址和端口是内网的,外网连接不上,因此需要映射。

主从容灾切换迁移

自动故障转移
当master宕机后,集群会快速的从该master的slave节点中选择一个作为master节点。旧master重启后,重新连接集群,成为slave节点。

CAP理论,仅保证AP
Redis集群不保证强一致性,这意味着在特定条件下,Redis集群可能会丢掉一些被系统收到的写入请求命令。

手动故障转移,调整节点从属
有时候,我们希望节点的从属关系按照我们的设计来,这时候我们可以使用命令cluster failover来手动故障转移,将当前连接的节点设置为master。

主从扩容

如果现有的节点无法满足逐渐增加的数据量时,我们需要对Redis集群进行扩容,以增加吞吐量。Redis集群进行主从扩容步骤如下:

  1. 新建两个服务器实例,配置好配置文件后,启动节点。
  2. 启动后的两个节点(1,2)都是Master节点,使用命令redis-cli -a 123456 --cluster add-node 要加入的节点1ip:port 集群中的节点ip:port将节点 1 加入集群
  3. 在redis-cli中使用命令cluster nodes可以发现该节点已经加入集群,但是还没有分配槽
  4. 使用命令redis-cli -a 123456 --cluster reshard 节点1ip:port给节点1分配哈希槽,示例图如下
  5. 可以再次在redis-cli使用命令cluster nodes查看哈希槽的分配情况,发现已经分配完成
  6. 为节点1分配从节点2,使用命令redis-cli -a 123456 --cluster add-node 节点2ip:port 节点1ip:port --cluster-slave --cluster-master-id 节点1的id

这里还有一个检查集群状态的命令redis-cli -a 123456 --cluster check 节点ip:port

主从缩容

假设我们现在要删除节点1和节点2,其中节点1是节点2的master节点,Redis进行主从缩容的步骤如下:

  1. 先删除节点2(slave),使用命令redis-cli -a 123456 --cluster del-node 节点2ip:port 节点2id
  2. 然后对节点1上的哈希槽重新分配,使用命令redis-cli -a 123456 --cluster reshard 节点1ip:port,将节点1上的哈希槽分给其他的节点,示例图如下:
  3. 使用命令查看节点信息,如果节点2的槽位已全部分给其他节点,那么可以使用命令将节点1从集群中删除redis-cli -a 123456 --cluster del-node 节点1ip:port 节点1id

集群常见操作命令和CRC16算法分析

多键操作,通识占位符

不在同一个slot槽位下的多键操作支持不好,需要用到通识占位符。不在同一个slot槽位下的键值无法使用mset、mget等多键操作。可以通过{}来定义同一个组的概念,使key中{}相同内容的键值对被放到同一个slot槽位去。{}也叫通识占位符。

源码层面,在计算槽位时,会先判断key中是否含有{},如果有,将使用大括号中间的字符串来计算槽位号。举个例子,下面的key具有相同的槽位:key1{slot1} key2{slot1} key3{slot1},对这几个键,将使用slot1来计算槽位,而不是真实的键值。

CRC16

CRC16全称16位循环冗余校验码。Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置在那个槽,集群的每个节点负责一部分哈希槽

常用命令
  • 配置集群是否完整才能对外提供服务,配置项为:cluster-require-full-coverge。集群完整的定义为:集群中没有分配到相同槽位号的master和slave同时宕机,集群拥有完整的数据。如果具有相同槽位的一组节点同时宕机,那么就表示集群丢失了这部分数据,这是集群是不完整的。单个master宕机后,它的slave节点也会及时称为新master节点。如果集群不完整也想对外提供服务,则需要将该项设置为no,默认该值为yes
  • 查看槽位上是否有数据,cluster countkeysinslot 槽位号,0:没占用,1:占用
  • 查看键对应的槽位号,cluster keyslot 键名称