共计 10796 个字符,预计需要花费 27 分钟才能阅读完成。
设计集群方案时,至少要考虑以下因素:
(1)高可用要求:根据故障转移的原理,至少需要 3 个主节点才能完成故障转移,且 3 个主节点不应在同一台物理机上;每个主节点至少需要 1 个从节点,且主从节点不应在一台物理机上;因此高可用集群至少包含 6 个节点。
(2)数据量和访问量:估算应用需要的数据量和总访问量(考虑业务发展,留有冗余),结合每个主节点的容量和能承受的访问量(可以通过 benchmark 得到较准确估计),计算需要的主节点数量。
(3)节点数量限制:Redis 官方给出的节点数量限制为 1000,主要是考虑节点间通信带来的消耗。在实际应用中应尽量避免大集群;如果节点数量不足以满足应用对 Redis 数据量和访问量的要求,可以考虑:
a. 业务分割,大集群分为多个小集群;
b. 减少不必要的数据;
c. 调整数据过期策略等。
(4)适度冗余:Redis 可以在不影响集群服务的情况下增加节点,因此节点数量适当冗余即可,不用太大。
集群的原理:
集群最核心的功能是数据分区,因此首先介绍数据的分区规则;然后介绍集群实现的细节:通信机制和数据结构;最后以 cluster meet(节点握手)、cluster addslots(槽分配)为例,说明节点是如何利用上述数据结构和通信机制实现集群命令的。
数据分区方案:
数据分区有顺序分区、哈希分区等,其中哈希分区由于其天然的随机性,使用广泛;集群的分区方案便是哈希分区的一种。
哈希分区的基本思路是:对数据的特征值(如 key)进行哈希,然后根据哈希值决定数据落在哪个节点。常见的哈希分区包括:哈希取余分区、一致性哈希分区、带虚拟节点的一致性哈希分区等。
(1)哈希取余分区
哈希取余分区思路非常简单:计算 key 的 hash 值,然后对节点数量进行取余,从而决定数据映射到哪个节点上。该方案最大的问题是,当新增或删减节点时,节点数量发生变化,系统中所有的数据都需要重新计算映射关系,引发大规模数据迁移。
(2)一致性哈希分区
一致性哈希算法将整个哈希值空间组织成一个虚拟的圆环,范围为 0 -2^32-1;对于每个数据,根据 key 计算 hash 值,确定数据在环上的位置,然后从此位置沿环顺时针行走,找到的第一台服务器就是其应该映射到的服务器。
与哈希取余分区相比,一致性哈希分区将增减节点的影响限制在相邻节点。如果在 node1 和 node2 之间增加 node5,则只有 node2 中的一部分数据会迁移到 node5;如果去掉 node2,则原 node2 中的数据只会迁移到 node4 中,只有 node4 会受影响。
一致性哈希分区的主要问题在于,当节点数量较少时,增加或删减节点,对单个节点的影响可能很大,造成数据的严重不平衡。还是以上图为例,如果去掉 node2,node4 中的数据由总数据的 1 / 4 左右变为 1 / 2 左右,与其他节点相比负载过高。
(3)带虚拟节点的一致性哈希分区
该方案在一致性哈希分区的基础上,引入了虚拟节点的概念。Redis 集群使用的便是该方案,其中的虚拟节点称为槽(slot)。槽是介于数据和实际节点之间的虚拟概念;每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据。引入槽以后,
数据的映射关系由数据 hash-> 实际节点,变成了数据 hash-> 槽 -> 实际节点。
在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽解耦了数据和实际节点之间的关系,增加或删除节点对系统的影响很小。仍以上图为例,系统中有 4 个实际节点,假设为其分配 16 个槽(0-15);槽 0 - 3 位于 node1,4- 7 位于 node2,
以此类推。如果此时删除 node2,只需要将槽 4 - 7 重新分配即可,例如槽 4 - 5 分配给 node1,槽 6 分配给 node3,槽 7 分配给 node4;可以看出删除 node2 后,数据在其他节点的分布仍然较为均衡。槽的数量一般远小于 2^32,远大于实际节点的数量;
在 Redis 集群中,槽的数量为 16384
下面这张图很好的总结了 Redis 集群将数据映射到实际节点的过程:
(1)Redis 对数据的特征值(一般是 key)计算哈希值,使用的算法是 CRC16。Crc16(key)= hash
(2)根据哈希值,计算数据属于哪个槽。Hash % 16384
(3)根据槽与节点的映射关系,计算数据属于哪个节点。
Redis 集群搭建:
一、主 / 从(master/slave)(缺点:数据冗余,浪费内存(ping – pong)
1. 主节点读写,从节点只能读,不能写
2. 当主节点读写数据变化时,会直接同步到从节点
3. 一个 master 可以拥有多个 slave,但是一个 slave 只能对应一个 master
主从复制工作机制:
当 slave 启动后,主动向 master 发送 SYNC 命令。master 接收到 SYNC 命令后在后台保存快照(RDB 持久化)和缓存保存快照这段时间的命令,然后将保存的快照文件和缓存的命令发送给 slave。slave 接收到快照文件和命令后加载快照文件和缓存的执行命令。复制初始化后,master 每次接收到的写命令都会同步发送给 slave,保证主从数据一致性。
主从配置:
redis 默认是主数据,所以 master 无需配置,我们只需要修改 slave 的配置即可。
a. 设置需要连接的 master 的 ip 端口:
slaveof 192.168.0.107 6379
b. 如果 master 设置了密码。需要配置:
masterauth
c. 连接成功进入命令行后,可以通过以下命令行查看连接该数据库的其他库信息:
info replication
二、哨兵模式(sentinel)
哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待 Redis 服务器响应,从而监控运行的多个 Redis 实例。(ping – pong)
这里的哨兵有两个作用:
• 通过发送命令,让 Redis 服务器返回监控其运行状态,包括主服务器和从服务器。
• 当哨兵监测到 master 宕机,会自动将 slave 切换成 master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
然而一个哨兵进程对 Redis 服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
故障切换(failover)的过程:
假设主服务器宕机,哨兵 1 先检测到这个结果,系统并不会马上进行 failover 过程,仅仅是哨兵 1 主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行 failover 操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的
配置:
配置 Redis 的主从服务器,修改 redis.conf 文件如下
# 使得 Redis 服务器可以跨网络访问
bind 0.0.0.0
# 设置密码
requirepass “123456”
# 指定主服务器,注意:有关 slaveof 的配置只是配置从服务器,主服务器不需要配置
slaveof 192.168.11.128 6379
# 主服务器密码,注意:有关 slaveof 的配置只是配置从服务器,主服务器不需要配置
masterauth 123456
配置哨兵,在 Redis 安装目录下有一个 sentinel.conf 文件,copy 一份进行修改
# 禁止保护模式
protected-mode no
#配置监听的主服务器,这里 sentinel monitor 代表监控,mymaster 代表服务器的名称,可以自定义,192.168.11.128 代表监控的主服务器,6379 代表端口,2 代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行 failover 操作。
sentinel monitor mymaster 192.168.11.128 6379 2
# sentinel author-pass 定义服务的密码,mymaster 是服务名称,123456 是 Redis 服务器密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster 123456
启动:
# 启动 Redis 服务器进程
./redis-server ../redis.conf
# 启动哨兵进程
./redis-sentinel ../sentinel.conf
需要特别注意的是:
客观下线是主节点才有的概念;如果从节点和哨兵节点发生故障,被哨兵主观下线后,不会再有后续的客观下线和故障转移操作。选举领导者哨兵节点:当主节点被判断客观下线以后,各个哨兵节点会进行协商,选举出一个领导者哨兵节点,并由该领导者节点对其进行故障转移操作。监视该主节点的所有哨兵都有可能被选为领导者,选举使用的算法是 Raft 算法;Raft 算法的基本思路是先到先得:即在一轮选举中,哨兵 A 向 B 发送成为领导者的申请,如果 B 没有同意过其他哨兵,则会同意 A 成为领导者。选举的具体过程这里不做详细描述,一般来说,哨兵选择的过程很快,谁先完成客观下线,一般就能成为领导者。
故障转移:选举出的领导者哨兵,开始进行故障转移操作,该操作大体可以分为 3 个步骤:
• 在从节点中选择新的主节点:选择的原则是,首先过滤掉不健康的从节点;然后选择优先级最高的从节点 (由 slave-priority 指定);如果优先级无法区分,则选择复制偏移量最大的从节点; 如果仍无法区分,则选择 runid 最小的从节点。
• 更新主从状态:通过 slaveof no one 命令,让选出来的从节点成为主节点;并通过 slaveof 命令让其他节点成为其从节点。
• 将已经下线的主节点 (即 6379) 设置为新的主节点的从节点,当 6379 重新上线后,它会成为新的主节点的从节点。
三、集群
集群,即 Redis Cluster,是 Redis 3.0 开始引入的分布式存储方案。
集群由多个节点 (Node) 组成,Redis 的数据分布在这些节点中。集群中的节点分为主节点和从节点:只有主节点负责读写请求和集群信息的维护;从节点只进行主节点数据和状态信息的复制。
集群的作用,可以归纳为两点:
1、数据分区:数据分区 (或称数据分片) 是集群最核心的功能。
集群将数据分散到多个节点,一方面突破了 Redis 单机内存大小的限制,存储容量大大增加;另一方面每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。
Redis 单机内存大小受限问题,在介绍持久化和主从复制时都有提及;例如,如果单机内存太大,bgsave 和 bgrewriteaof 的 fork 操作可能导致主进程阻塞,主从环境下主机切换时可能导致从节点长时间无法提供服务,全量复制阶段主节点的复制缓冲区可能溢出……。
2、高可用:集群支持主从复制和主节点的自动故障转移(与哨兵类似);当任一节点发生故障时,集群仍然可以对外提供服务。
集群的搭建:
这一部分我们将搭建一个简单的集群:共 6 个节点,3 主 3 从。方便起见:所有节点在同一台服务器上,以端口号进行区分;配置从简。3 个主节点端口号:7000/7001/7002,对应的从节点端口号:8000/8001/8002。
集群的搭建有两种方式:
(1)手动执行 Redis 命令,一步步完成搭建;
(2)使用 Ruby 脚本搭建。二者搭建的原理是一样的,只是 Ruby 脚本将 Redis 命令进行了打包封装;在实际应用中推荐使用脚本方式,简单快捷不容易出错。下面分别介绍这两种方式。
集群的搭建可以分为四步:(1)启动节点:将节点以集群模式启动,此时节点是独立的,并没有建立联系;(2)节点握手:让独立的节点连成一个网络;(3)分配槽:将 16384 个槽分配给主节点;(4)指定主从关系:为从节点指定主节点。
第一种搭建:手动
(1)启动节点
集群节点的启动仍然是使用 redis-server 命令,但需要使用集群模式启动。下面是 7000 节点的配置文件(只列出了节点正常工作关键配置,其他配置(如开启 AOF) 可以参照单机节点进行):
#redis-7000.conf
port 7000
cluster-enabled yes
cluster-config-file “node-7000.conf”
logfile “log-7000.log”
dbfilename “dump-7000.rdb”
daemonize yes
其中的 cluster-enabled 和 cluster-config-file 是与集群相关的配置。
cluster-enabled yes:Redis 实例可以分为单机模式 (standalone) 和集群模式 (cluster);cluster-enabled yes 可以启动集群模式。在单机模式下启动的 Redis 实例,如果执行 info server 命令,可以发现 redis_mode 一项为 standalone
cluster-config-file:该参数指定了集群配置文件的位置。每个节点在运行过程中,会维护一份集群配置文件;每当集群信息发生变化时(如增减节点),集群内所有节点会将最新信息更新到该配置文件;当节点重启后,会重新读取该配置文件,获取集群信息,可以方便的重新加入到集群中。也就是说,当 Redis 节点以集群模式启动时,会首先寻找是否有集群配置文件,如果有则使用文件中的配置启动,如果没有,则初始化配置并将配置保存到文件中。集群配置文件由 Redis 节点维护,不需要人工修改。
编辑好配置文件后
使用 redis-server 命令启动该节点:
redis-server redis-7000.conf
(2)节点握手
节点启动以后是相互独立的,并不知道其他节点存在;需要进行节点握手,将独立的节点组成一个网络。
节点握手使用 cluster meet {ip} {port} 命令实现
例如在 7000 节点中执行 cluster meet 192.168.72.128 7001,可以完成 7000 节点和 7001 节点的握手;注意 ip 使用的是局域网 ip 而不是 localhost 或 127.0.0.1,
是为了其他机器上的节点或客户端也可以访问
同理,在 7000 节点中使用 cluster meet 命令,可以将所有节点加入到集群,完成节点握手:
1 cluster meet 192.168.72.128 7002
2 cluster meet 192.168.72.128 8000
3 cluster meet 192.168.72.128 8001
4 cluster meet 192.168.72.128 8002
(3)分配槽
在 Redis 集群中,借助槽实现数据分区,具体原理后文会介绍。集群有 16384 个槽,槽是数据管理和迁移的基本单位。当数据库中的 16384 个槽都分配了节点时,集群处于上线状态(ok);如果有任意一个槽没有分配节点,则集群处于下线状态(fail)。
分配槽使用 cluster addslots 命令,执行下面的命令将槽(编号 0 -16383)全部分配完毕:
1 redis-cli -p 7000 cluster addslots {0..5461}
2 redis-cli -p 7001 cluster addslots {5462..10922}
3 redis-cli -p 7002 cluster addslots {10923..16383}
(4)指定主从关系
集群中指定主从关系不再使用 slaveof 命令,而是使用 cluster replicate 命令;参数使用节点 id。
通过 cluster nodes 获得几个主节点的节点 id 后,执行下面的命令为每个从节点指定主节点:
1 redis-cli -p 8000 cluster replicate be816eba968bc16c884b963d768c945e86ac51ae
2 redis-cli -p 8001 cluster replicate 788b361563acb175ce8232569347812a12f1fdb4
3 redis-cli -p 8002 cluster replicate a26f1624a3da3e5197dde267de683d61bb2dcbf1
第二种搭建:脚本
使用 Ruby 脚本搭建集群
在{REDIS_HOME}/src 目录下可以看到 redis-trib.rb 文件,这是一个 Ruby 脚本,可以实现自动化的集群搭建。
(1)安装 Ruby 环境
以 Ubuntu 为例,如下操作即可安装 Ruby 环境:
1 apt-get install ruby #安装 ruby 环境
2 gem install redis #gem 是 ruby 的包管理工具,该命令可以安装 ruby-redis 依赖
(2)启动节点
与第一种方法中的“启动节点”完全相同。
(3)搭建集群
redis-trib.rb 脚本提供了众多命令,其中 create 用于搭建集群,使用方法如下
./redis-trib.rb create –replicas 1 192.168.72.128:7000 192.168.72.128:7001 192.168.72.128:7002 192.168.72.128:8000 192.168.72.128:8001 192.168.72.128:8002
其中:–replicas= 1 表示每个主节点有 1 个从节点;后面的多个 {ip:port} 表示节点地址,前面的做主节点,后面的做从节点。使用 redis-trib.rb 搭建集群时,要求节点不能包含任何槽和数据。
执行创建命令后,脚本会给出创建集群的计划;计划包括哪些是主节点,哪些是从节点,以及如何分配槽
集群扩展:
1. 集群伸缩
实践中常常需要对集群进行伸缩,如访问量增大时的扩容操作。Redis 集群可以在不影响对外服务的情况下实现伸缩;伸缩的核心是槽迁移:修改槽与节点的对应关系,实现槽(即数据) 在节点之间的移动。例如,如果槽均匀分布在集群的 3 个节点中,此时增加一个节点,则需要从 3 个节点中分别拿出一部分槽给新节点,从而实现槽在 4 个节点中的均匀分布。
增加节点、
假设要增加 7003 和 8003 节点,其中 8003 是 7003 的从节点;步骤如下:
(1)启动节点:方法参见集群搭建
(2)节点握手:可以使用 cluster meet 命令,但在生产环境中建议使用 redis-trib.rb 的 add-node 工具,其原理也是 cluster meet,但它会先检查新节点是否已加入其它集群或者存在数据,避免加入到集群后带来混乱。
1 redis-trib.rb add-node 192.168.72.128:7003 192.168.72.128 7000
2 redis-trib.rb add-node 192.168.72.128:8003 192.168.72.128 7000
(3)迁移槽:推荐使用 redis-trib.rb 的 reshard 工具实现。reshard 自动化程度很高,只需要输入 redis-trib.rb reshard ip:port (ip 和 port 可以是集群中的任一节点),然后按照提示输入以下信息,槽迁移会自动完成:
○ 待迁移的槽数量:16384 个槽均分给 4 个节点,每个节点 4096 个槽,因此待迁移槽数量为 4096
○ 目标节点 id:7003 节点的 id
○ 源节点的 id:7000/7001/7002 节点的 id
(4)指定主从关系:方法参见集群搭建
减少节点、
假设要下线 7000/8000 节点,可以分为两步:
(1)迁移槽:使用 reshard 将 7000 节点中的槽均匀迁移到 7001/7002/7003 节点
(2)下线节点:使用 redis-trib.rb del-node 工具;应先下线从节点再下线主节点,因为若主节点先下线,从节点会被指向其他主节点,造成不必要的全量复制。
1 redis-trib.rb del-node 192.168.72.128:7001 {节点 8000 的 id}
2 redis-trib.rb del-node 192.168.72.128:7001 {节点 7000 的 id}
2. 故障转移
集群只实现了主节点的故障转移;从节点故障时只会被下线,不会进行故障转移。因此,使用集群时,应谨慎使用读写分离技术,因为从节点故障会导致读服务不可用,可用性变差。
这里不再详细介绍故障转移的细节,只对重要事项进行说明:
节点数量:在故障转移阶段,需要由主节点投票选出哪个从节点成为新的主节点;从节点选举胜出需要的票数为 N /2+1;其中 N 为主节点数量 (包括故障主节点),但故障主节点实际上不能投票。因此为了能够在故障发生时顺利选出从节点,集群中至少需要 3 个主节点(且部署在不同的物理机上)。
故障转移时间:从主节点故障发生到完成转移,所需要的时间主要消耗在主观下线识别、主观下线传播、选举延迟等几个环节;具体时间与参数 cluster-node-timeout 有关,一般来说:
故障转移时间(毫秒) ≤ 1.5 * cluster-node-timeout + 1000
cluster-node-timeout 的默认值为 15000ms(15s),因此故障转移时间会在 20s 量级
3. 集群的限制及应对方法
由于集群中的数据分布在不同节点中,导致一些功能受限,包括:
(1)key 批量操作受限:例如 mget、mset 操作,只有当操作的 key 都位于一个槽时,才能进行。针对该问题,一种思路是在客户端记录槽与 key 的信息,每次针对特定槽执行 mget/mset;另外一种思路是使用 Hash Tag,将在下一小节介绍。
(2)keys/flushall 等操作:keys/flushall 等操作可以在任一节点执行,但是结果只针对当前节点,例如 keys 操作只返回当前节点的所有键。针对该问题,可以在客户端使用 cluster nodes 获取所有节点信息,并对其中的所有主节点执行 keys/flushall 等操作。
(3)事务 /Lua 脚本:集群支持事务及 Lua 脚本,但前提条件是所涉及的 key 必须在同一个节点。Hash Tag 可以解决该问题。
(4)数据库:单机 Redis 节点可以支持 16 个数据库,集群模式下只支持一个,即 db0。
(5)复制结构:只支持一层复制结构,不支持嵌套。
4. Hash Tag
Hash Tag 原理是:当一个 key 包含 {} 的时候,不对整个 key 做 hash,而仅对 {} 包括的字符串做 hash。
Hash Tag 可以让不同的 key 拥有相同的 hash 值,从而分配在同一个槽里;这样针对不同 key 的批量操作 (mget/mset 等),以及事务、Lua 脚本等都可以支持。不过 Hash Tag 可能会带来数据分配不均的问题,这时需要:(1) 调整不同节点中槽的数量,使数据分布尽量均匀;(2)避免对热点数据使用 Hash Tag,导致请求分布不均。
下面是使用 Hash Tag 的一个例子;通过对 product 加 Hash Tag,可以将所有产品信息放到同一个槽中,便于操作。
5. 参数优化
cluster_node_timeout
cluster_node_timeout 参数在前面已经初步介绍;它的默认值是 15s,影响包括:
(1)影响 PING 消息接收节点的选择:值越大对延迟容忍度越高,选择的接收节点越少,可以降低带宽,但会降低收敛速度;应根据带宽情况和应用要求进行调整。
(2)影响故障转移的判定和时间:值越大,越不容易误判,但完成转移消耗时间越长;应根据网络状况和应用要求进行调整。
cluster-require-full-coverage
前面提到,只有当 16384 个槽全部分配完毕时,集群才能上线。这样做是为了保证集群的完整性,但同时也带来了新的问题:当主节点发生故障而故障转移尚未完成,原主节点中的槽不在任何节点中,此时会集群处于下线状态,无法响应客户端的请求。
cluster-require-full-coverage 参数可以改变这一设定:如果设置为 no,则当槽没有完全分配时,集群仍可以上线。参数默认值为 yes,如果应用对可用性要求较高,可以修改为 no,但需要自己保证槽全部分配。
6. redis-trib.rb
redis-trib.rb 提供了众多实用工具:创建集群、增减节点、槽迁移、检查完整性、数据重新平衡等;通过 help 命令可以查看详细信息。在实践中如果能使用 redis-trib.rb 工具则尽量使用,不但方便快捷,还可以大大降低出错概率。
缓存穿透
缓存穿透,是指查询一个数据库一定不存在的数据。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果 key 不存在或者 key 已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为��,则不放进缓存。
解决方案:如果从数据库查询的对象为空,也放入缓存,只是设定的缓存过期时间较短,比如设置为 60 秒,最大不超过 5min
缓存雪崩
缓存雪崩,是指在某一个时间段,缓存集中过期失效。
产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。
解决方案:一般是采取不同分类商品,缓存不同周期。在同一分类中的商品,加上一个随机因子。这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源
缓存击穿
缓存击穿,是指一个 key 非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个 key 在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
小编在做电商项目的时候,把这货就成为“爆款”。
其实,大多数情况下这种爆款很难对数据库服务器造成压垮性的压力。达到这个级别的公司没有几家的。所以,务实主义的小编,对主打商品都是早早的做好了准备,让缓存永不过期。即便某些商品自己发酵成了爆款,也是直接设为永不过期就好了。mutex key 互斥锁