Kafka 是如何建模数据的?
Kafka 是一个高度可扩展的消息系统,具有优秀的水平可扩展性和高吞吐率的特点,因而被许多公司所青睐并得到广泛的使用。本文首先介绍 Kafka 诞生的时代背景以及诞生之初的设计目标,随后回答 Kafka 作为一个消息系统是如何建模数据的,最后讲解 Kafka 作为一个软件系统的架构,为有志于深入了解的 Kafka 的同学做一个简单的框架梳理。
Kafka 诞生的时代背景与设计目标
大数据时代,为了分析数据背后的价值,组织所有服务和系统所产生的数据在权限允许的范围内对所有服务和系统都应该是可用的。从原始数据的收集到数据的使用,自底向上是一个金字塔形的结构,即底部以某种统一的方式采集数据,以统一的方式对数据建模,通过统一的方式对上层服务和应用提供访问和处理接口。
现实世界里,企业的每个应用程序都会产生数据,包括日志信息、监控指标、用户行为和请求响应等等。这些数据的产生源头不尽相同,可能散落在不同业务团队的逻辑代码当中。在传统的软件开发世界里,这些数据的持久化位置也不尽相同,日志文件可能存储在服务运行的机器的文件系统上,监控指标可能在监控系统拥有的数据库里,用户行为可能落到了数据仓库中,请求响应甚至没有被合理的持久化。
我们再从数据消费一端来观察这个过程,现在一个商业智能分析系统想要从不同业务的监控指标里聚合出统一的指标页面以供运营方快速决策应该采取什么行动来提升公司的盈利。最糟糕的情况莫过于该系统发现不同业务拥有不同的存放指标的方式,因此你要为接入每个系统的指标上报实现完全不同的逻辑,每来一个新的业务,就需要对接一个新的指标上报流程。对于一个新的分析系统来说,在它冷启动的时候,就要对接所有产生数据的业务方。这将成为所有开发人员的噩梦。
软件开发的一个重要技能就是观察出系统中存在的重复逻辑并抽出通用的模块以提升复用率和开发效率。注意到上面的数据对接流程涉及到数据的生产方和消费方,每对生产消费组合都要确定一个数据对接的方案。但是,数据的对接之所以会要求不同的方案,是因为数据的存储和模式是异构的。自然而然的,我们会想到定义一种统一的、足够通用以支持覆盖各种场景的数据交互模式,在数据的生产方和消费方之间引入一个中间层来解耦相互了解的负担。可以看到,原先有 N 个生产方和 M 个消费方,对接的要求次数是 N x M 次,而引入一个统一的数据交互中间层,消费方和生产方只需要分别对接这一中间层,对接的要求是 N + M 次。数据协同的复杂度被大大降低了。
这样的中间层解耦思路就是所谓的发布订阅系统。不同于数据的消费方与生产方直接建立联系,在系统里引入一个统一的消息总线以支持消息的发布与订阅。因为一方与另一方的交互被这一总线所隐藏,因此原来 N 的对接需求也就变成了 1 的对接需求。
然而,在上面的场景里,只有商业智能分析这一个类型的不同需求方。在现实世界当中,我们还会遇到日志搜索的数据需求方,风险控制的数据需求方等等。为每个数据处理领域维护一个领域专门的,但实现又高度相似的发布订阅系统是不合实际的,这会导致各个大同小异的系统有着各自的缺陷和不足。在业务团队对接不同的数据处理领域时,仍然需要进行相似但又细微不同的数据上报逻辑,这些细微的不同将成为后期维护深海暗礁。
为了从公司层面解耦数据交互,实际上需要的是一个同构的消息系统,它可以用来发布通用类型的数据,其规模可以随着公司业务的增长平稳的水平扩展。
这个同构的消息系统方案在 LinkedIn 的实现就是 Kafka,它能够作为数据源或数据汇,并且数据生产者上报或消费者访问的继承工作只需要连接到 Kafka 的一个单独的管道,而无须连接到实际的对应的消费者或生产者。Kafka 作为消息总线,管理从各个应用程序汇集到此的消息流,经过处理以后在分发到所有对数据感兴趣的订阅方。
Kafka 作为一种分布式的、基于发布订阅模式的消息系统,它的主要涉及目标包括以下几点。
- 首先,解耦数据的生产者和消费者。在项目研发的过程过程中,数据的产生和处理会根据需求发生什么样的变化是不可预知的。Kafka 作为消息交互的中间层,提供统一的消息交互模式。只要生产者按照统一的模式上报,消费者就可以按照相同的模式来访问和处理。这使得开发者能够独立的修改数据的产生或处理的逻辑。
- 数据的消费在整个系统中并不是一次性的,既不只给一个消费者消费,也不只给消费者消费一次,而是可以支持多个消费者消费若干次。Kafka 按照一定顺序持久化保存数据,从而支持通过重放消息来重复消费同一份数据;同时,通过消费组来区分不同的消费者群体,以支持同一份数据被不同的消费者所消费。
- Kafka 作为一个分布式消息系统,需要能够容忍分布式系统固有的任意故障,同时要支持在数据增长或减少的时候通过水平伸缩平滑地应对变化。
Kafka 建模数据的方式
消息
Kafka 的数据单元就叫做消息,消息的概念可以和数据库里的一行数据或者一条记录相对应。Kafka 看到的消息是字节数组形式的,因此对 Kafka 来说消息里的数据没有特别的模式或含义。消息可以带有一个键( Key ),键在 Kafka 中也是字节数组形式,因此没有特别的模式或含义,只是作为后续将提到的消息分区的逻辑中的一个参考数据。
主题和分区
Kafka 的消息通过主题( topic )来进行分类,主题的概念可以类比数据库里的表,可以将同一个主题下的消息看做带有主题标签的消息,Kafka 消费消息的粒度就是按照主题来选择的。主题可以被分为若干个分区( partition ),分区是性能提升的核心概念。对主题做分区的本质是将主题的数据切分到多个并行的数据流中,这也是 Kafka 能够实现巨大吞吐量的关键。同一个分区的消息以追加的形式写入,然后按照先进先出的顺序被读取。不同分区之间的消息无法排序,但同一个分区的消息将按照进入 Kafka 系统的时间顺序消费。注意这个时间戳未必是消息携带的业务时间戳。通过分区,Kafka 将消息处理的压力分摊到多个消息流中,从而获得吞吐量的提升。此外,在分布式场景下,通过将不同分区的消息分散在不同的机器上,能够以水平扩展的方式容纳和处理海量的数据。
前面提到,键在分区逻辑中作为参考数据被使用。实际上,同一个键的消息将被分发到同一个分区,从而确保具有相同键的消息被同一个进程所处理。由于分布式系统在处理数据是很难协同系统层面的统一状态,很多传统编程概念里全局的状态,典型的例如全局映射,实际上将在各个具体的工作机器上各不相同。通过将同一个键的消息分发到同一个分区,能够保证在键这一量级上,代码中的全局状态对同一个键的所有消息在处理时都是同一个引用。这样,我们才能够进行有状态的消息处理,而不是只依赖于消息本身的无状态的消息处理。有时候,业务层面的键值对和期望的分区处理的键存在阻抗失配的情形,此时可以通过业务层面将数据转换成分区键到业务键到值的数据结构,或者自定义分区器( partitioner )来解决这个问题。
Kafka 社区经常使用流( stream )这个名词来指代 Kafka 中同一个主题的消息。这里的流是一组从生产者移动到消费者的数据,几乎所有的流式处理框架都是这样看待数据的。
模式
从数据的角度,还有一个对于 Kafka 来说外挂的逻辑值得一提,即消息的模式。上面提到 Kafka 中的消息是无具体含义的字节数组,这实际上要求消费者知道生产者是如何上报数据的;反过来说,消息的生产者不能自由地改变消息发布的模式,而是要等待消费者发布版本兼容两种模式之后才能灰度发布生产者的新版本。为了处理这个问题,Kafka 支持模式注册( scheme registry )功能。通常,开发者使用 Apache Avro 序列化框架来定义消息的模式,从而使得生产者按照 Avro 或其他序列化框架支持的模式演进规则来改变消息模式。在 Avro 的例子里,模式的演进发生在模式文件的配置上,因此不需要改变代码。对于消息的消费者来说,按照演进规则改变的消息模式能够良好的向前及向后兼容,不会出现模式演进导致消费端消费失败的情况。消费端需要利用新模式时,只需要对应更新模式文件。
Kafka 系统架构
Kafka 的系统架构包括四个主要组成部分
- Kafka 服务器 Broker
- 消息的生产者 Producer
- 消息的消费者 Consumer
- 元数据管理 ZooKeeper
元数据管理 ZooKeeper
其中 ZooKeeper 作为外部依赖用于保存 Kafka 集群的元数据信息,包括分区的消费位点( offset )等元数据和 Broker 选举的元数据。早期还保存了消费者的消费位点等元数据,但在新版本中这一职责已经交给了 Broker 来管理。Kafka 源代码中 kafka.zookeeper 和 kafka.zk 包下的内容封装了所有跟 ZooKeeper 打交道的代码。值得一提的是,Kafka 社区正在实现中的 KIP-500 提议致力于在 Kafka 之内实现管理元数据的 Controller Quroum 机制以移除对 ZooKeeper 的依赖。
Kafka 服务器 Broker
一台 Kafka 服务器就是一个 Broker,一个 Kafka 集群由多个 Broker 组成,一个 Broker 能够支持多个分区。Broker 接收来自生产者的消息,拨动消费位点并将消息提交到磁盘保存。Broker 同时向消费者提供服务,对消费者读取分区消息的请求做出相应,返回已经提交到磁盘上的消息。
在集群中,一个分区归属于一个 Broker,该 Broker 称为分区的首领。一个分区可以分配给多个 Broker,此时将发生分区复制,从而制造消息冗余以容忍某个 Broker 的故障。在首领 Broker 故障时,其它分配到其分区的 Broker 可以成为新的首领并在消费者和生产者连接到新首领之后恢复消息服务。
每个集群的 Broker 当中会有一个被选举成为集群控制器( Controller )。它和 ZK 交互,负责集群元数据的管理,包括将分区分配给具体的 broker 和监控 broker 等。如前所述,这个从 Broker 中选取 Controller 的逻辑将被替换成专门的 Controller Quorum 机制,即 Broker 不再兼任 Controller 的角色。
Kafka 消息的生产者 Producer
消息生产者 Producer 将消息发布到指定的主题中。消息发布的时候支持设定相应的元数据,例如消息的键和用于确定消息分区的分区器。
Kafka 消息的消费者 Consumer
消息消费者 Consumer 向 Broker 请求特定的一个或多个主题的消息,根据消息的分区索引以及对应分区的消费位点获取实际的当前消费消息。
消费者将归属于某个消费组,也就是说,会有一个或多个消费者读取同一个主题。Kafka 保证每个分区在同一个消费组里只能被同一个消费者使用,从而保证消息的消费压力被平摊到消费组内的消费者上。而且,当一个消费者失效时,通过再平衡机制能够将其消费工作摊派到其他消费者之上,从而达到消费者层面的容错。
在实际生产中,一个常见的问题就是同一个业务方的不同业务逻辑要重复消费同一份消息。由于消费组的申请在公司中通常是需要审批,缺乏经验的开发者会简单的在原先的消费组上新增一个消费者并期待它能够完整的消费一份重复的数据。通过上面的介绍,很显然这只会引起消费组内分区和消费者对应关系的再平衡。为了重复同一份消息,应该重新申请一个消费组消费同一个主题的数据。可以说,消费组是业务层面的隔离机制。
一个典型的 Kafka 集群的拓扑结构如下图所示。