kafka做为一种消息中间件,有如下特性:java
根据Kafka的特性,有如下应用场景:算法
kafka是一个自然支持分布式架构的发布订阅模式的rpc通讯框架,kafka集群为典型的去中心化的设计,主体设计以下:api
Kafka集群的元数据保存在Zookeeper中,除此以外不存储任何消息数据。每个Broker都须要在Zookeeper上注册并不断在上面更新本身的元数据(Topic和Partition信息),Zookeeper会使用这些数据信息来实现动态的集群扩容数组
Producer和Consumer都会在Zookeeper上注册监听器(Watcher),用于在Zookeeper发生变化时做出响应的调整。同时,Consumer还会向Zookeeper中注册本身消费的Partition列表,用于发现Broker并与Partition创建socket链接缓存
Kafka中的Topic是以Partition的形式存放的,一个Topic会被拆分为多个Partition,存放在多台服务器上。Producer在生产数据时会根据必定的规则将数据写入指定Topic下的Partition中服务器
能够设置每个Topic的Partition数量,可是须要注意的是,一个Partition只能供一个Consumer消费,若是Partition过少,就可能会有Consumer消费不到数据。另外,建议partition的数量也须要大于集群中Broker的数量,这样可让Partition Leader尽可能均匀地分布在各个Broker中。同时也须要注意,拆分的Partiton越多,也就意味着须要更多的空间网络
一般一个Partition须要有数个副本(Replication),Kafka容许用户设置一份数据的备份个数,副本会存储在不一样的Broker上。在全部的副本中(包括本身),会存在一个Partition Leader用于进行读写,Leader的选举调度等操做由Zookeeper来完成数据结构
Producer直接将消息发送到Broker的Partition Leader上,不须要通过代理中转等操做,由于在设计时,Kafka集群中的每个Broker均可以单独响应Producer的操做,并返回Topic的一些信息(存活的机器/Leader位置/...)架构
Producer客户端负责采用指定的负载均衡算法,管理消息会被推送到哪些Partition上。同时Producer能够将消息在内存中累计到必定数量时,做为一个Batch进行发送,可以有效减小IO次数,进而提升效率。具体的Batch参数能够手动设置,能够是累计的数量大小/时间间隔等并发
Producer能够异步地向Kafka发送数据,在发送后会收到一个Futrue响应,包含offset值等信息。能够经过指定acks参数来控制Producer要求收到的确认消息个数
Kafka中,读取消息的offset值由Consumer进行维护,所以consumer能够自由选取读取消息的方式。同时,无论消息有没有被消费,数据都会在kafka中保存一段时间
Kafka提供了两种consumer api,分别是high-level api和sample api。Sample api只维持了和单一Broker的链接,同时是无状态的,每次请求都须要指定offset值,因此也更为灵活
High-Level api封装了对集群中broker的访问,能够透明的访问一个topic,同时也维持了已消费消息的状态,每次消费的都是下一个消息。High-Level api还支持以组(CG)的形式消费消息,消息会被发送给全部的CG,CG内部会选择按顺序发送给全部Consumer或是指定的Consumer
Kafka能够以集合(batch)形式发送数据,在此基础上,kafka能够对batch进行压缩。在producer端进行压缩后,在consumer进行解压,减小了传输所需的数据量,减轻对网络的压力。kafka在消息头部增长了一个字节用于描述压缩属性,这个字节后两位表示压缩采用的编码,若是后两位为0,表示消息未被压缩
最理想的状况是消息发送成功,而且只发送了一次,这种状况叫作exactly-once,可是不可避免的会发生消息发送失败以及消息重复发送的状况
为了解决这类问题,在producer端,当一个消息被发送后,producer会等待broker发送响应,收到响应后producer会确认消息已经被正确发送给kafka,不然就会从新发送
在consumer端,由于broker记录了partition中的offset值,这个值指向consumer下一个消费的消息,若是consumer收到消息可是消费失败,broker能够根据offset值来找到上一个消息,同时consumer还能够控制offset值,来对消息进行任意处理
(在“核心组件-Partition”中已经对此部分作了叙述)
consumer在进行消息消费时,能够指定消息某分区的消息
通常地,一个topic下会有多个partition,而一个partition只能被一个CG中的consumer消费,能够经过指定rebalance策略,来采用不一样的消费方式。Rebalance策略有两种,范围分区(Range)和轮询分区(RoundRobin),范围分区策略,即对topic下的partition进行排序,将partition数量除以CG下的consumer数量,从而得出每个consumer消费哪几个分区
轮询分区策略则是将partition按照hashcode进行排序,而后经过分区取模来给consumer分配partition
当如下三种状况发生时,会触发rebalance操做,从新指定分区:
rebalance的执行由CG Leader来完成,并负责在执行结束后将执行结果经过broker集群中的coordinator广播到CG。当CG的第一个consumer启动后,这个consumer会和kafka肯定组内的coordinator,以后CG内的全部成员都会和该coordinator进行通讯
CG Leader的选举有两个阶段,Join Group
和Synchronizing Group State
。
Join Group
阶段,全部成员都会向coordinator发送JoinGroup请求,当全部consumer都发送请求后, coordinator会选择一个consumer担任leader,并把CG的信息发送给该leaderSynchronizing Group State
阶段,全部consumer都会向coordinator发送SynchronizingGroupState请求,而leader则将分区方案发送给coordinator,coordinator会在接受到分区方案后,将分区结果返回给全部consumer,这样就完成分区方案的同步消息的持久化并不只仅是出于数据备份的须要,一个事实是,线性读写的时间远远高于随机读写,对磁盘的线性读所消耗的时间在有些状况下能够比内存的随机访问更快,因此现代不少操做系统会把空闲的内存用做磁盘缓存,尽管会在内存回收时带来性能损耗,可是在读写上带来的效率提高是显著的
基于这样的事实,利用文件系统依靠页缓存来维护数据,会比维护一个内存缓存更好,由于采用了更为紧凑的数据结构。不一样于维护尽量多的内存缓存,若是咱们将数据写入到一个持久化日志中,不调用刷新程序,这意味着数据将被传输到内核中并在稍后被刷新,咱们也能够经过配置来控制数据在何时刷新到物理磁盘上
kafka中持久化消息队列采用对文件的读写来实现,相似日志的形式。尽管这种操做不支持丰富的语义,可是能够很高效的进行并行操做,而且全部的操做都是常数时间,最终系统的性能和数据大小彻底无关,能够充分利用硬盘来进行高效的消息服务
为了解决字节拷贝的问题,kafka采用“标准字节消息”这种消息格式,这种格式在producer、consumer和broker间共享,kafka的日志文件都是按“标准字节消息”这种格式写入磁盘中。unix系统为了提升页面缓存和socket之间的数据传递效率,使用了“零拷贝”机制,即sendfile system call 系统调用,java中也提供了访问这个系统调用的接口
为了解释为何这种方式能解决字节拷贝带来的性能损耗,咱们先来描述将数据从文件发送到socket的通常步骤:
咱们能够发现这个过程至少涉及4次字节拷贝,2次系统调用,2次内核态到用户态的切换,而若是咱们可以直接将数据写入socket缓存中,就能减小不少没必要要的切换。若是使用了sendfile的方式,数据能够直接由内核页缓存直接拷贝到内核socket缓存中,不须要进行额外的系统状态切换。经过这种方式,即便下游有不少consumer,也不会对集群服务形成压力
想更详细了解零拷贝机制的可见个人另外一篇文章:浅谈零拷贝机制
频繁的小io能够经过一次性发送一个消息集合,而不是只发送一条消息来解决,消息在服务器以消息块的形式添加到日志中。同时consumer在查询时也会一次查询大量的线性数据块。消息集合(Message Set)将一个字节数组或文件进行打包,同时能够有选择地进行反序列化