一、概念理解
Kafka 是最初由Linkedin 公司开发,**是一个分布式、支持分区的(patition)、多副本的(replica)、**基于zookeeper协调的分布式消息系统,它的最大的特性就是可以实时的处理大量数据以满足各种需求场景。
1、Kafka的特性
- 高吞吐量、低延迟: Kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒。
- 可扩展性: kafka集群支持热扩展。
- **持久性,可靠性:**消息被持久化到本地磁盘,并且支持数据备份防止数据丢失。
- 容错性 :允许集群中节点失败(若副本数量为n, 则允许n-1个节点失败)
- 高并发:支持数千个客户端同时读写
2、kafka应用场景
- 日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,例如hadoop、Hbase、Solr等。
- 消息系统: 解耦和生产者和消费者、缓存消息等。
- **用户活动跟踪:**Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖掘
- 运营指标: Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告
- 流事处理:比如spark streaming和storm
- 事件源
3、Kafka一些重要设计思想
- ConsumerGroup: 各个Consumer可以组成一个组,每个消息只能被组中的一个consumer消费,如果一个消息可以被多个consumer消费的话,那么这些consumer必须在不同的组。
- 消息状态:在kafka中,消息的状态被保存在consumer中,broker不会关心哪个消息被消费了或者被谁消费了,只记录一个offset值(指向partition中下一个被消费的消息位置),这就意味着如果consumer处理不好的话,broker上的一个消息可能会被次消费。
- 消息持久化:kafka中会把消息持久化到本地文件系统中,并且保持极高的效率。
- 消息有效期:Kafka会长久保留其中的消息,以便consumer可以多次消费,当然其中可以有很多细节设置。
- 批量发送: Kafka支持以消息集合为单位进行批量发送,以提高push效率。
- push-all-pull:Kafka中的Producer和consumer采用的是push-all-pull模式,即Producer只管向broker push消息,consumer只管从broker pull 取消息,两者对消息的生产和消费是异步的。
- **Kafka集群中broker之间的关系:**不是主从关系,各个broker在集群中地位是一样的,我们可以随意的增加或删除任何一个broker节点。
- 负载均衡方面:Kafka提供了一个metadata API 来管理broker之间的负载(对kafka0.8x 而言,对于0.7x 而言主要是靠zookeeper来实现负载均衡的)。
- 同步异步:Producer 采用异步push方式,极大提高kafka系统的吞吐率(可以通过参数控制是采用同步或异步方式)。
- 分区机制Partition:Kafka的broker端支持消息分区,producer可以决定把消息发到那个分区,在一个分区中消息的顺序就是producer发送消息的顺序。一个主题(topic)可以有多个分区,具体分区的数量是可配置的。但是同一类型的消息只能在一个分区中,不可以同一类型的消息在不同的分区。 producer,consumer 只能在主的Partition节点上进行读或写。
- 离线数据装载:Kafka由于对可扩展的数据持久化的支持,它也非常适合向Hadoop或者数据仓库中进行数据装载。
二、消费模式
1、点对点模式
如上图所示,点对点模式通常是基于拉取或轮询的消息传递模型,这个模型的特点是发送到队列的消息被一个且只有一个消费者进行处理。生产者将消息放入消息队列后,由消费者主动的去拉取消息进行消费。点对点模型的优点是消费者拉取消息的频率可以由自己控制,但是消费队列是否有消息需要消费,在消费端是无发感知的,所以在消费者端需要额外的线程去监控。
2、发布订阅模式
如上图所示,发布订阅模式是一个基于消息发送的消息传递模型,该模型可以有多种不同的订阅者。生产者将消息放入消息队列后,队列会将消息推送给订阅过该类消息的消费者。由于是消费者被动接收推送,所以无需感知消息队列是否有待消费的消息!
但是consumer1,consumer2,consumer3 由于机器性能不一样,所以处理消息的能力也会不一样,但消息队列却无法感知消费者消费的速度! 所以推送的速度成了发布订阅模式的一个问题! 假设三个消费者处理速度分别是 8M/s , 5M/s , 2M/s ,如果队列推送的速度为5M/s , 则consumer3 无法承受,如果队列推送的速度为 2M/s ,则consumer1,consumer2 会出现大量资源极大的浪费。
三、架构模型
1、基础架构与名词解释
- Producer: Producer 是生产者,消费的产生着,是消息的入口。
- Broker:Broker是kafka的实例,每个服务器上有一个或多个kafka实例。如果站在java开发的角度,我们可以将Broker理解为JVM进程,每个kafka集群内的broker都有一个不重复的编号,如上图的broker-0,broker-1等。
- topic:消息的主题,逻辑概念,可以理解为消息的分类。在每个broker上都可以创建多个topic。
- Partition:Topic的分区,每个topic可以有多个分区,分区的作用是做负载,提供kafka的吞吐量,而kafka的数据就是保存在topic的不同的分区上的。同一个topic在不同的分区的数据是不重复的。 无关的数据必然分治,有关联的数据必然聚合。 就是我们在处理业务数据的时候,假如这些数据没有前后关系和顺序关系的话,为了加快消费的数据,可以将他们分散到不同的分区中。 而有关联的数据必然要按原有的发送顺序放到一个分区中。
- **Replication:**每一个分区都有多个副本,副本的作用是做备胎的。当主分区(Leader)故障的时候会选择一个备胎(Follower)上位,成为Leader,在kafka中默认副本的最大数量是10个,且副本的数量不能大于Broker的数量,follower和leader绝对是在不同的机器上,同一个机器对同一个分区也只能存放一个副本(包括自己)。
- **Message:**每一条发送的消息体。
- **Consumer:**消费者,即消息的消费方,是消息的出口。
- Consumer Group:我们可以将多个消费者组成一个消费组,在kafka的设计中同一个分区的数据只能被消费组中的某一个消费者消费。同一个消费者组的消费者可以消费同一个topic的不同分区的数据,这也是为了提高kafka的吞吐量。
- zookeeper: kafka集群依赖zookeeper来保存集群的元信息,来保证系统的可用性。
2、工作流程
1.1 producer发送消息
Kafka中消息是以topic进行分类的,生产者生产消息,消费者消费者消息,都是面向topic的。
topic是逻辑概念,而partition是物理上的概念,每个partition对应于一个log文件,该log文件中存储的就是producer生产的数据。Producer上产的数据会被不断追加到该log文件末尾,且每条数据都有自己的offset。消费组中的每个消费者,都会实时记录自己消费到那个offset,以便出错回复时从上次的位置继续消费。
我们上面的架构图中,produce是生产者,是数据的入口,Producer 在写入数据的时候永远的找leader,不会直接将数据写入follower。
流程:
1、先从集群获取分区的leader。因为producer写入数据只能操作partition leader节点。
2、producer 将消息发送给broker-topic-partition-leader节点。如果 ack = 0, 则producer发送完数据后,broker直接返回ack。
3、leader将消息写入本地文件。如果 ack = 1(kafka默认),即发送到partition-leader分区写入磁盘返回ack
4、follower副本从leader pull消息。 如果ack=-1,即所有ISR过半 返回ack。
可靠性ack 问题后面我们细说。
producer 采用push模式将数据发布到broker,每条消息追加到分区中,顺序写入磁盘,所以保证同一分区内的数据是有序的! 而对于多个producer的socket连接发送数据是没有办法保证顺序的 写入示意图:
分区的主要目的:
- 方便扩展: 因为topic可以有多个partition,所以我们可以通过扩展器去轻松的应对日益增长的数据量。
- **提高并发:**以partition为读写单位,可以多个消费者同时消费数据,提高了消息的处理效率。
熟悉负载均衡的我们都知道,当我们向某个服务器发送请求的时候,服务端可能会对请求做一个负载,将流量分发到不同的服务器,那么在kafka中,如果某个topic有多个partition,producer又怎么知道将数据发送到哪个partition呢?
kafka中有几个原则:
- partition在写入的时候可以指定需要写入partition,如果有指定,则写入对应的partition。
- 如果没有指定partition,但是设置了数据的key,则会根据key的值hash出一个partition。
- 如果即没有指定partition,又没有设置key, 则会轮询出一个partition。
我们的原则是:
1、对于没有key 只有消息Value,如果没有顺序上的约束的话,我们就水平扩展,将消息分发到不同的partition分区。
2、一旦消息(消息很多,但是消息种类一定多),而且需要同一类消息的有序性,消息是K, V,相同的key一定去到一个分区里的,因为broker会保证producer发送的消息的顺序。一个分区可能有不同的key,且不同的key是交叉的,相同的key在一个分区里没有排列在一起。
1.2 producer可靠性
我们以单节点分析:
** 为保证producer发送的数据,能可靠的发送到指定的topic,** topic的每个partition收到producer发到的数据后,都需要向producer发送ack(确认收到),如果producer收到ack,就会进行下一轮的发送,否则重新发送数据。
ack参数配置:
0 : producer 不等待broker的ack,这一操作提供了一个最低的延迟,broker一接收还没有写入磁盘就已经返回,也就是producer发送完数据 立马发送下一条记录,当broker故障时有可能会丢数据。
1 : producer等待broker的ack,partition的leader落盘成功后返回ack,如果follower同步成功之前leader故障了,那么将会丢失数据。 kafka默认的参数。
-1(all):producer等待broker的ack,partition的leader和follower全部落盘(ISR内的partition副本)成功后才返回ack。但是如果在 follower同步完成后,broker发送ack之前,leader发生故障,那么会造成数据重复的消费。
我们从上图也可以清晰的看到,ack=1和ack=0时,什么时候触发ack的。对于ack=-1,我们下面详细的分析:
对于一致性:
1、强一致性,所有节点必须全部存活,一致性破坏了可用性。
2、最终一致性,【网络到分布式】,过半通过,最常用的分布式一致性解决方案。
对于kafka采用了第二种方式,在最终一致性上进行改造。
我们先了解几个概念:
ISR(in-sync-replicas): 连通性 & 活跃性。Leader维护了一个动态的ISR,意为和leader保持同步的follower集合。如果follower长时间 未向leader同步数据,则该follower会被剔出ISR,该时间阈值由replica.lag.time.max.ms 参数设定,Leader发生故障,就会从ISR 中选举新的leader。
OSR(outof-sync-replicas): 超过阈值时间10s 没有心跳的副本。
AR(Assigned replicas):面向分区的副本集合,创建topic的时候你给出了分区的副本数,那么controller在创建的时候就已经分配了 broker和分区的对应关系,并得到了该分区的broker集合。
AR = ISR + OSR;
当ack = -1 时,要求所有的存活的副本(ISR)都要同步一致。
上图过程:
1、假如一个partition-0 leader 有2个副本。一开始它们都是存活的,所以ISR包含这个三个节点。
2、过了一段时间(10s),其中的一个副本没有向leader节点同步消息或者是心跳超过10s断开了,那么leader节点会将这个副本给剔除。 所以这个副本就会被假如到 OSR集合中。
3、当producer发送一条消息5,到达leader partition节点后,follower副本会去同步消息,如果ISR所有副本都同步完成,那么leader partition会给producer返回ack, 表示这个消息被持久化完成了。
1.3 故障处理
LEO: 指的每个副本最大的offset。
HW:指的是消费者见到的最大的offset,ISR队列中最小的LEO。
① follower故障
follower发生故障后会被剔出临时ISR,待该follower恢复后,follower会读取本地磁盘记录的上次的HW,并将log文件高于HW的部分截取掉,从HW开始向leader进行同步。等该follower的LEO 大于等于该partition的HW,即follower追上了leader之后,就可以重新加入ISR了。
② leader故障
leader发生故障之后,会从ISR中选出一个新的leader之后,为保证多个副本之间的数据一致性,其余的follower会先将各自的log文件高于HW的部分截取掉,然后从新的leader同步数据。
注意:这只能保证副本之间数据的一致性,并不能保证数据不丢失或者重复消费。
1.4 幂等性
将服务器ack的级别设为 -1 ,可以保证Producer到server之间不丢失数据,即 At Least Once语义。
kafka0.11版本引入了 幂等性; 所谓的幂等性就是不管Producer 向server发送多少重复的数据,server端只持久化一条。
要启用幂等性,只需要将Producer的参数中enable.idompotence 设置为 true 即可。开启幂等性的producer在初始化的时候会被分配一个PID,发往同一Parititon 的消息会附带 Sequence Number。而broker端会对 <PID , Partition, Sequence> 做缓存,当具有相同主键的消息提交时,broker只会持久化一条。
但是PID重启会发生变化,同时不同的Partition也具有不同的主键,所以幂等性无法保证跨分区会话的完全唯一。
2.1 保存数据
(1)顺序写入磁盘
Kafka 的 producer 生产数据,要写入到 log 文件中,写的过程是一直追加到文件末端, 为顺序写。官网有数据表明,同样的磁盘,顺序写能到 600M/s,而随机写只有 100K/s。这 与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。
(2)零拷贝sendfile
为了理解sendfile的影响,需要理解一般的将数据从文件传到socket的路径:
- 操作系统将数据从磁盘读到内核空间的页缓存中
- 应用将数据从内核空间读到用户空间的缓存中
- 应用将数据写回内核空间的socket缓存中
- 操作系统将数据从socket缓存写到网卡缓存中,以便将数据经网络发出
这种操作方式明显是非常低效的,这里有四次拷贝,两次系统调用。如果使用sendfile,就可以避免两次拷贝:操作系统将数据直接从页缓存发送到网络上。所以在这个优化的路径中,只有最后一步将数据拷贝到网卡缓存中是需要的。
我们期望一个主题上有多个消费者是一种常见的应用场景。利用上述的zero-copy,数据只被拷贝到页缓存一次,然后就可以在每次消费时被重得利用,而不需要将数据存在内存中,然后在每次读的时候拷贝到内核空间中。这使得消息消费速度可以达到网络连接的速度。这样以来,通过页面缓存和sendfile的结合使用,整个kafka集群几乎都已以缓存的方式提供服务,而且即使下游的consumer很多,也不会对整个集群服务造成压力。
零拷贝 当 consumer消费者发送读数据到broker时,broker会请求内核读数据,内核返回的数据的不用返回到 broker应用端 ,而是直接返回给consumer,这样就减少了内核数据返回给broker应用程序的过程。
2.2 Partition 结构
前面说过了每个topic都可以分为一个或多个partition,如果你觉得topic比较抽象,那partition就是比较具体的东西了!Partition在服务器上的表现形式就是一个一个的文件夹,每个partition的文件夹下面会有多组segment文件,每组segment文件又包含.index文件、.log文件、.timeindex文件(早期版本中没有)三个文件, log文件就实际是存储message的地方,而index和timeindex文件为索引文件,用于检索消息。
如上图,这个partition有三组segment文件,每个log文件的大小是一样的,但是存储的message数量是不一定相等的(每条的message大小不一致)。文件的命名是以该segment最小offset来命名的,如000.index存储offset为0~368795的消息,kafka就是利用分段+索引的方式来解决查找效率的问题。
2.3 offset 的维护
由于Consumer 在消费过程中可能会出现断电或宕机等故障,consumer恢复后,需要从故障前的位置继续消费,所以consumer需要实时记录自己消费了那个offset,以便故障恢复后继续消费。
KafKa0.9版本之前,consumer默认将 offset 保存在Zookeeper中,从0.9版本之后,consumer 默认将offset保存在kafka一个内置的topic中,该topic 为 _consumer_offset.
2.4 Zookeeper作用
Kafka集群中有一个broker会被选举为Controller,负责管理集群broker的上下线,所以topic的分区副本分配和leader选举等工作。
Controller 的管理工作都是依赖于Zookeeper的。
以下为partition的leader选举过程:
3.1Consumer 消费消息
consumer 采用 pull(拉)模式从 broker 中读取数据。
push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由 broker 决定的。
**pull 模式不足之处是,如果 kafka 没有数据,消费者可能会陷入循环中,一直返回空数 据。**针对这一点,Kafka 的消费者在消费数据时会传入一个时长参数 timeout,如果当前没有 数据可供消费,consumer 会等待一段时间之后再返回,这段时长即为 timeout。
多个消费者可以组成一个消费者组(consumer group),每个消费者组都有一个组id!同一个消费组者的消费者可以消费同一topic下不同分区的数据,但是不会组内多个消费者消费同一分区的数据!
图示是消费者组内的消费者小于partition数量的情况,所以会出现某个消费者消费多个partition数据的情况,消费的速度也就不及只处理一个partition的消费者的处理速度!如果是消费者组的消费者多于partition的数量,那会不会出现多个消费者消费同一个partition的数据呢?上面已经提到过不会出现这种情况!多出来的消费者不消费任何partition的数据。所以在实际的应用中,建议消费者组的consumer的数量与partition的数量一致! 在保存数据的小节里面,我们聊到了partition划分为多组segment,每个segment又包含.log、.index、.timeindex文件,存放的每条message包含offset、消息大小、消息体……我们多次提到segment和offset,查找消息的时候是怎么利用segment+offset配合查找的呢?假如现在需要查找一个offset为368801的message是什么样的过程呢?我们先看看下面的图:
- 先找到offset的368801message所在的segment文件(利用二分法查找),这里找到的就是在第二个segment文件。
- 打开找到的segment中的.index文件(也就是368796.index文件,该文件起始偏移量为368796+1,我们要查找的offset为368801的message在该index内的偏移量为368796+5=368801,所以这里要查找的相对offset为5)。由于该文件采用的是稀疏索引的方式存储着相对offset及对应message物理偏移量的关系,所以直接找相对offset为5的索引找不到,这里同样利用二分法查找相对offset小于或者等于指定的相对offset的索引条目中最大的那个相对offset,所以找到的是相对offset为4的这个索引。
- 根据找到的相对offset为4的索引确定message存储的物理偏移位置为256。打开数据文件,从位置为256的那个地方开始顺序扫描直到找到offset为368801的那条Message。
这套机制是建立在offset为有序的基础上,利用segment+有序offset+稀疏索引+二分查找+顺序查找等多种手段来高效的查找数据!至此,消费者就能拿到需要处理的数据进行处理了。那每个消费者又是怎么记录自己消费的位置呢?在早期的版本中,消费者将消费到的offset维护Zookeeper中,consumer每间隔一段时间上报一次,这里容易导致重复消费,且性能不好!在新的版本中消费者消费到的offset已经直接维护在kafk集群的__consumer_offsets这个topic中!
3、总结
1、producer
Producers直接发送消息到broker上的leader partition,不需要经过任何中介一系列的路由转发。为了实现这个特性,kafka集群中的每个broker都可以响应producer的请求,并返回topic的一些元信息,这些元信息包括哪些机器是存活的,topic的leader partition都在哪,现阶段哪些leader partition是可以直接被访问的。
Producer客户端自己控制着消息被推送到哪些partition。实现的方式可以是随机分配、实现一类随机负载均衡算法,或者指定一些分区算法。Kafka提供了接口供用户实现自定义的分区,用户可以为每个消息指定一个partitionKey,通过这个key来实现一些hash分区算法。比如,把userid作为partitionkey的话,相同userid的消息将会被推送到同一个分区。
以Batch的方式推送数据可以极大的提高处理效率,kafka Producer 可以将消息在内存中累计到一定数量后作为一个batch发送请求。Batch的数量大小可以通过Producer的参数控制,参数值可以设置为累计的消息的数量(如500条)、累计的时间间隔(如100ms)或者累计的数据大小(64KB)。通过增加batch的大小,可以减少网络请求和磁盘IO的次数,当然具体参数设置需要在效率和时效性方面做一个权衡。
Producers可以异步的并行的向kafka发送消息,但是通常producer在发送完消息之后会得到一个future响应,返回的是offset值或者发送过程中遇到的错误。
这其中有个非常重要的参数“acks”,这个参数决定了producer要求leader partition 收到确认的副本个数,如果acks设置数量为0,表示producer不会等待broker的响应,所以,producer无法知道消息是否发送成功,这样有可能会导致数据丢失,但同时,acks值为0会得到最大的系统吞吐量。
若acks设置为1,表示producer会在leader partition收到消息时得到broker的一个确认,这样会有更好的可靠性,因为客户端会等待直到broker确认收到消息。
若设置为-1,producer会在所有备份的partition收到消息时得到broker的确认,这个设置可以得到最高的可靠性保证。
Kafka 消息有一个定长的header和变长的字节数组组成。因为kafka消息支持字节数组,也就使得kafka可以支持任何用户自定义的序列号格式或者其它已有的格式如Apache Avro、protobuf等。Kafka没有限定单个消息的大小,但我们推荐消息大小不要超过1MB,通常一般消息大小都在1~10kB之前。
2、consumer
Kafka提供了两套consumer api,分为high-level api和sample-api。Sample-api 是一个底层的API,它维持了一个和单一broker的连接,并且这个API是完全无状态的,每次请求都需要指定offset值,因此,这套API也是最灵活的。
在kafka中,当前读到消息的offset值是由consumer来维护的,因此,consumer可以自己决定如何读取kafka中的数据。比如,consumer可以通过重设offset值来重新消费已消费过的数据。不管有没有被消费,kafka会保存数据一段时间,这个时间周期是可配置的,只有到了过期时间,kafka才会删除这些数据。
High-level API封装了对集群中一系列broker的访问,可以透明的消费一个topic。它自己维持了已消费消息的状态,即每次消费的都是下一个消息。
High-level API还支持以组的形式消费topic,如果consumers有同一个组名,那么kafka就相当于一个队列消息服务,而各个consumer均衡的消费相应partition中的数据。若consumers有不同的组名,那么此时kafka就相当与一个广播服务,会把topic中的所有消息广播到每个consumer。
3、kafka维度
kafka可以用三个维度表示:
X: 水平扩展,可靠性,主要用做分区副本。多主机的
Y:topic,不用的业务可以选择不同的topic
Z:是在Y轴的基础上进行分区的。 就是一个topic可以有不同的分区。
四、源码分析
后面会更新!!!!