读书笔记之《深入理解Kafka-核心设计与实践原理》-1~3章知识点

蓝星 2020年01月04日 439次浏览

第一章 初识Kafka

1.消息系统的角色

系统解耦、冗余存储、流量削峰填谷、缓冲、异步通信等

2. 基础概念

  1. Producer:生产者,发送消息的一方
  2. Consumer:消费者,接受消息的一方
  3. Broker:服务代理节点,一般可以看做一个独立的Kafka服务节点或者Kafka实例
  4. Topic:逻辑概念,消息以topic进行归类
  5. Partion:分区,一个topic可以包含多个partion,partion在存储层面是一个可追加的日志(Log)文件
  6. offset:消息偏移量,以partion为基础,作为分区中唯一标识,不可以跨分区。所以Kafka可以保证分区有序,不能保证topic有序
  7. Replica:partion的副本,主要做容灾使用,副本之间是“一主多从”的关系,一般replica都分布在不同的Broker上进行容灾处理。leader副本负责消息的读写,follower从leader副本同步消息及偏移量。leader宕机可进行重新选举。
  8. AR:Assigned Replicas分区中所有副本成为AR
  9. ISR:In-Sync Replicas,所有与leader副本保证一致同步的副本成为ISR,里面的消息和leader副本消息一致。当Producer发送消息时,只有ISR中的Replica都ack后才认为消息发送成功。存在的目的是保证消息不丢失
  10. OSR:和leader副本不一致的副本成为OSR,AR=ISR+OSR
  11. HW:High Water,高水位,标识一个特定的消息偏移量,消费者只能拉取这个offset之前的消息。
  12. LEO:Log End Offset,标识当前日志文件(partion)中下一条待写入消息的offset。ISR中LEO的最小值为当前分区的HW。消费者只能消费HW之前的消息。 image.png

第二章 生产者

知识点

  1. 消息序列化方式:broker接受到的消息必须是以字节数组(byte[])存在的。可以选择生产者和消费者消息的序列化方式,一般有StringSerializer、ByteArray、ByteBuffer等
  2. producer实例是线程安全的,因此可以多个线程共享单个实例或者使用池子将实例缓存起来。
  3. 消息发送方式:发后即忘(fier-and-forget),同步,异步。同步发送,如果发送失败会抛异常,异步发送可以指定CallBack方法回调。
  4. 分区器:如果发送消息时已经指定发送到哪个分区,不需要分区器进行分区。否则需要经过分区器决定将消息发送到哪个分区(按照计算出的key%partionCount)。
  5. 生产者拦截器:可以设置一些拦截器组成拦截链,用于对消息进行过滤

基础架构:

image.png

  1. 主线程用来生产消息、sender线程用来发送消息
  2. RecordAccumulator:消息收集器,用来缓存消息,方便sender线程批量发送消息。其中维护一个双端队列Dequeue,每个ProducerBatch(一个消息批次)中包含多个ProducerRecord,本身也是一个消息缓存。
  3. sender线程从RecordAccumulator获取的消息格式是Map<分区,Dequeue>。将不同分区对应的Dequeue发送到相应的分区。
  4. ProducerBatch和设置的batch.size有关,当一条消息(ProducerRecord)过来时会先判断当前ProducerBatch是否可以写入消息,如果不可以写入则重新创建一个ProducerBatch。

重要参数

  1. asks:指定分区中必须多少个副本收到消息才认为成功,默认ask=1,只要leader写入成功则认为成功。0:发送者不需要关心是否有副本写入成功(高吞吐)。-1或者all:所有ISR都必须写入成功才认为成功(高可靠)
  2. max.request.size:客户端发送消息的最大值
  3. retries和retry.backoff.ms:发送失败时最大的重试次数以及重试间隔。
  4. compression.type:消息压缩模式,默认“none”,可以选择gzip,snappy和lz4

第三章 消费者

  1. 消费组:每个消费组对应一个消费组,消息发布到主题后只会投递给消费组中的一个消费者。
  2. 一个消费者可以订阅一个或者多个topic
  3. 对于partion来说,每一条消息都有一条offset。消费者需要记录当前消费到的位置
  4. 消费者消费完消息后,需要提交消费位置偏移量,提交的偏移量是x+1,即指向下一条可以拉取的消息

位移提交

Kafka中位移提交是一个比较困难的点,如果处理不好会造成消息重复消费或者消息丢失

Kafka默认是在poll函数中直接提交位移。如上图所示,当消费者拉取了一批消息,消费到第5条时崩溃,则会造成新的位移未被提交,下次拉取时仍然从上次的位移处拉取,造成消息的重复消费。 而对于消息丢失,一般来说都是消费者将拉取的消息进行本地缓存,由另一个线程进行消费。当线程崩溃时,造成已经拉取的消息没有被消费,从而导致消息丢失。

在均衡过程

在均衡是指分区的所有权从一个消费者转移到另一个消费者,是消费组具备高可用性及伸缩性的保证。我们可以方便安全地增加或者删除消费组里的消费者。 分区策略(7.1章节):

RangeAssignor策略

按照消费者总数和分区总数进行整除运算获得,然后将分区按照跨度进行平均分配。对于每一个主题,该策略会将消费组内订阅这个主题的消费者按照字典名称排序,然后为每个消费者分配固定的分区范围。如果不够平均分配,按照字段顺序靠前的消费者被多分配一个分区

t0和t1两个主题,每个主题都有4个分区,被c0和c1消费者订阅,则分配结果如下: c0:t0p0,t0p1,t1p0,t1p1 c1:t0p2,t0p3,t1p2,t1p3

这样看似比较合理,但当分区数量不能被消费者数量整除时就会有问题。当上述主题只有三个分区时,按照RangeAssignor策略,分配结果如下

c0: t0p0,t0p1,t1p0,t1p1 c1: t0p2,t1p2

会造成分配不均衡。

RoundRobinAssignor策略

将消费组内所有消费者及消费者订阅的所有主题的分区按照字典进行排序,然后通过轮询的方式逐个将分区分配给每个消费者。 对于上述不均衡的分配,按照该策略分配结果如下:

c0: t0p0,t0p2,t1p1 c1: t0p1,t1p0,t1p2

相对来说比较公平。 但是当同一个消费组内消费者订阅的信息不相同时(理论上这种情况在实际生产中不应该出现),仍然会存在分配不均匀的问题。

3个消费者订阅了3个主题,3个主题分别有1,2,3个分区。这个消费者订阅了t0p0,t1p0,t1p2,t2p0,t2p1,t2p2六个分区,c0订阅了t0主题,c1订阅了t0,t1主题,c2订阅了t0,t1,t2主题,按照此策略分配结果如下: c0: t0p0 c1: t1p0 c2:t1p1,t2p0,t2p1,t2p2

分配结果仍然不均衡,理论上t1p1应该分配给c1的。

StickyAssignor分配策略

StickyAssignor分配策略主要有两个目的:1、分区分配尽可能的均匀 2、分区分配尽可能与上次分配保持相同。当两者发生冲突的时候,第一个目标优先于第二个目标 实际上是结合了RoundRobinAssignor

多线程实现

Consumer是线程不安全的,当多个线程在操作时会抛出异常。 多线程实现方式:

  1. 线程分封闭:每个线程实例化一个Consumer对象,每个Consumer对应一个或者多个partion。优点:实现起来比较简单,类似于开启多个消费者进程。缺点:每个线程都需要维护一个一个TCP链接,且线程开的太多会造成系统资源的浪费。 image.png
  2. 只有一个Consumer,Consumer实例将消息分配个不同的Handler线程消费。Consumer维护一个公共的提交offset(handler线程写写offset时需要加锁),Consumer拉取完消息,读取公共的offset进行提交。 image.png

对于上述方式,除了横向扩展能力外,还减少了系统自愿的消耗,但是维护公共的offset会有一定的困难,可能造成消息丢失。

假如handler1正在处理0~99的消息,handler2处理100~199的消息。handler2先处理完并提交了offset,handler1线程崩溃或者消费者宕机了,当消费者重启时或者分配新的消费者时,消息就会从200开始拉取,导致0~99的消息丢失。

为了解决消息丢失的问题,Kafka采用活动窗口式多线程消费方式。 image.png

滑动窗口内方格的个数表示消费的线程数,每个线程对应一个方格,每个方格对应一批消息。Consumer将方格内消息的offset进行缓存。 当滑动窗口最左侧消费完一个方格,则滑动窗口向右移动一格,拉取新的一格消息。 如果一个方格内的消息无法被标记为消费完成,那么startOffset就会悬停。当悬停超过一定时间(可以设置)时,则将现行进行本地重试消费,如果重试失败则转入重试队列,如果还失败则需要转入死信队列等待消费。