Kafka 探险 – 架构简介
这个 Kafka 的专题,我会从系统整体架构,设计到代码落地。和大家一起杠源码,学技巧,涨知识。希望大家持续关注一起见证成长!
我相信:技术的道路,十年如一日!十年磨一剑!
简介
Kafka 是一种分布式的,基于发布 / 订阅的消息系统。最初被 LinkedIn 开发,并在 2011 年初开源,2012 年 10 月从 Apache 孵化器破壳而出,成为 Apache 的顶级项目。
Kafka 最初被设计的目的是 LinkedIn 流量和运维数据分析。流量数据包含 PV (Page View) , UV (Unique Visitor) ,搜索数据,详情页数据等。在高并发场景对于这些数据的统计并非实时的,不是简单的对于数据库的某个字段数据量 +1 这么简单,超大的流量洪峰下并不能因为统计数据将业务主流程阻塞。所以通常会将这些数据记录在文件或大数据存储引擎中,然后周期性的进行统计分析。
Kafka 被越来越多的公司青睐主要和他的特性优势有关:
- 以 O(1) 时间复杂度消息持久化,对于 TB 级别的数据也能够保证 O(1) 的访问效率
- 支持批量
读
和
写
数据,并且对于数据进行压缩保证高吞吐
- 支持消息分区,分布式发送,分布式消费,便于水平扩展 (Scale out),具有很高的并发能力
应用场景
那为何需要使用消息队列,或者说在什么场景下 Kafka 更加合适
解耦
在大数据,高并发的场景下为了突破性能瓶颈会对系统进行水平扩展和垂直拆分,将一个复杂的系统拆分多个独立,纯净的子系统。数据在各个系统之间流转,但是如果某一个服务处理速度过慢,就会拖累整个链路的性能,形成瓶颈降低整个系统的性能,造成“旱的旱死涝的涝死”的局面。
举个简单例子:在淘宝下单时,交易系统完成扣款,后续会有很多动作:提醒卖家发货,生成卖家工作流,核销优惠券,增加购物积分等等,如果这一步全部写到交易系统的扣款代码之后,很有可能交易系统就会被拖死,下游任何一个环节失败也会导致扣款回滚,并且如果需要添加一个新的动作需要交易去做大量修改,设计肯定是不合理的。实际上交易系统在处理完扣款后会发送一个扣款完成消息,下游接这个消息即可,下游失败不会影响核心流程失败,并且各个系统的边界更加清楚,分层更更加合理。
数据持久化
如今的应用程序基本都会涉及到多个系统之间的对接,数据在系统之间通过 RPC 进行传递,处理数据的过程失败就会导致数据丢失,除非数据被持久化到磁盘上。而 Kafka 将所有需要流转的数据都
持久化到磁盘上
,保证数据不会丢失。另外还有一个很重要的能力就是保留现场便于后续问题排查跟踪,经历过系统失败但是无法复现的人才会体会到的痛!
为了保证磁盘上的数据不会爆炸式疯涨,Kafka 提供了数据清理,数据压缩等功能,清除处理完成的历史数据。
扩展性
在应用的访问量剧增的情况下,代码优化往往没有直接进行水平扩展来的那么及时。诊断,分析,方案,优化,验证 一系列复杂流程让代码优化看起来只能是一个从长计议的方案。这时止血的方案只能是降级,限流,扩机器 三板斧。Kafka 的扩展性主要就体现在能热扩容,不需要修改参数,不需要修改代码,上机器 -> 注册服务 就完成了扩容。并非所有系统都具备这个像
调节音量旋钮一样简单的提高系统性能
的能力,这里会涉及到扩容之前的数据是否会有热点,新节点对集群的同步,流量重分配等等一系列复杂流程。
容灾
系统的部分组件失败不会影响这个系统的运行,消息队列降低了进程间的耦合度,上游或者下游服务挂掉后不会影响其他系统的运行,在服务重新在线后能够继续处理之前未处理的数据,只是会存在一定的延时但是能够保证
最终业务正确性
。
保序
强哥:你这瓜保熟吗?哦不,你这队列保序吗?
在大多数场景下,数据处理顺序是至关重要的,顺序错乱很可能导致数据结果错误。除非这个处理过程是无状态的,此时消息只是起到事件触发的作用,触发下游进行计算。Kafka 可以保证分区内部有序而不能保证全局有序。
核心概念
架构图
上图是一个典型的 Kafka 架构图,左边为消息生产者(Producer) ,发送消息到一个特定的主题(Topic),由于 Kafka 的分布式设计每个 Topic 被分成多个分区,因此发送到每个 Topic 的消息会被存储到对应的分区。另外如果 Topic 设置了副本,则每个分区都会有对应的副本。这些 Topic 被不同的消费者(Consumer)订阅,如果两个消费者在同一个消费者组,那么里面的消费者只能订阅一个固定的分区。
用上图的 Topic A 举例, Producer 1 发送消息到 Topic-A ,消息会在存放在 Broker-2 和 Broker-3 的两个分区上,并且由于 Topic-A 开启了分区备份,所以每个分区都会由另外一个节点 Topic-A\’ 备份分区数据 。发送到 Broker 的数据会被消费者订阅,由于 Consumer-1 和 Consumer-2 在同一个消费者组中,他们只能消费一个固定分区的消息, Consumer-1 只会接收到 Topic-A Partition-1 的消息,Consumer-2 只会接收到 Topic-A Partition-0 的消息。
Broker
在 Kafka 集群中的一个 Kafka Server 就是一个 Broker ,生产者将消息投递到 Broker ,Broker 保证消息的 持久化,容灾,准确性等。同时接受消费者的消息订阅,向消费者分发消息。一般来说在生产环境一台 Kafka 服务器就是一个 Broker。
Topic & Partition & Log
Topic 可以认为是用来存储消息的逻辑概念,可简单认为他是一个
信箱
。每条消息发送的时候都需要指定需要发送到哪个 Topic ,消息被消费的时候也需要指定消费哪个 Topic 中的消息。
Kafka 为了提高可扩展性以及吞吐量,Topic 被分成多个分区 (Partition) ,每个 Partition 对应一个 Log,Log 是一个逻辑概念, 它会对应服务器上一个文件夹,这个文件夹下存放的是这个 Partition 下所有的消息数据和消息索引。在面对海量数据的时候,为了避免出现巨大文件出现 I/O 瓶颈,Kafka 又将 Log 分为多个 Segment 。每个 Segment 包含
log 文件
和
index 文件
文件命名是以该 Segment 第一条消息的 offset 命名。这样说下来其实还是很绕的直接看下面的架构图,可以仔细留意一下各个部分的标识和数字再结合这段文字,理解起来应该就很轻松了。
另外因为 Kafka 采用顺序 I/O,顺序 I/O 效率非常高,甚至比随机写内存效率更高,这也是 Kafka 高性能的原因之一。
Replication
在生产环境中,我们一般会开启 Kafka 消息冗余特性,每个 Partition 都有 1 个或多个副本,我们称之为 Replication。当分区只有一个副本的时候,该分区数据只保留了一份。每个分区副本都会选出一个 Leader ,
Leader 是所有读写请求的 “接口人”
,其余副本均为 Follower 。Follower 作用有两个:拉取 Leader 的 Log 数据做
备份
,在 Leader 失败后作为候选人
参与 Leader 选举
。
Producer
消息产出的源头,通过一定的策略推送到 Topic 的各个分区 。这里所说的推送策略就是消息路由机制,Kafka 内置多种策略可选例如:按照消息 Key ,轮训等等,甚至用户可以写扩展代码来自定义路由策略。
Consumer & Consumer Group
消费者(Consumer)
主要工作是从 Broker 拉取消息,进行消费处理。每个消费者维护自己的消费进度,这样的设计有诸多好处,比如:每个消费者进度能够轻松的进行区分,并且可以修改单个消费者的消费位点跳过或者重新消费某些消息,避免了位点信息的集中化管理的单点故障问题。
现在的应用程序大部分为分布式的系统,一个应用有几十台上百台服务器,这些服务器上运行着相同的代码,那么一个消息过来,每台服务器都执行一次消费逻辑,岂不是会造成巨大的问题。
所以 Kafka 引入了一个新的概念:
消费者组(Consumer Group)
。我们可以将这些应用的服务器都放到同一个消费者组中,而 Kafka 规定一条消息只能被同一个消费者组中的一个消费者消费,这样就能完美避免分布式情况下的重复消费问题了。上面所说的情况简单来说是希望实现消息被某台服务器独占,也就是
单播
问题。假如我们希望这条消息被广播出去,每台收到这个消息的服务器都做处理,例如发消息做日志清理,这种情况称为
广播
, 那我们只需要将每个消费者放到不同的消费者组即可。
Kafka 引入消费者组的概念巧妙解决了单播和广播问题,而没有区分订阅类型,通过14ec一种逻辑概念来屏蔽掉多种订阅实现。
另外在同一个消费者组中的消费者订阅的分区是确定的,只有在消费者组中的消费者有变化的时候才会进行重分配。例如我们有四个分区,三个消费者,就会出现一个消费者订阅两个分区的情况。而三个分区四个消费者就会出现有消费者处于空闲状态,造成浪费,所以一般消费者的数量尽量不要大于 Topic 的分区数。
尾声(唠叨)
这是我 2021 年的第一篇博客,年底做回顾的时候才知道去年我过的究竟有多么糟糕。既没有输入也没有输出,虽然工作进入一个新的阶段,会越来越忙,但忙不是拒绝成长的借口,必须保证每月一到两本书的输入,一到两周输出一篇优质文章。
最长的路属于一颗孤独的心,与君共勉
下期我会从整体梳理 Kafka 生产者,包括消息发送客户端,发送端数据缓存从源码角度看看其中的设计模式,代码组织技巧。