Kafka概述
什么是Kafka?
Apache Kafka是一个分布式发布订阅消息系统/队列,可以处理大量的数据,并使您能够将消息从一个端点传递到另一个端点。
Kafka适合离线、在线的消息消费。 Kafka的消息保留在磁盘上,并在集群内复制以防止数据丢失。 Kafka构建在ZooKeeper同步服务之上。 它可以与Apache Storm、Apache Spark非常好地集成,并用于实时流式数据处理。
支持的客户端语言:
C/C++
Python
Go (AKA golang)
.NET
Node.js
Perl
PHP
Rust
Alternative Java
Scala DSL
Swift
消息队列是什么?
Kafka是一个消息队列,那么消息队列是什么呢?
消息队列:一般我们会简称它为MQ(Message Queue)。
消息(Message):传输的数据。
队列(Queue):队列是一种先进先出FIFO的数据结构。
消息队列从字面的含义来看就是一个存放消息的容器。
消息队列可以简单理解为:把要传输的数据放在队列中。
生产者:把数据放到消息队列。
消费者:从消息队列里边取数据。
消息队列的作用
解耦
允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。
异步通信
很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。
灵活性 & 峰值处理能力
在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见。如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃,俗称削峰/限流。
四种主流限流算法:固定窗口算法、滑动窗口算法、令牌桶算法、漏桶算法。
消息队列分类 – 点对点模式
点对点模式:一对一,消费者主动拉取数据,消息收到后消息清除。
工作模式:消息生产者生产消息发送到 queue 中,然后消息消费者从 queue 中取出并且消费消息。消息被消费以后,queue 中不再有存储,所以消息消费者不可能消费到已经被消费的消息。
点对点模式支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。
消息队列分类 – 发布/订阅模式
发布/订阅模式:一对多,消费者消费数据之后不会清除消息。
工作模式:消息生产者(发布)将消息发布到 topic主题 中,同时有多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到 topic 的消息会被所有订阅者消费。
细分消费种类:
推送模式 - 由消息队列推送到消费者。
拉取模式 - 由消费者来消息队列拉取数据(Kafka的消息消费是由Consumer做轮询,不断的poll请求检查消息队列来拉取数据)。
Kafka入门
基本测试、运行环境搭建
zookeeper环境搭建
$ docker pull zookeeper:3.5.5
$ docker run --name zoo --publish 2181:2181 --restart always -d zookeeper:3.5.5
kafka环境搭建
- 下载我们的kafka使用版本 kafka_2.11-2.3.0.tgz 并解压缩
- 修改配置文件并作出如下修改
$ vi config/server.properties
broker.id=1
listeners=PLAINTEXT://:9092
advertised.listeners=plaintext://192.168.33.10:9092
zookeeper.connect=192.168.33.10:2181
- 启动
$ ./bin/kafka-server-start.sh ./config/server.properties
常用命令
查看当前服务器中的所有 topic
$ ./bin/kafka-topics.sh --zookeeper 192.168.33.10:2181 --list
创建 topic – 1个分区1个副本
$ ./bin/kafka-topics.sh --zookeeper 192.168.33.10:2181 --create --replication-factor 1 --partitions 1 --topic zxtopic
查看topic信息
$ ./bin/kafka-topics.sh --zookeeper 192.168.33.10:2181 --describe --topic zxtopic
如果创建 topic 设定为3个副本,会发生什么?
$ ./bin/kafka-topics.sh --zookeeper 192.168.33.10:2181 --create --replication-factor 3 --partitions 1 --topic zxtopicA
报错原因是因为我只开启了一个kafka服务,但是想要3个副本的情况下,必须需要3个kafka服务支持。
生产者producer
$ ./bin/kafka-console-producer.sh --broker-list localhost:9092 --topic zxtopic
消费者consumer
$ ./bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic zxtopic --from-beginning
Kafka角色介绍
Producer:消息生产者,将消息push到Kafka集群中的Broker。
Consumer:消息消费者,从Kafka集群中pull消息,消费消息。
Consumer Group:消费者组,由一到多个Consumer组成,每个Consumer都属于一个Consumer Group。消费者组在逻辑上是一个订阅者。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不影响。即每条消息只能被Consumer Group中的一个Consumer消费;但是可以被多个Consumer Group组消费。这样就实现了单播和多播。
Broker:一台Kafka服务器就是一个Broker,一个集群由多个Broker组成,每个Broker可以容纳多个Topic。
Topic:消息的类别或者主题,逻辑上可以理解为队列。Producer只关注push消息到哪个Topic,Consumer只关注订阅了哪个Topic。
Partition:负载均衡与扩展性考虑,一个Topic可以分为多个Partition,物理存储在Kafka集群中的多个Broker上。可靠性上考虑,每个Partition都会有备份Replica。
Replica:Partition的副本,为了保证集群中的某个节点发生故障时,该节点上的Partition数据不会丢失,且Kafka仍能继续工作,所以Kafka提供了副本机制,一个Topic的每个Partition都有若干个副本,一个Leader和若干个Follower。
Leader:Replica的主角色,Producer与Consumer只跟Leader交互。
Follower:Replica的从角色,实时从Leader中同步数据,保持和Leader数据的同步。Leader发生故障时,某个Follower会变成新的Leader。
Controller:Kafka集群中的其中一台服务器,用来进行Leader election以及各种Failover(故障转移)。
系统架构 - 基础
Producer:生产者
Consumer:消费者
Kafka Broker:KAFKA实例服务
Zookeeper:注册选举Leader
系统架构 – Topic+Partition
Producer:生产者1和生产者2
Kafka Broker:3个KAFKA服务
Consumer group:消费组2个
Topic:topicA、topicB
- topicA:2个partition分区(p1、p2),3个副本(broker1、2、3)
- topicB:1个partition分区(p1),2个副本(broker2、3)
Zookeeper:注册选举Leader
Kafka架构深入
工作流程
Kafka 中消息是以 topic 进行分类的,topic 是逻辑上的概念,而 partition 是物理上的概念。每个 partition 对应于一个 log 文件,该 log 文件中存储的就是 producer 生产的数据。
消费者组中的每个消费者,都会实时记录自己消费到了哪个 offset,以便出错恢复时,从上次的位置继续消费。
topic存储流程
topic 数据存在 partition, 以 topicName-partitionNum 的形式创建文件夹。
文件夹下面 partition 的数据存储在 log 里面。
为防止 log 文件过大导致数据定位效率低下,Kafka 采取了分片和索引机制,将每个 partition 分为多个 segment。
log.segment.bytes=1073741824 (1G)参数来决定何时生成新的 segment。
segment 核心文件由两部分组成,分别为 “.index” 文件和 “.log” 文件,分别表示为 segment 索引文件和数据文件。这两个文件的命令规则为:partition 全局的第一个 segment 从 0 开始,后续每个 segment 文件名为上一个 segment 文件最后一条消息的 offset 值,数值大小为 64 位,20 位数字字符长度,没有数字用 0 填充。
**.index 文件:**文件存储大量的索引信息,采用了稀疏索引数据结构。索引条目中只有一些搜索键值,并不是所有搜索键值,要定位一个记录,需要找到包含小于等于待查找的搜索键值的最大的搜索键值的索引条目,从该索引条目指向的记录开始,沿着文件中的指针开始遍历,直到找到所需记录。
示例:
- 找到小于等于ID=22222的最大索引条目
- 找到ID=10101的索引条目
- 从ID=10101的索引条目中记录的指针指向的记录ID=10101开始沿着文件里的指针遍历,直到找到ID=22222的记录
稀疏数组
稀疏数组可以看做是普通数组的压缩,但是这里说的普通数组是值无效数据量远大于有效数据量的数组
压缩前:
0 0 0 0 0 0 0 0 0 0 0
0 0 1 0 0 0 0 0 0 0 0
0 0 0 0 2 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
压缩后:
11 11 2
1 2 1
2 4 2
**.log 文件:**文件存储大量的数据,index 和 log 文件以当前 segment 的第一条消息的 offset 命名,索引文件中的元数据指向对应数据文件中 message 的物理偏移地址。
如何从 partition 中通过 offset 查找 message 呢?
3个segment数据
00000000000000000000.index
00000000000000000000.log
00000000000000170410.index
00000000000000170410.log
00000000000000239430.index
00000000000000239430.log
以上图为例,读取 offset=170418 的消息,首先查找 segment 文件,其中 00000000000000000000.index 为最开始的文件,第二个文件为 00000000000000170410.index(起始偏移为 170410+1=170411),而第三个文件为 00000000000000239430.index(起始偏移为 239430+1=239431),所以这个 offset=170418 就落到了第二个文件之中。其它后续文件可以依次类推,以其偏移量命名并排列这些文件,然后根据二分查找法就可以快速定位到具体文件位置。其次根据 00000000000000170410.index 文件中的 [8,1325] 定位到 00000000000000170410.log 文件中的 1325 的位置进行读取。
要是读取 offset=170418 的消息,从 00000000000000170410.log 文件中的 1325 的位置进行读取。
生产者
分区策略
分区的原因:方便在集群中扩展,每个 Partition 可以通过调整以适应它所在的机器,而一个 topic 又可以有多个 Partition 组成,因此整个集群就可以适应任意大小的数据了。
分区的原则:
- 指明 partition 的情况下,直接将指明的值直接作为 partiton 值
- 没有指明 partition 值但有 key 的情况下,将 key 的hash值与 topic 的 partition 数进行取余得到 partition 值
- 既没有 partition 值又没有 key 值的情况下,第一次调用时随机生成一个整数(后 面每次调用在这个整数上自增),将这个值与 topic 可用的 partition 总数取余得到 partition 值,也就是常说的 round-robin 算法
数据可靠性保证
为保证 producer 发送的数据,能可靠的发送到指定的 topic,topic 的每个 partition 收到 producer 发送的数据后,都需要向 producer 发送 ack(acknowledgement 确认收到),如果 producer 收到 ack,就会进行下一轮的发送,否则重新发送数据。
ACK应答机制
0: producer 不等待 broker 的 ack。这一操作提供了一个最低的延迟,broker 一接收到还没有写入磁盘就已经返回,当 broker 故障时有可能 丢失数据。
1: producer 等待 broker 的 ack,partition 的 leader 落盘成功后返回 ack;如果在 follower 同步成功之前 leader 故障,那么将会 丢失数据。
-1: producer 等待 broker 的 ack,partition 的 leader 和 follower(ISR中的) 全部落盘成功后才 返回 ack。但是如果在 follower 同步完成后,broker 发送 ack 之前,leader 发生故障,那么会 造成数据重复。
ISR
$ kafka-topics.sh --zookeeper 192.168.33.10:2181 --describe --topic zxtopic
Leader 维护了一个动态的 in-sync replica set (ISR),意为和 leader 保持同步的 follower 集合,如果 follower 长时间未向 leader 同步数据,则该 follower 将被踢出 ISR(该时间阈值由replica.lag.time.max.ms 参数设定)。Leader 发生故障之后,就会从 ISR 中选举新的 leader。
上面也就是意味着,并不是所有副本replica的follower这个leader都关心,leader只关心ISR里面的follower。
HW,LEO 保证consumer消费数据的可靠性
LEO:指的是每个副本最大的 offset。
HW:指的是消费者能见到的最大的 offset,ISR 队列中最小的 LEO。
故障处理机制
leader 故障:leader 发生故障之后,会从 ISR 中选出一个新的 leader,为保证多个副本之间的数据一致性,其余的 follower 会先将各自的 log 文件高于 HW 的部分截掉,然后从新的 leader 同步数据。
follower 故障:follower 发生故障后会被临时踢出 ISR,待该 follower 恢复后,follower 会读取本地磁盘记录的上次的 HW,将 log 文件高于 HW 的部分截取掉,从 HW 开始向 leader 进行同步,等该 follower 的 LEO 大于等于该 Partition 的 HW,即 follower 追上 leader 之后,就可以重 新加入 ISR 了。
Exactly Once 语义
At Most Once 语义:将服务器 ACK 级别设置为 0,可以保证 Producer 每条消息只会被发送一次。
At Least Once 语义:将服务器的 ACK 级别设置为-1,可以保证 Producer 到 Server 之间不会丢失数据。
Exactly Once 语义:对于一些非常重要的信息,下游数据消费者要求数据既不重复也不丢失,这个时候引入了幂等性,幂等性就是指 Producer 不论 向 Server 发送多少次重复数据,Server 端都只会持久化一条。
幂等性 结合 At Least Once 语义,就构成了 Kafka 的 Exactly Once 语义(At Least Once + 幂等性 = Exactly Once)。
启用幂等性
将 Producer 的参数中 enable.idompotence 设置为 true。
原理:
开启幂等性的 Producer 在初始化的时候会被分配一个 PID,发往同一 Partition 的消息会附带 Sequence Number,Broker 端会对<PID, Partition, SeqNumber>做缓存,当具有相同主键的消息提交时,Broker 只会持久化一条。
消费者
消费方式
consumer 采用 pull(拉)模式从 broker 中读取数据,push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由 broker 决定的。pull 模式不足之处是,如果 kafka 没有数据,消费者可能会陷入循环中,一直返回空数据。Kafka 的消费者在消费数据时会传入一个时长参数 timeout,如果当前没有数据可供消费,consumer 会等待一段时间之后再返回,这段时长即为 timeout。
分区Partition分配策略
一个 topic 有多个 partition;一个 consumer group 中有多个 consumer;partition 的分配问题,即确定哪个 partition 由哪个 consumer 来消费。
Range 范围分区(默认的):基于Topic进行分配
原理:假如有10个分区 Partition;3个消费者 Consumer(当然这3个Comsumer是同一个消费组的),因为分配策略是基于消费组讨论的。把分区按照序号排列0,1,2,3,4,5,6,7,8,9;消费者为C1,C2,C3;C1、C2、C3都订阅了Topic。
分配算法为:分区Partition总数除以Consumer的个数。
分配结果:C1:0,1,2,3;C2:4,5,6;C3:7,8,9;
如果存在多个Topic呢?
假设 Topic1 和 Topic2 都有10个分区,并且C1、C2订阅了Topic1;C3订阅了Topic2。
很明显:即使C1,C2,C3都是同一个组,也不能跨Topic消费,因为Range的分配是基于Topic而不是基于组(RoundRobin)。
假设 Topic1 和 Topic2 都有10个分区,并且C1、C2、C3都订阅了Topic1和Topic2。
很明显:如果存在多个Topic,C1消费者比别的消费者多消费2个Partition,所以使用Range分区策略的话,当Topic越多,那么会造成某个Consumer消费的Partition越来越多。
RoundRobin 范围分区:基于组进行分配
这种策略会把 Topic 下面的所有 Partition 全部列出来,参考一个类 TopicAndPartition。
正常订阅轮询
假设存在2个 Topic T1和T2,T1 存在3个Partition P1-1 P1-2 P1-3;T2 存在4个Partition P2-1 P2-2 P2-3 P2-4。存在一个消费组,消费组有C1和C2并且C1和C2都订阅了T1、T2。
现在会把所有的 Topic(T1、T2) 和 Partition 组合起来形成列表。
然后根据轮询算法分配给 C1、C2。
错误订阅轮询
假设存在2个 Topic T1和T2,T1 存在3个Partition P1-1 P1-2 P1-3;T2 存在4个Partition P2-1 P2-2 P2-3 P2-4。存在一个消费组,消费组有C1和C2,但是其中 C1订阅了T1,C2订阅了T2。
这个时候因为RoundRobin基于组进行分配(C1订阅了T1,代表消费组要消费T1;C2订阅了T2,代表消费组要消费T2);消费者依然会把所有的 Topic(T1、T2) 和 Partition 组合起来形成列表。
然后根据轮询算法分配给 C1、C2。
我们发现 C1 根本没有订阅 T2 ,但是由于RoundRobin是基于组的分配;C1 也分配到了消费 T2 的 Partition,这是错误的。所以这种情况我们不能使用RoundRobin分配策略,只能使用Range分配策略。使用RoundRobin分配策略的前提条件是同一个组(Consumer Group)下面的 Consumer 订阅了相同的 Topic。
offset的维护
Kafka 0.9 版本之前,consumer 默认将 offset 保存在 Zookeeper 中;从 0.9 版本后, consumer 默认将 offset 保存在 Kafka 一个内置的 topic 中,该 topic 为 __consumer_offsets。
Kafka API
Kafka事务
事务可以保证 Kafka 在 Exactly Once 语义的基础上,生产和消费可以跨分区和会话,要么全部成功,要么全部失败,所以明显开启事务之前必须要开启幂等性。
Producer 事务
由客户端引入一个全局唯一的 Transaction ID,并将 Producer 获得的 PID 和 Transaction ID 绑定。At Least Once + 幂等性 = Exactly Once,幂等性由 PID、SeqNum、Partition组成。引入了 Transaction ID,后面每次Producer重启,不会请求Broker获得新的PID,而是通过Transaction ID 获得原来的 PID,为了管理 Transaction,Kafka 引入了一个新的组件 Transaction Coordinator。
Transaction Coordinator
Producer 就是通过和 Transaction Coordinator 交互获得 Transaction ID 对应的任务状态,Transaction Coordinator 还负责将事务所有写入 Kafka 的一个内部 Topic,这样即使整个服务重启,由于 事务状态得到保存,进行中的事务状态可以得到恢复,从而继续进行。
Producer API
发送流程
Kafka 的 Producer 发送消息采用的是异步发送的方式,在消息发送的过程中,涉及到了 main 线程和 Sender 线程,一个线程共享变量 RecordAccumulator。main 线程将消息发送给 RecordAccumulator,Sender 线程不断从 RecordAccumulator 中拉取消息发送到 Kafka broker。
源码:
设计图:
Main线程:发送数据经过拦截器、序列化器、分区器,最后到达 RecordAccumulator。
Send线程:发送数据相关的参数batch.size和linger.ms。
API
分区器:
拦截器:
配置:
消费者打印:
Consumer API
定期消费生产者数据,重点配置enable.auto.commit: 是否开启自动提交 offset 功能;auto.commit.interval.ms: 自动提交 offset 的时间间隔。
自动提交
如果我们把 ENABLE_AUTO_COMMIT_CONFIG 设置为false,那么系统不为我们自动提交消费过的 offset。这样会造成一个问题就是:我们的消费者消费数据后,因为没有提交offset,那么consumer下次来消费数据读取的offset又是消费前的那个offset。
数据重复读取:假设当前consumer的 offset 是90,Producer 产生了10条数据被 consumer 消费了,如果存在自动提交,则当前 consumer 的 offset 会在硬盘上写为100;如果不存在自动提交,则 consumer 消费数据后,这个offset没有写入到硬盘,下次读取数据还是从 offset = 90 这个位置开始读取 broker 的数据。
自动提交产生的问题
默认系统自动提交offset的时间间隔是1s,假设consumer正在处理100条数据,过了1s后系统把offset提交到了硬盘,offset记录为100。但是consumer在处理到第80条数据的时候因为某种原因崩了,这个时候重启consumer,他读取的数据offset就是从100开始,会造成丢失了20条数据,这个时候产生的问题会导致 consumer 消费丢失了数据,需要考虑手动提交offset。
手动提交 offset 的方法有两种:commitSync(同步提交) 和 commitAsync(异步提交)。
两者相同点:都会将本次 consumer.poll 拉到的一批数据的最高的偏移量提交。
两者不同点:commitSync 阻塞当前线程一直到提交成功,并且会自动失败重试;commitAsync 没有失败重试机制,故有可能提交失败。
同步提交
异步提交
需要注意:无论是同步提交还是异步提交 offset,都有可能会造成数据的漏消费或者重复消费。先提交 offset 后消费,如果处理数据的时候服务挂了,则有可能造成数据的漏消费;先消费后提交 offset;如果提交offset的时候服务挂了,则有可能会造成数据重复消费。