Flume主要是做日志数据(离线或实时)的采集。
下图显示的是flume采集完毕数据之后,进行的离线处理和实时处理两条业务线,本文将介绍flume和kafka的整合处理。
# kafka-topics.sh --create \
--topic flume-kafka \
--zookeeper bigdata01:2181/kafka \
--partitions 3 \
--replication-factor 3
Created topic "flume-kafka".
flume-kafka-sink.conf
##a1就是flume agent的名称
a1.sources = r1
a1.sinks = k1
a1.channels = c1
# Describe/configure the source
a1.sources.r1.type = netcat
a1.sources.r1.bind = bigdata01
a1.sources.r1.port = 44444
# 修改sink为kafka
a1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSink
a1.sinks.k1.kafka.bootstrap.servers = bigdata01:9092,bigdata02:9092,bigdata03:9092
a1.sinks.k1.kafka.topic = flume-kafka
a1.sinks.k1.kafka.producer.acks = 1
a1.sinks.k1.kafka.producer.linger.ms = 1
# Use a channel which buffers events in memory
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100
# Bind the source and sink to the channel
a1.sources.r1.channels = c1
a1.sinks.k1.channel = c1
# kafka-console-consumer.sh --topic flume-kafka \
--bootstrap-server bigdata01:9092 \
--from-beginning
# nohup bin/flume-ng agent -n a1 -c conf -f conf/flume-kafka-sink.conf >/dev/null 2>&1 &
# telnet bigdata01 44444
Trying 192.168.10.101...
Connected to bigdata01.
Escape character is '^]'.
Hello Oak
OK
Good Good Study Day Day Up!
OK
# kafka-console-consumer.sh --topic flume-kafka --bootstrap-server bigdata01:9092 --from-beginning
Hello Oak
Good Good Study Day Day Up!
replica:
每⼀个分区,根据副本因子N,会有N个副本。比如在broker1上有一个topic,分区为topic-1, 副本因子为2,那么在两个broker的数据目录里,都会有⼀个topic-1,其中⼀个是leader,⼀个follower。
Segment:
partition 物理上由多个 segment 组成,每个 Segment 存着 message 信息。
Leader:
每个partition有多个副本,其中有且仅有一个作为Leader,Leader是当前负责数据的读写的partition。
Follower:
Follower跟随Leader,所有写请求都通过Leader路由,数据变更会广播给所有Follower,Follower与Leader保持数据同步。如果Leader失效,则从Follower中选举出⼀个新的Leader。当Follower与Leader挂掉、卡住或者同步太慢,leader会把这个follower从“in sync replicas”(ISR)列表中删除,重新创建一个Follower。
Offset
kafka的存储文件都是按照offset.log来命名,用offset做名字的好处是方便查找。例如你想找位于2049的位置, 只要找到2048.log的文件即可。当然the first offset就是00000000000.log
通常,一个典型的Kafka集群中包含若干Producer(可以是web前端产⽣的Page View,或者是服务器日志,系统CPU、Memory等),若干broker(Kafka支持水平扩展,一般broker数量越多,集群吞吐率越高),若干 Consumer Group,以及⼀个Zookeeper集群。Kafka通过Zookeeper管理集群配置,选举leader,以及在 Consumer Group发生变化时进行rebalance。Producer使用push模式将消息发布到broker,Consumer使用pull 模式从broker订阅并消费消息。
Kafka分布式主要是指分区被分布在多台server(broker)上,同时每个分区都有leader和follower(不是必须),即老大和小弟的角色,老大负责处理,小弟负责同步,小弟也可以变成老大,形成分布式模型。
kafka的分区日志(message)被分布在kafka集群的服务器上,每⼀个服务器处理数据和共享分区请求。每⼀个分区是被复制到⼀系列配置好的服务器上来进行容错。
每个分区有⼀个server节点来作为分区leader和零个或者多个server节点来作为分区followers。分区leader处理指定分区的所有读写请求,同时分区follower被动复制分区leader。如果leader失败,follwers中的⼀个将会自动地变成⼀个新的leader。每⼀个服务器都能作为分区的⼀个leader和作为其它分区的follower,因此kafka集群能被很好地平衡。kafka集群是一个去中心化的集群。
以上信息参考官网:http://kafka.apache.org/intro.html#intro_distribution
kafka消费的并行度就是kafka topic分区的个数,或者说分区的个数决定了同一时间同一消费者组内最多可以有多少个消费者消费数据。
分区的目的
可以想象,如果⼀个topic就⼀个分区,要是这个分区有1T数据,那么kafka就想把大文件划分到更多的目录来管理,这就是kafka所谓的分区。
分区的好处
单节点partition的存储分布
Kafka集群只有⼀个broker,默认/var/log/kafka-log为数据文件存储根目录,在Kafka broker中 server.properties文件配置(参数log.dirs=/opt/data/kafka),例如创建2个topic名称分别为test-1、test-2, partitions数量都为partitions=4
存储路径和目录规则为:
|--test-1-0
|--test-1-1
|--test-1-2
|--test-1-3
|--test-2-0
|--test-2-1
|--test-2-2
|--test-2-3
在Kafka文件存储中,同⼀个topic下有多个不同partition,每个partition为⼀个⽬录,partiton命名规则为:topic 名称+分区编号(有序),第⼀个partiton序号从0开始,序号最⼤值为partitions数量减1。
多节点partition存储分布
分区策略举例
test3的topic,4个分区,2个副本。
# kafka-topics.sh --describe --zookeeper bigdata01:2181/kafka --topic test3
Topic:test3 PartitionCount:4 ReplicationFactor:2 Configs:
Topic: test3 Partition: 0 Leader: 1 Replicas: 1,3 Isr: 1,3
Topic: test3 Partition: 1 Leader: 2 Replicas: 2,1 Isr: 1,2
Topic: test3 Partition: 2 Leader: 3 Replicas: 3,2 Isr: 2,3
Topic: test3 Partition: 3 Leader: 1 Replicas: 1,2 Isr: 1,2
第1个Partition分配到第(1 mode 3)= 1个broker上
第2个Partition分配到第(2 mode 3)= 2个broker上
第3个Partition分配到第(3 mode 3)= 3个broker上
第4个Partition分配到第(4 mode 3)= 1个broker上
副本分配算法:
分区及副本分配举例
# kafka-server-start.sh config/server.properties #0 0
# kafka-server-start.sh config/server.1.properties #10 1
# kafka-server-start.sh config/server.2.properties #20 2
zookeeper-shell.sh localhost:2181
ls /brokers/ids
[0, 20, 10]
# kafka-topics.sh --bootstrap-server hadoop00:9093 --create --topic test1 --partitions 3 --replication-factor 2
# kafka-topics.sh --bootstrap-server hadoop00:9093 --describe --topic test1
Topic:test1 PartitionCount:3 ReplicationFactor:2 Configs:segment.bytes=1073741824
Topic: test1 Partition: 0 Leader: 20 Replicas: 20,10 Isr: 20,10
Topic: test1 Partition: 1 Leader: 0 Replicas: 0,20 Isr: 0,20
Topic: test1 Partition: 2 Leader: 10 Replicas: 10,0 Isr: 10,0
分区0 第1个副本应该位于 0+1%3=1 =>broker[1]= 10
分区0 第2个副本应该位于 0+2%3=2 =>broker[2]= 20
分区1 第1个副本应该位于 1+1%3=2 =>broker[2]= 20
分区1 第2个副本应该位于 1+2%3=0 =>broker[0]= 0
下图是⼀个partition-0的存储示意图
Kafka文件系统是以partition方式存储,下面深入分析partitiion中segment file组成和物理结构。
通过上面两张图,我们已经知道topic、partition、segment、.log、.index等文件的关系,下⾯深⼊介绍segment 相关组成原理。
segment file组成:
由2大部分组成,分别为index file和log file(即数据文件),这2个⽂件⼀⼀对应,成对出现,后缀".index"和“.log” 分别表示为segment索引文件、数据文件。
segment⽂件命名规则:
partition全局的第⼀个segment从0开始,后续每个segment⽂件名为上⼀个segment⽂件最后⼀条消息的 offset值。数值最大为64位long大小,20位数字字符长度,不够的左边用0填充。
验证:
创建⼀个topic为test5包含1 partition,设置每个segment⼤⼩为1G,并启动producer向Kafka broker写入大量数据。
# bin/kafka-topics.sh --create --zookeeper bigdata01:2181/kafka --replication-factor 1 --partitions 1 --topic test5
Created topic "test5".
# ./bin/kafka-console-producer.sh --broker-list bigdata01:9092 --topic test5
>nihao beijing
>nihao qianfeng
>124
>123456789
>098765
>999
>laowang
......
查看segment文件列表:
# ll /opt/data/kafka/test5-0/ #查看分区⽬录
total 8
-rw-r--r--. 1 root root 10485760 Nov 21 10:50 00000000000000000000.index #segment⽂件索引⽂件
-rw-r--r--. 1 root root 1073761826 Nov 21 10:53 00000000000000000000.log #segment的log⽂件
-rw-r--r--. 1 root root 10485760 Nov 21 10:50 00000000000000023060.index
-rw-r--r--. 1 root root 892 Nov 21 10:53 00000000000000023060.log
-rw-r--r--. 1 root root 10485756 Nov 21 10:50 00000000000000003268.timeindex
-rw-r--r--. 1 root root 8 Nov 21 10:52 leader-epoch-checkpoint
查看segment文件内容
1. 查看.log
# usr/local/kafka/bin/kafka-run-class.sh kafka.tools.DumpLogSegments \
--files 00000000000000000000.log --print-data-log
效果:
Starting offset: 0
offset: 0 position: 0 CreateTime: 1577994283622 isvalid: true keysize: -1 valuesize: 1 magic: 2
compresscodec: NONE producerId: -1 producerEpoch: -1 sequence: -1 isTransactional: false
headerKeys: [] payload: a
offset: 1 position: 69 CreateTime: 1577994466159 isvalid: true keysize: -1 valuesize: 1 magic: 2
compresscodec: NONE producerId: -1 producerEpoch: -1 sequence: -1 isTransactional: false
headerKeys: [] payload: 1
offset: 2 position: 138 CreateTime: 1577994474463 isvalid: true keysize: -1 valuesize: 1 magic: 2
compresscodec: NONE producerId: -1 producerEpoch: -1 sequence: -1 isTransactional: false
headerKeys: [] payload: 4
2. 查看.index
# /usr/local/kafka/bin/kafka-run-class.sh kafka.tools.DumpLogSegments \
--files 00000000000000000000.index --print-data-log
Dumping 00000000000000000000.index
offset: 0 position: 0
segment⽂件的物理结构:
如上图所示,索引文件存储大量元数据,数据文件存储大量消息,索引文件中元数据指向对应数据文件中message的物理偏移地址。
举例
上述索引⽂件中元数据6-->266为例,依次在数据⽂件中表示第6个message(在全局partiton表示第20366个 message)、以及该消息的物理偏移地址为266。
message物理结构
⼀个segment data file由许多message组成,⼀个message物理结构具体如下:
具体参数详解:
关键字 | 解释说明 |
8 byte offset | 在parition(分区)内的每条消息都有⼀个有序的id号,这个id号被称为偏移(offset),它可以唯⼀ 确定每条消息在parition(分区)内的位置。即offset表示partiion的第多少message |
4 byte message size | message大小 |
4 byte CRC32 | 用crc32校验message |
1 byte “magic" | 表示本次发布Kafka服务程序协议版本号 |
1 byte “attributes" | 表示为独⽴版本、或标识压缩类型、或编码类型 |
4 byte key length | 表示key的⻓度,当key为-1时,K byte key字段不填 |
K byte key | 可选 |
value bytes payload | 表示实际消息数据 |
举例
查找offset=23066的message,需要通过如下2个步骤查找:
第一步 查找segment file
00000000000000000000.index
00000000000000000000.log
00000000000000023060.index
00000000000000023060.log
根据.index和.log物理结构对应关系图可知,其中00000000000000000000.index表示最开始的⽂件,起始偏移 量(offset)为0.第⼆个⽂件00000000000000023060.index的消息量起始偏移量为23060 = 23059 + 1.同样,其他后 续⽂件依次类推,以起始偏移量命名并排序这些⽂件,只要根据offset ⼆分查找⽂件列表,就可以快速定位到具体文件。
当offset=23066时定位到0000000000000023060.index和log⽂件。
第⼆步 通过segment file查找message
通过第⼀步定位到segment file,当offset=23066时,依次定位到0000000000000023060.index的元数据物理 位置和 0000000000000023060.log的物理偏移地址,然后再通过0000000000000023060.log顺序查找直到 offset=23066为⽌。
segment index file采取稀疏索引存储方式,即<偏移量、位置>,它减少索引文件大小,通过map可以直接内存 操作,稀疏索引为数据⽂件的每个对应message设置⼀个元数据指针,它⽐稠密索引节省了更多的存储空间,但查找起来需要消耗更多的时间。
consumer group是kafka提供的可扩展且具有容错性的消费者机制。既然是⼀个组,那么组内必然可以有多个消费 者或消费者实例(consumer instance),它们共享⼀个公共的ID,即group ID。组内的所有消费者协调在⼀起来消 费订阅主题(subscribed topics)的所有分区(partition)。当然,每个分区只能由同⼀个消费组内的⼀个consumer来 消费。理解consumer group记住下⾯这三个特性就好了:
由于consumer在消费过程中可能会出现断电宕机等故障,consumer恢复后,需要从故障前的位置的继续消 费,所以consumer需要实时记录⾃⼰消费到了哪个offset,以便故障恢复后继续消费。
Kafka默认是定期帮你⾃动提交位移的(enable.auto.commit = true),你当然可以选择⼿动提交位移实现⾃⼰控 制。另外kafka会定期把group消费情况保存起来,做成⼀个offset map,如下图所示:
上图表示,有⼀个test-group的消费者组,该组消费两个分区分别为topicA-0和topicA-1,对topicA-0分区消费到 offset为8这个位置,⽽对topicA-1分区消费到offset为6这个位置。
Kafka 0.9版本之前,consumer默认将offset保存在Zookeeper中,zk中的目录结构是:/consumers/[group.id] (http://group.id/)/offsets//。但是zookeeper其实并不适合进行大批量的读写操作,尤其是写操作。 因此从0.9版 本开始,consumer默认将offset保存在Kafka⼀个内置的topic中,该topic为__consumer_offsets。该topic的格式大概如下:
group.id:分组id,唯⼀。
high level 和low level
- 将zookeeper维护offset 的⽅式称为 low level API
- 将kafka broker 维护offset的⽅式称为high level API
使⽤high level API 更新offset具体设置
可以在consumer的代码中设置这个属性
- 自动提交,设置enable.auto.commit=true,更新的频率根据参数【auto.commit.interval.ms】来定。这种方式也被称为【at most once】,fetch到消息后就可以更新offset,无论是否消费成功。默认就是true。
- 手动提交,设置enable.auto.commit=false,这种⽅式称为【at least once】。fetch到消息后,等消费完成再调用方法【consumer.commitSync()】,手动更新offset;如果消费失败,则offset也不会更新,此条消息会被重复消费⼀次。
为保证producer发送的数据,能可靠的发送到指定的topic,topic的每个partition收到producer发送的数据后, 都需要向producer发送ack(acknowledgement确认收到),如果producer收到ack,就会进⾏下⼀轮的发送,否则重新发送数据。
1)副本数据同步策略
⽅案 | 优点 | 缺点 |
半数以上完成同步,就发送ack | 延迟低 | 选举新的leader时,容忍n台节点的故障,需要2n+1个副本 |
全部完成同步,才发送ack | 选举新的leader时,容忍n台节点的故障,需要n+1个副本 | 延迟⾼ |
Kafka选择了第⼆种⽅案,原因如下:
2)ISR
采⽤第⼆种⽅案之后,设想以下情景:leader收到数据,所有follower都开始同步数据,但有⼀个follower,因为某种故障,迟迟不能与leader进⾏同步,那leader就要⼀直等下去,直到它完成同步,才能发送ack。这个问题怎 么解决呢?
Leader维护了⼀个动态的in-sync replica set (ISR),意为和leader保持同步的follower集合。当ISR中的follower完 成数据的同步之后,leader就会给follower发送ack。如果follower⻓时间未向leader同步数据,则该follower将被 踢出ISR,该时间阈值由replica.lag.time.max.ms参数设定。Leader发⽣故障之后,就会从ISR中选举新的 leader。
注:
对于某些不太重要的数据,对数据的可靠性要求不是很⾼,能够容忍数据的少量丢失,所以没必要等ISR中的 follower全部接收成功。
所以Kafka为⽤户提供了三种可靠性级别,⽤户根据对可靠性和延迟的要求进⾏权衡,选择以下的配置。
故障处理细节
(1) follower故障
follower发⽣故障后会被临时踢出ISR,待该follower恢复后,follower会读取本地磁盘记录的上次的HW,并将 log⽂件⾼于HW的部分截取掉,从HW开始向leader进⾏同步。等该follower的LEO⼤于等于该Partition的HW, 即follower追上leader之后,就可以重新加⼊ISR了。
(2) leader故障
leader发⽣故障之后,会从ISR中选出⼀个新的leader,之后,为保证多个副本之间的数据⼀致性,其余的 follower会先将各⾃的log⽂件⾼于HW的部分截掉,然后从新的leader同步数据。
注意:这只能保证副本之间的数据⼀致性,并不能保证数据不丢失或者不重复。
对于某些⽐较重要的消息,我们需要保证exactly once(⼀次正好)语义,即保证每条消息被发送且仅被发送⼀次。
在0.11版本之后,Kafka引入了幂等性机制(idempotent),配合acks = -1时的at least once(最少⼀次)语义, 实现了producer到broker的exactly once语义。
*idempotent + at least once = exactly once*
使⽤时,只需将enable.idempotence属性设置为true(在⽣产者的位置),kafka⾃动将acks属性设为-1。
ps:幂等性机制是什么意思,幂等简单说1的⼏次幂都等于1,也就是说⼀条消息⽆论发⼏次都只算⼀次,⽆论多少条消 息但只实例化⼀次
kafka完成幂等性其实就是给消息添加了唯⼀ID, 这个ID的组成是PID(ProducerID)这样保证每⼀个Producer发送 的时候是唯⼀的,还会为Producer中每条消息添加⼀个消息ID,也就是说当前Producer中⽣产的消息会加⼊ Producer的ID和消息ID这样就能保证消息唯⼀了,这个消息发送到Kafka中的时候回暂时缓存ID,写⼊数据后没有收 到ack,那么会从新发送这个消息,新消息过来的时候会和缓存中ID进⾏⽐较如果发现已经存在就不会再次接受了
详细解析:
为了实现Producer的幂等性,Kafka引入了Producer ID(即PID)和Sequence Number。
PID。每个新的Producer在初始化的时候会被分配⼀个唯⼀的PID,这个PID对⽤户是不可⻅的。
Sequence Numbler。(对于每个PID,该Producer发送数据的每个都对应⼀个从0开始单调 递增的Sequence Number
Kafka可能存在多个⽣产者,会同时产⽣消息,但对Kafka来说,只需要保证每个⽣产者内部的消息幂等就可以了,所以引入了PID来标识不同的⽣产者。
对于Kafka来说,要解决的是⽣产者发送消息的幂等问题。也即需要区分每条消息是否重复。 Kafka通过为每条消息增加⼀个Sequence Numbler,通过Sequence Numbler来区分每条消息。每条消息对应⼀个分区,不同的分区产⽣的消息不可能重复。所有Sequence Numbler对应每个分区
Broker端在缓存中保存了这seq number,对于接收的每条消息,如果其序号⽐Broker缓存中序号⼤1则接受 它,否则将其丢弃。这样就可以实现了消息重复提交了。但是,只能保证单个Producer对于同⼀个的Exactly Once语义。不能保证同⼀个Producer⼀个topic不同的partion幂等。
Kafka集群中有⼀个broker会被选举为Controller,负责管理集群broker的上下线,所有topic的分区副本分配和 leader选举等⼯作。
Controller的管理⼯作都是依赖于Zookeeper的。
只有KafkaController Leader会向zookeeper上注册Watcher,其他broker⼏乎不⽤监听zookeeper的状态变化。
Kafka集群中多个broker,有⼀个会被选举为controller leader(谁先到就是谁),负责管理整个集群中分区和副本 的状态,⽐如partition的leader 副本故障,由controller 负责为该partition重新选举新的leader 副本;当检测到 ISR列表发⽣变化,有controller通知集群中所有broker更新其MetadataCache信息;或者增加某个topic分区的时 候也会由controller管理分区的重新分配⼯作
当broker启动的时候,都会创建KafkaController对象,但是集群中只能有⼀个leader对外提供服务,这些每个 节点上的KafkaController会在指定的zookeeper路径下创建临时节点,只有第⼀个成功创建的节点的 KafkaController才可以成为leader,其余的都是follower。当leader故障后,所有的follower会收到通知,再次竞 争在该路径下创建节点从⽽选举新的leader。
日志允许序列附加,总是附加到最后⼀个⽂件。当该⽂件达到可配置的⼤⼩(⽐如1GB)时,就会将其刷新到⼀个新⽂件。⽇志采⽤两个配置参数:M和S,前者给出在强制OS将⽂件刷新到磁盘之前要写⼊的消息数量(条数),后者给出多少秒之后被强制刷新。这提供了⼀个持久性保证,在系统崩溃的情况下最多丢失M条消息或S秒的数据。
1、读取的实际过程是:⾸先根据offset去定位数据⽂件中的log segment⽂件,然后从全局的offset值中计算指定⽂件offset,然后从指定⽂件offset读取消息。查找使⽤的是⼆分查找(基于快排队segment⽂件名进⾏排序),每⼀个⽂件的范围都被维护到内存中。
2、读取是通过提供消息的64位逻辑偏移量(8字节的offset)和s字节的最⼤块⼤⼩来完成。
3、读取将返回⼀个迭代器包含有s字节的缓冲区,缓冲区中含有消息。S字节应该⽐任何单个消息都⼤,但是在出现异常⼤的消息时,可以多次重试读取,每次都将缓冲区⼤⼩加倍,直到成功读取消息为⽌。
4、可以指定最⼤的消息(message.max.bytes)和缓冲区大小,以使服务器拒绝的消息大于某个大小,并为客户机提供其获得完整消息所需的最⼤读取量。
注意: 必须处理两种类型的损坏:中断(由于崩溃⽽丢失未写的块)和损坏(向⽂件添加⽆意义块)。