对于Kafka的基本认识

  • 在项目中使用到了Kafka作为消息队列中间件以同步容器状态,但是对于为什么使用Kafka,以及与其他的消息队列中间件有什么区别不甚了解,因此撰写此文,作为日后学习Kafka的入门文章

消息队列的简介

消息队列的基本功能

  • 解耦

    • 各个服务之间不是直接的调用关系,而是使用消息队列进行解耦,消息的生产者不用关系消息如何被消费,消费者也不用关心消息如何被生产

      • 可以联想下线程池中的阻塞任务队列
  • 异步

    • 对于一些同步操作,比较耗费时间,导致响应会比较慢,如果不要求强实时性,就可以使用消息队列将其转为异步执行的任务

      • 比如短信服务、邮件服务等等
    • 除了考虑到同步操作耗时之外,还有一些天然的异步动作,比如项目中的容器状态同步,需要使用到消息队列来维持
  • 削峰

    • 相当于做了一个蓄水池,请求比较少时平安无事,请求激增时使用消息队列缓存请求(请求可以快速的返回),消费系统再去拉取消息进行处理

      • 和限流算法中的令牌桶算法比较类似

消息队列的缺点

  • 降低系统的可用性:系统引入的外部依赖越多,越容易挂掉
  • 系统复杂度提高使用 MQ 后可能需要处理消息没有被重复消费(消息队列的幂等性问题)、消息丢失的情况,保证消息传递的顺序性等等问题

    • 消息有序性
    • 消息丢失
    • 消息重复消费

消息队列的两个实现规范

JMS
  • JMS(Java Message Service)是 Java 的消息服务,可以视作一套API标准
  • 支持队列模型与发布订阅模型
  • ActiveMQ 就是基于 JMS 规范实现的
AMQP
  • AMQP,即 Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准 高级消息队列协议(二进制应用层协议),是应用层协议的一个开放标准,为面向消息的中间件设计,兼容 JMS基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件产品类型,开发语言等条件的限制

    • 一套应用层协议而非API
    • 跨语言与平台
    • 仅支持二进制数据

消息队列系统的几个关键的问题

消息队列中的消息大量积压
  • 可能的原因就是消费端挂掉了,导致大量数据存储在消息队列不能被消费

    • 首先恢复consumer的问题,保证其可用
    • 新建一个topic Partition设置为正常的十倍(视情况而定)

      • 重新投递并消费是必要的,但是数据量比较大,所以需要设置一个Partition扩大的topic
    • 临时写一个consumer,消费积压的数据,然后重新投递到新的topic中,主要的就是保证consumer快速消费,而不像普通的consumer可能做一些比较耗时的操作
    • 临时加服务器扩容consumer(也为之前的十倍,视情况而定)消费对应的topic中的数据
    • 消费完毕后,进行可能的架构升级以保证后续不再发生消费端挂掉的情况
消息丢失
  • 这里指的是因为消息积压带来的消息丢失,手动写临时的程序,查出丢失的程序,再将其投喂到消息系统中

    • 后面有避免消息丢失的三个维度的方案

Kafka简介

  • Kafka是由LinkedIn开发的一个分布式的基于发布/订阅模型的消息系统,使用Scala编写,它以可水平扩展高吞吐率(高性能)而被广泛使用,同时支持消息持久化。Kafka并不是JMS或AMQP任一标准的实现,Kafka自成标准

    • 水平扩展:支持broker的添加与topic的Partition的扩展
    • 高吞吐率,生产者将消息push到Partition的方式是追加的方式,是磁盘的顺序IO,性能比较高,并且由于同一主题可以有多个Partition,因此吞吐率高
  • Kafka对消息保存时根据 Topic 进行归类,发送消息者称为 Producer,消息接受者称为 Consumer,此外 Kafka集群有多个 Kafka 实例组成,每个实例(server)称为 broker

常见的使用场景

消息系统

  • Kafka与传统的消息中间件都具备系统解耦冗余存储流量削峰缓冲异步通信扩展性可恢复性等功能。与此同时,Kafka还提供了大多数消息系统难以实现的消息顺序性保障及回溯性消费的功能
  • 早期的时候 Kafka 并不是一个合格的消息队列,这也和 LinkedIn 最早开发 Kafka 用于处理海量的日志有很大关系,并不是作为消息队列使用的,随着后续的发展,这些短板都被 Kafka 逐步修复完善

存储系统

  • Kafka把消息持久化到磁盘,相比于其他基于内存存储的系统而言,有效的降低了消息丢失的风险。这得益于其消息持久化多副本机制。也可以将Kafka作为长期的存储系统来使用,只需要把对应的数据保留策略设置为“永久”或启用主题日志压缩功能

流式处理平台

  • Kafka为流行的流式处理框架提供了可靠的数据来源,还提供了一个完整的流式处理框架,比如窗口、连接、变换和聚合等各类操作

Website activity tracking

Kafka可以作为网站活性跟踪的最佳工具;可以将网页/用户操作等信息发送到Kafka中,并实时监控,或者离线统计分析等

Metrics

Kafka通常被用于可操作的监控数据。这包括从分布式应用程序来的聚合统计用来生产集中的运营数据提要

Log Aggregation

Kafka的特性决定它非常适合作为日志收集中心;application可以将操作日志批量异步的发送到Kafka集群中,而不是保存在本地或者DB中;Kafka可以批量提交消息/压缩消息等,这对producer端而言,几乎感觉不到性能的开支,此时consumer端可以是hadoop等其他系统化的存储和分析系统

Kafka的安装与使用方法

Kafka基础

队列模型与发布订阅模型

队列模型(一对一)
  • 使用队列维护消息,只能保证一个消息被一个消费者消费,当需要将特定的消息分发给多个消费者时,则不合适了,可以使用线程池中的BlockingQueue来理解队列模型

    • 比如有多个消费者连接了同一个消息队列,当生产者发送多条消息A B C D后,多个Consumer可能分别只接受到其中一个消息,这个过程是不受控的,因此队列模型更适用于一对一的消息发布
发布订阅模型
  • 通过订阅主题的方式实现消息向多个消费者的分发
  • 在发布 - 订阅模型中,如果只有一个订阅者,那它和队列模型就基本是一样的了。所以说,发布 - 订阅模型在功能层面上是可以兼容队列模型的
  • 要注意的是Kafka的发布订阅模型中没有队列这个概念(即消息传递通道的角色),只有Partition的概念(实际上Partition用来支持某主题对应的消息通道的水平扩展)

Kafka使用的发布订阅模型

  • 常见的架构图

    image-20210711211452355

    image-20210711215444240

    <img src="https://images.demoli.xyz/image-20210803211115318.png" alt="image-20210803211115318" style="zoom:67%;" />

    • 无论是 Kafka 集群,还是 consumer 都依赖于 Zookeeper 集群保存一些 meta 信息, 来保证系统可用性,不过在Kafka的2.8版本就已经开始尝试抛弃Zookeeper了

      • Zookeeper 是 Kafka 用来负责集群元数据管理、控制器选举等操作的
      • 关于Kafka2.8版本抛弃Zookeeper的情况,可参考这篇文章

基础概念

image-20210711214709158

Broker
  • Kafka集群包含一个或多个服务器,这种服务器被称为broker。broker不维护数据的消费状态,直接使用磁盘进行消息存储,线性读写,速度快:避免了数据在JVM内存和系统内存之间的复制,减少耗性能的创建对象和垃圾回收
Cluster Controller
  • 若干个 Broker 组成一个 集群其中集群内某个 Broker 会成为集群控制器,它负责管理集群,包括分配Partition到 Broker、监控 Broker 故障等
Producer
  • 负责发布消息到Kafka broker
  • producer 采用推(push)模式将消息发布到 broker,每条消息都被追加(append)到Partition(patition)中,属于顺序写磁盘(顺序写磁盘效率比随机写内存要高,保障 Kafka 吞吐率)

image-20210711215844963

  • 每个 Partition 中的消息都是 有序 的,生产的消息被不断追加到 Partition log 上,其中的每一个消息都被赋予了一个唯一的 offset

    • 注意是同一个Partition内的消息是有序的,同一个主题的多个Partition之间不是有序的
    • 消费者消费的位置由offset指定,offset本身由消费者维持,broker不维护此值(保证性能)

      • 可以使用zookeeper来维护消费状态
  • push消息时指定key和Partition,同一个key的消息,使用同一个Partition,保证消息有序,这里的key实际上指代的是某业务,某对象的id
Consumer
  • 消息消费者,向Kafka broker读取消息的客户端,consumer从broker拉取(pull)数据并进行处理
Topic
  • Producer 将消息发送到特定的主题,Consumer 通过订阅特定的 Topic(主题) 来消费消息

    • 同一个主题的消息可能存储在不同的节点(实际上是其对应的传输通道--Partition可以分布在不同的节点上),但是消费者不用关心具体的存储位置
  • 一个主题的消息可以对应多个Partition,即使用多个Partition传输该主题的消息
Partition
  • 在集群内,一个Partition由一个 Broker 负责,这个 Broker 也称为这个Partition的 Leader;当然一个Partition可以被复制到多个 Broker 上来实现冗余,这样当存在 Broker 故障时可以将其Partition重新分配到其他 Broker 来负责
  • 设计Partition的原因

    • 方便在集群中扩展,每个 Partition 可以通过调整以适应它所在的机器,而一个 topic 又可以有多个 Partition 组成,因此整个集群就可以适应任意大小的数据了
    • Kafka 通过给特定 Topic 指定多个 Partition, 而各个 Partition 可以分布在不同的 Broker 上, 这样便能提供比较好的并发能力(负载均衡)
  • Partition的多副本机制

    • Partition中的多个副本之间会有一个叫做 leader 的家伙,其他副本称为 follower。我们发送的消息会被发送到 leader 副本,然后 follower 副本(做冷备)才能从 leader 副本中拉取消息进行同步
    • Kafka 会均匀的将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性

      • <span style='color:blue;background:背景颜色;font-size:文字大小;'>提升了信息的安全性,防止消息丢失,提高了容灾能力,但是同时也增加了占用的空间</span>
      生产者和消费者只与 leader 副本交互。可以理解为其他副本只是 leader 副本的拷贝,它们的存在只是为了保证消息存储的安全性。当 leader 副本发生故障时会从 follower 中选举出一个 leader,但是 follower 中如果有和 leader 同步程度达不到要求的参加不了 leader 的竞选。

Zookeeper在Kafka中起到的作用

  • 主要就是服务发现与元数据的存储,与每一个分布式系统中的元数据存储一样(比如Spring loud中的eureka),就是一个加了事件通知机制的分布式数据库,主要用来做服务发现与服务治理
  • Broker 注册 :在 Zookeeper 上会有一个专门用来进行 Broker 服务器列表记录的节点。每个 Broker 在启动时,都会到 Zookeeper 上进行注册,即到/brokers/ids 下创建属于自己的节点。每个 Broker 就会将自己的 IP 地址和端口等信息记录到该节点中去
  • Topic 注册 : 在 Kafka 中,同一个Topic 的消息会被分成多个Partition并将其分布在多个 Broker 上,这些Partition信息及与 Broker 的对应关系也都是由 Zookeeper 在维护。比如创建了一个名字为 my-topic 的主题并且它有两个Partition,对应到 zookeeper 中会创建这些文件夹:/brokers/topics/my-topic/Partitions/0/brokers/topics/my-topic/Partitions/1
  • 负载均衡 :上面也说过了 Kafka 通过给特定 Topic 指定多个 Partition, 而各个 Partition 可以分布在不同的 Broker 上, 这样便能提供比较好的并发能力。 对于同一个 Topic 的不同 Partition,Kafka 会尽力将这些 Partition 分布到不同的 Broker 服务器上。当生产者产生消息后也会尽量投递到不同 Broker 的 Partition 里面。当 Consumer 消费的时候,Zookeeper 可以根据当前的 Partition 数量以及 Consumer 数量来实现动态负载均衡。
  • ......

Kafka如何保证消息不丢失

生产者的消息丢失
  • 生产者的消息发送是异步的,应该添加回调,再发送失败后,执行重发
  • 设置生产者的自动重试次数,当网络出现问题时会自动重发,一般是 3 ,但是为了保证消息不丢失的话一般会设置比较大一点。设置完成之后,当出现网络问题之后能够自动重试消息发送,避免消息丢失。另外,建议还要设置重试间隔,因为间隔太小的话重试的效果就不明显了

    • 可能导致消息的重复发送
消费者的消息丢失
  • 当消费者拉取到了Partition的某个消息之后,消费者会自动提交了 offset。自动提交的话会有一个问题,试想一下,当消费者刚拿到这个消息准备进行真正消费的时候,突然挂掉了,消息实际上并没有被消费,但是 offset 却被自动提交了。

    • 解决办法也比较粗暴,手动关闭自动提交 offset,每次在真正消费完消息之后之后再自己手动提交 offset 。但是会带来消息被重新消费的问题。比如刚刚消费完消息之后,还没提交 offset,结果挂掉了,那么这个消息理论上就会被消费两次
Kafka自身丢了消息
  • 从Partition的多副本机制中follower没有完成对于leader的同步,但是leader所在的broker挂掉的情况开始谈起

    • Kafka 为Partition引入了多副本(Replica)机制。Partition中的多个副本之间会有一个叫做 leader 的家伙,其他副本称为 follower。我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。生产者和消费者只与 leader 副本交互。可以理解为其他副本只是 leader 副本的拷贝,它们的存在只是为了保证消息存储的安全性。
    • 试想一种情况:假如 leader 副本所在的 broker 突然挂掉,那么就要从 follower 副本重新选出一个 leader ,但是 leader 的数据还有一些没有被 follower 副本的同步的话,就会造成消息丢失。解决办法如下所示:

      • 设置 acks = all,acks 是 Kafka Producer很重要的一个参数。acks 的默认值即为1,代表我们的消息被leader副本接收之后就算被成功发送。当我们配置 acks = all 代表则所有副本都要接收到该消息之后该消息才算真正成功被发送
      • 设置 replication.factor >= 3,为了保证 leader 副本能有 follower 副本能同步消息,我们一般会为 topic 设置 replication.factor >= 3。这样就可以保证每个 Partition(partition) 至少有 3 个副本。虽然造成了数据冗余,但是带来了数据的安全性
      • 设置 min.insync.replicas > 1,一般情况下我们还需要设置 min.insync.replicas> 1 ,这样配置代表消息至少要被写入到 2 个副本才算是被成功发送。min.insync.replicas 的默认值为 1 ,在实际生产中应尽量避免默认值 1。
      • 为了保证整个 Kafka 服务的高可用性,还需要确保 replication.factor > min.insync.replicas 。为什么呢?设想一下假如两者相等的话,只要是有一个副本挂掉,整个Partition就无法正常工作了。这明显违反高可用性!一般推荐设置成 replication.factor = min.insync.replicas + 1
      • 设置 unclean.leader.election.enable = false,我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。多个 follower 副本之间的消息同步情况不一样,当我们配置了 unclean.leader.election.enable = false ,当 leader 副本发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了消息丢失的可能性。

        Kafka 0.11.0.0版本开始 unclean.leader.election.enable 参数的默认值由原来的true 改为false

Kafka如何保证消息不重复(幂等问题)

  • 有一个观点要理解,即分布式的环境下,不丢失消息与不重复是矛盾的,但是相对来说重复消费是可以在消费端设计的,但是消息丢失就比较麻烦了
出现消息重复的三种场景
  • 发送消息时出现重复

    • 消息已经被push到broker,在broker响应之前,网络故障,生产者没有收到回应,以为没有成功发出,于是重复发送
    • 其实就是前边说的为了保证生产者部分消息不丢失导致的
  • 消费消息时出现重复

    • 前文中消费端避免消息丢失的部分已经说明了
    • 所谓避免Kafka出现消息重复本质上指的是避免消息被重复消费
  • broker重启或者扩容缩容时出现消息的重复
解决方案
方案1
  • 从关系型数据库的幂等性上考虑(依赖于事务的消息幂等方案)

    • 如果是插入数据的话,在插入前首先检查数据是否已经存储了(已经被消费了),如果没有再进行消费(数据的插入)
    • 但是如果是并发的重复消息的话,这么检查也没有用,因此可以使用加共享锁的读select for update但是又会导致并发度下降,于是又可以使用乐观锁等等,涉及的业务表的涉及就比较复杂了
    • 更合理的一个方式是在消费端的数据库中维护一个消息消费的记录表,进行消息消费时,把消费记录插入记录表与业务表的数据更新做到一个事务中去执行,就可以保证不会重复消费数据了(如果有重复消息的话,会导致插入消息表时出现主键冲突),局限性在于事务带来的性能的消耗以及消费端的业务消费必须仅仅包含MySQL这样的关系型数据库,如果还涉及到Redis等nosql的数据库,则无效了

      • 注意消息消费记录表的记录ID应该是业务相关的ID,而不是消息ID,因为如果生产者重复发送消息的话,消息ID也是不同的,可能导致消息被重复记录,从而导致重复消费
方案2
  • 不依赖于事务的消息幂等方案,核心在于去重表(对于消费状态的维护)

<img src="https://images.demoli.xyz/image-20210816184127192.png" alt="image-20210816184127192" style="zoom:67%;" />

  • 所谓的去重表就是依赖事务的案例中的那个消息消费记录表的一个新版本
  • 为什么要延迟消费,延迟消费说明重复的消息遇到了本消息正在处理的场景,为了避免正在处理的失败,因此不是直接丢弃而是延迟再消费,以保证消息不会丢失

    • <span style='color:;background:背景颜色;font-size:文字大小;'>至于延迟消费的方式可以是将消息投到一个特定的topic中(或者是原topic),然后有特定的消费端重新消费</span>
  • 为什么消息消费记录表中的消息记录要维护一个消费的时间呢?还是上边的场景,如果第一个消息没能成功处理,而第二个消息延迟消费后,每次都是看到正在消费中,会导致消息处理的失败,也就意味着消息的丢失,因此正在处理中的消息有时间阈值,规定时间内没有消费完成的需要从表中删除,表示消费失败

    • 删除后,会一直进行重试延迟消费
  • 去重表一般使用Redis维护,性能好,过期时间还可以使用Redis的ttl实现

Kafka性能原理

  • 顺序读写

    • producer 采用推(push)模式将消息发布到 broker,每条消息都被追加(append)到Partition中,属于顺序写磁盘(顺序写磁盘效率比随机写内存要高,保障 Kafka 吞吐率)
    • Kafka默认不会删除持久化的数据,Kakfa提供了两种策略来删除数据。一是基于时间,二是基于partition文件大小。具体配置可以参看它的配置文档
  • 使用了操作系统的Page Cache
  • 批量读写

    • 异步的思想,批量读写在MySQL中也有用到比如redolog的wal技术
  • Partition+分段+索引,提高数据读写效率,并发读写
  • 支持数据的批量压缩,以降低网络IO的时间
  • 零拷贝

    • 零拷贝并非指一次拷贝都没有,而是避免了在内核空间和用户空间之间的拷贝
    • 消费过程中使用到了零拷贝
    • 避免了数据在JVM内存和系统内存之间的复制,减少耗性能的创建对象和垃圾回收

<img src="https://images.demoli.xyz/image-20210815185423195.png" alt="image-20210815185423195" style="zoom:67%;" />

其他消息队列中间件

ActiveMQ

  • 优点

    • 单机吞吐量:万级
    • topic数量都吞吐量的影响:
    • 时效性:ms级
    • 可用性:高,基于主从架构实现高可用性
    • 消息可靠性:有较低的概率丢失数据
    • 功能支持:MQ领域的功能极其完备
  • 缺点:

    • 官方社区现在对ActiveMQ 5.x维护越来越少,较少在大规模吞吐的场景中使用,但是ActiveMQ Artemis版本性能有了很大的提升

Kafka

  • 优点

    • 性能卓越,单机写入TPS约在百万条/秒,最大的优点,就是吞吐量高
    • 时效性:ms级
    • 可用性:非常高,Kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用
    • 消费者采用Pull方式获取消息, 消息有序, 通过控制能够保证所有消息被消费且仅被消费一次
    • 有优秀的第三方Kafka Web管理界面Kafka-Manager
    • 在日志领域比较成熟,被多家公司和多个开源项目使用
    • 功能支持:功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用
  • 缺点

    • Kafka单机超过64个队列/Partition,Load会发生明显的飙高现象,队列越多,load越高,发送消息响应时间变长
    • 使用短轮询方式,实时性取决于轮询间隔时间
    • 消费失败不支持重试
    • 支持消息顺序,但是一台代理宕机后,就会产生消息乱序
    • 社区更新较慢

RabbitMQ

  • 优点

    • 由于erlang语言的特性,mq 性能较好,高并发
    • 吞吐量到万级,MQ功能比较完备
    • 健壮、稳定、易用、跨平台、支持多种语言、文档齐全
    • 开源提供的管理界面非常棒,用起来很好用
    • 社区活跃度高
  • 缺点

    • erlang开发,很难去看懂源码,基本职能依赖于开源社区的快速维护和修复bug,不利于做二次开发和维护
    • RabbitMQ确实吞吐量会低一些,这是因为他做的实现机制比较重。
    • 需要学习比较复杂的接口和协议,学习和维护成本较高

RocketMQ

  • 优点

    • 单机吞吐量:十万级
    • 可用性:非常高,分布式架构
    • 消息可靠性:经过参数优化配置,消息可以做到0丢失
    • 功能支持:MQ功能较为完善,还是分布式的,扩展性好
    • 支持10亿级别的消息堆积,不会因为堆积导致性能下降
    • 源码是Kafka,我们可以自己阅读源码,定制自己公司的MQ,可以掌控
  • 缺点

    • 支持的客户端语言不多,目前是Kafka及c++,其中c++不成熟;
    • 社区活跃度一般
    • 没有在 mq 核心中去实现JMS等接口,有些系统要迁移需要修改大量代码

区别

  • Kafka 支持实时的流式处理
  • 极致的性能:基于 Scala 和 Kafka 语言开发,设计中大量使用了批量处理和异步的思想,最高可以每秒处理千万级别的消息
  • 生态系统兼容性无可匹敌 :Kafka 与周边生态系统的兼容性是最好的没有之一,尤其在大数据和流计算领域(也可以描述为比较好找资料)

参考


demoli
16 声望0 粉丝

bug创建者


下一篇 »
Go异常处理