Kafka的架构原理-你真的理解吗 (kafka的作用)

教程大全 2025-07-15 12:27:57 浏览

Kafka的架构原理,你真的理解吗?

2018-08-20 08:30:05Apache Kafka 最早是由 LinkedIn 开源出来的分布式消息系统,现在是 Apache 旗下的一个子项目,并且已经成为开源领域应用最广泛的消息系统之一。

Apache Kafka 最早是由 LinkedIn 开源出来的分布式消息系统,现在是 Apache 旗下的一个子项目,并且已经成为开源领域应用最广泛的消息系统之一。

Kafka 社区非常活跃,从 0.9 版本开始,Kafka 的标语已经从“一个高吞吐量,分布式的消息系统”改为”一个分布式流平台”。

Kafka 和传统的消息系统不同在于:

Kafka 和其他消息队列的对比:

代码如下:

代码如下:

对于 Kafka 的架构原理,我们先提出如下几个问题:

在一套 Kafka 架构中有多个 Producer,多个 Broker,多个 Consumer,每个 Producer 可以对应多个 TOPic,每个 Consumer 只能对应一个 Consumer Group。

整个 Kafka 架构对应一个 ZK 集群,通过 ZK 管理集群配置,选举 Leader,以及在 Consumer Group 发生变化时进行 Rebalance。

在 Kafka 中的每一条消息都有一个 Topic。一般来说在我们应用中产生不同类型的数据,都可以设置不同的主题。

一个主题一般会有多个消息的订阅者,当生产者发布消息到某个主题时,订阅了这个主题的消费者都可以接收到生产者写入的新消息。

Kafka 为每个主题维护了分布式的分区(Partition)日志文件,每个 Partition 在 Kafka 存储层面是 Append Log。

任何发布到此 Partition 的消息都会被追加到 Log 文件的尾部,在分区中的每条消息都会按照时间顺序分配到一个单调递增的顺序编号,也就是我们的 Offset。Offset 是一个 Long 型的数字。

我们通过这个 Offset 可以确定一条在该 Partition 下的唯一消息。在 Partition 下面是保证了有序性,但是在 Topic 下面没有保证有序性。

在上图中我们的生产者会决定发送到哪个 Partition:

如果没有 Key 值则进行轮询发送。

如果有 Key 值,对 Key 值进行 Hash,然后对分区数量取余,保证了同一个 Key 值的会被路由到同一个分区;如果想队列的强顺序一致性,可以让所有的消息都设置为同一个 Key。

消息由生产者发送到 Kafka 集群后,会被消费者消费。一般来说我们的消费模型有两种:

基于推送模型的消息系统,由消息代理记录消费状态。消息代理将消息推送到消费者后,标记这条消息为已经被消费,但是这种方式无法很好地保证消费的处理语义。

比如当我们已经把消息发送给消费者之后,由于消费进程挂掉或者由于网络原因没有收到这条消息,如果我们在消费代理将其标记为已消费,这个消息就***丢失了。

如果我们利用生产者收到消息后回复这种方法,消息代理需要记录消费状态,这种不可取。

如果采用 Push,消息消费的速率就完全由消费代理控制,一旦消费者发生阻塞,就会出现问题。

Kafka 采取拉取模型(Poll),由自己控制消费速度,以及消费的进度,消费者可以按照任意的偏移量进行消费。

比如消费者可以消费已经消费过的消息进行重新处理,或者消费最近的消息等等。

Kafka Client:单线程 Selector

单线程模式适用于并发链接数小,逻辑简单,数据量小的情况。在 Kafka 中,Consumer 和 Producer 都是使用的上面的单线程模式。

这种模式不适合 Kafka 的服务端,在服务端中请求处理过程比较复杂,会造成线程阻塞,一旦出现后续请求就会无法处理,会造成大量请求超时,引起雪崩。而在 服务器 中应该充分利用多线程来处理执行逻辑。

Kafka server:多线程 Selector

在 Kafka 服务端采用的是多线程的 Selector 模型,Acceptor 运行在一个单独的线程中,对于读取操作的线程池中的线程都会在 Selector 注册 Read 事件,负责服务端读取请求的逻辑。

成功读取后,将请求放入 Message Queue共享队列中。然后在写线程池中,取出这个请求,对其进行逻辑处理。

这样,即使某个请求线程阻塞了,还有后续的线程从消息队列中获取请求并进行处理,在写线程中处理完逻辑处理,由于注册了 OP_WIRTE 事件,所以还需要对其发送响应。

高可靠分布式存储模型

在 Kafka 中保证高可靠模型依靠的是副本机制,有了副本机制之后,就算机器宕机也不会发生数据丢失。

高性能的日志存储

Kafka 一个 Topic 下面的所有消息都是以 Partition 的方式分布式的存储在多个节点上。

同时在 Kafka 的机器上,每个 Partition 其实都会对应一个日志目录,在目录下面会对应多个日志分段(LogSegment)。

LogSegment 文件由两部分组成,分别为“.index”文件和“.log”文件,分别表示为 Segment 索引文件和数据文件。

这两个文件的命令规则为:Partition 全局的***个 Segment 从 0 开始,后续每个 Segment 文件名为上一个 Segment 文件***一条消息的 Offset 值,数值大小为 64 位,20 位数字字符长度,没有数字用 0 填充。

如下,假设有 1000 条消息,每个 LogSegment 大小为 100,下面展现了 900-1000 的索引和 Log:

由于 Kafka 消息数据太大,如果全部建立索引,既占了空间又增加了耗时,所以 Kafka 选择了稀疏索引的方式,这样索引可以直接进入内存,加快偏查询速度。

简单介绍一下如何读取数据,如果我们要读取第 911 条数据首先***步,找到它是属于哪一段的。

根据二分法查找到它属于的文件,找到 0000900.index 和 00000900.log 之后,然后去 index 中去查找 (911-900) = 11 这个索引或者小于 11 最近的索引。

在这里通过二分法我们找到了索引是 [10,1367],然后我们通过这条索引的物理位置 1367,开始往后找,直到找到 911 条数据。

上面讲的是如果要找某个 Offset 的流程,但是我们大多数时候并不需要查找某个 Offset,只需要按照顺序读即可。

而在顺序读中,操作系统会在内存和磁盘之间添加 Page Cache,也就是我们平常见到的预读操作,所以我们的顺序读操作时速度很快。

但是 Kafka 有个问题,如果分区过多,那么日志分段也会很多,写的时候由于是批量写,其实就会变成随机写了,随机 I/O 这个时候对性能影响很大。所以一般来说 Kafka 不能有太多的 Partition。

针对这一点,RocketMQ 把所有的日志都写在一个文件里面,就能变成顺序写,通过一定优化,读也能接近于顺序读。

大家可以思考一下:

Kafka 的副本机制是多个服务端节点对其他节点的主题分区的日志进行复制。

当集群中的某个节点出现故障,访问故障节点的请求会被转移到其他正常节点(这一过程通常叫 Reblance)。

Kafka 每个主题的每个分区都有一个主副本以及 0 个或者多个副本,副本保持和主副本的数据同步,当主副本出故障时就会被替代。

在 Kafka 中并不是所有的副本都能被拿来替代主副本,所以在 Kafka 的 Leader 节点中维护着一个 ISR(In Sync Replicas)集合。

翻译过来也叫正在同步中集合,在这个集合中的需要满足两个条件:

另外还有个 AR(Assigned Replicas)用来标识副本的全集,OSR 用来表示由于落后被剔除的副本集合。

所以公式如下:ISR = Leader + 没有落后太多的副本;AR = OSR+ ISR。

这里先要说下两个名词:HW(高水位)是 Consumer 能够看到的此 Partition 的位置,LEO 是每个 Partition 的 Log ***一条 Message 的位置。

HW 能保证 Leader 所在的 Broker 失效,该消息仍然可以从新选举的 Leader 中获取,不会造成消息丢失。

当 Producer 向 Leader 发送数据时,可以通过 request.required.acks 参数来设置数据可靠性的级别:

但是这样也不能保证数据不丢失,比如当 ISR 中只有 Leader 时(其他节点都和 ZK 断开连接,或者都没追上),这样就变成了 acks = 1 的情况。

在分布式系统中一般有三种处理语义:

至少一次,有可能会有多次。如果 Producer 收到来自 Ack 的确认,则表示该消息已经写入到 Kafka 了,此时刚好是一次,也就是我们后面的 Exactly-once。

但是如果 Producer 超时或收到错误,并且 request.required.acks 配置的不是 -1,则会重试发送消息,客户端会认为该消息未写入 Kafka。

如果 Broker 在发送 Ack 之前失败,但在消息成功写入 Kafka 之后,这一次重试将会导致我们的消息会被写入两次。

所以消息就不止一次地传递给最终 Consumer,如果 Consumer 处理逻辑没有保证幂等的话就会得到不正确的结果。

在这种语义中会出现乱序,也就是当***次 Ack 失败准备重试的时候,但是第二消息已经发送过去了,这个时候会出现单分区中乱序的现象。

我们需要设置 Prouducer 的参数 max.in.flight.requests.per.Connection,flight.requests 是 Producer 端用来保存发送请求且没有响应的队列,保证 Produce r端未响应的请求个数为 1。

如果在 Ack 超时或返回错误时 Producer 不重试,也就是我们讲 request.required.acks = -1,则该消息可能最终没有写入 Kafka,所以 Consumer 不会接收消息。

刚好一次,即使 Producer 重试发送消息,消息也会保证最多一次地传递给 Consumer。该语义是最理想的,也是最难实现的。

在 0.10 之前并不能保证 exactly-once,需要使用 Consumer 自带的幂等性保证。0.11.0 使用事务保证了。

如何实现 exactly-once

要实现 exactly-once 在 Kafka 0.11.0 中有两个官方策略:

单 Producer 单 Topic

每个 Producer 在初始化的时候都会被分配一个唯一的 PID,对于每个唯一的 PID,Producer 向指定的 Topic 中某个特定的 Partition 发送的消息都会携带一个从 0 单调递增的 Sequence Number。

在我们的 Broker 端也会维护一个维度为,每次提交一次消息的时候都会对齐进行校验:

上面所说的解决了两个问题:

上面所说的都是在同一个 PID 下面,意味着必须保证在单个 Producer 中的同一个 Seesion 内,如果 Producer 挂了,被分配了新的 PID,这样就无法保证了,所以 Kafka 中又有事务机制去保证。

事务

在 Kafka 中事务的作用是:

事务可以保证就算跨多个,在本次事务中的对消费队列的操作都当成原子性,要么全部成功,要么全部失败。

并且,有状态的应用也可以保证重启后从断点处继续处理,也即事务恢复。

在 Kafka 的事务中,应用程序必须提供一个唯一的事务 ID,即 Transaction ID,并且宕机重启之后,也不会发生改变。

Transactin ID 与 PID 可能一一对应,区别在于 Transaction ID 由用户提供,而 PID 是内部的实现对用户透明。

为了 Producer 重启之后,旧的 Producer 具有相同的 Transaction ID 失效,每次 Producer 通过 Transaction ID 拿到 PID 的同时,还会获取一个单调递增的 Epoch。

由于旧的 Producer 的 Epoch 比新 Producer 的 Epoch 小,Kafka 可以很容易识别出该 Producer 是老的,Producer 并拒绝其请求。

为了实现这一点,Kafka 0.11.0.0 引入了一个服务器端的模块,名为 Transaction Coordinator,用于管理 Producer 发送的消息的事务性。

该 Transaction Coordinator 维护 Transaction Log,该 Log 存于一个内部的 Topic 内。

由于 Topic 数据具有持久性,因此事务的状态也具有持久性。Producer 并不直接读写 Transaction Log,它与 Transaction Coordinator 通信,然后由 Transaction Coordinator 将该事务的状态插入相应的 Transaction Log。

Transaction Log 的设计与 Offset Log 用于保存 Consumer 的 Offset 类似。

关于消息队列或者 Kafka 的一些常见的面试题,通过上面的文章可以提炼出以下几个比较经典的问题,大部分问题都可以从上面总结后找到答案:

作者:李钊

简介:目前就职于美团点评餐饮生态技术部,喜欢钻研阅读开源源码。招3年以上Java开发,请发送简历到邮箱:[emailprotected]。


request.getRequestDispatcher().forward(request.response)中的那两个参数是哪里来的?

Web是请求/响应架构的使用,而request和response就是在服务器端生成的相应的两个对象,request能够获取客户端传递的参数及相关的一些信息,而response就是给客户端响应相关的页面及信息。 ()()这个语句意思是将客户端的请求转向(forward)到getRequestDispatcher()方法中参数定义的页面或者链接。 说通俗点就是,当一个客户端的请求到这个页面后,不做处理或者不处理完,将请求转给另一个页面处理,然后再响应给客户端。 建议在学JSP的时候,好好学学servlet,servlet是本质,好好看看servlet的API.可能表述上不是很严谨,自己好好理解,加油!

荷叶母亲中的“母亲啊!你是荷叶,我是红莲”对这句话是怎么理解的?在文章结构上有何作用

这几句话写出了母亲是保护自己度过人生坎坷与磨难的人,母爱是伟大的,可以使我的痛苦得到安慰,失望中得到希望,在冷落时得到幸福,表达了子女对母爱的赞颂,结构上深化了主题。

生存模式杀怪掉的BUFF有哪些?能详细说明下作用吗?

就是杀怪得的金钱双倍 全员有效只有 秒 这个没有什么好分析的 建议在怪多的时候拿 要不就算你拿了没杀死怪也没用啊

也是全员有效 秒射击速度双倍 这个在每关后期清兵的时候都是很有用的 还是建议怪多的时候拿 这样能很有效的杀怪 不过如果换弹时间长就不好了。 。 要是用这个最好拿一些子弹多的武器

还是全员有效 秒移动速度双倍 有的人可能要问了这个和杀怪总算没关系了吧 对这个是和杀怪没关系 这是个逃命用的 因为如果你双倍移动速度了即使是最快的小兵可能都追不上你(前提你不拿锯子之类重武器) 吃完之后可以边绕地图快速跑边补给 和弹药

秒钟内房间内怪物速度减缓 不过只是对房间内有效的。。。貌似房间外面的只有进到房间内才能被减速至于减的速度没研究过 不过小兵减速后基本就是比摁着 走还慢呢 这个道具也能有效的保命

好东西 全体队员不管多少血 全部补满!这个没有 秒什么的。 。 。 就是瞬间补满

这个道具定位是保命 省去了去补给点补血的路程和钱这个也可以救人 比如在队友被怪围住红血的时候捡起来 队友又满血了

也是好东西 这个也是全员有效相比于补给点 这个的好处是不用花 块钱 而且最最重要的是它连你第二把主武器(花 元随机买的)也能补给 关键时刻往往很有用处

你真的理解吗 这个牛 道具!!!!场内所有怪物全部秒杀!!!管你多少只多少血通通得死!!!

清怪第一神器 不过我觉得出现率相比其他的少的可怜有生之年我只用过 次 每次都是很好的效果。

本文版权声明本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,请联系本站客服,一经查实,本站将立刻删除。

发表评论

热门推荐