MQ知识整理

1,253 阅读18分钟

1 Mafka和Kafka的区别,大公司为什么需要自研

这个原因参考# 消息中间件:为什么我们选择 RocketMQ 像美团这样规模的公司选择自研自己的消息中间件,主要有以下原因: 从公司的角度

  1. 技术成本 总而言之如果每个使用消息中间件的业务方如果都自己需要关注部署,运维,都要时刻小心部署的开源中间件的稳定性,因为出的问题也只能自己背。那么业务团队就没有足够的精力来做好自己的业务开发。

这种模式当然也有坏处,就是如果不是平时有时间钻研和总结。业务团队对中间件实现原理不是很了解,对技术人员自己不利

  1. 硬件成本 每个业务方都自建集群,每个团队都花很多时间在运维上,浪费的都是公司的钱

  2. 人力成本 如果公司基础设施做的比较好,那么业务方可以专注于自己的领域开发。招聘可以有所侧重,不需要对中间件有特别深入的了解。反之就是要求候选人啥都要会。。。

从中间件的角度

  1. 稳定性 这个不用多说,中间件提供SLA,出事背锅的是中间件团队
  2. 功能支持 比如地域敏感和批量消费,单消费者实例可以设置多线程并行加快消费这样的featrue
  3. 性能
  4. 用户友好 管理平台操作更友好
  5. 监控告警
  6. 可运维性

区别,这个等待有机会补充 不过可以看看RocketMQ和Kafka之间的区别

2 kafka的一些概念

下面给出 Kafka 一些重要概念,让大家对 Kafka 有个整体的认识和感知,后面还会详细的解析每一个概念的作用以及更深入的原理:

  • Producer:消息生产者,向 Kafka Broker 发消息的客户端。
  • Consumer:消息消费者,从 Kafka Broker 取消息的客户端。
  • Consumer Group:消费者组(CG),消费者组内每个消费者负责消费不同分区的数据,提高消费能力。一个分区只能由组内一个消费者消费,消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
  • Broker:一台 Kafka 机器就是一个 Broker。一个集群由多个 Broker 组成。一个 Broker 可以容纳多个 Topic。
  • Topic:可以理解为一个队列,Topic 将消息分类,生产者和消费者面向的是同一个 Topic。
  • Partition:为了实现扩展性,提高并发能力,一个非常大的 Topic 可以分布到多个 Broker (即服务器)上,一个 Topic 可以分为多个 Partition,每个 Partition 是一个 有序的队列。
  • Replica:副本,为实现备份的功能,保证集群中的某个节点发生故障时,该节点上的 Partition 数据不丢失,且 Kafka 仍然能够继续工作,Kafka 提供了副本机制,一个 Topic 的每个分区都有若干个副本,一个 Leader 和若干个 Follower。
  • Leader:每个分区多个副本的“主”副本,生产者发送数据的对象,以及消费者消费数据的对象,都是 Leader。
  • Follower:每个分区多个副本的“从”副本,实时从 Leader 中同步数据,保持和 Leader 数据的同步。Leader 发生故障时,某个 Follower 还会成为新的 Leader。
  • Offset:消费者消费的位置信息,监控数据消费到什么位置,当消费者挂掉再重新恢复的时候,可以从消费位置继续消费。
  • ZooKeeper:Kafka 集群能够正常工作,需要依赖于 ZooKeeper,ZooKeeper 帮助 Kafka 存储和管理集群信息。

3 kafka写的过程

Producer发送消息到broker时,会根据Paritition机制选择将其存储到哪一个Partition。如果Partition机制设置合理,所有消息可以均匀分布到不同的Partition里,这样就实现了负载均衡。用户可以通过指定parition key来改变默认的分区分配规则。 kafka写

一个Topic的消息分为几个分区,不同分区散部在不同broker上,每个分区都有自己的分区首领和副本,生产者实例只会向分区首领写入消息。然后分区副本会通过异步的方式同步分区首领中的分区数据。

写入并复制

acks

这里有个重要的参数是 acks

acks 参数指定了要有多少个分区副本接收消息,生产者才认为消息是写入成功的。此参数对消息丢失的影响较大.

  • acks = 0 生产者不关心发送是否成功,他也不知道,这就类似于 UDP 的运输层协议,只管发,服务器接受不接受它也不关心。
  • acks = 1,只要集群的 Leader 接收到消息,就会给生产者返回一条消息,告诉它写入成功。如果发送途中造成了网络异常或者 Leader 还没选举出来等其他情况导致消息写入失败,生产者会受到错误消息,这时候生产者往往会再次重发数据。因为消息的发送也分为 同步异步,Kafka 为了保证消息的高效传输会决定是同步发送还是异步发送。如果让客户端等待服务器的响应(通过调用 Future 中的 get() 方法),显然会增加延迟,如果客户端使用回调,就会解决这个问题。
  • acks = all,只有当所有参与复制的节点都收到消息时,生产者才会接收到一个来自服务器的消息。

4 kafka读的过程

消费的规则

消费的规则

  1. 一个Topic的消息只能被相同消费组下的消费者实例消费,这种情况为P2P模型(单播)
  2. Topic消息可以被多个消费组消费,不互相影响,这种情况为PUB/SUB模型(多播)
  3. 同一个partition不能被多个消费者实例线程同时消费

kafka每个Topic下有多个分区,每个分区只能由相同消费组的一个消费者实例的一个线程进行消费(一个消费者可以多线程,多线程情况下消息不保序,这样在Kafka也不支持,美团Mafka支持)

下面简单化只有一个消费组的情况,消费者线程如何读取多个分区的数据。

一个消费者消费的情况

两个消费者消费的情况

消费者数量超过分区数,多余的消费者会被闲置

如果你的分区数是N,那么最好线程数也保持为N,这样通常能够达到最大的吞吐量。超过N的配置只是浪费系统资源,因为多出的线程不会被分配到任何分区。

Rebalance 消费者重平衡

image.png

重平衡非常重要,它为消费者群组带来了高可用性 和 伸缩性,在重平衡期间消费者组中的消费者实例都会停止消费,等待重平衡的完成,并且重平衡的这个过程很慢。

消费者通过向组织协调者(Kafka Broker)发送心跳来维护自己是消费者组的一员并确认其拥有的分区。如果过了一段时间 Kafka 停止发送心跳了,会话(Session)就会过期,组织协调者就会认为这个 Consumer 已经死亡,就会触发一次重平衡。

5 分区副本的角色与选主

kafka为每个分区可以配置多个副本,这些副本分散在不同的broker(机器节点)上。这些副本存在的作用显然是为了通过冗余备份增加数据的可靠性,类似RAID阵列。

分区副本之间如何实现数据同步的呢,kafka的实现是基于领导者(Leader-based)的副本机制。也就是说分区副本的角色分为两种,分区首领(Leader Replica)和 分区平民(Follower Replica)。每个分区在创建时都要选举一个副本为分区首领,其余的副本自动为分区平民。

5.1 分区的分配

分配的原则是尽量将副本分配到多个broker上。 kafka副本概念图 分配的过程

  1. 将所有Broker(假设共n个Broker)和待分配的Partition排序
  2. 将第i个Partition分配到第(i mod n)个Broker上 (这个就是leader)
  3. 将第i个Partition的第j个Replica分配到第((i + j) mode n)个Broker上 这部分的思想可以参考一致性哈希算法 # 漫画:什么是一致性哈希?

只有分区首领才可以进行生产和消费活动。也就是说客户端(包括生产者和消费者)只能和分区首领交互。当分区首领所在节点挂了,kafka通过在剩下的分区副本中重新选出分区首领提高系统可用性。

5.2 选主机制

ISR(in-sync replicas) 副本同步队列

Kafka在Zookeeper中动态维护了一个ISR(in-sync replicas 副本同步队列),这个ISR里的所有Replica都跟上了leader,只有ISR里的成员才有被选为Leader的可能。我们首先要明确的是,Leader 副本天然就在 ISR 中。也就是说,ISR 不只是追随者副本集合,它必然包括 Leader 副本。甚至在某些情况下,ISR 只有 Leader 这一个副本。

基于领导者的副本机制

如何选举

最简单的选选举算法就是ISR每个副本都参与竞选,成功竞选的为分区首领,其他的自动为追随者 Follower。所有Follower都在Zookeeper上设置一个Watch,一旦Leader宕机,其对应的ephemeral znode会自动删除,此时所有Follower都尝试创建该节点,而创建成功者(Zookeeper保证只有一个能创建成功)即是新的Leader,其它Replica即为Follower。这种做法有以下问题:

  1. split-brain 脑裂
  2. herd effect 羊群效应
  3. Zookeeper负载过重 每个Replica都要为此在Zookeeper上注册一个Watch,当集群规模增加到几千个Partition时Zookeeper负载会过重。

由于这些问题该方案不可行,Kafka的做法是在所有broker中选出一个controller,所有Partition的Leader选举都由controller决定。同时controller也负责增删Topic以及Replica的重新分配。

Kafka集群controller的选举

每个Broker都会在Controller Path (/controller)上注册一个Watch。

当前Controller失败时,对应的Controller Path会自动消失(因为它是ephemeral Node),此时该Watch被fire,所有“活”着的Broker都会去竞选成为新的Controller(创建新的Controller Path),但是只会有一个竞选成功(这点由Zookeeper保证)。

竞选成功者即为新的Leader,竞选失败者则重新在新的Controller Path上注册Watch。因为Zookeeper的Watch是一次性的,被fire一次之后即失效,所以需要重新注册。

Kafka 分区领导的选举

  • 从Zookeeper中读取当前分区的所有ISR(in-sync replicas)集合
  • 调用配置的分区选择算法选择分区的leader

5 如何保证消息不被重复消费,或者说,如何保证消息消费的幂等性?

幂等(Idempotence)本来是一个数学上的概念,它是这样定义的:

如果一个函数 f(x) 满足:f(f(x)) = f(x),则函数 f(x) 满足幂等性。

这个概念被拓展到计算机领域,被用来描述一个操作、方法或者服务。一个幂等操作的特点是,其任意多次执行所产生的影响均与一次执行的影响相同。

美团内部消息队列组件有消息回溯的功能,这种特定使用场景下打破了exactly once的语义

Kafka提供以下三种交付语义:

  • At most once——消息可能会丢失但绝不重传。
  • At least once——消息可以重传但绝不丢失。
  • Exactly once——这正是人们想要的, 每一条消息只被传递一次.

用 MQ 有个基本原则,就是数据不能多也不能少。 假设在计费场景,多了就会导致重复计费,客户就会来投诉,少了就少计费,公司损失。

Kafka 默认保证 At least once,这种情况下如果用户想要实现Exactly once这种语义需要借助类似mysql这种外部系统来做。

At least once + 幂等消费 = Exactly once。 kafka提供的服务质量是At least once,用户最好从业务逻辑设计上入手,将消费的业务逻辑设计成具备幂等性的操作。
1.利用数据库的唯一约束实现幂等
只要是支持类似“INSERT IF NOTEXIST”语义的存储类系统都可以用于实现幂等,比如,你可以用 Redis 的 SETNX 命令来替代数据库中的唯一约束,来实现幂等消费。
2. 为更新的数据设置前置条件
“将账户 X 的余额增加 100 元”这个操作并不满足幂等性,我们可以把这个操作加上一个前置条件,变为:“如果账户 X 当前的余额为 500 元,将余额加100 元”,这个操作就具备了幂等性。类似于CAS
3.记录并检查操作
在执行数据更新操作之前,先检查一下是否执行过这个更新操作。具体的实现方法是,在发送消息时,给每条消息指定一个全局唯一的 ID,消费时,先根据这个 ID 检查这条消息是否有被消费过,如果没有消费过,才更新数据,然后将消费状态置为已消费。

kafka消费示意图

6 如何保证消息不丢失?

image.png

kafka在生产端,broker端和消费端都有可能导致数据丢失。

1. 生产阶段
生产阶段,如果是同步发送,则需要在异常catch块中处理发送失败。如果是异步消费,则在回调函数中处理发送失败,不断重试直至发送成功。只要Producer收到来自Broker的ACK信息,可以认为在生产端没有消息丢失的情况。

// 同步消息发送
try {
    RecordMetadata metadata = producer.send(record).get();
    System.out.println("消息发送成功。");
} catch (Throwable e) {
    System.out.println("消息发送失败!");
    System.out.println(e);
}
// 异步消息发送
producer.send(record, (metadata, exception) -> {
    if (metadata != null) {
        System.out.println("消息发送成功。");
    } else {
        System.out.println("消息发送失败!");
        System.out.println(exception);
    }
});

2.存储阶段
在存储阶段正常情况下,只要Broker在正常运行,就不会出现丢消息的问题;但是如果Broker出现故障,比如进程死掉或者服务器宕机,还是可能会丢失消息的。所以此时一般是要求起码设置如下 4 个参数:

  • 给 topic 设置 replication.factor 参数:这个值必须大于 1,要求每个 partition 必须有至少 2 个副本。
  • 在 Kafka 服务端设置 min.insync.replicas 参数:这个值必须大于 1,这个是要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 吧。
  • 在 producer 端设置 acks=all :这个是要求每条数据,必须是写入所有 replica 之后,才能认为是写成功了
  • 在 producer 端设置 retries=MAX (很大很大很大的一个值,无限次重试的意思):这个是要求一旦写入失败,就无限重试,卡在这里了。 我们生产环境就是按照上述要求配置的,这样配置之后,至少在 Kafka broker 端就可以保证在 leader 所在 broker 发生故障,进行 leader 切换时,数据不会丢失。

3.消费阶段
消费阶段采用和生产阶段类似的确认机制来保证消息的可靠传递,客户端从 Broker 拉取消息后,执行用户的消费业务逻辑,成功后,才会给 Broker 发送消费确认响应。如果 Broker 没有收到消费确认响应,下次拉消息的时候还会返回同一条消息,确保消息不会在网络传输过程中丢失,也不会因为客户端在执行消费逻辑中出错导致丢失。

在编写消费代码时需要注意的是:

  • 不要在收到消息后就立即发送消费确认,而是应该在执行完所有消费业务逻辑之后,再发送消费确认
  • 关闭自动提交、

7 Kafka Controller 控制器

控制器组件(Controller),是 Apache Kafka 的核心组件。它的主要作用是在 ApacheZooKeeper 的帮助下管理和协调整个 Kafka 集群。集群中任意一台 Broker 都能充当控制器的角色,但是,在运行过程中,只能有一个 Broker 成为控制器,行使其管理和协调的职责。

Kafka 当前选举控制器的规则是:第一个成功创建zk /controller 节点的 Broker 会被指定为控制器。

控制器的职责

  1. 主题管理(创建、删除、增加分区)
  2. 分区重分配
  3. Preferred领导者选举。 为了避免部分 Broker 负载过重而提供的一种换Leader 的方案。
  4. 集群成员管理(新增 Broker、Broker 主动关闭、Broker 宕机)
  5. 数据服务。控制器上保存了最全的集群元数据信息,其他所有 Broker 会定期接收控制器发来的元数据更新请求,从而更新其内存中的缓存数据。

image.png

7.1 控制器故障转移 Failover

最开始时,Broker 0 是控制器。当 Broker 0 宕机后,ZooKeeper 通过 Watch 机制感知到并删除了 /controller 临时节点。之后,所有存活的 Broker 开始竞选新的控制器身份。Broker 3 最终赢得了选举,成功地在 ZooKeeper 上重建了 /controller 节点。之后,Broker 3 会从 ZooKeeper 中读取集群元数据信息,并初始化到自己的缓存中。至此,控制器的 Failover 完成,可以行使正常的工作职责了。 image.png

8 Kafka高性能设计

抛开kafka不谈,在linux框架内,高性能设计离不开两个方面 计算IO

计算部分

  1. 让更多的核来参与计算:比如用多线程代替单线程、用集群代替单机等。
  2. 减少计算量:比如用索引来取代全局扫描、用异步代替同步、通过限流来减少请求处理量、采用更高效的数据结构和算法等。

IO部分

  1. 加快 IO 速度:比如用磁盘顺序写代替随机写、用 NIO 代替 BIO、用性能更好的 SSD 代替机械硬盘等。
  2. 减少 IO 次数或者 IO 数据量:比如借助系统缓存或者外部缓存、通过零拷贝技术减少 IO 复制次数、批量读写、数据压缩等。

那么我们再来看kafka做了哪些事情保证高性能。 image.png

消息压缩, 消息压缩可以节省数据传输量,减少网络传输带宽,提高吞吐。
高效序列化, 用户可以根据实际情况选用快速且紧凑的序列化方式(比如 ProtoBuf、Avro)来减少实际的网络传输量以及磁盘存储量,进一步提高吞吐量
image.png 内存池复用,producer在发送消息之前,现在本地内存攒下batch的数据再发送,本地内存池可以复用,避免JVM垃圾回收。
计算前置,有点移动边缘计算的思想,传统的数据库或者消息中间件都是想办法让 Client 端更轻量,将 Server 设计成重量级。kafka在将消息发送给 Broker 之前,需要先在 Client 端 完成大量的工作,例如: 消息的分区路由、校验和的计算、压缩消息等。 这样便 很好地分摊 Broker 的计算压力
IO多路复用,最高效的IO模型,可以复用一个线程去处理大量的 Socket 连接,从而保证高性能。
磁盘顺序写,因为顺序写入,大大节省磁盘寻道和盘片旋转的时间,因此性能提升了 3 个数量级。 image.png Page Cache,Page Cache 缓存的是最近会被使用的磁盘数据,利用的是「时间局部性」原理,依据是:最近访问的数据很可能接下来再访问到。而预读到 Page Cache 中的磁盘数据,又利用了「空间局部性」原理,依据是:数据往往是连续访问的。Kafka读取和写入都契合这两个原理。 image.png 分区分段结构,kafka每个Topic一个文件夹,文件夹下有一个索引文件和一个日志文件。日志文件存储kafka消息,以顺序写的方式追加,每个segment日志文件以该segment第一条消息的offset命名并以“.kafka”为后缀,如图中的topic/34477849968.kafka。另外会有一个索引文件,它标明了每个segment下包含的log entry的offset范围,如图中的Active Segment List。

kafka这种设计类似于log4j日志文件,顺序写滚动,并设置过期策略,满足策略的历史文件将会清掉。如果只有一个文件显然不符合Kafka顺序写的思路,并且查找不便。 image.png image.png 稀疏矩阵,kafka将日志文件中的消息划分成若干block,然后建立稀疏哈希索引。这样在空间和时间性能上取得了平衡。 image.png mmap,kafka利用直接内存映射读写方式(mmap)使得文件的读写性能非常快。
零拷贝技术,零拷贝技术避免了数据在用户态和内核态之间来回拷贝,提升了IO的性能。 image.png 批量拉取,和消息生产者一样,消息消费也是批量拉取的。

参考链接

# Kafka 架构原理解析
# Kafka 设计解析(一):Kafka 背景及架构介绍 # 《吃透MQ系列》之 Kafka 精妙的高性能设计