1.基本介绍
Apache的Kafka是一个分布式流平台(a distributed streaming platform)。
一个流处理平台应该具有三个关键能力:
- 它可以让你发布和订阅记录流。在这方面,它类似于一个消息队列或企业消息系统。
- 它可以让你持久化收到的记录流,从而具有容错能力。
- 它可以让你处理收到的记录流。
Kafka应用场景:
- 建立实时流数据管道从而能够可靠地在系统或应用程序之间的共享数据
- 构建实时流应用程序,能够变换或者对数据
- 进行相应的处理。
想要了解Kafka如何具有这些能力,让我们从下往上深入探索Kafka的能力。
首先,明确几个概念:
- Kafka是运行在一个或多个服务器的集群(Cluster)上的。
- Kafka集群分类存储的记录流被称为主题(Topics)。
- 每个消息记录包含一个键,一个值和时间戳。
Kafka有四个核心API:
- 生产者 API 允许应用程序发布记录流至一个或多个Kafka的话题(Topics)。
- 消费者API允许应用程序订阅一个或多个主题,并处理这些主题接收到的记录流。
- Streams API允许应用程序充当流处理器(stream processor),从一个或多个主题获取输入流,并生产一个输出流至一个或多个的主题,能够有效地变换输入流为输出流。
- Connector API允许构建和运行可重用的生产者或消费者,能够把 Kafka主题连接到现有的应用程序或数据系统。例如,一个连接到关系数据库的连接器(connector)可能会获取每个表的变化。
Kafka的客户端和服务器之间的通信是靠一个简单的,高性能的,与语言无关的TCP协议完成的。这个协议有不同的版本,并保持向后兼容旧版本(向前兼容旧版本?)。Kafka不光提供了一个Java客户端,还有许多语言版本的客户端。
1.1 主题(Topic)和日志
Kafka的核心抽象概念记录流 – Topic。
Topic是用于存储消息的逻辑概念,可以看作一个消息集合。每个topic可以有多个生产者向其推送消息,也可以有任意多个消费者消费其中的消息,每个topic可以划分多个分区(每个Topic至少有一个分区),同一topic下的不同分区包含的消息是不同的。每个消息在被添加到分区时,区会给每个消息记录分配一个顺序ID号 -offset(称之为偏移量),它是消息在此分区中的唯一编号,kafka通过offset保证消息在分区内的顺序,offset的顺序不跨分区,即kafka只保证在同一个分区内的消息是有序的;
Partition(区)是以文件的形式存储在文件系统中,存储在kafka-log目录下,命名规则是:<topic_name>-<partition_id>
对于每一个主题,Kafka集群保持一个分区日志文件,看下图:
Kafka集群保留所有发布的记录,不管这个记录有没有被消费过,Kafka提供可配置的保留策略去删除旧数据(还有一种策略根据分区大小删除数据)。例如,如果将保留策略设置为两天,在记录公布后两天,它可用于消费,之后它将被丢弃以腾出空间。Kafka的性能跟存储的数据量的大小无关, 所以将数据存储很长一段时间是没有问题的。
事实上,保留在每个消费者元数据中的最基础的数据就是消费者正在处理的当前记录的偏移量(offset)或位置(position)。这种偏移是由消费者控制:通常偏移会随着消费者读取记录线性前进,但事实上,因为其位置是由消费者进行控制,消费者可以在任何它喜欢的位置读取记录。例如,消费者可以恢复到旧的偏移量对过去的数据再加工或者直接跳到最新的记录,并消费从“现在”开始的新的记录。
数据日志的分区,首先,它们允许数据能够扩展到更多的服务器上去。每个单独的分区的大小受到承载它的服务器的限制,但一个主题可能有很多分区,以便它能够支持海量的的数据。更重要的意义是分区是进行并行处理的基础单元。
1.2 分布式
日志的分区会跨服务器的分布在Kafka集群中,每个服务器会共享分区进行数据请求的处理。每个分区可以配置一定数量的副本分区提供容错能力。
每个分区都有一个服务器充当“leader”和零个或多个服务器充当“followers”。 leader处理所有的读取和写入分区的请求,而followers被动的从领导者拷贝数据。如果leader失败了,followers之一将自动成为新的领导者。每个服务器可能充当一些分区的leader和其他分区的follower,这样的负载就会在集群内很好的均衡分配。
1.3 生产者
生产者发布数据到他们所选择的主题。生产者负责选择把记录分配到主题中的哪个分区。这可以使用轮询算法( round-robin)进行简单地平衡负载,也可以根据一些更复杂的语义分区算法(比如基于记录一些键值)来完成。
1.4 消费者
消费者以消费群(consumer group )的名称来标识自己,每个发布到主题的消息都会发送给订阅了这个主题的消费群里面的一个消费者的一个实例。消费者的实例可以在单独的进程或单独的机器上。
如果所有的消费者实例都属于相同的消费群,那么记录将有效地被均衡到每个消费者实例。
如果所有的消费者实例有不同的消费群,那么每个消息将被广播到所有的消费者进程。
两个服务器的Kafka集群具有四个分区(P0-P3)和两个消费群。A消费群有两个消费者,B群有四个。
更常见的是,我们会发现主题有少量的消费群,每一个都是“逻辑上的订阅者”。每组都是由很多消费者实例组成,从而实现可扩展性和容错性。这只不过是发布 – 订阅模式的再现,区别是这里的订阅者是一组消费者而不是一个单一的进程的消费者。
Kafka消费群的实现方式是通过分割日志的分区,分给每个Consumer实例,使每个实例在任何时间点的都可以“公平分享”独占的分区。维持消费群中的成员关系的这个过程是通过Kafka动态协议处理。如果新的实例加入该组,他将接管该组的其他成员的一些分区; 如果一个实例死亡,其分区将被分配到剩余的实例。
Kafka只保证一个分区内的消息有序,不能保证一个主题的不同分区之间的消息有序。分区的消息有序与依靠主键进行数据分区的能力相结合足以满足大多数应用的要求。但是,如果你想要保证所有的消息都绝对有序可以只为一个主题分配一个分区,虽然这将意味着每个消费群同时只能有一个消费进程在消费。
保证
Kafka提供了以下一些高级别的保证:
- 由生产者发送到一个特定的主题分区的消息将被以他们被发送的顺序来追加。也就是说,如果一个消息M1和消息M2都来自同一个生产者,M1先发,那么M1将有一个低于M2的偏移,会更早在日志中出现。
- 消费者看到的记录排序就是记录被存储在日志中的顺序。
- 对于副本因子N的主题,我们将承受最多N-1次服务器故障切换而不会损失任何的已经保存的记录。
对这些保证的更多细节可以参考文档的设计部分。
Kafka比传统的消息系统具有更强的消息顺序保证的能力。
传统的消息队列的消息在队列中是有序的,多个消费者从队列中消费消息,服务器按照存储的顺序派发消息。然而,尽管服务器是按照顺序派发消息,但是这些消息记录被异步传递给消费者,消费者接收到的消息也许已经是乱序的了。这实际上意味着消息的排序在并行消费中都将丢失。消息系统通常靠 “排他性消费”( exclusive consumer)来解决这个问题,只允许一个进程从队列中消费,当然,这意味着没有并行处理的能力。
Kafka做的更好。通过一个概念:并行性-分区-主题实现主题内的并行处理,Kafka是能够通过一组消费者的进程同时提供排序保证和负载均衡。每个主题的分区指定给每个消费群中的一个消费者,使每个分区只由该组中的一个消费者所消费。通过这样做,我们确保消费者是一个分区唯一的读者,从而顺序的消费数据。因为有许多的分区,所以负载还能够均衡的分配到很多的消费者实例上去。但是请注意,一个消费群的消费者实例不能比分区数量多。
Kafka作为存储系统
任何消息队列都能够解耦消息的生产和消费,还能够有效地存储正在传送的消息。Kafka与众不同的是,它是一个非常好的存储系统。
Kafka把消息数据写到磁盘和备份分区。Kafka允许生产者等待返回确认,直到副本复制和持久化全部完成才认为成功,否则则认为写入服务器失败。
Kafka使用的磁盘结构很好扩展,Kafka将执行相同的策略不管你是有50 KB或50TB的持久化数据。
由于存储的重要性,并允许客户控制自己的读取位置,你可以把Kafka认为是一种特殊用途的分布式文件系统,致力于高性能,低延迟的有保障的日志存储,能够备份和自我复制。
Kafka流处理
只是读,写,以及储存数据流是不够的,目的是能够实时处理数据流。
在Kafka中,流处理器是从输入的主题连续的获取数据流,然后对输入进行一系列的处理,并生产连续的数据流到输出主题。
例如,零售应用程序可能需要输入销售和出货量,根据输入数据计算出重新订购的数量和调整后的价格,然后输出到主题。
这些简单处理可以直接使用生产者和消费者的API做到。然而,对于更复杂的转换Kafka提供了一个完全集成的流API。这允许应用程序把一些重要的计算过程从流中剥离或者加入流一起。
这种设施可帮助解决这类应用面临的难题:处理杂乱的数据,改变代码去重新处理输入,执行有状态的计算等
流API建立在Kafka提供的核心基础单元之上:它使用生产者和消费者的API进行输入输出,使用Kafka存储有状态的数据,并使用群组机制在一组流处理实例中实现容错。
2.kafka的高吞吐量的因素
2.1 顺序写的方式存储数据 ;
2.2 批量发送;在异步发送模式中。kafka允许进行批量发送,也就是先讲消息缓存到内存中,然后一次请求批量发送出去。这样减少了磁盘频繁io以及网络IO造成的性能瓶颈
batch.size 每批次发送的数据大小
linger.ms 间隔时间
2.3 零拷贝
消息从发送到落地保存,broker维护的消息日志本身就是文件目录,每个文件都是二进制保存,生产者和消费者使用相同的格式来处理。在消费者获取消息时,服务器先从硬盘读取数据到内存,然后把内存中的数据原封不懂的通过socket发送给消费者。虽然这个操作描述起来很简单,但实际上经历了很多步骤
▪ 操作系统将数据从磁盘读入到内核空间的页缓存 ▪ 应用程序将数据从内核空间读入到用户空间缓存中 ▪ 应用程序将数据写回到内核空间到socket缓存中 ▪ 操作系统将数据从socket缓冲区复制到网卡缓冲区,以便将数据经网络发出
| |
通过“零拷贝”技术可以去掉这些没必要的数据复制操作,同时也会减少上下文切换次数
3.日志策略
3.1 日志保留策略
无论消费者是否已经消费了消息,kafka都会一直保存这些消息,但并不会像数据库那样长期保存。为了避免磁盘被占满,kafka会配置响应的保留策略(retention policy),以实现周期性地删除陈旧的消息
kafka有两种“保留策略”:
- 根据消息保留的时间,当消息在kafka中保存的时间超过了指定时间,就可以被删除;
- 根据topic存储的数据大小,当topic所占的日志文件大小大于一个阀值,则可以开始删除最旧的消息
3.2 日志压缩策略
在很多场景中,消息的key与value的值之间的对应关系是不断变化的,就像数据库中的数据会不断被修改一样,消费者只关心key对应的最新的value。我们可以开启日志压缩功能,kafka定期将相同key的消息进行合并,只保留最新的value值
4.消息可靠性机制
4.1 消息发送可靠性
生产者发送消息到broker,有三种确认方式(request.required.acks)
acks = 0: producer不会等待broker(leader)发送ack 。因为发送消息网络超时或broker crash(1.Partition的Leader还没有commit消息 2.Leader与Follower数据不同步),既有可能丢失也可能会重发。
acks = 1: 当leader接收到消息之后发送ack,丢会重发,丢的概率很小
acks = -1: 当所有的follower都同步消息成功后发送ack. 丢失消息可能性比较低。
4.2 消息存储可靠性
每一条消息被发送到broker中,会根据partition规则选择被存储到哪一个partition。如果partition规则设置的合理,所有消息可以均匀分布到不同的partition里,这样就实现了水平扩展。
在创建topic时可以指定这个topic对应的partition的数量。在发送一条消息时,可以指定这条消息的key,producer根据这个key和partition机制来判断这个消息发送到哪个partition。
kafka的高可靠性的保障来自于另一个叫副本(replication)策略,通过设置副本的相关参数,可以使kafka在性能和可靠性之间做不同的切换。
高可靠性的副本
sh kafka-topics.sh --create --zookeeper 192.168.11.140:2181 --replication-factor 2 --partitions 3 --topic sixsix
--replication-factor表示的副本数
4.2.1 副本机制
ISR(副本同步队列)
维护的是有资格的follower节点
- 副本的所有节点都必须要和zookeeper保持连接状态
- 副本的最后一条消息的offset和leader副本的最后一条消息的offset之间的差值不能超过指定的阀值,这个阀值是可以设置的(replica.lag.max.messages)
HW&LEO(https://blog.youkuaiyun.com/varyall/article/details/102903379)
HW
(High Watermark)俗称高水位,它标识了一个特定的消息偏移量(offset),消费者只能拉取到这个offset之前的消息。
LEO
(Log End Offset),标识当前日志文件中下一条待写入的消息的offset。
关于follower副本同步的过程中,还有两个关键的概念,HW(HighWatermark)和LEO(Log End Offset). 这两个参数跟ISR集合紧密关联。HW标记了一个特殊的offset,当消费者处理消息的时候,只能拉去到HW之前的消息,HW之后的消息对消费者来说是不可见的。也就是说,取partition对应ISR中最小的LEO作为HW,consumer最多只能消费到HW所在的位置。每个replica都有HW,leader和follower各自维护更新自己的HW的状态。对于leader新写入的消息,consumer不能立刻消费,leader会等待该消息被所有ISR中的replicas同步更新HW,此时消息才能被consumer消费。这样就保证了如果leader副本损坏,该消息仍然可以从新选举的leader中获取
LEO 是所有副本都会有的一个offset标记,它指向追加到当前副本的最后一个消息的offset。当生产者向leader副本追加消息的时候,leader副本的LEO标记就会递增;当follower副本成功从leader副本拉去消息并更新到本地的时候,follower副本的LEO就会增加
5. 文件存储机制
5.1 存储机制
在kafka文件存储中,同一个topic下有多个不同的partition,每个partition为一个目录,partition的名称规则为:topic名称+有序序号,第一个序号从0开始,最大的序号为partition数量减1,partition是实际物理上的概念,而topic是逻辑上的概念
partition还可以细分为segment,这个segment是什么呢? 假设kafka以partition为最小存储单位,那么我们可以想象当kafka producer不断发送消息,必然会引起partition文件的无线扩张,这样对于消息文件的维护以及被消费的消息的清理带来非常大的挑战,所以kafka 以segment为单位又把partition进行细分。每个partition相当于一个巨型文件被平均分配到多个大小相等的segment数据文件中(每个setment文件中的消息不一定相等),这种特性方便已经被消费的消息的清理,提高磁盘的利用率
segment file组成:由2大部分组成,分别为index file和data file,此2个文件一一对应,成对出现,后缀".index"和“.log”分别表示为segment索引文件、数据文件.
segment文件命名规则:partion全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset值。数值最大为64位long大小,19位数字字符长度,没有数字用0填充
查找方式
以上图为例,读取offset=170418的消息,首先查找segment文件,其中00000000000000000000.index为最开始的文件,第二个文件为00000000000000170410.index(起始偏移为170410+1=170411),而第三个文件为00000000000000239430.index(起始偏移为239430+1=239431),所以这个offset=170418就落到了第二个文件之中。其他后续文件可以依次类推,以其实偏移量命名并排列这些文件,然后根据二分查找法就可以快速定位到具体文件位置。其次根据00000000000000170410.index文件中的[8,1325]定位到00000000000000170410.log文件中的1325的位置进行读取。
6. 消息确认的几种方式
自动提交
手动提交
手动异步提交
consumer. commitASync() //手动异步ack
手动同步提交
consumer. commitSync() //手动异步ack
指定消费某个分区的消息
消息的消费原理
之前Kafka存在的一个非常大的性能隐患就是利用ZK来记录各个Consumer Group的消费进度(offset)。当然JVM Client帮我们自动做了这些事情,但是Consumer需要和ZK频繁交互,而利用ZK Client API对ZK频繁写入是一个低效的操作,并且从水平扩展性上来讲也存在问题。所以ZK抖一抖,集群吞吐量就跟着一起抖,严重的时候简直抖的停不下来。
新版Kafka已推荐将consumer的位移信息保存在Kafka内部的topic中,即__consumer_offsets topic。通过以下操作来看看__consumer_offsets_topic是怎么存储消费进度的,__consumer_offsets_topic默认有50个分区
- 计算consumer group对应的hash值
- 获得consumer group的位移信息
bin/kafka-simple-consumer-shell.sh --topic __consumer_offsets --partition 15 -broker-list 192.168.11.140:9092,192.168.11.141:9092,192.168.11.138:9092 --formatter kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter
7. kafka的分区分配策略
在kafka中每个topic一般都会有很多个partitions。为了提高消息的消费速度,我们可能会启动多个consumer去消费; 同时,kafka存在consumer group的概念,也就是group.id一样的consumer,这些consumer属于一个consumer group,组内的所有消费者协调在一起来消费消费订阅主题的所有分区。当然每一个分区只能由同一个消费组内的consumer来消费,那么同一个consumer group里面的consumer是怎么去分配该消费哪个分区里的数据,这个就设计到了kafka内部分区分配策略(Partition Assignment Strategy)
在 Kafka 内部存在两种默认的分区分配策略:Range(默认) 和 RoundRobin。通过:partition.assignment.strategy指定
consumer rebalance
当以下事件发生时,Kafka 将会进行一次分区分配:
- 同一个consumer group内新增了消费者
- 消费者离开当前所属的consumer group,包括shuts down 或crashes
- 订阅的主题新增分区(分区数量发生变化)
- 消费者主动取消对某个topic的订阅
- 也就是说,把分区的所有权从一个消费者移到另外一个消费者上,这个是kafka consumer 的rebalance机制。如何rebalance就涉及到前面说的分区分配策略。
两种分区策略
Range 策略(默认)
0 ,1 ,2 ,3 ,4,5,6,7,8,9
c0 [0,3] c1 [4,6] c2 [7,9]
10(partition num/3(consumer num) =3
roundrobin 策略
0 ,1 ,2 ,3 ,4,5,6,7,8,9
c0,c1,c2
c0 [0,3,6,9]
c1 [1,4,7]
c2 [2,5,8]
kafka 的key 为null, 是随机{一个Metadata的同步周期内,默认是10分钟}