读写数据基于topic,每个topic分为多个partition,每个partition有多个副本。每个partition分为多个段文件,在/KaTeX parse error: Expected '}', got 'EOF' at end of input: {topicName}-{partitionid}/目录下存储一个个segment文件。分为log文件和索引文件,索引文件分为按照offset查找文件和按时间查找文件。找到消息是先根据二分法找到消息在哪个段文件(段文件中的消息是按序号递增的),然后根据索引文件找到消息在段文件中大概的位置,再遍历查找到消息。
消费顺序
同个消费组一个partition只能由一个消费者消费,因此只要让需要保证消费顺序的消息,发送到同一个partition,那么消费就是有序的
partition
术语
- ISR:in-sync replics,每个分区(Partition)中同步的副本列表。一个动态副本列表,当副本与leader已经超过
replica.lag.time.max.ms
设置的时间没有通信,则会踢出isr。 - Hight Watermark:副本水位值,表示分区中最新一条已提交(Committed)的消息的Offset。
- LEO:Log End Offset,Leader中最新消息的Offset。
- Committed Message:
已提交消息,已经被所有ISR同步的消息
。 - Lagging Message:没有达到所有ISR同步的消息。
消息发送
可以指定发送到哪个partition,也可以不指定,通过指定一个key,kafka根据这个key计算命中的partition。如果key也没指定就按照Round Robin轮询的方式发送到不同的partition。
leader follower
每个partition都有副本,有个broker中的partition作为leader,若干个broker的partition作为follower,读写数据都基于leader,每次数据变动都会同步到followe
r。当leader挂了,会基于zk的通知机制,重新选举新的leader
,恢复partition正常运行。也就是一个broker可能包含多个partition,某些partition是leader,某些可能其他partition的follower.
分散数据读写到多个broker,因此也提高了吞吐量。
partition在各broker的分配
broker也有一个负责协调的执行者,称为controller
,这个broker就负责了partition在broker的分配
、leader的选举
。
分配规则:将broker和Partition进行排序,然后将第i个Partition对broker数进行取模命中一个broker,将第i个Partition的第j个副本对broker数进行取模命中一个broker.
partition在消费组中的分配
reblance
。消费端也要选一个broker作为分配者,将partition分给各消费者处理。称为coordinator
。每个消费者都会定期给coordinator发送心跳,如果需要reblance,则会响应对应的信号
。同时也可以知道每个消费者存活的情况。消费线程和心跳报告的线程是分开的。
- 怎么选择coordinator?offset所在的partition的leader所在的broker作为分配者(
offset保存在一个特定topic,通过消费组id进行hash,所以同个消费组的offset都保存在同一个partitio
)。 - 流程。consumer启动后随便连接一个broker,返回coordinator,consumer发送心跳包给coordinator,coordinator返回
IllegalGeneration
,说明当前需要reblance
重新分配partition
,之后该消费者继续 - rereblance过程。触发rereblance时,当消费者发送心跳请求过来时,会通知消费者准备进行rereblance,然后从消费者中选一个
leader
,并把其他消费者信息传给消费者。消费者进行rereblance对partition进行分配,将分配结果发给服务端。服务端将分配结果告诉所有leader分配结果。
触发reblance的情况
- 增加partition
- 增加消费者
- 消费者主动关闭
- 消费者宕机了
- coordinator自己也宕机了
参数配置
heartbeat.interval.ms
参数指定多久发一次心跳session.timeout.ms
指定多久没发心跳则认为消费者挂了。max.poll.interval.ms
指定两次poll数据最大允许的时间延迟,如果超过了设定的时间,也会认为消费者挂了。
之所以区分两个参数,是因为消费线程和心跳报告的线程是独立的。因此用两个参数来保证消费者是否正常。
leader选举
controller
还会负责leader的选举,监控zk broker目录,当有broker挂了,就给受影响的partition从指定目录下找到已经同步了数据的partition副本ISR(in-sync replica已同步的副本)
,选出新的leader。然后直接发送请求给其他broker通知它们
Controller选举
Controller
选举就是依赖zk,每个broker去指定目录创建临时节点,创建成功的broker则称为Controller.
存储
每个partition对应一个文件目录,包含数据和日志文件,因此partition量大时,还需要考虑系统文件句柄数的问题。
partition数量
partition越多吞吐量越高,但是还与broker的传输能力有关系,有相对的上限,需要也提供broker数量。
数量多可能存在其他问起:
1.且broker出现异常,需要恢复的leader数也更多
2.需要分配更多的内存支持批量提交.
3.消息发送后,需要复制到其他副本后,才对客户端可见,所以会导致一定的数据延迟。
消息确定
通过参数request.required.acks
可设置消息确认机制
acks | 说明 |
---|---|
0 | 只管发送,不关心broker是否处理成功,可能丢数据 |
1 | 当写Leader成功后就返回,其他的replica都是通过fetcher去同步的,所以kafka是异步写,主备切换可能丢数据 |
-1 | 要等到isr里所有机器同步成功,才能返回成功 ,消息才可见,延时取决于最慢的机器。强一致,不会丢数据。而且如果isr个数小于min.insync.replicas,也会返回不可用 |
同步的方式
leader提供读写,follower主动定时拉取,不对外提供服务,只是数据备份作用。
即由follower进行拉取同步,而如果request.required.acks
设置为-1,则同步到isr中所有broker后消息才提交成功,消费端才可见。
导致副本复制跟不上
- 可能是副本机器挂了
- 可能是副本io瓶颈,写入速度慢,跟不上从leader拉取消息的速度
- 刚启动的副本,还没同步完
leader epoch
offset
通过offset记录某个消费组中某个消费者消费到某个partition的哪个位置。
保存方式
1.0以前保存在zk,但是存在性能问题,之后通过讲offset发送到__consumeroffsets
topic,存储在内存中,消息key由分组id、partition、topic组成,value为偏移量。消息存储在内存中,消息清除的策略是compact
,只保留最新的key,其余都清掉。
这个topic默认有50个分区,通过offsets.topic.num.partitions
参数指定。基于消费组id
进行hash取模。
消息投递语义
kafka 3种消息投递语义,即product在遇到网络异常等情况,消息推送的不同处理。
- At most once:最多一次,消息可能会丢失,但不会重复。比如消息发送后,收到确认超时不处理,可能消息并没有到达broker,导致消息丢失。
- At least once:最少一次,消息不会丢失,可能会重复。比如消息发送后,收到确认超时后重发一次,但是实际上消息已经收到了。
- Exactly once:只且一次,消息不丢失不重复,只且消费一次(0.11中实现,仅限于下游也是kafka)。难保证,需要业务系统配合处理
各种投递方式下的场景:
At least once
先获取数据,再进行业务处理,业务处理成功后commit offset。
1、生产者生产消息异常,消息是否成功写入不确定,重做,可能写入重复的消息
2、消费者处理消息,业务处理成功后,更新offset失败,消费者重启的话,会重复消费At most once
先获取数据,commit offset,再进行业务处理。
1、生产者生产消息异常,不管,,消息就丢了
2、消费者处理消息,先更新offset,再做业务处理,做业务处理失败,消费者重启,消息就丢了Exactly once
保证刚好一次,则生产者要保证幂等,消息只有一次发送。
保证刚好一次的实现
- 序号。broker配置enable.idempotence=true,每个生产者为每个topic的每个partition维护一个递增的序号,发送的消息带上这个序号。
broker同样维护producer的最新序号,如果发送的消息的序号不等于最新序号+1,则消息发送失败。
如果序号大于最新序号+1,则有可能是乱序后面的消息先到达或者消息有丢失的情况,如果序号小于最新序号+1,则说明消息已经被保存过了。(类似tcp的有序处理) - 事务。由于可能一个事件处理需要把消息发到多个topic,则可以通过事务保证每个消息发送可以一起成功或者一起失败。如果有上游消息,还得保证offset的提交也在一个事务内。kafka的事务是基于两阶段提交形势,消息有准备提交状态、拒绝状态、提交状态,先把消息都发送后,再都改提交状态。未提交的消息,kafka是通过在消费端进行过滤。
重复消费
避免重复消费
1 手动提交offset,在处理一批消息后再提交offset,或者消费一条手动提交一条。
2 最好业务做到幂等。
3 利用事务
4 发送方0.11后通过配置保证发送方的幂等。
事务
kafka提交事务,用于保证消息发到多个topic、offset的手动提交这些操作同时成功。a->b->c,b在消费的时候,事务可以保证offset的提交和发往c的过程同时成功。
事务提交支持异步提交和同步提交。
https://www.jianshu.com/p/3a25a1aaebc1
分区分配策略
Range
rang策略针对单个主题
进行分配。分区按照序号排序,消费组中的消费者按注册顺序排序。分配的规则是 分区数%消费者数,余数平均给排在前面的消费者。
比如分区数是3个、消费者是2个,那么消费者1处理分区1和分区2,消费者2处理分区3。
缺点:
如果有多个主题,而且分区数都没办法刚好分配均匀,就会导致前面的消费者在每个主题中都分配到较多的分区,主题越多差异越大。比如2个主题,按照上面例子,消费者1最终负责4个分区,消费者2负责2个分区。
Round Robin
roundRobin策略是将消费组所有订阅的主题
的分区进行排序,然后按照轮询的方式将分区分配个每个消费者。如果消费组中的消费者订阅的主题都相同,那么分配则会均匀。如果两个消费者都订阅3个分区的两个主题,则会平均分配到两个消费者上,格子负责3个分区。
如果消费组中消费者订阅的主题不完全相同,则会分配不均
。比如消费者1订阅主题1、主题2,消费2订阅主题1,两个主题都有两个分区,那么最终消费者1负责3个分区,消费者2负责主题1的一个分区。这里可以优化的点是,可以把主题1的都交给消费者2,消费者1只消费主题2,这样就能都负责两个分区
。
缺点
只能保证多主题时,消费组中订阅相同主题的分配均匀。
Sticky
0.11后的版本新引入的粘性策略。这个策略首先保证分配的均匀,其次还会保证分配尽量保持与之前一致
。
保持与之前一致
比如本来是有三个消费者,有一个消费者挂了之后,此时会把挂了的消费者负责的分区平均到另外两个消费者,而不是全部打乱重新排,避免分区充分配。
均匀分配
相对round robin还会考虑当前消费者当前已经负责消费的partition数,解决Round Robin的缺点。
比如消费者1订阅主题1、主题2,消费2订阅主题1,两个主题都有两个分区,会把主题1的都交给消费者2,消费者1只消费主题2,这样就能都负责两个分区
。
批量提交
batch.size
通过这个参数来设置批量提交的数据大小,默认是16k,当积压的消息达到这个值的时候就会统一发送(发往同一分区的消息)
linger.ms
表示消息发送后多久后发到broker。默认大小是0ms,表示立即发送。
两个参数条件满足一个即可,所以默认是send之后直接发到broker
缓存池
kafka通过占用固定的内存块,用于缓存发送缓冲数据。通过缓存池的形势,为每个partition发送分配一个内存块,数据发送后,内存块又回到缓存池。如果缓存池满了则阻塞发送操作。通过这样的方式即避免发送缓存占用过多内存,又避免内存重复申请和失败带来的垃圾回收。
消费端拉取方式
消费端只支持拉取模式,不能由broker主动推送。
基于poll方法拉取,可以指定超时等待时间。
基于业务决定长轮询还是短轮询
kafka用zk做了什么
broker topic partition一些元信息会通过zk存储
Broker 列表管理
leader选举
Partition 与 Broker 的关系
Partition 与 Consumer 的关系
Producer 与 Consumer 负载均衡
消费进度 Offset 记录
消费者注册
自动提交
提交时机
每5秒自动提交一次,或者在下一个拉取消息时,提交当前的offset。
通过两个参数配置
- enable.auto.commit 的默认值是 true;就是默认采用自动提交的机制。
- auto.commit.interval.ms 的默认值是 5000,单位是毫秒。
存在的问题
- 可能offset提交了,当时消息还没处理完,客户端挂了,那么下次启动就丢了部分消息。
- 如果消息处理了一些还没提交offset的时候挂了,那么重新启动又从之前的位置开始消费,会导致重复消费的问题。
所以最好是手动提交来保证消息的可靠
吞吐量
高吞吐量,但是如果一定要保证可靠性,会带来一定的性能损耗,所以鉴于这点,如果要求高可靠性最好还是使用rabbitmq,如果对数据并非那么严谨,但是要吞吐量高,则可以使用kafka。
持久化
以顺序写文件
的方式,避免随机io提供io速度。依赖文件系统缓存,可以配置达到多少条消息后强制写到文件中,或者配置多少秒刷新一次到文件中
.
写文件基于内存映射技术mmap.拿文件基于sendFile从文件内核缓冲区->socket缓存区。
文件按照topic-partition为目录,以一个段为一个文件,每个文件大小超过log.segment.bytes
配置的大小就会新加一个文件。文件分为日志文件和索引文件
,索引文件用于帮助查找偏移量对应的数据位置。
可靠性
发送端:
- 为了防止网络波动导致消息发送丢失,通过kafkaTemplate发送kafka消息可以添加回调,在消息成功发送后,回调通知。
- 设置product重试次数和重试间隔
消费端:
自动确认,拿到消息就确认。消费过程失败了,导致消息丢失。改成手动提交offset,但是可能消费完了要提交offset时挂了。导致重复消费。
服务端:
消息发送到leader后,其他副本区leader拉取数据。如果其他副本还没同步完,leader挂了,消息就丢了。
- 可以通过 acks = all参数,指定发送消息时,消息同步给所有副本后再返回发送成功
- 默认ack=1,发送给leader后就返回成功,副本再慢慢同步。
- 设置 replication.factor >= 3 保证最少有3个副本服务才可用
- 通过min.insync.replicas > 1可以指定同步到几个副本后才返回成功。(leader也算是副本,要设置大于1)。同时也确保
- replication.factor > min.insync.replicas,否则如果有一个副本挂了,就不能写入,违反高可用。
- 设置 unclean.leader.election.enable = false,可以让leader选举不会选择未同步完的副本。
消息清理策略
启用删除策略 log.cleanup.policy=delete
直接删除,删除后的消息不可恢复。可配置以下两个策略:
- 清理超过指定时间清理,
log.retention.hours
- 超过指定大小后,删除旧的消息,
log.retention.bytes
压缩策略
按照key进行聚合,每次只保留每个key最新的数据。会出现日志序号不连续的情况,因为某些序号的数据被清楚。
offset
保存的topic就是这个策略。
flume
flume可以用于消费kafka,同时支持将消息直接转发给es、kafka等等。
消费kafka的机制是会预先拉取一批消息到内存中,程序消费时去内存中拉取,优化了拉取消息这部分操作的时间。
消费的过程是单线程进行的。