Kafka是一种分布式的发布/订阅消息系统。主要设计目标如下:
从上图可以看到kafka的核心组件是Broker、Producer和consumer。
以上组件在分布式环境下均可以是多个,支持故障转移。同时ZK仅和broker和consumer相关。值得注意的是broker的设计是无状态的,消费的状态信息依靠消费者自己维护,通过一个offset偏移量。
Broker内部维护着Topic和Partition。
Producer发送消息到达Broker后,消息按照Topic提交到Partition上。在Partition中,消息是按序号顺序存储的。Consumer从Partition上顺序读取消息,并将从Partition上读取的最后一条消息的offset存储到zookeeper。一个partition只能有一个consumer进行消费。因此,Partition是Kafka中横向扩展和一切并行化的基础。
kafka设计的目标是:
消息交付是否可靠,有以下几种保证:
高吞吐量依赖于OS文件系统的页缓存、sendfile技术和线性读写磁盘:
依赖OS的页缓存能大量减少IO,高效利用内存来作为缓存。当上层有写操作时,操作系统只是将数据写入OS的PageCache,同时标记Page属性为Dirty。当读操作发生时,先从PageCache中查找,如果发生缺页才进行磁盘调度,最终返回需要的数据。实际上PageCache是把尽可能多的空闲内存都当做了磁盘缓存来使用。同时如果有其他进程申请内存,回收PageCache的代价又很小。
传统网络IO,OS 从硬盘把数据读到内核区的PageCache,用户进程把数据从内核区Copy到用户区。然后用户进程再把数据写入到Socket,数据流入内核区的Socket Buffer上。OS 再把数据从Buffer中Copy到网卡的Buffer上,这样完成一次发送。整个过程共经历两次Context Switch,四次System Call。
同一份数据在内核Buffer与用户Buffer之间重复拷贝,效率低下。其中2、3两步没有必要,完全可以直接在内核区完成数据拷贝。这也正是Sendfile所解决的问题,经过Sendfile优化后,整个I/O过程就变成了下面这个样子。
磁盘线性读写要比随机读写快很多。顺序IO不仅可以利用RAID技术带来很高的吞吐量,同时可以基于文件的读和追加来构建持久化队列,利用队列来提供常量时间O(1)时间复杂度的put和get。
Producer支持End-to-End的压缩。数据在本地压缩后放到网络上传输,在Broker一般不解压(除非指定要Deep-Iteration),直至消息被Consume之后在客户端解压。
当然用户也可以选择自己在应用层上做压缩和解压的工作(毕竟Kafka目前支持的压缩算法有限,只有GZIP和Snappy),不过这样可能造成效率的意外降低!
Kafka的End-to-End压缩与MessageSet配合在一起工作效果最佳,上面的做法直接割裂了两者间联系。至于道理其实很简单,压缩算法中一条基本的原理“重复的数据量越多,压缩比越高”。大多数情况下输入数据量大一些会取得更好的压缩比。
producer只向leader partition发送消息。通过load balance,将消息发送到不同的partition。
producer采用push模式,任性地不需要考虑consumer是否能处理。如果要保证低延迟,就需要consumer快速处理,一般是在consumer端进行缓存。
为了保证低延迟,producer一次只发送一条数据,比较浪费,可采用批量发送。
kafka采用pull模式。push模式和pull模式相比,push模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。push模式的目标是尽可能以最快速度传递消息,但是这样很容易造成consumer来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而pull模式则可以根据consumer的消费能力以适当的速率消费消息,可使消费速率最大化。
基于pull模式的另一个优点是,它有助于积极的批处理的数据发送到消费者。基于push模式必须选择要么立即发送请求或者积累更多的数据,稍后发送它,无论消费者是否能立刻处理它,如果是低延迟,这将导致短时间只发送一条消息,不用缓存,这是实在是一种浪费,基于pull的设计解决这个问题,消费者总是pull在日志的当前位置之后pull所有可用的消息(或配置一些大size),所以消费者可设置消费多大的量,也不会引入不必要的等待时间。
kafka引入replication和leader机制来实现高可用。
将Topic的Partition的副本分配到集群中的其他Broker上,允许故障时请求自动转到副本上。分配Replica的算法如下:
(1) 将n个Broker和待分配的Partition排序
(2) 第i个Partition分配到第i%n个Broker
(3) 第i个Partition的第j个Replica分配到第(i + j) %n个Broker
Leader负责数据读写,Follower只向Leader顺序Fetch数据(N条通路)。Leader会跟踪与其保持同步的Replica列表,该列表称为ISR(即in-sync Replica)。Consumer读消息也是从Leader读取,只有被commit过的消息才会暴露给Consumer。
当Producer发布消息到某个Partition时:
(1) 先通过ZooKeeper找到该Partition的Leader,然后无论该Topic的Replication Factor为多少(也即该Partition有多少个Replica),Producer只将该消息发送到该Partition的Leader。Leader会将该消息写入其本地Log。
(2) 每个Follower都从Leader pull数据。这种方式上,Follower存储的数据顺序与Leader保持一致。Follower在收到该消息并写入其Log后,向Leader发送ACK。
(3) 一旦Leader收到了ISR中的所有Replica的ACK,该消息就被认为已经commit了,Leader将增加HW并且向Producer发送ACK。
(4) 为了提高性能,每个Follower在接收到数据后就立马向Leader发送ACK,而非等到数据写入Log中。因此,对于已经commit的消息,Kafka只能保证它被存于多个Replica的内存中,而不能保证它们被持久化到磁盘中,也就不能完全保证异常发生后该条消息一定能被Consumer消费。
当leader宕机时,通过leader election从replica中选举leader。只有ISR里的成员才有被选为Leader的可能。如果所有Replica都不工作,那么kafka将选择第一个“活”过来的Replica(不一定是ISR中的)作为Leader。
所有Partition的Leader选举Leader选举都由controller决定。在所有broker中选出一个controller,controller会将Leader的改变直接通过RPC的方式(比ZooKeeper Queue的方式更高效)通知需为为此作为响应的Broker。同时controller也负责增删Topic以及Replica的重新分配。
broker failover过程如下:
本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。