一、Kafka概念及特性

1. 概念

Kafka是一种高吞吐量的分布式发布订阅消息系统。可以用于搜索日志,监控日志,访问日志等。Kafka将消息以topic为单位进行归纳。Kafka层采用无缓存设计,而是依赖于底层的文件系统页缓存。

Kafka将向Kafka topic发布消息的程序称为producers,将预订topics并消费消息的程序称为consumer,以集群的方式运行,可以由一个或多个服务(server)组成,每个服务叫做一个broker。

producers通过网络将消息发送到Kafka集群,集群向消费者提供消息。

kafka集群、producer、consumer都依赖于zookeeper来保证系统可用性集群保存一些meta信息。
Kafka 可以处理大量的数据,并使能够将消息从一个端点传递到另一个端点,kafka适合离线和在线消息消费。

kafka直接将消息保留在磁盘上,不用保存到内存,并在集群内复制以防止数据丢失。kafka通过“预读”和“后写”加快磁盘访问。

kafka构建在zookeeper同步服务之上。它与apache和spark非常好的集成,应用于实时流式数据分析。

2. 数据存储于日志

Kafka每个程序都在自己的线程里只缓存了一份数据,但在操作系统的缓存里还有一份,这等于存了两份数据。

JVM局限性:

•Java对象占用空间是非常大的,差不多是要存储的数据的两倍甚至更高。

•随着堆中数据量的增加,垃圾回收回变的越来越困难。

基于以上分析,如果把数据缓存在内存里,因为需要存储两份,使用两倍的内存空间,Kafka基于JVM,又将空间再次加倍,再加上要避免GC带来的性能影响,在一个32G内存的机器上,不得不使用到28-30G的内存空间。并且当系统重启的时候,又必须要将数据刷到内存中( 10GB 内存差不多要用10分钟),就算使用冷刷新(不是一次性刷进内存,而是在使用数据的时候没有就刷到内存)也会导致最初的时候新能非常慢。但是使用文件系统,即使系统重启了,也不需要刷新数据。使用文件系统也简化了维护数据一致性的逻辑。

所以与传统的将数据缓存在内存中然后刷到硬盘的设计不同,Kafka直接将数据写到了文件系统的日志中。

3. Kafka有如下特性:

  • 通过O(1)的磁盘数据结构提供消息的持久化,这种结构对于即使数以TB的消息存储也能够保持长时间的稳定性能
  • 高吞吐量:即使是非常普通的硬件kafka也可以支持每秒数十万的消息。
  • 支持通过kafka服务器和消费机集群来分区消息。
  • 支持Hadoop并行数据加载。

二. Kafka应用场景

1、指标分析

kafka通常用于操作监控数据。用于接收、聚合来自多种应用程序的统计信息, 以便于向生产环境中的数据集中反馈数据

2、日志聚合解决方法

kafka可用于跨组织从多个服务器收集日志,并使他们以标准的合适提供给多个服务器。

3、流式处理

流式处理框架(spark,storm,flink)从主题中读取数据,对齐进行处理,并将处理后的数据写入新的主题,供用户和应用程序使用,kafka的强耐久性在流处理的上下文中也非常的有用。

三. Kafka架构

kafka验证失败 kafka has no active member_kafka


kafka验证失败 kafka has no active member_缓存_02


kafka验证失败 kafka has no active member_缓存_03

四. Kafka组件及其作用

  1. Broker
    kafka集群中包含一个或者多个服务实例,这种服务实例被称为Broker。通俗来说,每一台机器叫一个Broker
  2. Producer
    日志消息生产者,用来写数据,负责发布消息到kafka集群的Broker中。。
  • Producer将消息发布到它指定的topic中,并负责决定发布到哪个分区。通常简单的由负载均衡机制随机选择分区,但也可以通过特定的分区函数选择分区。使用的更多的是第二种。
  • producer直接将数据发送到broker的leader(主节点),不需要在多个节点进行分发。为了帮助producer做到这点,所有的Kafka节点都可以及时的告知:哪些节点是活着的,目标topic目标分区的leader在哪。这样producer就可以直接将消息发送到目的地了。
    客户端控制消息将被分发到哪个分区。可以通过负载均衡随机的选择,或者使用分区函数。Kafka允许用户实现分区函数,指定分区的key,将消息hash到不同的分区上(当然有需要的话,也可以覆盖这个分区函数自己实现逻辑).比如如果你指定的key是user id,那么同一个用户发送的消息都被发送到同一个分区上。经过分区之后,consumer就可以有目的的消费某个分区的消息。
  • 异步发送
    批量发送可以很有效的提高发送效率。Kafka producer的异步发送模式允许进行批量发送,先将消息缓存在内存中,然后一次请求批量发送出去。这个策略可以配置的,比如可以指定缓存的消息达到某个量的时候就发出去,或者缓存了固定的时间后就发送出去(比如100条消息就发送,或者每5秒发送一次)。这种策略将大大减少服务端的I/O次数。
    既然缓存是在producer端进行的,那么当producer崩溃时,这些消息就会丢失。Kafka0.8.1的异步发送模式还不支持回调,就不能在发送出错时进行处理。
  1. Consumer
    消息的消费者,用来读数据,向kafka的broker中读取消息的客户端。
  • 实际上每个consumer唯一需要维护的数据是消息在日志中的位置,也就是offset.这个 offset由consumer来维护(i.e. kafka无状态,消费者自己维护以消费的状态信息) :一般情况下随着consumer不断的读取消息,这offset的值不断增加,但其实consumer可以以任意的顺序读取消息,比如它可以将offset设置成为一个旧的值来重读之前的消息。
  • consumers非常的轻量级:它们可以在不对集群和其他consumer造成影响的情况下读取消息。你可以使用命令行来"tail"消息而不会对其他正在消费消息的consumer造成影响。
  • Kafa consumer消费消息时,向broker发出"fetch"请求去消费特定分区的消息 consumer指定消息在日志中的偏移量(offset),就可以消费从这个位置开始的消息。customer拥有了offset的控制权,可以向后回滚去重新消费之前的消息
  • 发布消息通常有两种模式:队列模式(queuing)和发布-订阅模式(publish-subscribe)。队列模式中,consumers可以同时从服务端读取消息,每个消息只被其中一个consumer读到;发布-订阅模式中消息被广播到所有的consumer中。Consumers可以加入一个consumer 组,共同竞争一个topic,topic中的消息将被分发到组中的一个成员中。同一组中的consumer可以在不同的程序中,也可以在不同的机器上。如果所有的consumer都在一个组中,这就成为了传统的队列模式,在各consumer中实现负载均衡。如果所有的consumer都不在不同的组中,这就成为了发布-订阅模式,所有的消息都被分发到所有的consumer中。更常见的是,每个topic都有若干数量的consumer组,每个组都是一个逻辑上的“订阅者”,为了容错和更好的稳定性,每个组由若干consumer组成。这其实就是一个发布-订阅模式,只不过订阅者是个组而不是单个consumer 。同一个组中的消费者对于同一条消息只消费一次。因此,同一个消费组中某一个topic的某一个partion只会被一个消费者消费
  1. Topic
    每条发布到kafka集群的消息都有一个类别,这个类别就叫做Topic。每个topic是对一组消息的归纳。
  • 不同消费者去指定的Topic中读,不同的生产者往不同的Topic中写。
  • 在一个时间段内(可通过配置修改),Kafka集群保留所有发布的消息,不管这些消息有没有被消费。一般的消息系统都是在消息被消费后立即删除,Kafka却可以将消息保存一段时间。比如,如果消息的保存策略被设置为2天,那么在一个消息被发布的两天时间内,它都是可以被消费的。
  1. Partition
    Partition在Topic基础上做了进一步区分分层。Partition是一个物理上的概念,每个Topic包含一个或者多个Partition。每个分区都是一个并行单元。Topic的每个Partition对应一个逻辑日志。一个partition内部是有序的,但全局多个partition之间无法确定有序
  • 每个分区都由一系列有序的、不可变的消息组成,这些消息被连续的追加到分区中。分区中的每个消息都有一个连续的序列号叫做offset,用来在分区中唯一的标识这个消息。
  • 将日志分区可以达到以下目的:首先这使得每个日志的数量不会太大,可以在单个服务上保存。另外每个分区可以单独发布和消费,为并发操作topic提供了一种可能。
  • 注意consumer组的数量不能多于分区的数量,也就是有多少分区就允许最多多少并发消费。
  • topic的Partition数量和每个partition副本数量在创建topic时配置。
    Partition数量决定了每个Consumer group中并发消费者的最大数量。
    例如:Consumer group A 有两个消费者来读取4个partition中数据;
    Partition数量决定了每个Consumer group中并发消费者的最大数量(效率最高的情况)。
    Partition = 并发度: 刚刚好,效率最高
    Partition > 并发度:有部分消费者消费多个分区的数据。
    Partition < 并发度 :有部分消费者闲置(任意时刻一个分区内的一条数据只能被消费组中的一个消费者消费)
    一个partition同一时间只能服务一个consumer,也就是,kafka中一个partition在被一个consumer消费时,是不允许其他consumer消费该partition的。
    总结:一个topic可以属于多个消费组(一对多)。topic的一个partition只能被一个consumer消费,一个consumer可以消费多个不同topic的partition(多对一)。数据流图如下
  1. segment
    一个partition当中存在多个segment文件段,每个segment分为两部分,.log文件和.index文件,其中.log文件包含了我们发送的数据存储,.index文件,记录的是我们.log文件的数据索引值,以便于我们加快数据的查询速度。其中.index文件是索引文件,主要用于快速查询.log文件当中数据的偏移量位置,二分查找+顺序遍历

索引文件中元数据指向对应数据文件中message的物理偏移地址

比如:索引文件中3,497代表:数据文件中的第三个message,它的偏移地址为497。再来看数据文件中,Message 368772表示:在全局partiton中是第368772个message。

注:segment index file采取稀疏索引存储方式,它减少索引文件大小,通过map可以直接内存操作,稀疏索引为数据文件的每个对应message设置一个元数据指针,它比稠密索引节省了更多的存储空间,但查找起来需要消耗更多的时间。

kafka验证失败 kafka has no active member_kafka_04

五、Kafka副本管理

1. 多副本

每个partition分区在Kafka集群的若干服务中都有副本,这样这些持有副本的服务可以共同处理数据和请求,副本数量是可以配置的。副本使Kafka具备了容错能力。

2. 1个leader 和 多个follower

每个分区都由一个服务器作为“leader”,零或若干服务器作为“followers”,leader负责处理消息的读和写,followers则去复制leader。如果leader down了,followers中的一台则会自动成为leader。集群中的每个服务都会同时扮演两个角色:作为它所持有的一部分分区的leader,同时作为其他分区的followers,这样集群就会据有较好的负载均衡。

3. 副本存放规则

(topic的Partition数量和每个partition副本数量在创建topic时配置。)
假设有n个broker,一个topoic包含5个partition,每个partition都有3个replication,则kafka分配replication的规则为:
将i号partition的j号副本放置在 (i+j)% n 这个broker上。

4. 可靠性保证

为保证producer发送的数据,能可靠的到达指定的Topic,topic的每个partition收到producer收到消息后,都需要向producer发送ack确认收到,如果producer收到ack,就会进行下一轮的发送,否则重新发送数据。如下图:

kafka验证失败 kafka has no active member_数据_05

两种**副本同步策略**(半数以上同步、全同步)及优缺点对比

kafka验证失败 kafka has no active member_kafka验证失败_06

Kafka选择全同步方案,原因如下:

a) 同样为了容忍 n 台节点的故障,第一种方案需要 2n+1 个副本,而第二种方案只需要 n+1个副本,而 Kafka 的每个分区都有大量的数据,第一种方案会造成大量数据的冗余。

b) 虽然第二种方案的网络延迟会比较高,但网络延迟对 Kafka 的影响较小。

Kafka通过配置来指定producer生产者在发送消息时的***ack策略(三种可靠性等级)***:

Request.required.acks=-1 (全量同步确认,强可靠性保证);
Request.required.acks = 1(leader 确认收到, 默认);
Request.required.acks = 0 (无需ack,最低时延但不能保证可靠,brocker故障时会丢失数据)。

5. AR, ISR, OSR

分区中的所有副本统称为AR(Assigned Repllicas)。所有与leader副本保持一定程度同步的副本(包括Leader)组成ISR(In-Sync Replicas),ISR集合是AR集合中的一个子集。消息会先发送到leader副本,然后follower副本才能从leader副本中拉取消息进行同步,同步期间内follower副本相对于leader副本而言会有一定程度的滞后。前面所说的“一定程度”是指可以忍受的滞后范围,这个范围可以通过参数进行配置。与leader副本同步滞后过多的副本(不包括leader)副本,组成OSR(Out-Sync Relipcas),由此可见:AR=ISR+OSR。在正常情况下,所有的follower副本都应该与leader副本保持一定程度的同步,即AR=ISR,OSR集合为空。

Leader副本负责维护和跟踪ISR集合中所有的follower副本的滞后状态,当follower副本落后太多或者失效时,leader副本会吧它从ISR集合中剔除。如果OSR集合中follower副本“追上”了Leader副本,之后再ISR集合中的副本才有资格被选举为leader,而在OSR集合中的副本则没有机会(这个原则可以通过修改对应的参数配置来改变)
ISR的伸缩:

Kafka在启动的时候会开启两个与ISR相关的定时任务,名称分别为“isr-expiration"和”isr-change-propagation".。isr-expiration任务会周期性的检测每个分区是否需要缩减其ISR集合。这个周期和“replica.lag.time.max.ms”参数有关。大小是这个参数一半。默认值为5000ms,当检测到ISR中有是失效的副本的时候,就会缩减ISR集合。如果某个分区的ISR集合发生变更, 则会将变更后的数据记录到ZooKerper对应/brokers/topics//partition//state节点中。节点中数据示例如下:

{“controller_cpoch":26,“leader”:0,“version”:1,“leader_epoch”:2,“isr”:{0,1}}

其中controller_epoch表示的是当前的kafka控制器epoch。leader表示当前分区的leader副本所在的broker的id编号,version表示版本号,(当前版本固定为1),leader_epoch表示当前分区的leader纪元,isr表示变更后的ISR列表。

六、消息传输的事务定义

之前讨论了consumer和producer是怎么工作的,现在来讨论一下数据传输方面。数据传输的事务定义通常有以下三种级别(消息投递策略)(producer指定)

  • at most once: 消息不会被重复发送,最多被传输一次,但也有可能一次不传输。有丢数据的风险
  • at least once(默认): 消息不会被漏发送,最少被传输一次,但也有可能被重复传输.
  • Exactly once: 不会漏传输也不会重复传输,每个消息都传输被一次而且仅仅被传输一次,这是最理想的,靠客户端维护,事务保证。

大多数消息系统声称可以做到“精确的一次”,但是仔细阅读它们的的文档可以看到里面存在误导,比如没有说明当consumer或producer失败时怎么样,或者当有多个consumer并行时怎么样,或写入硬盘的数据丢失时又会怎么样。kafka的做法要更先进一些。当发布消息时,Kafka有一个“committed”的概念,一旦消息被提交了,只要消息被写入的分区的所在的副本broker是活动的,数据就不会丢失。

如果producer发布消息时发生了网络错误,但又不确定实在提交之前发生的还是提交之后发生的,这种情况虽然不常见,但是必须考虑进去,现在Kafka版本还没有解决这个问题,将来的版本正在努力尝试解决。

并不是所有的情况都需要“精确的一次”这样高的级别,Kafka允许producer灵活的指定级别。比如producer可以指定必须等待消息被提交的通知,或者完全的异步发送消息而不等待任何通知,或者仅仅等待leader声明它拿到了消息(followers没有必要)。

现在从consumer的方面考虑这个问题,所有的副本都有相同的日志文件和相同的offset,consumer维护自己消费的消息的offset,如果consumer不会崩溃当然可以在内存中保存这个值,当然谁也不能保证这点。如果consumer崩溃了,会有另外一个consumer接着消费消息,它需要从一个合适的offset继续处理。这种情况下可以有以下选择:

  • consumer可以先读取消息,然后将offset写入日志文件中,然后再处理消息。这存在一种可能就是在存储offset后还没处理消息就crash了,新的consumer继续从这个offset处理,那么就会有些消息永远不会被处理,这就是上面说的“at most once最多一次
  • consumer可以先读取消息,处理消息,最后记录offset,当然如果在记录offset之前就crash了,新的consumer会重复的消费一些消息,这就是上面说的 “at least once最少一次”
  • “精确一次”可以通过将提交分为两个阶段来解决:保存了offset后提交一次,消息处理成功之后再提交一次。但是还有个更简单的做法:将消息的offset和消息被处理后的结果保存在一起。比如用Hadoop ETL处理消息时,将处理后的结果和offset同时保存在HDFS中,这样就能保证消息和offser同时被处理了。

七、消费组内分区分配策略

Kafka consumer为了及时消费消息,会以Consumer Group(消费组)的形式,启动多个consumer消费消息。不同的消费组在消费消息时彼此互不影响,同一个消费组的consumer协调在一起消费订阅的topic所有分区消息。这就引申一个问题:消费组中的consumer是如何确定自己该消费哪些分区的数据的?

Kafka提供了多种分区策略如RoundRobin(轮询)、Range(按范围),可通过参数partition.assignment.strategy进行配置。

Range 策略

range (默认分配策略)对应的实现类是org.apache.kafka.clients.consumer.RangeAssignor 。

首先,将分区按数字顺序排行序,消费者按名称的字典序排序。
然后,用分区总数除以消费者总数。如果能够除尽,平均分配;若除不尽,则位于排序前面的消费者将多负责一个分区。

然后为每个consumer划分固定的分区范围,如果不够平均分配,那么排序靠前的消费者会被多分配分区。具体就是将partition的个数除于consumer线程数来决定每个consumer线程消费几个分区。如果除不尽,那么前面几个消费者线程将会多分配分区。

通过下面公式更直观:

假设n = 分区数 / 消费者数量,m = 分区数 % 消费者线程数量,那么前m个消费者每个分配n+1个分区,后面的(消费者线程数量 - m)个消费者每个分配n个分区

kafka验证失败 kafka has no active member_kafka验证失败_07

RoundRobin

RoundRobin策略的工作原理:将所有topic的partition组成TopicAndPartition列表,然后对TopicAndPartition列表按照hashCode进行排序:

kafka验证失败 kafka has no active member_kafka_08

val allTopicPartitions = ctx.partitionsForTopic.flatMap { case(topic, partitions) =>
  info("Consumer %s rebalancing the following partitions for topic %s: %s"
       .format(ctx.consumerId, topic, partitions))
  partitions.map(partition => {
    TopicAndPartition(topic, partition)
  })
}.toSeq.sortWith((topicPartition1, topicPartition2) => {
  /*
   * Randomize the order by taking the hashcode to reduce the likelihood of all partitions of a given topic ending
   * up on one consumer (if it has a high enough stream count).
   */
  topicPartition1.toString.hashCode < topicPartition2.toString.hashCode
})

八、rebalace触发场景

  1. 有Consumer加入或退出Consumer Group;
  2. Consumer Group订阅的topic分区发生变化如新增分区

九、性能优化

Kafka在提高效率方面做了很大努力。Kafka的一个主要使用场景是处理网站活动日志,吞吐量是非常大的,每个页面都会产生好多次写操作。读方面,假设每个消息只被消费一次,读的量的也是很大的,Kafka也尽量使读的操作更轻量化。

我们之前讨论了磁盘的性能问题,线性读写的情况下影响磁盘性能问题大约有两个方面:太多的琐碎的I/O操作和太多的字节拷贝。I/O问题发生在客户端和服务端之间,也发生在服务端内部的持久化的操作中。

1. 消息集(message set)

为了避免这些问题,Kafka建立了“消息集(message set)”的概念,将消息组织到一起,作为处理的单位。以消息集为单位处理消息,比以单个的消息为单位处理,会提升不少性能。Producer把消息集一块发送给服务端,而不是一条条的发送;服务端把消息集一次性的追加到日志文件中,这样减少了琐碎的I/O操作。consumer也可以一次性的请求一个消息集。

另外一个性能优化是在字节拷贝方面。在低负载的情况下这不是问题,但是在高负载的情况下它的影响还是很大的。为了避免这个问题,Kafka使用了标准的二进制消息格式,这个格式可以在producer,broker和producer之间共享而无需做任何改动。

2. zero copy

Broker维护的消息日志仅仅是一些目录文件,消息集以固定队的格式写入到日志文件中,这个格式producer和consumer是共享的,这使得Kafka可以一个很重要的点进行优化:消息在网络上的传递。现代的unix操作系统提供了高性能的将数据从页面缓存发送到socket的系统函数,在linux中,这个函数是sendfile。Kafka层采用无缓存设计,而是依赖于底层的文件系统页缓存

为了更好的理解sendfile的好处,我们先来看下一般将数据从文件发送到socket的数据流向:

kafka验证失败 kafka has no active member_kafka验证失败_09

  • 操作系统把数据从文件拷贝内核中的页缓存中
  • 应用程序从页缓存从把数据拷贝自己的内存缓存中
  • 应用程序将数据写入到内核中socket缓存中
  • 操作系统把数据从socket缓存中拷贝到网卡接口缓存,从这里发送到网络上。

这显然是低效率的,有4次拷贝和2次系统调用。Sendfile通过直接将数据从页面缓存发送网卡接口缓存,避免了重复拷贝,大大的优化了性能。zero-copy减少kernel与user模式的上下文切换,直接把disk上的data传输给socket,而不需要通过应用程序传输.

kafka验证失败 kafka has no active member_kafka验证失败_10

在一个多consumers的场景里,数据仅仅被拷贝到页面缓存一次而不是每次消费消息的时候都重复的进行拷贝。这使得消息以近乎网络带宽的速率发送出去。这样在磁盘层面你几乎看不到任何的读操作,因为数据都是从页面缓存中直接发送到网络上去了。
这篇文章详细介绍了sendfile和zero-copy技术在Java方面的应用。

十、kafka的优点

  1. 可靠性:

分布式的,分区,复制和容错。

  1. 可扩展性:

kafka消息传递系统轻松缩放,无需停机。

  1. 耐用性:

kafka使用分布式提交日志,这意味着消息会尽可能快速的保存在磁盘上,因此它是持久的。

  1. 性能:

kafka对于发布和定于消息都具有高吞吐量。即使存储了许多TB的消息,他也爆出稳定的性能。

  1. kafka非常快:

保证零停机和零数据丢失。