Kafka随笔

Kafka

这里本人会对Kafka消息中间件分别以生产者、服务端、消费者的角度去阐述Kafka较为重要的特性,内容可能会显得生硬,希望读者能从中收获到自己想要的

生产端

关键参数说明

acks
acks表示生产者与服务端的消息确认参数。 acks = 0:表示生产者发送消息,不需要等待broker确认,数据丢失可能性比较高 acks = 1:表示生产者发送消息,只要Leader确认了即表示消息已确认,延迟性小,数据可能丢失 acks = all:表示生产者发送消息,需要所有同步副本收到即表示消息已确认。但是由于如果仅有一个副本,数据还是有丢失的可能性,但是延迟性最高

batch.size
生产者发送多个消息到broker上,为了减少网络开销,通过批量发送消息,设置size的字节数大小,默认是16KB。达到指定大小后统一发送。

linger.ms
生产者发送消息到broker上,达到指定逗留时间后统一发送。此参数为了减少broker发送次数,注意这个参数和batch.size是或的关系,满足任意一个条件则发送

max.request.size
单个请求的最大字节数,为了防止较大数据影响吞吐量,默认为1MB

compression.type
数据压缩算法,gzip提供更高的压缩比,但是相对消耗CPU资源;snappy压缩占用少量cpu资源,但是压缩比较低

retries
指定消息发送失败重试次数,默认重试时间是100ms,建议重试时间间隔大于崩溃恢复的时间(先测试崩溃恢复的时长),所以没有必要在代码逻辑里处理可重试的错误,只需要处理不可重试错误或者次数超出上限情况。

max.in.flight.requests.per.connection
表示生产者在收到服务端响应之前可以发送多少个消息,默认是5。如果要保证消息被消费的顺序性和成功,建议设置为1,然后retries建议设置大于0。

试想如果生产者发送第一批消息失败了,随着第二批消息发送过来并处理成功了,第一批消息继续重试,这样消息有序性就无法保证了。但是设置为1会严重影响到吞吐量

其他参数说明

delivery.timeout.ms
消息交付的超时时间。从send()发送之后到收到成功或者失败的响应之后的最大值。它的值应该大于等于request.timeout.ms和linger.ms之和。

request.timeout.ms
指定了生产者发送数据时等待服务器返回响应的时间,这个时间是指已经发送到broker上的时间,并没有算上逗留时间

同步发送

生产者发送消息,通过Future接收消息的内容。如果返回异常一般会是可重试异常,超过次数则抛出异常;还有一种就是直接抛出的异常,比如消息太大,或者其他原因。

异步发送

通过回调Callback接口的onCompletion方法,可以实现消息异步发送,不需要同步等待broker响应

消息发送策略

如果发送者指定了分区,那么Kafka的分区器直接返回对应的分区。
Kafka提供了默认的消息发送策略;如果指定了key,那么消息会通过key的Hash值指定的分区(即使Java升级,散列值也不会变);‘如果没有指定key,那么消息会被轮询发送。当然我们也可以实现自己的分发策略,重写默认实现见《Kafka权威指南》3.6节。

服务端

Topic&Partition

Topic是一个存储消息的逻辑概念。不同Topic的消息是分开存储在磁盘的。Topic采用多对多的对接方式。
一个Topic可以由多个Partition组成,每个分区可以看成是一个队列,也可以理解成是DB中的分库分表,消息会均匀的分布在多个分区里并且不重复。消费者生产者和分区存在一定的映射关系。

offset

查看每个消费组各个消费者消费主题分区的情况
bin/kafka-consumer-groups.sh --bootstrap-server 127.0.0.1:9092 --describe --group KafkaConsumerDemo

TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST CLIENT-ID firstTopic 1 28 154803 154775 - - - firstTopic 2 27 154801 154774 - - - test 0 178 178 0 - - - firstTopic 0 36 154809 154773 - - -

在Kafka服务端,存在一个_consumeroffset的主题(默认50个分区),存储着每个消费者的offset

关键参数说明

default.replication.factor
配置表明自动创建主题的复制系数,意思是如果创建主题不指定,则默认会以此参数创建副本数(这个参数应该是不支持修改的)

replication.factor
也可以在创建主题的时候指定副本系数。

min.insync.replicas
表示最小的ISR同步数量,如果broker数量小于这个值,则服务无法使用。这个是保证线上服务最少同步的数量。默认值为1。如果没有足够的副本存活,broker则会停止接收生产者的消息。

Rebalance

Kafka通过服务端的Coordinator来执行重新均衡和管理consumer group:
1.消费者通过向集群发送GroupCoordinatorRequest请求,服务端会返回一个负载最小的broker的id,将broker设置为协调者。
2.在进行重新均衡之前,消费者通过向协调者发送joinGroup请求,协调者会确认一个消费组的leader角色,协调者会将leader、组成员相关信息和年代信息发送给各个消费者。
3.之后组leader将消费者分区分配方案发送给协调者,然后由协调者将分配方案信息发送给各个消费者,这样所有消费者就知道了自己的分区。(分区分配方案实际上是在客户端进行的,这样具备更好的灵活性)

性能

数据从磁盘到输出流采用“零拷贝” <参考链接零拷贝>
文章的大致意思是:Kafka省掉了2-3步骤,直接从内核空间读取到socket缓冲区,然后发送到网卡缓冲区

传统文件传输

  1. 操作系统从磁盘读取数据到内核空间的页缓存中
  2. 应用从内核空间读取数据到用户空间缓冲区
  3. 应用将数据写到内核空间的socket缓冲区
  4. 操作系统从socket缓冲区复制数据到通过网络发送的网卡缓冲区

Zero copy

通过Java提供的FileChannel.transferTo()方法,可以直接将内核空间的页缓存数据读取到socket缓冲区中。减少了上下文切换次数和复制次数

消息存储

我们知道服务端将topic的消息分区到各个broker的磁盘上保存,默认在/tmp/kafka-logs/的相应主题分区中,可以找到不同的segment分段文件,segment的阈值在配置文件中可以设置。

每个segment文件分为index, log, timeindex还有snapshot这几类文件。segment文件之间的文件命名根据上一个segment文件内容的最后一个offset值+1作为新文件名,这样便于二分半查找。通过kafka-run-class.sh可以查看文件内容

命令:sh kafka-run-class.sh kafka.tools.DumpLogSegments --files /tmp/kafka-logs/test-0/00000000000000000000.log --print-data-log

• .index: 保存的是offset和position的对应关系,采用的是稀疏索引。

说明:通过offset可以查找到一个范围,比如传递的是offset=10123,index文件中能找到的最接近的是[10100, 1233421],可以得到position=1233421,通过position=1233421然后去log文件中查找。

• .log:保存的是每条消息的offset和内容。

说明:通过index的position值,在log文件中直接通过流读取到position的内容,从读取到的消息得到的offset开始判断和需要的offset是否相等,如果相等则返回消息。

• .timeindex:保存的是offset和timestamp之间的关系,便于后面做日志清理。

个人理解:这里维护的是时间和偏移量的关系,为了方便日志做清理的时候,判断数据是否过期。
这里采用的是稀疏索引,对于数据量特别大的时候查找效率高,读取索引文件不会特别消耗内存。

日志处理策略

清理策略

Kafka对日志采用分段存储,既方便存储,也方便清理。

  1. 根据消息的保留时间,当消息在kafka的保存时间超过了指定的时间,就会触发清理
  2. 或者当topic所占的文件大小超过一定的阈值就开始清理最旧的数据,Kafka会启动一个后台线程清理。

参数:log.retention.bytes和log.retention.hours

压缩策略

我们知道我们可以对每个消息加以Key标识,但是即使key的值一致并不会导致消息被覆盖,同样会存储。不过在Kafka的消息压缩策略中,Kafka会根据key来做压缩策略,也就是说同一个key,旧的value会被清理掉,同样也提供了相关配置

Partition的副本机制

参考链接

副本分配算法

如果有M个broker和N个排序过的partition
第N个分区分配到 (N mod M)个broker上
第N个分区的第J个副本分配到(N + j mod M)个broker上,mod是取模算法
理解成:副本是依次排序在多个broker上的

查看分区副本Leader

get /brokers/topics/topicName/partitions/1/state {"controller_epoch":12,"leader":0,"version":1,"leader_epoch":0,"isr":[0,1]}

leader=0则表示分区为1的leader是broker=0的机器。

副本机制的概念

partition提供副本机制保证消息的可靠性,提供分区的多个备份。每个分区均有一个Leader和0个或者多个Follower,Leader用于接收生产者的消息并提供消息给消费者,Follower从Leader同步数据。生产者发送消息有一个acks参数,这个参数就是用于保证消息需要多少个副本确认才能算发送成功。

unclean.leader.election.enable 表示不完全选举:不完全选举就是允许不同步的副本成为新的Leader。如果启用不完全选举那么要承担数据丢失的可能性,也可以禁用

ISR (in sync replicas) 表示可消费的消息量和Leader的数量差不多并且没有超过阈值

满足如下条件:如果follower与leader保持的同步消息数超出阈值则会被Leader踢出去
副本最后一条消息的offset与leader的最后一条消息,如果在指定时间replica.lag.time.max.ms还没有同步所有消息,则将被移出副本。

如果所有副本分区全部挂掉?

  1. 等待ISR中的一个副本“活”过来,并选择这个副本作为Leader(只能期望它包含了所有数据)
  2. 选择第一个“活”过来的副本作为Leader(不一定是ISR)

实际上这并不是Kafka的窘境,对于所有基于“法定人数”的场景都会存在这个问题。要么选择100%丢失数据或者违反一致性

如果允许不同步的副本成为首领,那么我们要承担数据丢失和数据不一致的风险,如果不允许他们成为首领,那么要接受较低的可用性,因为必须等待原先的首领恢复。

消费端

group.id

每个主题可以由多个组同时消费,主题的每个分区只能由同一个组的一个消费者消费,一个消费者可以消费多个分区。简单的可以理解成:不同业务组可以消费同一个主题,但是同一个业务组内的消费者之间不能并发消费同一个分区(这里一个分区可以理解成一个队列)。

关键参数说明

fetch.min.bytes
指定消费者从服务器获取消息的最小字节数,服务端会等到达到这个字节数值才会返回消费者,这样可以减少broker和消费者之间的负载和请求次数

fetch.max.wait.ms
和前面一样,有最小字节数,那么也有最大等待时间,如果达到最大等待时间,服务端就将消息返回给消费者,主要看两个条件那个先被满足。

max.partition.fetch.bytes
指定服务器从每个分区里返回给消费者的最大字节数,默认为1MB。这个需要结合分区数、消费者数和崩溃时消费者多消费的字节数综合考虑。(还需要考虑max.message.size broker能接受的最大消息的字节数,否则这个消息无法被接受)

max.poll.records
一次性拉取消息的最大记录数

poll(数据获取)

轮询方法 见4.4 轮询不只是获取数据那么简单。在第一次调用新消费者的 poll () 方法时,它会负责查找GroupCoordinator , 然后加入群组,接受分配的分区。 如果发生了再均衡,整个过程也是在轮询期间进行的。当然心跳也是从轮询里发迭出去的。所以我们要确保在轮询期间所有的任何处理工作都应该尽快完成。

org.apache.kafka.clients.consumer.CommitFailedException: Commit cannot be completed since the group has already rebalanced and assigned the partitions to another member. This means that the time between subsequent calls to poll() was longer than the configured max.poll.interval.ms, which typically implies that the poll loop is spending too much time message processing. You can address this either by increasing the session timeout or by reducing the maximum size of batches returned in poll() with max.poll.records. 大致的意思是如果两次poll之间的时间大于 max.poll.interval.ms 值,组会发生均衡。 建议增加间隔时间或者减少poll返回的最大消息数量max.poll.records。 为什么是两次poll之间呢?原因是客户端poll的时候会发送心跳,心跳和消息轮询是

pause和resume

consumer的pause和resume方法,调用pause会暂停轮询,poll不会返回消息,直到调用resume继续的时候。这个方法可以保证在消费的时候发生异常需要重试

偏移量

在每个分区里都存在一个消息偏移量,每个消费者将它的消费偏移量发送给它所在组的协调者(broker)。协调者将偏移量提交请求同步给组的所有副本主题,之后将成功响应给消费者。默认5s自动提交。假设我们仍然使用默认的 5s 提交时间间隔,在最近一次提交之后的 3s 发生了再均衡,再均衡之后,消费者从最后一次提交的偏移量位置开始读取消息。这个时候偏移量已经落后了 3s ,所以在这 3s 内到达的消息会被重复处理。可以通过修改提交时间间隔来更频繁地提交偏移量,减小可能出现重复悄息的时间窗,不过这种情况是无也完全避免的

同步提交

自动提交enable.auto.commit=true、auto.commit.interval.ms=1000,但是最好手动提交避免消息重复处理和丢失,比如消息还未处理完,就被自动提交了commitSync()手动提交。程序处理完消息之后,同步提交偏移量,成功之后立即返回,如果在处理过程中,发生了再均衡消息将被重复处理。

异步提交

异步提交需要注意提交的顺序性,可以采用递增序列化来保证消息的顺序性

在成功提交或碰到无怯恢复的错误之前,commitSync () 会一直重试,但是 commitAsync() 不会,这也是 commitAsync () 不好的一个地方。它之所以不进行重试,是因为在它收到 服务器响应的时候,可能有一个更大的偏移量 已经提交成功。假设我们发出一个请求用 于提交偏移量 2000,这个时候发生了短暂的通信问题 ,服务器收不到请求,自然也不会 作出任何响应。与此同时,我们处理了另外一批消息,并成功提交了偏移量 3000。如果 commitAsync () 重新尝试提交偏移量 2000 ,它有可能在偏移量 3000 之后提交成功。这个时 候如果发生再均衡,就会出现重复消息。 (解决方案:使用一个递增的序列号来维护异步提交的顺序)提交指定的偏移量commitSync()commitAsync()只会提交最后的一个偏移量,可能消息还未处理完,这会造成消息的丢失。并且在这种情况下,如果有新的消费者代替了工作,那么丢失的消息无法被消费了

异步和同步组合提交

先采用异步提交,如果发生异常,在finally模块采用同步提交

分区分配策略

消费者可以指定特定的消费分区,也可以订阅主题的所有分区。

在Kafka中有三种分区分配策略,范围分区和轮询分区还有一个StickyAssignor
范围分区:首先将分区排序,然后平均分配,注意这里是按照一个消费者分配相邻的几个分区依次分配下去,比如:C1消费0,1,2,3;C2消费4,5,6; C3消费7,8,9。如果有多个主题的时候,那么第一个消费者会多消费两个分区,负载会不太均衡。
轮询分区:使用前提:每个主题的消费者实例具有相同数量的流;每个消费者订阅的主题必须是相同的。否则会分配不均衡

何时触发分区分配

• 同一个消费组里面新增了消费者
• 或者消费者离开(主动和被动)
• 新增了分区

可靠性消息投递

说明:消息的可靠性投递实际上是从消息是否会丢失、重复消费、乱序消费的角度来考虑的

保证发送端和服务端的可靠性

  1. 发送端配置恰当的acks值,并且针对为成功发送的消息做重试机制
  2. 服务端保证完全选举,不允许未同步的副本成为分区首领(但是要承担较低的可用性)。
  3. 服务端可以配置最少存在的同步副本才允许数据写入分区:min.insync.replicas。

这是最保险的做法也是最慢的,可以使用异步模式和更大的批次来加快速度,但是会降低吞吐量。

保证消费者的可靠性

  1. 处理完消息之后提交偏移量,不要在处理完成之前提交偏移量,否则会有消息丢失的可能
  2. 采取消费者的重试机制
  3. 把没有处理成功的消息放到缓冲区,调用消费者的pause()方法确保poll不会返回数据,尝试重新处理消息,之后在调用resume()继续获取数据。
  4. 将消息写入一个独立的主题,然后继续使用一个消费者读取这个主题进行重试,重试的时候暂停pause(),类似其他系统的死信队列
  5. 处理消息将结果写入支持唯一键的系统,保证同一个消息不会被重复处理。保证幂等性
    个人总结:实际上对于Kafka来说,合理的配置加上生产者、消费者的确认机制就可以保证消息可靠性投递,而并不是说Kafka一定会造成消息丢失。对于分布式消息架构,是对C和A的一种平衡。

参考资料《Kafka权威指南》
如有错误欢迎各位读者指出

原创地址https://www.jianshu.com/p/6490ad640cc2

你可能感兴趣的:(Kafka随笔)