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

@@ -172,7 +172,7 @@ function hide_canvas() {
<p>Availability = MTBF / (MTBF + MTTR)*100%</p>
</blockquote>
<p>其中MTBFMean Time Between Failure是指相邻两次故障之间的平均工作时间MTTRMean Time To Repair是指系统由故障状态转为工作状态所需修复时间的平均值。通常用 N 个9来表征系统可用性比如99.9%3-nines Availability99.999%5-nines Availability</p>
<p><img src="assets/0ed5f4d0-90ea-11e8-a17a-affb06b38793" alt="enter image description here" /></p>
<p><img src="assets/0ed5f4d0-90ea-11e8-a17a-affb06b38793" alt="png" /></p>
<p>图片出自:<a href="https://blog.csdn.net/zyhlwzy/article/details/78658002">CSDN 博客</a></p>
<h4>2.4 可靠性</h4>
<p>与可用性不同,可靠性是指在给定的时间间隔和给定条件下,系统能正确执行其功能的概率。可靠性的量化指标是周期内系统平均无故障运行时间,可用性的量化指标是周期内系统无故障运行的总时间。这种“官方定义”比较晦涩,下面举一个简单的例子。</p>
@@ -299,15 +299,15 @@ str.toUpperCase();//指令3
<p>支持异步通信协议,消息的发送者将消息发送到消息队列后可以立即返回,不用等待接收者的响应。消息会被保存在队列中,直到被接收者取出。消息的发送与处理是完全异步的。下面通过一个例子来说明。</p>
<p>对于大多数应用,在用户注册后,都需要发注册邮件和注册短信。传统的做法有两种:</p>
<p><strong>1.</strong> 串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端,如下图示:</p>
<p><img src="assets/b4d236f0-966c-11e8-bd60-15398afc36e1" alt="enter image description here" /></p>
<p><img src="assets/b4d236f0-966c-11e8-bd60-15398afc36e1" alt="png" /></p>
<p><strong>2.</strong> 并行方式:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的效率。</p>
<p><img src="assets/cd60d5f0-966c-11e8-bd60-15398afc36e1" alt="enter image description here" /></p>
<p><img src="assets/cd60d5f0-966c-11e8-bd60-15398afc36e1" alt="png" /></p>
<p>接下来,我们引入消息队列,来实现异步处理。</p>
<p>将注册信息写入数据库成功后,将消息写入消息队列,然后立即返回成功;此后,邮件系统和短信系统分别从消息队列中获取注册信息,再发送注册邮件和短信。很明显,借助消息队列的异步处理能力,将极大的提高响应速度。</p>
<p><img src="assets/de0257d0-966c-11e8-bd60-15398afc36e1" alt="enter image description here" /></p>
<p><img src="assets/de0257d0-966c-11e8-bd60-15398afc36e1" alt="png" /></p>
<h5><strong>应用解耦</strong></h5>
<p>以电商 IT 架构为例,在传统紧耦合订单场景里,客户在电商网站下订单,订单系统接收到请求后,立即调用库存系统接口,库存减一,如下图所示:</p>
<p><img src="assets/13138c50-966d-11e8-9c35-b59aad3fef8b" alt="enter image description here" /></p>
<p><img src="assets/13138c50-966d-11e8-9c35-b59aad3fef8b" alt="png" /></p>
<p>上述模式存在巨大风险:</p>
<ol>
<li>假如库存系统无法访问(升级、业务变更、故障等),则订单减库存将失败,从而导致订单失败;</li>
@@ -316,7 +316,7 @@ str.toUpperCase();//指令3
<p>我们引入消息队列,解除强耦合性,处理流程又会怎样呢?</p>
<p>订单系统中,用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功,此时客户可以认为下单成功。消息队列提供异步的通信协议,消息的发送者将消息发送到消息队列后可以立即返回,不用等待接收者的响应。消息会被保存在队列中,直到被接收者取出。</p>
<p>库存系统中,从消息队列中获取下单信息,库存系统根据下单信息进行库存操作。</p>
<p><img src="assets/2ef43c80-966d-11e8-bd60-15398afc36e1" alt="enter image description here" /></p>
<p><img src="assets/2ef43c80-966d-11e8-bd60-15398afc36e1" alt="png" /></p>
<h5><strong>流量削锋</strong></h5>
<p>像双11秒杀、预约抢购等活动通常会出现流量暴增当外部请求超过系统处理能力时如果系统没有做相应保护可能因不堪重负而挂掉。</p>
<p>这时,我们可以引入消息队列,缓解短时间内高流量压力:</p>
@@ -324,7 +324,7 @@ str.toUpperCase();//指令3
<li>用户的秒杀请求,服务器接收后,首先写入消息队列,然后返回成功。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到失败页面;</li>
<li>秒杀业务根据消息队列中的请求信息再做后续处理根据数据库实际的select、insert、update 能力处理注册、预约申请)。</li>
</ol>
<p><img src="assets/3b1edc40-966d-11e8-9f67-05ec09da262a" alt="enter image description here" /></p>
<p><img src="assets/3b1edc40-966d-11e8-9f67-05ec09da262a" alt="png" /></p>
<h5><strong>消息通讯</strong></h5>
<p>消息通讯很好理解,以微信群聊为例:</p>
<ol>

View File

@@ -188,17 +188,17 @@ function hide_canvas() {
<p>Redis 的内存模型比较复杂,内容也较多,感兴趣的读者可以查阅<a href="https://www.cnblogs.com/qwangxiao/p/8921171.html">《深入了解 Redis 的内存模型》博客</a>做更深了解。</p>
<h5><strong>Redis 开源客户端</strong></h5>
<p>Redis 的开源客户端众多,几乎支持所有编程语言,如下图所示。其中常用的 Java 客户端有 Jedis、Lettuce 以及 Redission。</p>
<p><img src="assets/d3a75580-916a-11e8-be3d-c18388642029" alt="enter image description here" /></p>
<p><img src="assets/d3a75580-916a-11e8-be3d-c18388642029" alt="png" /></p>
<h5><strong>Redis 支持事务</strong></h5>
<p>Redis 提供了一些在一定程度上支持线程安全和事务的命令,例如 multi/exec、watch、inc 等。由于 Redis 服务器是单线程的,任何单一请求的服务器操作命令都是原子的,但跨客户端的操作并不保证原子性,所以对于同一个连接的多个操作序列也不保证事务。</p>
<h4>1.3 Redis 高可用解决方案</h4>
<p>Redis 有很多高可用的解决方案,本节只简单介绍其中三种。</p>
<h5><strong>方案1Redis Cluster</strong></h5>
<p>从3.0版本开始Redis 支持集群模式——Redis Cluster可线性扩展到1000个节点。Redis-Cluster 采用无中心架构,每个节点都保存数据和整个集群状态,每个节点都和其它所有节点连接,客户端直连 Redis 服务,免去了 Proxy 代理的损耗。Redis Cluster 最小集群需要三个主节点,为了保障可用性,每个主节点至少挂一个从节点(当主节点故障后,对应的从节点可以代替它继续工作),三主三从的 Redis Cluster 架构如下图所示:</p>
<p><img src="assets/d4ca8050-966e-11e8-bd60-15398afc36e1" alt="enter image description here" /></p>
<p><img src="assets/d4ca8050-966e-11e8-bd60-15398afc36e1" alt="png" /></p>
<h5><strong>方案2Twemproxy</strong></h5>
<p>Twemproxy 是一个使用 C 语言编写、以代理的方式实现的、轻量级的 Redis 代理服务器。它通过引入一个代理层,将应用程序后端的多台 Redis 实例进行统一管理,使应用程序只需要在 Twemproxy 上进行操作,而不用关心后面具体有多少个真实的 Redis 实例,从而实现了基于 Redis 的集群服务。当某个节点宕掉时Twemproxy 可以自动将它从集群中剔除而当它恢复服务时Twemproxy 也会自动连接。由于是代理Twemproxy 会有微小的性能损失。</p>
<p>Twemproxy 架构如下图所示: <img src="assets/4deaf140-9336-11e8-9530-11b8f149bb7c" alt="enter image description here" /></p>
<p>Twemproxy 架构如下图所示: <img src="assets/4deaf140-9336-11e8-9530-11b8f149bb7c" alt="png" /></p>
<h5><strong>方案3Codis</strong></h5>
<p>Codis 是一个分布式 Redis 解决方案,对于上层的应用来说,连接到 Codis Proxy 和连接原生的 Redis Server 没有明显的区别(部分命令不支持), 上层应用可以像使用单机的 Redis 一样使用Codis 底层会处理请求的转发,不停机的数据迁移等工作。关于 Codis在第03课中将详细介绍。</p>
<h3>2. Memcached 介绍</h3>

View File

@@ -136,11 +136,11 @@ function hide_canvas() {
<p>Redis 是一个开源的、高性能的 Key-Value 数据库。基于 Redis 的分布式缓存已经有很多成功的商业应用,其中就包括阿里 ApsaraDB阿里 Tair 中的 RDB 引擎,美团 MOS 以及腾讯云 CRS。本文我将着重介绍 Redis Cluster 原理、类 Codis 分布式方案以及分布式信息一致性协议 Gossip以帮助大家深入理解 Redis。</p>
<h3>1. Redis 单机模式</h3>
<p>顾名思义,单机模式指 Redis 主节点以单个节点的形式存在这个主节点可读可写上面存储数据全集。在3.0版本之前Redis 只能支持单机模式出于可靠性考量通常单机模式为“1主 N 备”的结构,如下所示:</p>
<p><img src="assets/37baddd0-9409-11e8-968c-a5eca6168c1e" alt="enter image description here" /></p>
<p><img src="assets/37baddd0-9409-11e8-968c-a5eca6168c1e" alt="png" /></p>
<p>需要说明的是,即便有很多个 Redis 主节点,只要这些主节点以单机模式存在,本质上仍为单机模式。单机模式比较简单,足以支撑一般应用场景,但单机模式具有固有的局限性:不支持自动故障转移,扩容能力极为有限(只能 Scale Up垂直扩容存在高并发瓶颈。</p>
<h4>1.1 不支持自动故障转移</h4>
<p>Redis 单机模式下即便是“1主 N 备”结构当主节点故障时备节点也无法自动升主即无法自动故障转移Failover。故障转移需要“哨兵”Sentinel 辅助Sentinel 是 Redis 高可用的解决方案,由一个或者多个 Sentinel 实例组成的系统可以监视 Redis 主节点及其从节点,当检测到 Redis 主节点下线时,会根据特定的选举规则从该主节点对应的所有从节点中选举出一个“最优”的从节点升主,然后由升主的新主节点处理请求。具有 Sentinel 系统的单机模式示意图如下:</p>
<p><img src="assets/43d7e900-9409-11e8-968c-a5eca6168c1e" alt="enter image description here" /></p>
<p><img src="assets/43d7e900-9409-11e8-968c-a5eca6168c1e" alt="png" /></p>
<h4>1.2 扩容能力极为有限</h4>
<p>这一点应该很好理解单机模式下只有主节点能够写入数据那么最大数据容量就取决于主节点所在物理机的内存容量而物理机的内存扩容Scale Up能力目前仍是极为有限的。</p>
<h4>1.3 高并发瓶颈</h4>
@@ -163,14 +163,14 @@ function hide_canvas() {
<h4>2.2 Redis-Cluster 实现基础:分片</h4>
<p>Redis 集群实现的基础是分片,即将数据集有机的分割为多个片,并将这些分片指派给多个 Redis 实例,每个实例只保存总数据集的一个子集。利用多台计算机内存和来支持更大的数据库,而避免受限于单机的内存容量;通过多核计算机集群,可有效扩展计算能力;通过多台计算机和网络适配器,允许我们扩展网络带宽。</p>
<p>基于“分片”的思想Redis 提出了 Hash Slot。Redis Cluster 把所有的物理节点映射到预先分好的16384个 Slot 上,当需要在 Redis 集群中放置一个 Key-Value 时,根据 CRC16(key) Mod 16384的值决定将一个 Key 放到哪个 Slot 中。</p>
<p><img src="assets/6d2751b0-9409-11e8-8505-51bc0cd8cebd" alt="enter image description here" /></p>
<p><img src="assets/6d2751b0-9409-11e8-8505-51bc0cd8cebd" alt="png" /></p>
<h4>2.3 Redis Cluster 请求路由方式</h4>
<p>客户端直连 Redis 服务进行读写操作时Key 对应的 Slot 可能并不在当前直连的节点上,经过“重定向”才能转发到正确的节点。如下图所示,我们直接登录 <code>127.0.0.1:6379</code> 客户端,进行 Set 操作,当 Key 对应的 Slot 不在当前节点时(如 key-test),客户端会报错并返回正确节点的 IP 和端口。Set 成功则返回 OK。</p>
<p><img src="assets/98cb58c0-9409-11e8-9195-716ac3b68939" alt="enter image description here" /></p>
<p><img src="assets/98cb58c0-9409-11e8-9195-716ac3b68939" alt="png" /></p>
<p>以集群模式登录 <code>127.0.0.1:6379</code> 客户端(注意命令的差别:<code>-c</code> 表示集群模式)则可以清楚的看到“重定向”的信息并且客户端也发生了切换“6379” -&gt; “6381”。</p>
<p><img src="assets/9f0bb040-9409-11e8-8505-51bc0cd8cebd" alt="enter image description here" /></p>
<p><img src="assets/9f0bb040-9409-11e8-8505-51bc0cd8cebd" alt="png" /></p>
<p>以三节点为例,上述操作的路由查询流程示意图如下所示:</p>
<p><img src="assets/cbc847a0-94d7-11e8-8763-ad4af9040eed" alt="enter image description here" /></p>
<p><img src="assets/cbc847a0-94d7-11e8-8763-ad4af9040eed" alt="png" /></p>
<p>和普通的查询路由相比Redis Cluster 借助客户端实现的请求路由是一种混合形式的查询路由,它并非从一个 Redis 节点到另外一个 Redis而是借助客户端转发到正确的节点。</p>
<p>实际应用中,可以在客户端缓存 Slot 与 Redis 节点的映射关系,当接收到 MOVED 响应时修改缓存中的映射关系。如此,基于保存的映射关系,请求时会直接发送到正确的节点上,从而减少一次交互,提升效率。</p>
<p>目前,包括 Lettuce、Jedis、Redission 在内的许多 Redis Client都已经实现了对 Redis Cluster 的支持关于客户端的内容将在第05课中详细介绍。</p>
@@ -287,9 +287,9 @@ function hide_canvas() {
</ol>
<h4>4.2 如何保证消息传播的效率?</h4>
<p>前面已经提到,集群的周期性函数 <code>clusterCron()</code> 执行周期是 100ms为了保证传播效率每10个周期也就是 1s每个节点都会随机选择5个其它节点并从中选择一个最久没有通信的节点发送 ing消息源码如下</p>
<p><img src="assets/77be7210-962b-11e8-a401-79c652738b8e" alt="enter image description here" /></p>
<p><img src="assets/77be7210-962b-11e8-a401-79c652738b8e" alt="png" /></p>
<p>当然这样还是没法保证效率毕竟5个节点是随机选出来的其中最久没有通信的节点不一定是全局“最久”。因此对哪些长时间没有“被” 随机到的节点进行特殊照顾每个周期100ms内扫描一次本地节点列表如果发现节点最近一次接受 Pong 消息的时间大于 <code>cluster_node_timeout/2</code>,则立刻发送 Ping 消息,防止该节点信息太长时间未更新。源码如下:</p>
<p><img src="assets/3f9d0570-962d-11e8-8341-6d7159452ca4" alt="enter image description here" /></p>
<p><img src="assets/3f9d0570-962d-11e8-8341-6d7159452ca4" alt="png" /></p>
<h4>4.3 规模效应——无法忽略的成本问题</h4>
<h5><strong>关键参数 cluster_node_timeout</strong></h5>
<p>从上面的分析可以看出,<code>cluster_node_timeout</code> 参数对消息发送的节点数量影响非常大。当带宽资源紧张时可以适当调大这个参数如从默认15秒改为30秒来降低带宽占用率。但是过度调大 <code>cluster_node_timeout</code> 会影响消息交换的频率从而影响故障转移、槽信息更新、新节点发现的速度,因此需要根据业务容忍度和资源消耗进行平衡。同时整个集群消息总交换量也跟节点数成正比。</p>
@@ -360,7 +360,7 @@ function hide_canvas() {
<blockquote>
<p>hash(key)%N = 目标节点编号, 其中 N 为 Redis 主节点的数量,哈希取余的方式会将不同的 Key 分发到不同的 Redis 主节点上。</p>
</blockquote>
<p><img src="assets/3b9c14e0-95a4-11e8-918e-f54082ec58b3" alt="enter image description here" /></p>
<p><img src="assets/3b9c14e0-95a4-11e8-918e-f54082ec58b3" alt="png" /></p>
<p>但是Hash 算法有很多缺陷:</p>
<ol>
<li>不支持动态增加节点:当业务量增加,需要增加服务器节点后,上面的计算公式变为:<code>hash(key)%(N+1)</code>,那么,对于同一个 Key-Value增加节点前后对应的 Redis 节点可能是完全不同的,可能导致大量之前存储的数据失效;为了解决这个问题,需要将所有数据重新计算 Hash 值,再写入 Redis 服务器。</li>
@@ -372,12 +372,12 @@ function hide_canvas() {
<p>为了克服客户端分片业务逻辑与数据存储逻辑耦合的不足,可以通过 Proxy 将业务逻辑和存储逻辑隔离。客户端发送请求到一个代理,代理解析客户端的数据,将请求转发至正确的节点,然后将结果回复给客户端。这种架构还有一个优点就是可以把 Proxy 当成一个中间件,在这个中间件上可以做很多事情,比如可以把集群和主从的兼容性做到几乎一致,可以做无缝扩减容、安全策略等。</p>
<p>基于代理的分片已经有很多成熟的方案,如开源的 Codis阿里云的 ApsaraDB for Redis/ApsaraCache腾讯的 CRS 等。很多大企业也在采用 Proxy+Redis-Server 的架构。</p>
<p>基本原理如下图所示:</p>
<p><img src="assets/62990220-94e0-11e8-bcaa-73ee4805a006" alt="enter image description here" /></p>
<p><img src="assets/62990220-94e0-11e8-bcaa-73ee4805a006" alt="png" /></p>
<p>我们来了解下代理分片的缺点。没有完美的架构,由于使用了 Proxy带宽和 CPU 基本都要加倍,对资源的消耗会大很多。</p>
<h4>7.2 Codis 架构</h4>
<p>Codis 是一个分布式 Redis 解决方案,对于上层的应用来说,连接到 Codis Proxy 和连接原生的 Redis Server 没有明显的区别 参考6.1中的代理分片模式),客户端可以像使用单机 Redis 一样使用。</p>
<p>架构图如下:</p>
<p><img src="assets/f337f6f0-959f-11e8-b88f-171bb4051dab" alt="enter image description here" /></p>
<p><img src="assets/f337f6f0-959f-11e8-b88f-171bb4051dab" alt="png" /></p>
<h5><strong>Codis 简介</strong></h5>
<p>从 Codis 的官方架构图可以看出Codis 主要由四部分组成:</p>
<ul>
@@ -399,7 +399,7 @@ function hide_canvas() {
</ul>
<h4>7.3 类 Codis 架构Proxy + Redis-Server</h4>
<p>在上面曾提到,实现 Redis 分布式的基础是分片。目前,主流的分片方案有三种,即 Redis Cluster、客户端分片、代理分片。除了官方推出的 Redis Cluster大多数 IT 公司采用的都是基于代理的分片模式Proxy + Redis-Server这与 Codis 的原理类似,因此也称为“类 Codis”架构其架构图如下</p>
<p><img src="assets/0e83e460-95a4-11e8-b88f-171bb4051dab" alt="enter image description here" /></p>
<p><img src="assets/0e83e460-95a4-11e8-b88f-171bb4051dab" alt="png" /></p>
<p>该架构有以下特点:</p>
<ul>
<li>分片算法:基于代理的分片原理,将物理节点映射到 SlotCodis Slot 数为1024其它方案一般为16384对 Key-Value 进行读写操作时,采用一致性 Hash 算法或其它算法(如 Redis Cluster采用的 CRC16计算 Key 对应的 Slot 编号,根据 Slot 编号转发到对应的物理节点;</li>

View File

@@ -138,11 +138,11 @@ function hide_canvas() {
<p>本节将介绍基于 Redis 和 Lettuce 搭建一个分布式缓存集群的方法。为了生动地呈现集群创建过程,我没有采用 Redis 集群管理工具 redis-trib而是基于 Lettuce 编写 Java 代码实现集群的创建,相信,这将有利于读者更加深刻地理解 Redis 集群模式。</p>
<h4>1.1 方案简述</h4>
<p>Redis 集群模式至少需要三个主节点作为举例本文搭建一个3主3备的精简集群麻雀虽小五脏俱全。主备关系如下图所示其中 M 代表 Master 节点S 代表 Slave 节点A-M 和 A-S 为一对主备节点。</p>
<p><img src="assets/a43e6950-966f-11e8-9f67-05ec09da262a" alt="enter image description here" /></p>
<p><img src="assets/a43e6950-966f-11e8-9f67-05ec09da262a" alt="png" /></p>
<p>按照上图所示的拓扑结构,如果节点 1 故障下线,那么节点 2 上的 A-S 将升主为 A-MRedis 3 节点集群仍可用,如下图所示:</p>
<p><img src="assets/b04af9c0-966f-11e8-bee3-b1dbef72ca56" alt="enter image description here" /></p>
<p><img src="assets/b04af9c0-966f-11e8-bee3-b1dbef72ca56" alt="png" /></p>
<p>特别说明事实上Redis 集群节点间是两两互通的,如下图所示,上面作为示意图,进行了适当简化。</p>
<p><img src="assets/c06bb2e0-966f-11e8-9c35-b59aad3fef8b" alt="enter image description here" /></p>
<p><img src="assets/c06bb2e0-966f-11e8-9c35-b59aad3fef8b" alt="png" /></p>
<h4>1.2 资源准备</h4>
<p>首先,下载 Redis 包。前往 Redis 官网下载 Redis 资源包,本文采用的 Redis 版本为 4.0.8。</p>
<p>接着,将下载的 Redis 资源包 <code>redis-4.0.8.tar.gz</code> 放到自定义目录下,解压,编译便可生成 Redis 服务端和本地客户端 bin 文件 <code>redis-server</code><code>redis-cli</code>,具体操作命令如下:</p>
@@ -157,21 +157,21 @@ make
<p>根据端口号分别创建名为 6379、6380、6381、6382、6383、6384 的文件夹。</p>
<h5><strong>2修改配置文件</strong></h5>
<p>在解压文件夹 <code>redis-4.0.8</code> 中有一个 Redis 配置文件 <code>redis.conf</code>,其中一些默认的配置项需要修改(配置项较多,本文仅为举例,修改一些必要的配置)。以下仅以 6379 端口为例进行配置6380、6381等端口配置操作类似。将修改后的配置文件分别放入 6379~6384 文件夹中。</p>
<p><img src="assets/3bd42b30-cd23-11e8-8e49-d7b1f250ebf6" alt="enter image description here" /></p>
<p><img src="assets/3bd42b30-cd23-11e8-8e49-d7b1f250ebf6" alt="png" /></p>
<h5><strong>3创建必要启停脚本</strong></h5>
<p>逐一手动拉起 Redis 进程较为麻烦,在此,我们可以编写简单的启停脚本完成 <code>redis-server</code> 进程的启停(<code>start.sh</code><code>stop.sh</code>)。</p>
<p><img src="assets/120e9810-9670-11e8-bee3-b1dbef72ca56" alt="enter image description here" /></p>
<p><img src="assets/120e9810-9670-11e8-bee3-b1dbef72ca56" alt="png" /></p>
<h5><strong>4简单测试</strong></h5>
<p>至此,我们已经完成 Redis 集群创建的前期准备工作,在创建集群之前,我们可以简单测试一下,<code>redis-sever</code> 进程是否可以正常拉起。运行 <code>start.sh</code> 脚本,查看 <code>redis-server</code> 进程如下:</p>
<p><img src="assets/1b0eabd0-9670-11e8-9c35-b59aad3fef8b" alt="enter image description here" /></p>
<p><img src="assets/1b0eabd0-9670-11e8-9c35-b59aad3fef8b" alt="png" /></p>
<p>登录其中一个 Redis 实例的客户端(以 6379 为例),查看集群状态:很明显,以节点 6379 的视角来看,集群处于 Fail 状态,<code>clusterknownnodes:1</code> 表示集群中只有一个节点。</p>
<p><img src="assets/23e45430-9670-11e8-9c35-b59aad3fef8b" alt="enter image description here" /></p>
<p><img src="assets/23e45430-9670-11e8-9c35-b59aad3fef8b" alt="png" /></p>
<h3>2. 基于 Lettuce 创建 Redis 集群</h3>
<blockquote>
<p>关于创建 Redis 集群,官方提供了一个 Ruby 编写的运维软件 <code>redis-trib.rb</code>,使用简单的命令便可以完成创建集群、添加节点、负载均衡等操作。正因为简单,用户很难通过黑盒表现理解其中细节,鉴于此,本节将基于 Lettuce 编写创建 Redis 集群的代码,让读者对 Redis 集群创建有一个更深入的理解。</p>
</blockquote>
<p>Redis 发展至今,其对应的开源客户端几乎涵盖所有语言,详情请见官网,本节采用 Java 语言开发的 Lettuce 作为 Redis 客户端。Lettuce 是一个可伸缩线程安全的 Redis 客户端,多个线程可以共享同一个 RedisConnection。它采用优秀 Netty NIO 框架来高效地管理多个连接。关于 Lettuce 的详情,后面章节中会详细介绍。</p>
<p><img src="assets/49ae3fa0-9670-11e8-bee3-b1dbef72ca56" alt="enter image description here" /></p>
<p><img src="assets/49ae3fa0-9670-11e8-bee3-b1dbef72ca56" alt="png" /></p>
<h4>2.1 Redis 集群创建的步骤</h4>
<h5><strong>1相互感知初步形成集群。</strong></h5>
<p>在上文中,我们已经成功拉起了 6 个 <code>redis-server</code> 进程,每个进程视为一个节点,这些节点仍处于孤立状态,它们相互之间无法感知对方的存在,既然要创建集群,首先需要让这些孤立的节点相互感知,形成一个集群。</p>
@@ -393,7 +393,7 @@ class ClusterNode
}
</code></pre>
<p>运行上述代码创建集群,再次登录其中一个节点的客户端(以 6379 为例),通过命令:<code>cluster nodes</code><code>cluster info</code> 查看集群状态信息如下,集群已经处于可用状态。</p>
<p><img src="assets/614c8b30-9670-11e8-bd60-15398afc36e1" alt="enter image description here" /></p>
<p><img src="assets/614c8b30-9670-11e8-bd60-15398afc36e1" alt="png" /></p>
<h4>2.3 测试验证</h4>
<p>经过上述步骤,一个可用的 Redis 集群已经创建完毕,接下来,通过一段代码测试验证:</p>
<pre><code>public static void main(String[] args)
@@ -435,12 +435,12 @@ class ClusterNode
<h3>3. Redis SSL 双向认证通信实现</h3>
<h4>3.1 Redis 自带的鉴权访问模式</h4>
<p>默认情况下Redis 服务端是不允许远程访问的,打开其配置文件 <code>redis.conf</code>,可以看到如下配置:</p>
<p><img src="assets/9b307d70-9670-11e8-bd60-15398afc36e1" alt="enter image description here" /></p>
<p><img src="assets/9b307d70-9670-11e8-bd60-15398afc36e1" alt="png" /></p>
<p>根据说明,如果我们要远程访问,可以手动改变 <code>protected-mode</code> 配置,将 yes 状态置为 no 即可,也可在本地客服端 <code>redis-cli</code>,键入命令:<code>config set protected-mode no</code>。但是,这明显不是一个好的方法,去除保护机制,意味着严重安全风险。</p>
<p>鉴于此,我们可以采用鉴权机制,通过秘钥来鉴权访问,修改 <code>redis.conf</code>,添加 <code>requirepass mypassword</code> ,或者键入命令:<code>config set requirepass password</code> 设置鉴权密码。</p>
<p><img src="assets/a9e1a100-9670-11e8-bee3-b1dbef72ca56" alt="enter image description here" /></p>
<p><img src="assets/a9e1a100-9670-11e8-bee3-b1dbef72ca56" alt="png" /></p>
<p>设置密码后Lettuce 客户端访问 <code>redis-server</code> 就需要鉴权,增加一行代码即可,以单机模式为例:</p>
<p><img src="assets/bb32eae0-9670-11e8-9f67-05ec09da262a" alt="enter image description here" /></p>
<p><img src="assets/bb32eae0-9670-11e8-9f67-05ec09da262a" alt="png" /></p>
<h5><strong>补充</strong></h5>
<p>除了通过密码鉴权访问出于安全的考量Redis 还提供了一些其它的策略:</p>
<ul>
@@ -463,7 +463,7 @@ rename-command EVAL &quot;user-defined&quot;
<p>通过上面的介绍,相信读者已经对 Redis 自带的加固策略有了一定了解。客观地讲Redis 自带的安全策略很难满足对安全性要求普遍较高的商用场景,鉴于此,有必要优化。就 <code>Client-Server</code> 模式而言成熟的安全策略有很多本文仅介绍其一SSL 双向认证通信。关于 SSL 双向认证通信的原理和具体实现方式,网上有大量的博文可供参考,并非本文重点,因此不做详细介绍。</p>
<h5><strong>总体流程</strong></h5>
<p>我们首先看下 SSL 双向认证通信的总体流程,如下图所示:</p>
<p><img src="assets/f1d17620-9670-11e8-bd60-15398afc36e1" alt="enter image description here" /></p>
<p><img src="assets/f1d17620-9670-11e8-bd60-15398afc36e1" alt="png" /></p>
<p>首先Client 需要将 Server 的根证书 <code>ca.crt</code> 安装到自己的信任证书库中同时Server 也需要将根证书 <code>ca.crt</code> 安装到自己的信任证书库中。</p>
<p>接着,当 SSL 握手时Server 先将服务器证书 <code>server.p12</code> 发给 ClientClient 收到后,到自己的信任证书库中进行验证,由于 <code>server.p12</code> 是根证书 CA 颁发的,所以验证必然通过。</p>
<p>然后Client 将客户端证书 <code>client.p12</code> 发给 Server同理 <code>client.p12</code> 是根证书 CA 颁发的,所以验证也将通过。</p>
@@ -476,17 +476,17 @@ rename-command EVAL &quot;user-defined&quot;
<p>Redis 本身不支持 SSL 双向认证通信,因此,需要修改源码,且涉及修改较多,本文仅列出要点,具体实现层面代码不列。</p>
<p><strong>config.c</strong></p>
<p>SSL 双向认证通信涉及的 keyStore 和 trustStore 密码密文、路径等信息可由 Redis 的配置文件 <code>redis.conf</code> 提供如此我们需要修改加载配置文件的源码config.c-&gt;loadServerConfigFromString(char *config)),部分修改如下:</p>
<p><img src="assets/03a25040-9671-11e8-9c35-b59aad3fef8b" alt="enter image description here" /></p>
<p><img src="assets/03a25040-9671-11e8-9c35-b59aad3fef8b" alt="png" /></p>
<p><strong>redis.h</strong></p>
<p>Redis 的客户端redisClient和服务端redisServer都需要适配部分代码如下</p>
<p><img src="assets/0bb767c0-9671-11e8-9f67-05ec09da262a" alt="enter image description here" /></p>
<p><img src="assets/19777490-9671-11e8-bd60-15398afc36e1" alt="enter image description here" /></p>
<p><img src="assets/0bb767c0-9671-11e8-9f67-05ec09da262a" alt="png" /></p>
<p><img src="assets/19777490-9671-11e8-bd60-15398afc36e1" alt="png" /></p>
<p><strong>hiredis.h</strong></p>
<p>修改创建连接的原函数:</p>
<p><img src="assets/28862170-9671-11e8-9f67-05ec09da262a" alt="enter image description here" /></p>
<p><img src="assets/28862170-9671-11e8-9f67-05ec09da262a" alt="png" /></p>
<p><strong>anet.h</strong></p>
<p>定义 SSL 通信涉及的一些函数(实现在 anet.c 中):</p>
<p><img src="assets/2e74d680-9671-11e8-9c35-b59aad3fef8b" alt="enter image description here" /></p>
<p><img src="assets/2e74d680-9671-11e8-9c35-b59aad3fef8b" alt="png" /></p>
<ul>
<li>客户端</li>
</ul>
@@ -524,10 +524,10 @@ cmd.get(&quot;key&quot;);
</code></pre>
<h3>4. Redis 集群可靠性问题</h3>
<p>为了便于理解(同时也为了规避安全违规风险),我将原方案进行了适度简化,以 3 主 3 备 Redis 集群为例阐述方案(<code>redis-cluster</code> 模式最少需要三个主节点),如下图所示,其中 A-M 表示主节点 AA-S 表示主节点 A 对应的从节点,以此类推。</p>
<p><img src="assets/5e5c0580-9671-11e8-bee3-b1dbef72ca56" alt="enter image description here" /></p>
<p><img src="assets/5e5c0580-9671-11e8-bee3-b1dbef72ca56" alt="png" /></p>
<h4>4.1 可靠性问题一</h4>
<p>Redis 集群并不是将 <code>redis-server</code> 进程启动便可自行建立的。在各个节点启动 <code>redis-server</code> 进程后,形成的只是 6 个“孤立”的 Redis 节点而已,它们相互不知道对方的存在,拓扑结构如下:</p>
<p><img src="assets/6aa306e0-9671-11e8-bd60-15398afc36e1" alt="enter image description here" /></p>
<p><img src="assets/6aa306e0-9671-11e8-bd60-15398afc36e1" alt="png" /></p>
<p>查看每个 Redis 节点的集群配置文件 <code>cluster-config-file</code>,你将看到类似以下内容:</p>
<pre><code>2eca4324c9ee6ac49734e2c1b1f0ce9e74159796 192.168.1.3:6379 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0
@@ -541,21 +541,21 @@ vars currentEpoch 0 lastVoteEpoch 0
<p>使用 <code>redis-trib.rb</code> 建立集群虽然便捷,不过,由于 Ruby 语言本身的一系列安全缺陷,有些时候并不是明智的选择。考虑到 Lettuce 提供了极为丰富的 Redis 高级功能,我们完全可以使用 Lettuce 来创建集群,这一点在上一节已经介绍过。</p>
<h4>4.2 节点故障</h4>
<p>三个物理节点,分别部署两个 <code>redis-server</code>,且交叉互为主备,这样做可以提高可靠性:如节点 1 宕机,主节点 A-M 对应的从节点 A-S 将发起投票,作为唯一的备节点,其必然升主成功,与 B-M、C-M 构成新的集群,继续提供服务,如下图所示:</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>
<h4>4.3 故障节点恢复</h4>
<p>接续上一节,如果宕机的节点 1 经过修复重新上线,根据 Redis 集群原理,节点 1 上的 A-M 将意识到自己已经被替代,将降级为备,形成的集群拓扑结构如下:</p>
<p><img src="assets/81b34340-9671-11e8-9f67-05ec09da262a" alt="enter image description here" /></p>
<p><img src="assets/81b34340-9671-11e8-9f67-05ec09da262a" alt="png" /></p>
<h4>4.4 可靠性问题二</h4>
<p>基于上述拓扑结构,如果节点 3 宕机Redis 集群将只有一个主节点 C-M 存活,存活的主节点总数少于集群主节点总数的一半 <code>1&lt;3/2+1</code>),集群无法自愈,不能继续提供服务。</p>
<p>为了解决这个问题,我们可以设计一个常驻守护进程对 Redis 集群的状态进行监控,当出现主-备状态不合理的情况(如节点 1 重新上线后的拓扑结构守护进程主动发起主备倒换clusterFailover将节点 1 上的 A-S 升为主,节点 3 上的 A-M 降为备,如此,集群拓扑结构恢复正常,并且能够支持单节点故障。</p>
<p><img src="assets/8e851f80-9671-11e8-bee3-b1dbef72ca56" alt="enter image description here" /></p>
<p><img src="assets/8e851f80-9671-11e8-bee3-b1dbef72ca56" alt="png" /></p>
<p><strong>注:</strong> Lettuce 提供了主备倒换的方法,示例代码如下:</p>
<pre><code>// slaveConn为Lettuce与从节点建立的连接
slaveConn.sync().clusterFailover(true)
</code></pre>
<h4>4.5 可靠性问题三</h4>
<p>接续 4.1 节,如果节点 1 故障后无法修复,为了保障可靠性,通常会用一个新的节点来替换掉故障的节点——所谓故障替换。拓扑结构如下:</p>
<p><img src="assets/962f4030-9671-11e8-9c35-b59aad3fef8b" alt="enter image description here" /></p>
<p><img src="assets/962f4030-9671-11e8-9c35-b59aad3fef8b" alt="png" /></p>
<p>新的节点上面部署两个 <code>redis-server</code> 进程,由于是新建节点,<code>redis-server</code> 进程对应的集群配置文件 <code>cluster-config-file</code> 中只包含自身的信息,并没有整个集群的信息,简言之,新建的节点上的两个 <code>redis-server</code> 进程是“孤立”的。</p>
<p>为了重新组成集群,我们需要两个步骤:</p>
<ol>
@@ -563,7 +563,7 @@ slaveConn.sync().clusterFailover(true)
<li>为新加入集群的两个 <code>redis-server</code> 设置主节点:节点 3 上的两个主 A-M 和 B-M 都没有对应的从节点,因此,可将新加入的两个 <code>redis-server</code> 分别设置为它们的从节点。</li>
</ol>
<p>完成上述两个步骤后Redis 集群的拓扑结构将演变成如下形态:</p>
<p><img src="assets/a0b9eb90-9671-11e8-bee3-b1dbef72ca56" alt="enter image description here" /></p>
<p><img src="assets/a0b9eb90-9671-11e8-bee3-b1dbef72ca56" alt="png" /></p>
<p>很明显,变成了问题一的形态,继续通过问题一的解决方案便可修复。</p>
<h4>4.6 其它</h4>
<p>上面仅介绍了几个较为常见的问题,在实际使用 Redis 的过程中可能遇到的问题远不止这些。在第 05 课中,我将介绍一些更为复杂的异常场景。</p>
@@ -571,19 +571,19 @@ slaveConn.sync().clusterFailover(true)
<p>不同的应用场景,关注的问题、可能出现的异常不尽相同,上文中介绍的问题仅仅是一种商业应用场景中遇到的。为了解决上述问题,可基于 Lettuce 设计一个常驻守护进程,实现集群创建、添加节点、平衡主备节点分布、集群运行状态监测、故障自检及故障自愈等功能。</p>
<h4>5.1 总体流程图</h4>
<p>下面是精简后的流程图:</p>
<p><img src="assets/a93a20f0-9671-11e8-bd60-15398afc36e1" alt="enter image description here" /></p>
<p><img src="assets/a93a20f0-9671-11e8-bd60-15398afc36e1" alt="png" /></p>
<p>流程图中ETCD 选主部分需要特别说明一下ETCD 和 ZooKeeper 类似,可提供 Leader 选举功能。Redis 集群模式下,在各个 Redis 进程所在主机上均启动一个常驻守护进程,以提高可靠性,但是,为了避免冲突,只有被 ETCD 选举为 Leader 的节点上的常驻守护进程可以执行 “守护” 流程,其它主机上的守护进程呈 “休眠” 状态。关于 Leader 选举的实现,方式很多,本文仅以 ETCD 为例。</p>
<h4>5.2 实现</h4>
<p><strong>集群状态检测</strong></p>
<p>读者应该知道Redis 集群中每个节点都保存有集群所有节点的状态信息,虽然这些信息可能并不准确。通过状态信息,我们可以判断集群是否存在以及集群的运行状态,基于 Lettuce 提供的方法,简要代码如下:</p>
<p><img src="assets/cab6ab40-9671-11e8-bee3-b1dbef72ca56" alt="enter image description here" /></p>
<p><img src="assets/cab6ab40-9671-11e8-bee3-b1dbef72ca56" alt="png" /></p>
<p>上面代码只从一个节点的视角进行了检查,完整的代码将遍历所有节点,从所有节点的视角分别检查。</p>
<p><strong>Redis 集群创建</strong></p>
<p>大家可参考第二节“2. 基于 Lettuce 创建 Redis 集群”中的内容。</p>
<p><strong>替换故障节点</strong></p>
<p>1加入新节点</p>
<p>替换上来的新节点本质上是“孤立”的,需要先加入现有集群:通过集群命令 RedisAdvancedClusterCommands 对象调用 <code>clusterMeet()</code> 方法,便可实现:</p>
<p><img src="assets/5ac7ecd0-9672-11e8-bd60-15398afc36e1" alt="enter image description here" /></p>
<p><img src="assets/5ac7ecd0-9672-11e8-bd60-15398afc36e1" alt="png" /></p>
<p>2为新节点设置主备关系</p>
<p>首先需要明确,当前集群中哪些 Master 没有 Slave然后新节点通过 <code>clusterReplicate()</code> 方法成为对应 Master 的 Slave</p>
<pre><code>slaveConn.sync().clusterReplicate(masterNode);
@@ -591,7 +591,7 @@ slaveConn.sync().clusterFailover(true)
<p><strong>平衡主备节点的分布</strong></p>
<p>1状态检测</p>
<p>常驻守护进程通过遍历各个节点获取到的集群状态信息,可以确定某些 Host 上 Master 和 Slave 节点数量不平衡,比如,经过多次故障后,某个 Host 上的 Redis 节点角色全部变成了 Master不仅影响性能还会危及可靠性。这个环节的关键点是如何区分 Master 和 Slave通常我们以是否被指派 Slot 为依据:</p>
<p><img src="assets/736e0b70-9672-11e8-bee3-b1dbef72ca56" alt="enter image description here" /></p>
<p><img src="assets/736e0b70-9672-11e8-bee3-b1dbef72ca56" alt="png" /></p>
<p>2平衡</p>
<p>如何平衡呢,在创建 Redis 集群的时候,开发者需要制定一个合理的集群拓扑结构(或者算法)来指导集群的创建,如本文介绍的 3 主 3 备模式。那么,在平衡的时候,同样可以依据制定的拓扑结构进行恢复。具体操作很简单:调用 Lettuce 提供的 <code>clusterFailover()</code> 方法即可。</p>
<h4>参考文献与致谢:</h4>

View File

@@ -455,9 +455,9 @@ if (!del &amp;&amp; server.cluster-&gt;slots[slot])
</ol>
<p>故障点找到了,那么,既然存在故障,为何集群状态显示正常,只有部分读写操作失败呢?有必要解释一下,为了便于阐明问题,我以 3 主 3 备集群为例。</p>
<p>在前面的章节中,已经介绍了 Redis 集群混合路由查询的原理,在此,直接引用原理示意图,客户端与主节点 A 直连进行读写操作时Key 对应的 Slot 可能并不在当前直连的节点上,经过“重定向”才能转发到正确的节点,如下图所示:</p>
<p><img src="assets/a9a50eb0-3fb4-11e8-9e37-a924cb695a5d" alt="enter image description here" /></p>
<p><img src="assets/a9a50eb0-3fb4-11e8-9e37-a924cb695a5d" alt="png" /></p>
<p>如果 A、C 节点之间通信被阻断,上述混合路由查询自然就不能成功了,如下图所示:</p>
<p><img src="assets/9073dc30-3fb6-11e8-9e37-a924cb695a5d" alt="enter image description here" /></p>
<p><img src="assets/9073dc30-3fb6-11e8-9e37-a924cb695a5d" alt="png" /></p>
<p>如上图所示,节点 1 与节点 3 互相不可访问,这种情况下,节点 1 和节点 3 相互认为对方下线,因此会将对方标记为 PFAIL 状态,但由于持有这一观点(认为节点 1、3 下线)的主节点数量少于主节点总数的一半,不会发起故障倒换,集群状态正常。</p>
<p>虽然集群显示状态正常,但存在潜在问题,比如节点 1 上的客户端进行读写操作的 Key 位于节点 3 主节点的 Slot 中,这时进行读写操作,由于互不可达,必然失败。读写操作的目标节点是由 Key 决定的CRC16 算法计算出 Key 对应的 Slot 编号,根据 Slot 编号确定目标节点。同时,不同的 Key 对应的 Slot 不尽相同,从节点 1 的视角来看,那些匹配节点 2 所属 Slot 位的 Key读写操作都可以正常进行而匹配节点 3 所属 Slot 位的 Key 则会报错,这样就解释了为何只有部分读写操作失败。</p>
<h4>5.3 解决方案</h4>
@@ -480,7 +480,7 @@ if (!del &amp;&amp; server.cluster-&gt;slots[slot])
<li>通过 <code>telnet ip port</code> 命令检测节点间通信情况,发现其中一个主节点与备节点无法联通,进一步定位为交换机故障。</li>
</ol>
<p>上述故障场景示意图如下:</p>
<p><img src="assets/92a19d60-3fd1-11e8-9e37-a924cb695a5d" alt="enter image description here" /></p>
<p><img src="assets/92a19d60-3fd1-11e8-9e37-a924cb695a5d" alt="png" /></p>
<p>故障主节点 A-M 的备节点 A-S 升主需要获得超过半数的主节点投票故障场景下存活的两个主节点中C-M 与备节点 A-S 内部通信被阻断,导致备节点 A-S 只能获得 1 张票没有超过集群规模的半数3 节点集群,至少需要 2 张票),从而无法升主,进而导致故障主节点故障倒换失败,集群无法恢复。</p>
<h4>6.3 解决方案及改进措施</h4>
<p>本节所述故障场景,基于 3 主 3 备的架构Redis 集群不具备自愈的硬性条件,没有解决方案。不过,如果扩大集群的规模,比如 5 主 5 备,出现同样故障则是可以自愈的。</p>

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>

View File

@@ -153,7 +153,7 @@ function hide_canvas() {
</ol>
<h4>1.2 基于 Redis 实现分布式锁(以 Redis 单机模式为例)</h4>
<p>基于 Redis 实现锁服务的思路比较简单。我们把锁数据存储在分布式环境中的一个节点所有需要获取锁的调用方客户端都需访问该节点如果锁数据Key-Value 键值对已经存在则说明已经有其它客户端持有该锁可等待其释放Key-Value 被主动删除或者因过期而被动删除再尝试获取锁如果锁数据不存在则写入锁数据Key-Value其中 Value 需要保证在足够长的一段时间内在所有客户端的所有获取锁的请求中都是唯一的,以便释放锁的时候进行校验;锁服务使用完毕之后,需要主动释放锁,即删除存储在 Redis 中的 Key-Value 键值对。其架构如下:</p>
<p><img src="assets/0aa048d0-8130-11e8-a935-d59fe50595b6" alt="enter image description here" /></p>
<p><img src="assets/0aa048d0-8130-11e8-a935-d59fe50595b6" alt="png" /></p>
<h4>1.3 加解锁流程</h4>
<p>根据 Redis 官方的文档,获取锁的操作流程如下。</p>
<p>**步骤1向 Redis 节点发送命令,请求锁。**代码如下:</p>

View File

@@ -159,28 +159,28 @@ function hide_canvas() {
<p>根据 Raft 协议,一个应用 Raft 协议的集群在刚启动时,所有节点的状态都是 Follower。由于没有 LeaderFollowers 无法与 Leader 保持心跳Heart Beat因此Followers 会认为 Leader 已经下线,进而转为 Candidate 状态。然后Candidate 将向集群中其它节点请求投票,同意自己升级为 Leader。如果 Candidate 收到超过半数节点的投票N/2 + 1它将获胜成为 Leader。</p>
<p><strong>第一阶段:所有节点都是 Follower。</strong></p>
<p>上面提到,一个应用 Raft 协议的集群在刚启动(或 Leader 宕机)时,所有节点的状态都是 Follower初始 Term任期为 0。同时启动选举定时器每个节点的选举定时器超时时间都在 100~500 毫秒之间且并不一致(避免同时发起选举)。</p>
<p><img src="assets/0e6c7fa0-b831-11e8-9da0-3bed0a166513" alt="enter image description here" /></p>
<p><img src="assets/0e6c7fa0-b831-11e8-9da0-3bed0a166513" alt="png" /></p>
<p><strong>第二阶段Follower 转为 Candidate 并发起投票。</strong></p>
<p>没有 LeaderFollowers 无法与 Leader 保持心跳Heart Beat节点启动后在一个选举定时器周期内未收到心跳和投票请求则状态转为候选者 Candidate 状态,且 Term 自增,并向集群中所有节点发送投票请求并且重置选举定时器。</p>
<p>注意,由于每个节点的选举定时器超时时间都在 100-500 毫秒之间,且彼此不一样,以避免所有 Follower 同时转为 Candidate 并同时发起投票请求。换言之,最先转为 Candidate 并发起投票请求的节点将具有成为 Leader 的“先发优势”。</p>
<p><img src="assets/192d60c0-b832-11e8-9da0-3bed0a166513" alt="enter image description here" /></p>
<p><img src="assets/192d60c0-b832-11e8-9da0-3bed0a166513" alt="png" /></p>
<p><strong>第三阶段:投票策略。</strong></p>
<p>节点收到投票请求后会根据以下情况决定是否接受投票请求:</p>
<ol>
<li>请求节点的 Term 大于自己的 Term且自己尚未投票给其它节点则接受请求把票投给它</li>
<li>请求节点的 Term 小于自己的 Term且自己尚未投票则拒绝请求将票投给自己。</li>
</ol>
<p><img src="assets/cf56fbe0-b832-11e8-9da0-3bed0a166513" alt="enter image description here" /></p>
<p><img src="assets/cf56fbe0-b832-11e8-9da0-3bed0a166513" alt="png" /></p>
<p><strong>第四阶段Candidate 转为 Leader。</strong></p>
<p>一轮选举过后,正常情况下,会有一个 Candidate 收到超过半数节点N/2 + 1的投票它将胜出并升级为 Leader。然后定时发送心跳给其它的节点其它节点会转为 Follower 并与 Leader 保持同步,到此,本轮选举结束。</p>
<p>注意:有可能一轮选举中,没有 Candidate 收到超过半数节点投票,那么将进行下一轮选举。</p>
<p><img src="assets/206d5880-b833-11e8-9469-d363e3097731" alt="enter image description here" /></p>
<p><img src="assets/206d5880-b833-11e8-9469-d363e3097731" alt="png" /></p>
<h3>3. Raft 算法之 Log Replication 原理</h3>
<p>在一个 Raft 集群中,只有 Leader 节点能够处理客户端的请求(如果客户端的请求发到了 FollowerFollower 将会把请求重定向到 Leader客户端的每一个请求都包含一条被复制状态机执行的指令。Leader 把这条指令作为一条新的日志条目Entry附加到日志中去然后并行得将附加条目发送给 Followers让它们复制这条日志条目。</p>
<p>当这条日志条目被 Followers 安全复制Leader 会将这条日志条目应用到它的状态机中,然后把执行的结果返回给客户端。如果 Follower 崩溃或者运行缓慢再或者网络丢包Leader 会不断得重复尝试附加日志条目(尽管已经回复了客户端)直到所有的 Follower 都最终存储了所有的日志条目,确保强一致性。</p>
<p><strong>第一阶段:客户端请求提交到 Leader。</strong></p>
<p>如下图所示Leader 收到客户端的请求,比如存储数据 5。Leader 在收到请求后会将它作为日志条目Entry写入本地日志中。需要注意的是此时该 Entry 的状态是未提交UncommittedLeader 并不会更新本地数据,因此它是不可读的。</p>
<p><img src="assets/46017c00-b835-11e8-9469-d363e3097731" alt="enter image description here" /></p>
<p><img src="assets/46017c00-b835-11e8-9469-d363e3097731" alt="png" /></p>
<p><strong>第二阶段Leader 将 Entry 发送到其它 Follower</strong></p>
<p>Leader 与 Floolwers 之间保持着心跳联系,随心跳 Leader 将追加的 EntryAppendEntries并行地发送给其它的 Follower并让它们复制这条日志条目这一过程称为复制Replicate</p>
<p>有几点需要注意:</p>
@@ -192,7 +192,7 @@ function hide_canvas() {
<p>在正常情况下Leader 和 Follower 的日志保持一致所以追加日志的一致性检查从来不会失败。然而Leader 和 Follower 一系列崩溃的情况会使它们的日志处于不一致状态。Follower可能会丢失一些在新的 Leader 中有的日志条目,它也可能拥有一些 Leader 没有的日志条目,或者两者都发生。丢失或者多出日志条目可能会持续多个任期。</p>
<p>要使 Follower 的日志与 Leader 恢复一致Leader 必须找到最后两者达成一致的地方(说白了就是回溯,找到两者最近的一致点),然后删除从那个点之后的所有日志条目,发送自己的日志给 Follower。所有的这些操作都在进行附加日志的一致性检查时完成。</p>
<p>Leader 为每一个 Follower 维护一个 nextIndex它表示下一个需要发送给 Follower 的日志条目的索引地址。当一个 Leader 刚获得权力的时候,它初始化所有的 nextIndex 值,为自己的最后一条日志的 index 加 1。如果一个 Follower 的日志和 Leader 不一致,那么在下一次附加日志时一致性检查就会失败。在被 Follower 拒绝之后Leader 就会减小该 Follower 对应的 nextIndex 值并进行重试。最终 nextIndex 会在某个位置使得 Leader 和 Follower 的日志达成一致。当这种情况发生,附加日志就会成功,这时就会把 Follower 冲突的日志条目全部删除并且加上 Leader 的日志。一旦附加日志成功,那么 Follower 的日志就会和 Leader 保持一致,并且在接下来的任期继续保持一致。</p>
<p><img src="assets/87e3fb10-b836-11e8-a627-cbcd94258d1d" alt="enter image description here" /></p>
<p><img src="assets/87e3fb10-b836-11e8-a627-cbcd94258d1d" alt="png" /></p>
<p><strong>第三阶段Leader 等待 Followers 回应。</strong></p>
<p>Followers 接收到 Leader 发来的复制请求后,有两种可能的回应:</p>
<ol>
@@ -200,14 +200,14 @@ function hide_canvas() {
<li>一致性检查失败,拒绝写入,返回 False原因和解决办法上面已做了详细说明。</li>
</ol>
<p>需要注意的是,此时该 Entry 的状态也是未提交Uncommitted。完成上述步骤后Followers 会向 Leader 发出 Success 的回应,当 Leader 收到大多数 Followers 的回应后,会将第一阶段写入的 Entry 标记为提交状态Committed并把这条日志条目应用到它的状态机中。</p>
<p><img src="assets/1802d130-b837-11e8-9d9d-aba50ff29480" alt="enter image description here" /></p>
<p><img src="assets/1802d130-b837-11e8-9d9d-aba50ff29480" alt="png" /></p>
<p><strong>第四阶段Leader 回应客户端。</strong></p>
<p>完成前三个阶段后Leader会向客户端回应 OK表示写操作成功。</p>
<p><img src="assets/6ea4b440-b837-11e8-9469-d363e3097731" alt="enter image description here" /></p>
<p><img src="assets/6ea4b440-b837-11e8-9469-d363e3097731" alt="png" /></p>
<p><strong>第五阶段Leader 通知 Followers Entry 已提交</strong></p>
<p>Leader 回应客户端后,将随着下一个心跳通知 FollowersFollowers 收到通知后也会将 Entry 标记为提交状态。至此Raft 集群超过半数节点已经达到一致状态,可以确保强一致性。</p>
<p>需要注意的是,由于网络、性能、故障等各种原因导致“反应慢”、“不一致”等问题的节点,最终也会与 Leader 达成一致。</p>
<p><img src="assets/080b5580-b838-11e8-9469-d363e3097731" alt="enter image description here" /></p>
<p><img src="assets/080b5580-b838-11e8-9469-d363e3097731" alt="png" /></p>
<h3>4. Raft 算法之安全性</h3>
<p>前面描述了 Raft 算法是如何选举 Leader 和复制日志的。然而,到目前为止描述的机制并不能充分地保证每一个状态机会按照相同的顺序执行相同的指令。例如,一个 Follower 可能处于不可用状态,同时 Leader 已经提交了若干的日志条目;然后这个 Follower 恢复(尚未与 Leader 达成一致)而 Leader 故障;如果该 Follower 被选举为 Leader 并且覆盖这些日志条目,就会出现问题,即不同的状态机执行不同的指令序列。</p>
<p>鉴于此,在 Leader 选举的时候需增加一些限制来完善 Raft 算法。这些限制可保证任何的 Leader 对于给定的任期号Term都拥有之前任期的所有被提交的日志条目所谓 Leader 的完整特性)。关于这一选举时的限制,下文将详细说明。</p>
@@ -225,7 +225,7 @@ function hide_canvas() {
<p>在 Unix 系统中,<code>/etc</code> 目录用于存放系统管理和配置文件。分布式系统Distributed System第一个字母是“d”。两者看上去并没有直接联系但它们加在一起就有点意思了分布式的关键数据系统管理和配置文件存储系统这便是 Etcd 命名的灵感之源。</p>
<h4>5.1 Etcd 架构</h4>
<p>Etcd 的架构图如下从架构图中可以看出Etcd 主要分为四个部分HTTP Server、Store、Raft 以及 WAL。</p>
<p><img src="assets/4577c8b0-b82c-11e8-b3f0-6503db3b148e" alt="enter image description here" /></p>
<p><img src="assets/4577c8b0-b82c-11e8-b3f0-6503db3b148e" alt="png" /></p>
<ul>
<li>HTTP Server用于处理客户端发送的 API 请求以及其它 Etcd 节点的同步与心跳信息请求。</li>
<li>Store用于处理 Etcd 支持的各类功能的事务,包括数据索引、节点状态变更、监控与反馈、事件处理与执行等等,是 Etcd 对用户提供的大多数 API 功能的具体实现。</li>

View File

@@ -293,7 +293,7 @@ maintClient.alarmDisarm(alarmList.get(0));
<p>完成业务流程后,删除对应的 Key 释放锁。</p>
<h4>3.2 基于 Etcd 的分布式锁的原理图</h4>
<p>根据上一节中介绍的业务流程基于Etcd的分布式锁示意图如下。</p>
<p><img src="assets/d29c3de0-b9c9-11e8-bcd3-a9db59a0d5f6" alt="enter image description here" /></p>
<p><img src="assets/d29c3de0-b9c9-11e8-bcd3-a9db59a0d5f6" alt="png" /></p>
<p>业务流程图大家可参看这篇文章<a href="https://blog.csdn.net/koflance/article/details/78616206">《Zookeeper 分布式锁实现原理》</a></p>
<h4>3.3 基于 Etcd 实现分布式锁的客户端 Demo</h4>
<p>Demo 代码如下:</p>

View File

@@ -186,13 +186,13 @@ function hide_canvas() {
<li>ZooKeeperKafka 集群依赖 ZooKeeper需根据 Kafka 的版本选择安装对应的 ZooKeeper 版本。</li>
</ul>
<h4>1.4 Kafka 架构</h4>
<p><img src="assets/98af5490-cc99-11e8-b452-15eec1b99303" alt="enter image description here" /></p>
<p><img src="assets/98af5490-cc99-11e8-b452-15eec1b99303" alt="png" /></p>
<p>如上图所示,一个典型的 Kafka 体系架构包括若干 Producer消息生产者若干 BrokerKafka 支持水平扩展,一般 Broker 数量越多,集群吞吐率越高),若干 ConsumerGroup以及一个 Zookeeper 集群。Kafka 通过 Zookeeper 管理集群配置,选举 Leader以及在 Consumer Group 发生变化时进行 Rebalance。Producer 使用 Push模式将消息发布到 BrokerConsumer 使用 Pull模式从 Broker 订阅并消费消息。</p>
<p><strong>各个名词的解释请见下表:</strong></p>
<p><img src="assets/67e36670-c5ec-11e8-94bc-4f9499501dff" alt="enter image description here" /></p>
<p><img src="assets/67e36670-c5ec-11e8-94bc-4f9499501dff" alt="png" /></p>
<h4>1.5 Kafka 高可用方案</h4>
<p>Kafka 高可用性的保障来源于其健壮的副本Replication策略。为了提高吞吐能力Kafka 中每一个 Topic 分为若干 Partitions为了保证可用性每一个 Partition 又设置若干副本Replicas为了保障数据的一致性Zookeeper 机制得以引入。基于 ZookeeperKafka 为每一个 Partition 找一个节点作为 Leader其余备份作为 Follower只有 Leader 才能处理客户端请求,而 Follower 仅作为副本同步 Leader 的数据如下示意图TopicA 分为两个 Partition每个 Partition 配置两个副本。</p>
<p><img src="assets/f02c1c10-c91c-11e8-8892-5b0a3ea2c61d" alt="enter image description here" /></p>
<p><img src="assets/f02c1c10-c91c-11e8-8892-5b0a3ea2c61d" alt="png" /></p>
<p>基于上图的架构,当 Producer Push 的消息写入 Partition分区Leader 所在的 BrokerKafka 节点)会将消息写入自己的分区,同时还会将此消息复制到各个 Follower实现同步。如果某个 Follower 挂掉Leader 会再找一个替代并同步消息;如果 Leader 挂了,将会从 Follower 中选举出一个新的 Leader 替代,继续业务,这些都是由 ZooKeeper 完成的。</p>
<h4>1.6 Kafka 优缺点</h4>
<p><strong>优点主要包括以下几点:</strong></p>
@@ -259,7 +259,7 @@ function hide_canvas() {
</ul>
<h4>2.4 ActiveMQ 架构</h4>
<p>ActiveMQ 的主体架构如下图所示。</p>
<p><img src="assets/18fba290-c96e-11e8-97d9-49b68de7724a" alt="enter image description here" /></p>
<p><img src="assets/18fba290-c96e-11e8-97d9-49b68de7724a" alt="png" /></p>
<p><strong>传输协议:</strong> 消息之间的传递,无疑需要协议进行沟通,启动一个 ActiveMQ 便打开一个监听端口。ActiveMQ 提供了广泛的连接模式,主要包括 SSL、STOMP、XMPP。ActiveMQ 默认的使用协议为 OpenWire端口号为 61616。</p>
<p><strong>通信方式:</strong> ActiveMQ 有两种通信方式Point-to-Point Model点对点模式Publish/Subscribe Model (发布/订阅模式),其中在 Publich/Subscribe 模式下又有持久化订阅和非持久化订阅两种消息处理方式。</p>
<p><strong>消息存储:</strong> 在实际应用中,重要的消息通常需要持久化到数据库或文件系统中,确保服务器崩溃时,信息不会丢失。</p>
@@ -277,7 +277,7 @@ function hide_canvas() {
</ul>
<h4>2.5 ActiveMQ 高可用方案</h4>
<p>在生产环境中高可用High AvailabilityHA可谓 “刚需”, ActiveMQ 的高可用性架构基于 Master/Slave 模型。ActiveMQ 总共提供了四种配置方案来配置 HA其中 Shared Nothing Master/Slave 在 5.8 版本之后不再使用了,并在 ActiveMQ 5.9 版本中引入了基于 Zookeeper 的 Replicated LevelDB Store HA 方案。</p>
<p><img src="assets/36564650-c7b2-11e8-89fa-6f2232e1f02e" alt="enter image description here" /></p>
<p><img src="assets/36564650-c7b2-11e8-89fa-6f2232e1f02e" alt="png" /></p>
<p>关于几种 HA 方案的详细介绍,读者可查看<a href="http://activemq.apache.org/masterslave.html">官网说明</a>,在此,我仅做简单介绍。</p>
<p><strong>方案一Shared Nothing Master/Slave</strong></p>
<p>这是一种最简单最典型的 Master-Slave 模式Master 与 Slave 有各自的存储系统不共享任何数据。“Shared Nothing” 模式有很多局限性,存在丢失消息、“双主”等问题。目前,在要求严格的生产环境中几乎没有应用,是一种趋于淘汰的方案,因此,本文就不作介绍了。</p>
@@ -299,7 +299,7 @@ function hide_canvas() {
<p>特别说明ActiveMQ 官网警告LevelDB 不再作为推荐的存储方案,取而代之的是 KahaDB。</p>
<h4>2.6 ActiveMQ HA 方案之 Network Bridges 模式</h4>
<p>在 2.5 节中介绍的几种 HA 方案,本质上都只有一个 Master 节点无法满足高并发、大吞吐量的商用场景因此ActiveMQ 官方推出了 “网桥”架构模式,即真正的“分布式消息队列”。该模式可应对大规模 Clients、高密度的消息增量的场景它以集群的模式承载较大数据量的应用。</p>
<p><img src="assets/c657fab0-c7b6-11e8-9830-8d538e1f1c90" alt="enter image description here" /></p>
<p><img src="assets/c657fab0-c7b6-11e8-9830-8d538e1f1c90" alt="png" /></p>
<p>如上图所示,集群由多个子 Groups 构成,每个 Group 为 M-S 模式、共享存储;多个 Groups 之间基于“Network Connector”建立连接Master-Slave 协议),通常为双向连接,所有的 Groups 之间彼此相连Groups 之间形成“订阅”关系,比如 G2 在逻辑上为 G1 的订阅者(订阅的策略是根据各个 Broker 上消费者的 Destination 列表进行分类),消息的转发原理也基于此。对于 Client 而言,仍然支持 FailoverFailover 协议中可以包含集群中“多数派”的节点地址。</p>
<p>Topic 订阅者的消息,将会在所有 Group 中复制存储,对于 Queue 的消息,将会在 Brokers 之间转发,并最终到达 Consumer 所在的节点。</p>
<p>Producers 和 Consumers 可以与任何 Group 中的 Master 建立连接并进行消息通信,当 Brokers 集群拓扑变化时Producers 或 Consumers 的个数变化时,将会动态平衡 Clients 的连接位置。Brokers 之间通过“Advisory”机制来同步 Clients 的连接信息,比如新的 Consumers 加入Broker 将会发送 Advisory 消息(内部的通道)通知其他 Brokers。</p>
@@ -354,7 +354,7 @@ function hide_canvas() {
</ul>
<h4>3.4 RabbitMQ 架构</h4>
<p>根据官方文档说明RabbitMQ 的架构图如下所示:</p>
<p><img src="assets/7f30a890-c91d-11e8-a4d8-9df8310cdd48" alt="enter image description here" /></p>
<p><img src="assets/7f30a890-c91d-11e8-a4d8-9df8310cdd48" alt="png" /></p>
<p>接下来解释几个重要的概念。</p>
<ul>
<li>Broker即消息队列服务器实体。</li>
@@ -448,7 +448,7 @@ function hide_canvas() {
</ul>
<h4>4.4 RocketMQ 架构</h4>
<p>RocketMQ 是一个具有高性能、高可靠、低延迟、分布式的万亿级容量,且可伸缩的分布式消息和流平台。它由 Name Servers、Brokers、 Producers 和 Consumers 四个部分组成。其架构如下图所示(取自<a href="https://rocketmq.apache.org/docs/rmq-arc/">官网</a>)。</p>
<p><img src="assets/0b881670-c91e-11e8-87ae-d94c3d83a2c9" alt="enter image description here" /></p>
<p><img src="assets/0b881670-c91e-11e8-87ae-d94c3d83a2c9" alt="png" /></p>
<p><strong>NameServer 集群</strong></p>
<p>NameServer 是一个功能齐全的服务器,其角色类似 Kafka 中的 ZooKeeper支持 Broker 的动态注册与发现。主要包括两个功能:</p>
<ul>
@@ -507,11 +507,11 @@ function hide_canvas() {
<h4>5.1 RocketMQ 官方评价</h4>
<p>所谓实践是检验真理的唯一标准,实际应用中的表现比文字更具说服力。在 <a href="https://rocketmq.apache.org/docs/motivation/">RocketMQ 官方文档中</a>,关于 RocketMQ 的研发背景是这样说的:在我们的研究中,随着使用 Queue 和 Topic 的增加ActiveMQ IO 模块很快达到了瓶颈。我们试图通过节流、断路器或降级来解决这个问题,但效果不佳。所以我们开始关注当时流行的消息解决方案 Kafka。不幸的是Kafka 不能满足我们的要求,特别是在低延迟和高可靠性方面。</p>
<p>简而言之ActiveMQ 和 Kafka 的性能都不能满足阿里的超大规模应用场景。在此背景下,阿里自研了 RocketMQ并捐赠给了开源社区目前有超过 100 家企业在使用其开源版本。关于 ActiveMQ 、Kafka 以及 RocketMQ 的比较如下所示(取自 <a href="https://rocketmq.apache.org/docs/motivation/">RocketMQ 官网文档</a></p>
<p><img src="assets/7a907710-c91e-11e8-9c50-75ee2aa67033" alt="enter image description here" /></p>
<p><img src="assets/7a907710-c91e-11e8-9c50-75ee2aa67033" alt="png" /></p>
<h4>5.2 对比四大消息队列</h4>
<p>消息队列利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。目前业界有很多的 MQ 产品,例如 RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMq 等,也有直接使用数据库 Redis 充当消息队列的案例。而这些消息队列产品,各有侧重,在实际选型时,需要结合自身需求及 MQ 产品特征,综合考虑。</p>
<p>以下是四种消息队列的差异对比(<a href="http://blog.51cto.com/caczjz/2141194?source=dra">图片源地址</a></p>
<p><img src="assets/cba21920-c968-11e8-bcac-99cd81fed45b" alt="enter image description here" /></p>
<p><img src="assets/cba21920-c968-11e8-bcac-99cd81fed45b" alt="png" /></p>
<h4>参考文献</h4>
<ol>
<li>RocketMQ 、RabbitMQ、Kafka 以及 ActiveMQ 官方文档;</li>

View File

@@ -137,11 +137,11 @@ function hide_canvas() {
<p>对于商业级消息中间件来说可靠性至关重要那么Kafka 是如何确保消息生产、传输、存储及消费过程中的可靠性的呢?本文将从 Kafka 的架构切入,解读 Kafka 基本原理,并对其存储机制、复制原理、同步原理、可靠性和持久性等作详细解读。</p>
<h3>1. Kafka 总体架构</h3>
<p>基于 Kafka、ZooKeeper 的分布式消息队列系统总体架构如下图所示:</p>
<p><img src="assets/e49bc290-cf95-11e8-8388-bd48f25029c6" alt="enter image description here" /></p>
<p><img src="assets/e49bc290-cf95-11e8-8388-bd48f25029c6" alt="png" /></p>
<p>典型的 Kafka 体系架构包括若干 Producer消息生产者若干 Broker作为 Kafka 节点的服务器),若干 Consumer Group以及一个 ZooKeeper 集群。</p>
<p>Kafka 通过 ZooKeeper 管理集群配置、选举 Leader并在 Consumer Group 发生变化时进行 Rebalance即消费者负载均衡在下一课介绍。Producer 使用 Push模式将消息发布到 BrokerConsumer 使用 Pull模式从 Broker 订阅并消费消息。</p>
<p>上图仅描摹了总体架构,并没有对作为 Kafka 节点的 Broker 进行深入刻画。事实上它的内部细节相当复杂如下图所示Kafka 节点涉及 Topic、Partition 两个重要概念。</p>
<p><img src="assets/4b558580-cafe-11e8-ba64-19e24fcb4ae1" alt="enter image description here" /></p>
<p><img src="assets/4b558580-cafe-11e8-ba64-19e24fcb4ae1" alt="png" /></p>
<p>在 Kafka 架构中,有几个术语需要了解下。</p>
<ul>
<li><strong>Producer</strong> 生产者即消息发送者Push 消息到 Kafka 集群的 Broker就是 Server</li>
@@ -159,7 +159,7 @@ function hide_canvas() {
<h4>1.1 Topic &amp; Partition</h4>
<p>为了便于区分消息Producer 向 Kafka 集群 Push 的消息会被归于某一类别,即 Topic。为了负载均衡、增强可扩展性Topic 又被分为多个 Partition。从存储层面来看每一个 Partition 都是一个有序的、不可变的记录序列通俗点就是一个追加日志Append Log文件。每个 Partition 中的记录都会被分配一个称为偏移量Offset的序列 ID 号,该序列 ID 号唯一地标识 Partition 内的每个记录。</p>
<p>Kafka 机制中Producer Push 的消息是追加Append到 Partition 中的,这是一种顺序写磁盘的机制,效率远高于随机写内存,如下图所示:</p>
<p><img src="assets/dba5c160-d66e-11e8-be45-7958f66a47cf" alt="enter image description here" /></p>
<p><img src="assets/dba5c160-d66e-11e8-be45-7958f66a47cf" alt="png" /></p>
<p>来源Kafka <a href="https://kafka.apache.org/intro">官网</a></p>
<h4>1.2 Kafka 为什么要将 Topic 分区?</h4>
<blockquote>
@@ -221,7 +221,7 @@ log.cleaner.enable=false #是否启用log压缩一般不用启用启用的
</code></pre>
<p>并没有发现“Segment”文件事实上Segment 文件由两部分组成,即 <code>.index</code> 文件和 <code>.log</code> 文件,分别为 Segment 索引文件和数据文件。观察上面的文件名很容易理解它们的命规则对于索引文件Partition 全局的第一个 Segment 从 0 开始,后续每个 Segment 文件名为上一个 Segment 文件最后一条消息的偏移量OffsetOffset 的数值由 20 位数字字符表示,没有数字的位置用 0 填充。对于数据文件,其命名与对应的索引文件保持一致即可。</p>
<p>为了便于读者理解,以上面的其中一“对” Segment 文件为例:<code>00000000000000170410.index</code><code>00000000000000170410.log</code>,绘制其关系图,如下所示:</p>
<p><img src="assets/f07c6d30-db6b-11e8-b319-bd941214645d" alt="enter image description here" /></p>
<p><img src="assets/f07c6d30-db6b-11e8-b319-bd941214645d" alt="png" /></p>
<p><code>.index</code> 文件作为索引文件,存储的是元数据;<code>.log</code> 文件作为数据文件,存储的是消息。如何通过索引访问具体的消息呢?事实上,索引文件中的元数据指向的是对应数据文件中消息的物理偏移地址,有了消息的物理地址,自然也就可以访问对应的消息了。</p>
<p>其中以 <code>.index</code> 索引文件中的元数据 <code>[2, 365]</code> 为例,在 <code>.log</code> 数据文件表示第 2 个消息,即在全局 Partition 中表示 170410+2=170412 个消息,该消息的物理偏移地址为 365。</p>
<p><strong>问题3如何从 Partition 中通过 Offset 查找 Message</strong></p>
@@ -270,7 +270,7 @@ records: [Record]
</ul>
<p>为了便于读者更好地理解副本概念,我们看下面这个例子。</p>
<p>一个具有 4 个 Broker 的 Kafka 集群TopicA 有 3 个 Partition每个 Partition 有 3 个副本Leader+Follower</p>
<p><img src="assets/616acd70-cf9b-11e8-8388-bd48f25029c6" alt="enter image description here" /></p>
<p><img src="assets/616acd70-cf9b-11e8-8388-bd48f25029c6" alt="png" /></p>
<p>如果 Leader 所在的 Broker 发生故障或宕机,对应 Partition 将因无 Leader 而不能处理客户端请求,这时副本的作用就体现出来了:一个新 Leader 将从 Follower 中被选举出来并继续处理客户端的请求。</p>
<p><strong>如何确保新选举出的 Leader 是最优秀的?</strong></p>
<p>一个 Partition 有多个副本Replica为了提高可靠性这些副本分散在不同的 Broker 上。由于带宽、读写性能、网络延迟等因素,同一时刻,这些副本的状态通常是不一致的,即 Follower 与 Leader 的状态不一致。那么,如何保证新 Leader 是优选出来的呢?</p>
@@ -281,7 +281,7 @@ records: [Record]
<p>前面提到 Kafka 中Topic 的每个 Partition 可能有多个副本Replica用于实现冗余从而实现高可用。每个副本又有两个重要的属性 LEO 和 HW。</p>
<p>通过前面内容的学习,我们知道在 Kafka 的存储机制中Partition 可以细分为 Segment而 Segment 是最终的存储粒度。不过,对于上层应用来说,仍然可以将 Partition 看作最小的存储单元,即 Partition 可以看作是由一系列的 Segment 组成的粒度更粗的存储单元,它由一系列有序的消息组成,这些消息被连续的追加到 Partition 中。</p>
<p>LEO、HW 以及 Offset 的关系图如下:</p>
<p><img src="assets/f7aa23c0-cfab-11e8-9378-c501de8503c2" alt="enter image description here" /></p>
<p><img src="assets/f7aa23c0-cfab-11e8-9378-c501de8503c2" alt="png" /></p>
<ul>
<li>LEO即日志末端位移Log End Offset表示每个副本的 Log 最后一条 Message 的位置。比如 LEO=10、HW=7则表示该副本保存了 10 条消息,而后面 3 条处于 Uncommitted 状态。</li>
<li>HW即水位值High Watermark。对于同一个副本而言其 HW 值不大于 LEO 值。小于等于 HW 值的所有消息都被认为是“已备份”的Replicated对于任何一个 Partition取其对应的 ISR 中最小的 LEO 作为 HWConsumer 最多只能消费到 HW 所在的位置。</li>
@@ -292,7 +292,7 @@ records: [Record]
</blockquote>
<p><strong>下面我们举例说明。</strong></p>
<p>某个 Partition 的 ISR 列表包括 3 个副本1 个 Leader+2 个 Follower当 Producer 向其 Leader 写入一条消息后HW 和 LEO 有如下变化过程:</p>
<p><img src="assets/ac639f00-cfaa-11e8-add6-178f04093f1a" alt="enter image description here" /></p>
<p><img src="assets/ac639f00-cfaa-11e8-add6-178f04093f1a" alt="png" /></p>
<p>由上图可以看出Kafka 的复制机制既不是完全的同步复制,也不是单纯的异步复制。同步复制要求所有能工作的 Follower 都复制完,这条消息才会被置为 Committed 状态,该复制方式受限于复制最慢的 Follower会极大地影响吞吐率因而极少应用于生产环境。而异步复制方式下Follower 异步地从 Leader 复制数据,数据只要被 Leader 写入 Log 就被认为已经 Committed类似 Redis主从异步复制。如果在 Follower 尚未复制完成的情况下Leader 宕机,则必然导致数据丢失,很多时候,这是不可接受的。</p>
<p>相较于完全同步复制和异步复制Kafka 使用 ISR 的策略则是一种较“中庸”的策略在可靠性和吞吐率方面取得了较好的平衡。某种意义上ISR 策略与第 8 课中介绍的 Raft 算法所采用的“多数派原则”类似,不过 ISR 更为灵活。</p>
<h4>2.4 Kafka 消息生产的可靠性</h4>
@@ -345,9 +345,9 @@ records: [Record]
<h4>3.2 Topic 在 ZooKeeper 中的注册</h4>
<p>在 Kafka 中,所有 Topic 与 Broker 的对应关系都由 ZooKeeper 来维护,在 ZooKeeper 中,通过建立专属的节点来存储这些信息,其路径为 <code>/brokers/topics/{topic_name}</code></p>
<p>前面说过,为了保障数据的可靠性,每个 Topic 的 Partition 实际上是存在备份的,并且备份的数量由 Kafka 机制中的 Replicas 来控制。那么问题来了,如下图所示,假设某个 TopicA 被分为 2 个 Partition并且存在两个备份由于这 2 个 Partition1-2被分布在不同的 Broker 上,同一个 Partiton 与其备份不能(也不应该)存储于同一个 Broker 上。以 Partition1 为例,假设它被存储于 Broker2其对应的备份分别存储于 Broker1 和 Broker4有了备份可靠性得到保障但数据一致性却是个问题。</p>
<p><img src="assets/7161bf30-cb00-11e8-bcac-99cd81fed45b" alt="enter image description here" /></p>
<p><img src="assets/7161bf30-cb00-11e8-bcac-99cd81fed45b" alt="png" /></p>
<p>为了保障数据的一致性ZooKeeper 机制得以引入。基于 ZooKeeperKafka 为每一个 Partition 找一个节点作为 Leader其余备份作为 Follower接续上图的例子就 TopicA 的 Partition1 而言,如果位于 Broker2Kafka 节点)上的 Partition1 为 Leader那么位于 Broker1 和 Broker4 上面的 Partition1 就充当 Follower则有下图</p>
<p><img src="assets/780e1ef0-cb00-11e8-9b13-63a667cc1a24" alt="enter image description here" /></p>
<p><img src="assets/780e1ef0-cb00-11e8-9b13-63a667cc1a24" alt="png" /></p>
<p>基于上图的架构,当 Producer Push 的消息写入 Partition分区作为 Leader 的 BrokerKafka 节点)会将消息写入自己的分区,同时还会将此消息复制到各个 Follower实现同步。如果某个 Follower 挂掉Leader 会再找一个替代并同步消息;如果 Leader 挂了Follower 们会选举出一个新的 Leader 替代,继续业务,这些都是由 ZooKeeper 完成的。</p>
<h4>3.3 Consumer 在 ZooKeeper 中的注册</h4>
<p><strong>Consumer Group 注册</strong></p>

View File

@@ -149,7 +149,7 @@ function hide_canvas() {
<p>如果出现 Leader 故障下线的情况,就需要从所有的 Follower 中选举新的 Leader以便继续提供服务。为了保证一致性通常只能从 ISR 列表中选取新的 Leader 上面已经介绍ISR 列表中的 Follower 与原 Leader 保持同步),因此,无论 ISR 中哪个 Follower 被选为新的 Leader它都知道 HW 之前的数据,可以保证在切换了 Leader 后Consumer 可以继续“看到”之前已经由 Producer 提交的数据。</p>
<p>如下图所示,如果 Leader 宕机Follower1 被选为新的 Leader而新 Leader (原 Follower1 )并没有完全同步之前 Leader 的所有数据(少了一个消息 6之后新 Leader 又继续接受了新的数据,此时,原本宕机的 Leader 经修复后重新上线,它将发现新 Leader 中的数据和自己持有的数据不一致,怎么办呢?</p>
<p>为了保证一致性,必须有一方妥协,显然旧的 Leader 优先级较低,因此, 它会将自己的数据截断到宕机之前的 HW 位置HW 之前的数据,与 Leader 一定是相同的),然后同步新 Leader 的数据。这便是所谓的 “截断机制”。</p>
<p><img src="assets/0b5a9de0-cfad-11e8-8388-bd48f25029c6" alt="enter image description here" /></p>
<p><img src="assets/0b5a9de0-cfad-11e8-8388-bd48f25029c6" alt="png" /></p>
<h3>3. 消息生产的可靠性</h3>
<h4>3.1 消息可能重复生产</h4>
<p>在第 12 课 2.4 小节中,我们介绍了消息生产过程中保证数据可靠性的策略。该策略虽然可以保障消息不丢失,但无法避免出现重复消息。例如,生产者发送数据给 LeaderLeader 同步数据给 ISR 中的 Follower同步到一半 Leader 时宕机,此时选出新的 Leader它可能具有部分此次提交的数据而生产者收到发送失败响应后将重发数据新的 Leader 接受数据则数据重复。因此 Kafka 只支持“At Most Once”和“At Least Once”而不支持“Exactly Once”消息去重需在具体的业务中实现。</p>