MQ系列3-kafka

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/huanshirenjian/article/details/89792910

1.kafka

kafka天生就是持久化、分布式,而且单台并发支持百万级。
(1)消息
消息,Kafka里的数据单元,也就是我们一般消息中间件里的消息的概念。消息由字节数组组成。消息还可以包含键,用以对消息选取分区。
消息的保留策略是要么保留一段时间,要么保留一定大小。到了限制,旧消息过期并删除。每个主题可以配置自己的保留策略。
(2)批次
为了提高效率,消息被分批写入Kafka。批次就是一组消息,这些消息属于同一个主题和分区。但是,这个需要权衡,批次里包含的消息越多,单位时间内处理的消息就越多,单个消息的传输时间就越长。如果进行压缩,可以提升数据的传输和存储能力,但需要更多的计算处理。
(3)主题和分区
Kafka里的消息用主题进行分类,主题下有可以被分为若干个分区。分区本质上是个提交日志,有新消息,这个消息就会以追加的方式写入分区,然后用先入先出的顺序读取。主题会有多个分区,所以在整个主题的范围内,是无法保证消息的顺序的,单个分区则可以保证。
Kafka通过分区来实现数据冗余和伸缩性,因为分区可以分布在不同的服务器上,那就是说一个主题可以跨越多个服务器。
(4)生产者和消费者、偏移量、消费者群组
生产者默认情况下把消息均衡分布到主题的所有分区上,如果需要指定分区,则需要使用消息里的消息键和分区器。
消费者订阅主题,按照消息的生成顺序读取。消费者通过检查所谓的偏移量来区分消息是否读取过。偏移量是一种元数据,一个不断递增的整数值,创建消息的时候,Kafka会把他加入消息。在一个分区里,每个消息的偏移量是唯一的。每个分区最后读取的消息偏移量会保存到Zookeeper或者Kafka上,这样分区的消费者关闭或者重启,读取状态都不会丢失。
多个消费者可以构成一个消费者群组。群组可以保证每个分区只被一个消费者使用。
(5)Broker和集群
一个独立的Kafka服务器叫Broker。broker的主要工作是,接收生产者的消息,设置偏移量,提交消息到磁盘保存;为消费者提供服务,响应请求,返回消息。在合适的硬件上,单个broker可以处理上千个分区和每秒百万级的消息量。
多个broker可以组成一个集群。每个集群中broker会选举出一个集群控制器。控制器会进行管理,包括将分区分配给broker和监控broker。
集群里,一个分区从属于一个broker,这个broker被称为首领。但是分区可以被分配给多个broker,这个时候会发生分区复制。
分区复制带来的好处是,提供了消息冗余。一旦首领broker失效,其他broker可以接管领导权。当然相关的消费者和生产者都要重新连接到新的首领上。

2.kafka配置文件

kafka配置主要是在server.properties文件。

  • broker.id:每一个broker在集群中的唯一表示,要求是正数。当该服务器的IP地址发生改变时,broker.id没有变化,则不会影响consumers的消息情况
  • listeners:监听列表,以逗号分隔 不同的协议。
  • zookeeper.connect:zookeeper集群的地址,多个之间用逗号分割
  • log.dirs:存放这些数据的目录
  • num.recovery.threads.per.data.dir:用于日志恢复启动和关闭时的线程数量。因为这些线程只是服务器启动和关闭时会用到。所以完全可以设置大量的线程来达到并行操作的目的。注意,这个参数指的是每个日志目录的线程数,比如本参数设置为8,而log.dirs设置为了三个路径,则总共会启动24个线程。
  • auto.create.topics.enable:是否允许自动创建主题。如果设为true,那么produce,consume或者fetch metadata一个不存在的主题时,就会自动创建。缺省为true。
  • num.partitions:新建主题的分区个数。
  • log.retention.hours:日志保存时间,默认为7天(168小时)。超过这个时间会清理数据。类似还有log.retention.minutes和log.retention.ms,都设置的话,优先使用具有最小值的那个。
  • log.retention.bytes:topic每个分区的最大文件大小,一个topic的大小限制 = 分区数*log.retention.bytes。-1没有大小限制。
  • log.segment.bytes:分区的日志文件大小,当尺寸达到这个数值时,就会关闭当前文件,并创建新文件。被关闭的文件就开始等待过期。默认为1G。
    如果一个主题每天只接受100MB的消息,那么根据默认设置,需要10天才能填满一个文件。而且因为日志片段在关闭之前,消息是不会过期的,所以如果log.retention.hours保持默认值的话,那么这个日志片段需要17天才过期。因为关闭日志片段需要10天,等待过期又需要7天。类似的有log.segment.ms,以先到的为准。
  • message.max.bytes:一个服务器能够接收处理的消息的最大字节数,注意这个值producer和consumer必须设置一致,且不要大于fetch.message.max.bytes属性的值。该值默认是1000000字节,大概900KB~1MB。

3.发送消息

3.1发送消息流程

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

3.2 发送消息方式

(1)发送并忘记
忽略send方法的返回值,不做任何处理。大多数情况下,消息会正常到达,而且生产者会自动重试,但有时会丢失消息。
(2)同步非阻塞发送
获得send方法返回的Future对象,在合适的时候调用Future的get方法。
(3)异步发送
实现接口org.apache.kafka.clients.producer.Callback,然后将实现类的实例作为参数传递给send方法。

3.3 发送消息配置

(1)acks
指定了必须要有多少个分区副本收到消息,生产者才会认为写入消息是成功的
acks=0:生产者在写入消息之前不会等待任何来自服务器的响应,容易丢消息,但是吞吐量高。
acks=1:只要集群的首领节点收到消息,生产者会收到来自服务器的成功响应。如果消息无法到达首领节点(比如首领节点崩溃,新首领没有选举出来),生产者会收到一个错误响应,为了避免数据丢失,生产者会重发消息。不过,如果一个没有收到消息的节点成为新首领,消息还是会丢失。默认使用这个配置。
acks=all:只有当所有参与复制的节点都收到消息,生产者才会收到一个来自服务器的成功响应。延迟高。
(2)buffer.memory
设置生产者内存缓冲区的大小,生产者用它缓冲要发送到服务器的消息。如果数据产生速度大于向broker发送的速度,导致生产者空间不足,producer会阻塞或者抛出异常。缺省33554432 (32M)
(3)max.block.ms
指定了在调用send()方法或者使用partitionsFor()方法获取元数据时生产者的阻塞时间。当生产者的发送缓冲区已满,或者没有可用的元数据时,这些方法就会阻塞。在阻塞时间达到max.block.ms时,生产者会抛出超时异常。缺省60000ms
(4)retries
发送失败时,指定生产者可以重发消息的次数。默认情况下,生产者在每次重试之间等待100ms,可以通过参数retry.backoff.ms参数来改变这个时间间隔。缺省0
(5)receive.buffer.bytes和send.buffer.bytes
指定TCP socket接受和发送数据包的缓存区大小。如果它们被设置为-1,则使用操作系统的默认值。如果生产者或消费者处在不同的数据中心,那么可以适当增大这些值,因为跨数据中心的网络一般都有比较高的延迟和比较低的带宽。缺省102400
(6)batch.size
当多个消息被发送同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算。当批次内存被填满后,批次里的所有消息会被发送出去。但是生产者不一定都会等到批次被填满才发送,半满甚至只包含一个消息的批次也有可能被发送。缺省16384(16k)
(6)linger.ms
指定了生产者在发送批次前等待更多消息加入批次的时间。它和batch.size以先到者为先。也就是说,一旦我们获得消息的数量够batch.size的数量了,他将会立即发送。设置默认为0,即没有延迟。
(7)compression.type
producer用于压缩数据的压缩类型。默认是无压缩。正确的选项值是none、gzip、snappy。
(8)client.id
当向server发出请求时,这个字符串会发送给server。目的是能够追踪请求源头,以此来允许ip/port许可列表之外的一些应用可以发送信息。这项应用可以设置任意字符串,因为没有任何功能性的目的,除了记录和跟踪。
(9)max.in.flight.requests.per.connection
指定了生产者在接收到服务器响应之前可以发送多个消息,值越高,占用的内存越大,当然也可以提升吞吐量。发生错误时,可能会造成数据的发送顺序改变,默认是5 (修改)。
如果需要保证消息在一个分区上的严格顺序,这个值应该设为1。不过这样会严重影响生产者的吞吐量。
(10)request.timeout.ms
客户端将等待请求的响应的最大时间,如果在这个时间内没有收到响应,客户端将重发请求;超过重试次数将抛异常
(11)metadata.fetch.timeout.ms
是指我们所获取的一些元数据的时间。元数据包含:topic,host,partitions。此项配置是指当等待元数据fetch成功完成所需要的时间,否则会跑出异常给客户端
(12)timeout.ms
此配置选项控制broker等待副本确认的最大时间。如果确认的请求数目在此时间内没有实现,则会返回一个错误。这个超时限制是以server端度量的,没有包含请求的网络延迟。这个参数和acks的配置相匹配。
(13)max.request.size
控制生产者发送请求最大大小。注意:broker具有自己对消息记录尺寸的覆盖,如果这个尺寸小于生产者的这个设置,会导致消息被拒绝。

3.4 发送消息顺序

Kafka可以保证同一个分区里的消息是有序的。不过有时候也不一定。如果把retires设为非零整数,同时把max.in.flight.request.per.connection设为比1 大的数,那么,如果第一个批次消息写入失败,而第二个批次写入成功, broker 会重试写入第一个批次。如果此时第一个批次也写入成功,那么两个批次的顺序就反过来了。
如果对消息的顺序有严格要求,可以把max.in.flight.request.per.connection 设为1,这样在生产者尝试发送第一批消息时,就不会有其他的消息发送给broker 。不过这样会严重影响生产者的吞吐量。

3.5 分区

Kafka的消息都是一个个的键值对。键可以设置为默认的null。如果键值为null,并且使用默认的分区器,分区器使用轮询算法将消息均衡地分布到各个分区上。
如果键不为空,并且使用默认的分区器,Kafka对键进行散列,然后根据散列值把消息映射到特定的分区上。很明显,同一个键总是被映射到同一个分区。但是只有不改变主题分区数量的情况下,键和分区之间的映射才能保持不变。消费者不要大于分区数,多余的不会消费数据。
(1)分区再均衡
消费者变化,比如增加了消费者,新消费者会读取原本由其他消费者读取的分区,消费者减少,原本由它负责的分区要由其他消费者来读取,增加了分区,哪个消费者来读取这个新增的分区,这些行为,都会导致分区所有权的变化,这种变化就被称为再均衡。再均衡对Kafka很重要,这是消费者群组带来高可用性和伸缩性的关键所在。不过一般情况下,尽量减少再均衡,因为再均衡期间,消费者是无法读取消息的,会造成整个群组一小段时间的不可用。
消费者通过向称为群组协调器的broker(不同的群组有不同的协调器)发送心跳来维持它和群组的从属关系以及对分区的所有权关系。如果消费者长时间不发送心跳,群组协调器认为它已经死亡,就会触发一次再均衡。
(2)消费者分区分配的过程
消费者要加入群组时,会向群组协调器发送一个JoinGroup请求,第一个加入群主的消费者成为群主,群主会获得群组的成员列表,并负责给每一个消费者分配分区。分配完毕后,群组把分配情况发送给群组协调器,协调器再把这些信息发送给所有的消费者,每个消费者只能看到自己的分配信息,只有群主知道群组里所有消费者的分配信息。这个过程在每次再均衡时都会发生。

4.消费消息

4.1 订阅主题

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

4.2 轮询

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

4.3 KafkaConsumer

KafkaConsumer的实现不是线程安全

4.4 消费者配置

(1)fetch.min.bytes
每次fetch请求时,server应该返回的最小字节数。如果没有足够的数据返回,请求会等待,直到足够的数据才会返回。缺省为1个字节。多消费者下,可以设大这个值,以降低broker的工作负载。
(2)fetch.wait.max.ms
如果没有足够的数据能够满足fetch.min.bytes,则此项配置是指在应答fetch请求之前,server会阻塞的最大时间。缺省为500个毫秒。和上面的fetch.min.bytes结合起来,看哪个条件先满足。
(3)max.partition.fetch.bytes
指定了服务器从每个分区里返回给消费者的最大字节数,默认1MB,而且一旦有消费者崩溃,这个内存还需更大。注意,这个参数要比服务器的message.max.bytes更大,否则消费者可能无法读取消息。
(4)session.timeout.ms
如果consumer在这段时间内没有发送心跳信息,则它会被认为挂掉了。默认3秒。
(5)auto.offset.reset
消费者在读取一个没有偏移量的分区或者偏移量无效的情况下,如何处理。默认值是latest,从最新的记录开始读取,另一个值是earliest,表示消费者从起始位置读取分区的记录。
注意:默认值是latest,意思是说,在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的记录),可以先启动生产者,再启动消费者,观察到这种情况。
(6)enable .auto.commit
默认值true,表明消费者是否自动提交偏移。
(7)partition.assignment.strategy
分区分配给消费者的策略。系统提供两种策略。默认为Range。
(1)Range
把主题的连续分区分配给消费者。
(2)RoundRobin
把主题的分区循环分配给消费者。
(3)自定义策略
继承AbstractPartitionAssignor。
(4)client.id
客户端id
(5)max.poll.records
控制每次poll方法返回的的记录数量。
(6)receive.buffer.bytes和send.buffer.bytes
指定TCP socket接受和发送数据包的缓存区大小。如果它们被设置为-1,则使用操作系统的默认值。如果生产者或消费者处在不同的数据中心,那么可以适当增大这些值,因为跨数据中心的网络一般都有比较高的延迟和比较低的带宽。

4.5 偏移量

当我们调用poll方法的时候,broker返回的是生产者写入Kafka但是还没有被消费者读取过的记录,消费者可以使用Kafka来追踪消息在分区里的位置,我们称之为偏移量。消费者更新自己读取到哪个消息的操作,我们称之为提交。
消费者是如何提交偏移量的呢?消费者会往一个叫做_consumer_offset的特殊主题发送一个消息,里面会包括每个分区的偏移量。发生了再均衡之后,消费者可能会被分配新的分区,为了能够继续工作,消费者者需要读取每个分区最后一次提交的偏移量,然后从指定的地方,继续做处理。
如果提交的偏移量小于消费者实际处理的最后一个消息的偏移量,处于两个偏移量之间的消息会被重复处理。如果提交的偏移量大于客户端处理的最后一个消息的偏移量,那么处于两个偏移量之间的消息将会丢失。

4.6 提交

(1)自动提交
最简单的提交方式是让消费者自动提交偏移量。 如果 enable.auto.comnit被设为 true,消费者会自动把从poll()方法接收到的最大偏移量提交上去。提交时间间隔由auto.commit.interval.ms控制,默认值是5s。自动提交是在轮询里进行的,消费者每次在进行轮询时会检査是否该提交偏移量了,如果是,那么就会提交从上一次轮询返回的偏移量。
(2)手动提交
我们通过控制偏移量提交时间来防止丢失消息,并在发生再均衡时减少重复消息的数量。把auto.commit. offset设为 false,自行决定何时提交偏移量。使用 commitsync()提交偏移量最简单也最可靠。这个方法会提交由poll()方法返回的最新偏移量,提交成功后马上返回,如果提交失败就抛出异常。由于commitsync()将会提交由poll()返回的最新偏移量,所以在处理完所有记录后要确保调用了 commitsync(),否则还是会有丢失消息的风险。如果发生了再均衡,从最近批消息到发生再均衡之间的所有消息都将被重复处理。
(3)异步提交
手动提交时,在broker对提交请求作出回应之前,应用程序会一直阻塞。这时我们可以使用异步提交API,我们只管发送提交请求,无需等待broker的响应。在成功提交或碰到无法恢复的错误之前, commitsync()会一直重试,但是 commitAsync不会。它之所以不进行重试,是因为在它收到服务器响应的时候,可能有一个更大的偏移量已经提交成功。commitAsync()也支持回调。
(4)特定提交
消费者API允许在调用 commitsync()和 commitAsync()方法时传进去希望提交的分区和偏移量的map。
(5)再均衡监听器
在提交偏移量一节中提到过,消费者在退出和进行分区再均衡之前,会做一些清理工作比如,提交偏移量、关闭文件句柄、数据库连接等。在调用 subscribe()方法时传进去一个 ConsumerRebalancelistener实例就可以了。
ConsumerRebalancelistener有两个需要实现的方法。

  1. public void onPartitionsRevoked( Collection< TopicPartition> partitions)方法会在
    再均衡开始之前和消费者停止读取消息之后被调用。如果在这里提交偏移量,下一个接管分区的消费者就知道该从哪里开始读取了
  2. public void onPartitionsAssigned( Collection< TopicPartition> partitions)方法会在重新分配分区之后和消费者开始读取消息之前被调用。
    (6)从特定偏移量处开始
    seekToBeginning(Collection tp)和 seekToEnd( Collectiontp)以及seek方法。

4.7 优雅退出

如果确定要退出循环,需要通过另一个线程调用 consumer.wakeup()方法。如果循环运行在主线程里,可以在 ShutdownHook里调用该方法。要记住, consumer.wakeup()是消费者唯一一个可以从其他线程里安全调用的方法。调用 consumer. wakeup()可以退出poll(),并抛出 WakeupException异常。我们不需要处理 Wakeup Exception,因为它只是用于跳出循环的一种方式。不过,在退出线程之前调用 consumer.close()是很有必要的,它会提交任何还没有提交的东西,并向群组协调器发送消息,告知自己要离开群组,接下来就会触发再均衡,而不需要等待会话超时。

5.集群

5.1集群中的成员

Kafka使用 zookeeper来维护集群成员的信息。每个 broker都有个唯一标识符, 这个标识符可以在配置文件里指定, 也可以自动生成。 在 broker启动的时候, 它通过创建临时节点把自己的ID注册到zk。 Kafka组件订阅zk的/brokers/ids路径 , 当有 broker加入集群或退出集群时, 这些组件就可以获得通知 。
如果你要启动另一个具有相同 ID的 broker, 会得到一个错误。新 broker会试着进行注册,但不会成功, 因为 zookeeper里已经有一个具有相同 ID的 broker。
在 broker停机、 出现网络分区或长时间垃圾回收停顿时, broker会从zk上断开连接, 此时 broker在启动时创建的临时节点会自动从 Zookeeper上移除。 监听 broker列表的Kafka 组件会被告知该 broker已移除。
在关闭 broker时, 它对应的节点也会消失, 不过它的 ID会继续存在于其他数据结构中 。 例如,主题的副本列表里就可能包含这些ID。在完全关闭一个broker之后, 如果使用相同的ID启动另一个全新的 broker, 它会立刻加入集群, 并拥有与旧 broker 相同的分区和主题。

5.2 控制器

控制器其实就是一个 broker, 只不过它除了具有一般 broker的功能之外, 还负责分区首领的选举。 集群里第一个启动的 broker通过在Zookeeper里创建一个临时节点/controuer让自己成为控制器。 其他 broker在启动时也会尝试创建这个节点,不过它们会收到一个“节点已存在”的异常,然后“意识”到控制器节点已存在, 也就是说集群里已经有一个控制器了 。其他 broker在控制器节点上创建Zookeeperwatch对象,这样它们就可以收到这个节点的变更通知。这种方式可以确保集群里一次只有一个控制器存在。
当控制器发现一个 broker已经离开集群,它就知道,那些失去首领的分区需要一个新首领 。控制器遍历这些分区, 并确定谁应该成为新首领 (简单来说就是分区副本列表里的下一个副本) , 然后向所有包含新首领或现有跟随者的 broker发送请求。该请求消息包含了谁是新首领以及谁是分区跟随者的信息。随后,新首领开始处理来自生产者和消费者的情求,而跟随者开始从新首领那里复制消息。
当控制器发现一个 broker加入集群时, 它会使用 broker ID来检査新加入的 broker是否包含现有分区的副本。 如果有, 控制器就把变更通知发送给新加入的broker和其他 broker, 新broker上的副本开始从首领那里复制消息。
简而言之, Kafka使用 Zookeeper的临时节点来选举控制器,并在节点加入集群或退出集群时通知控制器。 控制器负责在节点加入或离开集群时进行分区首领选举。

5.3 复制

复制功能是Kafka架构的核心。复制之所以这么关键,是因为它可以在个别节点失效时仍能保证 Kafka的可用性和持久性。
Kafka使用主题来组织数据, 每个主题被分为若干个分区,每个分区有多个副本。那些副本被保存在 broker上, 每个 broker可以保存成百上千个属于不同主题和分区的副本。
(1)副本类型

  • 首领副本:每个分区都有一个首领副本。为了保证一致性,所有生产者请求和消费者请求都会经过这个副本。
  • 跟随者副本:首领以外的副本都是跟随者副本。跟随者副本不处理来自客户端的请求,它们的任务就是从首领那里复制消息, 保持与首领一致的状态 。

(2)工作机制
首领的另一个任务是搞清楚哪个跟随者的状态与自己是一致的。为了与首领保持同步, 跟随者向首领发送获取数据的请求, 这种请求与消费者为了读取消息而发送的请求是一样的。首领将响应消息发给跟随者。请求消息里包含了跟随者想要获取消息的偏移量, 而且这些偏移量总是有序的 。
通过査看每个跟随者请求的最新偏移量, 首领就会知道每个跟随者复制的进度。如果跟随者在10s内没有请求任何消息,或者虽然在请求消息,但在10s内没有请求最新的数据,那么它就会被认为是不同步的。如果一个副本无法与首领保持一致,在首领发生失效时,它就不可能成为新首领,因为它没有包含全部的消息。相反,持续请求得到的最新消息副本被称为同步副本。在首领发生失效时,只有同步副本才有可能被选为新首领。
跟随者成为不同步副本之前的时间可以通过replica.lag.time.max.ms参数来配置的。 这个时间间隔直接影响着首领选举期间的客户端行为和数据保留机制 。。
除了当前首领之外, 每个分区都有一个优先副本(首选首领),创建主题时选定的首领分区就是分区的优先副本。之所以把它叫作优先副本, 是因为在创建分区时, 需要在 broker之间均衡首领副本。因此, 我们希望首选首领在成为真正的首领时, broker间的负载最终会得到均衡。默认情况下, Kafka的auto.leader.rebalance.enable被设为 true,它会检査优先副本是不是当前首领,如果不是,并且该副本是同步的, 那么就会触发首领选举, 让优先副本成为当前首领。

5.4 请求处理

broker的大部分工作是处理客户端、分区副本和控制器发送给分区首领的请求。 Kafka提供了一个二进制协议(基于TCP),指定了请求消息的格式以及 broker如何对请求作出响应。
客户端发起连接并发送请求,broker处理请求并作出响应。 broker按照请求到达的顺序来处理它们这种顺序保证让Kaka具有了消息队列的特性,同时保证保存的消息也是有序的。
所有的请求消息都包含一个标准消息头:

  • Request type(也就是 API key)
  • Request version( broker可以处理不同版本的客户端请求,并根据客户端版本作出不同的响应)
  • Correlation id-一个具有唯一性的数字,用于标识请求消息,同时也会出现在响应消息和错误日志里
  • Client Id用于标识发送请求的客户端
  • broker会在它所监听的每一个端口上运行一个 Acceptor线程,这个线程会创建一个连接并把它交给 Processor线程去处理。 Processor线程数量是可配置的。Processor线程负责从客户端获取请求消息,把它们放进请求队列,然后从响应队列获取响应消息,把它们发送给客户端。

在这里插入图片描述
请求消息被放到请求队列后,IO线程会负责处理它们。比较常见的请求类型有:
生产请求:生产者发送的请求,它包含客户端要写入 broker的消息。
获取请求:在消费者和跟随者副本需要从 broker读取消息时发送的请求。

在这里插入图片描述
生产请求和获取请求都必须发送给分区的首领副本。如果broker 收到一个针对特定分区的请求,而该分区的首领在另一个broker 上,那么发送请求的客户端会收到一个“非分区首领”的错误响应。当针对特定分区的获取请求被发送到一个不含有该分区首领的broker上,也会出现同样的错误。Kafka 客户端要自己负责把生产请求和获取请求发送到正确的broker 上。
那么客户端怎么知道该往哪里发送请求呢?客户端使用了另一种请求类型,也就是元数据请求。这种请求包含了客户端感兴趣的主题列表。服务器端的响应消息里指明了这些主题所包含的分区、每个分区都有哪些副本, 以及哪个副本是首领。元数据请求可以发送给任意一个broker ,因为所有broker 都缓存了这些信息。
一般情况下,客户端会把这些信息缓存起来,并直接往目标broker 上发送生产请求和获取请求。它们需要时不时地通过发送元数据请求来刷新这些信息(刷新的时间间隔通过meta.max.age.ms参数来配置),从而知道元数据是否发生了变更, 比如,在新broker 加入集群时,部分副本会被移动到新的broker 上。另外,如果客户端收到“非首领”错误,它会在尝试重发请求之前先刷新元数据,因为这个错误说明了客户端正在使用过期的元数据信息,之前的请求被发到了错误的broker 上。
(1)生产请求
包含首领副本的 broker在收到生产请求时, 会对请求做一些验证。
发送数据的用户是否有主题写入权限,请求里包含的acks值是否有效(只允许出现0、1或all) ,如果 acks=all, 是否有足够多的同步副本保证消息已经被安全写入。之后消息被写入本地磁盘。在Linux系统上,消息会被写到文件系统缓存里,并不保证它们何时会被刷新到磁盘上。
Kafka不会一直等待数据被写到磁盘上,它依赖复制功能来保证消息的持久性。在消息被写入分区的首领之后, broker开始检査 acks配置参数一如果 acks被设为 0或1, 那么 broker立即返回响应;如果acks被设为all,那么请求会被保存在一个叫作炼狱的缓冲区里, 直到首领发现所有跟随者副本都复制了消息, 响应才会被返回给客户端。
(2)获取请求
客户端发送请求,向 broker请求主题分区里具有特定偏移量的消息。客户端应该指定 broker最多可以从一个分区里返回多少数据。 如果没有这个限制, broker返回的大量数据有可能耗尽客户端的内存。
我们之前讨论过,请求需要先到达指定的分区首领上,然后客户端通过査询元数据来确保请求的路由是正确的。首领在收到请求时,它会先检査请求是否有效,比如,指定的偏移量在分区上是否存在,如果客户端请求的是已经被删除的数据,或者请求的偏移量不存在, 那么 broker将返回一个错误。如果请求的偏移量存在, broker将按照客户端指定的数量上限从分区里读取消息, 再把消息返回给客户端。
Kafka使用零复制技术向客户端发送消息一一也就是说, Kafka直接把消息从文件(或者更确切地说是 Linux文件系统缓存)里发送到网络通道,而不需要经过任何中间缓冲区。 这是 Kafka与其他大部分数据库系统不一样的地方。
(3)ISR
并不是所有保存在分区首领上的数据都可以被客户端读取。大部分客户端只能读取已经被写入所有同步副本的消息。 分区首领知道每个消息会被复制到哪个副本上, 在消息还没有被写入所有同步副本之前, 是不会发送给消费者的,尝试获取这些消息的请求会得到空的响应而不是错误。
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中。

5.5 物理存储机制

Kafka的基本存储单元是分区。分区无法在多个broker间进行再细分,也无法在同一个broker的多个磁盘上进行再细分。
(1)分区分配
在创建主题时, Kafka首先会决定如何在 broker间分配分区。假设你有6个 broker, 打算创建一个包含10个分区的主题,并且复制系数为3。那么 Kafka就会有30个分区副本, 它们可以被分配给6个 broker。 在进行分区分配时, 要达到如下的目标。

  • 在 broker间平均地分布分区副本。
  • 确保每个分区的每个副本分布在不同的 broker上。
    (2)文件管理
    Kafka为每个主题配置了数据保留期限, 规定数据被删除之前可以保留多长时间, 或者清理数据之前可以保留的数据量大小 。
    这个在之前配置中讲过
    (3)文件格式
    Kafka的消息和偏移量保存在文件里。保存在磁盘上的数据格式与从生产者发送过来或者发送给消费者的消息格式是一样的 。 因为使用了相同的消息格式进行磁盘存储和网络传输, Kafka可以使用零复制技术给消费者发送消息, 同时避免了对生产者已经压缩过的消息进行解压和再圧缩。
    除了键、值和偏移量外,消息里还包含了消息大小、校验和、消息格式版本号、压缩算法(snappy、 Gzip或Lz4)和时间戳(在0.10.0版本里引入的)。时间戳可以是生产者发送消息的时间, 也可以是消息到达 broker的时间, 这个是可配置的。
    如果生产者发送的是圧缩过的消息, 那么同一个批次的消息会被压缩在一起, 被当作 “包装消息”进行发送。于是, broker就会收到一个这样的消息,然后再把它发送给消费者。 消费者在解压这个消息之后, 会看到整个批次的消息, 它们都有自己的时间戳和偏移量。
    如果在生产者端使用了压缩功能(极力推荐),那么发送的批次越大,就意味着在网络传输和磁盘存储方面会获得越好的压缩性能, 同时意味着如果修改了消费者使用的消息格式 (例如, 在消息里增加了时间戳) , 那么网络传输和磁盘存储的格式也要随之修改, 而且 broker要知道如何处理包含了两种消息格式的文件。
    Kafka附带了一个叫 DumpLogSegment的工具, 可以用它査看片段的内容。 它可以显示每个消息的偏移量、校验和、魔术数字节、消息大小和压缩算法。
    (4)索引
    消费者可以从Kafka的任意可用偏移量位置开始读取消息。为了帮助 broker更快地定位到指定的偏移量, Kafka 为每个分区维护了一个索引。索引把偏移量映射到片段文件和偏移量在文件里的位置。
    索引也被分成片段, 所以在删除消息时, 也可以删除相应的索引 。 如果索引出现损坏, Kafka会通过重新读取消息并录制偏移量和位置来重新生成索引。

猜你喜欢

转载自blog.csdn.net/huanshirenjian/article/details/89792910