浅谈Kafka特性与架构

kafka是一个天然支持分布式架构的发布订阅模式的rpc通信框架,kafka集群为典型的去中心化的设计,主体设计如下:

生产者向Kafka集群提供数据,消费者从Kafka集群拉取数据,Kafka集群的调度由Zookeeper负责

Zookeeper

Kafka集群的元数据保存在Zookeeper中,除此之外不存储任何消息数据。每一个Broker都需要在Zookeeper上注册并不断在上面更新自己的元数据(Topic和Partition信息),Zookeeper会使用这些数据信息来实现动态的集群扩容

Producer和Consumer都会在Zookeeper上注册监听器(Watcher),用于在Zookeeper发生变化时作出响应的调整。同时,Consumer还会向Zookeeper中注册自己消费的Partition列表,用于发现Broker并于Partition建立socket连接

核心组件

Partition

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

Producer直接将消息发送到Broker的Partition Leader上,不需要经过代理中转等操作,因为在设计时,Kafka集群中的每一个Broker都可以单独响应Producer的操作,并返回Topic的一些信息(存活的机器/Leader位置/...)

Producer客户端负责采用指定的负载均衡算法,管理消息会被推送到哪些Partition上。同时Producer可以将消息在内存中累计到一定数量时,作为一个Batch进行发送,能够有效减少IO次数,进而提高效率。具体的Batch参数可以手动设置,可以是累计的数量大小/时间间隔等

Producer可以异步地向Kafka发送数据,在发送后会收到一个Futrue响应,包含offset值等信息。可以通过指定acks参数来控制Producer要求收到的确认消息个数

acks参数为n时,只有当n个partition副本收到消息后,producer才会收到broker的确认

acks参数为-1时,producer会在所有partition副本收到消息后得到broker的确认

acks参数为0时,producer不会等待broker的响应,可以得到最大的吞吐量,但是可能会导致数据丢失

Consumer

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值,来对消息进行任意处理

备份机制

(在“核心组件-Producer”中已经对此部分做了叙述)

消息消费策略

消费策略分类

固定分区消费

consumer在进行消息消费时,可以指定消息某分区的消息

Rebalance分区消费

一般地,一个topic下会有多个partition,而一个partition只能被一个CG中的consumer消费,可以通过指定rebalance策略,来采用不同的消费方式。Rebalance策略有两种,范围分区(Range)和轮询分区(RoundRobin),范围分区策略,即对topic下的partition进行排序,将partition数量除以CG下的consumer数量,从而得出每一个consumer消费哪几个分区

轮询分区策略则是将partition按照hashcode进行排序,然后通过分区取模来给consumer分配partition

Rebalance的触发时机

当以下三种情况发生时,会触发rebalance操作,重新指定分区:

  • CG内部加入了新的consumer
  • consumer离开CG
  • topic新增partition

Rebalance的执行过程

rebalance的执行由CG Leader来完成,并负责在执行结束后将执行结果通过broker集群中的coordinator广播到CG。当CG的第一个consumer启动后,这个consumer会和kafka确定组内的coordinator,之后CG内的所有成员都会和该coordinator进行通信

CG Leader的选举有两个阶段, Join GroupSynchronizing Group State

  1. Join Group 阶段 ,所有成员都会向coordinator发送JoinGroup请求,当所有consumer都发送请求后, coordinator会选择一个consumer担任leader,并把CG的信息发送给该leader
  2. Synchronizing Group State 阶段 ,所有consumer都会向coordinator发送SynchronizingGroupState请求,而leader则将分区方案发送给coordinator,coordinator会在接受到分区方案后,将分区结果返回给所有consumer,这样就完成分区方案的同步

高效性设计

消息持久化

消息的持久化并不仅仅是出于数据备份的需要,一个事实是,线性读写的时间远远高于随机读写,对磁盘的线性读所消耗的时间在有些情况下可以比内存的随机访问更快,所以现代很多操作系统会把空闲的内存用作磁盘缓存,尽管会在内存回收时带来性能损耗,但是在读写上带来的效率提升是显著的

基于这样的事实,利用文件系统依靠页缓存来维护数据,会比维护一个内存缓存更好,因为采用了更为紧凑的数据结构。不同于维护尽可能多的内存缓存,如果我们将数据写入到一个持久化日志中,不调用刷新程序,这意味着数据将被传输到内核中并在稍后被刷新,我们也可以通过配置来控制数据在什么时候刷新到物理磁盘上

常数时间的保证

kafka中持久化消息队列采用对文件的读写来实现,类似日志的形式。尽管这种操作不支持丰富的语义,但是可以很高效的进行并行操作,并且所有的操作都是常数时间,最终系统的性能和数据大小完全无关,可以充分利用硬盘来进行高效的消息服务

字节拷贝

为了解决字节拷贝的问题,kafka采用“标准字节消息”这种消息格式,这种格式在producer、consumer和broker间共享,kafka的日志文件都是按“标准字节消息”这种格式写入磁盘中。unix系统为了提高页面缓存和socket之间的数据传递效率,使用了“零拷贝”机制,即sendfile system call 系统调用,java中也提供了访问这个系统调用的接口

为了解释为什么这种方式能解决字节拷贝带来的性能损耗,我们先来描述将数据从文件发送到socket的一般步骤:

  1. os将数据从磁盘读到内核空间的页缓存中
  2. 应用将数据从内核空间读到用户空间的页缓存中
  3. 应用将数据写回内核空间的socket缓存中
  4. os将数据从socket缓存写到网卡缓存中
  5. 数据经网络发出

我们可以发现这个过程至少涉及4次字节拷贝,2次系统调用,2次内核态到用户态的切换,而如果我们能够直接将数据写入socket缓存中,就能减少很多不必要的切换。如果使用了sendfile的方式,数据可以直接由内核页缓存直接拷贝到内核socket缓存中,不需要进行额外的系统状态切换。通过这种方式,即使下游有很多consumer,也不会对集群服务造成压力

频繁小IO

频繁的小io可以通过一次性发送一个消息集合,而不是只发送一条消息来解决,消息在服务器以消息块的形式添加到日志中。同时consumer在查询时也会一次查询大量的线性数据块。消息集合(Message Set)将一个字节数组或文件进行打包,同时可以有选择地进行反序列化

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章