0%

1. Apache Kafka是什么?

Apach Kafka是一款分布式流处理平台,用于实时构建流处理应用。它有一个核心的功能广为人知,即作为企业级的消息引擎被广泛使用(通常也会称之为消息总线message bus)。

2. Kafka 的设计是什么样的?

Kafka 将消息以 topic 为单位进行归纳

将向 Kafka topic 发布消息的程序成为 producers.

将预订 topics 并消费消息的程序成为 consumer.

Kafka 以集群的方式运行,可以由一个或多个服务组成,每个服务叫做一个 broker.

producers 通过网络将消息发送到 Kafka 集群,集群向消费者提供消息

3. Kafka 如何保证高可用?

Kafka 的基本架构组成是:由多个 broker 组成一个集群,每个 broker 是一个节点;当创建一个 topic 时,这个 topic 会被划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 只存放一部分数据。

这就是天然的分布式消息队列,就是说一个 topic 的数据,是分散放在多个机器上的,每个机器就放一部分数据

Kafka 0.8 版本之前,是没有 HA 机制的,当任何一个 broker 所在节点宕机了,这个 broker 上的 partition 就无法提供读写服务,所以这个版本之前,Kafka 没有什么高可用性可言。

Kafka 0.8 以后,提供了 HA 机制,就是 replica 副本机制。每个 partition 上的数据都会同步到其它机器,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,消息的生产者和消费者都跟这个 leader 打交道,其他 replica 作为 follower。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。Kafka 负责均匀的将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性。

img

拥有了 replica 副本机制,如果某个 broker 宕机了,这个 broker 上的 partition 在其他机器上还存在副本。如果这个宕机的 broker 上面有某个 partitionleader,那么此时会从其 follower 中重新选举一个新的 leader 出来,这个新的 leader 会继续提供读写服务,这就有达到了所谓的高可用性。

写数据的时候,生产者只将数据写入 leader 节点,leader 会将数据写入本地磁盘,接着其他 follower 会主动从 leader 来拉取数据,follower 同步好数据了,就会发送 ackleaderleader 收到所有 followerack 之后,就会返回写成功的消息给生产者。

消费数据的时候,消费者只会从 leader 节点去读取消息,但是只有当一个消息已经被所有 follower 都同步成功返回 ack 的时候,这个消息才会被消费者读到。

img

4. Kafka 消息是采用 Pull 模式,还是 Push 模式?

生产者使用push模式将消息发布到Broker,消费者使用pull模式从Broker订阅消息。

push模式很难适应消费速率不同的消费者,如果push的速度太快,容易造成消费者拒绝服务或网络拥塞;如果push的速度太慢,容易造成消费者性能浪费。但是采用pull的方式也有一个缺点,就是当Broker没有消息时,消费者会陷入不断地轮询中,为了避免这点,kafka有个参数可以让消费者阻塞知道是否有新消息到达。

5. Kafka 与传统消息系统之间的区别

  • Kafka 持久化日志,这些日志可以被重复读取和无限期保留

  • Kafka 是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性

  • Kafka 支持实时的流式处理

6. 什么是消费者组?

消费者组是Kafka独有的概念,即消费者组是Kafka提供的可扩展且具有容错性的消费者机制。

但实际上,消费者组(Consumer Group)其实包含两个概念,作为队列,消费者组允许你分割数据处理到一组进程集合上(即一个消费者组中可以包含多个消费者进程,他们共同消费该topic的数据),这有助于你的消费能力的动态调整;作为发布-订阅模型(publish-subscribe),Kafka允许你将同一份消息广播到多个消费者组里,以此来丰富多种数据使用场景。

需要注意的是:在消费者组中,多个实例共同订阅若干个主题,实现共同消费。同一个组下的每个实例都配置有相同的组ID,被分配不同的订阅分区。当某个实例挂掉的时候,其他实例会自动地承担起它负责消费的分区。 因此,消费者组在一定程度上也保证了消费者程序的高可用性。

1.jpg

7. 在Kafka中,ZooKeeper的作用是什么?

目前,Kafka使用ZooKeeper存放集群元数据、成员管理、Controller选举,以及其他一些管理类任务。之后,等KIP-500提案完成后,Kafka将完全不再依赖于ZooKeeper。

  • “存放元数据”是指主题分区的所有数据都保存在 ZooKeeper 中,且以它保存的数据为权威,其他 “人” 都要与它保持对齐。
  • “成员管理” 是指 Broker 节点的注册、注销以及属性变更,等等。
  • “Controller 选举” 是指选举集群 Controller,而其他管理类任务包括但不限于主题删除、参数配置等。

KIP-500 思想,是使用社区自研的基于Raft的共识算法,替代ZooKeeper,实现Controller自选举。

8. 解释下Kafka中位移(offset)的作用

在Kafka中,每个主题分区下的每条消息都被赋予了一个唯一的ID数值,用于标识它在分区中的位置。这个ID数值,就被称为位移,或者叫偏移量。一旦消息被写入到分区日志,它的位移值将不能被修改。

9. kafka 为什么那么快?

  • Cache Filesystem Cache PageCache缓存
  • 顺序写:由于现代的操作系统提供了预读和写技术,磁盘的顺序写大多数情况下比随机写内存还要快。
  • Zero-copy:零拷技术减少拷贝次数
  • Batching of Messages:批量量处理。合并小的请求,然后以流的方式进行交互,直顶网络上限。
  • Pull 拉模式:使用拉模式进行消息的获取消费,与消费端处理能力相符。

10. kafka producer发送数据,ack为0,1,-1分别是什么意思?

  • 1(默认) 数据发送到Kafka后,经过leader成功接收消息的的确认,就算是发送成功了。在这种情况下,如果leader宕机了,则会丢失数据。
  • 0 生产者将数据发送出去就不管了,不去等待任何返回。这种情况下数据传输效率最高,但是数据可靠性确是最低的。
  • -1producer需要等待ISR中的所有follower都确认接收到数据后才算一次发送完成,可靠性最高。当ISR中所有Replica都向Leader发送ACK时,leader才commit,这时候producer才能认为一个请求中的消息都commit了。

11. Kafka如何保证消息不丢失?

首先需要弄明白消息为什么会丢失,对于一个消息队列,会有 生产者MQ消费者 这三个角色,在这三个角色数据处理和传输过程中,都有可能会出现消息丢失。

img

消息丢失的原因以及解决办法:

消费者异常导致的消息丢失

消费者可能导致数据丢失的情况是:消费者获取到了这条消息后,还未处理,Kafka 就自动提交了 offset,这时 Kafka 就认为消费者已经处理完这条消息,其实消费者才刚准备处理这条消息,这时如果消费者宕机,那这条消息就丢失了。

消费者引起消息丢失的主要原因就是消息还未处理完 Kafka 会自动提交了 offset,那么只要关闭自动提交 offset,消费者在处理完之后手动提交 offset,就可以保证消息不会丢失。但是此时需要注意重复消费问题,比如消费者刚处理完,还没提交 offset,这时自己宕机了,此时这条消息肯定会被重复消费一次,这就需要消费者根据实际情况保证幂等性。

生产者数据传输导致的消息丢失

对于生产者数据传输导致的数据丢失主常见情况是生产者发送消息给 Kafka,由于网络等原因导致消息丢失,对于这种情况也是通过在 producer 端设置 acks=all 来处理,这个参数是要求 leader 接收到消息后,需要等到所有的 follower 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者会自动不断的重试。

Kafka 导致的消息丢失

Kafka 导致的数据丢失一个常见的场景就是 Kafka 某个 broker 宕机,,而这个节点正好是某个 partitionleader 节点,这时需要重新重新选举该 partitionleader。如果该 partitionleader 在宕机时刚好还有些数据没有同步到 follower,此时 leader 挂了,在选举某个 followerleader 之后,就会丢失一部分数据。

对于这个问题,Kafka 可以设置如下 4 个参数,来尽量避免消息丢失:

  • topic 设置 replication.factor 参数:这个值必须大于 1,要求每个 partition 必须有至少 2 个副本;
  • Kafka 服务端设置 min.insync.replicas 参数:这个值必须大于 1,这个参数的含义是一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 节点。
  • producer 端设置 acks=all,这个是要求每条数据,必须是写入所有 replica 之后,才能认为是写成功了;
  • producer 端设置 retries=MAX(很大很大很大的一个值,无限次重试的意思):这个参数的含义是一旦写入失败,就无限重试,卡在这里了。

13. Kafka 如何保证消息的顺序性

在某些业务场景下,我们需要保证对于有逻辑关联的多条MQ消息被按顺序处理,比如对于某一条数据,正常处理顺序是新增-更新-删除,最终结果是数据被删除;如果消息没有按序消费,处理顺序可能是删除-新增-更新,最终数据没有被删掉,可能会产生一些逻辑错误。对于如何保证消息的顺序性,主要需要考虑如下两点:

  • 如何保证消息在 Kafka 中顺序性;
  • 如何保证消费者处理消费的顺序性。

如何保证消息在 Kafka 中顺序性

对于 Kafka,如果我们创建了一个 topic,默认有三个 partition。生产者在写数据的时候,可以指定一个 key,比如在订单 topic 中我们可以指定订单 id 作为 key,那么相同订单 id 的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。消费者从 partition 中取出来数据的时候,也一定是有顺序的。通过制定 key 的方式首先可以保证在 kafka 内部消息是有序的。

如何保证消费者处理消费的顺序性

对于某个 topic 的一个 partition,只能被同组内部的一个 consumer 消费,如果这个 consumer 内部还是单线程处理,那么其实只要保证消息在 MQ 内部是有顺序的就可以保证消费也是有顺序的。但是单线程吞吐量太低,在处理大量 MQ 消息时,我们一般会开启多线程消费机制,那么如何保证消息在多个线程之间是被顺序处理的呢?对于多线程消费我们可以预先设置 N 个内存 Queue,具有相同 key 的数据都放到同一个内存 Queue 中;然后开启 N 个线程,每个线程分别消费一个内存 Queue 的数据即可,这样就能保证顺序性。当然,消息放到内存 Queue 中,有可能还未被处理,consumer 发生宕机,内存 Queue 中的数据会全部丢失,这就转变为上面提到的如何保证消息的可靠传输的问题了。

14. Kafka中的ISR、AR代表什么?ISR的伸缩指什么?

  • ISR:In-Sync Replicas 副本同步队列
  • AR:Assigned Replicas 所有副本

ISR是由leader维护,follower从leader同步数据有一些延迟(包括延迟时间replica.lag.time.max.ms延迟条数replica.lag.max.messages两个维度,当前最新的版本0.10.x中只支持replica.lag.time.max.ms这个维度),任意一个超过阈值都会把follower剔除出ISR,存入OSR(Outof-Sync Replicas)列表,新加入的follower也会先存放在OSR中。

AR=ISR+OSR。

15. 描述下 Kafka 中的领导者副本(Leader Replica)和追随者副本(Follower Replica)的区别

Kafka副本当前分为领导者副本和追随者副本。只有Leader副本才能对外提供读写服务,响应Clients端的请求。Follower副本只是采用拉(PULL)的方式,被动地同步Leader副本中的数据,并且在Leader副本所在的Broker宕机后,随时准备应聘Leader副本。

加分点:

  • 强调Follower副本也能对外提供读服务。自Kafka 2.4版本开始,社区通过引入新的Broker端参数,允许Follower副本有限度地提供读服务。
  • 强调Leader和Follower的消息序列在实际场景中不一致。通常情况下,很多因素可能造成Leader和Follower之间的不同步,比如程序问题,网络问题,broker问题等,短暂的不同步我们可以关注(秒级别),但长时间的不同步可能就需要深入排查了,因为一旦Leader所在节点异常,可能直接影响可用性。

注意:之前确保一致性的主要手段是高水位机制(HW),但高水位值无法保证Leader连续变更场景下的数据一致性,因此,社区引入了Leader Epoch机制,来修复高水位值的弊端。

16. 分区Leader选举策略有几种?

分区的Leader副本选举对用户是完全透明的,它是由Controller独立完成的。你需要回答的是,在哪些场景下,需要执行分区Leader选举。每一种场景对应于一种选举策略。

  • OfflinePartition Leader选举:每当有分区上线时,就需要执行Leader选举。所谓的分区上线,可能是创建了新分区,也可能是之前的下线分区重新上线。这是最常见的分区Leader选举场景。
  • ReassignPartition Leader选举:当你手动运行kafka-reassign-partitions命令,或者是调用Admin的alterPartitionReassignments方法执行分区副本重分配时,可能触发此类选举。假设原来的AR是[1,2,3],Leader是1,当执行副本重分配后,副本集合AR被设置成[4,5,6],显然,Leader必须要变更,此时会发生Reassign Partition Leader选举。
  • PreferredReplicaPartition Leader选举:当你手动运行kafka-preferred-replica-election命令,或自动触发了Preferred Leader选举时,该类策略被激活。所谓的Preferred Leader,指的是AR中的第一个副本。比如AR是[3,2,1],那么,Preferred Leader就是3。
  • ControlledShutdownPartition Leader选举:当Broker正常关闭时,该Broker上的所有Leader副本都会下线,因此,需要为受影响的分区执行相应的Leader选举。

这4类选举策略的大致思想是类似的,即从AR中挑选首个在ISR中的副本,作为新Leader。

17. Kafka的哪些场景中使用了零拷贝(Zero Copy)?

在Kafka中,体现Zero Copy使用场景的地方有两处:基于mmap的索引和日志文件读写所用的TransportLayer。

先说第一个。索引都是基于MappedByteBuffer的,也就是让用户态和内核态共享内核态的数据缓冲区,此时,数据不需要复制到用户态空间。不过,mmap虽然避免了不必要的拷贝,但不一定就能保证很高的性能。在不同的操作系统下,mmap的创建和销毁成本可能是不一样的。很高的创建和销毁开销会抵消Zero Copy带来的性能优势。由于这种不确定性,在Kafka中,只有索引应用了mmap,最核心的日志并未使用mmap机制。

再说第二个。TransportLayer是Kafka传输层的接口。它的某个实现类使用了FileChannel的transferTo方法。该方法底层使用sendfile实现了Zero Copy。对Kafka而言,如果I/O通道使用普通的PLAINTEXT,那么,Kafka就可以利用Zero Copy特性,直接将页缓存中的数据发送到网卡的Buffer中,避免中间的多次拷贝。相反,如果I/O通道启用了SSL,那么,Kafka便无法利用Zero Copy特性了。

18. 为什么Kafka不支持读写分离?

在 Kafka 中,生产者写入消息、消费者读取消息的操作都是与 leader 副本进行交互的,从 而实现的是一种主写主读的生产消费模型。

Kafka 并不支持主写从读,因为主写从读有 2 个很明 显的缺点:

  • 数据一致性问题。数据从主节点转到从节点必然会有一个延时的时间窗口,这个时间 窗口会导致主从节点之间的数据不一致。某一时刻,在主节点和从节点中 A 数据的值都为 X, 之后将主节点中 A 的值修改为 Y,那么在这个变更通知到从节点之前,应用读取从节点中的 A 数据的值并不为最新的 Y,由此便产生了数据不一致的问题。
  • 延时问题。类似 Redis 这种组件,数据从写入主节点到同步至从节点中的过程需要经历网络→主节点内存→网络→从节点内存这几个阶段,整个过程会耗费一定的时间。而在 Kafka 中,主从同步会比 Redis 更加耗时,它需要经历网络→主节点内存→主节点磁盘→网络→从节点内存→从节点磁盘这几个阶段。对延时敏感的应用而言,主写从读的功能并不太适用。

参考

http://dockone.io/article/10853

https://segmentfault.com/a/1190000023716306

https://dongzl.github.io/2020/03/16/13-Solve-MQ-Problem-With-Kafka/index.html

1 背景

当今的数据中心和应用程序在高度动态的环境中运行,为了应对高度动态的环境,它们通过额外的服务器进行横向扩展,并且根据需求进行扩展和收缩。同时,服务器和网络故障也很常见。

因此,系统必须在正常操作期间处理服务器的上下线。它们必须对变故做出反应并在几秒钟内自动适应;对客户来说的话,明显的中断通常是不可接受的。

幸运的是,分布式共识可以帮助应对这些挑战。

# 1.1 拜占庭将军

在介绍共识算法之前,先介绍一个简化版拜占庭将军的例子来帮助理解共识算法。

假设多位拜占庭将军中没有叛军,信使的信息可靠但有可能被暗杀的情况下,将军们如何达成是否要进攻的一致性决定?

解决方案大致可以理解成:先在所有的将军中选出一个大将军,用来做出所有的决定。

举例如下:假如现在一共有 3 个将军 A,B 和 C,每个将军都有一个随机时间的倒计时器,倒计时一结束,这个将军就把自己当成大将军候选人,然后派信使传递选举投票的信息给将军 B 和 C,如果将军 B 和 C 还没有把自己当作候选人(自己的倒计时还没有结束),并且没有把选举票投给其他人,它们就会把票投给将军 A,信使回到将军 A 时,将军 A 知道自己收到了足够的票数,成为大将军。在有了大将军之后,是否需要进攻就由大将军 A 决定,然后再去派信使通知另外两个将军,自己已经成为了大将军。如果一段时间还没收到将军 B 和 C 的回复(信使可能会被暗杀),那就再重派一个信使,直到收到回复。

# 1.2 共识算法

共识是可容错系统中的一个基本问题:即使面对故障,服务器也可以在共享状态上达成一致。

共识算法允许一组节点像一个整体一样一起工作,即使其中的一些节点出现故障也能够继续工作下去,其正确性主要是源于复制状态机的性质:一组Server的状态机计算相同状态的副本,即使有一部分的Server宕机了它们仍然能够继续运行。

rsm-architecture.png

1
图-1 复制状态机架构

一般通过使用复制日志来实现复制状态机。每个Server存储着一份包括命令序列的日志文件,状态机会按顺序执行这些命令。因为每个日志包含相同的命令,并且顺序也相同,所以每个状态机处理相同的命令序列。由于状态机是确定性的,所以处理相同的状态,得到相同的输出。

因此共识算法的工作就是保持复制日志的一致性。服务器上的共识模块从客户端接收命令并将它们添加到日志中。它与其他服务器上的共识模块通信,以确保即使某些服务器发生故障。每个日志最终包含相同顺序的请求。一旦命令被正确地复制,它们就被称为已提交。每个服务器的状态机按照日志顺序处理已提交的命令,并将输出返回给客户端,因此,这些服务器形成了一个单一的、高度可靠的状态机。

适用于实际系统的共识算法通常具有以下特性:

  • 安全。确保在非拜占庭条件(也就是上文中提到的简易版拜占庭)下的安全性,包括网络延迟、分区、包丢失、复制和重新排序。
  • 高可用。只要大多数服务器都是可操作的,并且可以相互通信,也可以与客户端进行通信,那么这些服务器就可以看作完全功能可用的。因此,一个典型的由五台服务器组成的集群可以容忍任何两台服务器端故障。假设服务器因停止而发生故障;它们稍后可能会从稳定存储上的状态中恢复并重新加入集群。
  • 一致性不依赖时序。错误的时钟和极端的消息延迟,在最坏的情况下也只会造成可用性问题,而不会产生一致性问题。
  • 在集群中大多数服务器响应,命令就可以完成,不会被少数运行缓慢的服务器来影响整体系统性能。

# 2 基础

# 2.1 节点类型

一个 Raft 集群包括若干服务器,以典型的 5 服务器集群举例。在任意的时间,每个服务器一定会处于以下三个状态中的一个:

  • Leader:负责发起心跳,响应客户端,创建日志,同步日志。
  • Candidate:Leader 选举过程中的临时角色,由 Follower 转化而来,发起投票参与竞选。
  • Follower:接受 Leader 的心跳和日志同步数据,投票给 Candidate。

在正常的情况下,只有一个服务器是 Leader,剩下的服务器是 Follower。Follower 是被动的,它们不会发送任何请求,只是响应来自 Leader 和 Candidate 的请求。

img

1
图-2:服务器的状态

# 2.2 任期

img

1
图-3:任期

如图 3 所示,raft 算法将时间划分为任意长度的任期(term),任期用连续的数字表示,看作当前 term 号。每一个任期的开始都是一次选举,在选举开始时,一个或多个 Candidate 会尝试成为 Leader。如果一个 Candidate 赢得了选举,它就会在该任期内担任 Leader。如果没有选出 Leader,将会开启另一个任期,并立刻开始下一次选举。raft 算法保证在给定的一个任期最少要有一个 Leader。

每个节点都会存储当前的 term 号,当服务器之间进行通信时会交换当前的 term 号;如果有服务器发现自己的 term 号比其他人小,那么他会更新到较大的 term 值。如果一个 Candidate 或者 Leader 发现自己的 term 过期了,他会立即退回成 Follower。如果一台服务器收到的请求的 term 号是过期的,那么它会拒绝此次请求。

# 2.3 日志

  • entry:每一个事件成为 entry,只有 Leader 可以创建 entry。entry 的内容为<term,index,cmd>其中 cmd 是可以应用到状态机的操作。
  • log:由 entry 构成的数组,每一个 entry 都有一个表明自己在 log 中的 index。只有 Leader 才可以改变其他节点的 log。entry 总是先被 Leader 添加到自己的 log 数组中,然后再发起共识请求,获得同意后才会被 Leader 提交给状态机。Follower 只能从 Leader 获取新日志和当前的 commitIndex,然后把对应的 entry 应用到自己的状态机中。

# 3 领导人选举

raft 使用心跳机制来触发 Leader 的选举。

如果一台服务器能够收到来自 Leader 或者 Candidate 的有效信息,那么它会一直保持为 Follower 状态,并且刷新自己的 electionElapsed,重新计时。

Leader 会向所有的 Follower 周期性发送心跳来保证自己的 Leader 地位。如果一个 Follower 在一个周期内没有收到心跳信息,就叫做选举超时,然后它就会认为此时没有可用的 Leader,并且开始进行一次选举以选出一个新的 Leader。

为了开始新的选举,Follower 会自增自己的 term 号并且转换状态为 Candidate。然后他会向所有节点发起 RequestVoteRPC 请求, Candidate 的状态会持续到以下情况发生:

  • 赢得选举
  • 其他节点赢得选举
  • 一轮选举结束,无人胜出

赢得选举的条件是:一个 Candidate 在一个任期内收到了来自集群内的多数选票(N/2+1),就可以成为 Leader。

在 Candidate 等待选票的时候,它可能收到其他节点声明自己是 Leader 的心跳,此时有两种情况:

  • 该 Leader 的 term 号大于等于自己的 term 号,说明对方已经成为 Leader,则自己回退为 Follower。
  • 该 Leader 的 term 号小于自己的 term 号,那么会拒绝该请求并让该节点更新 term。

由于可能同一时刻出现多个 Candidate,导致没有 Candidate 获得大多数选票,如果没有其他手段来重新分配选票的话,那么可能会无限重复下去。

raft 使用了随机的选举超时时间来避免上述情况。每一个 Candidate 在发起选举后,都会随机化一个新的枚举超时时间,这种机制使得各个服务器能够分散开来,在大多数情况下只有一个服务器会率先超时;它会在其他服务器超时之前赢得选举。

# 4 日志复制

一旦选出了 Leader,它就开始接受客户端的请求。每一个客户端的请求都包含一条需要被复制状态机(Replicated State Mechine)执行的命令。

Leader 收到客户端请求后,会生成一个 entry,包含<index,term,cmd>,再将这个 entry 添加到自己的日志末尾后,向所有的节点广播该 entry,要求其他服务器复制这条 entry。

如果 Follower 接受该 entry,则会将 entry 添加到自己的日志后面,同时返回给 Leader 同意。

如果 Leader 收到了多数的成功响应,Leader 会将这个 entry 应用到自己的状态机中,之后可以成为这个 entry 是 committed 的,并且向客户端返回执行结果。

raft 保证以下两个性质:

  • 在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们一定有相同的 cmd
  • 在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们前面的 entry 也一定相同

通过“仅有 Leader 可以生存 entry”来保证第一个性质,第二个性质需要一致性检查来进行保证。

一般情况下,Leader 和 Follower 的日志保持一致,然后,Leader 的崩溃会导致日志不一样,这样一致性检查会产生失败。Leader 通过强制 Follower 复制自己的日志来处理日志的不一致。这就意味着,在 Follower 上的冲突日志会被领导者的日志覆盖。

为了使得 Follower 的日志和自己的日志一致,Leader 需要找到 Follower 与它日志一致的地方,然后删除 Follower 在该位置之后的日志,接着把这之后的日志发送给 Follower。

Leader 给每一个Follower 维护了一个 nextIndex,它表示 Leader 将要发送给该追随者的下一条日志条目的索引。当一个 Leader 开始掌权时,它会将 nextIndex 初始化为它的最新的日志条目索引数+1。如果一个 Follower 的日志和 Leader 的不一致,AppendEntries 一致性检查会在下一次 AppendEntries RPC 时返回失败。在失败之后,Leader 会将 nextIndex 递减然后重试 AppendEntries RPC。最终 nextIndex 会达到一个 LeaderFollower 日志一致的地方。这时,AppendEntries 会返回成功,Follower 中冲突的日志条目都被移除了,并且添加所缺少的上了 Leader 的日志条目。一旦 AppendEntries 返回成功,FollowerLeader 的日志就一致了,这样的状态会保持到该任期结束。

# 5 安全性

# 5.1 选举限制

Leader 需要保证自己存储全部已经提交的日志条目。这样才可以使日志条目只有一个流向:从 Leader 流向 Follower,Leader 永远不会覆盖已经存在的日志条目。

每个 Candidate 发送 RequestVoteRPC 时,都会带上最后一个 entry 的信息。所有节点收到投票信息时,会对该 entry 进行比较,如果发现自己的更新,则拒绝投票给该 Candidate。

判断日志新旧的方式:如果两个日志的 term 不同,term 大的更新;如果 term 相同,更长的 index 更新。

# 5.2 节点崩溃

如果 Leader 崩溃,集群中的节点在 electionTimeout 时间内没有收到 Leader 的心跳信息就会触发新一轮的选主,在选主期间整个集群对外是不可用的。

如果 Follower 和 Candidate 崩溃,处理方式会简单很多。之后发送给它的 RequestVoteRPC 和 AppendEntriesRPC 会失败。由于 raft 的所有请求都是幂等的,所以失败的话会无限的重试。如果崩溃恢复后,就可以收到新的请求,然后选择追加或者拒绝 entry。

# 5.3 时间与可用性

raft 的要求之一就是安全性不依赖于时间:系统不能仅仅因为一些事件发生的比预想的快一些或者慢一些就产生错误。为了保证上述要求,最好能满足以下的时间条件:

1
broadcastTime << electionTimeout << MTBF
  • broadcastTime:向其他节点并发发送消息的平均响应时间;
  • electionTimeout:选举超时时间;
  • MTBF(mean time between failures):单台机器的平均健康时间;

broadcastTime应该比electionTimeout小一个数量级,为的是使Leader能够持续发送心跳信息(heartbeat)来阻止Follower开始选举;

electionTimeout也要比MTBF小几个数量级,为的是使得系统稳定运行。当Leader崩溃时,大约会在整个electionTimeout的时间内不可用;我们希望这种情况仅占全部时间的很小一部分。

由于broadcastTimeMTBF是由系统决定的属性,因此需要决定electionTimeout的时间。

一般来说,broadcastTime 一般为 0.5~20ms,electionTimeout 可以设置为 10~500ms,MTBF 一般为一两个月。

# 6 参考

单机版Redis问题

  • 机器故障需要做数据手动迁移
  • 容量瓶颈
  • QPS瓶颈

引入正题

前面列出的容量瓶颈和QPS瓶颈是redis分布式要解决的问题,本篇还是主要解决
redis怎么实现高可用,机器故障的问题

主从复制介绍

作用:

  • 流量分流和负载均衡
  • 提供多个数据分布
  • 扩展redis读性能

简单总结

  • 一个master可以有多个slave
  • 一个slave只能有一个master
  • 数据流向是单向的,master到slave

主从复制操作

master节点:

1
2
cp ${redis_src}/redis.conf redis-6379.conf
vim redis-6379.conf

改动项:

1
2
3
4
5
6
7
8
9
daemonize yes
pidfile /var/run/redis-6379.pid
logfile "6379.log"
logfile "6379.log"
#save 900 1
#save 300 10
#save 60 10000
dbfilename dump-6379.rdb
dir /opt/soft/data

slave节点:

1
2
cp ${redis_src}/redis.conf redis-6380.conf
vim redis-6380.conf

改动项:

1
2
3
4
5
6
7
8
9
10
11
daemonize yes
pidfile /var/run/redis-6380.pid
logfile "6380.log"
logfile "6380.log"
#save 900 1
#save 300 10
#save 60 10000
dbfilename dump-6380.rdb
dir /opt/soft/data
salveof 127.0.0.1 6379 #master节点 ip port
masterauth #主节点设置密码时需要配置

启动:

1
2
redis-server 6379.conf
redis-server 6380.conf

检查主从状态:

1
2
3
4
5
redis-cli
127.0.0.1:6379> info replication
127.0.0.1:6380> info replication
127.0.0.1:6379> set hello world
127.0.0.1:6380> get hello

runid和复制偏移量

runid

redis每次启动后都会随机生成一个runid执行:

1
2
redis-cli -p 6379 info server |grep run
redis-cli -p 6380 info server |grep run

redis每次重启runid会发生变化,redis从节点每次会检测主节点runid变化来进行一次全量复制

偏移量

主节点和从节点都会记录执行一条命令时数据写入的字节数,当偏移量达到一致时,数据才会同步完成

全量复制

全量复制开销

  • bgsave时间
  • RDB文件网络传输时间
  • 从节点清空数据时间
  • 从节点加载RDB时间
  • 如果配置AOF开启会有AOF重写时间

部分复制

开发运维中的问题

规避全量复制:

1:第一次全量复制

  • 问题:第一次不可避免
  • 解决:小主节点,低峰

2:节点运行ID不匹配

  • 问题:主节点重启runid变化
  • 解决:故障转移,例如哨兵或集群

3:复制积压缓冲区不足

  • 问题:网络中断,部分复制无法满足
  • 解决:增大复制缓冲区配置rel_backlog_size

规避复制风暴:

1:单主节点复制风暴

  • 问题:主节点重启,多从节点复制
  • 解决:更换复制拓扑(树形架构)

1:单机器复制风暴

  • 问题:机器宕机后,大量全量复制
  • 解决:主节点分散多机器

前言

话不多说直奔主题…

RDB

主要触发机制

save

cli命令: save

文件策略: 如果存在老的RDB文件,新的将其替换掉

时间复杂度: O(n)

我们把客户端和服务端用一个图来表示,save时会帮我们生成一个RDB文件

由于它是同步命令,并且在单线程中执行,在数据量非常多的时候,此时执行save命令,他会将数据进行完整拷贝,可能会造成redis阻塞。

bgsave

通过在后台fork一个子进程完成复制

自动

根据REDIS配置定时同步数据到RDB文件

配置SecondsChanges
save9001
save30010
save6010000

eg:60秒中改变了10000次会发生备份RDB

触发机制-不容忽略的方式

  • 全量复制
  • Debug Reload
  • shutdown

save or bgsave ?

命令savebgsave
IO类型同步异步
阻塞发生在fork时
复杂度O(N)O(N)
优点不会消耗内存不阻塞客户端命令
缺点阻塞客户端命令消耗内存
### 配置
1
2
3
4
5
6
7
8
save 900 1
save 300 10
save 60 10000
dbfilename dump.rdb
dir ./
stop-writes-on-bgsave-error yes //bgsave出现问题会停止写入
rdbcompression yes //压缩模式
rdbchecksum yes //对RDB进行校验和检验
#### 最佳配置
1
2
3
4
5
dbfilename dump-${port}.rdb
dir bigdiskpath //选择大的硬盘
stop-writes-on-bgsave-error yes //bgsave出现问题会停止写入
rdbcompression yes //压缩模式
rdbchecksum yes //对RDB进行校验和检验

小结

  • RDB是Redis内存到硬盘的快照,用于持久化
  • save通常会阻塞Redis
  • bgsave不会阻塞Redis,但是会fork子进程
  • save自动配置满足其一就会被执行
  • 有些触发机制不容忽视

RDB问题

耗时耗性能

O(N)数据耗时

fork耗内存

Disk I/O:IO性能

不可控丢失数据

时间戳save
T1执行多个命令
T2满足RDB自动创建条件
T3再次执行多条命令
T4宕机

宕机会发生数据丢失

AOF

三种策略

everysec

always

同everysec流程,只不过always会把每条命令都写入到AOF文件中

no

由操作系统来决定是否刷新

比较

命令alwayseverysecno
优点不丢失数据每秒一次fsync丢1秒数据不用管理
缺点IO开销比较大丢1秒数据不可控
### AOF重写

作用

  • 减少硬盘占用量
  • 加快回复速度

重写两种方式

bgrewriteaof

命令:bgrewriteaof

重写配置

配置名含义
auto-aof-rewirte-min-sizeauto-aof-rewirte-percentage
AOF文件重写尺寸AOF文件增长率
**统计**
统计名含义
auto-current-sizeauto-base-size
AOF当前尺寸AOF上次启动和重写的尺寸
#### 自动触发时机
  • auto-current-size > auto-aof-rewirte-min-size
  • (auto-current-size - auto-base-size) / auto-base-size > auto-aof-rewirte-percentage

AOF重写流程

配置

  • appendonly yes
  • appendfilename “appendonly-${port}.aof”
  • appendfsync everysec
  • dir /bigdisk
  • no-appendfsync-on-rewrite no //aof重写失败是否允许丢失数据
  • auto-aof-rewrite-percentage 100 //增长率
  • auto-aof-rewrite-min-size 64mb //最小尺寸

RDB 和 AOF 抉择

命令RDBAOF
启动优先级
体积
恢复速度
数据安全性丢数据根据策略决定
轻重

故障发现

通过ping/pong消息实现故障发现,不依赖sentinel

主观下线

定义:某个节点认为另外一个节点不可用“偏见”

主观下线流程:

客观下线

定义:当半数以上持有槽的主节点都标记某节点主观下线

客观下线流程:

故障恢复

资格检查

  • 每个从节点检查与故障节点的断线时间
  • 超过cluster-node-timeout*cluster-slave-validity-factor取消资格
  • cluster-slave-validity-factor:默认为10

准备选举时间

选举投票

替换主节点

  • 当前从节点复制变为主节点。(slaveof no one)
  • 执行clusterDelSlot撤销故障主节点负责的槽,并执行clusterAddSlot
    把这些槽分配给自己
  • 向群广播自己的pong消息,表明已替换了故障从节点

redis cluster 开发常见问题

集群完整性

cluster-require-full-coverage默认为yes
问题:

  • 集群中16384个槽全部可用,保证完整性
  • 节点故障转移或正在转移
    大多数业务无法容忍,建议设置为no

    当其中一台机器发生故障,此时集群状态不可用,不可以set ket,不建议设置为yes

宽带消耗

官方建议:1000个节点

  • 消息频率 节点发现和节点最后通信时间超过cluster-node-timeout/2时会发送ping消息
  • 消息数据量 slots数据组(2k)和整个集群1/10的状态数据(10个节点状态数据约1k)
  • 节点部署机器规模 分布机器越多且每台机器划分的节点越均匀,整体的可用带宽越高
    例子:200个节点,20个物理机器(每台10个节点)

cluster-node-timeout=15000 ping/pong带宽约25MB

cluster-node-timeout=20000 ping/pong带低于15MB

优化

  • 避免多业务使用多集群,大业务可以多集群
  • cluster-node-timeout 带宽和故障转移速度的均衡
  • 尽量均匀分配多个机器,保证带宽

PUB/SUB广播

问题:publish在集群每个节点广播:加重带宽
解决:单独走一套redis sentinel

数据倾斜

数据倾斜:内存不均匀

节点和槽分配不均匀

不同槽对应键值数差异大

  • 可能存在has_tag
  • cluster countkeysinslot {slot}获取槽对应键值个数

包含bigkey

  • 例如大字符串,几百万元素的hash,set等
  • 从节点,redis-cli –bigkeys
  • 优化数据结构,拆分key

内存相关配置不一致

  • hash-max-ziplist-value, set-max-intset-entries等

请求倾斜:key热点重要的key或者bigkey
优化:

  • 避免big_key
  • 热键不要使用hash_tag(避免落在一个节点)
  • 当一致性不高时可以使用本地缓存+MQ

读写分离

只读连接:集群模式的从节点不接受任何读写请求

  • 重定向到负责槽的主节点
  • readonly命令可以读:连接级的命令
    读写分离:更加复杂
  • 复制延迟,从节点故障,读取过期数据
  • 修改客户端:cluster slaves {nodeid}

数据迁移

官方工具:redis-trib.rb import

  • 只能从单机迁移到集群
  • 不支持在线迁移,source需要停写
  • 不支持断点续传
  • 单线程迁移,影响速度
    在线迁移:

唯品会:redis-migrate-tool

豌豆荚:redis-port

集群VS单机

集群限制

  • key批量操作限制
  • key事物和lua支持有限,操作的key必须在同一个节点
  • key是数据分区的最小粒度:不支持bigkey分区
  • 不支持多个数据库:集群模式下只有一个db0
  • 复制只支持一层,不支持树形
  1. Redis Cluster: 满足容量和性能的扩展性:很多业务不需要
  2. 很多场景Redis Sentinel足够好

MOVED重定向

槽命中:直接返回

算出key的slot值

1
>127.0.0.1> 6379 cluster keyslot hello

返回结果:

1
(integer) 866

槽不命中:moved异常

算出key的slot值

1
>127.0.0.1> 6379 cluster keyslot php

返回结果:

1
(integer) 9244

看一个小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
redis-cli -c -p 7000    //集群模式
127.0.0.1:7000> cluster keyslot hello
(integer) 866
127.0.0.1:7000> set hello word
OK
127.0.0.1:7000> cluster keyslot php
(integer) 9244
127.0.0.1:7000> set php best
-> Redirected to slot [9244] located at 127.0.0.1:7001
OK
127.0.0.1:7001> get php
"best"
127.0.0.1:7001>
redis-cli -p 7000
127.0.0.1:7000> cluster keyslot php
(integer) 9244
127.0.0.1:7000> set php best
(error) MOVED 9244 127.0.0.1:7001
127.0.0.1:7000>

ASK重定向

在集群缩容扩容的时候,要对槽进行迁移,槽迁移过程中要遍历进行migrate,迁移时间比较长,
此时在此过程中访问一个key,但是key已经迁移到目标节点,就需要一个新的方案来解决这个问题,redis cluster 对这个问题已经有解决方案

我们来看它的一个实现演示:

moved & ask

  • 两者都是客户端重定向
  • moved:槽已确定迁移
  • ask:槽还在迁移中

smart客户端

基本原理

追求性能:

  1. 从集群中选一个可运行的节点,使用cluster slots 初始化槽和节点映射
  2. 将cluster slots结果映射到本地,为每个节点创建redisPool
  3. 执行命令

基本流程:

关于redis cluster 客户端使用可参考
redis-go-cluster

批量操作优化

批量操作怎么实现?meget meset必须在一个槽

串行mget

串行IO

并行IO

hash_tag

四种方案优缺点对比

方案优点缺点网络IO
串行mget编程简单,少量keys满足需求大量keys请求延迟严重O(keys)
串行IO编程简单,少量节点满足需求大量node延迟严重O(nodes)
并行IO利用并行特性,延迟取决于最慢的节点编程复杂,超市定位问题难O(max(node))
hash_tag性能最高读写增加tag维护成本,tag分布易出现数据倾斜O(1))

集群伸缩原理


集群伸缩=槽和数据在节点之间的移动

扩容集群

准备新节点

新节点:

  • 集群模式
  • 配置和其它节点统一
  • 启动后仍是孤儿节点

加入集群

1
2
cluster meet 127.0.0.1 6385
cluster meet 127.0.0.1 6386

加入后的效果

加入集群的作用:

  • 为它迁移槽和数据实现扩容
  • 作为从节点负责故障转移
1
redis-cli --cluster add-node new_host:new_port existing_host:existing_port --cluster-slave --cluster-master-id <arg>

建议使用redis-trib.rb能够避免新节点已经加入了其它集群,造成故障

迁移槽和数据

槽迁移计划:


迁移数据:

  1. 对目标节点发送: cluster setslot{slot} importing {sourceNodeId}命令,让目标节点准备导入槽的数据。
  2. 对源节点发送: cluster setslot{slot} migrating {targetNodeId}命令,让源节点准备迁出槽的数据。
  3. 源节点循环执行: cluster getkeysinslot{slot}{count}命令,每次获取count个属于槽的键。
  4. 在源节点执行: migrate {targetIP}{targetPort} key 0 {timeout}命令把指定key迁移。
  5. 重复执行3-4直到槽下所有数据节点均迁移到目标节点。
  6. 向集群内所有主节点发送cluster setslot{slot} node {targetNodeId}命令,通知槽分配给目标节点。

数据迁移伪python代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def move_slot(source,target,slot):
#目标节点准备导入槽slot
target.cluster("setslot",slot,"importing",source.nodeID)
#目标节点准备全出槽slot
target.cluster("setslot",slot,"migrating",source.nodeID)
while True:
#批量从源节点获取键
keys = source.cluster("getkeysinslot",slot,pipeline_size)
if keys.length ==0:
#键列表为空时,退出循环
break
#批量迁移键到目标节点
source.call("migrate",target.host,target.port,"",timeout,"keys")
#向集群所有主节点通知槽slot被分配给目标节点
for node in nodes:
if node.flag =="slave":
continue
node.cluster("setslot",slot,"node",target.nodeID)

pipline迁移

3.0.6 版本pipline数据迁移会有丢失数据bug,在3.2.8已解决

扩容演示

环境准备


当前集群是三主三从结构,此时我们加入两个新节点7006,7007。7007是7006的从节点,我们需要从7001,7002节点把一部分数据迁移给7006。
配置准备:

1
2
sed 's/7000/7006/g' redis-7000.conf > redis-7006.conf
sed 's/7000/7007/g' redis-7000.conf > redis-7007.conf

meet:

1
2
redis-cli -p 7000 cluster meet 127.0.0.1 7006
redis-cli -p 7000 cluster meet 127.0.0.1 7007

replicate:

1
redis-cli -p 7007 cluster replicate d57d27051ce9db7752f894394b621368f9e0a058

此时7007已经属于7006的从节点

迁移数据:

由于槽数量比较多,所以这里使用redis-trib来迁移

1
redis-cli --cluster reshard 127.0.0.1 7000


此时给我们提示出了当前集群的信息,由于我们现在是4个主节点,所以需要分成四等份来支持向master写入数据

槽迁移后的信息:

收缩集群

下线迁移槽

忘记节点

1
redis-cli>cluster forget {downNodeId}

关闭节点

收缩集群演示

例:下线7006,7007

迁移槽:

迁移过程命令:

redis-cli –cluster reshard –cluster-from {7006nodeid} –cluster-to 7000{7000nodeid} –cluster-slots {slot num} 127.0.0.1:7006

redis-cli –cluster reshard –cluster-from {7006nodeid} –cluster-to 7001{7001nodeid} –cluster-slots {slot num} 127.0.0.1:7006

redis-cli –cluster reshard –cluster-from {7006nodeid} –cluster-to 7002{7002nodeid} –cluster-slots {slot num} 127.0.0.1:7006

迁移到7000示例:

1
redis-cli --cluster reshard --cluster-from d57d27051ce9db7752f894394b621368f9e0a058 --cluster-to 092fd7c3cf19693eddec5c0fae9894d681023ce5 --cluster-slots 1365 127.0.0.1:7006

之后选择yes即可


可观察出0-1364的槽节点以迁移完毕,重复上述步骤,迁移剩余的槽

迁移后:

忘记节点:

1
redis-cli --cluster del-node 127.0.0.1:7000 d57d27051ce9db7752f894394b621368f9e0a058

需要先下线从节点在下线主节点,否则会发生故障转移

完成缩容

呼唤集群

当系统应用需要有更大容量和QPS的支撑,此时就需要采用的集群的方式,也可以简单理解为加机器

数据分区:

分布方式

顺序分布

哈希分布

  • 节点取余
  • 一致性hash
  • 虚拟槽分区

节点取余

  • 客户端分片: 哈希+取余
  • 节点伸缩: 数据节点关系变化,导致数据迁移
  • 迁移数量和添加节点数量相关:建议翻倍扩容

一致性hash

一致性hash扩容

  • 客户端分片:哈希+顺时针(优化取余)
  • 节点伸缩:只影响临近节点,但是还是有数据迁移
  • 翻倍伸缩:保证最小迁移数据和负载均衡

虚拟槽分布

  • 预设虚拟槽:每个槽映射一个数据子集,一般比节点数大
  • 良好的hash函数:如crc16
  • 服务端管理节点,槽,数据:例如redis cluster

对比

分布方式特点典型产品
哈希分布数据分散度高
key,value分布业务无关
无法顺序访问
支持批量操作
一致性hash memcache
redis cluster
其它缓存产品
顺序分布数据分散度易倾斜
key,value业务相关
可顺序访问
支持批量操作
BigTable
HBase
## Redis Cluster

架构

  • 节点
  • meet
  • 指派槽
  • 复制

特性:

  • 复制
  • 分片
  • 高可用

安装

原生安装

1: 配置开启节点:

1
2
3
4
5
6
7
port{$port}
daemonize yes
dir "${redis-src}/data/"
dbfilename "dump-{$port}.rdb"
logfile "{$port}.log"
cluster-enabled "yes
cluster-config-file nodes-{$port}.conf

批量生成配置文件:

执行:

1
2
3
4
5
6
redis-server redis-7000.conf
redis-server redis-7001.conf
redis-server redis-7002.conf
redis-server redis-7003.conf
redis-server redis-7004.conf
redis-server redis-7005.conf

2: meet

cluster meet ip port

1
2
3
4
5
redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7001
redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7002
redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7003
redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7004
redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7005

先进行7000和7001的握手

发现7000和7001已经完成握手,继续meet其他的节点

此时执行cluster nodescluster info均发现6个节点相互关联,证明已经握手成功

3: 指派槽

cluster addslots slot [slot…]

由于一共要分配16384个槽,所以需要借助脚本去分配槽

1
2
3
4
5
6
7
8
start=$1
end=$2
port=$3
for slot in `seq ${start} ${end}`
do
echo "slot:${slot}"
redis-cli -p ${port} cluster addslots ${slot}
done

我们要配置的是三主三从,所以要把16384三等分

1
0-5461 7000 5462-10922 7001 10923-16383 7002

执行以下命令:

1
2
3
sh addslots.sh 0 5461 7000
sh addslots.sh 5462 10922 7001
sh addslots.sh 10923 16383 7002

查看槽分配状态


此时发现16384个槽确实已经分配完毕,槽分配完毕

4: 主从

cluster replicate node-id

给7003分配到master7000主节点上:

1
2
3
redis-cli -p 7003 cluster replicate 6d4942b15eb5e02bb6193453443ccb827c13c6df
redis-cli -p 7004 cluster replicate 916f84dee5fbef724ddf2f90fddf51fd654113f1
redis-cli -p 7005 cluster replicate fee14c1f872fc4c7d451f84a616cf735d220538b

主从分配结果:

官方工具

由于原生安装过程比较麻烦,又容易出错,所以正常的生产环境使用官方工具安装,但是掌握原生安装的方式更容易让我们理解集群分配的原理

ruby环境准备

  1. 下载编译安装ruby
  2. 安装rubygem redis
  3. 安装redis-trib.rb

1: 配置开启节点:

1
2
3
4
5
6
7
port{$port}
daemonize yes
dir "${redis-src}/data/"
dbfilename "dump-{$port}.rdb"
logfile "{$port}.log"
cluster-enabled "yes
cluster-config-file nodes-{$port}.conf

执行:

1
2
3
4
5
6
redis-server redis-7000.conf
redis-server redis-7001.conf
redis-server redis-7002.conf
redis-server redis-7003.conf
redis-server redis-7004.conf
redis-server redis-7005.conf

2: 集群创建

1
2
//1 表示1个主节点分配1个从节点
redis-cli --cluster create --cluster-replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005

上图分别展示了槽分配,主从和节点信息,符合预期执行yes即可

分配成功信息:

集群验证:

当然如果维护上百台集群显然也不是最好的方式,可以借助或构建云平台来管理集群

Redis Sentinel基本架构

  1. 多个sentinel发现并确认master有问题
  2. 选举出一个sentinel作为领导
  3. 选出一个salve作为master
  4. 通知其余slave成为新的master的slave
  5. 通知客户端主从变化
  6. 等待master复活成为新的master的slave

安装与配置

  1. 配置开启主从节点
  2. 配置开启sentinel监控master节点

Redis主节点
启动redis-server redis-7000.conf

redis配置:

主节点:

1
2
3
4
5
port 7000
daemonize yes
pidfile /var/run/redis/7000.pid
logfile 7000.log
dir "${redis-src}/data/"

从节点:

1
2
3
4
5
6
port 7001
daemonize yes
pidfile /var/run/redis/7001.pid
logfile 7001.log
dir "${redis-src}/data/"
slaveof 127.0.0.1 7000
1
2
3
4
5
6
port 7002
daemonize yes
pidfile /var/run/redis/7002.pid
logfile 7002.log
dir "${redis-src}/data/"
slaveof 127.0.0.1 7000

sentinel配置:

1
2
3
4
5
6
7
port ${port}
dir "${redis-src}/data/"
logfile ${port}.log
sentinel monitor mymaster 127.0.0.1 7000 2 #监控主节点的名字 2对应当两个sentinel发现主节点有问题就发生故障转移
sentinel down-after-millisenconds mymaster 30000 #ping30秒后不通认为有问题
sentinel parallel-sync mymaster 1
sentinel failover-timeout mymaster 180000

安装演示

配置redis

单机演示实际为能相互ping通的多台机器

1:创建master节点配置

2:创建7000,7001节点配置


3:查看主从状态

到此为止主从的配置搭建完毕了

配置sentinel

通过官方提供的配置模板导入sentinel配置


配置信息:

查看Sentinel监控状态:

到此我们可以发现Sentinel已经检测到了matser节点的主从信息,由于我们只启动一个Sentinel所以Sentinel发现的数目为1

再去看看redis-sentinel-26379.conf的变化:

发现他已经把7000的两个slave节点的信息配置自动写入到了配置文件中

生成另外两个Sentinel配置

查看sentinel状态

发现26380的Sentinel并没有发现其他节点

查看原因:

1
2
3
cat /var/log/redis/26379.log
cat /var/log/redis/26380.log
cat /var/log/redis/26381.log


我们发现三个节点的pid都是一样的,所以需要配置pid
编辑26379,26380,26381.conf 去掉配置文件自动生成的myid,并加入

1
2
3
pidfile "/var/run/redis/redis-sentinel-26379.pid"
pidfile "/var/run/redis/redis-sentinel-26380.pid"
pidfile "/var/run/redis/redis-sentinel-26381.pid"

状态已正常

go客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main

import (
"fmt"
"github.com/gomodule/redigo/redis"
"github.com/letsfire/redigo"
"github.com/letsfire/redigo/mode"
"github.com/letsfire/redigo/mode/sentinel"
"strconv"
"time"
"log"
)

func main(){
var (
i int
sentinelMode mode.IMode
)
sentinelMode = sentinel.New(
sentinel.Addrs([]string{"127.0.0.1:26379", "127.0.0.1:26380", "127.0.0.1:26381"}),
sentinel.PoolOpts(
mode.MaxActive(20), // 最大连接数,默认0无限制
mode.MaxIdle(0), // 最多保持空闲连接数,默认2*runtime.GOMAXPROCS(0)
mode.Wait(false), // 连接耗尽时是否等待,默认false
mode.IdleTimeout(0), // 空闲连接超时时间,默认0不超时
mode.MaxConnLifetime(0), // 连接的生命周期,默认0不失效
mode.TestOnBorrow(nil), // 空间连接取出后检测是否健康,默认nil
),
sentinel.DialOpts(
redis.DialReadTimeout(time.Second), // 读取超时,默认time.Second
redis.DialWriteTimeout(time.Second), // 写入超时,默认time.Second
redis.DialConnectTimeout(time.Second), // 连接超时,默认500*time.Millisecond
redis.DialPassword(""), // 鉴权密码,默认空
redis.DialDatabase(0), // 数据库号,默认0
redis.DialKeepAlive(time.Minute*5), // 默认5*time.Minute
redis.DialNetDial(nil), // 自定义dial,默认nil
redis.DialUseTLS(false), // 是否用TLS,默认false
redis.DialTLSSkipVerify(false), // 服务器证书校验,默认false
redis.DialTLSConfig(nil), // 默认nil,详见tls.Config
),
// 连接哨兵配置,用法于sentinel.DialOpts()一致
// 默认未配置的情况则直接使用sentinel.DialOpts()的配置
// sentinel.SentinelDialOpts()
)

var instance = redigo.New(sentinelMode)

for {
res, err := instance.String(func(c redis.Conn) (res interface{}, err error) {
return c.Do("set", "test" + strconv.Itoa(i), i)
})
if err != nil {
log.Println(err)
} else {
fmt.Println(res)
}
i++
time.Sleep(1 * time.Second)
}
}

执行以下命令,并模拟7000端口宕机

1
go run client.go

执行结果:


日志分析

查看7001.log

从日志中发现选举7002为master,执行redis-cli -p 7002 info replication验证下

看看sentinel日志的变化

故障转移

三个定时任务

  1. 每10秒每个sentinel对master和slave执行info
  • 发现slave节点
  • 确定主从关系
  1. 每2秒每个sentinel通过master节点的channel节点交换信息(pub/sub)
  • 通过__sentinel__和:hello频道交互
  • 交互对节点的看法和自身的信息
  1. 每1秒每个Sentinel对其它Sentinel和Redis执行ping
  • 失败判定依据,心跳检测

主观下线和客观下线

  • 主观下线:每个sentinel节点对Redis节点失败的偏见
  • 客观下线:所有sentinel节点对Redis节点失败达成共识(quorum:建议节点数/2+1)

领导者选举

  • 原因:只有一个sentinel节点完成故障转移
  • 选举:通过sentinel is-master-down-by-addr命令都希望成为领导者
  1. 每个主观下线的Sentinel节点向其他Sentinel节点发送命令,要求它设置为领导者
  2. 收到命令的Sentinel节点如果没有同意通过其他Sentinel节点发送的命令,那么该同意将被拒绝
  3. 如果该Sentinel节点发现通过的票数已经超过Sentinel集合半数且超过quorum,那么它将成为领导者
  4. 如果此过程中有多个Sentinel节点成为领导者,那么将等待一段时间重新选举

故障转移

  1. 从slave节点选出一个”合适的”节点作为新的master节点
  2. 对上面的slave节点执行slaveof no one 命令让其成为master节点
  3. 向剩余的slave节点发送命令,让他们成为新master节点的slave节点,复制规则和parallel-syncs参数有关
  4. 更新原来的master节点并配置为slave,并保持对其”关注”,当其恢复后,命令他去复制新的master节点

选择”合适的”slave节点

  1. 选择slave-priority(slave节点优先级)最高的slave节点。如果存在则返回,不存在则继续
  2. 选择复制偏移量最大的slave节点(复制的最完整)如果存在则返回,不存在则继续
  3. 选择runid最小的slave节点

hexo部署流程

  • 开启本地web服务:
    1
    hexo s
  • 生成静态文件:
    1
    hexo g
  • 文件压缩:
    1
    gulp
  • 文件部署到远程服务器:
    1
    hexo d

gulp安装:

安装 gulp

使用 npm install xxx –save命令分别安装如下工具

1
2
3
4
5
6
"gulp": "^3.9.1",
"gulp-htmlclean": "^2.7.6",
"gulp-htmlmin": "^1.3.0",
"gulp-imagemin": "^2.4.0",
"gulp-minify-css": "^1.2.4",
"gulp-uglify": "^1.5.3",

建立 gulpfile.js 文件

在 Hexo 的根目录建立 gulpfile.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
var gulp = require('gulp');
var minifycss = require('gulp-minify-css');
var uglify = require('gulp-uglify');
var htmlmin = require('gulp-htmlmin');
var htmlclean = require('gulp-htmlclean');
var imagemin = require('gulp-imagemin');

// 压缩html
gulp.task('minify-html', function() {
return gulp.src('./public/**/*.html')
.pipe(htmlclean())
.pipe(htmlmin({
removeComments: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
}))
.pipe(gulp.dest('./public'))
});
// 压缩css
gulp.task('minify-css', function() {
return gulp.src('./public/**/*.css')
.pipe(minifycss({
compatibility: 'ie8'
}))
.pipe(gulp.dest('./public'));
});
// 压缩js
gulp.task('minify-js', function() {
return gulp.src('./public/js/**/*.js')
.pipe(uglify())
.pipe(gulp.dest('./public'));
});
// 压缩图片
gulp.task('minify-images', function() {
return gulp.src('./public/images/**/*.*')
.pipe(imagemin(
[imagemin.gifsicle({'optimizationLevel': 3}),
imagemin.jpegtran({'progressive': true}),
imagemin.optipng({'optimizationLevel': 7}),
imagemin.svgo()],
{'verbose': true}))
.pipe(gulp.dest('./public/images'))
});
// 默认任务
gulp.task('default', [
'minify-html','minify-css','minify-js','minify-images'
]);