数据分布
- 数据分布理论
常见的分区规则有哈希分区和顺序分区两种:
哈希分区:
1.节点取余分区(如Redis的键或用户ID,再根据节点数量N使用公式:hash(key)%N计算出哈希值)
2.一致性哈希分区
一致性哈希分区(Distributed Hash Table)实现思路是为系统中每个节点分配一个token,范围一般在0~232,这些token构成一个哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点
3.虚拟槽分区
Redis Cluster槽范围是0~16383。槽是集群内数据管理和迁移的基本单位。采用大范围槽的主要目的是为了方便数据拆分和集群扩展。
-
Redis数据分区
Redis Cluser采用虚拟槽分区,所有的键根据哈希函数映射到0~16383整数槽内,计算公式:slot=CRC16(key)&16383。每一个节点负责维护一部分槽以及槽所映射的键值数据
·解耦数据和节点之间的关系,简化了节点扩容和收缩难度。
·节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据。
·支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景。
- 集群功能限制
1)key批量操作支持有限。如mset、mget,目前只支持具有相同slot值的key执行批量操作。对于映射为不同slot值的key由于执行mget、mget等操作可能存在于多个节点上因此不被支持。
2)key事务操作支持有限。同理只支持多key在同一节点上的事务操作,当多个key分布在不同的节点上时无法使用事务功能。
3)key作为数据分区的最小粒度,因此不能将一个大的键值对象如hash、list等映射到不同的节点。
4)不支持多数据库空间。单机下的Redis可以支持16个数据库,集群模式下只能使用一个数据库空间,即db0。
5)复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。
搭建集群
- 准备节点
节点数量至少为6个才能保证组成完整高可用的集群。每个节点需要开启配置cluster-enabled yes,让Redis运行在集群模式下。建议为集群内所有节点统一目录,一般划分三个目录:conf、data、log,分别存放配置、数据和日志相关文件。
#节点端口
port 6379
# 开启集群模式
cluster-enabled yes
# 节点超时时间,单位毫秒
cluster-node-timeout 15000
# 集群内部配置文件
cluster-config-file "nodes-6379.conf"
其他配置和单机模式一致即可,配置文件命名规则redis-{port}.conf,准备好配置后启动所有节点,命令如下:
redis-server conf/redis-6379.conf
redis-server conf/redis-6380.conf
redis-server conf/redis-6381.conf
redis-server conf/redis-6382.conf
redis-server conf/redis-6383.conf
redis-server conf/redis-6384.conf
6379节点启动成功,第一次启动时如果没有集群配置文件,它会自动创建一份,文件名称采用cluster-config-file参数项控制,建议采用node-{port}.conf格式定义,通过使用端口号区分不同节点,防止同一机器下多个节点彼此覆盖,造成集群信息异常。
-
节点握手
节点握手是集群彼此通信的第一步,由客户端发起命令:cluster meet{ip}{port}
1)节点6379本地创建6380节点信息对象,并发送meet消息。
2)节点6380接受到meet消息后,保存6379节点信息并回复pong消息。
3)之后节点6379和6380彼此定期通过ping/pong消息进行正常的节点通信。
- 分配槽
Redis集群把所有的数据映射到16384个槽中。每个key会映射为一个固定的槽,只有当节点分配了槽,才能响应和这些槽关联的键命令。
redis-cli -h 127.0.0.1 -p 6379 cluster addslots {0...5461}
redis-cli -h 127.0.0.1 -p 6380 cluster addslots {5462...10922}
redis-cli -h 127.0.0.1 -p 6381 cluster addslots {10923...16383}
首次启动的节点和被分配槽的节点都是主节点,从节点负责复制主节点槽信息和相关的数据。使用cluster replicate{nodeId}命令让一个节点成为从节点。其中命令执行必须在对应的从节点上执行,nodeId是要复制主节点的节点ID
127.0.0.1:6382>cluster replicate cfb28ef1deee4e0fa78da86abe5d24566744411e
OK
- 用redis-trib.rb搭建集群
redis-trib.rb是采用Ruby实现的Redis集群管理工具。
1.Ruby环境准备
安装Ruby:
-- 下载ruby wget https:// cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.1.tar.gz -- 安装ruby tar xvf ruby-2.3.1.tar.gz ./configure -prefix=/usr/local/ruby make make install cd /usr/local/ruby sudo cp bin/ruby /usr/local/bin sudo cp bin/gem /usr/local/bin
安装rubygem redis依赖:
wget http:// rubygems.org/downloads/redis-3.3.0.gem gem install -l redis-3.3.0.gem gem list --check redis gem
安装redis-trib.rb:
sudo cp /{redis_home}/src/redis-trib.rb /usr/local/bin
安装完Ruby环境后,执行redis-trib.rb命令确认环境是否正确
2.准备节点
我们跟之前内容一样准备好节点配置并启动
redis-server conf/redis-6481.conf
redis-server conf/redis-6482.conf
redis-server conf/redis-6483.conf
redis-server conf/redis-6484.conf
redis-server conf/redis-6485.conf
redis-server conf/redis-6486.conf
3.创建集群
启动好6个节点之后,使用redis-trib.rb create命令完成节点握手和槽分配过程,命令如下:
redis-trib.rb create --replicas 1 127.0.0.1:6481 127.0.0.1:6482 127.0.0.1:6483 127.0.0.1:6484 127.0.0.1:6485 127.0.0.1:6486
4.集群完整性检查
集群完整性指所有的槽都分配到存活的主节点上,只要16384个槽中有一个没有分配给节点则表示集群不完整。check命令只需要给出集群中任意一个节点地址就可以完成整个集群的检查工作.
redis-trib.rb check 127.0.0.1:6379
节点通信
-
通信流程
常见的元数据维护方式分为:集中式和P2P方式。Redis集群采用P2P的Gossip(流言)协议。
1)集群中的每个节点都会单独开辟一个TCP通道,用于节点之间彼此通信,通信端口号在基础端口上加10000。
2)每个节点在固定周期内通过特定规则选择几个节点发送ping消息。
3)接收到ping消息的节点用pong消息作为响应。 -
Gossip消息
Gossip协议的主要职责就是信息交换,消息可分为:ping消息、pong消息、meet消息、fail消息等。
meet消息:用于通知新节点加入。消息发送者通知接收者加入到当前集群
ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。
pong消息:当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。
fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。
- 消息头
typedef struct {
char sig[4]; /* 信号标示 */
uint32_t totlen; /* 消息总长度 */
uint16_t ver; /* 协议版本*/
uint16_t type; /* 消息类型,用于区分meet,ping,pong等消息 */
uint16_t count; /* 消息体包含的节点数量,仅用于meet,ping,ping消息类型*/
uint64_t currentEpoch; /* 当前发送节点的配置纪元 */
uint64_t configEpoch; /* 主节点/从节点的主节点配置纪元 */
uint64_t offset; /* 复制偏移量 */
char sender[CLUSTER_NAMELEN]; /* 发送节点的nodeId */
unsigned char myslots[CLUSTER_SLOTS/8]; /* 发送节点负责的槽信息 */
char slaveof[CLUSTER_NAMELEN]; /* 如果发送节点是从节点,记录对应主节点的nodeId */
uint16_t port; /* 端口号 */
uint16_t flags; /* 发送节点标识,区分主从角色,是否下线等 */
unsigned char state; /* 发送节点所处的集群状态 */
unsigned char mflags[3]; /* 消息标识 */
union clusterMsgData data /* 消息正文 */;
} clusterMsg;
- 消息体
union clusterMsgData {
/* ping,meet,pong消息体*/
struct {
/* gossip消息结构数组 */
clusterMsgDataGossip gossip[1];
} ping;
/* FAIL 消息体 */
struct {
clusterMsgDataFail about;
} fail;
// ...
};
-
消息处理
-
节点选择
Redis集群内节点通信采用固定频率(定时任务每秒执行10次)。因此节点每次选择需要通信的节点列表变得非常重要。
1.选择发送消息的节点数量
2.消息数据量 -
集群伸缩
伸缩原理:原理可抽象为槽和对应数据在不同节点之间灵活移动。
集群伸缩=槽和数据在节点之间的移动
扩容集群:
1)准备新节点。
redis-server conf/redis-6385.conf redis-server conf/redis-6386.conf
2)加入集群。
127.0.0.1:6379> cluster meet 127.0.0.1 6385 127.0.0.1:6379> cluster meet 127.0.0.1 6386
3)迁移槽和数据。
(1)槽迁移计划
(2)迁移数据
数据迁移过程是逐个槽进行的。
1)对目标节点发送cluster setslot{slot}importing{sourceNodeId}命令,让目标节点准备导入槽的数据。
2)对源节点发送cluster setslot{slot}migrating{targetNodeId}命令,让源节点准备迁出槽的数据。
3)源节点循环执行cluster getkeysinslot{slot}{count}命令,获取count个属于槽{slot}的键。
4)在源节点上执行migrate{targetIp}{targetPort}""0{timeout}keys{keys...}命令,把获取的键通过流水线(pipeline)机制批量迁移到目标节点,批量迁移版本的migrate命令在Redis3.0.6以上版本提供,之前的migrate命令只能单个键迁移。对于大量key的场景,批量键迁移将极大降低节点之间网络IO次数。
5)重复执行步骤3)和步骤4)直到槽下所有的键值数据迁移到目标节点。
6)向集群内所有主节点发送cluster setslot{slot}node{targetNodeId}命令,通知槽分配给目标节点。为了保证槽节点映射变更及时传播,需要遍历发送给所有主节点更新被迁移的槽指向新节点。
(3)添加从节点127.0.0.1:6386>cluster replicate 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756
收缩集群:
收缩集群意味着缩减规模,需要从现有集群中安全下线部分节点。
1.下线迁移槽
原理与之前节点扩容的迁移槽过程一致。127.0.0.1:6381> cluster nodes 40b8d09d44294d2e23c7c768efc8fcd153446746 127.0.0.1:6381 myself,master - 0 0 2 connected 12288-16383 4fa7eac4080f0b667ffeab9b87841da49b84a6e4 127.0.0.1:6384 slave 40b8d09d44294d2e23c7c768efc8fcd153446746 0 1469894180780 5 connected
6381是主节点,负责槽(12288-16383),6384是它的从节点
收缩正好和扩容迁移方向相反,6381变为源节点,其他主节点变为目标节点,源节点需要把自身负责的4096个槽均匀地迁移到其他主节点上。
2.忘记节点
建议使用redistrib.rb del-node{host:port}{downNodeId}命令
对于主从节点都下线的情况,建议先下线从节点再下线主节点,防止不必要的全量复制。
-
请求路由
1.请求重定向
请求重定向在集群模式下,Redis接收任何键相关命令时首先计算键对应的槽,再根据槽找出所对应的节点,如果节点是自身,则处理键命令;否则回复MOVED重定向错误,通知客户端请求正确的节点。这个过程称为MOVED重定向.
使用redis-cli命令时,可以加入-c参数支持自动重定向,简化手动发起重定向操作
1).计算槽
根据键的有效部分使用CRC16函数计算出散列值,再取对16383的余数
Pipeline同样可以受益于hash_tag,由于Pipeline只能向一个节点批量发送执行命令,而相同slot必然会对应到唯一的节点,降低了集群使用Pipeline的门槛。
2).槽节点查找
根据MOVED重定向机制,客户端可以随机连接集群内任一Redis获取键所在节点,这种客户端又叫Dummy(傀
儡)客户端,它优点是代码实现简单,对客户端协议影响较小,只需要根据重定向信息再次发送请求即可。但是它的弊端很明显,每次执行键命令前都要到Redis上进行重定向才能找到要执行命令的节点,额外增加了IO开销,这不是Redis集群高效的使用方式。正因为如此通常集群客户端都采用另一种实现:Smart(智能)客户端。 -
Smart客户端
1.smart客户端原理
Smart客户端通过在内部维护slot→node的映射关系,本地就可实现键到节点的查找,从而保证IO效率的最大化,而MOVED重定向负责协助Smart客户端更新slot→node映射。
2.Smart客户端——JedisCluster
public JedisCluster(Set jedisClusterNode, int connectionTimeout, int
soTimeout, int maxAttempts, final GenericObjectPoolConfig poolConfig) {
...
}
# ·SetjedisClusterNode:所有Redis Cluster节点信息(也可以是一部分,因为客户端可以通过cluster slots自动发现)。
# ·int connectionTimeout:连接超时。
# ·int soTimeout:读写超时。
# ·int maxAttempts:重试次数。
# ·GenericObjectPoolConfig poolConfig:连接池参数,JedisCluster会为Redis Cluster的每个节点创建连接池,
-
ASK重定向
1.客户端ASK重定向流程
ASK与MOVED:ASK重定向说明集群正在进行slot数据迁移,客户端无法知道什么时候迁移完成,因此只能是临时性的重定向,客户端不会更新slots缓存。但是MOVED重定向说明键对应的槽已经明确指定到新的节点,因此需要更新slots缓存。
2.节点内部处理
为了支持ASK重定向,源节点和目标节点在内部的clusterState结构中维护当前正在迁移的槽信息,用于识别槽迁移情况,结构如下:
typedef struct clusterState {
clusterNode *myself; /* 自身节点 /
clusterNode *slots[CLUSTER_SLOTS]; /* 槽和节点映射数组 */
clusterNode *migrating_slots_to[CLUSTER_SLOTS];/* 正在迁出的槽节点数组 */
clusterNode *importing_slots_from[CLUSTER_SLOTS];/* 正在迁入的槽节点数组*/
...
} clusterState;
集群环境下对于使用批量操作的场景,建议优先使用Pipeline方式,在客户端实现对ASK重定向的正确处理,这样既可以受益于批量操作的IO优化,又可以兼容slot迁移场景。
故障转移
- 故障发现
故障发现通过消息传播机制实现的,主要环节包括:主观下线(pfail)和客观下线(fail)。
主观下线:指某个节点认为另一个节点不可用,即下线状态,这个状态并不是最终的故障判定,只能代表一个节点的意见,可能存在误判情况。
客观下线:指标记一个节点真正的下线,集群内多个节点都认为该节点不可用,从而达成共识的结果。如果是持有槽的主节点故障,需要为该节点进行故障转移。
广播fail消息是客观下线的最后一步,通知集群内所有的节点标记故障节点为客观下线状态并立刻生效。通知故障节点的从节点触发故障转移流程。
问题:
网络分区会导致分割后的小集群无法收到大集群的fail消息,因此如果故障节点所有的从节点都在小集群内将导致无法完成后续故障转移,因此部署主从结构时需要根据自身机房/机架拓扑结构,降低主从被分区的可能性。
-
故障恢复
1.资格检查 检查从节点是否有资格替换故障的主节点
2.准备选举时间 当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该时间后才能执行后续流程。
3.发起选举
(1)更新配置纪元(标示当前主节点的版本)
配置纪元的主要作用:
①·标示集群内每个主节点的不同版本和当前集群最大的版本。
②·每次集群发生重要事件时,这里的重要事件指出现新的主节点(新加入的或者由从节点转换而来),从节点竞争选举。都会递增集群全局的配置纪元并赋值给相关主节点,用于记录这一关键事件。
③.主节点具有更大的配置纪元代表了更新的集群状态,因此当节点间进行ping/pong消息交换时,如出现slots等关键信息不一致时,以配置纪元更大的一方为准,防止过时的消息状态污染集群。
(2)广播选举消息 在集群内广播选举消息(FAILOVER_AUTH_REQUEST)
4.选举投票
5.替换主节点
1)当前从节点取消复制变为主节点。
2)执行clusterDelSlot操作撤销故障主节点负责的槽,并执行clusterAddSlot把这些槽委派给自己。
3)向集群广播自己的pong消息,通知集群内所有的节点当前从节点变为主节点并接管了故障主节点的槽信息。 - 故障转移时间
failover-time(毫秒) ≤ cluster-node-timeout + cluster-node-timeout/2 + 1000 // cluster-node-timeout默认15秒
主观识别时间+传播时间+选举时间
故障转移演练
集群运维
- 集群完整性
默认情况下当集群16384个槽任何一个没有指派到节点时整个集群不可用。但是当持有槽的主节点下线时,从故障发现到自动完成转移期间整个集群是不可用状态,对于大多数业务无法容忍这种情况,因此建议将参数cluster-require-full-coverage配置为no,当主节点故障时只影响它负责槽的相关命令执行,不会影响其他主节点的可用性。 - 带宽消耗
消息发送频率(cluster-node-timeout),消息数据量(slots槽数组和整个集群1/10的状态数据),节点部署的机器规模(机器越多每台机器划分的节点越均匀,则集群内整体的可用带宽越高。)
1.在满足业务需要的情况下尽量避免大集群。
2.适度提高cluster-node-timeout降低消息发送频率,同时cluster-nodetimeout还影响故障转移的速度,因此需要根据自身业务场景兼顾二者的平衡。
3.如果条件允许集群尽量均匀部署在更多机器上。 -
Pub/Sub广播问题(宽带)
- 集群倾斜
- 1.数据倾斜
节点和槽分配严重不均
不同槽对应键数量差异过大
集合对象包含大量元素
内存相关配置不一致
- 2.请求倾斜
合理设计键,热点大集合对象做拆分或使用hmget替代hgetall避免整体读取。
不要使用热键作为hash_tag,避免映射到同一槽。
对于一致性要求不高的场景,客户端可使用本地缓存减少热键调用。
集群读写分离
- 只读连接
// 默认连接状态为普通客户端:flags=N
127.0.0.1:6382> client list
id=3 addr=127.0.0.1:56499 fd=6 name= age=130 idle=0 flags=N db=0 sub=0 psub=0 multi=-1
qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
// 命令重定向到主节点
127.0.0.1:6382> get key:test:3130
(error) MOVED 12944 127.0.0.1:6379
// 打开当前连接只读状态
127.0.0.1:6382> readonly
OK
// 客户端状态变为只读:flags=r
127.0.0.1:6382> client list
id=3 addr=127.0.0.1:56499 fd=6 name= age=154 idle=0 flags=r db=0 sub=0 psub=0 multi=-1
qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
// 从节点响应读命令
127.0.0.1:6382> get key:test:3130
"value:3130"
- 读写分离
一般不建议集群模式下做读写分离。
手动故障转移
1)从节点通知主节点停止处理所有客户端请求。
2)主节点发送对应从节点延迟复制的数据。
3)从节点接收处理复制延迟的数据,直到主从复制偏移量一致为止,保证复制数据不丢失。
4)从节点立刻发起投票选举(这里不需要延迟触发选举)。选举成功后断开复制变为新的主节点,之后向集群广播主节点pong消息
5)旧主节点接受到消息后更新自身配置变为从节点,解除所有客户端请求阻塞,这些请求会被重定向到新主节点上执行。
6)旧主节点变为从节点后,向新的主节点发起全量复制流程。
-
应用场景
1.调整节点部署的问题.
2.当自动故障转移失败时,只要故障的主节点有存活的从节点就可以通过手动转移故障强制让从节点替换故障的主节点,保证集群的可用性。
cluster failver>cluster failover force>cluster failover takeover
数据迁移(单机-->集群)
唯品会:redis-migrate-tool
重点回顾
1)Redis集群数据分区规则采用虚拟槽方式,所有的键映射到16384个槽中,每个节点负责一部分槽和相关数据,实现数据和请求的负载均衡。
2)搭建集群划分三个步骤:准备节点,节点握手,分配槽。可以使用redis-trib.rb create命令快速搭建集群。
3)集群内部节点通信采用Gossip协议彼此发送消息,消息类型分为:ping消息、pong消息、meet消息、fail消息等。节点定期不断发送和接受ping/pong消息来维护更新集群的状态。消息内容包括节点自身数据和部分其他节点的状态数据。
4)集群伸缩通过在节点之间移动槽和相关数据实现。扩容时根据槽迁移计划把槽从源节点迁移到目标节点,源节点负责的槽相比之前变少从而达到集群扩容的目的,收缩时如果下线的节点有负责的槽需要迁移到其他节点,再通过cluster forget命令让集群内其他节点忘记被下线节点。
5)使用Smart客户端操作集群达到通信效率最大化,客户端内部负责计算维护键→槽→节点的映射,用于快速定位键命令到目标节点。集群协议通过Smart客户端全面高效的支持需要一个过程,用户在选择Smart客户端时建议review下集群交互代码如:异常判定和重试逻辑,更新槽的并发控制等。节点接收到键命令时会判断相关的槽是否由自身节点负责,如果不是则返回重定向信息。重定向分为MOVED和ASK,ASK说明集群正在进行槽数据迁移,客户端只在本次请求中做临时重定向,不会更新本地槽缓存。MOVED重定向说明槽已经明确分派到另一个节点,客户端需要更新槽节点缓存。
693
6)集群自动故障转移过程分为故障发现和故障恢复。节点下线分为主观下线和客观下线,当超过半数主节点认为故障节点为主观下线时标记它为客观下线状态。从节点负责对客观下线的主节点触发故障恢复流程,保证集群的可用性。
7)开发和运维集群过程中常见问题包括:超大规模集群带宽消耗,pub/sub广播问题,集群节点倾斜问题,手动故障转移,在线迁移数据等。