1、kafka概述
1.1 消息队列
Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者在网站中的所有动作流数据
客户端消费Queue的数据有两种方式:
- 发布/订阅模式,也就是一对多,数据生产之后,推给所有的订阅者,打个比方:就像是手机上面的QQ消息,你没有打开手机看消息,但是如果有消息就会一直有消息推送过来。
- 点对点模式,也就是一对一,这个是主动模式,第一种模式更像是被动模式,这个就是消费者主动拉取生产后的数据。
1.2 优点
解耦 | 可恢复性。 |
冗余。 | 顺序保证。(ps:kafka保证一个partition内的数据是有序的) |
扩展性 | 缓冲 |
灵活性and峰值处理能力 | 异步通信 |
1.3 基本术语
- 无论是kafka集群还是consumer,都依赖zookeeper集群来保存一些meta信息。
- Producer:消息生产者,向kafka broker发消息的客户端。
- consumer:消息消费者,向kafka broker取消息的客户端。
- Topic:可以理解是一个队列,一个topic里有很多个partition。
- consumer group:这是kafka用来实现topic广播的和单播的手段。topic的消息会复制(不是真正的复制)到所有的CG,但是每个partition只会把消息发给该CG中的一个consumer。广播的实现方法是:只要每个consumer有一个独立的CG就行了。单播的实现方法是:只要所有的consumer在同一个CG。
- Broker:一台kafka服务器就是一个broker,一个集群由多个broker。一个broker可以容纳多个topic。
- partition:一个非常大恶的topic可以分布在多个broker上,一个topic可以分成多个partition,每个partition是一个有序队列。partition中的每条消息都会被分配一个有序的ID,kafka只保证按一个partition中的顺序将消息发给consumer,不保证一个topic的整体(多个partition)的顺序。
1.4 存在硬盘上的消息格式
消息由一个固定长度的头部和可变长度的字节数组组成。头部包含了一个版本号和CRC32校验码。
- 消息长度: 4 bytes (value: 1+4+n)
- 版本号: 1 byte
- CRC校验码: 4 bytes
- 具体的消息: n bytes
2、kafka工作流程
2.1 Kafka生产过程
(1) 消息发布到broker:producer采用推(push)模式将消息发布到broker,每条消息都被追加(append)到分区(patition)中,属于顺序写磁盘(顺序写磁盘效率比随机写内存要高,保障kafka吞吐率)。
(2) 分区:消息发送时都被发送到一个topic,其本质就是一个目录,而topic是由一些Partition Logs(分区日志)组成。
图中两个消费者:
- 顺序写入Consumer1有两个offset分别对应Partition0、Partition1(假设每一个Topic一个Partition);
- 顺序写入Consumer2有一个offset对应Partition2。
这个offset是由客户端SDK负责保存的,Kafka的Broker完全无视这个东西的存在;一般情况下SDK会把它保存到Zookeeper里面,所以需要给Consumer提供zookeeper的地址。
注意,每个Partition中的消息都是有序的,生产的消息被不断追加到Partition log上,其中的每一个消息都被赋予了一个唯一的offset值。
分区的原因:
- 方便在集群中扩展,每个Partition可以通过调整以适应它所在的机器,而一个topic又可以有多个Partition组成,因此整个集群就可以适应任意大小的数据了;
- 可以提高并发,因为可以以Partition为单位读写了。
分区的原则:
- 指定了patition,则直接使用;
- 未指定patition但指定key,通过对key的value进行hash出一个patition;
- patition和key都未指定,使用轮询选出一个patition。
(3) 副本:同一个partition可能会有多个replication,没有replication的情况下,一旦broker 宕机,其上所有 patition 的数据都不可被消费,同时producer也不能再将数据存于其上的patition,没有replication的情况下,一旦broker 宕机,其上所有 patition 的数据都不可被消费,同时producer也不能再将数据存于其上的patition。
★Kafka创建Topic时如何将分区放置到不同的Broker中???
- 副本因子不能大于 Broker 的个数;
- 第一个分区(编号为0)的第一个副本放置位置是随机从 brokerList 选择的;
- 其他分区的第一个副本放置位置相对于第0个分区依次往后移。也就是如果我们有5个 Broker,5个分区,假设第一个分区放在第四个 Broker 上,那么第二个分区将会放在第五个 Broker 上;第三个分区将会放在第一个 Broker 上;第四个分区将会放在第二个 Broker 上,依次类推;
- 剩余的副本相对于第一个副本放置位置其实是由 nextReplicaShift 决定的,而这个数也是随机产生的。
★Kafka新建的分区会在哪个目录下创建??
在启动 Kafka 集群之前,我们需要配置好 log.dirs 参数,其值是 Kafka 数据的存放目录,这个参数可以配置多个目录,目录之间使用逗号分隔,通常这些目录是分布在不同的磁盘上用于提高读写性能。
当然我们也可以配置 log.dir 参数,含义一样。只需要设置其中一个即可。
如果 log.dirs 参数只配置了一个目录,那么分配到各个 Broker 上的分区肯定只能在这个目录下创建文件夹用于存放数据。
但是如果 log.dirs 参数配置了多个目录,那么 Kafka 会在哪个文件夹中创建分区目录呢?
答案是:Kafka 会在含有分区目录最少的文件夹中创建新的分区目录,分区目录名为 Topic名+分区ID。注意,是分区文件夹总数最少的目录,而不是磁盘使用量最少的目录!也就是说,如果你给 log.dirs 参数新增了一个新的磁盘,新的分区目录肯定是先在这个新的磁盘上创建直到这个新的磁盘目录拥有的分区目录不是最少为止。
(4) 写入流程
2.2 Broker保存消息
(1) 存储方式
物理上把topic分成一个或多个patition,每个patition物理上对应一个文件夹(该文件夹存储该patition的所有消息和索引文件)。
★partition的数据如何保存到硬盘??
- topic中的多个partition以文件夹的形式保存到broker,每个分区序号从0递增,且消息有序
- Partition文件下有多个segment(xxx.index,xxx.log)
- segment 文件里的 大小和配置文件大小一致可以根据要求修改 默认为1g
- 如果大小大于1g时,会滚动一个新的segment并且以上一个segment最后一条消息的偏移量命名
(2)存储策略
无论消息是否被消费,kafka都会保留所有消息。有两种策略可以删除旧数据:
- 基于时间:log.retention.hours=138
- 基于大小:log.retention.bytes=1066666666
需要注意的是,因为Kafka读取特定消息的时间复杂度为O(1),即与文件大小无关,所以这里删除过期文件与提高 Kafka 性能无关。
2.3 kafka消费过程
2.3.1 高级API
(1)高级API优点:
- 高级API 写起来简单
- 不需要自行去管理offset,系统通过zookeeper自行管理。
- 不需要管理分区,副本等情况,.系统自动管理。
- 消费者断线会自动根据上一次记录在zookeeper中的offset去接着获取数据(默认设置1分钟更新一下zookeeper中存的offset)。
- 可以使用group来区分对同一个topic 的不同程序访问分离开来(不同的group记录不同的offset,这样不同程序读取同一个topic才不会因为offset互相影响)
(2)高级API缺点:
- 不能细化控制如分区、副本、zk等。
- 不能自行控制offset(对于某些特殊需求来说)。
2.3.2 低级API
(1)低级API优点
- 能够让开发者自己控制offset,想从哪里读取就从哪里读取。
- 自行控制连接分区,对分区自定义进行负载均衡
- 对zookeeper的依赖性降低(如:offset不一定非要靠zk存储,自行存储offset即可,比如存在文件或者内存中)
(2)低级API缺点
- 太过复杂,需要自行控制offset,连接哪个分区,找到分区leader 等。
2.3.3 消费者组
(1)消费者负载均衡策略
一个消费者组中的一个分片对应一个消费者成员,他能保证每个消费者成员都能访问,如果组中成员太多会有空闲的成员
消费者可以通过水平扩展的方式同时读取大量的消息。另外,如果一个消费者失败了,那么其他的group成员会自动负载均衡读取之前失败的消费者读取的分区。消费者是以consumer group消费者组的方式工作,由一个或者多个消费者组成一个组,共同消费一个topic。每个分区在同一时间只能由group中的一个消费者读取,但是多个group可以同时消费这个partition。
在图中,有一个由三个消费者组成的group,有一个消费者读取主题中的两个分区,另外两个分别读取一个分区。某个消费者读取某个分区,也可以叫做某个消费者是某个分区的拥有者。
(2)数据有序:
- 一个消费者组里它的内部是有序的
- 消费者组与消费者组之间是无序的
2.3.4 消费方式
- consumer采用pull(拉)模式从broker中读取数据。
- push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。它的目标是尽可能以最快速度传递消息,但是这样很容易造成consumer来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而pull模式则可以根据consumer的消费能力以适当的速率消费消息。
- 对于Kafka而言,pull模式更合适,它可简化broker的设计,consumer可自主控制消费消息的速率,同时consumer可以自己控制消费方式——即可批量消费也可逐条消费,同时还能选择不同的提交方式从而实现不同的传输语义。
- pull模式不足之处是,如果kafka没有数据,消费者可能会陷入循环中,一直等待数据到达。为了避免这种情况,我们在我们的拉请求中有参数,允许消费者请求在等待数据到达的“长轮询”中进行阻塞(并且可选地等待到给定的字节数,以确保大的传输大小)。
3、kafka消息保证机制
3.1 At most once
消息可能会丢,但是绝对不会重复传递。读完消息之后先commit然后再处理消息,在这种模式下comsumer在commit后还没有来得及处理就crash了,下次重新开始工作之后无法读到刚刚已提交而未处理的消息,这就对应了At most once。
3.2 At least once
消息绝对不会丢,但是可能重复传递。读完消息之后先处理消息然后再commit,如果在消息处理之后commite之前crash,下次重新开始工作的时候刚刚处理未提交commit消息,但是实际上该消息已经被处理过,这就对应了At least once。
3.3 Exactly once
每条消息之传输一次仅被传输一次。如果要做到Exactly once,那么就需要offset来帮助实现,通用的方式是将offset和操作输出输出到同一个地方,但是对于high API来说,offset是保存在zk中的,无法输出到同一个低方,lower API是自己维护offset的,可以将它存在同一个地方,一般存在HDFS。
3.4 如何保证数据不丢失
(1)broker如何保证数据的不丢失
- acks=all : 所有副本都写入成功并确认。
- retries = 一个合理值。
- min.insync.replicas=2 消息至少要被写入到这么多副本才算成功。
- unclean.leader.election.enable=false 关闭unclean leader选举,即不允许非ISR中的副本被选举为leader,以避免数据丢失。
(2)consumer如果保证数据得不丢失:
- enable.auto.commit=false 关闭自动提交offset。
(3)kafka的ack机制
request.required.acks有三个值 0 1 -1:
- 0:生产者不会等待broker的ack,这个延迟最低但是存储的保证最弱当server挂掉的时候就会丢数据;
- 1:服务端会等待ack值 leader副本确认接收到消息后发送ack但是如果leader挂掉后他不确保是否复制完成新leader也会导致数据丢失;
- -1:同样在1的基础上 服务端会等所有的follower的副本受到数据后才会受到leader发出的ack,这样数据不会丢失。
4、Kafka 为什么速度那么快?
即使是普通的服务器,Kafka也可以轻松支持每秒百万级的写入请求,超过了大部分的消息中间件,这种特性也使得Kafka在日志处理等海量数据场景广泛应用。
下面从数据写入和读取两方面分析,为什么Kafka速度这么快。
4.1 写入数据
Kafka会把收到的消息都写入到硬盘中,它绝对不会丢失数据。为了优化写入速度Kafka采用了两个技术, 顺序写入和MMFile 。
4.1.1 顺序写入
磁盘读写的快慢取决于你怎么使用它,也就是顺序读写或者随机读写。在顺序读写的情况下,磁盘的顺序读写速度和内存持平。
因为硬盘是机械结构,每次读写都会寻址->写入,其中寻址是一个“机械动作”,它是最耗时的。所以硬盘最讨厌随机I/O,最喜欢顺序I/O。为了提高读写硬盘的速度,Kafka就是使用顺序I/O。
而且Linux对于磁盘的读写优化也比较多,包括read-ahead和write-behind,磁盘缓存等。如果在内存做这些操作的时候,一个是JAVA对象的内存开销很大,另一个是随着堆内存数据的增多,JAVA的GC时间会变得很长,使用磁盘操作有以下几个好处:
- 顺序写入磁盘顺序读写速度超过内存随机读写
- 顺序写入JVM的GC效率低,内存占用大。使用磁盘可以避免这一问题
- 顺序写入系统冷启动后,磁盘缓存依然可用
2.1节中的图(1)展示了Kafka是如何写入数据的, 每一个Partition其实都是一个文件 ,收到消息后Kafka会把数据插入到文件末尾(虚框部分)。这种方法有一个缺陷——没有办法删除数据 ,所以Kafka是不会删除数据的,它会把所有的数据都保留下来,每个消费者(Consumer)对每个Topic都有一个offset用来表示读取到了第几条数据 。
在2.1的分区的原因中说明了可以提高并发性。
如果不删除硬盘肯定会被撑满,所以Kakfa提供了两种策略来删除数据,但是,在2.2的存储策略中已经说明了删除过期文件与提高 Kafka 性能无关。
4.1.2 Memory Mapped Files
即便是顺序写入硬盘,硬盘的访问速度还是不可能追上内存。所以Kafka的数据并不是实时的写入硬盘 ,它充分利用了现代操作系统分页存储来利用内存提高I/O效率。
Memory Mapped Files(后面简称mmap)也被翻译成 内存映射文件 ,在64位操作系统中一般可以表示20G的数据文件,它的工作原理是直接利用操作系统的Page来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上(操作系统在适当的时候)。
通过mmap,进程像读写硬盘一样读写内存(当然是虚拟机内存),也不必关心内存的大小有虚拟内存为我们兜底。
使用这种方式可以获取很大的I/O提升,省去了用户空间到内核空间复制的开销(调用文件的read会把数据先放到内核空间的内存中,然后再复制到用户空间的内存中。)
但也有一个很明显的缺陷——不可靠,写到mmap中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘。
Kafka提供了一个参数——producer.type来控制是不是主动flush,如果Kafka写入到mmap之后就立即flush然后再返回Producer叫 同步 (sync);写入mmap之后立即返回Producer不调用flush叫异步 (async)。
4.2 读取数据
Kafka在读取磁盘时做了哪些优化?
4.2.1 基于sendfile实现Zero Copy
传统模式下,当需要对一个文件进行传输的时候,其具体流程细节如下:
- 基于sendfile实现Zero Copy调用read函数,文件数据被copy到内核缓冲区
- read函数返回,文件数据从内核缓冲区copy到用户缓冲区
- write函数调用,将文件数据从用户缓冲区copy到内核与socket相关的缓冲区。
- 数据从socket缓冲区copy到相关协议引擎。
以上细节是传统read/write方式进行网络文件传输的方式,我们可以看到,在这个过程当中,文件数据实际上是经过了四次copy操作:
硬盘—>内核buf—>用户buf—>socket相关缓冲区—>协议引擎
而sendfile系统调用则提供了一种减少以上多次copy,提升文件传输性能的方法。
在内核版本2.1中,引入了sendfile系统调用,以简化网络上和两个本地文件之间的数据传输。sendfile的引入不仅减少了数据复制,还减少了上下文切换。
sendfile(socket, file, len);
运行流程如下:
- sendfile系统调用,文件数据被copy至内核缓冲区
- 再从内核缓冲区copy至内核中socket相关的缓冲区
- 最后再socket相关的缓冲区copy到协议引擎
相较传统read/write方式:
- 2.1版本内核引进的sendfile已经减少了内核缓冲区到user缓冲区,再由user缓冲区到socket相关缓冲区的文件copy;
- 在内核版本2.4之后,文件描述符结果被改变,sendfile实现了更简单的方式,再次减少了一次copy操作。
- 在Apache、Nginx、lighttpd等web服务器当中,都有一项sendfile相关的配置,使用sendfile可以大幅提升文件传输性能。
Kafka把所有的消息都存放在一个一个的文件中,当消费者需要数据的时候Kafka直接把文件发送给消费者,配合mmap作为文件读写方式,直接把它传给sendfile。
4.2.2 批量压缩
在很多情况下,系统的瓶颈不是CPU或磁盘,而是网络IO,对于需要在广域网上的数据中心之间发送消息的数据流水线尤其如此。进行数据压缩会消耗少量的CPU资源,不过对于kafka而言,网络IO更应该需要考虑。
- 如果每个消息都压缩,但是压缩率相对很低,所以Kafka使用了批量压缩,即将多个消息一起压缩而不是单个消息压缩;
- Kafka允许使用递归的消息集合,批量的消息可以通过压缩的形式传输并且在日志中也可以保持压缩格式,直到被消费者解压缩;
- Kafka支持多种压缩协议,包括Gzip和Snappy压缩协议
参考:
https://zhuanlan.zhihu.com/p/147054382
https://blog.csdn.net/qq_35641192/article/details/80956244?utm_source=blogxgwz8