百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

Redis集群(cluster)-下:Redis集群的深入介绍

mhr18 2024-11-15 22:09 22 浏览 0 评论

在前面几篇文章我们介绍了Redis的基本原理,部署过程和故障切换等,本篇文章在前面的基础上对Redis做更进一步的介绍.

Redis的主要特性和架构解析

Redis集群目标

Redis被设计成一个可以解决以下问题的分布式架构:

  • 提供高性能和最高到1000个集群节点的横向线性扩展,访问集群不通过代理,集群数据异步复制等.
  • 可接受的写安全,redis系统使用最高效的方式来确保连接了大部分主节点的客户端发起的所有写操作都能够被接收到,当然也有一些少量的写丢失存在,因为网络分区故障导致客户端将数据写入到集群中的少数主节点上,集群恢复后写入到少数主节点的数据会被丢弃.
  • 高可用性:在确保每个主节点都有一个从节点的情况下,集群可以容忍一部分主节点宕机,这样也不会不影响集群正常运行.

哈希TAG功能

Redis集群在执行多键操作时,如果所执行的键不属于同一个哈希槽,则会报错,如下:

127.0.0.1:6379> mset user01 Chris user02 Jimes
(error) CROSSSLOT Keys in request don't hash to the same slot
127.0.0.1:6379> mget user01 user02
(error) CROSSSLOT Keys in request don't hash to the same slot

通过使用哈希tag的概念,我们可以强制让多个键属于同一个哈希槽中,这样我们就可以通过多键操作命令对这些键执行查询写入等操作.

客户端和服务器端在Redis集群中扮演的角色

Redis集群中的节点负责存储数据,获取集群运行状态,包括将指定的key映射到正确的节点上,同时集群节点还负责自动发现其他节点,探测故障节点,在主节点故障时选举出一个从节点代替故障主节点等.

为了实现上述Redis集群节点的功能,集群所有节点都是通过"TCP bus"和一个二进制协议互相连接,二进制协议和"TCP bus"统称为"Redis Cluster Bus",这里的Bus可以理解为一个交换机总线一样,各个集群节点通过交换机端口和交换机总线与其他集群节点相连接,我们不需要太纠结这两个概念到底什么意思.集群中每个节点都是通过Cluster Bus与其他节点相互通信.集群节点为了发现新的节点,通过gossip协议广播集群信息,发送ping包和其他节点通信来确保所有节点都是正常工作状态,其他节点收到ping包后返回PONG信息证明自己处于正常工作状态,当然也会向集群发送一些特殊的消息来标记一个特定事件的发生.Cluster Bus也被用来向整个集群广播发布/订阅(Pub/Sub)类型的消息用于手动执行故障切换.

由于集群节点没有使用代理,客户端的读写请求是由被访问的集群节点通过重定向错误信号MOVED和ASK将请求重定向到正确的数据节点上,理论上客户端是向集群所有节点发送读写请求,请求的数据会被重定向到正确数据的节点,因此客户端并不需要去保存集群的状态信息,但是客户端如果可以缓存键与集群节点的对应关系,这也是可以看做提高集群性能的一种方式.

写安全

Redis集群在不同节点之间使用异步复制,在一个主从架构的Redis集群中,如果其中一个主节点因为网络问题导致与集群大部分主节点隔离开来,那么客户端连接到该被隔离的主节点写入的数据可能丢失,因为写是异步的,这些写的数据不一定会同步到从节点上,而此时发生了故障转移,从节点被集群中其他大多数主节点选举成为了主节点,那么这些还没有来得及写入从节点的写就会被遗弃,这也意味着在一个网络分区故障的环境中,总会有一个短暂的时间造成数据写入丢失的现象,这和客户端的数据是写入了集群大部分节点还是集群中小部分节点上有关.

Redis尝试确保客户端的写操作被复制到集群中大部分主节点,与数据被写入到集群中少部分节点相比,在故障转移发生时,下列情况将会导致在集群网络分区故障后接收到的写数据丢失.

  1. 客户端一个写操作到达了主节点,与此同时写完成的确认消息发送给了客户端,由于redis使用异步复制,这个写操作可能还没有同步到它的从节点,如果此时主节点突然宕机,从节点被选举成了主节点,那么这个写数据就会永久丢失.
  2. 另外一种写失败的情况如下所述.
  • 主节点由于网络故障不可达
  • 该主节点被它的从节点执行了故障转移
  • 一段时间以后该主节点再次上线
  • 一个过期的客户端继续向该过期的主节点执行写入操作,那么在该过期的主节点被集群降级为从节点之前这段时间里,所有的写也同样会丢失.

第二种写故障是一种极端理想化的现象,但是一般不会发生,因为在实际中,一个主节点宕机再恢复后如果没有在规定的时间取得与集群其他节点通信,那么该恢复的主节点会拒绝客户端的读写请求.

如果对一个主节点执行故障转移,那么该主节点必须被集群大部分节点认为不可达且超过了NODE_TIMEOUT时间限制,因此如果在该超时之前网路故障恢复,那么该主节点不会丢失任何数据,但是当主节点网络分区超过了NODE_TIMEOUT时间限制后,这期间所有写入到该节点的的数据都会丢失,同时主节点将拒绝写入请求.

高可用性

网络故障会将集群一分为二,即一小部分节点在一个隔离的网络中,另外一大部分节点被隔离在另外一个网络中,当小部分节点网络分区故障时将会导致Redis集群暂时不可用,在其他网络分区至少拥有集群大部分主节点和一些不可达的主节点的从节点,当网路故障期间故障节点的超时时间超过NODE_TIMEOUT后,它的从节点将会请求选举并故障转移它的主节点,这样集群将会重新上线.

上述意味着Redis集群设计之初就允许一小部分集群节点故障,并且需要一定的恢复时间,在应用面对网络分区时要求高可用状态的条件下该种方式也许并不是一个非常适合的解决方案.

例如在一个集群中,共有N个主节点,每个主节点都有一个从节点,那么当集群有两个节点同时发生网络故障后,就有1/(N*2-1)的可能性导致集群不可用,例如五主五从的架构中,如果集群有两个节点宕机,那么集群就有1/(5*2-1)=11.11%的可能性出现故障的情况.

Redis集群提供了副本移动(replica migration)的功能,确保Redis集群在实际工作过程中不会出现单点故障的现象,即当一个主节点的从节点故障发生后,该主节点会变成orphaned节点,此时其他主节点的富余的从节点会自动作为该orphaned节点的从节点,所谓富余的意思是该主节点不止一个从节点,可能有多个.

性能

Redis集群的节点不负责代理功能,即将客户端访问的读写请求转发到正确的节点上,而是让客户端重定向到正确的真正负责存储数据的节点上.

通过上述方式,客户端最终会缓存一个最新的集群信息,即哪个节点负责处理哪些数据的信息,因此客户端可以通过缓存直接访问对应的负责存取数据的节点上.

由于Redis复制是异步的,集群反馈给客户端写入成功时并不等待其他节点返回数据写入是否成功的消息,除非显式的使用wait命令来降低数据未被写入集群节点的概率.

我们可以像操作单个Redis节点一样操作Redis集群,如果单个Redis节点的性能为1,如果集群由N个节点组成的话,那么集群的性能就是N,即单个节点的N倍.同时,关于延迟的问题,Redis集群的延迟和单个节点的延迟没有什么区别.

Redis集群主要组件概述

键分布模型(Keys distribution model)

Redis集群存放键的空间被分割成16384个哈希槽,集群中理论最多可以有16384个主节点(即一个节点保存一个槽位的数据),但是实际上推荐的主节点数量是1~1000个之间.

其中集群中的每个主节点都保存16384个哈希槽的一部分,集群环境在没有重新配置时处于稳定状态(重新配置指的是哈希槽从一个节点移动到另外一个节点),此时一个哈希槽只能被一个主节点负责处理(但是该主节点可以拥有多个从节点,并且在主节点出现故障时代替主节点也可以在一个不要求严格时效性的环境下分担主节点的查询操作.)

如何确定一个key所属的哈希槽

HASH_SLOT = CRC16(key) mod 16384

哈希标记(Keys hash tags)

在计算键所属的哈希槽时有一个例外的情况,即启用hash tags,hash tags确保将带有相同关键字的键存放到同一个哈希槽,上述介绍时我们知道使用多键命令读写的键只能属于同一个哈希槽.通过使用hash tags功能可以强制将不同的键统一放到指定的哈希槽.

关于如何使用hash tags,如果一个键名包含花括号{keyword},那么只有花括号中的keyword关键字部分才会被哈希并计算所属哈希槽,下面举例说明.

使用多键操作字符记录一个用户的信息时提示多个键不在同一个哈希槽

127.0.0.1:6379> mset userid0001_username Chris userid0001_age 20 userid0001_address Chicago
(error) CROSSSLOT Keys in request don't hash to the same slot

查看这些键数据哪些哈希槽

127.0.0.1:6379> CLUSTER KEYSLOT userid0001_username
(integer) 3121
127.0.0.1:6379> CLUSTER KEYSLOT userid0001_age
(integer) 267
127.0.0.1:6379> CLUSTER KEYSLOT userid0001_address
(integer) 7464

使用hash tags对这些键名做简单的修改,可以发现这些使用hash tags的键属于同一个哈希槽.

127.0.0.1:6379> CLUSTER KEYSLOT {userid0001}_username
(integer) 14177
127.0.0.1:6379> CLUSTER KEYSLOT {userid0001}_age
(integer) 14177
127.0.0.1:6379> CLUSTER KEYSLOT {userid0001}_address
(integer) 14177

我们就可以使用多键操作同时读写这些键

127.0.0.1:6379> mset {userid0001}_username Chris {userid0001}_age 20 {userid0001}_address Chicago
-> Redirected to slot [14177] located at 172.16.101.55:6379
OK
172.16.101.55:6379> mget {userid0001}_username {userid0001}_age {userid0001}_address
1) "Chris"
2) "20"
3) "Chicago"

集群节点属性(Cluster nodes attributes)

集群中每个节点都有一个唯一的ID,称为node ID,node ID是一个160bit的16进制随机数,节点第一次启动就会获得node ID,节点将会永久使用该node ID并将它保存到当前的持久化配置文件中,除非系统管理员删除该配置文件,或者集群通过CLUSTER RESTE命令重置.

node ID用于在整个集群中唯一标识一个节点,即使集群中的节点修改了IP地址,node ID也不会改变,集群会通过gossip协议探测出修改的IP和端口信息,并使用gossip协议通过Cluster BUS更新配置信息.

node ID不仅仅用来唯关联识一个节点,也用来保持全局一致性,每个节点都会关联下列信息,有的是关于该节点的集群配置信息,并且这些信息在全局中都是一致的,有的是关于该节点的自身信息,例如一个节点最后一次被ping的时间.

每个节点同时也维护有关于集群中其他节点的信息,格式为

<id> <ip:port@cport> <flags> <master> <ping-sent> <pong-recv> <config-epoch> <link-state> <slot> <slot> ... <slot>
  • id:表示节点的node ID
  • ip:port@cport:节点的ip地址,端口号和集群通信的端口号
  • flags:以逗号隔开,例如,master表示当前节点是master,slave表示当前是slave角色,myself表示是当前正在哪个节点执行cluster nodes命令等等.
  • master:如果当前节点是从节点,那么后面紧跟的是它的主节点的node ID,如果是主节点,那么紧跟着用"-"表示.
  • ping-sent:以毫秒为单位的unix时间戳,表示发送ping命令的时间,0表示当前没有ping
  • pong-recv:上一次收到pong回复的以毫秒为单位的unix时间戳.
  • config-epoch: 当前节点的配置纪元版本(如果当前节点是从节点,代表它的主节点的配置纪元),每次集群配置发生故障转移,节点的配置纪元都会单调递增一次,如果多个节点都宣称负责相同的哈希槽,那么配置纪元最高的节点将会获胜.
  • link-state:节点的链路状态,有两种情况,connected或者disconnected.
  • slot:节点负责的哈希槽.

上述信息我们可以在任意节点通过cluster nodes命令来查看,该命令用于描述以当前节点的角度,集群其他节点的信息.

127.0.0.1:6379> cluster nodes
3b234165c1a221916797154fe885527d747750d0 172.16.101.56:6379@16379 master - 0 1596383693000 11 connected 5501-10883
de7d464c9597f67bc4d129dfc5c3e2794c0194d0 172.16.101.60:6379@16379 slave 3b234165c1a221916797154fe885527d747750d0 0 1596383695105 11 connected
9641623bcd76ac49955e717e0c8815c4dbd19751 172.16.101.54:6379@16379 myself,master - 0 1596383694000 9 connected 0-5500
6b5c74706f418b0c5f97614781f0e1b7e4bdc90d 172.16.101.55:6379@16379 master - 0 1596383694595 10 connected 10884-16383
c5fecb329a41aeb0fd20285e8a92260a03654fd2 172.16.101.58:6379@16379 slave 9641623bcd76ac49955e717e0c8815c4dbd19751 0 1596383694095 9 connected
06da948f9ad937b93aa6dfd262ea6ee5e271ae9c 172.16.101.59:6379@16379 slave 6b5c74706f418b0c5f97614781f0e1b7e4bdc90d 0 1596383694596 10 connected
8add856f82fe30595835cc2194362898b14187c7 172.16.101.66:6379@16379 slave 9641623bcd76ac49955e717e0c8815c4dbd19751 0 1596383693091 9 connected

集群bus(The Cluster bus)

集群中的每个节点除了服务于客户端的默认6379端口外,还有一个用来与集群其他节点通信的额外端口,默认为服务端口加10000,即16379.

集群节点之间的通信通过集群的bus和集群的bus协议(由不同类型和大小的帧组成),该bus协议为Redis Cluster内部使用,暂时不会公开.

集群拓扑(Cluster topology)

Redis集群是一个由各个节点通过tcp相互连接的网格结构,在一个由N个节点组成的集群中,每个节点都有N-1出连接和N-1个入连接,这些连接会一直存在,当一个节点向集群的bus ping一个节点并期望得到pong回复时,在未到超时时间前,该节点将会重新刷新连接信息.

这里需要注意的是,Redis集群节点使用gossip协议和配置更新策略来避免在集群节点间正常通信时交换太多信息,因此信息交互并不会占用太多网络带宽.

节点握手(Nodes handshake)

集群节点会一直在集群的bus端口接受连接请求,回复ping求情,即使该节点是不可靠的,然而,如果发送ping的节点不是集群的一部分,那么节点将会丢弃除ping消息的其他消息请求.

一个节点只能通过以下两种方式接受另外一个节点作为集群的一部分.

  • 另一个节点通过meet消息告知当前节点,meet消息类似于ping的功能,但是meet消息强制接收者认可该节点作为集群的一部分,meet消息只能由管理员显式通过cluster meet ip port发出.
  • 如果节点A认可节点B,节点B认可节点C,那么最终节点B会发送gossip消息给节点A和节点C,节点A将会注册节点C的信息,并尝试与节点C建立连接,

这就意味着我们通过meet方式与集群中任意一个节点建立连接,那么最终通过gossip消息集群所有节点都会认可该节点作为集群的一部分,这种方式也使得集群更加稳健,也避免了其他集群的节点误加入到该集群中.

重定向和重新哈希(Redirection and resharding)

移动重定向(MOVED Redirection)

Redis客户端可以不受限制的向集群任意节点发送查询请求,包括从节点.被访问的节点会分析查询,如果查询是可接受的(意味着查询只涉及一个键,或者查询涉及多个键同时这些键都在同一个哈希槽),那么该节点将会查找负责处理这些键所属的哈希槽的节点.

如果哈希槽被当前节点管理,那么Redis客户端的查询会直接被当前节点处理,否则节点会检查内部的哈希槽到节点的映射关系,以MOVED error错误类型回复客户端,类似以下.

172.16.101.54:6379> get key2699
(error) MOVED 16165 172.16.101.55:6379

返回的错误包括该键所在的哈希槽(16165)以及负责该哈希槽的节点172.16.101.55:6379,客户端需要重新发起到正确节点的查询.这里需要注意的是,即使客户端在重新发起查询前等待了很长时间,在此期间集群配置更新过,当目标节点不再负责该哈希槽(16165)时将会再次回复客户端"MOVED error",如果所连接的节点没有更新配置信息,那么将返回同样的"MOVED error"给客户端,直到客户端找到负责该哈希槽的节点.

从集群节点的角度来看,集群的节点之间是通过node ID相互通信的,但是对客户端来说则简化为以ip:port和哈希槽的对应关系来展示.

客户端不需要将哈希槽和节点对应关系缓存起来,但是如果客户端缓存哈希槽16165和节点172.16.101.55:6379对应关系的话,当有新的查询请求到该哈希槽时,哈希槽的准确定位的实效性会高很多.

一个可供选择的客户端一侧的设计方案就是当客户端遇到"MOVED"重定向时通过"cluster nodes"或"cluster slots"命令刷新客户端本地的缓存.当重定向发生时,意味着可能不止一个哈希槽被重新配置,因此尽可能经常更新客户端一侧的配置信息是很好的策略.需要注意的是,如果集群是一个比较稳定的运行环境,最终所有客户端都将会获得哈希槽到节点的对应关系,这样可以使集群更有效,客户端不需要重定向或代理等可以直接访问到正确的节点.

一个客户端可以不处理MOVED重定向,但是必须能够处理稍后文章中解释的ASK重定向.

集群动态重配置

Redis集群支持在正常运行时进行增删节点的操作,增删节点本身都是要执行相同的操作:将哈希槽从一个节点转移到其他节点,这就意味着可以使用相同的基本策略再平衡集群配置,包括增加或者移除节点等.

  • 向集群增加一个节点:一个空节点将会被添加到集群,并从一个节点分配一部分哈希槽到新增加的节点.
  • 从集群移除一个节点:将该节点负责的哈希槽移动到其他节点.
  • 集群再平衡,在不同节点之间移动一部分哈希槽.

上述操作的核心是在集群内部不同节点之间互相移动哈希槽,从一个哈希槽的角度来看,就时一部分键的集合.因此Redis集群重新哈希的实质是将键从一个节点转移到另外一个节点,转移哈希槽意味着转移该哈希槽负责的所有键.

为了理解上述的工作过程,我们需要展示集群哈希槽的相关操作命令,下列哈希槽操作相关的命令是我们经常用到的.

  • CLUSTER ADDSLOTS slot1 [slot2] ... [slotN]
  • CLUSTER DELSLOTS slot1 [slot2] ... [slotN]
  • CLUSTER SETSLOT slot NODE node
  • CLUSTER SETSLOT slot MIGRATING node
  • CLUSTER SETSLOT slot IMPORTING node

ADDSLOTS和DELSLOTS命令仅仅用来在指定的节点分配或者移除哈希槽,分配哈希槽意味着通知一个主节点它将会负责存储和处理有关指定槽的读写请求.哈希槽分配之后,节点将会通过gossip协议向集群广播,在后面的章节会详细介绍.DELSLOTS操作实际上并不常用.

如果SETSLOT 后面跟是 <slot> NODE,SETSLOT 命令用来分配一个哈希槽到到一个指定的node ID,否则该哈希槽会被设置成如下两种状态MIGRATING和IMPORTING,这两种状态用来将一个哈希槽从一个节点移动到另外一个节点.

  • 当一个节点的哈希槽被设置成MIGRATING状态,该节点将会接受所有针对该哈希槽(键)的查询,但仅仅是要查询的键存在,否则查询被-ASK重定向到要MIGRATE的目标节点上.
  • 当一个哈希槽被设置成IMPORTING状态,该节点将会接受所有针对该哈希槽(键)的查询,但是仅仅接收被ASKING命令转发过来的查询请求,如果客户端没有发出ASKING命令,说明这是一个正常访问的客户端,不是由上述MIGRATING状态的节点转发的,那么该节点会返回MOVED错误并告知负责该哈希槽的正确节点信息.

假设我们有两个节点,节点A和节点B,我们当前把编号为8的哈希槽从节点A移动到节点B.

  • 我们在节点B上执行:CLUSTER SETSLOT 8 IMPORTING A
  • 我们在节点A上执行:CLUSTER SETSLOT 8 MIGRATING B

所有节点会继续将客户端对编号为8的哈希槽的查询转发给节点A,因此会发生下列现象.

  • 针对哈希槽8中存在节点A上的键的查询都由节点A处理.
  • 针对哈希槽8中不存在节点A上的键的查询都由节点A转发给节点B处理.

这种方式我们不会在节点A上新建任何key,在此期间,重新哈希过程中,redis-trib程序用来将哈希槽8的所有键从节点A迁移到节点B.该程序是通过以下命令实现的.

CLUSTER GETKEYSINSLOT slot count

上述命令将会返回指定哈希槽的键的信息,对于每个返回的key,redis-trib命令向节点A送一个"migrate"命令,用于原子性的将指定的键从节点A移动到节点B(源节点和目标节点会短暂锁住,这样确保移动过程没有其他竞赛的操作,例如删除等.),下列展示migrate工作流程.

MIGRATE target_host target_port key target_database id timeout

migerate命令会连接目标节点,发送序列化后的key版本,目标节点返回OK后,migrate会将源节点的key删除,从客户端的角度来看,任何时间该key在节点A或者节点B上.

在Redis集群中不需要指定一个大于0的数据库编号,因为migerate不是集群专用的命令,还会用来处理非集群的需求.migrate命令即使处理复杂的列表key也会足够快.

当migrate操作完成后,SETSLOT <slot> NODE <node-id> 命令被发送给源和目标节点,以便再次将哈希槽设置为正常的状态,相同的命令也会第一时间发送给集群中其他节点更新配置信息.

ASK重定向(ASK redirection)

ASK重定向主要发生在哈希槽从源节点移动到目标节点的过程中.

在前面的章节中我们简单介绍过ASK重定向,那么为什么不能简单的直接使用MOVED重定向呢?因为MOVED重定向意味着我们认为哈希槽已经移动到其他节点,下一个查询请求将会直接针对其他节点,ASK重定向意味着下一次查询只发送给指定的节点,是一种临时的重定向操作.

因为下一次针对编号为8的哈希槽的key查询很可能还在节点A上,因此我们仍希望客户端优先访问节点A,其次再是节点B,由于这种情况仅仅发生在16384个可用槽的其中一个,对集群性能的影响是可接受的.

我们需要强制客户端这样做,因此为了确保客户端仅仅在尝试节点A后再尝试节点B,节点B将会只接受客户端针对被设置IMPORTING 属性的槽发送ASKING命令的查询.

客户端的ASKING命令会设置一个一次性的标记来强制节点处理关于IMPORTING状态的哈希槽的查询.

以客户端的视角来看,一个完整的ASK重定向如下:

  • 如果客户端接收到ASK重定向,仅仅发送需要重定向的查询到指定的节点,但还是会持续向旧的节点发送后续查询.
  • 开始重定向查询
  • 不要更新本地编号为8的哈希槽映射到节点B.

一旦哈希槽8迁移完成,节点A将会发送MOVED消息同时客户端可以永久性的将哈希槽8到新的节点B的映射关系保存到缓存中.

客户端第一次连接和重定向处理

当客户端第一次连接集群环境时,并不知道哈希槽和集群节点之间的映射关系,仅仅依靠连接随机的集群节点并根据实际访问的哈希槽被集群节点重定向到指定的节点.可以看到这种方式效率比较低下.

Redis集群客户端应该智能的将集群的哈希槽分配信息持久化到自己的客户端缓存中,即使该持久化不要求保持最新的哈希槽分布信息,因为即使连接的节点不负责哈希槽数据,也会将请求重定向到正确的节点,但同时客户端也应该将该信息保存到本地.

客户端一般在下列情况会将集群哈希槽与节点映射关系保存到本地.

  • 客户端启动时在缓冲中初始化集群哈希槽配置情况.
  • MOVED操作发生时

这里需要注意的是,Redis集群客户端不一定是添加MOVED的哈希槽信息到缓存,也有可能会更新现有的缓存表中哈希槽的分布,一次性更新多个哈希槽信息并没有什么影响(例如从节点被提升为主节点的情况下,原主节点负责的哈希槽都会被重映射到被提升的从节点,从而导致大量的哈希槽被更新.),这种情况下如果客户端重新获取一份完整的哈希槽和节点的分布情况到本地缓存会简单很多.

为了检索集群节点的哈希槽分布情况,Redis集群提供了cluster nodes命令,客户端通过调用该命令可以获得哈希槽分布情况.

新的命令cluster slots以数组方式提供了更详细的哈希槽信息还包括被分配给的主节点以及从节点信息.

Redis集群容错系统

心跳检测和gossip信息

Redis集群节点之间不断的交换ping和pong报文,这两种报文都有相同的数据结构,并且均携带重要的配置信息,唯一的区别是消息类型字段不一样,我们称ping和pong为心跳检测报文.

一般情况下,节点发送ping报文的时候会触发接收节点回复pong报文,但有时候也不一定,节点仅仅发送关于节点配置信息的ping报文到其他节点这时并不要求回复pong报文,例如尽快通过广播方式发生一个更新的配置信息.

每个集群节点每秒钟会随机抽取其他几个集群节点发送ping报文,因为无论集群的节点数有多少,ping报文和pong报文的总数是一定的.

然而每个节点需要确保被ping的节点发送的ping或pong的时间不超过NODE_TIMEOUT 限制,在NODE_TIMEOUT 超时时间到达之前,节点会一直尝试与其他节点建立tcp连接以确保不是因为当前的tcp连接故障导致的..

心跳报文的组成

ping和pong报文包含了其他类型的包(例如故障转移的投票报文)都会有的头部信息,以及专用于ping和pong包中特定的gossip部分.

头部信息由下列组成.


  • node ID,20位的十六进制字符串,节点第一次创建的时候自动生成,当该节点作为集群节点存在时候会一直使用该node ID
  • 发送节点的当前纪元(currentEpoch)和配置纪元(configEpoch),Epoch用于在Redis集群分布式算法中使用,下一节会详细介绍,如果发送节点是从节点,则configEpoch是已知的它的主节点的最新的configEpoch值
  • node flags,用来声明该节点状态信息,一个主节点或者一个从节点,pfail和fail状态等.
  • 发送节点负责处理的哈希槽信息,如果发送节点是一个从节点的话,这里保存的就是它的主节点负责的哈希槽信息..
  • 发送者的tcp端口
  • 从发送者的角度观察的集群状态信息,值为down或者ok
  • 如果发送者是slave,这里保存的是它的主节点的node ID

ping和pong报文也包含了gossip协议的一部分,这部分主要提供了以发送节点的角度来看待集群其他节点的状态,这对于故障探测和发现集群新节点比较有用,gossip部分包含了发送节点已知节点中的随机一部分节点信息,gossip涉及的节点数量和集群规模有关.

gossip中每个节点都包含了如下信息:

  • Node ID
  • Node IP和端口号
  • Node Flags

gossip部分允许接收节点根据发送者的描述来感知其他节点的状态,在故障检测和新节点发现时候比较有用.

故障检测(Failure detection)

当一个集群主节点或者从节点被集群大部分节点认为不可达的时候会触发故障检测,从而导致从节点做出反应被提升为主节点,当从节点无法被提升为主节点时,集群节点就会抛出异常并停止接收客户端的读请求.

前面介绍过,每个节点都会都会包括多个关于其他节点的flags信息,用于标记其他节点的状态,例如master,slave,pfail,fail等.这里有两个用于故障检测的flag,称为pfail和fail,pfail意味着Possible failure,是一个不确定的故障类型,fail意味着一个节点被集群其他大多数主节点在指定的时间内认为已发生故障.

pfail flag:

当一个节点将其他节点标记为pflag状态时,说明该节点超过NODE_TIMEOUT超时时间仍无法访问,主节点和从节点都可以将其他节点标记为pfail状态,而无论该节点是否为主从.

集群节点不可达的判定是通过当前节点向其他节点发送的ping包被挂起,无法在指定的时间(NODE_TIMEOUT)内获得pong回复,所以在一个大的集群环境中,要保证NODE_TIMEOUT的值在一个合理的范围内,为了提高集群可靠性,当目标节点在到达NODE_TIMEOUT的一半还没有回复ping时,当前节点将会重新发起对该节点的连接,这个策略确保目标节点不是因为网络抖动导致的故障.

fail flag:

pfail flag仅仅是当前节点对其他节点的看法,还不至于触发从节点选举,如果一个节点处于pfail状态,那么只有节点状态到fail时才会被集群大多数节点认为故障.

在节点心跳部分我们提到过,每个节点都会随机选择若干其他节点发送gossip消息,最终每个节点都会获得其他节点的node flags信息.通过该方式每个节点都可以探测到其他节点的故障状态.

那么一个节点什么时候会从pfail状态过渡到fail状态呢?

  • 在一个gossip消息后,节点A将节点B标记为pfail状态.
  • 节点A通过gossip消息被告知集群大多数节点都将节点B标记为pfail状态.
  • 集群大多数节点在参数NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT的时间内标记节点B为pfail或者fail状态.

当以上条件都达到时,节点A将会

  • 将节点B标记为fail状态
  • 向所有可到达的节点发送节点B fail的消息

Redis集群将会强制每个接收到fail消息的节点将节点B标记为fail状态,而不管该节点是否将节点B标记为pfail状态.

这里需要注意是,fail状态是单向的,即只能由pfail过渡到fail状态,但是fail状态在下列情况下可以被清除.

  • 节点重新可达并且该节点是一个从节点,此时由于从节点fail不会触发故障转移,所以fail状态可以被清除.
  • 节点重新可达并且该节点是一个不负责任何哈希槽的主节点,由于该节点还未正式加入集群分配槽位,所以可以清除fail状态.
  • 节点重新可达并且该节点是一个长时间(NODE_TIMEOUT的N倍)内没有检测到从节点发生选举的主节点,这个问题最好的方法是让该节点重新加入集群.

需要注意是,pfail到fail的状态转换使用类似协议的方式,但是该协议有一定弊端,这是因为:

  • 节点需要经过一段时间来收集其他节点的状态,即使大部分节点认可某个节点pfail或者fail,该"认可"只是我们从不同节点的不同时刻收集到的状态,并不是实时的.然而集群会自动过滤掉某个接单过期的故障报告,因此pfail或者fail状态是一个特定时间段内的状态.
  • 当每个节点探测到fail状态发生后都会强制其他节点接收fail消息将对应节点标记为fail状态,但是并不能确保集群所有节点都会收到该消息,例如有的节点因为网络故障与集群隔离开来.

然而Redis集群故障探测有一个活跃度的要求:即最终所有节点对某一个故障节点都会有一个一致性状态.但是有两种情况是例外,这和集群脑裂有关,例如少数节点认为一个节点处于fail状态,或者少数节点认为一个节点不处于fail状态,无论这两种情况哪一种,最终集群状态收敛后只会有一种关于该节点的状态.

  • 由于故障检测和产生的连锁效应,集群大多数节点将节点标记为fail状态,最终在特定的时间范围内其他少数节点也会将该节点标记为fail状态.
  • 当只有集群少数节点将节点标记为fail状态,从节点选举不会发生(因为选举没有经过大数据节点的同意),这些少数节点将会清除fail信息(根据前面提到的清除fail的规则,超过NODE_TIMEOUT*N的时间限制后仍然没有发生选举请求. )

配置的更新,传播和故障转移(Configuration handling, propagation, and failovers)

集群当前纪元(Cluster current epoch)

epoch赋予一个给定事件的版本号,且该版本只能递增,当多个节点提供相同事件的不同状态时,通过epoch的值来决定哪个状态是最新的.

集群中的每个节点第一次创建时,包括从节点或者主节点,currentEpoch的值为0.

节点收到消息后,会检查本地保存的epoch和发送者的epoch的值,如果发送者的epoch的值大于本地的,currentEpoch的值会被更新为发送者的值,最终所有节点都将同意集群中最大的configEpoch的配置.

当集群状态发生变化时,节点将会根据epoch的值执行一些操作.当前仅仅在发生从节点提升主节点时,epoch可以简单的认为是集群的逻辑时钟并且规定在给定的信息中,epoch大的赢得epoch小的.

配置纪元(Configuration epoch)

每个主节点都会在ping和pong包中连同configEpoch和负责的哈希槽的位图情况通告给其他节点.

当节点新建时,主节点的configEpoch的值为0,

在从节点发生选举时,一个新的configEpoch会被创建,从节点尝试代替故障主节点时,会将自己的configEpoch加1并尝试得到大数据节点的授权,当从节点被授权后,一个新的唯一的configEpoch被创建,从节点使用新的configEpoch将自己转化成主节点.

如稍后的章节中描述,configEpoch用来帮助解决多个节点宣称有分歧的配置(一种可能的情况是部分节点由于网络分区故障).

从节点也会在ping和pong包中通告自己的configEpoch信息,但是它的configEpoch是对应主节点的或者最后一次获得的主节点的configEpoch.其他节点根据从节点的configEpoch来探测一个从节点的配置是否最新以及是否需要更新(主节点不会投票给一个有过期数据的从节点).

每次节点的configEpoch变化后,该值都会持久化到配置文件nodes.conf中,currentEpoch的值也是如此.

从节点选举和提升为主节点(Slave election and promotion)

从节点的选举和提升为主节点的操作由从节点发起,并由主节点授权,当一个主节点被至少一个从节点认为处于fail状态时从节点的选举会产生.

一个从节点提升自己为主节点,需要发起一场选举并且在选举中获胜,选举的目的是为了在多个从节点中选出一个最优的从节点,每个从节点都可以发起一个选举请求,然而只有一个从节点会赢得选举并顺利提升为主节点.

当下列条件达到后,从节点开始发起异常选举.

  • 从节点的主节点处于fail状态
  • 主节点负责处理至少一个哈希槽
  • 从节点和主节点断开的时间不超过指定的时间,这样可以确保被提升的从节点的数据是较新的,该时间可以通过参数指定.

为了参与选举,从节点的第一步就是将currentEpoch的值增加一位,然后让集群其他主节点进行投票.

从节点通过向集群广播"FAILOVER_AUTH_REQUEST"包来请求其他主节点投票,然后它将最多等待两倍的NODE_TIMEOUT时间(至少2秒)来等待其他主节点回复.其他主节点通过返回"FAILOVER_AUTH_ACK"投票给一个从节点,一旦其他主节点投票给了一个从节点后,它将不能再给定的时间周期(两倍的NODE_TIMEOUT时间)内再次给其他拥有相同主节点的从节点投票,同时在该周期内,也不回复原主节点的任何授权请求,这不是确保安全所必需的,但是可以阻止多个拥有相同主节点的从节点同时发起投票.

从节点会丢弃epoch小于当前currentEpoch的主节点的"FAILOVER_AUTH_ACK"回复,这样可以确保它不会接收到过期版本(小于自己的epoch)的投票.

一旦从节点接收到大多数主节点的ACK,它就会赢得选举,否则在一个特定时间内(两倍的NODE_TIMEOUT时间(至少2秒))没有接收到足够数量的ACK,从节点的选举就会放弃,一个新的选举将会重新产生(四倍的NODE_TIMEOUT时间(至少4秒)).

从节点等级(Slave rank)

一旦一个主节点处于fail状态,从节点将会等待短暂时间的延迟后再开始执行选举操作,延迟经过下列计算获得

DELAY = 500 milliseconds + random delay between 0 and 500 milliseconds + SLAVE_RANK * 1000 milliseconds.

延迟确保主节点的fail信息可以传播到集群所有节点,否则集群其他节点还没有感知主节点fail时从节点可能试着去选举,从而导致其他主节点拒绝该操作.

random delay用来确保从节点不在同一时刻发起选举请求.

SLAVE_RANK是一个从节点所处理的主节点数据量的等级,当主节点fail时从节点之间相互交换信息用以建立起一个等级,例如,拥有最新数据的从节点的等级为rank 0,拥有第二最新数据的从节点的等级为rank 1...,通过这种方式拥有最新数据的rank 0等级的从节点将会优先获得选举.

rank等级并不是严格遵循的,如果一个等级更高的从节点选举失败,那么其他的从节点将会重试.

一旦一个从节点赢得选举,它将会获得一个唯一的,递增的并且比其他所有已知主节点大的configEpoch值,并且作为主节点开始通过ping和pong包向集群广播,提供一个最新的configEpoch值以及负责处理的哈希槽信息.

为了更高效的将新的配置传播到其他节点,ping包将会广播到集群所有节点,当前不可达的节点最终也会被其他节点的ping包或pong包重新配置,或者如果它发布的信息通过心跳包被检测是过时的,它将从其他节点接收update包并更新自己的配置.

其他节点探测到有一个新的主节点负责处理相同的哈希槽并且比老的主节点拥有更大的configEpoch,将会更新它们的配置信息,原来老的主节点的从节点不仅会更新配置信息而且也会从新的主节点复制数据

主节点回复从节点的投票请求(Masters reply to slave vote request)

之前的章节我们讲述了从节点如何发起选举请求,这里我们了解一下从主节点的角度来看选举发生了什么.

主节点接收到从节点发出的投票包"FAILOVER_AUTH_REQUEST ",当下列条件到达后主节点会投票给从节点.

  • 一个主节点一次只投票给一个给定的epoch,并拒绝投票给老的epoch:每个主节点都有一个lastVoteEpoch字段,并且当从节点的授权请求包中的currentEpoch小于lastVoteEpoch字段的值时会拒绝再次投票,当一个主节点回复投票请求后,lastVoteEpoch会被立刻被更新,并且持久化到硬盘中.
  • 主节点只投票给主节点处于fail状态的从节点.
  • currentEpoch小于主节点的currentEpoch的授权请求将会被忽略,这是因为主节点已经保存了上一次从节点请求投票时的currentEpoch值,如果该从节点想再次请求投票,那么必须将自己的currentEpoch值加1.这样可以确保从节点不接受主节点的过期延迟的新的投票回复.

这里解释一下不遵循第三条规则产生的问题.

主节点的currentEpoch值为5,lastVoteEpoch值为1(可能发生多次失败的选举),

  • 从节点的currentEpoch值为3
  • 从节点尝试使用epoch 4(3+1)进行选举请求,主节点回复携带currentEpoch的值为5的确认消息,然而回复发生了延迟.
  • 从节点将会再次选举,稍后会使用epoch 5(4+1),延迟的带有currentEpoch为5的回复消息到达,从节点认为该值是真实有效可接受的.

实际上最后一条的延迟回复不应该起作用,因为那是针对上一次的投票请求的回复.

  1. 主节点不会在一个时间周期内(NODE_TIMEOUT * 2)再次投票给拥有相同主节点的从节点的投票请求,这不是严格要求的,因为同一时刻不可能有两个slave赢得选举请求,实际上这也是为了确保当一个从节点选举成功后有足够的时间来通知其他从节点避免其他节点发起新的选举请求.
  2. 主节点不参与选择一个最优的从节点,最优的从节点最有可能最早参与选举并且赢得投票,因为它的rank优先级最高.
  3. 如果一个主节点拒绝了一个从节点的投票请求,那么该请求将被从节点忽略.
  4. 主节点不会投票给一个发送configEpoch的值小于主节点保存的表中configEpoch的从节点.这里需要记住的,从节点发送的configEpoch的值是它的主节点的configEpoch,位图信息也是它的主节点的位图信息,这就意味着请求投票的进行故障转移的从节点必须拥有一个更新的或者与投票的主节点相等的哈希槽信息.

哈希槽配置传播(Hash slots configuration propagation)

Redis集群中一个重要的部分是广播哪个节点负责处理指定的哈希槽的信息机制.在下列情况发生时传播哈希槽信息时非常重要的,在一个全新的集群启动时和故障转移完成后从节点提升为主节点的配置更新时.

相同的机制允许被网络分割一段时间的节点以合理的方式重新加入集群.

有两种方式会导致哈希槽的配置被更新.

  1. heartbeat信息,发送者的ping或者pong信息携带这该节点负责的哈希槽信息(如果是从节点,则表示他的主节点).
  2. update信息,由于每个hearbeat包都有一个关于发送者的configEpoch和负责的哈希槽的信息,如果接收者发现发送者的heartbeat信息包含过期信息,它将会发送一个包含新信息的包,强制过期的节点更新自己的信息.

以上对Redis集群做一个深入的介绍.

相关推荐

Java培训机构,你选对了吗?(java培训机构官网)

如今IT行业发展迅速,不仅是大学生,甚至有些在职的员工都想学习java开发,需求量的扩大,薪资必定增长,这也是更多人选择java开发的主要原因。不过对于没有基础的学员来说,java技术不是一两天就能...

产品经理MacBook软件清单-20个实用软件

三年前开始使用MacBookPro,从此再也不想用Windows电脑了,作为生产工具,MacBook可以说是非常胜任。作为产品经理,值得拥有一台MacBook。MacBook是工作平台,要发挥更大作...

RAD Studio(Delphi) 本月隆重推出新的版本12.3

#在头条记录我的2025#自2024年9月,推出Delphi12.2版本后,本月隆重推出新的版本12.3,RADStudio12.3,包含了Delphi12.3和C++builder12.3最...

图解Java垃圾回收机制,写得非常好

什么是自动垃圾回收?自动垃圾回收是一种在堆内存中找出哪些对象在被使用,还有哪些对象没被使用,并且将后者删掉的机制。所谓使用中的对象(已引用对象),指的是程序中有指针指向的对象;而未使用中的对象(未引用...

Centos7 初始化硬盘分区、挂载(针对2T以上)添加磁盘到卷

1、通过命令fdisk-l查看硬盘信息:#fdisk-l,发现硬盘为/dev/sdb大小4T。2、如果此硬盘以前有过分区,则先对磁盘格式化。命令:mkfs.文件系统格式-f/dev/sdb...

半虚拟化如何提高服务器性能(虚拟化 半虚拟化)

半虚拟化是一种重新编译客户机操作系统(OS)将其安装在虚拟机(VM)上的一种虚拟化类型,并在主机操作系统(OS)运行的管理程序上运行。与传统的完全虚拟化相比,半虚拟化可以减少开销,并提高系统性能。虚...

HashMap底层实现原理以及线程安全实现

HashMap底层实现原理数据结构:HashMap的底层实现原理主要依赖于数组+链表+红黑树的结构。1、数组:HashMap最底层是一个数组,称为table,它存放着键值对。2、链...

long和double类型操作的非原子性探究

前言“深入java虚拟机”中提到,int等不大于32位的基本类型的操作都是原子操作,但是某些jvm对long和double类型的操作并不是原子操作,这样就会造成错误数据的出现。其实这里的某些jvm是指...

数据库DELETE 语句,还保存原有的磁盘空间

MySQL和Oracle的DELETE语句与数据存储MySQL的DELETE操作当你在MySQL中执行DELETE语句时:逻辑删除:数据从表中标记为删除,不再可见于查询结果物理...

线程池—ThreadPoolExecutor详解(线程池实战)

一、ThreadPoolExecutor简介在juc-executors框架概述的章节中,我们已经简要介绍过ThreadPoolExecutor了,通过Executors工厂,用户可以创建自己需要的执...

navicat如何使用orcale(详细步骤)

前言:看过我昨天文章的同鞋都知道最近接手另一个国企项目,数据库用的是orcale。实话实说,也有快三年没用过orcale数据库了。这期间问题不断,因为orcale日渐消沉,网上资料也是真真假假,难辨虚...

你的程序是不是慢吞吞?GraalVM来帮你飞起来性能提升秘籍大公开

各位IT圈内外的朋友们,大家好!我是你们的老朋友,头条上的IT技术博主。不知道你们有没有这样的经历:打开一个软件,半天没反应;点开一个网站,图片刷不出来;或者玩个游戏,卡顿得想砸电脑?是不是特别上火?...

大数据正当时,理解这几个术语很重要

目前,大数据的流行程度远超于我们的想象,无论是在云计算、物联网还是在人工智能领域都离不开大数据的支撑。那么大数据领域里有哪些基本概念或技术术语呢?今天我们就来聊聊那些避不开的大数据技术术语,梳理并...

秒懂列式数据库和行式数据库(列式数据库的特点)

行式数据库(Row-Based)数据按行存储,常见的行式数据库有Mysql,DB2,Oracle,Sql-server等;列数据库(Column-Based)数据存储方式按列存储,常见的列数据库有Hb...

AMD发布ROCm 6.4更新:带来了多项底层改进,但仍不支持RDNA 4

AMD宣布,对ROCm软件栈进行了更新,推出了新的迭代版本ROCm6.4。这一新版本里,AMD带来了多项底层改进,包括更新改进了ROCm的用户空间库和AMDKFD内核驱动程序之间的兼容性,使其更容易...

取消回复欢迎 发表评论: