Kafka设计理念总结(更新中)

Kafka设计理念总结

要解决的问题

linkedin当时希望可以有一个队列,让用户在web页面的所有操作记录都被发送都后端。因为用户的行为记录数据量很大,他们希望这个队列的吞吐量尽可能地高。
这个场景有点像推荐里面,需要收集用户的一些行为数据,比如说点赞评论进房间退房间等。

持久化设计

Kafka使用磁盘存储,策略是简单地将log顺序写入磁盘,但不flush。这样有以下好处:

  1. 顺序写入磁盘可以很高效地利用磁盘,吞吐量很高。顺序写入磁盘时,吞吐量可以达到600MB/s
  2. 享受操作系统自带的页缓存。由于每次写入不显式地flush到磁盘,所以实际上log只是写在操作系统自带的页缓存里面。操作系统会将所有空闲内存用来做页缓存,而且在内存不足时使用页置换算法将不太可能用到的页写入磁盘,这导致写入log天然地高效,且程序编写起来也很简单。
  3. 避免了将磁盘的数据load到内存里面带来的内存浪费(page cache一份,程序的内存里面一份),同时也避免了JVM存储大量对象的开销。

在数据结构方面,Kafka采用了简单的线性队列结构,而没有采用B+树。这是因为虽然B+树的复杂度为O(logN),但由于每一次磁盘的随机访问seek代价昂贵(10ms),实际上采用B+树的增删改查的消耗会随着数据集的大小线性增长。而采用线性队列结构,则可以实现O(1)的性能,在很廉价的磁盘上实现很高的读写性能。

性能

Kafka要解决的关键问题就是性能,这是由于Kafka最初设计被用于传递用户的行为信息:用户的每个行为,比如浏览某个页面,都会产生一条消息。这样的消息的量非常大。
在确定了数据的存储方式之后,只剩下两种可能会导致低效的问题:

  1. 多次的少量IO
  2. 大量的数据复制

为了避免多次少量IO,Kafka的协议围绕"messageset"去构建。这导致网络IO和磁盘读写都是以批量的方式进行,使得吞吐量提高几个数量级。

为了避免大量的数据复制,Kafka有如下的特点:

  1. 设计了一种标准协议。这使得数据从Producer到Kafka再到Consumer不会被修改
  2. 使用sendfile,使得网络传输来的数据直接到pagecache或者从pagecache直接到网络

这样,Kafka只需要先用sendfile将Producer发送来的消息放到pagecache,再用sendfile将这些消息发给Consumer。

(这里可以引申到:sendfile的内部实现)

Producer

Producer直接将数据发送给对应Partition的Leader。为了实现这个,每个Broker都可以响应元数据请求,里面包含哪个Partition的Leader在哪里等信息。
Client可以自己决定将消息发给哪个Partition。可以通过随机发送给任何一个Partition来实现负载均衡。同时,Kafka通过对key进行hash从而实现将具有相同的key的消息发送到相同的Partition,用户也可以覆盖这个hash逻辑。
同时Producer发送消息到Kafka也是批量的,Client会自己将message在内存中累计到一定数量或者一个时间段才批量发送给Kafka。

Consumer

pull和push

Consumer使用pull的模型,这样有以下好处

  1. 消费速率由Consumer控制,不会出现队列推得太快,Consumer消费不及时
  2. 如果消费者因为异常,消费速度变慢,导致积压,他可以在正常的时候再加快消费速度赶上来
  3. Kafka可以大批量地发送给消费者

pull模型会有"忙-等待"的效率问题。Kafka Client可以使用"long-polling"的方式去获取,即发起一个请求,直到有数据到达或timeout之前,这个请求会被blocked。

消费的位置

Kafka对于每个Topic下的每个Partition,都维持一个offset,记录着Consumer下一次需要消费的位置。这样维持消费位置会变得十分廉价。
这样也有一个好处,就是Consumer可以重头开始消费。

语义

有三种传输保证:

  1. 最多一次(at most once)
  2. 最少一次(at least once)
  3. 有且只有一次(exactly once)

kafka在0.11之前,对于每条记录,如果broker写入成功但producer不知道,那么producer会重试。这是at least once语义。
在0.11之后,每个producer会有一个ID,且每个producer对于其发送的每条记录都有一个序列ID。如果他写入成功了,并重试,那么重试的写入不会成功。这是exactly once语义。

consumer则根据offset的提交策略,有两种语义:

  1. 处理完之后再提交,则有可能处理完提交前down掉,这是at least once语义
  2. 提交完再处理,则有可能提交后处理前提交,这是at most once语义

如果consumer要实现exactly once语义,则需要事务的支持。

复制

复制的单位是Topic-Partition,每个Partition都有一个Leader和多个Follower,Leader和Follower加起来等于Replication Factor。每个Follower像普通的Consumer一样从Leader那里去消费log,这样可以批量消费。

Kafka这样定义节点alive:

  1. 和Zookeeper保持Session
  2. 和Leader的log对比不是太落后

Kafka里面这样的节点被定义为"in sync"
一个消息被认为commited的定义是:所有in sync的节点都apply了这个log
当一个消息被commited,则可以可以被消费。
对于生产者,可以忽略Broker返回的ACK,从而实现提高吞吐量。

Kafka采用ISR(in-sync Replication)的复制策略。ISR是指保证某几个节点是in-sync的。当Leader down了,所有的in-sync节点(又称为ISR set)都可以被选举成为Leader,而ISR set是具体哪几个节点,则被写入在zookeeper里面。通过这种方式,Kafka可以使用f+1个节点去容忍f个机器down掉。
不使用传统Quorum的原因是:Quorum模型对资源代价很大,每增加一个容错就要增加两次写入,两个备份,不适合这种大量写入的系统。
(这里可以引申到Quorum模型,Raft算法)

如果一个节点down了,他再次假如ISR set之前,他必须把已有的log全部重新同步给自己。这个是因为:

  1. 在现实情况下,一般节点down掉,存储的内容可能是错误的
  2. 不希望在每次写入时,都要执行fsync。这样会导致吞吐下降几个数量级

如果所有节点都挂了,那么可以通过配置选择

  1. 第一个恢复的在ISR set上的节点成为Leader
  2. 第一个恢复的节点成为Leader,无论他是不是ISR set上的

如果你希望,在部分节点down掉的情况下,宁可不可用,也希望确保数据的正确性,你可以配置最小的ISR set数量。如果节点数少于这个数量,则集群直接不可用。

在选举方面,Kafka会令一个节点作为Controller,由他进行对Leader为down掉节点的所有partition重新选举。

TODO:看完日志压缩部分https://kafka.apache.org/documentation/#compaction

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值