你说嘴巴嘟嘟,ok我是扣码,现在开始学kafka

kafka特性

  • 高吞吐、低延迟

kakfa 最大的特点就是收发消息非常快,kafka 每秒可以处理几十万条消息,它的最低延迟只有几毫秒。

  • 高伸缩性

每个主题(topic) 包含多个分区(partition),主题中的分区可以分布在不同的主机(broker)中。

  • 持久性、可靠性

Kafka 能够允许数据的持久化存储,消息被持久化到磁盘,并支持数据备份防止数据丢失。

  • 容错性

允许集群中的节点失败,某个节点宕机,Kafka 集群能够正常工作。


  • producer

消息生产者,发布消息到Kafka集群的终端或服务

  • broker

Kafka集群中包含的服务器,一个borker就表示kafka集群中的一个节点

  • topic

每条发布到Kafka集群的消息属于的类别,即Kafka是面向 topic 的。
更通俗的说Topic就像一个消息队列,生产者可以向其写入消息,消费者可以从中读取消息,一个Topic支持多个生产者或消费者同时订阅它,所以其扩展性很好。

  • partition

每个 topic 包含一个或多个partition。Kafka分配的单位是partition

  • replica

partition的副本,保障 partition 的高可用。

  • consumer

从Kafka集群中消费消息的终端或服务

  • consumer group

每个 consumer 都属于一个 consumer group,每条消息只能被 consumer group 中的一个 Consumer 消费,但可以被多个 consumer group 消费。

  • leader

每个partition有多个副本,其中有且仅有一个作为Leader,Leader是当前负责数据的读写的partition。 producer 和 consumer 只跟 leader 交互

  • follower

Follower跟随Leader,所有写请求都通过Leader路由,数据变更会广播给所有Follower,Follower与Leader保持数据同步。如果Leader失效,则从Follower中选举出一个新的Leader。

  • controller

知道大家有没有思考过一个问题,就是Kafka集群中某个broker宕机之后,是谁负责感知到他的宕机,以及负责进行Leader Partition的选举?如果你在Kafka集群里新加入了一些机器,此时谁来负责把集群里的数据进行负载均衡的迁移?包括你的Kafka集群的各种元数据,比如说每台机器上有哪些partition,谁是leader,谁是follower,是谁来管理的?如果你要删除一个topic,那么背后的各种partition如何删除,是谁来控制?还有就是比如Kafka集群扩容加入一个新的broker,是谁负责监听这个broker的加入?如果某个broker崩溃了,是谁负责监听这个broker崩溃?这里就需要一个Kafka集群的总控组件,Controller。他负责管理整个Kafka集群范围内的各种东西。

  • zookeeper

(1) Kafka 通过 zookeeper 来存储集群的meta元数据信息
(2)一旦controller所在broker宕机了,此时临时节点消失,集群里其他broker会一直监听这个临时节点,发现临时节点消失了,就争抢再次创建临时节点,保证有一台新的broker会成为controller角色。

  • offset
    • 偏移量

消费者在对应分区上已经消费的消息数(位置),offset保存的地方跟kafka版本有一定的关系。
kafka0.8 版本之前offset保存在zookeeper上。
kafka0.8 版本之后offset保存在kafka集群上。
它是把消费者消费topic的位置通过kafka集群内部有一个默认的topic,
名称叫 __consumer_offsets,它默认有50个分区。

  • ISR机制

光是依靠多副本机制能保证Kafka的高可用性,但是能保证数据不丢失吗?不行,因为如果leader宕机,但是leader的数据还没同步到follower上去,此时即使选举了follower作为新的leader,当时刚才的数据已经丢失了。

ISR是:in-sync replica,就是跟leader partition保持同步的follower partition的数量,只有处于ISR列表中的follower才可以在leader宕机之后被选举为新的leader,因为在这个ISR列表里代表他的数据跟leader是同步的。

分区是实现负载均衡以及高吞吐量的关键,故在生产者这一端就要仔细盘算合适的分区策略,避免造成消息数据的“倾斜”,使得某些分区成为性能瓶颈,这样极易引发下游数据消费的性能下降

 1. kafka的文件存储机制

1.1 概述

同一个topic下有多个不同的partition,每个partition为一个目录,partition命名的规则是topic的名称加上一个序号,序号从0开始。

每一个partition目录下的文件被平均切割成大小相等(默认一个文件是1G,可以手动去设置)的数据文件,每一个数据文件都被称为一个段(segment file),但每个段消息数量不一定相等,这种特性能够使得老的segment可以被快速清除。默认保留7天的数据。
每次满1G后,在写入到一个新的文件中。

另外每个partition只需要支持顺序读写就可以。如上图所示:
首先00000000000000000000.log是最早产生的文件,该文件达到1G后又产生了新的00000000000002025849.log文件,新的数据会写入到这个新的文件里面。
这个文件到达1G后,数据又会写入到下一个文件中。也就是说它只会往文件的末尾追加数据,这就是顺序写的过程,生产者只会对每一个partition做数据的追加(写操作)。

1.2 数据消费问题讨论

问题:如何保证消息消费的有序性呢?比如说生产者生产了0到100个商品,那么消费者在消费的时候按照0到100这个从小到大的顺序消费?

*** 那么kafka如何保证这种有序性呢?***
难度就在于,生产者生产出0到100这100条数据之后,通过一定的分组策略存储到broker的partition中的时候,
比如0到10这10条消息被存到了这个partition中,10到20这10条消息被存到了那个partition中,这样的话,消息在分组存到partition中的时候就已经被分组策略搞得无序了。

那么能否做到消费者在消费消息的时候全局有序呢?
遇到这个问题,我们可以回答,在大多数情况下是做不到全局有序的。但在某些情况下是可以做到的。比如我的partition只有一个,这种情况下是可以全局有序的。

那么可能有人又要问了,只有一个partition的话,哪里来的分布式呢?哪里来的负载均衡呢?
所以说,全局有序是一个伪命题!全局有序根本没有办法在kafka要实现的大数据的场景来做到。但是我们只能保证当前这个partition内部消息消费的有序性。

结论:一个partition中的数据是有序的吗?回答:间隔有序,不连续。

针对一个topic里面的数据,只能做到partition内部有序,不能做到全局有序。特别是加入消费者的场景后,如何保证消费者的消费的消息的全局有序性,
这是一个伪命题,只有在一种情况下才能保证消费的消息的全局有序性,那就是只有一个partition。

1.3 Segment文件
  • Segment file是什么

生产者生产的消息按照一定的分区策略被发送到topic中partition中,partition在磁盘上就是一个目录,该目录名是topic的名称加上一个序号,在这个partition目录下,有两类文件,一类是以log为后缀的文件,一类是以index为后缀的文件,每一个log文件和一个index文件相对应,这一对文件就是一个segment file,也就是一个段。
其中的log文件就是数据文件,里面存放的就是消息,而index文件是索引文件,索引文件记录了元数据信息。log文件达到1个G后滚动重新生成新的log文件

  • Segment文件特点

  segment文件命名的规则:partition全局的第一个segment从0(20个0)开始,后续的每一个segment文件名是上一个segment文件中最后一条消息的offset值。

那么这样命令有什么好处呢?
假如我们有一个消费者已经消费到了368776(offset值为368776),那么现在我们要继续消费的话,怎么做呢?

看下图,分2个步骤;
第1步是从所有文件log文件的的文件名中找到对应的log文件,第368776条数据位于上图中的“00000000000000368769.log”这个文件中,
这一步涉及到一个常用的算法叫做“二分查找法”(假如我现在给你一个offset值让你去找,你首先是将所有的log的文件名进行排序,然后通过二分查找法进行查找,
很快就能定位到某一个文件,紧接着拿着这个offset值到其索引文件中找这条数据究竟存在哪里);
第2步是到index文件中去找第368776条数据所在的位置。

索引文件(index文件)中存储这大量的元数据,而数据文件(log文件)中存储这大量的消息。

索引文件(index文件)中的元数据指向对应的数据文件(log文件)中消息的物理偏移地址。

1.4 kafka如何快速查询数据

上图的左半部分是索引文件,里面存储的是一对一对的key-value,其中key是消息在数据文件(对应的log文件)中的编号,比如“1,3,6,8……”,
分别表示在log文件中的第1条消息、第3条消息、第6条消息、第8条消息……,那么为什么在index文件中这些编号不是连续的呢?
这是因为index文件中并没有为数据文件中的每条消息都建立索引,而是采用了稀疏存储的方式,每隔一定字节的数据建立一条索引。
这样避免了索引文件占用过多的空间,从而可以将索引文件保留在内存中。
但缺点是没有建立索引的Message也不能一次定位到其在数据文件的位置,从而需要做一次顺序扫描,但是这次顺序扫描的范围就很小了。

其中以索引文件中元数据8,1686为例,其中8代表在右边log数据文件中从上到下第8个消息(在全局partiton表示第368777个消息),其中1686表示该消息的物理偏移地址(位置)为1686。

要是读取offset=368777的消息,从00000000000000368769.log文件中的1325的位置进行读取,那么怎么知道何时读完本条消息,否则就读到下一条消息的内容了?

参数说明:

关键字

解释说明

8 byte offset

在parition(分区)内的每条消息都有一个有序的id号,这个id号被称为偏移(offset),它可以唯一确定每条消息在parition(分区)内的位置。即offset表示partiion的第多少message

4 byte message size

message大小

4 byte CRC32

用crc32校验message

1 byte “magic"

表示本次发布Kafka服务程序协议版本号

1 byte “attributes"

表示为独立版本、或标识压缩类型、或编码类型。

4 byte key length

表示key的长度,当key为-1时,K byte key字段不填

K byte key

可选

value bytes payload

表示实际消息数据。

这个就需要涉及到消息的物理结构了,消息都具有固定的物理结构,包括:offset(8 Bytes)、消息体的大小(4 Bytes)、crc32(4 Bytes)、magic(1 Byte)、attributes(1 Byte)、key length(4 Bytes)、key(K Bytes)、payload(N Bytes)等等字段,可以确定一条消息的大小,即读取到哪里截止。

1.5 kafka高效文件存储设计特点
  • Kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。
  • 通过索引信息可以快速定位message
  • 通过index元数据全部映射到memory,可以避免segment file的IO磁盘操作。
  • 通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小。

��� 2. 为什么Kafka速度那么快

Kafka是大数据领域无处不在的消息中间件,目前广泛使用在企业内部的实时数据管道,并帮助企业构建自己的流计算应用程序。
Kafka虽然是基于磁盘做的数据存储,但却具有高性能、高吞吐、低延时的特点,其吞吐量动辄几万、几十上百万,这其中的原由值得我们一探究竟。

2.1 顺序读写
  • 磁盘顺序读写性能要高于内存的随机读写

众所周知Kafka是将消息记录持久化到本地磁盘中的,一般人会认为磁盘读写性能差,可能会对Kafka性能如何保证提出质疑。实际上不管是内存还是磁盘,快或慢关键在于寻址的方式,磁盘分为顺序读写与随机读写,内存也一样分为顺序读写与随机读写。基于磁盘的随机读写确实很慢,但磁盘的顺序读写性能却很高,一般而言要高出磁盘随机读写三个数量级,一些情况下磁盘顺序读写性能甚至要高于内存随机读写。
磁盘的顺序读写是磁盘使用模式中最有规律的,并且操作系统也对这种模式做了大量优化,Kafka就是使用了磁盘顺序读写来提升的性能。Kafka的message是不断追加到本地磁盘文件末尾的,而不是随机的写入,这使得Kafka写入吞吐量得到了显著提升

2.2 Page Cache(页缓存)

为了优化读写性能,Kafka利用了操作系统本身的Page Cache,就是利用操作系统自身的内存而不是JVM空间内存。这样做的好处有:

(1)避免Object消耗:如果是使用Java堆,Java对象的内存消耗比较大,通常是所存储数据的两倍甚至更多。
(2)避免GC问题:随着JVM中数据不断增多,垃圾回收将会变得复杂与缓慢,使用系统缓存就不会存在GC问题。

2.3 零拷贝(sendfile)
  • 零拷贝并不是不需要拷贝,而是减少不必要的拷贝次数。通常是说在IO读写过程中。

Kafka利用linux操作系统的 "零拷贝(zero-copy)" 机制在消费端做的优化。

  • 首先来了解下数据从文件发送到socket网络连接中的常规传输路径

比如:读取文件,再用socket发送出去
传统方式实现:
先读取、再发送,实际经过1~4四次copy。
buffer = File.read
Socket.send(buffer)

  • 第一步:操作系统从磁盘读取数据到内核空间(kernel space)的Page Cache缓冲区
  • 第二步:应用程序读取内核缓冲区的数据copy到用户空间(user space)的缓冲区
  • 第三步:应用程序将用户空间缓冲区的数据copy回内核空间到socket缓冲区
  • 第四步:操作系统将数据从socket缓冲区copy到网卡,由网卡进行网络传输

传统方式,读取磁盘文件并进行网络发送,经过的四次数据copy是非常繁琐的。实际IO读写,需要进行IO中断,需要CPU响应中断(带来上下文切换),尽管后来引入DMA来接管CPU的中断请求,但四次copy是存在“不必要的拷贝”的。

重新思考传统IO方式,会注意到实际上并不需要第二个和第三个数据副本。应用程序除了缓存数据并将其传输回套接字缓冲区之外什么都不做。相反,数据可以直接从读缓冲区传输到套接字缓冲区。

显然,第二次和第三次数据copy 其实在这种场景下没有什么帮助反而带来开销,这也正是零拷贝出现的意义。

这种场景:是指读取磁盘文件后,不需要做其他处理,直接用网络发送出去。试想,如果读取磁盘的数据需要用程序进一步处理的话,必须要经过第二次和第三次数据copy,让应用程序在内存缓冲区处理。

此时我们会发现用户态“空空如也”。数据没有来到用户态,而是直接在核心态就进行了传输,但这样依然还是有多次复制。首先数据被读取到read buffer中,然后发到socket buffer,最后才发到网卡。虽然减少了用户态和核心态的切换,但依然存在多次数据复制。

如果可以进一步减少数据复制的次数,甚至没有数据复制是不是就会做到最快呢?

  • DMA
    • DMA,全称叫Direct Memory Access,一种可让某些硬件子系统去直接访问系统主内存,而不用依赖CPU的计算机系统的功能。听着是不是很厉害,跳过CPU,直接访问主内存。传统的内存访问都需要通过CPU的调度来完成。如下图:

    • DMA,则可以绕过CPU,硬件自己去直接访问系统主内存。如下图

    • 回到本文中的文件传输,有了DMA后,就可以实现绝对的零拷贝了,因为网卡是直接去访问系统主内存的。如下图:

  • 总结

Kafka采用顺序读写、Page Cache、零拷贝以及分区分段等这些设计,再加上在索引方面做的优化,另外Kafka数据读写也是批量的而不是单条的,使得Kafka具有了高性能、高吞吐、低延时的特点。这样Kafka提供大容量的磁盘存储也变成了一种优点

Java的NIO提供了FileChannle,它的transferTo、transferFrom方法就是Zero Copy。

��� 5. kafka内核原理

5.1 ISR机制

光是依靠多副本机制能保证Kafka的高可用性,但是能保证数据不丢失吗?
不行,因为如果leader宕机,但是leader的数据还没同步到follower上去,此时即使选举了follower作为新的leader,当时刚才的数据已经丢失了。

ISR是:in-sync replica,就是跟leader partition保持同步的follower partition的数量,只有处于ISR列表中的follower才可以在leader宕机之后被选举为新的leader,因为在这个ISR列表里代表他的数据跟leader是同步的。

如果要保证写入kafka的数据不丢失,首先需要保证ISR中至少有一个follower,其次就是在一条数据写入了leader partition之后,要求必须复制给ISR中所有的follower partition,才能说代表这条数据已提交,绝对不会丢失,这是Kafka给出的承诺

5.2 HW&LEO原理
  • LEO

last end offset,日志末端偏移量,标识当前日志文件中下一条待写入的消息的offset。举一个例子,若LEO=10,那么表示在该副本日志上已经保存了10条消息,位移范围是[0,9]。

  • HW

Highwatermark,俗称高水位,它标识了一个特定的消息偏移量(offset),消费者只能拉取到这个offset之前的消息。任何一个副本对象的HW值一定不大于其LEO值。
小于或等于HW值的所有消息被认为是“已提交的”或“已备份的”。HW它的作用主要是用来判断副本的备份进度.

下图表示一个日志文件,这个日志文件中只有9条消息,第一条消息的offset(LogStartOffset)为0,最有一条消息的offset为8,offset为9的消息使用虚线表示的,代表下一条待写入的消息。日志文件的 HW 为6,表示消费者只能拉取offset在 0 到 5 之间的消息,offset为6的消息对消费者而言是不可见的。

leader持有的HW即为分区的HW,同时leader所在broker还保存了所有follower副本的leo

(1)关系:leader的leo >= follower的leo >= leader保存的follower的leo >= leader的hw >= follower的hw
(2)原理:上面关系反应出各个值的更新逻辑的先后

5.3 更新LEO和HW
  • ==更新LEO的机制==
    • 注意
      • follower副本的LEO保存在2个地方

(1)follower副本所在的broker缓存里。
(2)leader所在broker的缓存里,也就是leader所在broker的缓存上保存了该分区所有副本的LEO。

    • 更新LEO的时机
      • follower更新LEO

(1)follower的leo更新时间
每当follower副本写入一条消息时,leo值会被更新

(2)leader端的follower副本的leo更新时间
当follower从leader处fetch消息时,leader获取follower的fetch请求中offset参数,更新保存在leader端follower的leo。

      • leader更新LEO

(1)leader本身的leo的更新时间:leader向log写消息时

  • ==更新HW的机制==
    • follower更新HW

follower更新HW发生在其更新完LEO后,即follower向log写完数据,它就会尝试更新HW值。具体算法就是比较当前LEO(已更新)与fetch响应中leader的HW值,取两者的小者作为新的HW值。

    • leader更新HW
      • leader更新HW的时机

(1)producer 向 leader 写消息时
(2)leader 处理 follower 的 fetch 请求时
(3)某副本成为leader时
(4)broker 崩溃导致副本被踢出ISR时

      • leader更新HW的方式

  当尝试确定分区HW时,它会选出所有满足条件的副本,比较它们的LEO(当然也包括leader自己的LEO),并选择最小的LEO值作为HW值。
  这里的满足条件主要是指副本要满足以下两个条件之一:
  (1)处于ISR中
  (2)副本LEO落后于leader LEO的时长不大于replica.lag.time.max.ms参数值(默认值是10秒)

��� 6. producer消息发送流程

  • producer核心流程概览

  • 1、ProducerInterceptors是一个拦截器,对发送的数据进行拦截

ps:说实话这个功能其实没啥用,我们即使真的要过滤,拦截一些消息,也不考虑使用它,我们直接发送数据之前自己用代码过滤即可

  • 2、Serializer 对消息的key和value进行序列化
  • 3、通过使用分区器作用在每一条消息上,实现数据分发进行入到topic不同的分区中
  • 4、RecordAccumulator收集消息,实现批量发送

它是一个缓冲区,可以缓存一批数据,把topic的每一个分区数据存在一个队列中,然后封装消息成一个一个的batch批次,最后实现数据分批次批量发送。

  • 5、Sender线程从RecordAccumulator获取消息
  • 6、构建ClientRequest对象
  • 7、将ClientRequest交给 NetWorkClient准备发送
  • 8、NetWorkClient 将请求放入到KafkaChannel的缓存
  • 9、发送请求到kafka集群
  • 10、调用回调函数,接受到响应

��� 7. producer核心参数

7.1 常见异常处理

  • 不管是异步还是同步,都可能让你处理异常,常见的异常如下:

1)LeaderNotAvailableException:这个就是如果某台机器挂了,此时leader副本不可用,会导致你写入失败,要等待其他follower副本切换为leader副本之后,才能继续写入,此时可以重试发送即可。如果说你平时重启kafka的broker进程,肯定会导致leader切换,一定会导致你写入报错,是LeaderNotAvailableException

2)NotControllerException:这个也是同理,如果说Controller所在Broker挂了,那么此时会有问题,需要等待Controller重新选举,此时也是一样就是重试即可

3)NetworkException:网络异常,重试即可
我们之前配置了一个参数,retries,他会自动重试的,但是如果重试几次之后还是不行,就会提供Exception给我们来处理了。

  • ==retries==
    • 重新发送数据的次数
      • 默认为0,表示不重试
  • ==retry.backoff.ms==
    • 两次重试之间的时间间隔
      • 默认为100ms
7.2 提升消息吞吐量
  • ==buffer.memory==
    • 设置发送消息的缓冲区,默认值是33554432,就是32MB

如果发送消息出去的速度小于写入消息进去的速度,就会导致缓冲区写满,此时生产消息就会阻塞住,所以说这里就应该多做一些压测,尽可能保证说这块缓冲区不会被写满导致生产行为被阻塞住

  • ==compression.type==
    • producer用于压缩数据的压缩类型。默认是none表示无压缩。可以指定gzip、snappy
    • 压缩最好用于批量处理,批量处理消息越多,压缩性能越好。
  • ==batch.size==
    • producer将试图批处理消息记录,以减少请求次数。这将改善client与server之间的性能。
    • 默认是16384Bytes,即16kB,也就是一个batch满了16kB就发送出去

如果batch太小,会导致频繁网络请求,吞吐量下降;如果batch太大,会导致一条消息需要等待很久才能被发送出去,而且会让内存缓冲区有很大压力,过多数据缓冲在内存里。

  • ==linger.ms==
    • 这个值默认是0,就是消息必须立即被发送

一般设置一个100毫秒之类的,这样的话就是说,这个消息被发送出去后进入一个batch,如果100毫秒内,这个batch满了16kB,自然就会发送出去。
但是如果100毫秒内,batch没满,那么也必须把消息发送出去了,不能让消息的发送延迟时间太长,也避免给内存造成过大的一个压力。

7.3 请求超时
  • ==max.request.size==
    • 这个参数用来控制发送出去的消息的大小,默认是1048576字节,也就1mb
    • 这个一般太小了,很多消息可能都会超过1mb的大小,所以需要自己优化调整,把他设置更大一些(企业一般设置成10M)
  • ==request.timeout.ms==
    • 这个就是说发送一个请求出去之后,他有一个超时的时间限制,默认是30秒
    • 如果30秒都收不到响应,那么就会认为异常,会抛出一个TimeoutException来让我们进行处理
7.4 ACK参数

acks参数,其实是控制发送出去的消息的持久化机制的。

  • ==acks=0==
    • 生产者只管发数据,不管消息是否写入成功到broker中,数据丢失的风险最高

producer根本不管写入broker的消息到底成功没有,发送一条消息出去,立马就可以发送下一条消息,这是吞吐量最高的方式,但是可能消息都丢失了。
你也不知道的,但是说实话,你如果真是那种实时数据流分析的业务和场景,就是仅仅分析一些数据报表,丢几条数据影响不大的。会让你的发送吞吐量会提升很多,你发送弄一个batch出去,不需要等待人家leader写成功,直接就可以发送下一个batch了,吞吐量很大的,哪怕是偶尔丢一点点数据,实时报表,折线图,饼图。

  • ==acks=1==
    • 只要leader写入成功,就认为消息成功了.

默认给这个其实就比较合适的,还是可能会导致数据丢失的,如果刚写入leader,leader就挂了,此时数据必然丢了,其他的follower没收到数据副本,变成leader.

  • ==acks=all 或者 acks=-1==
    • 这个leader写入成功以后,必须等待其他ISR中的副本都写入成功,才可以返回响应说这条消息写入成功了,此时你会收到一个回调通知.

这种方式数据最安全,但是性能最差。

  • ==如果要想保证数据不丢失,得如下设置==

(1)min.insync.replicas = 2
ISR里必须有2个副本,一个leader和一个follower,最最起码的一个,不能只有一个leader存活,连一个follower都没有了。

(2)acks = -1
每次写成功一定是leader和follower都成功才可以算做成功,这样leader挂了,follower上是一定有这条数据,不会丢失。

(3)retries = Integer.MAX_VALUE
无限重试,如果上述两个条件不满足,写入一直失败,就会无限次重试,保证说数据必须成功的发送给两个副本,如果做不到,就不停的重试。
除非是面向金融级的场景,面向企业大客户,或者是广告计费,跟钱的计算相关的场景下,才会通过严格配置保证数据绝对不丢失

7.5 重试乱序
  • max.in.flight.requests.per.connection
    • 每个网络连接可以忍受 producer端发送给broker 消息然后消息没有响应的个数

消息重试是可能导致消息的乱序的,因为可能排在你后面的消息都发送出去了,你现在收到回调失败了才在重试,此时消息就会乱序,所以可以使用“max.in.flight.requests.per.connection”参数设置为1,这样可以保证producer同一时间只能发送一条消息

��� 8. broker核心参数

  • server.properties配置文件核心参数

【broker.id】
每个broker都必须自己设置的一个唯一id

【log.dirs】
这个极为重要,kafka的所有数据就是写入这个目录下的磁盘文件中的,如果说机器上有多块物理硬盘,那么可以把多个目录挂载到不同的物理硬盘上,然后这里可以设置多个目录,这样kafka可以数据分散到多块物理硬盘,多个硬盘的磁头可以并行写,这样可以提升吞吐量。

【zookeeper.connect】
连接kafka底层的zookeeper集群的

【Listeners】
broker监听客户端发起请求的端口号,默认是9092

【unclean.leader.election.enable】
默认是false,意思就是只能选举ISR列表里的follower成为新的leader,1.0版本后才设为false,之前都是true,允许非ISR列表的follower选举为新的leader

【delete.topic.enable】
默认true,允许删除topic

【log.retention.hours】
可以设置一下,要保留数据多少个小时(默认168小时),这个就是底层的磁盘文件,默认保留7天的数据,根据自己的需求来就行了

��� 9. consumer消费原理

9.1 Offset管理

每个consumer内存里数据结构保存对每个topic的每个分区的消费offset,定期会提交offset,老版本是写入zk,但是那样高并发请求zk是不合理的架构设计,zk是做分布式系统的协调的,轻量级的元数据存储,不能负责高并发读写,作为数据存储。所以后来就是提交offset发送给内部topic:consumer_offsets,提交过去的时候,key是group.id+topic+分区号,value就是当前offset的值,每隔一段时间,kafka内部会对这个topic进行compact。也就是每个group.id+topic+分区号就保留最新的那条数据即可。而且因为这个 consumer_offsets可能会接收高并发的请求,所以默认分区50个,这样如果你的kafka部署了一个大的集群,比如有50台机器,就可以用50台机器来抗offset提交的请求压力,就好很多。

9.2 Coordinator
  • Coordinator的作用

每个consumer group都会选择一个broker作为自己的coordinator,他是负责监控这个消费组里的各个消费者的心跳,以及判断是否宕机,然后开启rebalance.
根据内部的一个选择机制,会挑选一个对应的Broker,Kafka总会把你的各个消费组均匀分配给各个Broker作为coordinator来进行管理的.
consumer group中的每个consumer刚刚启动就会跟选举出来的这个consumer group对应的coordinator所在的broker进行通信,然后由coordinator分配分区给你的这个consumer来进行消费。coordinator会尽可能均匀的分配分区给各个consumer来消费。

  • 如何选择哪台是coordinator

首先对消费组的groupId进行hash,接着对consumer_offsets的分区数量取模,默认是50,可以通过offsets.topic.num.partitions来设置,找到你的这个consumer group的offset要提交到consumer_offsets的哪个分区。
比如说:groupId,"membership-consumer-group" -> hash值(数字)-> 对50取模 -> 就知道这个consumer group下的所有的消费者提交offset的时候是往哪个分区去提交offset,找到consumer_offsets的一个分区,consumer_offset的分区的副本数量默认来说1,只有一个leader,然后对这个分区找到对应的leader所在的broker,这个broker就是这个consumer group的coordinator了,consumer接着就会维护一个Socket连接跟这个Broker进行通信。

��� 10. consumer消费者Rebalance策略

比如我们消费的一个topic主题有12个分区:p0,p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11
假设我们的消费者组里面有三个消费者。

10.1 range范围策略
  • RangeAssignor(默认策略)

range策略就是按照partiton的序号范围
p0~3             consumer1
p4~7             consumer2
p8~11            consumer3
默认就是这个策略

10.2 round-robin轮训策略
  • RoundRobinAssignor

consumer1: 0,3,6,9
consumer2: 1,4,7,10
consumer3: 2,5,8,11

但是前面的这两个方案有个问题:
假设consuemr1挂了:p0-5分配给consumer2,p6-11分配给consumer3
这样的话,原本在consumer2上的的p6,p7分区就被分配到了 consumer3上

10.3 sticky黏性策略
  • StickyAssignor

最新的一个sticky策略,就是说尽可能保证在rebalance的时候,让原本属于这个consumer
的分区还是属于他们,然后把多余的分区再均匀分配过去,这样尽可能维持原来的分区分配的策略

consumer1: 0-3
consumer2:  4-7
consumer3:  8-11

假设consumer3挂了
consumer1:0-3,+8,9
consumer2: 4-7,+10,11

  • 消费者分配策略
    • 由参数==partition.assignment.strategy==控制,默认是RangeAssignor表示范围策略

//设置消费者分配策略:
properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
StickyAssignor.class.getName());

��� 11. Rebalance 高级

11.1 Consumer Group Coordinator
  • Rebalance 就是让一个 Consumer Group 下所有的 Consumer 实例就如何消费订阅主题的所有分区达成共识的过程。在 Rebalance 过程中,所有 Consumer 实例共同参与,在协调者组件的帮助下,完成订阅主题分区的分配。但是,在整个过程中,所有实例都不能消费任何消息,因此它对 Consumer 的 TPS 影响很大。

所谓协调者,在 Kafka 中对应的术语是 Coordinator,它专门为 Consumer Group 服务,负责为 Group 执行 Rebalance 以及提供位移管理和组成员管理等。

Consumer 端应用程序在提交位移时,其实是向 Coordinator 所在的 Broker 提交位移。同样地,当 Consumer 应用启动时,也是向 Coordinator 所在的 Broker 发送各种请求,然后由 Coordinator 负责执行消费者组的注册、成员管理记录等元数据管理操作。

所有 Broker 在启动时,都会创建和开启相应的 Coordinator 组件。也就是说,所有 Broker 都有各自的 Coordinator 组件。

11.2 rebalance的弊端

1、Rebalance 影响 Consumer 端 TPS。这个之前也反复提到了,这里就不再具体讲了。总之就是,在 Rebalance 期间,Consumer 会停下手头的事情,什么也干不了。

2、Rebalance 很慢。如果你的 Group 下成员很多,就一定会有这样的痛点。

3、Rebalance 效率不高。当前 Kafka 的设计机制决定了每次 Rebalance 时,Group 下的所有成员都要参与进来,而且通常不会考虑局部性原理,但局部性原理对提升系统性能是特别重要的。

  • 那么针对这些问题,kafka是否可以解决?
    • 所以需要尽量的避免rebalance。

发生rebalance的情况如下:
1、组成员数量发生变化
2、订阅主题数量发生变化
3、订阅主题的分区数发生变化

后面两个通常都是运维的主动操作,所以它们引发的 Rebalance 大都是不可避免的。那么如何避免组成员数量发生变化?

11.3. 如何避免组成员数量发生变化
  • 如果 Consumer Group 下的 Consumer 实例数量发生变化,就一定会引发 Rebalance。这是 Rebalance 发生的最常见的原因。
11.3.1 增加实例
  • 通常来说,增加 Consumer 实例的操作都是计划内的,可能是出于增加 TPS 或提高伸缩性的需要。总之,它不属于我们要规避的那类“不必要 Rebalance”。
11.3.2 减少实例
  • 如果就是要停掉某些 Consumer 实例,那自不必说,关键是在某些情况下,Consumer 实例会被 Coordinator 错误地认为“已停止”从而被“踢出”Group。如果是这个原因导致的 Rebalance,就需要处理了。

Coordinator 会在什么情况下认为某个 Consumer 实例已挂从而要退组呢?

当 Consumer Group 完成 Rebalance 之后,每个 Consumer 实例都会定期地向 Coordinator 发送心跳请求,表明它还存活着。如果某个 Consumer 实例不能及时地发送这些心跳请求,Coordinator 就会认为该 Consumer 已经“死”了,从而将其从 Group 中移除,然后开启新一轮 Rebalance。

Consumer 端有个参数,session.timeout.ms:指group coordinator检测consumer发生崩溃所需的时间。一个consumer group里面的某个consumer挂掉了,最长需要 session.timeout.ms 秒检测出来,该参数的默认值是 10 秒,即如果 Coordinator 在 10 秒之内没有收到 Group 下某 Consumer 实例的心跳,它就会认为这个 Consumer 实例已经挂了。可以这么说,session.timout.ms 决定了 Consumer 存活性的时间间隔。

除了这个参数,Consumer 还提供了一个允许你控制发送心跳请求频率的参数,就是 heartbeat.interval.ms。这个值设置得越小,Consumer 实例发送心跳请求的频率就越高。频繁地发送心跳请求会额外消耗带宽资源,但好处是能够更加快速地知晓当前是否开启 Rebalance,因为,目前 Coordinator 通知各个 Consumer 实例开启 Rebalance 的方法,就是将 REBALANCE_NEEDED 标志封装进心跳请求的响应体中。

除了以上两个参数,Consumer 端还有一个参数,用于控制 Consumer 实际消费能力对 Rebalance 的影响,即 max.poll.interval.ms 参数。它限定了 Consumer 端应用程序两次调用 poll 方法的最大时间间隔。它的默认值是 5 分钟,表示你的 Consumer 程序如果在 5 分钟之内无法消费完 poll 方法返回的消息,那么 Consumer 会主动发起“离开组”的请求,Coordinator 也会开启新一轮 Rebalance。

11.4 解决非必要的rebalance
  • 第一类非必要 Rebalance 是因为未能及时发送心跳,导致 Consumer 被“踢出”Group 而引发的

需要仔细地设置session.timeout.ms 和 heartbeat.interval.ms的值。

设置 session.timeout.ms = 6s。
设置 heartbeat.interval.ms = 2s。
要保证 Consumer 实例在被判定为“dead”之前,能够发送至少 3 轮的心跳请求,即 session.timeout.ms >= 3 * heartbeat.interval.ms。

将 session.timeout.ms 设置成 6s 主要是为了让 Coordinator 能够更快地定位已经挂掉的 Consumer。毕竟,我们还是希望能尽快揪出那些“尸位素餐”的 Consumer,早日把它们踢出 Group。

  • 第二类非必要 Rebalance 是 Consumer 消费时间过长导致的

合理的设置max.poll.interval.ms:
参数用于指定consumer两次poll的最大时间间隔(默认5分钟),如果超过了该间隔consumer client会主动向coordinator发起LeaveGroup请求,触发rebalance;然后consumer重新发送JoinGroup请求

  • 如果上述两种配置完成后,还会进行rebalance,那么就需要去设置GC了。

��� 12. consumer核心参数

【heartbeat.interval.ms】
默认值:3000
consumer心跳时间,必须得保持心跳才能知道consumer是否故障了,然后如果故障之后,就会通过心跳下发rebalance的指令给其他的consumer通知他们进行rebalance的操作

【session.timeout.ms】
默认值:10000 
kafka多长时间感知不到一个consumer就认为他故障了,默认是10秒

【max.poll.interval.ms】
默认值:300000  
如果在两次poll操作之间,超过了这个时间,那么就会认为这个consume处理能力太弱了,会被踢出消费组,分区分配给别人去消费,一遍来说结合你自己的业务处理的性能来设置就可以了

【fetch.max.bytes】
默认值:1048576
获取一条消息最大的字节数,一般建议设置大一些

【max.poll.records】
默认值:500条
一次poll返回消息的最大条数,

【connections.max.idle.ms】
默认值:540000  
consumer跟broker的socket连接如果空闲超过了一定的时间,此时就会自动回收连接,但是下次消费就要重新建立socket连接,这个建议设置为-1,不要去回收

【auto.offset.reset】
  earliest
当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费  
latest
当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从当前位置开始消费
none
topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常

注:我们生产里面一般设置的是latest

【enable.auto.commit】
默认值:true
设置为自动提交offset

【auto.commit.interval.ms】
默认值:60 * 1000
每隔多久更新一下偏移量


如果消费者这端要保证数据被处理且只被处理一次:
  屏蔽掉了下面这2种情况:
(1)数据的重复处理
(2)数据的丢失

一般来说:需要手动提交偏移量,需要保证数据处理成功与保存偏移量的操作在同一事务中就可以了 

��� 13. 如何保证kafka数据不丢失

  • Kafka是一种高吞吐量的分布式发布订阅消息系统。在使用过程中如果使用不当,经常会出现消息丢失的情况,这是业务系统不能容忍的,消息系统最重要的是保证数据不丢失。
    • 三个维度来保证:消息发送端保证数据不丢失,kafka服务保证消息不丢失,消费者保证消息不丢失。
13.1 Producer 生产端保证数据不丢失
  • 1、发送消息API使用
    • 在生产中Kafka生产者的开发我们都会用异步调用的方式,异步调用方式有如下两个API:

(1)producer.send(msg)            不带回调方法
(2)producer.send(msg,callback)   带回调方法

    • 记得要使用带有回调方法的API,我们可以根据回调函数得知消息是否发送成功,如果发送失败了我们要进行异常处理,比如存储到其他介质来保证消息不丢。
  • 2、ack的配置策略

设置 acks = all或者-1。
生产者在发送消息之后,需要等待ISR中所有的副本都成功写入消息之后才能够收到来自服务端的成功响应,在配置环境相同的情况下此种配置可以达到最强的可靠性。即:在发送消息时,需要leader 向fllow 同步完数据之后,也就是ISR队列中所有的broker全部保存完这条消息后,才会向ack发送消息,表示发送成功。

  • 3、retries参数设置

设置 retries 为一个较大的值。这里的 retries 同样是 Producer 的参数,对应前面提到的 Producer 自动重试。当出现网络的瞬时抖动时,消息发送可能会失败,此时配置了 retries > 0 的 Producer 能够自动重试消息发送,避免消息丢失。

在kafka中错误分为2种,一种是可恢复的,另一种是不可恢复的。
  可恢复性的错误:
      如遇到在leader的选举、网络的抖动等这些异常时,如果我们在这个时候配置的retries大于0的,也就是可以进行重试操作,那么等到leader选举完成后、网络稳定后,这些异常就会消息,错误也就可以恢复,数据再次重发时就会正常发送到broker端。需要注意retries(重试)之间的时间间隔,以确保在重试时可恢复性错误都已恢复。
  不可恢复性的错误:
      如:超过了发送消息的最大值(max.request.size)时,这种错误是不可恢复的,如果不做处理,那么数据就会丢失,因此我们需要注意在发生异常时把这些消息写入到DB、缓存本地文件中等等,把这些不成功的数据记录下来,等错误修复后,再把这些数据发送到broker端。

13.2 Broker服务端保证数据不丢失
  • 1、replication-factor 参数设置

这个参数设置的是partition副本的个数,如果我们要想保证数据不丢,这个副本数需要设置成大于1。

  • 2、unclean.leader.election.enable 参数设置

是否允许从非ISR队列中选举leader副本,默认值是false,如果设置成true,则可能会造成数据丢失。

  • 3、min.insync.replicas 参数设置

  分区ISR队列集合中最少有多少个副本,默认值是1
  这个参数要跟生产者里的acks参数配合使用,当生产者acks=-1时,服务端的ISR列表里的所有副本都写入成功,才会给生产者返回成功的响应。而min.insync.replicas这个参数就是控制ISR列表的,假设min.insync.replicas=1,这就意味着ISR列表里可以只有一个副本,这个副本就是leader replica,这个时候即使acks设置的是-1,但其实消息只发送到leader replica,以后就返回成功的响应了。
  因为ISR只有一个副本,我们知道这种情况是有可能会丢数据的,所以min.insync.replicas这个值需要大于1的。     如果ISR列表里面副本的个数小于min.insync.replicas,生产者发送消息是失败的
   
  确保 replication.factor > min.insync.replicas。如果两者相等,那么只要有一个副本挂机,整个分区就无法正常工作了。我们不仅要改善消息的持久性,防止数据丢失,还要在不降低可用性的基础上完成。推荐设置成 replication.factor = min.insync.replicas + 1。

13.3 Consumer消费者保证数据不丢失
  • 1、enable.auto.commit 参数设置

消费者是可以自动提交offset的,但是如果是自动提交offset,可能会丢数据
确保消息消费完成再提交。Consumer 端有个参数 enable.auto.commit,最好把它设置成 false,并采用手动提交位移的方式afka

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值