一、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 中,这里涉及到内核态和用户态的上下文切换,会导致效率不高。
1.3.2 零拷贝
从上面的非零拷贝来看,那两次数据拷贝完全是没有必要的,直接将数据发送给网卡,由网卡交给消费系统就可以。
通过 Linux 的 sendFile ,由 sendFile 进行检查,缓存中如果有就直接将数据发给网卡,如果没有就直接将磁盘中的数据读取到缓存并将其发送到网卡,由网卡直接将数据发送给消费系统。
二、节省空间的数据格式
2.1 旧消息格式
| CRC32 | magic | attibute | 时间戳 | key 长度 | key | value 长度 | value |
|---|---|---|---|---|---|---|---|
Kafka 的消息格式是通过 NIO 的 ByteBuffer 进行的二进制存储,相对于 java 对象序列化后存放,节约约 40% 的空间。
这个消息其实是封装在一个 Log Entity 中,在 kafka 中每个 topic 的 partition 就是一个日志文件,每条消息就是一个 Log Entity 就可以将消息理解为是一个日志条目,日志条目中还会包涵 offset , 消息长度 ,消息(就上面那一串)。
在日志文件中还有一个 RecodeBatch 的概念,也就是一组消息的集合。
2.2 新消息格式
******************************** **********************************
| 消息长度 | attibute(废弃) | 时间戳与 RecodeBatch 的差值 | offset 与 RecordBatch 的差值 | key 长度 | key | value 长度 | value | header 个数 | 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连接发送回客户端。
七、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 。
八、内核参数设置
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