Kafka是一个分布式的基于发布/订阅模式的消息队列,是一款分布式流处理框架,用于实时构建流处理应用,主要应用于大数据实时处理领域。
好处:1.解耦。2.缓冲。3.灵活性&峰值处理能力。4.异步通信。
消息队列的两种模型:
- 点对点模式(一对一,消费者主动拉取数据,消息收到后消除)
2.发布/订阅模式(kafka采用的模式,一对多,消费者消费数据后不会消除消息)
kafka角色说明:
角色说明
broker:kafka集群中包含一个或者多个服务实例(节点),这种服务实例被称为broker(一个broker就是一个节点/一个服务器)
topic:每条发布到kafka集群的消息都属于某个类别,这个类别就叫做topic
partition:partition是一个物理上的概念,每个topic包含一个或者多个partition,生成者发到某个topic的消息会根据指定的规则被均匀的分布到其下的多个partition。一个broker服务下,可以创建多个分区,broker数与分区数没有关系。在kafka中,每一个分区会有一个编号:编号从0开始。每一个分区内的数据是有序的,但全局的数据不能保证是有序的。(有序是指生产什么样顺序,消费时也是什么样的顺序)
segment:一个partition当中存在多个segment文件段,每个segment分为两部分.log文件(存放生产者发送的数据文件)和.index文件(存放.log文件的数据索引值,用于加快数据的查询速度)
producer:消息的生产者,负责发布消息到kafka的broker中
consumer:消息的消费者,向kafka的broker中读取消息的客户端
consumergroup:消费者组,每一个consumer属于一个特定的consumergroup(每个消费者组都有一个ID,即group ID,可以为每个consumer指定group ID),同一个组中的消费者对于同一条消息只消费一次,组内的所有消费者协调在一起来消费一个订阅主题( topic)的所有分区(partition),每个分区只能由同一个消费组内的一个消费者(consumer)来消费,但每个分区是可以由不同的消费组同时来消费的
特征
- kafka支持消息持久化
- 消费端是主动拉取数据,消费状态和订阅关系由客户端负责维护
- 消息消费完后,不会立即删除,会保留历史消息
- topic中所有的分区的消费顺序是随机的但是,在单个分区内的消费顺序是固定的
角色理解
每一个topic可以创建多个分区,每一个分区包含单独的文件夹,并且是多副本机制,即topic的每一个分区会有Leader与Follower,并且Kafka内部有机制保证topic的某一个分区的Leader与follow不会存在在同一台机器,并且每一台broker会尽量均衡的承担各个分区的Leader,当然在运行过程中如果不均衡,可以执行命令进行手动重平衡。Leader节点承担一个分区的读写,follow节点只负责数据备份。
如上所示:topicA有3个分区,partition 0、partition 1和partition 2。那么,3个broker上都topicA的3个分区。每个分区都会有一个leader,其他的为follow。如在broker1上,topicA的partition 0是leader,broker1和broker2上topicA的partition 0就是follow。Leader节点承担一个分区的读写,那么生产者往topicA发送一条数据时,根据key或者其他机制选择要存储的分区号,例如需要发送到partition 1上面去,那就会发送到broker1的partition 1上(broker1的partition 1为leader),然后会将数据备份到其他两个broker的partition 1上。
单机模式下,每个分区是没有follow的。
partition、consumergroup
- partition数量决定了每个consumer group中并发消费者的最大数量
- 分区数越多,同一时间可以有越多的消费者来进行消费,消费数据的速度就会越快,提高消费的性能
kafka架构:
Kafka中消息以 topic 进行分类,生产者生产消息,消费者消费消息,都是面向topic的。
Topic是逻辑上的概念,而partition是物理上的概念,每个partition对应一个log文件,该log文件中存储的就是producer生产的数据。Producer生产的数据会被不断追加到该log文件末端,且每条数据都有自己的offset。消费者组中的每个消费者,都会实时记录自己消费到的哪个offset,以便出错恢复时,从上次的位置继续消费。
Kafka文件存储机制:
由于生产者生产的消息会不断的追加到.log文件末尾,为防止log文件过大导致数据定位效率低下,kafka采取了分片和索引机制,将每个partiton分为多个segment。每个segment对应两个文件——“.index”文件和“.log”文件。这些文件位于一个文件夹下,该文件夹的命名规则为:topic名称+分区序号。例如:first这个topic有三个分区,则其对应的文件夹为first-0,first-1,first-2。
index和log文件以当前segment的第一条消息的offset命名。
下图为index文件和log文件的结构示意图:
“.index”文件存储大量的索引信息,“.log”文件存储大量的数据,索引文件的元数据指向对应数据文件中的message的物理偏移地址。
Kafka生产者
生成者发送数据的大致流程:
我们需要将producer发送的数据封装成一个ProducerRecord对象。ProducerRecord
是 Kafka 中的一个核心类,可以理解为消息的载体,包含:key/value 键值对
,目标topic
、分区号Partition Number
以及时间戳
(可选)
1.封装ProducerRecord并序列化数据:
序列化时要将键值对对象由序列化器转换为字节数组,这样它们才能够在网络上传输
2.分区器(实现接口Partitioner的类,可自定义)确认目标分区:然后,消息到达了分区器准备分发
- 如果发送过程中指定了有效的分区号,那么在发送记录时将使用该分区
- 如果发送过程中未指定分区,则将使用
key 的 hash 函数
映射指定一个分区,具体方法是:key.hashCode() % numberOfPartitions
,默认这种方法在分区数较小时容易出现消费者饥饿 - 如果发送的过程中既没有分区号也没有key,则将以循环的方式均匀分配到所有分区(注意不是复制分配)。选好分区后,生产者就知道向哪个主题和分区发送数据了
3.进入记录批次:以上都完成后,这条消息被存放在一个记录批次里,这个批次里的所有消息会被发送到相同的主题和分区上。由一个独立的线程负责把它们发到 Kafka Broker 上。也就是说,消息是先被写入分区中的缓冲区中,然后分批次发送给 Kafka Broker
4.接下来就是副本同步机制
5.Kafka Broker 在收到消息时会返回一个响应,如果写入成功,会返回一个 RecordMetaData
对象,它包含了主题和分区信息,以及记录在分区里的偏移量,上面两种的时间戳类型也会返回给用户。
具体来说:发送成功后,send() 方法会返回一个 Future(java.util.concurrent)
对象,Future 对象的类型是 RecordMetadata类型。在消息发送之前,生产者还可能发生其他的异常。这些异常有可能是 SerializationException
(序列化失败),BufferedExhaustedException
或 TimeoutException
(说明缓冲区已满),又或是 InterruptedException
(说明发送线程被中断)。
如果写入失败,会返回一个错误。生产者在收到错误之后会尝试重新发送消息,几次之后如果还是失败的话,就返回错误消息。
kafka的Producer是线程安全的,用户可以非常非常放心的在多线程中使用
kafka生产者还可以自定义拦截器:
kafka生产者主要的属性配置说明:
1. bootstrap.servers:
该属性指定broker
的地址清单,地址的格式为 host:port
,比如”xx.xx.xx.xx:9090”。清单里其实不需要包含所有的 broker 地址,生产者会从给定的 broker 里查找到其他的 broker 信息。不过建议至少要提供两个 broker 信息,一旦其中一个宕机,生产者仍然能够连接到集群上
2.key.serializer和value.serializer
broker 需要接收到序列化之后的 key/value值,所以生产者发送的消息需要经过序列化之后才传递给 Kafka Broker。生产者需要知道采用何种方式把 Java 对象转换为字节数组。key.serializer
必须被设置为一个实现了org.apache.kafka.common.serialization.Serializer
接口的类,生产者会使用这个类把键对象序列化为字节数组。
Serializer 是一个接口,它表示类将会采用何种方式序列化,它的作用是把对象转换为字节,实现了 Serializer 接口的类主要有 ByteArraySerializer
(Kafka 默认使用的序列化器)、StringSerializer
、IntegerSerializer
等,要注意的一点:key.serializer 是必须要设置的,即使发送消息的时候没有指定key只发送value。
3.acks
acks 参数指定了要有多少个分区副本接收消息,生产者才认为消息是写入成功的。此参数对消息丢失的影响较大.
0:producer不等待broker的ack,这一操作提供了一个最低的延迟,broker一接收到还没有写入磁盘就已经返回,当broker故障时有可能丢失数据。
1:partition的leader落盘成功后返回ack,如果在follower同步成功之前leader故障,那么将会丢失数据。
-1(all):partition的leader和follower全部落盘成功后才返回ack。但是如果在follower同步完成之后,broker发送ack之前,leader发生故障,那么会造成数据重复。
4.retries
代表生产者可以重发的消息次数,生产者从服务器收到的错误有可能是临时性的错误(比如分区找不到首领),如果达到参数设置的这个次数,生产者会放弃重试并返回错误。
默认情况下,生产者在每次重试之间等待 100ms,这个等待参数可以通过 retry.backoff.ms 进行修改
4.batch.size
该参数指定了一个批次可以使用的内存大小,按照字节数计算,从上面的介绍,当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。当批次被填满,批次里的所有消息会被发送出去。不过生产者不一定都会等到批次被填满才发送,任意条数的消息都可能被发送
5.lingger.ms
该参数指定了生产者在发送批次之前等待更多消息加入批次的时间。KafkaProducer
会在批次填满或linger.ms
达到上限时把批次发送出去。默认情况下,只要有可用的线程,就算批次里只有一个消息,生产者也会把消息发送出去。把linger.ms
设置成大于0的数,让生产者在发送批次前等一会,使更多的消息加入这个批次,这样做会增加延迟,但也会提高吞吐量。
同时设置batch.size
和 linger.ms
,就是哪个条件先满足就都会将消息发送出去
6.max.in.flight.requests.per.connection
此参数指定了生产者在收到服务器响应之前可以发送多少消息,它的值越高,就会占用越多的内存,不过也会提高吞吐量。把它设为1可以保证消息是按照发送的顺序写入服务器
7.timeout.ms、request.timeout.ms 和 metadata.fetch.timeout.ms
request.timeout.ms
指定了生产者在发送数据时等待服务器返回的响应时间
metadata.fetch.timeout.ms
指定了生产者在获取元数据(比如目标分区的首领是谁)时等待服务器返回响应的时间。如果等待时间超时,生产者要么重试发送数据,要么返回一个错误
timeout.ms
指定了 broker 等待同步副本返回消息确认的时间,往往配合 acks
的配置使用。如果在指定时间内没有收到同步副本的确认,那么 broker 就会返回一个错误
8.receive.buffer.bytes 和 send.buffer.bytes
Kafka 是基于 TCP 实现的,为了保证可靠的消息传输,这两个参数分别指定了 TCP Socket 接收和发送数据包的缓冲区的大小。如果它们被设置为 -1,就使用操作系统的默认值。如果生产者或消费者与 broker 处于不同的数据中心,那么可以适当增大这些值。
消息发送的同步方法(Producer#send().get()):
kafka的重试机制内部实现,当我们在程序中通过get()或者回调函数捕获的异常已经是重试过了,还失败后获取到异常。
首先调用 send() 方法,然后再调用 get() 方法等待 Kafka 响应。如果服务器返回错误,get() 方法会抛出异常,如果没有发生错误,会得到 RecordMetadata 对象,可以用它来查看消息记录。这里就涉及到生产者在进行消息生产的时候容易出现的两种错误:
- 可重试异常(继承RetriableException)
LeaderNotAvailableException
:分区的Leader副本不可用,这可能是换届选举导致的瞬时的异常,重试几次就可以恢复NotControllerException
:Controller主要是用来选择分区副本和每一个分区leader的副本信息,主要负责统一管理分区信息等,也可能是选举所致NetWorkerException
:瞬时网络故障异常所致,可以通过再次建立连接来解决- KafkaProducer 被配置为自动重试时,如果多次重试后仍无法解决问题,则会抛出重试异常
- 不可重试异常
- 这类异常KafkaProducer 不会进行重试,直接抛出异常
SerializationException
:序列化失败异常RecordToolLargeException
:消息尺寸过大导致
消息发送的异步方法(带callback的Producer#send()):
这里即可做到异步发送消息,还能利用回调对异常情况进行处理:
首先实现回调需要定义一个实现了org.apache.kafka.clients.producer.Callback
的类,这个接口只有一个 onCompletion方法。其中RecordMetadata 和 Exception 不可能同时为空,消息发送成功时,Exception为null,消息发送失败时,metadata为空
然后,结合上面分析的两种类别的异常,此时可以进行分别处理:
1)生产者分区原因
(1)方便在集群中扩展,每个partition可以通过调整以适应它所在的 机器,而一个topic又可以有多个partition组成,因此整个集群就可以适应任意大小的数据了。
(2)可以提高并发,因为可以以partition为单位读写了。
kafka生产者发送消息时,发送到哪个分区?根据分区策略决定。
2)分区策略
Kafka 的分区策略指的就是将生产者发送的消息发送到哪个分区的算法。Kafka提供了默认的分区策略(顺序轮巡),同时也支持自定义分区策略,主要是靠显示配置生产者端的参数 Partitioner#partition()
,位于org.apache.kafka.clients.producer
下
我们需要将producer发送的数据封装成一个ProducerRecord对象。
(1)指明partition的情况下,直接将指明的值直接作为partition值;
(2)没有指明partition的值但有key的情况下,将key的hash值与topic的partition数进行取余得到partition值;
(3)既没有partition值又没有key值得情况下,第一次调用时随机生成一个整数(后面每次调用在这个整数上自增),将这个值与topic的partition数进行取余得到partition值,也就是常说的round-robin算法。(其实就是一个轮询)
ISR:
设想一下情景:leader收到数据,所有follower都开始同步数据,但有一个follower,因为某种故障,迟迟不能与leader进行同步,那leader就要一直等下去,直到它完成同步,才能发送ack。这个问题怎么解决?
Leader维护了一个动态的in-sync replica set(ISR)列表,意味着和leader保持同步的follow集合(如果follow因为自身原因导致无法及时的从leader同步数据,那么这个follower就会从ISR列表中踢出去)。当ISR中的follower完成数据同步后,leader就会给follower发送ack。如果follower长时间未向leader同步数据,则该follower将被踢出ISR,该时间阈值replica.lag.time.max.ms参数设定。Leader发生故障后,就会从ISR中选举新的leader。
ISR、AR、OR:
分区中的所有副本统称为AR(Assigned Repllicas)。所有与leader副本保持一定程度同步的副本(包括Leader)组成ISR(In-Sync Replicas),ISR集合是AR集合中的一个子集。与leader副本同步滞后过多的副本(不包括leader)副本,组成OSR(Out-Sync Relipcas),由此可见:AR=ISR+OSR。
ack应答机制:
acks:
0:producer不等待broker的ack,这一操作提供了一个最低的延迟,broker一接收到还没有写入磁盘就已经返回,当broker故障时有可能丢失数据。
1:producer等待broker的ack,partition的leader落盘成功后返回ack,如果在follower同步成功之前leader故障,那么将会丢失数据。
-1(all):producer等待broker的ack,partition的leader和follower全部落盘成功后才返回ack。但是如果在follower同步完成之后,broker发送ack之前,leader发生故障,那么会造成数据重复。
数据可靠性保证:
HW(High Watermak):表示了一个特定消息的偏移量(offset),消费者只能拉取到这个offset之前的消息。
LEO(Log End Offset):表示了日志文件中下一条待写入消息的offset。如上图leader所示,12指向的是HW的值,在12这个位置上的消息长度为12到18之前。所以下一条消息就需要存储在19(LEO)的位置上。
follower故障
follower发生故障后会被临时踢出ISR,待该follower恢复后,follower会读取本地磁盘记录的上次的HW,并将log文件高于HW的部分截取掉,从HW开始向leader进行同步。等待该follower的LEO大于等于该partition的HW,即follower追上leader之后,就可以重新加入ISR了。
leader故障
leader发生故障之后,会从ISR中选出一个新的leader,之后,为保证多个副本之间的数据一致性,其余的follower会将各自的log文件高于HW的部分截掉,然后从新的leader同步数据。
注意:这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。
kafka幂等如何实现?
Producer的幂等性指的是当发送同一条消息时,数据在Server端只会被持久化一次,数据不重复。
Exactly Once语义
将服务器的ACK级别设置为-1,可以保证Producer到server之间不会丢数据,即At Least Once语义。相对的,将服务器ACK级别设置为0,可以保证生产者每条消息只会被发送一次,即At Most Once语义
At Least Once可保证数据不丢失,但不能保证数据不重复;At Most Once可保证数据不重复,但不能保证数据不丢失。但均无法做到数据既不重复也不丢失,即Exactly Once语义。0.11版本之前,无法实现。
0.11版本之后,引入了一项重大特效:幂等性。即:Producer不论向server发送多少次重复数据,server端都只会持久化一条。
At Least Once + 幂等性 = Exactly Once
要启用幂等性,只需要将Producer的参数中enable.idompotence设置为true即可。开启幂等性的producer在 初始化的时候会被分配一个PID,发往同一个partition的消息会附带Sequence Number。而Broker端会对做缓存,当具有相同主键的消息提交时,Broker只会持久化一条。
但是PID重启就会变化,同时不同的Partition也具有不同主键,所以幂等性无法保证跨分区会话的Exactly Once。
Kafka消费者:
因为Kafka 消费者从属于消费者群组。一个群组中的消费者订阅的都是相同的主题,每个消费者接收主题一部分分区的消息,一个partition在同一个时刻只有一个consumer instance在消费,而且一般来说,消费者的个数都会建议和生产者的分区个数相同,这样既不会有消费不过来的消费者,也不会有空闲的消费者。
kafka消费者参数配置说明:
1.group.id
消费者分组ID。
2.enable.auto.commit
指定了消费者是否自动提交偏移量,默认值是true,为了尽量避免重复数据和数据丢失,可以把它设置为false,由自己控制合适提交偏移量,如果设置为true, 可以通过设置 auto.commit.interval.ms属性来控制提交的频率。
3.auto.offset.reset
该属性指定了消费者在读取一个没有偏移量或者偏移量无效(消费者长时间失效当前的偏移量已经过时并且被删除了)的分区的情况下,应该作何处理,默认值是latest,也就是从最新记录读取数据(消费者启动之后生成的记录),另一个值是earliest,意思是在偏移量无效的情况下,消费者从起始位置开始读取数据。
4.auto.commit.interval.ms
自动提交的时间间隔。
5.session.timeout.ms
该属性指定了当消费者被认为已经挂掉之前可以与服务器断开连接的时间。默认是3s,消费者在3s之内没有再次向服务器发送心跳,那么将会被认为已经死亡。和heartbeat.interval.ms配合使用。
6.heartbeat.interval.ms
当使用Kafka的分组管理功能时,心跳到消费者协调器之间的预计时间。心跳用于确保消费者的会话保持活动状态,并当有新消费者加入或离开组时方便重新平衡。该值必须必比session.timeout.ms小,通常不高于1/3。它可以调整的更低,以控制正常重新平衡的预期时间。
7.max.poll.records
在单次调用poll()中返回的最大记录数。
8.max.poll.interval.ms
使用消费者组管理时poll()调用之间的最大延迟。消费者在获取更多记录之前可以空闲的时间量的上限。如果此超时时间期满之前poll()没有调用,则消费者被视为失败,并且分组将重新平衡,以便将分区重新分配给别的成员。
max.poll.interval.ms参数用于指定consumer两次poll的最大时间间隔(默认5分钟),如果超过了该间隔consumer client会主动向coordinator发起LeaveGroup请求,触发rebalance;然后consumer重新发送JoinGroup请求。
消费方式:
consumer采用pull(拉)模式从broker中读取数据。
push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。它的目标是尽可能以最快速度传递消息,但是这样很容易造成consumer来不及处理,典型的表现就是拒绝服务以及网络拥塞。而pull模式则可以根据consumer的消费能力以适应当前的速率消费消息。
pull不足:如果kafka没有数据,消费者可能会陷入循环中,一直返回空数据。针对这点,kafka的消费者在消费数据时会传入一个时长参数timeout,如果当前没有数据可消费,consumer会等待一段时间之后再返回。
分区分配策略:
一个consumer group中有多个consumer,一个topic有多个partition,所以必然会涉及到partition的分配问题,即确定哪个partition由哪个consumer来消费。
Kafka有两种分配策略:一是RoundRobin和Range。
range策略主要是基于范围的思想,它将单个topic的所有分区按照顺序排列,然后把这些分区划分成固定大小的分区段并依次分配给每个consumer。
round-robin策略则会把所有topic的所有分区顺序摆开,然后轮询式地分配给各个consumer。
同一时刻,一条消息只能被组中的一个消费者实例消费。
consumer group订阅一个topic,意味着topic下的所有分区都会被consumer group中的consumer消费掉,按从属关系来说,该topic下的每个分区只从属于该consumer group中的一个消费者,不可能出现该consumer group下多个消费者负责同一个分区。
多个消费者读取一个分区会出现什么情况?
Kafka它在设计的时候就是要保证分区下消息的顺序,也就是说消息在一个分区中的顺序是怎样的,那么消费者在消费的时候看到的就是什么样的顺序,那么要做到这一点就首先要保证消息是由消费者主动拉取的(pull),其次还要保证一个分区只能由一个消费者负责。倘若,两个消费者负责同一个分区,那么就意味着两个消费者同时读取分区的消息,由于消费者自己可以控制读取消息的offset,就有可能C1才读到2,而C2读到1,C1还没处理完,C2已经读到3了,则会造成很多浪费,因为这就相当于多线程读取同一个消息,会造成消息处理的重复,且不能保证消息的顺序。
offset的维护:
Kafka 0.9版本之前,consumer默认将offset保存在Zookeeper中,从0.9版本开始,consumer默认将offset保存在kafka一个内置topic中,该topic为_consumer_offset。
分区重平衡:
例如:某 Group 下有 20 个 consumer 实例,它订阅了一个具有 100 个 partition 的 Topic 。正常情况下,kafka 会为每个 Consumer 平均的分配 5 个分区。这个分配的过程就是 Rebalance。
最初是一个消费者订阅一个主题并消费其全部分区的消息,后来有一个消费者加入群组,随后又有更多的消费者加入群组,而新加入的消费者实例分摊了最初消费者的部分消息,这种把分区的所有权通过一个消费者转到其他消费者的行为称为重平衡,英文名也叫做 Rebalance
重平衡非常重要,它为消费者群组带来了高可用性 和 伸缩性,我们可以放心的添加消费者或移除消费者,不过在正常情况下我们并不希望发生这样的行为。在重平衡期间,消费者无法读取消息,造成整个消费者组在重平衡的期间都不可用。另外,当分区被重新分配给另一个消费者时,消息当前的读取状态会丢失,它有可能还需要去刷新缓存,在它重新恢复状态之前会拖慢应用程序。
Rebalance 的触发条件:(https://blog.csdn.net/qq_27357801/article/details/106825345)
组成员个数发生变化。例如有新的 consumer 实例加入该消费组或者离开组。
订阅的 Topic 个数发生变化。
订阅 Topic 的分区数发生变化。
消费者无法再指定的时间之内完成消息的消费。
Offset、偏移量:
生产者offset:
不管是多少个生产者,不管规定了这些生产者会写入哪一个分区。但只要生产者写入消息的时候,一定是每一个分区都有一个offset,这个offset就是生产者的offset,同时也是这个分区的最新最大的offset
消费者offset:
这是某一个分区的offset情况,我们已经知道生产者写入的offset是最新最大的值也就是12,而当Consumer A进行消费时,他从0开始消费,一直消费到了9,它的offset就记录在了9,再比如Consumer B纪录在了11。等下一次他们再来消费时,他们可以选择接着上一次的位置消费,当然也可以选择从头消费,或者跳到最近的记录并从“现在”开始消费。
Kafka为啥这么快:
数据写入:
1. 顺序写入磁盘
Kafka的producer生产数据,要写入到log文件中,写的过程是一直追加到文件末端,为顺序写(顺序写速度比随机写速度快得多)。官网有数据表明,同样的磁盘,顺序写能到600M/s,而随机写只有100K/s。这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。
2. Memory Mapped Files
即便是顺序写入硬盘,硬盘的访问速度还是不可能追上内存。所以Kafka的数据并不是实时的写入硬盘,它充分利用了现代操作系统分页存储来利用内存提高I/O效率。
Memory Mapped Files(后面简称mmap)也被翻译成内存映射文件,在64位操作系统中一般可以表示20G的数据文件,它的工作原理是直接利用操作系统的Page来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上(操作系统在适当的时候)。
通过mmap,进程像读写硬盘一样读写内存(当然是虚拟机内存),也不必关心内存的大小有虚拟内存为我们兜底。
使用这种方式可以获取很大的I/O提升,省去了用户空间到内核空间复制的开销(调用文件的read会把数据先放到内核空间的内存中,然后再复制到用户空间的内存中。)也有一个很明显的缺陷——不可靠,写到mmap中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘。Kafka提供了一个参数——producer.type来控制是不是主动flush,如果Kafka写入到mmap之后就立即flush然后再返回Producer叫同步(sync);写入mmap之后立即返回Producer不调用flush叫异步(async)。
数据读取:
3. 零复制技术(基于sendfile实现Zero Copy)
https://www.cnblogs.com/binyue/p/10308754.html
4.批量压缩
Kafka使用了批量压缩,即将多个消息一起压缩而不是单个消息压缩
Kafka允许使用递归的消息集合,批量的消息可以通过压缩的形式传输并且在日志中也可以保持压缩格式,直到被消费者解压缩
Kafka Producer API:
消息发送流程:
生产者消息发送的流程上面也说过了,这儿的流程和上面的差不多,可以一起参考这来看。
Kafka的Producer发送消息采用的是异步发送的方式。在消息发送的过程中,涉及到了两个线程——main线程和Sender线程,以及一个线程共享变量——RecordAccumulator。main线程将消息发送给RecordAccumulator,Sender线程不断从RecordAccumulator中拉取消息发送到Kafka broker。
手动提交offset:
自动提交是基于时间进行提交的,可能会出现某条消息正在处理过程中就进行了提交,但是当前消息却处理失败了,导致该条消息就没法再次进行处理了。因此,kafka还提供了手动提交offset的API。
手动提交offset的方法有两种:分别是commitSync(同步提交)和commitAsync(异步提交)。两者的相同点是,都会将本次poll的一批数据最高的偏移量提交;不同点是commitSync阻塞当前线程,一直到提交成功,并且会自动失败重试;而commitAsync则没有失败重试机制,固有提交失败可能。
kafka如何确保消息读取的顺序性:
不同的partition的顺序是无法保证有序的,但是在单个partition内消息时有序的,如果需要保证部分消息的有序,可以:
- 生产者发送数据时,指定发送到对应的分区,这样会严重影响吞吐量。
- 生产者发送数据时,同一类数据指定一个key,这样也可以保证同一类消息发送到同一个分区。
- 生产者发送数据时,设置max.in.flight.requests.per.connections=1(此参数指定了生产者在收到服务器响应之前可以发送多少消息,它的值越高,就会占用越多的内存,不过也会提高吞吐量。把它设为1可以保证消息是按照发送的顺序写入服务器)。如果该参数不为1,那么当第一个批次写入失败时,第二个批次写入成功,Broker会重试写入第一个批次,如果此时第一个批次重试写入成功,那么这两个批次消息的顺序就反过来了。
上面三种举措可以保证消息被顺序的发送到了同一个分区中,一个消费者如果是单线程消费的话,那么上诉举措就可以确保消息的顺序了。但是如果这个消费者是多线程消费的,就不能保证消息消费的有序性了。 如下图:
我们在消费者里可能会搞多个线程来并发处理消息。因为如果消费者是单线程消费处理,而处理比较耗时的话,比如处理一条消息耗时几十 ms,那么 1 秒钟只能处理几十条消息,这吞吐量太低了。而多个线程并发跑的话,顺序可能就乱掉了。
消费者多线程消费时,可以考虑用N个queue,把这个分区里面的消息根据key进行分组,同一个key的消息一般都是要确保顺序性的,那就放到同一个queue里面,那么N个queue,对应多个线程去消费就可以了。
kafka为啥会出现消息丢失:
1.auto.commit.enable=true,消费端自动提交offersets设置为true,当消费者拉到消息之后,还没有处理完 commit interval 提交间隔就到了,提交了offersets。这时consummer又挂了,重启后,从下一个offersets开始消费,之前的消息丢失了。
2.生产者发送消息时,发送失败。
3.kafka批量入盘时,kafka为了得到更高的性能和吞吐量,将数据异步批量的存储在磁盘中。将数据存储到linux操作系统中,会先存储到页缓存Page cache
中,按照时间/fsync命令
或者其他条件再从页缓存刷入file
。如果数据在page cache中时系统挂掉,数据会丢失。
4. 不完全首领选举也会导致消息丢失。
kafka如何保证不丢失数据?
从broker侧来看:
Kafka的复制机制和分区的多副本架构是kafka可靠性保证的核心。
主要包括3个方面:
1. Topic 副本因子个数:replication.factor >= 3
2. 最小同步副本数:min.insync.replicas = 2
3. 禁用unclean选举:unclean.leader.election.enable=false
副本因子:就是通过replication.factor设置副本数量(一个是leader,其余全是followe),leader提供接受生产者数据服务,如果laeder挂掉了,通过ISR机制从ISR列表中选取一个followe作为新的leader,确保消息的不丢失。一般来说,副本设置为3可满足大部分使用场景。副本因子越大,占用的磁盘空间也就越大。注:副本的数量不能超过broker的数量。
最小同步副本数: 如果要确保已提交的数据被写入不止一个副本,就需要把最小同步副本数量设置为大一点的值。对于一个包含3 个副本的主题分区,如果min.insync.replicas=2 ,那么至少要存在两个同步副本才能向分区写入数据。如果进行了上面的配置,此时必须要保证ISR中至少存在两个副本,如果ISR中的副本个数小于2,那么Broker就会停止接受生产者的请求。尝试发送数据的生产者会收到NotEnoughReplicasException异常,消费者仍然可以继续读取已有的数据.
禁用unclean选举(不完全首领选举): 选择一个同步副本列表(ISR中的follower)中的分区作为leader 分区的过程称为clean leader election。注意,这里要与在非同步副本中选一个分区作为leader分区的过程区分开,在非同步副本中选一个分区作为leader的过程称之为unclean leader election。由于ISR是动态调整的,所以会存在ISR列表为空的情况,通常来说,非同步副本落后 Leader 太多,因此,如果选择这些副本作为新 Leader,就可能出现数据的丢失。毕竟,这些副本中保存的消息远远落后于老 Leader 中的消息。在 Kafka 中,选举这种副本的过程可以通过Broker 端参数 unclean.leader.election.enable控制是否允许 Unclean 领导者选举。开启 Unclean 领导者选举可能会造成数据丢失,但好处是,它使得分区 Leader 副本一直存在,不至于停止对外提供服务,因此提升了高可用性。反之,禁止 Unclean Leader 选举的好处在于维护了数据的一致性,避免了消息丢失,但牺牲了高可用性。分布式系统的CAP理论说的就是这种情况。
从生产者和消费者侧来看(主要是参数设置):
生产者:
retires(消息发送失败,重试次数),尽量大一些,减少因为网络波动导致的发送失败。
acks = -1(all)。
max.in.flight.requests.per.connections=1(该参数指定了生产者在收到服务器晌应之前可以发送多少个消息)
使用带回调的api(producer.send(msg, callback)),查看响应信息进行对应处理。
消费者:
禁用自动提交:enable.auto.commit=false,消费者处理完消息之后再提交offset。与自动提交相关的是自动提交的间隔时间auto.commit.interval.ms,
默认是5秒钟提交一次,消费者会自动把从 poll()
方法轮询到的最大偏移量提交上去。
配置auto.offset.reset。这个参数指定了在没有偏移量可提交时(比如消费者第l次启动时)或者请求的偏移量在broker上不存在时(比如数据被删了),消费者会做些什么。这个参数有两种配置。一种是earliest:消费者会从分区的开始位置读取数据,不管偏移量是否有效,这样会导致消费者读取大量的重复数据,但可以保证最少的数据丢失。一种是latest(默认),如果选择了这种配置, 消费者会从分区的末尾开始读取数据,这样可以减少重复处理消息,但很有可能会错过一些消息。
手动同步提交commitSync():
commitSync() 将会提交由 poll() 返回的最新偏移量,如果处理完所有记录后要确保调用了该方法,否则还是会有丢失消息的风险。
只要没有发生不可恢复的错误,commitSync()方法会阻塞,会一直尝试直至提交成功,如果失败,也只能记录异常日志。
同步提交有重复消费的风险,如果发生了重平衡,从最近一批消息到发生在重平衡之间的所有消息都将被重复处理。
手动异步提交commitAsync():
异步提交不会进行重试
异步提交支持回调OffsetCommitCallback(),在 broker 作出响应时会执行回调。回调经常被用于记录提交错误或生成度量指标。如果要用它来进行重试,则一定要注意提交的顺序(可使用一个单调递增的序列号维护异步提交顺序)
同步和异步组合提交
一般情况下,针对偶尔出现的提交失败,不进行重试不会有太大的问题,因为如果提交失败是因为临时问题导致的,那么后续的提交总会有成功的。但是如果在关闭消费者或再均衡前的最后一次提交,就要确保提交成功
因此,在消费者关闭之前一般会组合使用commitAsync和commitSync提交偏移量
如何确定合适的kafka主题的分区数量?
一个简单的计算公式为:分区数 = max(生产者数量,消费者数量)
哪些情形会消息重复消费?
1.enable.auto.commit=false时,消费者消费一批数据时,例如有10条数据,消费到到第五条时,出现异常无法自动提交offset。那么下次消费时还是会把重新消费这10条数据,但是前面五条数据已经消费过了。
2.kafka的Rebalance机制。consumer在消息消费过程中,还未进行offset提交,Rebalance机制将该partition分配给了其他consumer,此时进行offset会失败,会造成重复消费。
在实际开发过程与,遇到个一个问题:
因为消费速度过慢而导致触发rebalance重平衡,然后分配给新的消费者,然后新的消费者还会消费过慢,再次rebalance, 这样一直恶性循环下去。
造成原因:没有合理配置好max.poll.interval.ms和max.poll.records这两个参数。
max.poll.interval.ms表示两次poll()之间的最大间隔,默认为5分钟。如果两次poll()的时间间隔超过了这个值,那么就认为这个consumer失败了,会触发rebalance。
原因就是max.poll.records设置过大,导致拉取到大量数据时,在max.poll.interval.ms这个时间间隔内没有消费完。触发了rebalance。
在0.10.1.0之前的版本还有一个原因(0.10.1.0及之后的版本进行了修复)就是在0.10.1.0之前的版本中,由于心跳请求是在poll()拉取消息的方法中执行的,因此如果当前批次处理消息耗时太长,就会导致consumer没有机会按时发送心跳,broker认为消费者已死,触发rebalance。在0.10.1.0或更新的版本中解决了这个问题,心跳请求会在单独的线程中发送,因此就不会出现因为消息处理过长而发不出心跳的问题了。
kafka如何避免消息的重复消费(如何保证消息的幂等性)?
最后一个大问题:
kafka的事务