This commit is contained in:
by931
2022-09-06 22:30:37 +08:00
parent 66970f3e38
commit 3d6528675a
796 changed files with 3382 additions and 3382 deletions

View File

@@ -139,14 +139,14 @@ function hide_canvas() {
<p>Redis-Cluster 中出现主节点故障后,检测故障需要经历单节点视角检测、检测信息传播、下线判决三个步骤,下文将结合源码分析。</p>
<h4>1.1 单点视角检测</h4>
<p>在第 03 课中介绍过,集群中的每个节点都会定期通过集群内部通信总线向集群中的其它节点发送 PING 消息,用于检测对方是否在线。如果接收 PING 消息的节点没有在规定的时间内(<code>cluster_node_timeout</code>)向发送 PING 消息的节点返回 PONG 消息,那么,发送 PING 消息的节点就会将接收 PING 消息的节点标注为疑似下线状态Probable FailPFAIL。如下源码</p>
<p><img src="assets/b77d4f90-a2ea-11e8-9b2d-01357ecc007a" alt="enter image description here" /></p>
<p><img src="assets/b77d4f90-a2ea-11e8-9b2d-01357ecc007a" alt="png" /></p>
<p>需要注意的是,判断 PFAIL 的依据也是参数 <code>cluster_node_timeout</code>。如果 <code>cluster_node_timeout</code> 设置过大,就会造成故障的主节点不能及时被检测到,集群恢复耗时增加,进而造成集群可用性降低。</p>
<h4>1.2 检测信息传播</h4>
<p>集群中的各个节点会通过相互发送消息的方式来交换自己掌握的集群中各个节点的状态信息如在线、疑似下线PFAIL、下线FAIL。例如当一个主节点 A 通过消息得知主节点 B 认为主节点 C 疑似下线时,主节点 A 会更新自己保存的集群状态信息,将从 B 获得的下线报告保存起来。</p>
<h4>1.3 基于检测信息作下线判决</h4>
<p>如果在一个集群里,超过半数的主节点都将某个节点 X 报告为疑似下线 (PFAIL),那么,节点 X 将被标记为下线FAIL并广播出去。所有收到这条 FAIL 消息的节点都会立即将节点 X 标记为 FAIL。至此故障检测完成。</p>
<p>下线判决相关的源码位于 <code>cluster.c</code> 的函数 <code>void markNodeAsFailingIfNeeded(clusterNode *node)</code> 中,如下所示:</p>
<p><img src="assets/8ded1b10-a2ef-11e8-bd43-c7fbf0d980da" alt="enter image description here" /></p>
<p><img src="assets/8ded1b10-a2ef-11e8-bd43-c7fbf0d980da" alt="png" /></p>
<p>通过源码可以清晰地看出,将一个节点标记为 FAIL 状态,需要满足两个条件:</p>
<ul>
<li>有超过半数的主节点将 Node 标记为 PFAIL 状态;</li>
@@ -164,7 +164,7 @@ function hide_canvas() {
<h3>2. Redis-Cluster 选举原理及优化分析</h3>
<h4>2.1 从节点拉票</h4>
<p>基于故障检测信息的传播集群中所有正常节点都将感知到某个主节点下线Fail的信息当然也包括这个下线主节点的所有从节点。当从节点发现自己复制的主节点的状态为已下线时从节点就会向集群广播一条请求消息请求所有收到这条消息并且具有投票权的主节点给自己投票。</p>
<p><img src="assets/a97b9c40-a3a2-11e8-a938-3b329a942b7b" alt="enter image description here" /></p>
<p><img src="assets/a97b9c40-a3a2-11e8-a938-3b329a942b7b" alt="png" /></p>
<h4>2.2 拉票优先级</h4>
<p>严格得讲,从节点在发现其主节点下线时,并不是立即发起故障转移流程而进行“拉票”的,而是要等待一段时间,在未来的某个时间点才发起选举,这个时间点的计算有两种方式。</p>
<h5><strong>方式一</strong></h5>
@@ -174,17 +174,17 @@ function hide_canvas() {
<p>其中newRank 和 oldRank 分别表示本次和上一次排名。</p>
<p>注意,如果当前系统时间小于需要等待的时刻,则返回,下一个周期再检查。</p>
<p>源码如下:</p>
<p><img src="assets/c67c3500-a39a-11e8-80dc-8d254ca863fe" alt="enter image description here" /></p>
<p><img src="assets/c67c3500-a39a-11e8-80dc-8d254ca863fe" alt="png" /></p>
<h5><strong>方式二</strong></h5>
<p>既然是拉票,就有可能因未能获得半数投票而失败,一轮选举失败后,需要等待一段时间(<code>auth_retry_time</code>)才能清理标志位,准备下一轮拉票。从节点拉票之前也需要等待,等待时间计算方法如下:</p>
<pre><code>mstime() + 500ms + random()%500ms + rank*1000ms
</code></pre>
<p>其中500 ms 为固定延时,主要为了留出时间,使主节点下线的消息能传播到集群中其它节点,这样集群中的主节点才有可能投票;<code>random()%500ms</code> 表示随机延时为了避免两个从节点同时开始故障转移流程rank 表示从节点的排名,排名是指当前从节点在下线主节点的所有从节点中的排名,排名主要是根据复制数据量来定,复制数据量越多,排名越靠前,因此,具有较多复制数据量的从节点可以更早发起故障转移流程,从而更可能成为新的主节点。</p>
<p>源码如下:</p>
<p><img src="assets/df21fe20-a39d-11e8-a938-3b329a942b7b" alt="enter image description here" /></p>
<p><img src="assets/df21fe20-a39d-11e8-a938-3b329a942b7b" alt="png" /></p>
<h5><strong>可优化点</strong></h5>
<p>上面提到的 <code>auth_retry_time</code> 是一个潜在的可优化点,也是一个必要的注意点,其计算方法如下源码所示:</p>
<p><img src="assets/3e9611b0-a39f-11e8-8b4a-cf3651600922" alt="enter image description here" /></p>
<p><img src="assets/3e9611b0-a39f-11e8-8b4a-cf3651600922" alt="png" /></p>
<p>从中可以看出,<code>auth_retry_time</code> 的取值为 <code>4*cluster_node_timeout (cluster_node_timeout&gt;1s)</code>。如果一轮选举没有成功,再次发起投票需要等待 <code>4*cluster_node_timeout</code>,按照 <code>cluster_node_timeout</code> 默认值为 15 s 计算,再次发起投票需要等待至少一分钟,如果故障的主节点只有一个从节点,则难以保证高可用。</p>
<p>在实际应用中,每个主节点通常设置 1-2 个从节点,为了避免首轮选举失败后的长时间等待,可根据需要修改源码,将 <code>auth_retry_time</code> 的值适当减小,如 10 s 左右。</p>
<h4>2.3 主节点投票</h4>
@@ -197,7 +197,7 @@ function hide_canvas() {
<p>选举新主节点的算法是基于 Raft 算法的 Leader Election 方法来实现的,关于 Raft 算法在第07课中将有详细介绍此处了解即可。</p>
<h3>3. Redis-Cluster 的 Failover 原理</h3>
<p>所有发起投票的从节点中,只有获得超过半数主节点投票的从节点有资格升级为主节点,并接管故障主节点所负责的 Slots源码如下</p>
<p><img src="assets/b720d8a0-a3a3-11e8-8b4a-cf3651600922" alt="enter image description here" /></p>
<p><img src="assets/b720d8a0-a3a3-11e8-8b4a-cf3651600922" alt="png" /></p>
<p>主要包括以下几个过程。</p>
<p>1身份切换</p>
<p>通过选举晋升的从节点会执行一系列的操作,清除曾经为从的信息,改头换面,成为新的主节点。</p>
@@ -208,10 +208,10 @@ function hide_canvas() {
<p>4履行义务</p>
<p>在其位谋其政,新的主节点开始处理自己所负责 Slot 对应的请求,至此,故障转移完成。</p>
<p>上述过程由 <code>cluster.c</code> 中的函数 <code>void clusterFailoverReplaceYourMaster(void)</code> 完成,源码如下所示:</p>
<p><img src="assets/62702c50-a3a5-11e8-a938-3b329a942b7b" alt="enter image description here" /></p>
<p><img src="assets/62702c50-a3a5-11e8-a938-3b329a942b7b" alt="png" /></p>
<h3>4. 客户端的优化思路</h3>
<p>Redis-Cluster 发生故障后,集群的拓扑结构一定会发生改变,如下图所示:</p>
<p><img src="assets/7a5b8850-9671-11e8-9c35-b59aad3fef8b" alt="enter image description here" /></p>
<p><img src="assets/7a5b8850-9671-11e8-9c35-b59aad3fef8b" alt="png" /></p>
<p>一个 3 主 3 从的集群,其中一台服务器因故障而宕机,从而导致该服务器上部署的两个 Redis 实例(一个 Master一个 Slava下线集群的拓扑结构变成了 3 主 1 备。</p>
<h4>4.1 客户端如何感知 Redis-Cluster 发生故障?</h4>
<p>结合上面介绍的故障场景,<strong>思考这样一个问题</strong>:当 Redis-Cluster 发生故障,集群拓扑结构变化时,如果客户端没有及时感知到,继续试图对已经故障的节点进行“读写操作”,势必会出现异常,那么,如何应对这种场景呢?</p>
@@ -237,7 +237,7 @@ function hide_canvas() {
<p>基于 4.1 节的分析,相信读者已经可以构想出优化思路。在此,我将以 Redis 的高级 Java 客户端 Lettuce 为例,简单介绍一下客户端的耗时优化。</p>
<p>2017 年,国内某电商巨头的仓储系统出现故障(一台服务器宕机),管理页面登录超时(超过一分钟才登录完成),经过评估,判定为系统性能缺陷,需要优化。通过分解登录耗时,发现缓存访问耗时长达 28 秒,进一步排查确认宕机的服务器上部署有两个 Redis 节点,结合日志分析,发现 Redis-Cluster 故障后(两个 Redis 节点下线),客户端感知故障耗 20 秒,为症结所在。</p>
<p>为了优化耗时,我当时阅读了开源客户端 Lettuce 的源码,原来 Lettuce 的连接超时机制采用的超时时间为 10s部分源码如下</p>
<p><img src="assets/f909d460-d2d8-11e8-93cf-c998d6347dcb" alt="enter image description here" /></p>
<p><img src="assets/f909d460-d2d8-11e8-93cf-c998d6347dcb" alt="png" /></p>
<p>当 Redis-Cluster 故障后客户端Lettuce感知到连接不可用后会分别与故障的 Redis 节点进行重试,而重试的超时时间为 10s两个节点耗时 <code>10*2 s = 20 s</code></p>
<p>至此,优化就显得很简单了,比如,思路 1 缩短超时参数 <code>DEFAULT_CONNECT_TIMEOUT</code>,思路 2 中 客户端感知到连接不可用之后不进行重试,直接重建新连接,关闭旧连接。</p>
<h3>5. 后记</h3>