Java最全消息中间件架构体系 - Kafka:从“入门”到“进阶”,Java开发面试技能介绍

Docker步步实践

目录文档:

①Docker简介

②基本概念

③安装Docker

④使用镜像:

⑤操作容器:

⑥访问仓库:

⑦数据管理:

⑧使用网络:

⑨高级网络配置:

⑩安全:

⑪底层实现:

⑫其他项目:

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

首先我们需要了解的是, kafka 是使用日志文件的方式来保存生产者和发送者的消息,每条消息都有一个 offset 值来表示它在分区中的偏移量。 Kafka 中存储的一般都是海量的消息数据,为了避免日志文件过大,Log 并不是直接对应在一个磁盘上的日志文件,而是对应磁盘上的一个目录,这个目录的明明规则是<topic_name>_<partition_id>比如创建一个名为 firstTopic 的 topic,其中有 3 个 partition,那么在 kafka 的数据目录(/tmp/kafka-log)中就有 3 个目录,firstTopic-0~3

多个分区在集群中的分配

如果我们对于一个 topic,在集群中创建多个 partition,那么 partition 是如何分布的呢?

1.将所有 N Broker 和待分配的 i 个 Partition 排序

2.将第 i 个 Partition 分配到第(i mod n)个 Broker 上

消息中间件架构体系 - Kafka:从“入门”到“进阶”,也不是很难

了解到这里的时候,大家再结合前面讲的消息分发策略,就应该能明白消息发送到 broker 上,消息会保存到哪个分区中,并且消费端应该消费哪些分区的数据了。

消息写入的性能

我们现在大部分企业仍然用的是机械结构的磁盘,如果把消息以随机的方式写入到磁盘,那么磁盘首先要做的就是寻址,也就是定位到数据所在的物理地址,在磁盘上就要找到对应的柱面、磁头以及对应的扇区;这个过程相对内存来说会消耗大量时间,为了规避随机读写带来的时间消耗, kafka 采用顺序写的方式存储数据。即使是这样,但是频繁的 I/O 操作仍然会造成磁盘的性能瓶颈,所以 kafka还有一个性能策略

5.1.5 页缓存

=========

顺序写入是Kafka高吞吐量的一个原因,当然即使采用的是磁盘的顺序写入,那么也是没有办法和内存相比的。因为为了再一次提高Kakfa的吞吐量,Kafka采用了Memory Mapped Files

(后面简称mmap)也被翻译成内存映射文件 ,它的工作原理是直接利用操作系统的page cache 来实现文件到物理内存的直接映射,完成映射之后你对物理内存的操作会被同步到硬盘上(操作系统在适当的时候)。

操作系统本身有一层缓存,叫做page cache,是在内存里的缓存,我们也可以称之为os cache,意思就是操作系统自己管理的缓存。你在写入磁盘文件的时候,可以直接写入这个os cache里,也就是仅仅写入内存中,接下来由操作系统自己决定什么时候把os cache里的数据真的刷入磁

盘文件中(每5秒检查一次是否需要将页缓存数据同步到磁盘文件)。仅仅这一个步骤,就可以将磁盘文件写性能提升很多了,因为其实这里相当于是在写内存,不是在写磁盘.

5.1.6 零拷贝

=========

消息从发送到落地保存,broker 维护的消息日志本身就是文件目录,每个文件都是二进制保存,生产者和消费者使用相同的格式来处理。在消费者获取消息时,服务器先从硬盘读取数据到内存,然后把内存中的数据原封不动的通过 socket 发送给消费者。虽然这个操作描述起来很简单,但实际上经历了很多步骤。

消息中间件架构体系 - Kafka:从“入门”到“进阶”,也不是很难

▪ 操作系统将数据从磁盘读入到内核空间的页缓存

▪ 应用程序将数据从内核空间读入到用户空间缓存中

▪ 应用程序将数据写回到内核空间到 socket 缓存中

▪ 操作系统将数据从 socket 缓冲区复制到网卡缓冲区,以便将数据经网络发出

这个过程涉及到 4 次上下文切换以及 4 次数据复制,并且有两次复制操作是由 CPU 完成。但是这个过程中,数据完全没有进行变化,仅仅是从磁盘复制到网卡缓冲区。

通过“零拷贝”技术,可以去掉这些没必要的数据复制操作,同时也会减少上下文切换次数。现代的 unix 操作系统提供一个优化的代码路径,用于将数据从页缓存传输到 socket;在 Linux 中,是通过 sendfile 系统调用来完成的。Java 提供了访问这个系统调用的方法: FileChannel.transferTo API

消息中间件架构体系 - Kafka:从“入门”到“进阶”,也不是很难

使用 sendfile,只需要一次拷贝就行,允许操作系统将数据直接从页缓存发送到网络上。所以在这个优化的路径中,只有最后一步将数据拷贝到网卡缓存中是需要的

5.1.7 消息的文件存储机制

===============

消息中间件架构体系 - Kafka:从“入门”到“进阶”,也不是很难

前面我们知道了一个 topic 的多个 partition 在物理磁盘上的保存路径,那么我们再来分析日志的存储方式。通过如下命令找到对应 partition 下的日志内容

[root@localhost ~]# ls /tmp/kafka-logs/firstTopic-1/00000000000000000000.index 00000000000000000000.log 00000000000000000000.timeindex leader-epochcheckpoint

kafka 是通过分段的方式将 Log 分为多个 LogSegment,LogSegment 是一个逻辑上的概念,一个 LogSegment 对应磁盘上的一个日志文件和一个索引文件,其中日志文件是用来记录消息的。索引文件是用来保存消息的索引。那么这个 LogSegment 是什么呢?

LogSegment

假设 kafka 以 partition 为最小存储单位,那么我们可以想象当 kafka producer 不断发送消息,必然会引起 partition文件的无线扩张,这样对于消息文件的维护以及被消费的消息的清理带来非常大的挑战,所以 kafka 以 segment 为单位又把 partition 进行细分。每个 partition 相当于一个巨型文件被平均分配到多个大小相等的 segment 数据文件中(每个 segment 文件中的消息不一定相等),这种特性方便已经被消费的消息的清理,提高磁盘的利用率。

➢ log.segment.bytes=107370 ( 设置分段大小 ), 默认是1gb,我们把这个值调小以后,可以看到日志分段的效果

➢ 抽取其中 3 个分段来进行分析

消息中间件架构体系 - Kafka:从“入门”到“进阶”,也不是很难

segment file 由 2 大部分组成,分别为 index file 和 data file,此 2 个文件一一对应,成对出现,后缀".index"和“.log”分别表示为 segment 索引文件、数据文件.segment 文件命名规则:partion 全局的第一个 segment从 0 开始,后续每个 segment 文件名为上一个 segment文件最后一条消息的 offset 值进行递增。数值最大为 64 位long 大小,20 位数字字符长度,没有数字用 0 填。

segment 中 index 和 log 的对应关系

从所有分段中,找一个分段进行分析为了提高查找消息的性能,为每一个日志文件添加 2 个索引索引文件: OffsetIndex 和 TimeIndex,分别对应_.index以及_.timeindex, TimeIndex 索引文件格式:它是映射时间戳和相对offset

查 看 索 引 内 容 : sh kafka-run-class.sh

kafka.tools.DumpLogSegments --files /tmp/kafkalogs/test-0/00000000000000000000.index --print-datalog

消息中间件架构体系 - Kafka:从“入门”到“进阶”,也不是很难

如图所示,index 中存储了索引以及物理偏移量。 log 存储了消息的内容。索引文件的元数据执行对应数据文件中

message 的物理偏移地址。举个简单的案例来说,以[4053,80899]为例,在 log 文件中,对应的是第 4053 条记录,物理偏移量( position )为 80899. position 是ByteBuffer 的指针位置

5.1.8 在 partition 中如何通过 offset 查找 message

=========================================

  1. 根据 offset 的值,查找 segment 段中的 index 索引文件。由于索引文件命名是以上一个文件的最后一个offset 进行命名的,所以,使用二分查找算法能够根据offset 快速定位到指定的索引文件。

  2. 找到索引文件后,根据 offset 进行定位,找到索引文件中的符合范围的索引。(kafka 采用稀疏索引的方式来提高查找性能)

  3. 得到 position 以后,再到对应的 log 文件中,从 position出开始查找 offset 对应的消息,将每条消息的 offset 与目标 offset 进行比较,直到找到消息

比如说,我们要查找 offset=2490 这条消息,那么先找到00000000000000000000.index, 然后找到[2487,49111]这个索引,再到 log 文件中,根据 49111 这个 position 开始查找,比较每条消息的 offset 是否大于等于 2490。最后查找到对应的消息以后返回

5.1.9 日志清除策略

============

前面提到过,日志的分段存储,一方面能够减少单个文件内容的大小,另一方面,方便 kafka 进行日志清理。日志的清理策略有两个

  1. 根据消息的保留时间,当消息在 kafka 中保存的时间超过了指定的时间,就会触发清理过程

  2. 根据 topic 存储的数据大小,当 topic 所占的日志文件大小大于一定的阀值,则可以开始删除最旧的消息。 kafka会启动一个后台线程,定期检查是否存在可以删除的消息

通过 log.retention.bytes 和 log.retention.hours 这两个参数来设置,当其中任意一个达到要求,都会执行删除。默认的保留时间是:7 天

5.1.10 日志压缩策略

=============

Kafka 还提供了“日志压缩(Log Compaction)”功能,通过这个功能可以有效的减少日志文件的大小,缓解磁盘紧张的情况,在很多实际场景中,消息的 key 和 value 的值之间的对应关系是不断变化的,就像数据库中的数据会不断被修改一样,消费者只关心 key 对应的最新的 value。 因此,我们可以开启 kafka 的日志压缩功能,服务端会在后台启动启动 Cleaner 线程池,定期将相同的 key 进行合并,只保留最新的 value 值。日志的压缩原理是

消息中间件架构体系 - Kafka:从“入门”到“进阶”,也不是很难

5.1.11 partition 的高可用副本机制

=========================

我们已经知道 Kafka 的每个 topic 都可以分为多个 Partition,并且多个 partition 会均匀分布在集群的各个节点下。虽然这种方式能够有效的对数据进行分片,但是对于每个partition 来说,都是单点的,当其中一个 partition 不可用的时候,那么这部分消息就没办法消费。所以 kafka 为了提高 partition 的可靠性而提供了副本的概念(Replica) ,通过副本机制来实现冗余备份。每个分区可以有多个副本,并且在副本集合中会存在一个leader 的副本,所有的读写请求都是由 leader 副本来进行处理。剩余的其他副本都做为 follower 副本,follower 副本 会 从 leader副本同步消息日志。 这 个 有 点 类 似zookeeper 中 leader 和 follower 的概念,但是具体的时间方式还是有比较大的差异。所以我们可以认为,副本集会存在一主多从的关系。

一般情况下,同一个分区的多个副本会被均匀分配到集群中的不同 broker 上,当 leader 副本所在的 broker 出现故障后,可以重新选举新的 leader 副本继续对外提供服务。通过这样的副本机制来提高 kafka 集群的可用性。

5.1.12 副本分配算法

=============

将所有 N Broker 和待分配的 i 个 Partition 排序.

将第 i 个 Partition 分配到第(i mod n)个 Broker 上.

将第 i 个 Partition 的第 j 个副本分配到第((i + j) mod n)个Broker 上.

5.1.13 kafka 副本机制中的几个概念

=======================

Kafka 分区下有可能有很多个副本(replica)用于实现冗余,从而进一步实现高可用。副本根据角色的不同可分为 3 类:

leader 副本:响应 clients 端读写请求的副本

follower 副本:被动地备份 leader 副本中的数据,不能响应 clients 端读写请求。

ISR 副本:包含了 leader 副本和所有与 leader 副本保持同步的 follower 副本——如何判定是否与 leader 同步后面会提到每个 Kafka 副本对象都有两个重要的属性:LEO 和HW。注意是所有的副本,而不只是 leader 副本。

LEO:即日志末端位移(log end offset),记录了该副本底层日志(log)中下一条消息的位移值。注意是下一条消息!也就是说,如果 LEO=10,那么表示该副本保存了 10 条消息,位移值范围是[0, 9]。另外, leader LEO 和follower LEO 的更新是有区别的。我们后面会详细说

HW:即上面提到的水位值。对于同一个副本对象而言,其

HW 值不会大于 LEO 值。小于等于 HW 值的所有消息都被认为是“ 已备份” 的(replicated )。同理, leader 副本和follower 副本的 HW 更新是有区别的

5.1.14 副本协同机制

=============

刚刚提到了,消息的读写操作都只会由 leader 节点来接收和处理。follower 副本只负责同步数据以及当 leader 副本所在的 broker 挂了以后,会从 follower 副本中选取新的leader。

消息中间件架构体系 - Kafka:从“入门”到“进阶”,也不是很难

请求首先由 Leader 副本处理,之后 follower 副本会从leader 上拉取写入的消息,这个过程会有一定的延迟,导致 follower 副本中保存的消息略少于 leader 副本,但是只要没有超出阈值都可以容忍。但是如果一个 follower 副本出现异常,比如宕机、网络断开等原因长时间没有同步到消息,那这个时候, leader 就会把它踢出去。 kafka 通过 ISR集合来维护一个分区副本信息

ISR

ISR 表示目前“可用且消息量与 leader 相差不多的副本集合,这是整个副本集合的一个子集”。怎么去理解可用和相差不多这两个词呢?具体来说,ISR 集合中的副本必须满足两个条件

  1. 副本所在节点必须维持着与 zookeeper 的连接

  2. 副本最后一条消息的 offset 与 leader 副本的最后一条消息的 offset 之间差 值 不 能 超 过 指定的 阈值(replica.lag.time.max.ms)replica.lag.time.max.ms:如果该 follower 在此时间间隔内一直没有追上过 leader 的所有消息,则该 follower 就会被剔除 isr 列表

➢ ISR 数 据 保 存 在 Zookeeper 的/brokers/topics//partitions//state 节点中

HW&LEO

关于 follower 副本同步的过程中,还有两个关键的概念,HW(HighWatermark)和 LEO(Log End Offset). 这两个参数跟 ISR 集合紧密关联。 HW 标记了一个特殊的 offset,当消费者处理消息的时候,只能拉去到 HW 之前的消息, HW之后的消息对消费者来说是不可见的。也就是说,取partition 对应 ISR 中最小的 LEO 作为 HW,consumer 最多只能消费到 HW 所在的位置。每个 replica 都有 HW,leader 和 follower 各自维护更新自己的 HW 的状态。一条消息只有被 ISR 里的所有 Follower 都从 Leader 复制过去才会被认为已提交。这样就避免了部分数据被写进了Leader,还没来得及被任何 Follower 复制就宕机了,而造成数据丢失(Consumer 无法消费这些数据)。而对于Producer 而言,它可以选择是否等待消息 commit,这可以通过 acks 来设置。这种机制确保了只要 ISR 有一个或以上的 Follower,一条被 commit 的消息就不会丢失。

5.1.15 数据的同步过程

==============

了解了副本的协同过程以后,还有一个最重要的机制,就是数据的同步过程。它需要解决

  1. 怎么传播消息

  2. 在向消息发送端返回 ack 之前需要保证多少个 Replica

已经接收到这个消息

数据的处理过程是

Producer 在 发 布消息到 某 个 Partition 时 ,先通过ZooKeeper 找到该 Partition 的 Leader 【 get /brokers/topics//partitions/2/state】,然后无论该Topic 的 Replication Factor 为多少(也即该 Partition 有多少个 Replica), Producer 只将该消息发送到该 Partition 的Leader。 Leader 会将该消息写入其本地 Log。每个 Follower都从 Leader pull 数据。这种方式上, Follower 存储的数据顺序与 Leader 保持一致。 Follower 在收到该消息并写入其Log 后,向 Leader 发送 ACK。一旦 Leader 收到了 ISR 中的所有 Replica 的 ACK,该消息就被认为已经 commit 了,Leader 将增加 HW(HighWatermark)并且向 Producer 发送ACK。

初始状态

初始状态下,leader 和 follower 的 HW 和 LEO 都是 0,leader 副本会保存 remote LEO,表示所有 follower LEO,也会被初始化为 0,这个时候,producer 没有发送消息。follower 会不断地个 leader 发送 FETCH 请求,但是因为没有数据,这个请求会被leader 寄存,当在指定的时间之后会强 制 完 成请求, 这 个 时间配置是(replica.fetch.wait.max.ms),如果在指定时间内 producer有消息发送过来,那么 kafka 会唤醒 fetch 请求,让 leader继续处理

消息中间件架构体系 - Kafka:从“入门”到“进阶”,也不是很难

这里会分两种情况,第一种是 leader 处理完 producer 请求之后,follower 发送一个 fetch 请求过来、第二种是follower 阻塞在 leader 指定时间之内,leader 副本收到producer 的请求。这两种情况下处理方式是不一样的。先来看第一种情况

一、follower 的 fetch 请求是当 leader 处理消息以后执行的

leader 处理完 producer 请求之后,follower 发送一个fetch 请求过来 。状态图如下

消息中间件架构体系 - Kafka:从“入门”到“进阶”,也不是很难

leader 副本收到请求以后,会做几件事情

  1. 把消息追加到 log 文件,同时更新 leader 副本的 LEO

  2. 尝试更新 leader HW 值。这个时候由于 follower 副本还没有发送 fetch 请求,那么 leader 的 remote LEO 仍然是 0。leader 会比较自己的 LEO 以及 remote LEO 的值发现最小值是 0,与 HW 的值相同,所以不会更新 HW

follower fetch 消息

消息中间件架构体系 - Kafka:从“入门”到“进阶”,也不是很难

follower 发送 fetch 请求,leader 副本的处理逻辑是:

  1. 读取 log 数据、更新 remote LEO=0(follower 还没有写入这条消息,这个值是根据 follower 的 fetch 请求中的offset 来确定的)

  2. 尝试更新 HW,因为这个时候 LEO 和 remoteLEO 还是不一致,所以仍然是 HW=0

  3. 把消息内容和当前分区的 HW值发送给 follower 副本follower 副本收到 response 以后

  4. 将消息写入到本地 log,同时更新 follower 的 LEO

  5. 更新 follower HW,本地的 LEO 和 leader 返回的 HW进行比较取小的值,所以仍然是 0第一次交互结束以后, HW 仍然还是 0,这个值会在下一次follower 发起 fetch 请求时被更新

follower 发第二次 fetch 请求,leader 收到请求以后

消息中间件架构体系 - Kafka:从“入门”到“进阶”,也不是很难

  1. 读取 log 数据

  2. 更新 remote LEO=1, 因为这次 fetch 携带的 offset 是1.

  3. 更新当前分区的 HW,这个时候 leader LEO 和 remoteLEO 都是 1,所以 HW 的值也更新为 1

  4. 把数据和当前分区的 HW 值返回给 follower 副本,这个时候如果没有数据,则返回为空

follower 副本收到 response 以后

  1. 如果有数据则写本地日志,并且更新 LEO

  2. 更新 follower 的 HW 值 到目前为止,数据的同步就完成了,意味着消费端能够消费 offset=0 这条消息。

二、follower 的 fetch 请求是直接从阻塞过程中触发

前面说过,由于 leader 副本暂时没有数据过来,所以follower 的 fetch 会被阻塞,直到等待超时或者 leader 接收到新的数据。当 leader 收到请求以后会唤醒处于阻塞的fetch 请求。处理过程基本上和前面说的一直

  1. leader 将消息写入本地日志,更新 Leader 的 LEO

  2. 唤醒 follower 的 fetch 请求

  3. 更新 HWkafka 使用 HW 和 LEO 的方式来实现副本数据的同步,本身是一个好的设计,但是在这个地方会存在一个数据丢失的问题,当然这个丢失只出现在特定的背景下。我们回想一下, HW 的值是在新的一轮 FETCH 中才会被更新。我们分析下这个过程为什么会出现数据丢失

5.1.16 数据丢失问题

=============

问题描述

前提: min.insync.replicas=1 的时候。 ->设定 ISR 中的最小副本数是多少,默认值为 1, 当且仅当 acks 参数设置为-1(表示需要所有副本确认) 时,此参数才生效. 表达的含义是,至少需要多少个副本同步才能表示消息是提交的所以,当 min.insync.replicas=1 的时候一旦消息被写入 leader 端 log 即被认为是“已提交”,而延迟一轮 FETCH RPC 更新 HW 值的设计使得 follower HW值是异步延迟更新的,倘若在这个过程中 leader 发生变更,那么成为新 leader 的 follower 的 HW 值就有可能是过期的,使得 clients 端认为是成功提交的消息被删除。

数据丢失的解决方案

在 kafka0.11.0.0 版本以后,提供了一个新的解决方案,使用 leader epoch 来解决这个问题, leader epoch 实际上是一对之(epoch,offset), epoch 表示 leader 的版本号,从 0开始,当 leader 变更过 1 次 时 epoch 就会+1,而 offset 则对应于该 epoch 版本的 leader 写入第一条消息的位移。比如说(0,0) ; (1,50); 表示第一个 leader 从 offset=0 开始写消息,一共写了 50 条,第二个 leader 版本号是 1,从 50 条处开始写消息。这个信息保存在对应分区的本地磁盘文件中,文件 名 为 : /tml/kafka-log/topic/leader-epochcheckpointleader broker 中会保存这样的一个缓存,并定期地写入到一个 checkpoint 文件中。

当 leader 写 log 时它会尝试更新整个缓存——如果这个leader 首次写消息,则会在缓存中增加一个条目;否则就不做更新。而每次副本重新成为 leader 时会查询这部分缓存,获取出对应 leader 版本的 offset

如何处理所有的 Replica 不工作的情况?

在 ISR 中至少有一个 follower 时,Kafka 可以确保已经commit 的数据不丢失,但如果某个 Partition 的所有Replica 都宕机了,就无法保证数据不丢失了

  1. 等待 ISR 中的任一个 Replica“活”过来,并且选它作为Leader

  2. 选择第一个“活”过来的 Replica(不一定是 ISR 中的)作为 Leader这就需要在可用性和一致性当中作出一个简单的折衷。如果一定要等待 ISR 中的 Replica“活”过来,那不可用的时间就可能会相对较长。而且如果 ISR 中的所有 Replica 都无法“活”过来了,或者数据都丢失了,这个 Partition 将永远不可用。

选择第一个 “ 活 ” 过来的 Replica 作为 Leader ,而这个Replica 不是 ISR 中的 Replica,那即使它并不保证已经包含了所有已 commit 的消息,它也会成为 Leader 而作为consumer 的数据源(前文有说明,所有读写都由 Leader完成)。

ISR 的设计原理

在所有的分布式存储中,冗余备份是一种常见的设计方式,而常用的模式有同步复制和异步复制,按照 kafka 这个副本模型来说如果采用同步复制,那么需要要求所有能工作的 Follower 副本都复制完,这条消息才会被认为提交成功,一旦有一个follower 副本出现故障,就会导致 HW 无法完成递增,消息就无法提交,消费者就获取不到消息。这种情况下,故障的Follower 副本会拖慢整个系统的性能,设置导致系统不可用.

​ 如果采用异步复制, leader 副本收到生产者推送的消息后,就认为次消息提交成功。follower 副本则异步从 leader 副本同步。这种设计虽然避免了同步复制的问题,但是假设所有follower 副本的同步速度都比较慢他们保存的消息量远远落后于 leader 副本。而此时 leader 副本所在的 broker 突然宕机,则会重新选举新的 leader 副本,而新的 leader 副本中没有原来 leader 副本的消息。这就出现了消息的丢失。

​ kafka 权衡了同步和异步的两种策略,采用 ISR 集合,巧妙解决了两种方案的缺陷:当 follower 副本延迟过高, leader 副本则会把该 follower 副本提出 ISR 集合,消息依然可以快速提交。当 leader 副本所在的 broker 突然宕机,会优先将 ISR 集合中follower 副本选举为 leader,新 leader 副本包含了 HW 之前的全部消息,这样就避免了消息的丢失。

5.1.17 Kafka顺序性保证

=================

Kafka保证消息顺序性的特点如下所示:

topic中的数据分割为一个或多个partition。每个topic至少有一个partition。在单个partition中的数据是有序的,如果消息分散在不同的partition,Kafka 无法保证其顺序性。但只需要确保要求顺序性的若干消息发送到同一个partiton,即可满足其顺序性。并且在进行消息消费的时候,需要确保消费者是进行单线程消费。

要保证若干消息发送到同一个partiton中,那么我们就需要在发送消息的时候指定一个分区的id,那么这样的话消息就被发送到同一个分区中。

// 发送消息到指定的分区,保证分区的消息顺序性

public static void sendMessageToDestPartition() {

for(int x = 0; x < 5; x++) {

// Kafka消息的异步发送

String msg = “Kakfa环境测试…” + x;

kafkaTemplate.send(“test”,0, “order”,

msg).addCallback((obj) ->{

LOGGER.info("send msg to kafka broker success —> {} ",

((SendResult)obj).getProducerRecord().value());

} , (t) ->{

t.printStackTrace();

});

LOGGER.info("send msg to local cache success —> {} ", msg);

}

}

消费者进行指定分区的消费:

@KafkaListener(topicPartitions =

{@org.springframework.kafka.annotation.TopicPartition(topic = “test”,

partitions = “0”)} , groupId = “test.demo”)

public void consumerOrderMessageHandler(String msg, KafkaConsumer

consumer) {

LOGGER.info("consumer topic is : {} , msg is ----> {} ", “test”,

msg);

consumer.commitAsync();

}

5.1.18 Kafka解决消息重复保证

====================

生产者消息重复

问题描述

生产者发送的消息没有收到正确的broke响应,导致producer重试。producer发出一条消息,broker落盘以后因为网络等种种原因发送端得到一个发送失败的响应或者网络中断,然后producer收到一个可恢复的Exception重试消息导致消息重复。

解决方案

解决方案:

1、启动kafka的幂等性

2、retries=0,不重试(可能会丢消息,适用于吞吐量指标重要性高于数据丢失,例如:日志收集)所谓幂等性,就是对接口的多次调用所产生的结果和调用一次是一致的。生产者在进行重试的时候有可能会重复写入消息,而使用Kafka的幂等性功能就可以避免这种情况。

开启幂等性的方式比较简单,我们只需要设置enable.idempotence参数为true就可以了。如下所示:

spring:

kafka:

producer:

bootstrap-servers: 192.168.23.131:9092

acks: all

retries: 2

properties: {‘max.in.flight.requests.per.connection’: 1,“enable.idempotence”:true}

如果使用幂等性,并且我们显示的指定了retries,acks,max.in.flight.requests.per.connection这几个参数,那么就对这几个参数的配置是有要求的:

retries的值必须是大于0,如果设置不对就会抛出如下异常:

Caused by: org.apache.kafka.common.config.ConfigException: Must set retries to non-zero when using the idempotent producer.

max.in.flight.requests.per.connection的值不能大于5,如果设置不对就会抛出如下异常:

Caused by: org.apache.kafka.common.config.ConfigException: Must set

max.in.flight.requests.per.connection to at most 5 to use the idempotent

producer.

acks的取值需要设置为-1/all,如果设置不对就会抛出如下异常:

Caused by: org.apache.kafka.common.config.ConfigException: Must set acks

to all in order to use the idempotent producer. Otherwise we cannot

guarantee idempotence.

幂等性原理介绍:

为了实现生产者幂等性,Kafka为此引入了producer id(PID) 和序列号(sequence number)这两个概念,每个新的生产者实例在初始化的时候都会被分配一个PID ,这个PID对用户而言是完全透明的。对于每个PID,消息发送到的每一个分区都有对应的序列号,这些序列号从0开始单调递增。生产者每发送一条消息就会将<PID , 分区>对应的序号的值加1。

broker端会在内存中为每一对<PID , 分区>维护一个序列号。对于收到的每一条消息,会存在这样的几种情

况:

1、SN_new = SN_old + 1时,broker才会接收它。

2、SN_new < SN_old + 1,那么说明消息被重复写入,broker可以直接将其丢弃。

3、SN_new > SN_old + 1,那么说明中间有数据尚未写入,出现了乱序,暗示可能有消息丢失,对应的生产者会抛出OutOfOrderSquenceException,这个异常时一个严重的异常。幂等性不能跨分区实现。

相关知识

幂等性并不能跨多个分区运作,比如我们现在要想发送3个消息,当第二个消息发送完毕以后程序报错了,这样第三个消息就没有发送成功,当下一次在调用这个方法发送数据的时候,就会导致消息重复发送(失去了幂等性)。而事务可以弥补这个缺憾,事务可以保证对多个分区写入操作的原子性。操作的原子性是指多个操作要么全部成功,要么全部失败,不存在部分成功部分失败的可能。为了实现事务,应用程序必须提供唯一的transactionalId,这个参数通过客户

端程序来进行设定。如下所示:

spring.kafka.producer.transaction-id-prefix=order_tx. # 表示开启事务机制

并且事务机制的使用需要幂等性的支持,所以我们还需要开启幂等性:enable.idempotence = true。如果没有开启幂等性的支持,就会报错,如下所示:

Caused by: org.apache.kafka.common.config.ConfigException: Cannot set a transactional.id without also enabling idempotence.

事务机制实现的两种方式:

1、第一种方式

// 事务消息发送的第一种方式

public static void sendTransactionMessageMethod01() {

// 发送事务消息

kafkaTemplate.executeInTransaction((operations) ->{

// 发送消息

kafkaTemplate.send(“itcast”, 0, “order”, “事务消息----> 1”)

;

kafkaTemplate.send(“itcast”, 0, “order”, “事务消息----> 2”)

;

// 产生异常代码

int a = 1 / 0;

kafkaTemplate.send(“itcast”, 0, “order”, “事务消息----> 3”)

;

// 返回true,表示发送成功

return true;

}) ;

LOGGER.info("send transaction message to local cache success ");

}

2、第二种方式

@Transactional(rollbackFor = RuntimeException.class)

public void sendTransactionMessage() {

// 发送消息

kafkaTemplate.send(“itcast”, 0, “order”, “事务消息----> 1”) ;

kafkaTemplate.send(“itcast”, 0, “order”, “事务消息----> 2”) ;

// 产生异常代码

int a = 1 / 0;

kafkaTemplate.send(“itcast”, 0, “order”, “事务消息----> 3”) ;

}

消费端消息重复

1、根本原因

数据消费完没有及时提交offset到broker。

解决方案

1、取消自动自动提交

每次消费完或者程序退出时手动提交。

2、下游做幂等

一般的解决方案是让下游做幂等。

5.2 Kafka为什么快?

==============

5.2.1 分区管理

==========

Kafka可以将主题划分为多个分区(Partition),会根据分区规则选择把消息存储到哪个分区中,只要分区

规则设置的合理,那么所有的消息将会被均匀的分布到不同的分区中,这样就实现了负载均衡和水平扩展。另外,多个订阅者可以从一个或者多个分区中同时消费数据,以支撑海量数据处理能力。顺便说一句,由于消息是以追加的方法存储到分区中的,多个分区顺序写磁盘的总效率要比随机写内存还要高(引用Apache Kafka – A High Throughput Distributed Messaging System的观点),是Kafka高吞吐率的重要保证之一。

5.2.2 分区副本机制

============

由于Producer和Consumer都只会与Leader角色的分区副本相连,所以kafka需要以集群的组织形式提供主题下的消息高可用。kafka支持主备复制,所以消息具备高可用和持久性。

一个分区可以有多个副本,这些副本保存在不同的broker上。每个分区的副本中都会有一个作为Leader。当

一个broker失败时,Leader在这台broker上的分区都会变得不可用,kafka会自动移除Leader,再其他副本中选一个作为新的Leader。在通常情况下,增加分区可以提供kafka集群的吞吐量。然而,也应该意识到集群的总分区数或是单台服务器上的分区数过多,会增加不可用及延迟的风险。

5.2.3 分区leader选举

================

可以预见的是,如果某个分区的Leader挂了,那么其它跟随者将会进行选举产生一个新的leader,之后所有的读写就会转移到这个新的Leader上,在kafka中,其不是采用常见的多数选举的方式进行副本的Leader选举,而是会在Zookeeper上针对每个T opic维护一个称为ISR(in-sync replica,已同步的副本)的集合,显然还有一些副本没有来得及同步。只有这个ISR列表里面的才有资格成为leader(先使用ISR里面的第一个,如果不行依次类推,因为ISR里面的是同步副本,消息是最完整且各个节点都是一样的)。通过ISR,kafka可以容忍的失败数比较高。

假设某个topic有f+1个副本,kafka可以容忍f个不可用,当然如果全部ISR里面的副本都不可用,也可以选择其他可用的副本,只是存在数据的不一致。

5.2.4 分区重新分配

============

我们往已经部署好的Kafka集群里面添加机器是最正常不过的需求,而且添加起来非常地方便,我们需要做

的事是从已经部署好的Kafka节点中复制相应的配置文件,然后把里面的

broker id修改成全局唯一的,最后启动这个节点即可将它加入到现有Kafka集群中。但是问题来了,新添加

的Kafka节点并不会自动地分配数据,所以无法分担集群的负载,除非我

们新建一个topic。但是现在我们想手动将部分分区移到新添加的Kafka节点上,Kafka内部提供了相关的工具

来重新分布某个topic的分区。

具体的实现步骤如下所示:

1、比如某一个主题的分区信息如下所示:

消息中间件架构体系 - Kafka:从“入门”到“进阶”,也不是很难

2、给某一个分区在添加一个新的分区

bin/kafka-topics.sh --alter --zookeeper 172.19.0.61:2181 --topic demo

–partitions 4

添加完毕以后,分区的信息如下所示:

消息中间件架构体系 - Kafka:从“入门”到“进阶”,也不是很难

这样会导致3个Broker上有重新维护了更多的分区节点。

3、再次创建一个kafka的容器

docker run -di --network=host --name=kafka_04 -v

/etc/localtime:/etc/localtime --privileged=true

wurstmeister/kafka:latest /bin/bash

查看itheima主题的分区情况,如下所示

消息中间件架构体系 - Kafka:从“入门”到“进阶”,也不是很难

和之前没有任何的变化。

3、修改集群配置文件

broker.id=3 # 表示broker的编号,如

果集群中有多个broker,则每个broker的编号需要设置的不同

port=9095 # 端口号

listeners=PLAINTEXT://192.168.23.131:9095 # brokder对外提供的服

务入口地址

log.dirs=/tmp/kafka-logs # 设置存放消息日志文件

的地址

zookeeper.connect=172.19.0.61:2181,172.19.0.62:2181,172.19.0.63:2181

Kafka所需Zookeeper集群地址,Zookeeper和Kafka都安装本机

4、重新分配

现在我们需要将原先分布在broker 1-3节点上的分区重新分布到broker 1-4节点上,借助kafkareassignpartitions.sh工具生成reassign plan,不过我们先得按照要求定义一个文件,里面说明哪些topic需要重新分区,文件内容如下:

demo@Server-node:/mnt/d/kafka-cluster/kafka-1$ cat reassign.json

{“topics”:[{“topic”:“demo”}],

“version”:1

}

然后使用 kafka-reassign-partitions.sh 工具生成reassign plan

bin/kafka-reassign-partitions.sh --zookeeper 172.19.0.61:2181 --topics-to-move-json-file

reassign.json --broker-list “0,1,2,3” --generate

命令会输出两个字符串,如下所示:

Current partition replica assignment

{“version”:1,“partitions”:[{“topic”:“demo”,“partition”:2,“replicas”:

[2,1,0],“log_dirs”:[“any”,“any”,“any”]},

{“topic”:“demo”,“partition”:1,“replicas”:[0,2,1],“log_dirs”:

[“any”,“any”,“any”]},{“topic”:“itheima”,“partition”:0,“replicas”:

[1,0,2],“log_dirs”:[“any”,“any”,“any”]},

{“topic”:“demo”,“partition”:3,“replicas”:[1,2,0],“log_dirs”:

[“any”,“any”,“any”]}]}

Proposed partition reassignment configuration

{“version”:1,“partitions”:[{“topic”:“demo”,“partition”:2,“replicas”:

[3,1,2],“log_dirs”:[“any”,“any”,“any”]},

{“topic”:“demo”,“partition”:1,“replicas”:[2,0,1],“log_dirs”:

[“any”,“any”,“any”]},{“topic”:“demo”,“partition”:3,“replicas”:

[0,2,3],“log_dirs”:[“any”,“any”,“any”]},

{“topic”:“demo”,“partition”:0,“replicas”:[1,3,0],“log_dirs”:

[“any”,“any”,“any”]}]}

第一个JSON内容为当前的分区副本分配情况,第二个为重新分配的候选方案,注意这里只是生成一份可行性的方案,并没有真正执行重分配的动作。

我们将第二个JSON内容保存到名为result.json文件里面(文件名不重要,文件格式也不一定要以json为结尾,只要保证内容是json即可),然后执行这些reassign plan。如下所示:

执行分配策略

bin/kafka-reassign-partitions.sh --zookeeper 172.19.0.61:2181 --reassignment-json-file result.json --execute

执行完毕以后,看出分区信息:

Spring全套教学资料

Spring是Java程序员的《葵花宝典》,其中提供的各种大招,能简化我们的开发,大大提升开发效率!目前99%的公司使用了Spring,大家可以去各大招聘网站看一下,Spring算是必备技能,所以一定要掌握。

目录:

部分内容:

Spring源码

  • 第一部分 Spring 概述
  • 第二部分 核心思想
  • 第三部分 手写实现 IoC 和 AOP(自定义Spring框架)
  • 第四部分 Spring IOC 高级应用
    基础特性
    高级特性
  • 第五部分 Spring IOC源码深度剖析
    设计优雅
    设计模式
    注意:原则、方法和技巧
  • 第六部分 Spring AOP 应用
    声明事务控制
  • 第七部分 Spring AOP源码深度剖析
    必要的笔记、必要的图、通俗易懂的语言化解知识难点

脚手框架:SpringBoot技术

它的目标是简化Spring应用和服务的创建、开发与部署,简化了配置文件,使用嵌入式web服务器,含有诸多开箱即用的微服务功能,可以和spring cloud联合部署。

Spring Boot的核心思想是约定大于配置,应用只需要很少的配置即可,简化了应用开发模式。

  • SpringBoot入门
  • 配置文件
  • 日志
  • Web开发
  • Docker
  • SpringBoot与数据访问
  • 启动配置原理
  • 自定义starter

微服务架构:Spring Cloud Alibaba

同 Spring Cloud 一样,Spring Cloud Alibaba 也是一套微服务解决方案,包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。

  • 微服务架构介绍
  • Spring Cloud Alibaba介绍
  • 微服务环境搭建
  • 服务治理
  • 服务容错
  • 服务网关
  • 链路追踪
  • ZipKin集成及数据持久化
  • 消息驱动
  • 短信服务
  • Nacos Confifig—服务配置
  • Seata—分布式事务
  • Dubbo—rpc通信

Spring MVC

目录:

部分内容:

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

目标是简化Spring应用和服务的创建、开发与部署,简化了配置文件,使用嵌入式web服务器,含有诸多开箱即用的微服务功能,可以和spring cloud联合部署。

Spring Boot的核心思想是约定大于配置,应用只需要很少的配置即可,简化了应用开发模式。

  • SpringBoot入门
  • 配置文件
  • 日志
  • Web开发
  • Docker
  • SpringBoot与数据访问
  • 启动配置原理
  • 自定义starter

[外链图片转存中…(img-XhWWOSUx-1715350838418)]

[外链图片转存中…(img-jMWDwCFs-1715350838418)]

微服务架构:Spring Cloud Alibaba

同 Spring Cloud 一样,Spring Cloud Alibaba 也是一套微服务解决方案,包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。

  • 微服务架构介绍
  • Spring Cloud Alibaba介绍
  • 微服务环境搭建
  • 服务治理
  • 服务容错
  • 服务网关
  • 链路追踪
  • ZipKin集成及数据持久化
  • 消息驱动
  • 短信服务
  • Nacos Confifig—服务配置
  • Seata—分布式事务
  • Dubbo—rpc通信

[外链图片转存中…(img-JN9EJ9co-1715350838419)]

[外链图片转存中…(img-JNAYUxDh-1715350838419)]

Spring MVC

目录:

[外链图片转存中…(img-FU2MufT0-1715350838420)]

[外链图片转存中…(img-rFpQGnAZ-1715350838420)]

[外链图片转存中…(img-gwFRLWqe-1715350838420)]

部分内容:

[外链图片转存中…(img-z0TcrgIt-1715350838420)]

[外链图片转存中…(img-um279zKH-1715350838421)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 28
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值