Kafka基础之入门知识

Kafka介绍

Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。Kafka是一种高吞吐量分布式发布订阅消息系统,他可以收集并处理用户在网站中的所有动作流数据以及物流网设备的采样信息

Kafka使用场景

  • 系统间消息解耦
  • 异步通信
  • 削峰填谷
  • Kafka Streaming实时在线流处理

Kafka基础架构

Kafka基本是以集群的形式存在的,以Topic形式负责分类集群中的Record,每一个Record属于一个Topic。每个Topic底层都会对应一组分区的日志用于持久化Topic中的Record。同时在Kafka集群中,Topic的每一个日志的分区都一定会有一个Broker担当该分区的Leader,其他的Broker担当该分区的Follower,Leader负责分区数据的读写操作,Follower负责同步该分区的数据。这样如果分区的Leader宕机,该分区的其他Follower会选取出新的Leader继续负责该分区数据的读写。其中集群中的Leader的监控和Topic的部分元数据是存储在zookeeper中。

简单场景示意
简单场景示意图

生产者:投递消息到一个topic中

消费者:可以同时订阅多个topic获取消息

消息分发策略
消息分发策略

hash(key)%分区:根据key的hash值,将消息均匀的散列在同一个topic的不同分区上

分区:一个topic下可以配置多个分区

副本因子:每个主要分区备份数据的副本数量

Broker:一个Broker一定是至少主要负责某一个分区的读写,可以负责其他分区的副本。当某一个分区的Broker宕机后,zookeeper会重新选举一个已宕机的分区Leader出来,一个Broker可能身兼多个分区Leader。

Kafka分区日志

Kafka中所有消息是通过Topic为单位进行管理,每个Kafka中的Topic通常会有多个订阅者,负责订阅发送到该Topic中的数据。Kafka负责管理集群中每个Topic的一组日志分区数据。

每组日志分区是一个有序的不可变的日志序列,分区中的每一个Record都被分配了唯一的序列编号称为是offset,Kafka集群会持久化所有发布到Topic中的Record消息,该Record的持久化时间是通过配置文件指定,默认是168小时。

1
log.retention.hours=168

Kafka底层会定期的检查日志文件,然后将过期的数据从log中移除,由于Kafka使用硬盘存储文件,因此使用Kafka长时间缓存一些日志文件是不存在问题的。

分区日志示意
分区日志示意

分发分区策略:可以选择轮询或者hash等不同策略

old -> new:消息有序按时间顺序增长,但是整个Topic内的顺序不能保证先进先出,只能保证单个分区是有序的。如果想作为先进先出的队列使用,建议不分区。

不能保证FIFO为啥还要对日志分区:

  • 首先,它们允许日志扩展到超出单个服务器所能容纳的大小。每个单独的分区都必须适合托管它的服务器,但是一个Topic可能有很多分区,因此它可以处理任意数量的数据。
  • 其次每个服务器充当某些分区的Leader,也可能充当其他分区的Follower,因此集群中的负载的得到了很好的平衡。
  • 单个Topic的写入性能得到了极大的提升,不同的分区是由不同的Broker来负责读写,提升了吞吐量

Kafka生产者&消费者组

消费者在消费Topic中的数据的时候,每个消费者会维护本次消费对应分区的偏移量(offset),消费者会在消费玩一个批次的数据之后,会将本次消费的偏移量提交给Kafka集群,因此对于每个消费者而言可以随意的控制该消费者的偏移量。因此Kafka中,消费者可以从一个topic分区中的任意位置读取队列数据,由于每个消费者控制了自己的消费的偏移量,因此多个消费者之间彼此相互独立

生产者&消费者组
生产者&消费者组

生产者偏移量:只管往后写,最后一个消息偏移量就是当前分区已写入的总消息量

消费者偏移量:消费者可以从分区的任意一个偏移量开始读,每次读之后,消费者会主动通知Kafka当前已读的偏移量,值是下一个偏移量。即当访问了偏移量为15时,提交访问偏移量为16。

消费者组:消费者会使用Consumer Group名称来标识自己,并且发布到Topic的每条记录都会传递到每个订阅Consumer Group中的一个消费者实例,如果所有的消费者实例都具有相同的Consumer Group,那么Topic中的记录会在该Consumer Group中Consumer实例进行均分消费;如果所有的Consumer实例具有不同的Consumer Group,则每条记录会广播到所有的Consumer Group进程。

简而言之:一个Consumer Group可以理解为一个逻辑上的订阅者。它由多个Consumer实例组成,以实现可伸缩性容错性能力。Topic按照分区的方式均分给一个Consumer Group下的所有实例,如果Consumer Group有新成员加入,则它会分担其他消费者负责的某些分区;同理如果一个Consumer Group下有实例宕机,则由该Group下的其他实例接管宕机的实例所负责的分区。

当消费者组内的消费者实例数大于Topic分区数时:多于的消费者实例会闲着,当存在已被分区的实例宕机时,会自动接管宕机实例的分区进行消费。

Kafka顺序写入和mmap

Kafka的特性之一就是高吞吐量,但是Kafka的消息是保存或缓存在磁盘上的,一般认为在磁盘上的读写数据是会降低性能的,但是Kafka即使是普通的服务器也可以轻松支持秒级百万的写入请求,超过了大部分的消息中间件,这种特性也使得Kafka在日志处理等海量数据场景应用广泛。Kafka会把收到的数据都写入到磁盘上,为了防止丢数据,优化写入速度,Kafka采用了2个技术:顺序写入MMFile

顺序写入:硬盘是机械结构,每次读写都会1.寻址 -> 2.写入。其中寻址是一个最耗时的动作,所以硬盘最讨厌随机IO,喜欢顺序IO,所以为了提高硬盘的读写速度,Kafka就是使用的顺序IO。这样省去了大量的内存开销以及节省了IO寻址的时间。但是单纯的使用顺序写入,Kafka的写入性能也不可能和内存进行对比,因此Kafka的数据并不是实时的写入磁盘中。

MMFile:Kafka充分利用了现代操作系统分页存储来利用内存提高IO效率。Memory Mapped Files(mmap)内存映射文件,在64位操作系统中一般可以表示20G的数据文件,它的工作原理是直接利用操作系统的Page实现文件到物理内存的直接映射。完成mmap映射后,用户对内存的所有操作会被操作系统自动的刷新到磁盘上,极大地降低了IO使用率。

顺序写&MMF
顺序写&MMF

用户空间:应用一般都是运行在用户空间下,只需要将数据写入到内存页PageCache中即可,后面不需要等待缓存刷新到磁盘的过程,而且即使应用宕机,也并不会影响已经写入内存页的数据丢失。

内核空间:由操作系统底层自己控制,自动将PageCache上的数据刷到磁盘上,没有用户空间切换下,减少了一定的IO,相对的,可支持的IO就更大。

问题:如果内核不稳定,出现问题,就会导致应用没有故障还是丢失数据的问题。毕竟高吞吐量和一致性不能全部都万无一失。

Kafka读取零拷贝

Kafka客户端在响应客户端读取的时候,底层使用Zero Copy(零拷贝)技术,直接将磁盘无需拷贝到用户空间,而是直接将数据通过内核空间传递输出,数据并没有抵达用户空间

传统IO操作示意图
传统IO操作示意图

传统IO操作流程:

  1. 用户进程调用read等系统调用向操作系统发出IO请求,请求读取数据到自己的内存缓冲区中,自己进入阻塞状态。
  2. 操作系统收到请求后,进一步将IO请求发送磁盘。
  3. 磁盘驱动器收到内核的IO请求,把数据从磁盘读取到驱动器的缓冲中,此时不占用CPU。当驱动器的缓冲区被读满后,向内核发起中断信号告知自己缓冲区已满。
  4. 内核收到中断,使用CPU时间将磁盘驱动器中缓冲中的数据拷贝到内核缓冲区中。
  5. 如果内核缓冲区的数据少于用户申请的读的数据,重复步骤3和步骤4,直到内核缓冲区的数据足够多为止。
  6. 将数据从内核缓冲区拷贝到用户缓冲区,同时从系统调用中返回,回到用户空间,完成任务。

DMA示意图
DMA直接寻址示意图

DMA:协处理器,协助CPU做IO调度。

相对于传统IO:减少了CPU控制中断的次数,不妨碍CPU的执行计算,可以大大提高CPU的计算能力。

传统或DMA模式下IO
传统或DMA模式下IO

用户访问服务器正常读取流程:

  1. 文件在磁盘中数据被copy到内核缓冲区。
  2. 从内核缓冲区copy到用户缓冲区。
  3. 用户缓冲区copy到内核与socket相关的缓冲区。
  4. 数据从socket缓冲区copy到相关协议引擎发送出去。

一共经历了4次数据拷贝,2次用户态和内核态的切换

零拷贝示意图
零拷贝示意图

零拷贝下的读取流程:

  1. 文件在磁盘中数据被copy到内核缓冲区。
  2. 从内核缓冲区copy到内核与socket相关的缓冲区。
  3. 数据从socket缓冲区copy到相关协议引擎发送出去。

一共经历了3次数据拷贝,没有用户态和内核态的切换

小结

Kafka为什么读入和写入性能高?

  1. 分区特性决定了读入和写入的性能,重点在高吞吐量。
  2. 顺序写入和MMF决定了写入性能的提升。
  3. 零拷贝决定了读取性能的提升。