Kafka 入门解析

675 阅读17分钟

kafka 入门.png

一、Kafka 优势

1.1 低延迟、高吞吐

延迟:延迟指的是处理一条数据所用的时间,比如一条数据发送到 kafka ,kafka 用了 0.1 毫秒将数据写入,这 0.1毫秒就是延迟。

吞吐:单位时间内处理数据的数目就叫做吞吐。

1.2 写入原理

Kafka 收到数据写入请求之后,会直接将数据写入到 OS 的 Page Cache 中,由 OS 定时执行 fsync 将 缓存中的数据落入磁盘,由于数据就直接写入内存,所以效率是很高的,也就是延迟会很低,延迟低了,单位时间内可以处理的数据量就会变大,这就是 kafka 的高吞吐和低延迟。

这里写入磁盘文件的时候,也是顺序写,直接将数据加入到文件中,顺序写的性能也很高。

1.3 数据读取

1.3.1 非零拷贝

如果是非拷贝的话,会先判断缓存中是否有数据,如果没有数据的话,会先从磁盘中获取到缓存中, 从缓存中将数据拷贝到 kafka 的应用进程中 ,之后在 从 kafka 的应用进程中拷贝到 OS 的 Socket Cache 中 ,通过 Socket Cache 发送给网卡 ,最后 由网卡将数据发送给需要进行消费的系统

这里的话,涉及到了两次数据拷贝的过程,一次从 OS 到 Kafka 的应用进程,一次从 Kafka 的应用进程到 OS 中,这里涉及到内核态和用户态的上下文切换,会导致效率不高。

非零拷贝.png

1.3.2 零拷贝

从上面的非零拷贝来看,那两次数据拷贝完全是没有必要的,直接将数据发送给网卡,由网卡交给消费系统就可以。

通过 Linux 的 sendFile ,由 sendFile 进行检查,缓存中如果有就直接将数据发给网卡,如果没有就直接将磁盘中的数据读取到缓存并将其发送到网卡,由网卡直接将数据发送给消费系统。

零拷贝.png

二、节省空间的数据格式

2.1 旧消息格式

CRC32magicattibute时间戳key 长度keyvalue 长度value

Kafka 的消息格式是通过 NIO 的 ByteBuffer 进行的二进制存储,相对于 java 对象序列化后存放,节约约 40% 的空间。

这个消息其实是封装在一个 Log Entity 中,在 kafka 中每个 topic 的 partition 就是一个日志文件,每条消息就是一个 Log Entity 就可以将消息理解为是一个日志条目,日志条目中还会包涵 offset , 消息长度 ,消息(就上面那一串)。

在日志文件中还有一个 RecodeBatch 的概念,也就是一组消息的集合。

2.2 新消息格式

******************************** **********************************

消息长度attibute(废弃)时间戳与 RecodeBatch 的差值offset 与 RecordBatch 的差值key 长度keyvalue 长度valueheader 个数header 对

这里 时间戳,key 长度 , 等字段都是使用了变长字段进行处理,且时间戳与 offset 的变成了存储于 RecordBatch 的差值,也会减少一部分的空间使用量。

******************************************** **********************************************

三、ISR、HW、LEO

3.1 ISR

每个 topic 可以有多个分区,不同的分区可以分布在不同的机器上,每个分区可以有自己的冗余副本,所以的分区选举出来一个 分区 Leader ,其余的作为 follow ,由 Leader 进行处理请求, follow 从 leader 上获取数据,进行同步。

ISR 代表的就是和 leader 副本保持较近的 kafka broker 的 id 集合。OSR 则相反, ISR + OSR = AR。

当 leader 发生宕机,后续进行选举的时候,只会从 ISR 集合中进行选举。

3.2 HW

HW 代表的是消费者只能消费到 HW 之前的数据。

3.3 LEO

LEO 指的是,下一条数据进来应该的 offset ,比如现在有 3条数据,那么这是的 LEO 就是4,代表的是下一条数据的 offset 。

3.4 更新 LEO 和 HW

每次 follow 向 leader 发送 fetch 请求的时候,都会将他们自身的 LEO 传递过来,在 Leader 这会保存一份 follow 的 LEO 信息,每次收到 follow 请求的时候进行更新。

每次 leader 给 follow 发送数据的时候,都会带着自己当前的 HW 过去,follow 拿着 HW 和自身的 LEO 做对比,取小值作为 follow 自己的 HW ,每次 follow 发送 fetch 请求带过来的 leo ,对比所有 follow 的 leo ,取最小值作为 leader 的 HW。

Leader 的 HW 代表当前 partition 的 HW

四、问题

4.1 高水位数据丢失问题

假如说生产者的min.insync.replicas设置为1,这个就会导致说生产者发送消息给leader,leader写入log成功后,生产者就会认为写成功了,此时假设生产者发送了两条数据给leader,leader写成功了,此时leader的LEO = 1,HW = 0,因为follower还没同步,HW肯定是0

接着follower发送fetch请求,此时leader发现follower LEO = 0,所以HW还是0,给follower带回去的HW也是0,然后follower开始同步数据也写入了两条数据,自己的LEO = 1,但是HW = 0,因为leader HW为0

接着follower再次发送fetch请求过来,自己的LEO = 1,leader发现自己LEO = 1,follower LEO = 1,所以HW更新为1,同时会把HW = 1带回给follower,但是此时follower还没更新HW的时候,HW还是0

这个时候假如说follower机器宕机了,重启机器之后,follower的LEO会自动被调整为0,因为会依据HW来调整LEO,而且自己的那两条数据会被从日志文件里删除,数据就没了

这个时候如果leader宕机,就会选举follower为leader,此时HW = 0,接着leader那台机器被重启后作为follower,这个follower会从leader同步HW是0,此时会截断自己的日志,删除两条数据,这种场景就会导致数据的丢失。

4.2 高水位数据不一致问题

假设min.insync.replicas = 1,那么只要leader写入成功,生产者而就会认为写入成功

如果leader写入了两条数据,但是follower才同步了一条数据,第二条数据还没同步,假设这个时候leader HW = 2,follower HW = 1,因为follower LEO小于leader HW,所以follower HW取自己的LEO

这个时候如果leader挂掉,切换follower变成leader,此时HW = 1,就一条数据,然后生产者又发了一条数据给新leader,此时HW变为2,但是第二条数据是新的数据。接着老leader重启变为follower,这个时候发现两者的HW都是2

所以他们俩就会继续运行了

这个时候他们俩数据是不一致的,本来合理的应该是新的follower要删掉自己原来的第二条数据,跟新leader同步的,让他们俩的数据一致,但是因为依赖HW发现一样,所以就不会截断数据了。

4.3 leader epoch

所谓的leader epoch大致理解为每个leader的版本号,以及自己是从哪个offset开始写数据的,类似[epoch = 0, offset = 0],这个就是epoch是版本号的意思,接着的话,按照之前的那个故障场景

假如说follower先宕机再重启,他会找leader继续同步最新的数据,更新自己的LEO和HW,不会截断数据,因为他会看看自己这里有没有[epoch, offset]对,如果有的话,除非是自己的offset大于了leader的offset,才会截断自己的数据

而且人家leader的最新offset = 1,自己的offset = 0,明显自己落后于人家,有什么资格去截断数据呢?对不对,就是这个道理。而且还会去从leader同步最新的数据过来,此时自己跟Leader数据一致。

如果此时leader宕机,切换到follower上,此时就会更新自己的[epoch = 1, offset = 2],意思是自己的leader版本号是epoch = 1,自己从offset = 2开始写数据的

然后接着老leader恢复变为follower,从新leader看一下epoch跟自己对比,人家offset = 2,自己的offset = 0,也不需要做任何数据截断,直接同步人家数据就可以了

然后针对数据不一致的场景,如果说老leader恢复之后作为follower,从新leader看到[epoch = 1, offset = 1],此时会发现自己的offset也是1,但是人家新leader是从offset = 1开始写的,自己的offset = 1怎么已经有数据了呢?

此时就会截断掉自己一条数据,然后跟人家同步保持数据一致。

五、文件

5.1 文件保存

每个分区对应的目录,就是“topic-分区号”的格式,比如说有个topic叫做“order-topic”,那么假设他有3个分区,每个分区在一台机器上,那么3台机器上分别会有3个目录,“order-topic-0”,“order-topic-1”,“order-topic-2”

每个分区里面就是很多的log segment file,也就是日志段文件,每个分区的数据会被拆分为多个段,放在多个文件里,每个文件还有自己的索引文件,大概格式可能如下所示:

00000000000000000000.index

00000000000000000000.log

00000000000000000000.timeindex

00000000000005367851.index

00000000000005367851.log

00000000000005367851.timeindex

00000000000009936472.index

00000000000009936472.log

00000000000009936472.timeindex

kafka broker有一个参数,log.segment.bytes,限定了每个日志段文件的大小,最大就是1GB,一个日志段文件满了,就自动开一个新的日志段文件来写入,避免单个文件过大,影响文件的读写性能,这个过程叫做log rolling

5.2 数据查找

kafka 在写入数据的时候,会将数据写入 .log 文件,并且将索引写入 .index 和 .timeindex 文件,当我们检索数据的时候,会先通过二分查找查询索引文件,根据索引文件中标记的 offset 对应的 log 的物理文件地址,再从 .log 文件中获取到实际的数据,这里的时间复杂度为 O(logN)。

.index

44576 物理文件(.log位置)

57976 物理文件(.log位置)

64352 物理文件(.log位置)

offset = 58892 => 57976这条数据对应的.log文件的位置

.tiemindex 是时间戳索引文件,和 .index 文件基本一致,不过是时间戳和物理文件地址的映射。

5.3 文件清理

kafka broker 有一个参数 log.retention.hours , 默认是7天,也就是说 kafka 的数据默认会保存7天,由 kafka 后台线程进行文件的清理。

六、kafka 请求

6.1 kafka 通讯

kafka broker ,生产者,消费者,broker 和 broker 之间,是通过 kafka 自身封装的协议来实现的,自定求了请求格式,请求数据,响应格式等。

broker端会构造一个请求队列,然后不停的获取请求放入队列,后台再搞一堆的线程来获取请求进行处理

broker端会构造一个请求队列,然后不停的获取请求放入队列,后台再搞一堆的线程来获取请求进行处理

是基于 tcp 实现的长连接,避免了频繁创建和销毁连接带来的性能开销。

6.2 Reactor模式进行多路复用处理请求

实际上每个 Broker 中都有一个 acceptor 线程和多个 processor 线程,由参数 num.network.threads 控制,默认是3个,client 和 broker 之间只会创建一个 Socket 连接,进行复用。每次连接之后, acceptor 会将 socket 请求转给 processor 进行处理,acceptor 会默认轮询所有的 processor 线程,将 socket 请求发给 processor 。

processor 处理多个客户端的 socket 连接,其实是通过 Selector 多路复用的思想来实现的,用一个 Selector 来监控各个 Socket 连接,看是否有请求过来,这样就可以处理多个客户端的Socket 请求。

processor 会将所有的请求,放入 broker 的请求队列中,默认是 500,由参数 queued.max.requests 控制,所以那默认的三个 Processor 会一直将请求放入到这个队列中。

接着就是一个 KafkaRequestHandler 线程池负责不停的从请求队列中获取请求来处理,这个线程池大小默认是8个,由 num.io.threads 参数来控制,处理完请求后的响应,会放入每个processor自己的响应队列里。

每个processor其实就是负责对多个socket连接不停的监听其传入的请求,放入请求队列让KafkaRequestHandler来处理,然后会监听自己的响应队列,把响应拿出来通过socket连接发送回客户端。

多路复用处理请求.png

七、Controller

7.1 作用

感知服务宕机,leader partition 的选举,负载均衡,等等,Controller。他负责管理整个kafka集群范围内的各种东西

7.2 选举

Kafka 集群在启动的时候,会通过 Zookeeper 选举出来一个 Controller ,负责管理集群。

基本原理就是:集群中的 Broker 节点,都尝试去 ZK 中创建 /Controller 临时节点,这时只会有一个节点创建成功,这个节点就是 Controller 节点,如果这个 Controller 节点宕机的话,ZK 中的节点就会消失,其余的 Broker 节点会监听到节点消失,去尝试创建节点,创建成功的节点就是新的 Controller 节点。

7.3 控制 - 创建 topic

当创建一个 topic 的时候,肯定是会有分区,分区还会有副本,这个时候会在 Zookeeper 中创建相应的 topic 的信息,此时 partition 的状态都是 NonExistentReplica

Controller 实际是会监控 zk 中的数据变化,这时会将 topic 的 partition 等信息加载到内存中,并将 partition 副本状态改为 NewReplica ,然后选择第一个副本作为 leader ,其余的作为 follow ,并将其加入到 ISR 集合中。

比如说你创建一topic,order_topic,3个partition,每个partition有2个副本,写入zk里去

/topics/order_topic

partitions = 3, replica_factor = 2

[partition0_1, partition0_2]

[partition1_1, partition1_2]

[partition2_1, partition2_2]

从每个parititon的副本列表中取出来第一个作为leader,其他的就是follower,把这些东西给放到partition对应的ISR列表里去。

每个 partition 会尽可能均匀的分布在不同的请求上,所有的请求都会分布在 leader 上面,同时还会设置 partition 的状态为 OnlinePartition ,同时 Controller 会把这个partition和副本所有的信息(包括谁是leader,谁是follower,ISR列表),都发送给所有broker让他们知晓,在kafka集群里,controller负责集群的整体控制,但是每个broker都有一份元数据。

7.4 控制 - 删除 topic

如果要删除掉 topic 的话, controller 会发送请求给这个 topic 的所有的 Partition 所在的机器上,将 partition 副本状态改为 OfflineReplica ,也就是让所有的副本下线,然后再将副本状态改为 ReplicaDeletionStarted

之后,通知 Broker ,把各个 partition 副本的数据删除掉,就是删除磁盘上的文件,这时候会将副本状态改为 ReplicaDeletionSuccessful ,接着在变为NonExistentReplica ,最后将 partition 状态改为 Offline

Kafka 参数设置.png

八、内核参数设置

broker.id

每个 Broker 的唯一 ID。

log.dirs

日志(数据)文件的存储路径。

zookeeper.connect

kafka 连接 zk 的地址

listeners

broker 监听客户端发起请求的端口

unclean.leader.election.enable

默认是false,意思就是只能选举ISR列表里的follower成为新的leader,1.0版本后才设为false,之前都是true,允许非ISR列表的follower选举为新的leader

delete.topic.enable

默认是 true ,表示允许删除 topic

log.retention.hours

数据文件可以保留多少个小时,默认是 168 , 也就是七天

min.insync.replicas

这个跟acks=-1配合起来使用,意思就是说必须要求ISR列表里有几个follower,然后acks=-1就是写入数据的时候,必须写入这个数量指定的follower才可以,一般是可以考虑如果一个leader两个follower,三个副本,那么这个设置为2,可以容忍一台机器宕机,保证高可用,但是写入数据的时候必须ISR里有2个副本,而且必须副本都写入才算成功

如果你就一个leader和follower,双副本,min.insync.replicas = 2。如果有一台机器宕机,导致follower没了,此时ISR列表里就一个leader的话,你就不能写入了,如果你只有一个副本了,此时还可以写入数据的话,就会导致写入到leader之后,万一leader也宕机了,此时数据必然丢失

一般来说为了避免数据占用磁盘空间过大,一般都是kafka设置双副本,一个leader和一个follower就够了,设置的三副本的话,数据在集群里乘以3倍的空间来存放,非常耗费磁盘空间,对你的整体集群的性能消耗也会更大

min.insync.replicas=1,acks=-1(一条数据必须写入ISR里所有副本才算成功),你写一条数据只要写入leader就算成功了,不需要等待同步到follower才算写成功。但是此时如果一个follower宕机了,你写一条数据到leader之后,leader也宕机,会导致数据的丢失。

一般来说双副本的场景下,我们为了避免数据丢失,min.insync.replicas=2,acks=-1,每次写入必须保证ISR里2个副本都写成功才可以,如果其中一个副本没了,会导致你就没法写入,必须阻塞等待kafka恢复,这样就可以保证数据的不丢失

num.network.threads

这个是负责转发请求给实际工作线程的网络请求处理线程的数量,默认是3,高负载场景下可以设置大一些,也就是 processor 处理线程的线程数

num.io.threads

这个是控制实际处理请求的线程数量,默认是8,高负载场景下可以设置大一些,也就是 kafkarequesthandler 线程池的线程数量

message.max.bytes

这个是broker默认能接受的消息的最大大小,默认是100MB

log.flush.interval.messages/log.flush.interval.ms

os 的 page cache 每有多少之后执行刷盘,或者每隔多长时间之后进行刷盘,线上的常用的一个配合,高负载高吞吐的情况下,建议设置为1分钟

九、JVM 参数设置

export KAFKA_HEAP_OPTS=”-Xmx6g -Xms6g -XX:MetaspaceSize=96m -XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35 -XX:G1HeapRegionSize=16M -XX:MinMetaspaceFreeRatio=50 -XX:MaxMetaspaceFreeRatio=80”

十 、OS 参数设置

文件描述符限制:kafka会频繁的创建和修改文件,大约是分区数量 * (分区总大小 / 日志段大小) * 3,比如一个broker上大概有100个分区,每个分区大概是10G的数据,日志段大小是默认的1G,那么就是100 * 10(分区里的日志段文件数量) * 3 = 3000个文件

ulimit -n 100000,这个可以设置很大的描述符限制,允许创建大量的文件

磁盘flush时间:默认是5秒,os cache刷入磁盘,可以设置为几分钟,比如1分钟才刷入磁盘,这样可以大幅度提升单机的吞吐量

sysctl -a | grep dirty
 
vm.dirty_background_bytes = 0
vm.dirty_background_ratio = 5
vm.dirty_bytes = 0
vm.dirty_expire_centisecs = 3000
vm.dirty_ratio = 10
vm.dirty_writeback_centisecs = 500

vm.dirty_writeback_centisecs:每隔5秒唤醒一次刷磁盘的线程
vm.dirty_expire_centisecs:os cache里的数据在3秒后会被刷入磁盘

可以设置大一些,1分钟~2分钟都可以,特别大规模的企业和公司,如果你可以接收机器宕机的时候,数据适当可以丢失一些,kafka里的数据可以适当丢失一些,但是为了提升集群的吞吐量的话

十一、性能测试

11.1 生产消息测试

bin/kafka-producer-perf-test.sh --topic test-topic --num-records 500000 --record-size 200 --throughput -1 --producer-props bootstrap.servers=hadoop03:9092,hadoop04:9092,hadoop05:9092 acks=-1

12.2 消费消息测试

bin/kafka-consumer-perf-test.sh --broker-list hadoop03:9092,hadoop04:9092,hadoop53:9092 --fetch-size 2000 --messages 500000 --topic test-topic