kafka笔记

399 阅读23分钟

Kafka是一款基于发布与订阅的消息系统。它一般被称为
“分布式提交日志”或者“分布式流平台”。文件系统或数据库提交日志用来提供所有事务的持久记录,通过重放这些日志可以重建系统的状态。同样地, Kafka的数据是按照一定顺序持久化保存的,可以按需读取。 此外, Kafka的数据分布在整个系统里,具备数据故障保护和性能伸缩能力。

总体架构

image.png

选择kafka的理由

  1. 多个生产者

Kafka可以无缝地支持多个生产者,不管客户端在使用单个主题还是多个主题。所以它很适合用来从多个前端系统收集数据,并以统一的格式对外提供数据。例如,一个包含了多个微服务的网站,可以为页面视图创建个单独的主题,所有服务都以相同的消息格式向该主题写入数据。消费者应用程序会获得统一的页面视图,而无需协调来自不同生产者的数据流。

  1. 多个消费者

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

  1. 基于磁盘的数据存储

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

  1. 伸缩性

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

  1. 高性能

上面提到的所有特性,让Kafka成为了一个高性能的发布与订阅消息系统。通过横向扩展 生产者、消费者和 broker,Kafka可以轻松处理巨大的消息流。在处理大量数据的同时, 它还能保证亚秒级的消息延迟。

重要概念

消息和批次

kafka的数据单元被称为消息。消息由字节数组组成,对kafka来说,消息里面的数据没有什么特殊的含义。消息可以有一个可选的元数据,也就是键。键也是一个字节数组,对于kafka来说没有特别的含义。当想把消息可控地写入不同的分区时,会用到键。

为了提高效率,消息被分批次地写入kafka。批次就是一组消息,这些消息属于同一个主题和分区。如果每个消息都单独穿行于网络,会导致大量的网络开销,分批次传输则可以减少网络开销。不过这需要在低延迟和高吞吐之间作出权衡,批次越大,单位时间内处理的消息就越多,单个消息传输时间越长。

主题和分区

Kafka 的消息通过主题进行分类。主题就好比数据库的表,或者文件系统里的文件夹。主题可以被分为若干个分区,一个分区就是一个提交日志。消息以追加的方式写入分区,然后以先入先出的顺序读取。要注意,由于一个主题一般包含几个分区,因此无发在整个主题范围内保证消息的顺序,但可以保证消息在单个分区内的顺序。 多个分区的主题表示如下:

我们通常会使用流这个词来描述 kafka 这类系统的数据。很多时候人们把一个主题的数 据看成一个流,不管它有多少个分区。流是一组从生产者移动到消费者的数据。当我们讨论流式处理时,一般都是这样描述消息的。Kafka Streams,Apache Samza 和Storm 这些框架以实时的方式处理消息,也就是所谓的流式处理。我可以将流式处理与离线处理进行比较,比如Hadoop就是被设计用于在稍后某个时刻处理大量的数据。

生产者消费者

Kafka的客户端就是Kafka系统的用户,它们被分为两种基本类型:生产者消费者。

生产者创建消息。在其他发布与订阅系统中,生产者可能被称为发布者写入者。一般情况下,一个消息会被发布到一个特定的主题上。生产者在默认情况下把消息均衡地分布到主题的所有分区上,而并不关心特定消息会被写到哪个分区。不过,在某些情况下,生产者会把消息直接写到指定的分区。这通常是通过消息键和分区器来实现的,分区器为键生成一个散列值,并将其映射到指定的分区上。这样可以保证包含同一个键的消息会被写到同一个分区上。生产者也可以使用自定义的分区器,根据不同的业务规则将消息映射到分区。

消费者读取消息。在其他发布与订阅系统中,消费者可能被称为订阅者读者。 消费者订阅一个或多个主题,并按照消息生成的顺序读取它们。消费者通过检查消息的偏移量来区分已经读取过的消息。偏移量是另一种元数据,它是个不断递增的整数值,在创建消息时, Kafka会把它添加到消息里。在给定的分区里,每个消息的偏移量都是唯一的。消费者把每个分区最后读取的悄息偏移量保存在Zookeeper或Kafka上,如果消费者关闭或重启,它的读取状态不会丢失。

消费者是消费者群组的一部分,也就是说,会有一个或多个消费者共同读取一个主题。群组保证每个分区只能被一个消费者使用。消费者与分区之间的映射通常被称为消费者对分区的所有权关系

broker和集群

一个独立的 Kafka 服务器被称为broker。broker接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。broker为消费者提供服务,对读取分区的请求作出响应,返回已经提交到磁盘上的消息。根据特定的硬件及其性能特征,单个broker可以轻松处理数千个分区以及每秒百万级的消息量。

broker 是集群的组成部分。每个集群都有一个broker同时充当了集群控制器的角色(自动从集群的活跃成员中选举出来)。控制器负责管理工作,包括将分区分配给 broker 和监控broker。在集群中,一个分区从属于一个broker,该broker被称为分区的首领。一个分区可以分配给多个broker,这个时候会发生分区复制(见下图)。这种复制机制为分区提供了消息冗余,如果有一个broker失效,其他broker可以接管领导权。不过,相关的消费者和生产者都要重新连接到新的首领。

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

生产者

消息的发送过程:

ProducerRecord是消息对象,包括:

  • topic 主题名字,必选
  • partition 分区,可选
  • key 键,可选
  • value 值,必选

在发送 ProduceRecord对象时,生产者要先把键和值对象序列化成字节数组,这样它们才能够在网络上传输。接下来,数据被传给分区器。如果之前在 ProducerRecord 对象里指定了分区,那么分区器就不会再做任何事情,直接把指定的分区返回。如果没有指定分区,那么分区器会根据ProducerRecord对象的键来选择一个分区。选好分区以后,生产者就知道该往哪个主题和分区发送这条记录了。紧接着,这条记录被添加到一个记录批次里,这个批次里的所有消息会被发送到相同的主题和分区上。有一个独立的线程负责把这些记录批次发送到相应的broker 上。

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

创建生产者

要往 Kafka 写入消息,首先要创建一个生产者对象,井设置一些属性。Kafka 生产者有3个必选的属性。

  • bootstrap.severs

该属性指定 broker 的地址清单,地址的格式为 host:port。清单里不需要包含所有的 broker 地址,生产者会从给定的broker里查找到其他broker的信息。不过建议至少要提供两个broker信息,一旦其中一个宕机,生产者仍然能够连接到集群上。

  • key.serializer

broker 希望接收到的消息的键和值都是字节数组。生产者接口允许使用参数化类型,因 此可以把 Java 对象作为键和值发送给 broker。这样的代码具有良好的可读性,不过生 产者需要知道如何把这些 Java 对象转换成字节数组。 key.serializer必须被设置为一 个实现了org.apache.kafka.common.serialization.Serializer 接口的类,生产者会使 用这个类把键对象序列化成字节数组。 Kafka 客户端默认提供了 ByteArraySerializer (这个只做很少的事情),StringSerializer 和 IntegerSerializer,因此,如果你只 使用常见的几种 Java 对象类型,那么就没必要实现自己的序列化器。要注意, key.serializer是必须设置的,就算你打算只发送值内容。

  • value.serializer

与key.serializer一样,value.serializer指定的类会将值序列化。如果键和值都是 符串,可以使用与key.serializer一样的序列化器。如果键是整数类型而值是字符串 那么需要使用不同的序列化器。

生产者发送方式

实例化生产者对象后,接下来就可以开始发送消息了。发送消息主要有以下3种方式。

  • 发送并忘记(fire-and-forget)

我们把消息发送给服务器,但井不关心它是否正常到达。大多数情况下,消息会正常到 达,因为kafka是高可用的,而且生产者会自动尝试重发。不过,使用这种方式有时候 也会丢失一些消息。

  • 同步发送

我们使用send()方怯发送消息 它会返回 Future 对象,调用get()方法进行等待 就可以知道悄息是否发送成功。

  • 异步发送

我们调用send()方怯,并指定一个回调函数,服务器在返回响应时调用该函数。

生产者其他配置

生产者还有很多可配置参数,在 kafka 文档里都有说明,它们大部分都有合理的默认值 所以没有必要去修改它 。不过有几个参数在内存使用、性能和可靠性方面对生产者影 响比较大,接下来简要说明。

  • acks

指定了必须要有多少个分区副本收到消息,生产者才会认为消息写入是成功的。

  • buffer.memory

设置生产者内存缓冲区的大小,生产者用它缓冲要发送到服务器的消息。

  • compression.type

默认情况下,消息发送时不会被压缩。该参数可以设置为 snappy,gzip和lz4 ,它指定了消息被发送给 broker 之前使用哪一种压缩算也进行压缩。

  • retries

决定了生产者可以重发消息的次数,如果达到这个次数,生产者会放弃重试并返回错误。

  • batch.size

指定了一个批次可以使用的内存大小。

  • linger.ms

指定了生产者在发送批次之前等待更多消息加入批次的时间。与batch.size配合使用。

  • client.id

任意的字符串,服务器会用它来识别消息的来橱。

  • max.in.flight.requests.per.connection

指定了生产者在接到服务器晌应之前可以发送多少个消息。它的值越高,就会占用 越多的内存,不过也会提升吞吐量。把它设为1可以保证消息是按照发送的顺序写入服 务器的,即使发生了重试。

  • request.timeout.ms

指定了生产者在发送数据 等待服务器返回响应的时间。

  • metadata.fetch.timeout.ms

指定了生产者在获取元数据(比如目标分区的首领是谁)时等待服务器返回响应的时间

  • max.request.size

控制生产者发送的请求大小。

顺序保证

Kafka 可以保证同一个分区里的消息是有序的。也就是说,如果生产者按照 一定的顺序发送消息, broker 就会按照这个顺序把它们写入分区,消费者 按照同样的顺序读取它们。

如果retries设为非零整数,同时把max.in.flight.requests.per.connection 设为比1大的数,那么,如果第1个批次消息写入失败,而第2个批次写入 成功, broker 会重试写入第1个批次。如果此时第1个批次也写入成功,那 么两个批次的顺序就反过来了。

如果某些场景要求消息是有序的,那么消息是否写入成功很关键的,所以不建议把retries设为0。可以max.in.flight.requests.per.connection设为1,这样在生产者尝试发送第一批消息时,就不会有其他的消息发送给broker。不过这样会严重影响生产者的吞吐量 ,所以只有在对消息的顺序有严格要求的情况下才能这么做。

消费者

Kafka 消费者从属于消费者群组。一个群组里的消费者订阅的是同1个主题,每个消费 者接收主题一部分分区的消息。

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

如果在群组 G1 里新增 个消费者 C2 ,那么每个消费者将分别从两个分区接收消息。我们 假设消费者 Cl 接收分区 0 和分区 2 的消息,消费者 C2 接收分区1 和分区 3 的消息,如下图所示。

如果群组有4个消费者,那么每个消费者可以分配到1个分区,如下图所示。

如果我们往群组里添加更多的消费者,超过主题的分区数量,那么有一部分消费者就会被闲置,不会接收到任何消息,如下图所示。

消费者群组和分区再均衡

一个新的悄费者加入群组时,它读取的是原本由其他消费者读取的消息。当一个消费者被关闭或发生崩愤时,它就离开群组,原本由它读取的分区将由群组里的其他消费者来读取。在主题发生变化时 比如管理员添加了新的分区,会发生分区重分配。

分区的所有权从一个消费者转移到另一个消费者,这样的行为被称为再均衡。 再均衡非常重要, 它为消费者群组带来了高可用性和伸缩性。不过在正常情况下,我们并不希望发生这样的行为。在再均衡期间,消费者无法读取消息,造成整个群组小段时间的不可用。另外,当分区被重新分配给另一个消费者时,消费者当前的读取状态会丢失,它有可能还需要去刷新缓存,在它重新恢复状态之前会拖慢应用程序。

消费者通过向被指派为群组协调器的 broker (不同的群组可以有不同的协调器)发送心跳来维持它们和群组的从属关系以及它们对分区的所有权关系。只要消费者以正常的时间间隔发送心跳,就被认为是活跃的,说明它还在读取分区里的消息。消费者会在轮询消息(为了获取消息)或提交偏移量时发送心跳。如果消费者停止发送心跳的时间足够长,会话就会过期,群组协调器认为它已经死亡,就会触发一次再均衡。

如果一个消费者发生崩愤,并停止读取消息,群组协调器会等待几秒钟,确认它死亡了才触发再均衡。在这几秒钟时间里,死掉的消费者不会读取分区里的消息。在清理消费者时,消费者会通知协调器它将要离开群组,协调器会立即触发一次再均衡,尽量降低处理停顿。

0.10.1 版本里, kafka 社区引入了一个独立的心跳线程,可以在轮均消息的空档发 送心跳。这样一来,发送心跳的频率(也就是消费者群纽用于检测发生崩溃的消费者 或不再发送心跳的消费者的时间)与消息轮询的频率(由处理消息所花费的时间未确 定)之间就是相互独立的。

分配分区的过程

当消费者要加入群组时,它会向群组协调器发送一个joinGroup请求。第 个加入群组的消费者将成为“群主”。群主从协调器那里获得群组的成员列 表(列表中包含了所有最近发送过心跳的消费者,它们被认为是活跃的), 并负责给每一个消费者分配分区。

分配完毕之后,群主把分配情况列表发送给群组协调器,协调器再把这些信息发 送给所有消费者。每个消费者只能看到自己的分配信息,只有群主知道群组 里所有消费者的分配信息。这个过程会在每次再均衡时重复发生。

创建消费者

与创建生产者类似,我们只需要使用3个必要的属性: bootstrap.servers, key.deserializer和value.deserializer。

消费者的其他配置

  • fetch.min.bytes

指定了消费者从服务器获取记录的最小字节数。 broker 在收到消费者的数据请求时, 如果可用的数据量小于fetch.min.bytes指定的大小,那么它会等到有足够的可用数据时 才把它返回给消费者。

  • fetch.max.wait.ms

指定 broker 的等待时间,默认是 500ms 。如果没有足够的数据流入 Kafka ,消费者获取最小数据量的要求就得不到满足,最终导致 500m 延迟。

  • max.partition.fetch.bytes

指定了服务器从每个分区里返回给消费者的最大字节数。它的默认值是1MB。

  • session.timeout.ms

指定了消费者在被认为死亡之前可以与服务器断开连接的时间,默认是 3s。 如果消费者没有在指定的时间内发送心跳给群组协调器,就被认为 已经死亡,协调器就会触发再均衡。

  • auto.offset.reset

指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下(因消费者长 时间失效,包含偏移量的记录已经过时井被删除)该作何处理。它的默认值是 latest 意思是说,在偏移量无效的情况下,消费者将从最新的记录开始读取数据。另一个值是 ealiest ,意思是说,在偏移量无效的情况下,消费者将从起始位置读取分区的记录。

  • enable.auto.commit

指定了消费者是否自动提交偏量,默认值是true。为了尽量避免出现重复数据和数据丢失,可以把它设为 false。由自己控制何时提交偏移量。如果把它设为 true ,还可以通过配置 auto.commit.interval.ms属性来控制提交的频率。

  • partition.assignment.strategy

选择分区策略。

Range(默认策略) 该策略会把主题的若干个连续的分区分配给消费者。假设消费者 C1 和消费者 C2 同时 订阅了主题 T1 和主题 T2 ,并且每个主题有 3 个分区。那么消费者 C1 有可能分配到这两个主题的分区 0 和分区 1 ,而消费者 C2 分配到这两个主题的分区2 。因为每个主题拥有奇数个分区,而分配是在主题内独立完成的,第 1个消费者最后分配到比第 2个消费者更多的分区。只要使用了 range策略,而且分区数量无法被消费者数量整除,就出现这种情况。

RoundRobin 该策略把主题的所有分区逐个分配给消费者。如果使用 RoundRobin 策略来给消费者 C1 和消费者 C2 分配分区,那么消费者C1 将分到主题 T1 的分区 0分区 2,以及主题 T2 的分区1 ,消费者 C2 将分配到主题 T1 分区1, 以及主题T2的分区0 和分区2 。一般 来说 ,如果所有消费者都订阅相同的主题(这种情况很常见), RoundRobin 策略会给所有消费者分配相同数量的分区(或最多就差1个分区)。

  • client.id

是任意字符串。 broker 用它来标识从客户端发送过来的消息。

  • max.poll.records

用于控制单次调用call()方法能够返回的记录数量。

提交和偏移量

更新分区当前位置的操作叫作提交

消费者往一个叫作 _consumer_offset的特殊主题发送消息,消息里包含每个分区的偏移量。 如果消费者一直处于运行状态,那么偏移量就没有什么用处。不过,如果悄费者发生崩愤或者有新的消费者加入群组,就会触发再均衡,完成再均衡之后,每个消费者可能分配到新的分区,而不是之前处理的那个。为了能够继续之前的工作,消费者需要读取每个分区最后一次提交的偏移量,然后从偏移量指定的地方继续处理。

提交偏移量的方式:

  • 自动提交

如果 enable.auto.commit 被设为 true ,那么每过5s,消费者会自动把从 poll()方法接收到的最大偏移量提交上去。

假设我们仍然使用默认的 5s提交时间间隔,在最近一次提交之后的 3s 发生了再均衡,再均衡之后,消费者从最后一次提交的偏移量位置开始读取消息。这个时候偏移量已经落后3s,所以在这3s内到达的消息会被重复处理。可以通过修改提交时间间隔来更频繁地提交偏移量,减小可能出现重复消息的时间长,不过这种情况是无也完全避免的。

  • 提交当前偏移量(同步提交)

enable.auto.commit设为 false,使用 commitSync()提交偏移量。这个 API 会提交由 poll()方法返回的最新偏移量,提交成功后马上返回,如果提交失败就抛出异常。只要没有发生不可恢复的错误,commitSync()方法会一直尝试直至提交成功。

  • 异步提交

同步提交有一个不足之处,在broker对提交请求作出回应之前,应用程序会一直阻塞,影响吞吐量。所以还有一个API,commitAsync(), 提交失败它不会重试。 它之所以不进行重试,是因为在它收到服务器相应的时候,可能有一个更大的偏移量已经提交成功,如果重试,可能发生重复消费。

kafka如何复制

复制功能是Kafka架构的核心。如上面所讲到的,kafka使用topic组织数据,topic 有多个分区,每个分区有多个副本。副本有以下两种类型:

  • 首领副本 每个分区都有一个首领副本,所有的生产者和消费者请求都会经过它
  • 随从副本 除了首领副本其他都是随从副本。随从副本不负责处理客户端请求,它唯一要做的就是从首领那儿复制消息。如果首领副本奔溃,其中一个随从副本会被提升为新的首领副本。