Kafka

什么是Kafaka

Kafka是一款基于发布与订阅的消息系统。
Kafka的数据是按照一定顺序持久化保存的,可以按需读取。此外,Kafka的数据分布在整个系统里,具备数据故障保护和性能伸缩能力。
Kafka为数据生态系统带来了循环系统,如图1-9所示。它在基础设施的各个组件之间传递消息,为所有客户端提供一致的接口。当与提供消息模式的系统集成时,生产者和消费者之间不再有紧密的耦合,也不需要在它们之间建立任何类型的直连。我们可以根据业务需要添加或移除组件,因为生产者不再关心谁在使用数据,也不关心有多少个消费者。
在这里插入图片描述

演变

原来服务间过度耦合
在这里插入图片描述
解耦后
在这里插入图片描述

消息和批次

Kafka的数据单元被称为消息。如果你在使用Kafka之前已经有数据库使用经验,那么可以把消息看成是数据库里的一个“数据行”或一条“记录”。

为了提高效率,消息被分批次写入Kafka。批次就是一组消息,这些消息属于同一个主题和分区。如果每一个消息都单独穿行于网络,会导致大量的网络开销,把消息分成批次传输可以减少网络开销。不过,这要在时间延迟和吞吐量之间作出权衡:批次越大,单位时间内处理的消息就越多,单个消息的传输时间就越长。批次数据会被压缩,这样可以提升数据的传输和存储能力,但要做更多的计算处理。

主题和分区

Kafka的消息通过主题进行分类。主题就好比数据库的表,或者文件系统里的文件夹。主题可以被分为若干个分区,一个分区就是一个提交日志。消息以追加的方式写入分区,然后以先入先出的顺序读取。要注意,由于一个主题一般包含几个分区,因此无法在整个主题范围内保证消息的顺序,但可以保证消息在单个分区内的顺序。图1-5所示的主题有4个分区,消息被追加写入每个分区的尾部。Kafka通过分区来实现数据冗余和伸缩性。分区可以分布在不同的服务器上,也就是说,一个主题可以横跨多个服务器,以此来提供比单个服务器更强大的性能。
图1-5:包含多个分区的主题表示

生产者和消费者

Kafka的客户端就是Kafka系统的用户,它们被分为两种基本类型:生产者和消费者。
生产者创建消息。一般情况下,一个消息会被发布到一个特定的主题上。生产者在默认情况下把消息均衡地分布到主题的所有分区上,而并不关心特定消息会被写到哪个分区。不过,在某些情况下,生产者会把消息直接写到指定的分区。这通常是通过消息键和分区器来实现的,分区器为键生成一个散列值,并将其映射到指定的分区上。这样可以保证包含同一个键的消息会被写到同一个分区上。
生产者也可以使用自定义的分区器,根据不同的业务规则将消息映射到分区。
消费者可能被称为订阅者或读者。消费者订阅一个或多个主题,并按照消息生成的顺序读取它们。消费者通过检查消息的偏移量来区分已经读取过的消息。偏移量是另一种元数据,它是一个不断递增的整数值,在创建消息时,Kafka会把它添加到消息里。在给定的分区里,每个消息的偏移量都是唯一的。消费者把每个分区最后读取的消息偏移量保存在Zookeeper或Kafka上,如果消费者关闭或重启,它的读取状态不会丢失。
消费者是消费者群组的一部分,也就是说,会有一个或多个消费者共同读取一个主题。群组保证每个分区只能被一个消费者使用。图1-6所示的群组中,有3个消费者同时读取一个主题。其中其中的两个消费者各自读取一个分区,另外一个消费者读取其他两个分区。消费者与分区之间的映射通常被称为消费者对分区的所有权关系。通过这种方式,消费者可以消费包含大量消息的主题。而且,如果一个消费者失效,群组里的其他消费者可以接管失效消费者的工作。
在这里插入图片描述

broker和集群

一个独立的Kafka服务器被称为broker。broker接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。broker为消费者提供服务,对读取分区的请求作出响应,返回已经提交到磁盘上的消息。根据特定的硬件及其性能特征,单个broker可以轻松处理数千个分区以及每秒百万级的消息量。
broker是集群的组成部分。每个集群都有一个broker同时充当了集群控制器的角色(自动从集群的活跃成员中选举出来)。控制器负责管理工作,包括将分区分配给broker和监控broker。在集群中,一个分区从属于一个broker,该broker被称为分区的首领。一个分区可以分配给多个broker,这个时候会发生分区复制(见图1-7)。这种复制机制为分区提供了消息冗余,如果有一个broker失效,其他broker可以接管领导权。不过,相关的消费者和生产者都要重新连接到新的首领。
在这里插入图片描述

保留消息

(在一定期限内)是Kafka的一个重要特性。Kafkabroker默认的消息保留策略是这样的:要么保留一段时间(比如7天),要么保留到消息达到一定大小的字节数(比如1GB)。当消息数量达到这些上限时,旧消息就会过期并被删除,所以在任何时刻,可用消息的总量都不会超过配置参数所指定的大小。主题可以配置自己的保留策略,可以将消息保留到不再使用它们为止。例如,用于跟踪用户活动的数据可能需要保留几天,而应用程序的度量指标可能只需要保留几个小时。可以通过配置把主题当作紧凑型日志,只有最后一个带有特定键的消息会被保留下来。这种情况对于变更日志类型的数据来说比较适用,因为人们只关心最后时刻发生的那个变更。

为什么选择Kafka

多个生产者

多个消费者

Kafka也支持多个消费者从一个单独的消息流上读取数据,而且消费者之间互不影响。这与其他队列系统不同,其他队列系统的消息一旦被一个客户端读取,其他客户端就无法再读取它。另外,多个消费者可以组成一个群组,它们共享一个消息流,并保证整个群组对每个给定的消息只处理一次。

基于磁盘的数据存储

Kafka消费者非实时地读取消息,这要归功于Kafka的数据保留特性。消息被提交到磁盘,根据设置的保留规则进行保存。每个主题可以设置单独的保留规则,以便满足不同消费者的需求,各个主题可以保留不同数量的消息。消费者可能会因为处理速度慢或突发的流量高峰导致无法及时读取消息,而持久化数据可以保证数据不会丢失。消费者可以在进行应用程序维护时离线一小段时间,而无需担心消息丢失或堵塞在生产者端。消费者可以被关闭,但消息会继续保留在Kafka里。消费者可以从上次中断的地方继续处理消息。

伸缩性

为了能够轻松处理大量数据,Kafka从一开始就被设计成一个具有灵活伸缩性的系统。用户在开发阶段可以先使用单个broker,再扩展到包含3个broker的小型开发集群,然后随着数据量不断增长,部署到生产环境的集群可能包含上百个broker。对在线集群进行扩展丝毫不影响整体系统的可用性。也就是说,一个包含多个broker的集群,即使个别broker失效,仍然可以持续地为客户提供服务。要提高集群的容错能力,需要配置较高的复制系数。

高性能

上面提到的所有特性,让Kafka成为了一个高性能的发布与订阅消息系统。通过横向扩展生产者、消费者和broker,Kafka可以轻松处理巨大的消息流。

生产者

我们从创建一个ProducerRecord对象开始,ProducerRecord对象需要包含目标主题和要发送的内容。我们还可以指定键或分区。在发送ProducerRecord对象时,生产者要先把键和值对象序列化成字节数组,这样它们才能够在网络上传输。接下来,数据被传给分区器。如果之前在ProducerRecord对象里指定了分区,那么分区器就不
会再做任何事情,直接把指定的分区返回。如果没有指定分区,那么分区器会根据ProducerRecord对象的键来选择一个分区。选好分区以后,生产者就知道该往哪个主题和分区发送这条记录了。紧接着,这条记录被添加到一个记录批次里,这个批次里的所有消息会被发送到相同的主题和分区上。有一个独立的线程负责把这些记录批次发送到相应的broker上。服务器在收到这些消息时会返回一个响应。如果消息成功写入Kafka,就返回一个RecordMetaData对象,它包含了主题和分区信息,以及记录在分区里的偏移量。如果写入失败,则会返回一个错误。生产者在收到错误之后会尝试重新发送消息,几次之后如果还是失败,就返回错误信息。
在这里插入图片描述

依赖

        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
            <version>2.1.8.RELEASE</version>
        </dependency>

API

调用 Spring Kafka 给我们封装的KafkaTemplate来发送消息,里面有一系列的send重载方法

org.springframework.kafka.core.KafkaTemplate<K, V> 
public ListenableFuture<SendResult<K, V>> send(@NotNull String topic,K key, V data)

配置

spring:
  kafka:
    producer:
      bootstrap-servers: 172.20.21.114:9091,172.20.21.114:9092,172.20.21.114:9093
      #  累积消息触发发送的字节数 值大 性能好 , 实时性差,可靠性差. 详见org.apache.kafka.clients.producer.ProducerConfig.BATCH_SIZE_DOC
      #  和 linger.ms 属性 只有有一个条件满足就会发送消息
      batch-size: 100
      #  消息的缓存容量  值不可低于batch-size.
      #  一定要比最大的消息大, 不然消息发布出去  消息长度超过此值会发送失败并爆出RecordTooLargeException
      buffer-memory: 102400
      #  指定消息key和消息体的编解码方式
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
      #  重试次数
      retries: 0
      properties:
        #  累积消息触发发送的毫秒数(默认值0) 值大 性能好 实时性差. 详见org.apache.kafka.clients.producer.ProducerConfig.LINGER_MS_DOC
        #  和 batch-size 属性 只有有一个条件满足就会发送消息
        linger.ms: 5
      #   控制可靠性的关键  是否同步等待服务端应答数  0 不等待 1 learder应答  all  全部副本应答
      acks: all

消费者

消费者和消费者群组

Kafka消费者从属于消费者群组。一个群组里的消费者订阅的是同一个主题,每个消费者接收主题一部分分区的消息。
假设主题T1有4个分区,我们创建了消费者C1,它是群组G1里唯一的消费者,我们用它订阅主题T1。消费者C1将收到主题T1全部4个分区的消息,如图4-1所示。

在这里插入图片描述
如果在群组G1里新增一个消费者C2,那么每个消费者将分别从两个分区接收消息。我们假设消费者C1接收分区0和分区2的消息,消费者C2接收分区1和分区3的消息,如图4-2所示。

妮哈·纳克海德(NehaNarkhede);格温·沙皮拉(GwenShapira);托德·帕利诺(ToddPalino).Kafka权威指南(Kindle位置1190-1192).人民邮电出版社.Kindle版本.
如果群组G1有4个消费者,那么每个消费者可以分配到一个分区,如图4-3所示。

妮哈·纳克海德(NehaNarkhede);格温·沙皮拉(GwenShapira);托德·帕利诺(ToddPalino).Kafka权威指南(Kindle位置1193).人民邮电出版社.Kindle版本.
如果我们往群组里添加更多的消费者,超过主题的分区数量,那么有一部分消费者就会被闲置,不会接收到任何消息,如图4-4所示。

妮哈·纳克海德(NehaNarkhede);格温·沙皮拉(GwenShapira);托德·帕利诺(ToddPalino).Kafka权威指南(Kindle位置1194-1195).人民邮电出版社.Kindle版本.
比如把数据写到数据库或HDFS,或者使用数据进行比较耗时的计算。在这些情况下,单个消费者无法跟上数据生成的速度,所以可以增加更多的消费者,让它们分担负载,每个消费者只处理部分分区的消息,这就是横向伸缩的主要手段。我们有必要为主题创建大量的分区,在负载增长时可以加入更多的消费者。不过要注意,不要让消费者的数量超过主题分区的数量,多余的消费者只会被闲置。
在上面的例子里,如果新增一个只包含一个消费者的群组G2,那么这个消费者将从主题T1上接收所有的消息,与群组G1之间互不影响。群组G2可以增加更多的消费者,每个消费者可以消费若干个分区,就像群组G1那样,如图4-5所示。总的来说,群组G2还是会接收到所有消息,不管有没有其他群组存在。
简而言之,为每一个需要获取一个或多个主题全部消息的应用程序创建一个消费者群组,然后往群组里添加消费者来伸缩读取能力和处理能力,群组里的每个消费者只处理一部分消息。

妮哈·纳克海德(NehaNarkhede);格温·沙皮拉(GwenShapira);托德·帕利诺(ToddPalino).Kafka权威指南(Kindle位置1208-1210).人民邮电出版社.Kindle版本.

消费者群组和分区再均衡

分区。一个新的消费者加入群组时,它读取的是原本由其他消费者读取的消息。当一个消费者被关闭或发生崩溃时,它就离开群组,原本由它读取的分区将由群组里的其他消费者来读取。在主题发生变化时,比如管理员添加了新的分区,会发生分区重分配。分区的所有权从一个消费者转移到另一个消费者,这样的行为被称为再均衡。再均衡非常重要,它为消费者群组带来了高可用性和伸缩性(我们可以放心地添加或移除消费者),不过在正常情况下,我们并不希望发生这样的行为。在再均衡期间,消费者无法读取消息,造成整个群组一小段时间的不可用。另外,当分区被重新分配给另一个消费者时,消费者当前的读取状态会丢失,它有可能还需要去刷新缓存,在它重新恢复状态之前会拖慢应用程序。
消费者通过向被指派为群组协调器的broker(不同的群组可以有不同的协调器)发送心跳来维持它们和群组的从属关系以及它们对分区的所有权关系。只要消费者以正常的时间间隔发送心跳,就被认为是活跃的,说明它还在读取分区里的消息。消费者会在轮询消息(为了获取消息)或提交偏移量时发送心跳。如果消费者停止发送心跳的时间足够长,会话就会过期,群组协调器认为它已经死亡,就会触发一次再均衡。如果一个消费者发生崩溃,并停止读取消息,群组协调器会等待几秒钟,确认它死亡了才会触发再均衡。在这几秒钟时间里,死掉的消费者不会读取分区里的消息。在清理消费者时,消费者会通知协调器它将要离开群组,协调器会立即触发一次再均衡,尽量降低处理停顿。

提交和偏移量

每次调用poll()方法,它总是返回由生产者写入Kafka但还没有被消费者读取过的记录,我们因此可以追踪到哪些记录是被群组里的哪个消费者读取的。之前已经讨论过,Kafka不会像其他JMS队列那样需要得到消费者的确认,这是Kafka的一个独特之处。相反,消费者可以使用Kafka来追踪消息在分区里的位置(偏移量)。我们把更新分区当前位置的操作叫作提交。那么消费者是如何提交偏移量的呢?消费者往一个叫作_consumer_offset的特殊主题发送消息,消息里包含每个分区的偏移量。如果消费者一直处于运行状态,那么偏移量就没有什么用处。不过,如果消费者发生崩溃或者有新的消费者加入群组,就会触发再均衡,完成再均衡之后,每个消费者可能分配到新的分区,而不是之前处理的那个。为了能够继续之前的工作,消费者需要读取每个分区最后一次提交的偏移量,然后从偏移量指定的地方继续处理。如果提交的偏移量小于客户端处理的最后一个消息的偏移量,那么处于两个偏移量之间的消息就会被重复处理,如图4-6所示。
在这里插入图片描述

API

调用SpringKafka给我们封装的@KafkaListener注解来定义消息

    @KafkaListener(groupId = "${jlpay.commons.kafka.groupid}", topics = "${jlpay.commons.kafka.topic}")
    public void listen(ConsumerRecord<String, String> record, Acknowledgment ack) {
    }

配置

spring:
  kafka:
    #  topic分片会均匀分配到消费者
    #  消费者大于分片数量,浪费,不会消费
    consumer:
      bootstrap-servers: 172.20.21.114:9091,172.20.21.114:9092,172.20.21.114:9093
      #  如果在消息处理完成前就提交了offset,那么就有可能造成数据的丢失。由于Kafka consumer默认是自动提交位移的,
      #  所以在后台提交位移前一定要保证消息被正常处理了,因此不建议采用很重的处理逻辑,如果处理耗时很长,
      #  则建议把逻辑放到另一个线程中去做。为了避免数据丢失 在消息被完整处理之后再手动提交位移
      enable-auto-commit: false
      #  earliest 获取log头部的数据(可能有重复), latest获取log尾部的数据(可能会丢失消息)
      auto-offset-reset: earliest
      #  指定默认消费者group id(会被KafkaListener注解的groupId属性覆盖) 同一个group id 只有一条消息只有一个消费者可以收到消息 ;
      #  不同同group id 只有一条消息每个group下面都会有一个消费者可以收到消息
      group-id: group-id-test
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      #  每次最多拉几条消息  值大性能好,
      max-poll-records: 10
      properties:
        max-poll-interval-ms: 600000  # 消费者每次拉消息的间隔时间, 如果大于这个时间消费者分区会重新分配


    listener:
      #  指定listener 容器中的线程数,用于提高并发量.  一个线程对应一个消费者, 消费者数量 多余分区数量没用
      concurrency: 1
      # 手动提交 设置enable-auto-commit 为 false (若为true则无法启动且抛出IllegalArgumentException), 并在KafkaListener注解的方法中引入org.springframework.kafka.support.Acknowledgment参数, 调用ack方法即可手动提交
      #  MANUAL_IMMEDIATE会立刻提交, MANUAL是批量处理的
      #  reference: https://segmentfault.com/a/1190000011541255
      ack-mode: MANUAL_IMMEDIATE

参考

大家可参考gitlab上面的demo
https://gitlab.jlpay.com/java/produce/baseserver/commons/tree/dev-kafka/commons-parent/commons-kafka/sample

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值