kafka 初学笔记

1. 概念分析

    broker: 消息处理结点, 1个broker就是一个kafka服务进程或者服务器,多个broker组成kafka集群。

    topic: 消息主题,所有的消息按照主题归类存储,每个topic会在broker上生成一个文件夹。
    partition: topic的物理分组,一个topic可以设置多个partition。partition数量会被各个consumer瓜分掉,consumer只能获取到跟它关联的partition中的消息,所以如果consumer数量超过partition数量,多出的consumer无法读到任何数据。例如,创建一个topic "haha",会在logs下面多出3个文件夹:haha-0, haha-1, haha-2。很明显,文件夹的名称格式是:topic-particion序号。但有一个问题大家都会问,producer往生产一个topic的消息,这条消息应该放到哪个partition中呢?key,消息key的作用就在此。根据key做哈希,最后根据哈希结果决定这条消息放哪个partition。假如说没有设置key,那就轮询分区决定。

    replication:分区可以有多个replication,这是broker级别的对partition做的replication。假如replication为3,那么每个partition都应该分别存在与3个broker中。但不一定每个broker都拥有所有的partition。

    segment: 为了快速查找到消息,系统将一个partition的数据分成多个segment存储。每个segment有大小限制,超过限制,系统会新建一个segment。系统接收到provider传
传来的消息时,会先将消息保存在内存的一个数据结构中,有另外一个线程,每隔一段时间(1秒,可配置)将内存中的消息flush到磁盘。只有磁盘中的数据,才能被consumer访问。

    offset: 一个连续的用于定位被追加到分区的每一个消息的序列号,最大值为64位的long大小,19位数字字符长度。这个offset说白了是某个consumer读取某个partition的最新位置,这个consumer必须是与那个partition关联的。consumer不可能一直在线,有可能挂掉,那么重启的时候consumer难道要从头开始读取消息?所以kafka干脆把consumer的读取进度,也就是offset干脆保存在server端。至于保存方式,可以保存在zookeeper中,我们可以通过在zk命令行中执行:get /consumers/jd-group/offsets/haha/0 来查看group ID为jd-group,topic为haha,partition为0的segment。为什么是group?因为kafka将consumer按group管理,一个group访问一个partition。至于同一个group的各个consumer,只能相互竞争了。但是,用zookeeper来记录offset,性能很容易到达瓶颈,因为zk并不适合大批量频繁的写操作。新版Kafka已推荐将consumer的位移信息保存在Kafka内部的topic中,即__consumer_offsets topic,并且默认提供了kafka_consumer_groups.sh脚本供用户查看consumer信息。

    其实,这几个概念(尤其是segment)关乎到kafka的核心——存储。kafka是基于磁盘存储的,而不是内存。我一开始也很纳闷,磁盘读写效率不是很低吗,kafka如何做到高吞吐量的?其实磁盘的顺序读写效率,是和内存的随机读写效率是同一个级别的。而且,用磁盘存储数据,可以持久化。
    
    segment是如何让kafka有高吞吐量的呢?
    
    首先是文件命名,它关系到如何定位到哪个segment的问题,segment利用了消息的offset,每一个segment的文件名是前一个segment中最后一个消息的offset。所以第1个segment的文件名是00000000000000000000,如果这个segment中存放了856个消息,那么下一个segment的文件名就是00000000000000000856,依次类推。所以,要读取某个offset时,只要对segment的文件列表做二分查找,很快就能找到消息所在的对应segment中。
    其次,定位了segment之后,如何又在segment中快速找到消息呢?setment的文件不是一个,其实是4个:log, index, timeindex, snapshot。主要是log 和 index。log存放消息的数据,index存放这些数据的索引,索引格式是:<offset, position>,即每个offset对应的消息,在log文件中的具体位置。所以,要在segment中获取到某个offset的消息数据,首先需要遍历index文件,查找到对应的offset的那条索引,然后找到对应的position,然后再读取log文件position位置的数据,就可以找到了。因为index文件很小,所以遍历index文件不会有太大的性能影响。
    
    下面是一个broker下的log文件目录:
    
        
    ${log.dir}
        |__ __consumer_offsets-0
        |  |__ 00000000000000000000.index
        |  |__ 00000000000000000000.log
        |  |__ 00000000000000000000.timeindex
        |  |__ leader_epoch_checkpoint
        |__ __consumer_offsets-1
        
        ......
        
        |__ __consumer_offsets-49
        
        |__ haha-0
           |__ 00000000000000000000.index
           |__ 00000000000000000000.log
           |__ 00000000000000000000.timeindex
           |__ 00000000000000034567.index
           |__ 00000000000000034567.log
           |__ 00000000000000034567.timeindex
           |__ 00000000000000078901.index
           |__ 00000000000000078901.log
           |__ 00000000000000078901.timeindex
           
           ...
           
        |__ haha-1
           |__ 00000000000000000000.index
           |__ 00000000000000000000.log
           |__ 00000000000000000000.timeindex
        |__ haha-2
           |__ 00000000000000000000.index
           |__ 00000000000000000000.log
           |__ 00000000000000000000.timeindex
           
    __consumer_offsets是kafka默认的topic,默认有50个partition,但都在同一个kafka服务器上,它用来替代使用zookeeper记录offset。
    
    一个简单的概念分析到此结束,通过将这几个概念彻底理解透,对于深入探索kafka至关重要。

2. Consumer 的 enable.auto.commit 属性问题

    首先要理解consumer的offset,它代表了每个consumer group当前读取到的数据位置。原因是kafka的只支持顺序读取数据,所以每个group读取到哪里了,需要offset记录这个位置。
    因此,kafka服务器会为每个topic-group维护一个offset。如果enable.auto.commit=true, consumer会起一个线程,自动向kafka报告最新的offset。反之,需要使用者手动commit。
    看到这里,包括我,绝大多数人都懂得下面这几行代码的意义:
    ConsumerRecords<?, ?> records = consumer.poll(1000);
    do something ...
    consumer.commitAsync();
    
    很多人包括我一开始都会这样想,假如没有调用commitAsync()函数,那么每次poll都能从头开发拿到数据,但事实上不是这样的。
    consumer poll时并不会使用kafka的offset,而是使用自身的offset。记住了,consumer客户端本身就有一个offset,每次poll都会自动向前增加,除非没有数据。而consumer每次poll时使用的是自身的offset。所以,调用了commitAsync()函数,只是更新了kafka服务器的offset,并没有更新consumer本地的offset。
    那么,kafka服务器的offset有什么作用???很明显,是供consumer启动的时候用,因为consumer的offset是内存数据,重启会丢失。因此consumer启动时会读取这个服务器的offset来初始化本地offset。反过来看commitAsync()函数的源代码,它也是读取本地的offset,然后更新到kafka服务器。
    但是问题又来来,既然每个consumer都保存并使用offset,那么如何做到组管理:一条数据只能让一个group的其中某一个consumer读到。暂时还不清楚,这点需要继续研究。
    
    回到最开始的问题,假如没有commit,又想重复消费,应该怎么办?答案是seek。seek函数是consumer维护本地offset使用的。
    我们有时候会遇到这样的场景:拿到50条数据,但处理了20条就出错了,于是想commit()20条,下次从第21条再获取数据。按照上面的逻辑,就可以这样操作:
    int offset = consumer.position(partition);    //获取当前的offset
    consumer.seek(partition, offset - (50 - 20));        //回退30条
    consumer.commitAsync();

    以上逻辑大部分是通过实验和看源码得出的结论,未必完全准确,但是基本上与实验结果对得上。

3. auto.offset.reset的作用
earliest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费 
latest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据 
none:topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常

这里有一个疑问,何时分区下会有没有提交过offset?大多数人都会想到:刚创建topic并且有数据但没有被消费过。

我们常常漏了这种情况,topic已经被反复消费过,但是某天启动了一个新的group.id的consumer,这个时候对这个consumer来说,它就没有提交过offset。这个时候,对它来说,它应该从哪个位置开始读取数据?auto.offset.reset的作用这时就可以发挥出来了。earliest时offset会被置为0,latest时offset会被置为consumer启动后的第一条数据的offset。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值