是消息中间件,又不仅仅是消息中间件的kafka

什么是 Kafka

kafka 最初是 LinkedIn 的一个内部基础设施系统。最初开发的起因是,LinkedIn 虽然有了数据库和其他系统可以用来存储数据,但是缺乏一个可以帮 助处理持续数据流的组件。所以在设计理念上,开发者不想只是开发一个能够存储数据的系统,如关系数据库、Nosql 数据库、搜索引擎等等,更希望把数据看成一个持续变化和不断增长的流,并基于这样的想法构建出一个数据系统,一个数据架构。诶?不是说kafka是一个消息中间件么?怎么照这个说法更像一个数据库呢?我们接着介绍。

Kafka与传统消息系统相比有哪些特点

Kafka 外在表现很像消息系统,允许发布和订阅消息流,但是它和传统的消息系统有很大的差异:

  1. Kafka 是个现代分布式系统,以集群的方式运行,可以自由伸缩。
  2. Kafka 可以按照要求存储数据,保存多久都可以,
  3. 流式处理将数据处理的层次提示到了新高度,消息系统只会传递数据,Kafka 的流式处理能力可以让我们用很少的代码就能动态地处理派生流和数据集(数据不一定需要完全交给消费者进行处理,而是在中间件中就可以提前进行流式处理。此技术在大数据中使用颇为广泛)。

所以 Kafka 不仅仅是个消息中间件,同时它是一个流平台,这个平台上可以发布和订阅数据流(Kafka 的流,有一个单独的包 Stream 的处理),并把他们保存起来,进行处理,这个是 Kafka 作者的设计理念。

Kafka中的一些基本概念

在深入学习kafka之前,我们需要先对Kafka的结构,以及一些专有名词进行了解。

Kafka的结构

是消息中间件,又不仅仅是消息中间件的kafka_第1张图片

主题和分区 (Partition)

Kafka 里的消息用主题进行分类(主题好比数据库中的表),主题下有可以被分为若干个分区(分表技术)。分区本质上是个提交日志文件,有新消息,这个消息就会以追加的方式写入分区(写文件的形式),然后用先入先出的顺序读取。

但是因为主题会有多个分区,所以在整个主题的范围内,是无法保证消息的顺序的,单个分区则可以保证。

Kafka 通过分区来实现数据冗余和伸缩性,因为分区可以分布在不同的服务器上,那就是说一个主题可以跨越多个服务器(这是 Kafka 高性能的一个原因,多台服务器的磁盘读写性能总值比单台更高)。

对比RabbitMQ的"假"分布式来说,kafka这样的设计结构具备天然分布式的优势,消费的时候可以保证不同的生产者,不同的消费者之间互不打扰的进行消费。

前面我们说 Kafka 可以看成一个流平台,很多时候,我们会把一个主题的数据看成一个流,不管有多少个分区。

消息和批次

消息,Kafka 里的数据单元,也就是我们一般消息中间件里的消息的概念(可以比作数据库中一条记录)。消息由字节数组组成。消息还可以包含键(key)(可选元数据,也是字节数组),主要用于对消息选取分区。

作为一个高效的消息系统,为了提高效率,消息可以被分批写入 Kafka。批次就是一组消息,这些消息属于同一个主题和分区。如果只传递单个消息,会导致大量的网络开销,把消息分成批次传输可以减少这开销。但是,这个需要权衡(时间延迟和吞吐量之间),批次里包含的消息越多,单位时间内处理的消息就越多,单个消息的传输时间就越长(吞吐量高延时也高)。如果进行压缩,可以提升数据的传输和存储能力,但需要更多的计算处理。

对于 Kafka 来说,消息是晦涩难懂的字节数组,一般我们使用序列化和反序列化技术,格式常用的有 JSON 和 XML,还有 Avro(Hadoop 开发的一款序列化框架),具体怎么使用依据自身的业务来定。

批次的弊端(消息重复)

当然,和RabbitMQ一样,批次发送可能会导致数据丢失的风险。原因是发送者将一个批次的消息中的一部分消息发布到Kafka中进行处理,另一部发送失败了。此时如果批次中的某个消息发送失败了,整个批次的消息会全部返回给发送者。

如果消息因为网络原因,或是Kafka中的主节点宕机,从而进行重新选举主节点而导致的消息会在Kafka内部不断地重试。但是重试我们可以设置时长,若是一段时间始终无法重试成功,依旧会导致消息发布失败返回到客户端。此时再次重发消息就会导致消息重复(以为Kafka中已经执行了该批次的部分信息了)。

Kafka的键和RabbitMQ的路由键

RabbitMQ中的路由键可以见过我们的路由器,将消息发送到指定的队列中,然后消费者可以对特定的队列进行绑定消费。

实际上在Kafka中是不存在路由键这个概念的。 Kafka为大数据而生,因此如果不主动设计路由键的话,我们的消息会根据Partition的个数均匀的进行散列。消费者一般也不会去指定特定分区去消费,而是直接订阅主题,随机分配到一个Partition中进行消费。

当然,我们的生产者API也提供了消息指定分区的功能。只不过,指定分区会影响Kafka的运行效率,同时也对生产者的负载均衡有所影响,缺失了原本的动态性。

生产者和消费者、偏移量、消费者群组(消费者负载均衡)

就是一般消息中间件里生产者和消费者的概念。一些其他的高级客户端 API,像数据管道 API 和流式处理的 Kafka Stream,都是使用了最基本的生产者和消费者作为内部组件,然后提供了高级功能。

生产者默认情况下把消息均衡分布到主题的所有分区上,如果需要指定分区,则需要使用消息里的消息键和分区器。

是消息中间件,又不仅仅是消息中间件的kafka_第2张图片

消费者订阅主题,一个或者多个,并且按照消息的生成顺序读取。消费者通过检查所谓的偏移量来区分消息是否读取过。偏移量是一种元数据,一个不断递增的整数值,创建消息的时候,Kafka 会把他加入消息。在一个主题中一个分区里,每个消息的偏移量是唯一的。每个分区最后读取的消息偏移量会保存到Zookeeper 或者 Kafka 上,这样分区的消费者关闭或者重启,读取状态都不会丢失。

多个消费者可以构成一个消费者群组。怎么构成?共同读取一个主题的消费者们,就形成了一个群组。群组可以保证每个分区只被一个消费者使用。

是消息中间件,又不仅仅是消息中间件的kafka_第3张图片

如何理解消费者的负载均衡

再举个例子子:我们现在穿越到古代。假设一个主题中的所有分区被我比喻为中国的女人。而消费者群组被我比喻为中国男人。所有的中国男人会抱团去和女人结婚。如果女人少男多的情况下,有些男人就有了多个妻子(一个消费者可以对应多个partition)。而如果男多女少的情况下,根据我们国家古时候的法律规定,女人只能有一个丈夫。因此当每个女人都配对了一个男人后,主动有一部分男人找不到对象,就注定做单身狗了。然而世事无常,战争频起,许多有了妻子的男人战死沙场后,许多女人(分区)成了寡妇。这时候这些没找到对象的男人就再次有了"转正"的机会,立刻和这些无消费者的分区进行配对,开始继续消费。这个概念被称为群组协调。

再假如,某个消费者不是我们这个组群的,那他就不具备群组协调的功能。也就是哪怕单身的女人(分区)再多,不在组群的消费者也不会去进行消费。

我们发现族群的无用消费者,往往让我们在消费端的可用性得到了巨大的保障。(高可用)

现在再回想我们之前的那个问题,如果不是有非常严格的需求需要我们生产者绑定特定的分区,就尽量不要直接绑定分区,这相当于屏蔽了分区器的工作,严重影响了负载均衡的效果,从而影响可用性。同时也会影响Kafka的吞吐量。是不是更能理解设计者的理念了呢?

Broker 和分区复制

一个独立的 Kafka 服务器叫 Broker。broker 的主要工作是,接收生产者的消息,设置偏移量,提交消息到磁盘保存;为消费者提供服务,响应请求,返回消息。在合适的硬件上,单个 broker 可以处理上千个分区和每秒百万级的消息量。(要达到这个目的需要做操作系统调优和 JVM 调优) 。

多个 broker 可以组成一个集群。每个集群中 broker 会选举出一个集群控制器。控制器会进行管理,包括将分区分配给 broker 和监控 broker。集群里,一个分区从属于一个 broker,这个 broker 被称为首领(后面我就简称主Broker了)。但是分区可以被分配给多个 broker,这个时候会发生分区复制。集群中 Kafka 内部一般使用管道技术进行高效的复制。

分区复制带来的好处是,提供了消息冗余。一旦首领 broker 失效,其他 broker 可以接管领导权。当然相关的消费者和生产者都要重新连接到新的首领上。


是消息中间件,又不仅仅是消息中间件的kafka_第4张图片

如图,这张图中每一个Broker都是一个服务器。每一个分区都存在一个主服务器,以及其他一个或多个备用服务器。生产者和消费者打交道的都是主分区的节点,而其他副本分区都不进行实际的业务处理。副本分区的工作就是不断地同步主分区的消息。这样,当某个分区的主分区所在的Broker挂了之后,该分区的其他副本Broker都会立马顶上(备胎转正)。

发现了么,这个设计思想和消费者群组和其相似?消费者群组中的消费者数如果大于分区数,那些不干活的消费者并不是没用,他就是我们的消费者备胎,随时准备这上位,以达到高可用的目的。

这种设计理念就是Kafka高可用的基石。让人们可以用砸钱砸设备的方式保证数据的完整性。并且Kafka还可以通过砸钱砸设备的方式提高他整体的吞吐量(新增分区)。

是消息中间件,又不仅仅是消息中间件的kafka_第5张图片

 

上图这种工作方式也是可以的。Broker1和Broker2互为主备(都数据首领Broker,同时包含着对方的副本Broker)。因此,这两个Broker都存在于集群之中。当其中任意一个Broker挂了之后,另一个Broker也能立马顶上。当然,这样的话单个Broker中存在两个(或多个)分区时,磁盘性能,以及内存性能都会受到影响,而导致吞吐量降低。但是在一个刚起步,业务上升期的公司,前期可以使用这种方式应对数据量不是特别大的场景。在节约了机器成本的同时,也尽可能的保证了数据的完整性。

保留消息

在一定期限内保留消息是 Kafka 的一个重要特性,Kafka broker 默认的保留策略是:要么保留一段时间(7 天),要么保留一定大小(比如 1 个 G)。到了限制,旧消息过期并删除。但是每个主题可以根据业务需求配置自己的保留策略(开发时要注意,Kafka 不像 Mysql 之类的永久存储)。

对比我们的RabbitMQ,我们发现Kafka的消息删除机制完全不同。kafka中数据的删除跟有没有消费者消费完全无关。数据的删除,只跟kafka broker上面上面的这两个配置有关:

  1. log.retention.hours=48 #数据最多保存48小时
  2. log.retention.bytes=1073741824 #数据最多1G

因此,我们发现Kafka的更像一个临时性数据库。

为什么选择 Kafka

优点(对比RabbitMQ而言)

  1. 多生产者和多消费者 (分布式架构)
  2. 基于磁盘的数据存储,换句话说,Kafka 的数据天生就是持久化的。 (类似数据库)
  3. 高伸缩性,Kafka 一开始就被设计成一个具有灵活伸缩性的系统,对在线集群的伸缩丝毫不影响整体系统的可用性。 (轻松搞定了RabbitMQ集群想做而做不到的事)
  4. 高性能,结合横向扩展生产者、消费者和 broker,Kafka 可以轻松处理巨大的信息流(LinkedIn 公司每天处理万亿级数据),同时保证亚秒级的消息延迟。(通过添加机器就可以使性能大幅提升。但分区过多依旧会使消息生产吞吐量下降)。

Kafka高性能的原因

顺序写磁盘

相比磁盘的随机写快很多)。如果你是追加文件末尾按照顺序的方式来写数据的话,那么这种磁盘顺序写的性能基本上可以跟写内存的性能本身也是差不多的。

Page Cache(缓存到内存,读取内存)

操作系统本身有一层缓存,叫做page cache,是在内存里的缓存,我们也可以称之为os cache,意思就是操作系统自己管理的缓存。原理就是Page Cache可以把磁盘中的数据缓存到内存中,把对磁盘的访问改为对内存的访问。

零拷贝

不适用零拷贝的流程

kafka从磁盘读数据发送给下游的消费者大概的过程为:kafka首先看看要读的数据在不在os cache里,如果不在的话就从磁盘文件里读取数据后放入os cache,接着再到应用程序进程的缓存里,再到操作系统层面的Socket缓存里,最后从Socket缓存里提取数据后发送到网卡,最后发送出去给消费者。

使用零拷贝的流程

直接让操作系统的cache中的数据发送到网卡后传输给下游的消费者,直接跳过了两次拷贝数据的步骤,Socket缓存中仅仅会拷贝一个描述符过去,不会拷贝数据到Socket缓存。

常见场景

活动跟踪

跟踪网站用户和前端应用发生的交互,比如页面访问次数和点击,将这些信息作为消息发布到一个或者多个主题上,这样就可以根据这些数据为机器学习提供数据,更新搜素结果等等(头条、淘宝等总会推送你感兴趣的内容,其实在数据分析之前就已经做了活动跟踪)。

当然,Kafka之所以能这么用,是因为他天生的持久化,分布式以及具备流式计算处理能力密不可分。

传递消息

标准消息中间件的功能,但需要注意和传统的消息中间件不同的是,消息不会消费删除。

收集指标和日志

收集应用程序和系统的度量监控指标,或者收集应用日志信息,通过 Kafka 路由到专门的日志搜索系统,比如 ES。(国内用得较多)

提交日志

收集其他系统的变动日志,比如数据库。可以把数据库的更新发布到 Kafka 上,应用通过监控事件流来接收数据库的实时更新,或者通过事件流将数据库的更新复制到远程系统。

还可以当其他系统发生了崩溃,通过重放日志来恢复系统的状态。(异地灾备)。

流处理

操作实时数据流,进行统计、转换、复杂计算等等。随着大数据技术的不断发展和成熟,无论是传统企业还是互联网公司都已经不再满足于离线批处理,实时流处理的需求和重要性日益增长 。

近年来业界一直在探索实时流计算引擎和 API,比如这几年火爆的 Spark Streaming、Kafka Streaming、Beam 和 Flink,其中阿里双 11 会场展示的实时销售金额,就用的是流计算,是基于 Flink,然后阿里在其上定制化的 Blink。

Kafka硬件对Kafka性能的影响

为 Kafka 选择合适的硬件更像是一门艺术,就跟它的名字一样,我们分别从磁盘、内存、网络和 CPU 上来分析,确定了这些关注点,就可以在预算范围之内选择最优的硬件配置。

磁盘吞吐量/磁盘容量

磁盘吞吐量(IOPS 每秒的读写次数)会影响生产者的性能。因为生产者的消息必须被提交到服务器保存,大多数的客户端都会一直等待,直到至少有一个服务器确认消息已经成功提交为止。也就是说,磁盘写入速度越快,生成消息的延迟就越低。(SSD 固态贵单个速度快,HDD 机械偏移可以多买几个,设置多个目录加快速度,具体情况具体分析)

磁盘容量的大小,则主要看需要保存的消息数量。如果每天收到 1TB 的数据,并保留 7 天,那么磁盘就需要 7TB 的数据。

内存

Kafka 本身并不需要太大内存,内存则主要是影响消费者性能。在大多数业务情况下,消费者消费的数据一般会从内存(页面缓存,从系统内存中分)中获取,这比在磁盘上读取肯定要快的多。一般来说运行 Kafka 的 JVM 不需要太多的内存,剩余的系统内存可以作为页面缓存,或者用来缓存正在使用的日志片段,所以我们一般 Kafka 不会同其他的重要应用系统部署在一台服务器上,因为他们需要共享页面缓存,这个会降低 Kafka 消费者的性能。

是消息中间件,又不仅仅是消息中间件的kafka_第6张图片

可以看到,我们虽然消费者一般进行消费要等到一个主题中的所有Broker全部持久化后才进行消费,但是消费的数据源获取来自于内存中,以提高消费速度。

网络

网络吞吐量决定了 Kafka 能够处理的最大数据流量。它和磁盘是制约 Kafka 拓展规模的主要因素。对于生产者、消费者写入数据和读取数据都要瓜分网络流量。同时做集群复制也非常消耗网络。

CPU

Kafka 对 cpu 的要求不高,主要是用在对消息解压和压缩上。所以 cpu 的性能不是在使用 Kafka 的首要考虑因素。

总结

我们要为 Kafka 选择合适的硬件时,优先考虑存储,包括存储的大小,然后考虑生产者的性能(也就是磁盘的吞吐量),选好存储以后,再来选择CPU 和内存就容易得多。网络的选择要根据业务上的情况来定,也是非常重要的一环。

Kafka的一些常用指令

##列出所有主题

kafka-topics.bat --zookeeper localhost:2181 --list

##列出所有主题的详细信息

kafka-topics.bat --zookeeper localhost:2181 --describe

##创建主题 主题名 my-topic1 副本,8 分区

kafka-topics.bat --zookeeper localhost:2181 --create --topic my-topic --replication-factor 1 --partitions 8

##增加分区,注意:分区无法被删除

kafka-topics.bat --zookeeper localhost:2181 --alter --topic my-topic --partitions 16##创建生产者(控制台)

kafka-console-producer.bat --broker-list localhost:9092 --topic my-topic

##创建消费者(控制台)

kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic my-topic --from-beginning

##列出消费者群组(仅 Linux

kafka-topics.sh --new-consumer --bootstrap-server localhost:9092 --list

##列出消费者群组详细信息(仅 Linux

kafka-topics.sh --new-consumer --bootstrap-server localhost:9092 --describe --group 群组名

Kafka 的集群

集群的成员关系

Kafka 使用 zookeeper 来维护集群成员的信息。每个 broker 都有个唯一标识符, 这个标识符可以在配置文件里指定, 也可以自动生成。 在 broker 启 动的时候, 它通过创建临时节点把自己的 ID 注册到 zoo-keeper。 Kafka 组件订阅 Zookeeper 的/brokers/ids 路径(broker 在 zookeeper 上的注册路径) , 当有 broker 加入集群或退出集群时, 这些组件就可以获得通知 。

如果你要启动另一个具有相同 ID 的 broker, 会得到一个错误。新 broker 会试着进行注册,但不会成功, 因为 zookeeper 里已经有一个具有相同 ID 的broker。

在 broker 停机、 出现网络分区或长时间垃圾回收停顿时, broker 会从 Zookeeper 上断开连接, 此时 broker 在启动时创建的临时节点会自动从Zookeeper 上移除。 监听 broker 列表的 Kafka 组件会被告知该 broker 已移除。

在关闭 broker 时, 它对应的节点也会消失, 不过它的 ID 会继续存在于其他数据结构中。 例如,主题的副本列表里就可能包含这些 ID。在完全关闭一个 broker 之后, 如果使用相同的 ID 启动另一个全新的 broker, 它会立刻加入集群, 并拥有与旧 broker 相同的分区和主题。

Kafka集群的结构与配置

    假设要配置三个Broker,那么kafka的集群就需要由三台ZooKeeper, 和三台kafka的集群组成的,想互的关系大约类似下面这张图:

 

是消息中间件,又不仅仅是消息中间件的kafka_第7张图片

Zookeeper集群与Kafka集群的配置

首先时候zookeeper集群,在每个zookpper节点的zoo.cfg文件中,会有类似如下的配置:

  1. server.1=172.17.80.219:2881:3881
  2. server.2=172.17.80.219:2882:3882
  3. server.3=172.17.80.219:2883:3883

这就表示这三台节点组成了一个zookeeper集群。

再说kafka集群,在kafka集群中每个broker的server.propertise配置文件中,都有如下的配置,

zookeeper.connect=172.17.80.219:2181,172.17.80.219:2182,172.17.80.219:2183

表示把该broker交给指定的这个zookpper集群来管理,其他的broker也会配这个,交给同一个zk集群来管理的broker节点就组成了kafka集群。

从配置立即上图应该理解的更加清晰了。

为何需要 Kafka 集群 (生产者的负载均衡)

是消息中间件,又不仅仅是消息中间件的kafka_第8张图片

借助这张图来进行说明。Kafka集群也同样能达到对生产者负载均衡的效果。

对于同一个Topic的不同Partition,Kafka会尽力将这些Partition分布到不同的Broker服务器上,这种均衡策略实际上是基于Zookeeper实现的。

其设计思想与之前的消费者集群完全一模一样。通过向集群中添加新的分区节点,达到数据分流的效果,再对应到该Topic的消费者群组中,就可以提升消费吞吐量。

这样kafka的设计理念和使用场景就非常明显了。我们幻想在双11的秒杀场景之下,生产的消息必定是铺天盖地的将数据丢到我们的Kafka集群当中。我们就可以通过疯狂堆设备,堆机器的方式去加速消息的处理速度。

一个消费者组可以消费一个集群中不同Topic么

之前一直在说消费者群组消费同一个主题的不同分区。实际上,一个消费者或一个消费者群组是可以对不同主题,但在同一集群的分区消息进行消费的。

项目中的Broker数量预估

需要多少磁盘空间保留数据,和每个 broker 上有多少空间可以用。比如,如果一个集群有 10TB 的数据需要保留,而每个 broker 可以存储 2TB,那么至少需要 5 个 broker。如果启用了数据复制,则还需要一倍的空间,那么这个集群需要 10 个 broker。

集群处理请求的能力。如果因为磁盘吞吐量和内存不足造成性能问题,可以通过扩展 broker 来解决。

RabbitMQ的集群和Kafka集群的对比

围绕着高性能与高可用进行对比讲解。

RabbitMQ集群

高性能

通过将消费者分不到不同的机器上工作,以提升工作效率。但是普通集群模式实际上都是在消费同一台RabbitMQ,镜像模式实际上也是将一台RabbitMQ的所有元数据以及消息本身同步到不同的主机节点中的这种理念(前者消费时导致数据消费频繁从主节点拉取,后者生产时导致数据不断进行拷贝),因此始终无法真正意义的做到高性能。即时集群能使用多个消费者进行消费,也受限于单主机节点的性能瓶颈。

高可用

普通模式在一台Broker宕机后,可以将该Broker的生产者,消费者,元数据转移到其他机器上继续工作。但是缓存在宕机Broker队列中的元素是无法恢复的。

镜像模式解决了普通模式中,宕机后消息丢失的问题,但是在生产时同步的效率大幅降低,并且内存开销过大,导致RabbitMQ的集群能用,但相对来说是比较鸡肋的。

另外提及一点,Rabbit在数据持久化的过程中也有微小的概率导致数据丢失,因此安全性上不能保证绝对的安全。

Kafka集群

高性能

Kafka的设计结构是天然的分布式的,可以将不同的分区部署到不同的Broker中,实现真正的分布式消费。吞吐量伴随着机器的增多而成正比例倍数增加,当分区过多时也会对性能有所影响,但是与RabbitMQ的方式相比强大太多了。

高可用

Kafka的集群更多的是保证高可用。我们的RabbitMQ是把数据持久化到磁盘。而Kafka本身消息就是持久化的。并且我们会备份消息到其他的Broker中。

分区复制带来的好处是,提供了消息冗余。一旦首领 broker 失效,其他 broker 可以接管领导权。并且,通过acks的设置机制可以保证数据不丢失,虽然会影响性能,但安全性比RabbitMQ更加高。但是Kafka要玩好所需要的硬件成本是巨大的,因此具体选择哪种消息中间件依旧取决于业务大小与业务场景。

Kafka的生产者

生产者发送消息的基本流程

是消息中间件,又不仅仅是消息中间件的kafka_第9张图片

从创建一个 ProducerRecord 对象开始, Producer Record 对象需要包含目标主题和要发送的内容。我们还可以指定键或分区。在发送 ProducerReco rd 对象时,生产者要先把键和值对象序列化成字节数组,这样它们才能够在网络上传输。

接下来,数据被传给分区器。如果之前在 Producer Record 对象里指定了分区,那么分区器就不会再做任何事情,直接把指定的分区返回。如果没有 指定分区,那么分区器会根据 Producer Record 对象的键来选择一个分区。选好分区以后,生产者就知道该往哪个主题和分区发送这条记录了。紧接着, 这条记录被添加到一个记录批次里(双端队列,尾部写入),这个批次里的所有消息会被发送到相同的主题和分区上。有一个独立的线程负责把这些记 录批次发送到相应的 broker 上。

服务器在收到这些消息时会返回一个响应。如果消息成功写入 Kafka ,就返回一个 RecordMetaData 对象,它包含了主题和区信息,以及记录在分 区里的偏移量。如果写入失败, 则会返回一个错误。生产者在收到错误之后会尝试重新发送消息,几次之后如果还是失败,就返回错误信息。

生产者发送失败处理方案

可重发类型

可重试错误,比如连接错误(可通过再次建立连接解决)、无主 no leader(可通过分区重新选举首领解决)。

不可重发类型

无法通过重试解决,比如“消息太大”异常。

Kafka反馈生产者消息接受状态的方式

我们生产者发送给Kafka一条消息时,这个过程并不能保证百分百投递成功,因此我们需要Kafka在接受到消息后通知我们的生产者一声:"消息已收到"。消息的反馈方式有以下三种。

生产者不设置反馈(不安全,但速度快)

忽略 send 方法的返回值,不做任何处理。大多数情况下,消息会正常到达,而且生产者会自动重试,但有时会丢失消息。

同步发送(安全,但速度不快)

获得 send 方法返回的 Future 对象,然后一直等待Kafka把消息传递回来才继续执行。这样消息的生产效率极差。

异步发送(安全,速度快)

异步的实现方式就是这种消息中间件保证安全与速度的发布的常用手段。相当于将发送消息与等待发送结果进行了解耦。

Kafka反馈结果给生产者的时机(可能造成数据丢失)

由于Kafka存在分区复制这么一个概念,我们一个分区的消息除了要传递到首领分区外,还行需要备份到其他副本分区中。那么也引出了Kafka反馈结果给生产者的时机这么一个概念。通过acks这个配置来设置反馈方案。

acks=0(消息无法保证到达Kafka)

生产者在写入消息之前不会等待任 何来自服务器的响应,容易丢消息,但是吞吐量高。

acks=1(消息保证到达Kafka,但数据可能丢失)

只要集群的首领节点收到消息,生产者会收到来自服务器的成功响应。如果消息无法到达首领节点(比如首领节点崩溃,新首领没有选举出来),生产者会收到一个错误响应,为了避免数据丢失,生产者会重发消息。默认使用这个配置。

acks=all(数据不丢失,但效率较低)

只有当所有参与复制的节点都收到消息,生产者才会收到一个来自服务器的成功响应。延迟高。

诶?此时你可能会有疑惑了。acks=1不是已经代表消息安全的到达了Kafka中了么?为啥还要提供这么一个低效的配置?

我们想一下,假设我们仅仅让首领节点收到消息就给生产者反馈接受成功。那么若是在备份到其他副本分区时(正在备份消息,还未备份成功),Kafka宕机了。此时这些副本分区就马上有了转正的机会,此时新选出的首领分区Brocker中是不存在那个还未备份成功的消息的。但是Kafka已经想生产者反馈接受成功了。此时这个消息就丢了。

因此,提供了这个效率不高,但是安全性极高的配置。可见Kafka中随时都需要在安全与效率之间做出权衡。

分区器与键的详解

我们在新增 ProducerRecord 对象中可以看到,ProducerRecord 包含了目标主题,键和值,Kafka 的消息都是一个个的键值对。键可以设置为默认的 null。

键的主要用途有两个:

  1. 用来决定消息被写往主题的哪个分区,拥有相同键的消息将被写往同一个分区,
  2. 可以作为消息的附加消息。

如果键值为 null,并且使用默认的分区器,分区器使用轮询算法将消息均衡地分布到各个分区上。

如果键不为空,并且使用默认的分区器,Kafka 对键进行散列(Kafka 自定义的散列算法,具体算法原理不知),然后根据散列值把消息映射到特定的分区上。很明显,同一个键总是被映射到同一个分区。但是只有不改变主题分区数量的情况下,键和分区之间的映射才能保持不变,一旦增加了新的分区,就无法保证了,所以如果要使用键来映射分区,那就要在创建主题的时候把分区规划好,而且永远不要增加新分区。 可见key的设置会严重影响Kafka的伸缩性,具体根据业务场景来选择是否使用。

自定义分区器

某些情况下,数据特性决定了需要进行特殊分区,比如电商业务,北京的业务量明显比较大,占据了总业务量的 20%,我们需要对北京的订单进行单独分区处理,默认的散列分区算法不合适了, 我们就可以自定义分区算法,对北京的订单单独处理,其他地区沿用Kafka自带的散列分区算法。甚至某些情况下,我们可以用 value 来进行分区。

Kafka的消费者

消费者往往远小于生产者

消费者的含义,同一般消息中间件中消费者的概念。在高并发的情况下,生产者产生消息的速度是远大于消费者消费的速度,单个消费者很可能会负担不起,此时有必要对消费者进行横向伸缩,于是我们可以使用多个消费者从同一个主题读取消息,对消息进行分流。

消费者中的基础概念

subscribe订阅

创建消费者后,使用 subscribe()方法订阅主题,这个方法接受一个主题列表为参数,也可以接受一个正则表达式为参数;正则表达式同样也匹配多个主题。如果新创建了新主题,并且主题名字和正则表达式匹配,那么会立即触发一次再均衡,消费者就可以读取新添加的主题。比如,要订阅所有和 test相关的主题,可以 subscribe(“tets.*”) 。

poll(long)轮询获取任务

Kafka消费者获取消息的逻辑和RabbitMQ是有区别的,RabbitMQ提供了推送和拉取任务两种模式来处理获取消息。但Kafka仅仅提供了拉取的方式。

为了不断的获取消息,我们要在循环中不断的进行轮询,也就是不停调用 poll 方法。 或许你会有一个疑问。poll方法会不会向阻塞队列一样,在没有消息时就阻塞在poll方法处,直到有订阅的分区中有消息时才会继续执行。经过博主验证。答案是不会阻塞,就是不断地去轮询获取。可能设计者认为Kafka所对应的项目一般业务量也会特别大,因此消费者基本也不怎么会停下来吧。

poll 方法的参数为超时时间,控制 poll 方法的阻塞时间,它会让消费者在指定的毫秒数内一直等待 broker 返回数据。poll 方法将会返回一个记录(消息)列表,每一条记录都包含了记录所属的主题信息,记录所在分区信息,记录在分区里的偏移量,以及记录的键值对。poll 方法不仅仅只是获取数据,在新消费者第一次调用时,它会负责查找群组,加入群组,接受分配的分区。如果发生了再均衡,整个过程也是在轮询期间进行的。

可见,Kafka处理任务的方式也是按批处理。当然,等待时间参数设置大的话,会降低响应效率,设置小了会造成IO交互变频繁。因此也需要我们开发者在此权衡。

提交和偏移量

当我们调用 poll 方法的时候,broker 返回的是生产者写入 Kafka 但是还没有被消费者读取过的记录,消费者可以使用 Kafka 来追踪消息在分区里的位置,我们称之为偏移量。(和数组的下标一样)。

消费者更新自己读取到哪个消息的操作,我们称之为提交。消费者是如何提交偏移量的呢?消费者会往一个叫做_consumer_offset 的特殊主题发送一个消息,里面会包括每个分区的偏移量。

既然有提交,那么也会有提交的方法与提交时机问题。我们在后续进行详细介绍。

消费者中的高级概念

群组协调

消费者要加入群组时,会向群组协调器发送一个 JoinGroup 请求,第一个加入群主的消费者成为群主,群主会获得群组的成员列表,并负责给每一个消费者分配分区。分配完毕后,群主把分配情况发送给群组协调器,协调器再把这些信息发送给所有的消费者,每个消费者只能看到自己的分配信息,只有群主知道群组里所有消费者的分配信息。群组协调的工作会在消费者发生变化(新加入或者掉线),主题中分区发生了变化(增加)时发生。

是消息中间件,又不仅仅是消息中间件的kafka_第10张图片

分区再均衡

当消费者群组里的消费者发生变化,或者主题里的分区发生了变化,都会导致再均衡现象的发生。从前面的知识中,我们知道,Kafka 中,存在着消费者对分区所有权的关系。

因此触发分区再均衡的场景有以下两个:

  1. 增加了消费者,新消费者会读取原本由其他消费者读取的分区,消费者减少,原本由它负责的分区要由其他消费者来读取。
  2. 增加了分区,哪个消费者来读取这个新增的分区。减小分区......(想什么呢,Kafka的分区只能变多,禁止变少)

这些行为,都会导致分区所有权的变化,这种变化就被称为再均衡

再均衡对 Kafka 很重要,这是消费者群组带来高可用性和伸缩性的关键所在。不过一般情况下,尽量减少再均衡,因为再均衡期间,消费者是无法读取消息的,会造成整个群组一小段时间的不可用。(STW)

Kafka如何检测消费者是否挂了

消费者通过向称为群组协调器的 broker(不同的群组有不同的协调器)发送心跳来维持它和群组的从属关系以及对分区的所有权关系。如果消费者长时间不发送心跳,群组协调器认为它已经死亡,就会触发一次再均衡。

消费者提交机制导致的消息丢失与重复

自动提交(可能导致消息丢失,重复)

首先,在博主第一次学习Kafka的时候,总是拿RabbitMQ的自动提交来进行类比。

RabbitMQ的自动提交,是当消费者收到消息的那一刻就直接提交给RabbitMQ了。

而Kafka使用的是轮询提交的方式。如果 enable.auto.comnit 被设为 true,消费者会自动把从 poll()方法接收到的最大偏移量提交上去。 提交时间间隔由 auto.commit.interval.ms 控制,默认值是 5s。

我们回忆一下,数据的获取方式,也是通过轮询获取的。

好家伙,数据轮循的获取,反馈轮回的提交。这俩玩意儿都是异步的了,那么就可能导致以下这两种个情况。


是消息中间件,又不仅仅是消息中间件的kafka_第11张图片

这看似简单的提交方式,让原本RabbitMQ中自动提交方式仅仅可能发生的丢失消息,变成了Kafka中可能导致消息的丢失+重复了。

消息重复

如图中情况1,当消费者实际处理的业务大于提交的偏移量。此时如果发生分区再均衡,那么就新分配的消费者就可能收到之前消费者已经执行过的业务消息。

消息丢失

如图中情况2。由于每次消费者提交的是poll方法接收到的最大偏移量,因此可能这个消息的消费者还进行消费,就通知Kafka:"我们搞定了!"。此时发生分区再均衡后,新的消费者也不会去接着原本消费者那些没处理完的消息,导致消息丢失。

自动提交机制的总结

无论是RabbitMQ还是Kafka,只要是和自动提交有关都绝对不安全。除了效率高一无是处。在生产环境下我认为基本上还是需要我们手动来配置这个地方来优化数据的可靠性的。为啥官方默认选择自动提交机制呢?我觉得就是设计者想追求极限的效率(方便给外界吹牛逼打广告)而采取的极为不优雅的手段。但这也恰恰成为了面试中常问到的点。因此面试时可以围绕着自动提交机制来铺开对Kafka的理解。

说了这么多,那具体如何保证消息的可靠呢?

手动同步提交(重复消息)

我们通过控制偏移量提交时间来消除丢失消息的可能性,并在发生再均衡时减少重复消息的数量。消费者 API 提供了另一种提交偏移量的方式,开发者可以在必要的时候提交当前偏移量,而不是基于时间间隔。

把 auto.commit. offset 设为 false,自行决定何时提交偏移量。使用 commitsync()提交偏移量最简单也最可靠。这个方法会提交由 poll()方法返回的最新偏移量,提交成功后马上返回,如果提交失败就抛出异常。

只要没有发生不可恢复的错误,commitSync()方法会阻塞,会一直尝试直至提交成功,如果失败,也只能记录异常日志。

当然,手动提交假设在消费者执行了消息执行了一半发生了分区再均衡,依旧会导致重复消息的产生。但我们知道重复消息是完全可以容忍的(通过幂等性)。

手动异步提交(重复消息)

手动提交时,在 broker 对提交请求作出回应之前,应用程序会一直阻塞。这时我们可以使用异步提交 commitsync(),我们只管发送提交请求,无需等待 broker的响应。

在成功提交或碰到无法恢复的错误之前, commitsync()会一直重试,但是 commitAsync 不会。它之所以不进行重试,是因为在它收到服务器响应的时候, 可能有一个更大的偏移量已经提交成功。

假设我们发出一个请求用于提交偏移量 2000,,这个时候发生了短暂的通信问题,服务器收不到请求,自然也不会作出任何响应。与此同时,我们处理了另外一批消息,并成功提交了偏移量 3000。如果 commitAsync()重新尝试提交偏移量 2000,它有可能在偏移量 3000 之后提交成功。这个时候如果发生再均衡, 就会出现重复消息。

commitAsync()也支持回调,在 broker 作出响应时会执行回调。回调经常被用于记录提交错误或生成度量指标。(好家伙,反复来回通知.......吐槽一下)。

总结

我们将自动改为手动,虽然无法解决重复消息的问题,但是可以搞定消息丢失的问题就保证了Kafka的高可用性。异步手动提交的方式可以大幅提升消费者的吞吐量。

消费者群组

之前也介绍过消费者群组了。此处我们用一组图片帮你加深印象。

点对点消费模式

多分区,1消费者

是消息中间件,又不仅仅是消息中间件的kafka_第12张图片

1<消费者<分区数

是消息中间件,又不仅仅是消息中间件的kafka_第13张图片

分区数=消费者

是消息中间件,又不仅仅是消息中间件的kafka_第14张图片

消费者>分区数

是消息中间件,又不仅仅是消息中间件的kafka_第15张图片

发布订阅模式

是消息中间件,又不仅仅是消息中间件的kafka_第16张图片

此模式下,可以,一个分区的消息可以通知不同的消费者来进行消费。RabbitMQ的Fanout路由器也能达到这个效果。

多主题,单消费者群组的分配策略

我们之前有提到过,一个Kafka集群中可以存在不同主题的Broker。那么不同主题的Broker是按怎样的规则分配消息给我们的消费者群组呢?

分区分配给消费者的策略。系统提供两种策略。默认为 Range。允许自定义策略。

Range

把主题的连续分区分配给消费者。(如果分区数量无法被消费者整除、第一个消费者会分到更多分区)

RoundRobin

把主题的分区循环分配给消费者

图示

是消息中间件,又不仅仅是消息中间件的kafka_第17张图片

 

Kafka如何保证有序性

Kafka 可以保证同一个分区里的消息是有序的。也就是说,发送消息时,主题只有且只有一个分区,同时生产者按照一定的顺序发送消息, broker 就 会按照这个顺序把它们写入分区,消费者也会按照同样的顺序读取它们。在某些情况下, 顺序是非常重要的。例如,往一个账户存入 100 元再取出来, 这个与先取钱再存钱是截然不同的。

重试机制导致有序性可能被破坏

我们之前说过,我们的生产者在调用Send方法想Kafka中投递消息时可能会失败。但Kafka内部支持消息重试机制。在符合重试机制的情况下,我们的顺序发送+单分区方式依旧无法绝对保证数据的有序性。情景如下:

是消息中间件,又不仅仅是消息中间件的kafka_第18张图片

可以发现,按照图中步骤执行,顺序就被打乱了。

那么如何处理这种情况呢?取消重试机制肯定不行,这样就可能导致数据丢失。

此时可以把生产者参数 max.in.flight.request.per.connection 设为 1,这样在生产者尝试发送第一批消息时,就不会有其他的消息发 送给 broker 。不过这样会严重影响生产者的吞吐量,所以只有在对消息的顺序有严格要求的情况下才能这么做。实际上,用Kafka去做顺序消费本身就是一个不符合开发者初衷的用法。因此在实际开发中我们尽量使用其他技术去保证数据的顺序性。

ISR机制(消费者什么时候能获取Kafka的消息)

回忆之前我们讲的Kafka反馈结果给生产者的时机的时候,我们为了保证消息的准确性,我们尽量要等待首领分区将所有数据同步到所有副本分区后,再反馈结果给我们的生产者。(acks=all)。

相对应的,我们的消费者什么时机才被允许去获取Kafka中的消息呢?

其实,并不是所有保存在分区首领上的数据都可以被消费者读取。大部分消费者只能读取已经被写入所有同步副本的消息。 分区首领知道每个消息会被复制到哪个副本上, 在消息还没有被写入所有同步副本之前, 是不会发送给消费者的,尝试获取这些消息的请求会得到空的响应而不是错误。

是消息中间件,又不仅仅是消息中间件的kafka_第19张图片

当然,这么做的原因和Kafka反馈结果给生产者需要完全同步的概念是一样的,因为图中的消息3,消息4如果分区首领宕机了,也会跟着丢失。因此消费者当然不能去消费这种不确定因素如此强的消息了。

Kafka 的数据复制是以 Partition 为单位的。而多个备份间的数据复制,通过 Follower 向 Leader 拉取数据完成。从一这点来讲,有点像 Master-Slave 方案。不同的是,Kafka 既不是完全的同步复制,也不是完全的异步复制,而是基于 ISR 的动态复制方案。

ISR,也即 In-Sync Replica。每个 Partition 的 Leader 都会维护这样一个列表,该列表中,包含了所有与之同步的 Replica(包含 Leader 自己)。每次数据写入时,只有 ISR 中的所有 Replica 都复制完,Leader 才会将其置为 Commit,它才能被 Consumer 所消费。

这种方案,与同步复制非常接近。但不同的是,这个 ISR 是由 Leader 动态维护的。如果 Follower 不能紧“跟上”Leader,它将被 Leader 从 ISR 中移除,待它又重新“跟上”Leader 后,会被 Leader 再次加加 ISR 中。每次改变 ISR 后,Leader 都会将最新的 ISR 持久化到 Zookeeper 中。

简单来说,通过ISR机制就可以保证消费者可以读到所有同步完成的消息了。

Kafka如何处理重复消息(幂等性)

让每个消息携带一个全局的唯一ID,即可保证消息的幂等性,具体消费过程为:

  1. 消费者获取到消息后先根据id去查询redis/db是否存在该消息(或者放到内存容器中)。
  2. 如果不存在,则正常消费,消费完毕后写入redis/db
  3. 如果存在,则证明消息被消费过,直接丢弃。

原来说的那么高大上,无非就是交给我们消费者自己去做去重校验。

Kafka流数据的处理

Stream是KafkaStreams提供的最重要的抽象。它代表的是无限的,不断更新的数据集,其中无限意味着大小不明确或者无大小限制。一个Stream是一个有序的,允许重放的不可变的数据记录。其中数据记录被定义为一个容错的键值对。

Kafka可以不断地对存储在其内部的数据进行流式处理,因此大数据环境下,Kafka根据其保存下来的数据不断进行分析。因此Kafka不仅仅用于做一个消息中间件,它可以被看成一种数据库,也可以被用于许多需要对大数据进行分析场景。细节此处暂时不一一细究。

 

 

你可能感兴趣的:(消息中间件)