一个流式数据平台,最重要的是要具备如下3个特点:
作为一个流式数据平台,Kafka如何实现上面3个功能特点?
消息系统(也叫消息队列)主要有两种消息模型:队列和发布订阅。
Kafka使用消费组统一了两种消息模型
任何消息队列要做到发布消息和消费消息的解耦合,实际上都要扮演一个存储系统的角色,负责保存还没有被消费的消息。如果消息只是在内存中,一单机器宕机或重启,内存中的消息就会全部丢失。Kafka也不例外,数据写入到Kafka集群的服务器节点时,黑灰赋值多份来保障出现故障时仍能可用。为了保证消息的可靠存储,Kafka还允许生产者的生产请求在收到应答结果前,阻塞式地等待一条消息,直到它完全地复制到多个节点上,才认为这条消息写入成功。
流式数据平台仅有消息的读取和写入、消息的存储是不够的,还需要流式数据处理能力。对于简单的处理,可直接使用Kafka提供的生产者API和消费者API来完成;但对于复杂的业务逻辑处理,Kafka提供了完整的流处理API,比如流的聚合、连接、各种转换操作。Kafka流处理框架内部解决很多流处理应用都会面临的问题:处理乱序或迟来的数据、重新处理输入数据、窗口和状态操作等。
Kafka将消息系统、存储系统、流处理系统都组合在一起,构成了以Kafka为中心的的流式处理数据处理平台。它既能处理最新的实时数据,也能处理过去的历史数据,其主要包括4种核心API:
先抛出3个问题,在回答这些问题时需要引入很多概念:
Kafka集群为每个主题维护了分布式的分区( partition )日志文件,物理意义上可以把主题看作分区的日志文件( partitioned log)。 每个分区都是一个有序的、不可变的记录序列,新的消息会不断追加到提交日志( commit log)。 分区中的每条消息都会按照时间顺序分配到一个单调递增的顺序编号,作偏移量( offset ),这个偏移量能够唯一地定位当前分区中的每一条消息。每个分区的偏移量都从0开始,不同分区间的偏移量都是独立的,不会互相影响。
如上图所示,主题有3个分区,每条消息包括键值和时间戳,消息到达后会按照规则到指定分区,得到一个分区内的自增偏移量,原始的消息内容和分配到的偏移量以及其他一些元数据信息会存储到分区日志文件中。
基于推送模型的消息系统,由消息代理记录消费者的消费状态。消息代理在消息推送到消费者后,标记这条消息为已消费。但是,如果消息代理将消息发出后,消费进程挂掉或网络原因消费者没有收到消息时,就可能造成消息丢失。要保证消息的处理语义,消息代理发送完消息后,要设置状态为已发送,只有收到消费者的确认请求才更新为已消费,这需要在消息代理中记录所有消息的消费状态。
Kafka采用拉取模型,由消费者自己记录消费状态,每个消费者独立地顺序读取每个分区的消息。
如图所示,有不同消费组的两个消费者订阅了同一个主题,并且分到了同一个分区,消费者A的进度为3,消费者B的进度是6。消费者拉取的最大上限通过最高水位(watermark)控制,生产者最新写入的消息如果还没有到达备份数量,对消费者是不可见的。这种由消费者控制偏移量的优点是:消费者读取间不受影响,可以按照任意顺序消费消息,甚至消费者可以充值偏移量,重新读取之前已经消费过的消息。
在一些消息系统中,消息代理会在消息被消费后立即删除消息。如果有不同类型的消费者订阅同一个主题,消息代理可能需要冗余地存储同一条信息;或者等素有消费者都消费完才删除,这就需要消息消息代理跟踪每个消费者的消费状态,这种设计很大程度上限制了消息系统的整体吞吐量和处理延迟。Kafka的做法是生产者发布的消息会一直保存在Kafka集群中,不管消息有没有被消费。用户可以通过设置保留时间来清理过期数据。
Kafka每个主题的多个分区日志分布式的存储在Kafka集群上,同时为了故障容错,每个分区都会以副本的方式复制到多个消息代理节点上,其中一个节点作为主副本(Leader),其他节点作为备份副本(Follower)。主副本会负责客户端的所有读写操作,备份副本仅仅从主副本同步数据。当主副本出现故障时,本分副本中的一个副本会被选择为新的主副本。即每个分区的副本中只有主副本负责接受读写,所以每个服务端都会作为某些主分区的副本,以及另外一些分区的本分副本。这样Kafka集群的所有服务端整体上对客户端是负责均衡的。
Kafka的生产者和消费者和消费者对于服务端来说都是客户端,生产者客户端发布消息到服务端的指定主题,会指定消息所属的分区。根据消息是否有键采用不同的分区策略:有键则Hash,无键则轮询。
同样地先抛出3个问题:
人们普遍认为一旦涉及磁盘访问,读写的性能就严重下降。实际上,现代操作系统针对磁盘的读写已经做了一些优化方案来加快磁盘的访问速度。
消息系统内的消息从生产者保存到服务端,再从服务端读取出来,数据的传出效率决定了生产者和消费者的性能。生产者如果每发送一条消息都直接通过网络发送到服务端,势必会造成过多的网络请求。如果我们能够将多条消息按照分区进行分组,并采用批量的方式一次发送一个消息集,并且对消息集进行压缩,就可以减少网络传输的带宽,进一步提高数据的传输效率。
消费者要读取服务端的数据,需要将服务端的磁盘文件通过网络发送到消费者进程,而网络发送通常涉及不同的网络节点。传统的读取磁盘文件在每次发送网络时,都需要将页面缓存先保存到用户缓存,然后读取消息时再将其复制到内核空间,步骤如下:
结合Kafka的消息有多个订阅者的使用场景,生产者发布的消息一般会被不同的消费者消费多次,数据传输十分频繁,使用"零拷贝技术"只需将磁盘文件的数据复制到页面缓存一次,然后将数据聪明和页面缓冲直接发送到网络中(发送给不同的使用者可以重复使用同一个页面缓存),避免了重复的复制操作。这样,消息的使用速度基本上等同于网络连接的速度了。
对比优化前后的两种方案。假设有10个消费者,传统复制方式的数据复制次数为4 x 10 = 40次,而"零拷贝技术"只需要将磁盘文件读入页面缓存1次加上10个消费者各读取页面缓存1次到网卡接口,共11次拷贝。显然减少了数据的复制次数,提高了消费性能。
Kafka的生产者将消息直接发送给分区主副本的消息代理节点,并不需要经过中间路由层,为了做到这一点,所有消息代理节点在发送消息之前,会向任意一个代理节点请求元数据,并确定每条消息对应的目标节点(分区对应的主节点),然后发送出去。分区的选择规则如下:
前面说过Kafka会将生产者的消息按照分区分组,同一分区的消息批量压缩发送,减少了网络请求。对于缓冲的调节我们可以在生产者客户端设置消息大小上限和延迟时间,达到消息大小上限或延迟时间,都会触发网络请求。
Kafka消息消费采用拉取模型,和生产者采用批量发送消息类似,消费者拉取消息可以一次拉取一批消息。拉取模型虽然不用消息代理记录消息的消费状态,但也会有一个缺点:消息代理没有数据或者数据量很少,消费者可能需要不断的轮询,并等待新数据。可以通过允许消费者拉取请求以阻塞式、长轮询的方式等待,直到有新的数据到来。我们可以在消费者客户端设置指定字节数量,表示在消息代理在还没有收集到足够的数据时,客户端的拉取请求不会立即返回。
Kafka的每个Broker在分区的层面上互为备份。本分副本始终尽量保持与主副本的数据同步。备份副本的日志文件和主副本的日志总是相同的,它们都有相同的偏移量和相同顺序的消息。备份副本从主副本消费消息的方式和普通消费者一样,只不过备份副本会将处理写入到本地日志文件。
分布式系统处理故障容错时,需要明确定义节点是否处于存活状态。Kafkaf对接点的存货定义有两个条件:
满足这两个条件,叫作"正在同步中"(in-sync)。每个分区的主副本会跟踪正在同步中的备份副本节点(In Sync Replicas,ISR)。如果一个备份副本挂掉、没有响应或者落后太多,主副本会将其从同步副本集合中移除。反之副本重新赶上主副本,它就会被重新加入集合中。
在Kafka中,一条消息只有被ISR集合中的所有副本都运用到本地的日志文件,才会认为消息被成功提交了。任何时刻,只要ISR至少有一个副本是存活的,Kafka就可以保证消息被提交就不会丢失。