1、工作流程

1、生产者生产消息,指定发送到apihello主题中。
2、询问zookeeper,获取leader节点。
3、根据分区规则(文章后面提),将消息发送到指定主题的指定分区的leader节点。
4、broker中主题的follower节点同步leader节点的消息。
5、消费者从broker中拉取数据。

2、文件存储

2.1、分区、副本

以topic为apihello为例,该主题配置为:2分区、2副本。
主题:apihello
分区:apihello-0、apihello-1两个分区
副本:在各节点apihello-0有2个,在各节点apihello-1也有2个

# 可以看到2个分区
[root@node2 kafka-2.2.2] cd datas
[root@node1 datas] 
apihello-0
apihello-1
# node2节点,发现有0分区的副本
[root@node2 datas]
apihello-0
# node3节点,发现有1分区的副本
[root@node3 datas]
apihello-1

2.2、片段

一个主题有多个分区,一个分区也有多个片段,一个片段包含有xxxxx.log、xxxxx.index、xxxxx.timeindex文件,多个片段只是偏移量xxxxx不同。

2.2.1、偏移量

片段的文件名即为偏移量,如0000000000000001123.log,偏移量为1123,即此文件最小的索引位置,也是上个片段的索引最大位置+1。

2.2.2、x.index

该文件固定大小1024kb,固定大小可以增加查找速度,采用二分查找法。

2.2.3、x.log

消息的具体储存文件,物理结构由:offset消息的偏移量、messagesize消息的大小、valueBytesPayload具体的消息值,等属性构成。
该文件大小的最大值由配置文件server.properties中属性log.segment.bytes=1073741824指定,默认为1g,超过该值则会新建片段。

2.2.4、x.timeindex

该文件固定大小1024kb,时间索引,超过配置文件server.properties中log.roll.hours=24 * 7,即使片段没有达到log.segment.bytes的值,也会由片段创建时间开始计算,超过则创建新的片段。

2.3、测试

由javaapi生产者生产数据 100W 条:

public static void main(String[] args) {
    Properties props = new Properties();
    // Kafka服务端的主机名和端口号
    props.put("bootstrap.servers", "localhost:9092");
    // 等待所有副本节点的应答
    props.put("acks", "all");
    // 消息发送最大尝试次数
    props.put("retries", 0);
    // 一批消息处理大小
    props.put("batch.size", 16384);
    // 请求延时
    props.put("linger.ms", 1);
    // 发送缓存区内存大小
    props.put("buffer.memory", 33554432);
    // key序列化
    props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
    // value序列化
    props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

    Producer<String, String> producer = new KafkaProducer<>(props);
    for (int i = 0; i < 1000000; i++) {
        producer.send(new ProducerRecord<String, String>("apihello", Integer.toString(i), "hello world-" + i),
                new Callback() {
            @Override
            public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                if (recordMetadata != null) {
                    System.err.println(recordMetadata.partition() + "---" + recordMetadata.offset());
                }
            }
        });
    }

    producer.close();
}
截取最后几条结果(由于分区,数据会均匀分配到0、1分区上,大约各50w条):
0---525767
0---525768
0---525769
0---525770
0---525771
0---525772
0---525773
0---525774
0---525775
# 查看数据索引文件详情(截取前n条记录)
# 结构为偏移量--位置
[root@node1 kafka-2.2.2] ./bin/kafka-run-class.bat kafka.tools.DumpLogSegments --files ./datas/apihello-0/00000000000000000000.index

Dumping …\datas\apihello-0\00000000000000000000.index
offset: 212 position: 5342
offset: 452 position: 11629
offset: 789 position: 20801
offset: 1059 position: 28358
offset: 1319 position: 35635
offset: 1602 position: 43556
offset: 1947 position: 53213
offset: 2392 position: 65670
offset: 2977 position: 82047
offset: 3562 position: 98424
offset: 4147 position: 114801

# 查看数据文件详情(截取前n条记录)
# 一行为一条message,结构如下....
[root@node1 kafka-2.2.2] ./bin/kafka-run-class.bat kafka.tools.DumpLogSegments --files ./datas/apihello-0/00000000000000000000.log
Starting offset: 0

Dumping …\datas\apihello-0\00000000000000000000.log
Starting offset: 0
baseOffset: 0 lastOffset: 25 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false position: 0 CreateTime: 1582440546347 isvalid: true size: 651 magic: 2 compresscodec: NONE crc: 116942323
baseOffset: 26 lastOffset: 51 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false position: 651 CreateTime: 1582450716789 isvalid: true size: 651 magic: 2 compresscodec: NONE crc: 1800459742
baseOffset: 52 lastOffset: 77 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false position: 1302 CreateTime: 1582450750958 isvalid: true size: 651 magic: 2 compresscodec: NONE crc: 1148911332
baseOffset: 78 lastOffset: 120 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false position: 1953 CreateTime: 1582450893840 isvalid: true size: 1042 magic: 2 compresscodec: NONE crc: 3224531442
baseOffset: 121 lastOffset: 211 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false position: 2995 CreateTime: 1582450893853 isvalid: true size: 2347 magic: 2 compresscodec: NONE crc: 2980159712
baseOffset: 212 lastOffset: 219 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false position: 5342 CreateTime: 1582450893854 isvalid: true size: 261 magic: 2 compresscodec: NONE crc: 1887267692
baseOffset: 220 lastOffset: 306 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false position: 5603 CreateTime: 1582450893864 isvalid: true size: 2259 magic: 2 compresscodec: NONE crc: 3447076745
baseOffset: 307 lastOffset: 451 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false position: 7862 CreateTime: 1582450893875 isvalid: true size: 3767 magic: 2 compresscodec: NONE crc: 2457973658
baseOffset: 452 lastOffset: 600 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false position: 11629 CreateTime: 1582450893883 isvalid: true size: 3911 magic: 2 compresscodec: NONE crc: 1647948968
baseOffset: 601 lastOffset: 788 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false position: 15540 CreateTime: 1582450893896 isvalid: true size: 5261 magic: 2 compresscodec: NONE crc: 1406775756
baseOffset: 789 lastOffset: 1058 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false position: 20801 CreateTime: 1582450893908 isvalid: true size: 7557 magic: 2 compresscodec: NONE crc: 207466547
baseOffset: 1059 lastOffset: 1318 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false position: 28358 CreateTime: 1582450893918 isvalid: true size: 7277 magic: 2 compresscodec: NONE crc: 860591728
baseOffset: 1319 lastOffset: 1601 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false position: 35635 CreateTime: 1582450893933 isvalid: true size: 7921 magic: 2 compresscodec: NONE crc: 1307927364

结果:当消费者指定offset=1319时候,采用二分查找扫描x.index文件,取得offset: 1319 position: 35635,再去x.log文件中找到position=35635的message。同时,可以看到首行Starting offset: 0说明当前的分段为首段00000000000000000000.log

2.4、message的物理结构

以该message为例:
baseOffset: 1319 该数据的起始偏移量
lastOffset: 1601 该数据的最后偏移量 与上面的相减即可得到该数据的偏移范围
baseSequence: -1
lastSequence: -1
producerId: -1
producerEpoch: -1
partitionLeaderEpoch: 0
isTransactional: false
position: 35635
CreateTime: 1582450893933
isvalid: true
size: 7921
magic: 2
compresscodec: NONE
crc: 1307927364

3、zookeeper储存结构

kafka 示例 kafka实战_java

# 进入zk客户端(使用kafka之前)
[zk: localhost:2181(CONNECTED) 0] ls /
[zookeeper]
# 进入zk客户端(使用kafka之后)
# 自行查看各节点的值ls / 或 get /
[zk: localhost:2181(CONNECTED) 0] ls /
[cluster, controller_epoch, controller, brokers, zookeeper, admin, isr_change_notification, consumers, latest_producer_id_block, config]

4、生产者

生产过程是采用push(推送)的模式到broker的,每条消息会被append(追加)到分区中。每条消息都会有一个唯一的offset值。
那么一条消息生产后,是如何决定保存在哪个分区中的呢?
原理:
1、指定分区:则该消息保存到自己指定的分区。
2、指定key:通过key的hash,再取模,则可以得到分区值。例如:"1234567"的hashcode为:2018166324,对2取模的绝对值,得到0,即保存在0号分区。
3、分区和key均未指定:轮询分区(即循环)保存。

生产流程:
1)producer先从zookeeper的 "/brokers/…/state"节点找到该partition的leader
2)producer将消息发送给该leader
3)leader将消息写入本地log
4)followers从leader pull消息,写入本地log后向leader发送ACK
5)leader收到所有ISR中的replication的ACK后,增加HW(high watermark,最后commit 的offset)并向producer发送ACK

5、消费者

消费过程是采用pull(拉取)模式从broker读取数据(此模式可能造成消费者一直在循环拉取数据,即使没有数据)
当消费者是以consumer group消费者组的方式工作时,由一个或者多个消费者组成一个组,共同消费一个topic。每个分区在同一时间只能由group中的一个消费者读取,但是多个group可以同时消费这个partition。