Fork me on GitHub

Kafka

分布式消息系统,解耦模块的桥梁

Kafka 是一个基于 发布-订阅 的分布式消息系统,主要面向于大数据应用场景。它最初由 LinkedIn 公司开发,之后成为 Apache 项目的一部分。Kafka 是一种快速、可扩展、本身就专注于分布式的、实时消息流系统。Kafka 在2010 年正式向 Apache 社区开源,目前社区活跃。目前在互联网公司使用非常广泛,已经成为大数据分析的基础服务。

应用场景

Kafka 是众多消息系统中的一种实现方式,那我们为什么需要用到 消息系统 ?这里我列出以下几种在业务中常常碰到的场景,分别从 系统架构视角消息传播视角消息处理视角自身系统结构视角 共四个角度的应用场景来说明:

模块解耦

从系统架构视角看

这个我想是最明显的一点了,在业务系统上通常会存在一些系统产生数据,一些系统消费数据,这实际上就是 生产者–消费者 模式。这里的解耦是什么意思呢?我把消息系统独立出来了,那我的消息系统则会依赖生产者系统,消费系统则会依赖消息系统,不是多出了两个依赖,何为 解耦 呢?

这里的解耦实际上是解耦 不等速率依赖 (这个名词是我造的,纯属个人观点)。也就是说生产者系统和消费者系统之间会有 生产和消费速度不一致 而导致消息丢失的情况。而消息系统与生产者系统、消息系统与消费者系统之间则没有这种情况,即使有也不会造成消息丢失(只会暂存下来)。因此,破除了生产者系统和消费者系统的依赖关系就叫 解耦,而消息系统正是为此而生。

异步通信/缓冲队列

从消息传播视角看

很多时候,或并发达到一定量级的时候,系统是不能完全提供 实时消息 处理的能力的。这时不能立即处理的消息我们必须把这些请求放入 缓冲队列 中以等待处理。这种场景在一个公司中可能会有多种业务都会涉及到,因此,领先者们自然想到可以将缓冲队列设计成一个独立的平台,以满足各种业务的接入,从而,消息系统作为缓冲异步队列轰然降临

这里我使用的是 缓冲 队列而非 缓存 队列,主要是因为通常我们所说的缓存都是基于内存的,而 缓冲 则更普遍一点,你可以让它基于内存,也可以让它基于 硬盘 的。通常消息系统基本上都是基于硬盘存储的,包括 Kafka 其也是持久化到硬盘的。

缓冲队列 与我们的 Redis/Memcached 缓存 或者 DB 数据库 有什么区别呢,为什么不用 Redis/Memcached 或者 DB 实现 缓冲 功能呢?

这里我也大概谈下自己的看法:

  1. 先说 基于内存的缓存 吧,缓存的出现都是为了用昂贵的内存代价换来性能上的提升的,缓存通常是暂存那些常被访问的数据以提升较好的用户体验而设计,我们的 消息系统 并不是为了 ,而是为了 数据完整性,不丢失 而作的缓冲设计。因此,我们无需用如此昂贵的内存来作为消息系统的存储介质。另外一点就是缓冲的队列有可能会很大,达到千万甚至更多级别,这样如果用内存,那么代价就更昂贵了。
  2. 再说 DB 数据库,上面讲了内存昂贵,那我用数据库总可以了吧!那我们还是先想想数据库的原生作用:数据库是为持久化,通常是恒久的持久化而生的,也就是数据存下去,基本就不用变了,只会少量的修改删除。我们再想想缓冲队列,它是为 临时存储 而生的。用持久化的数据库来存临时数据,那会造成频繁的增删操作,势必会给数据库带来极大的性能消耗。
  3. 总结下,缓冲队列目标是:数据完整性 (而非存读的快速性,不是缓存),临时存储能力 (而非恒久持久性,不是数据库)。

数据一致性保障

从消息处理视角看

有些情况下,我们将数据提交给某个系统处理,有可能那个系统突然崩溃了,那传给它的数据就都覆灭了,这可是企业不能容忍的!因此,我们可以利用消息系统,作为临时备份处,将消息同时发送给消息系统以及那个处理系统,当处理系统处理成功后,发送确认操作让消息系统删除那条消息,也就是采用 “插入-获取-删除“ 范式。这样,假如处理系统崩溃,那数据仍然在消息队里中,重启处理系统就可以了。

这里,我们都是假设消息系统很可靠,比处理系统更可靠!为什么有这个依据呢?这实际上也是消息系统的另一大特性,就是可扩展性强,部分组件失效可容忍。

可扩展性强,部分组件失效可容忍

从自身结构视角看

这部分的内容我们在后面几节进行说明。

消息模式

消息模式就是消息系统实现时需要考虑的业务场景中的不同情况。由于生产者(系统)和消费者(系统)都可能是多个,那么就会产生一些微妙的不同。这里我们只考虑它们都处理相同的消息。对于多个生产者而言,消息系统就是不断接受消息的一个存储域,因此没有什么不同。而对于多个消费者而言就会有两种情况了:

  1. 一个消息只给一个消费者消费:这个是最常见的情况了,一个消息无法被重复消费的,因此这种情况对应到消息模式就是 点对点模式或者叫队列模式
  2. 一个消息可以给多个消费者消费:这个情况类比订阅付费服务,比如我是一家报社,有很多人订阅了我的报纸,只要我这边有新报纸刊登,我就需要把这些新报纸寄送给每一个订阅的人。因此这种情况对应到消息模式就叫 发布/订阅模式

以上两种模式就是消息模式最常见的两种,所有的消息系统的实现都会考虑这两种模式的,因此大家在学习一个新的消息系统的时候就可以考虑这个消息系统 如何实现者两种情况的。对于 Kafka 我们会在下面讲到其对应的两种模式。

平台对比

消息系统目前最有名气的大概有四个:ActiveMQRabbitMQKafkaRocketMQ. 它们的对比网上也应该有很多了,我就不一一列举了。

这里我将我之前做过的 PPT 拿过来放这里作下对比,我简单说明下:

  • 下图各个消息系统从左到右,支持的消息量级越来越大,ActiveMQ 最小,RocketMQ 最大;
  • ActiveMQ、RabbitMQ 稳定性是相对较好的,RabbitMQ 稳定性更好同时数据安全性最高,如果对实时性、数据不允许丢失要求高时,可以用 RabbitMQ
  • RocketMQ 是阿里开源的,其处理量是最高的,但是生态比较少,因此若使用过程中出现问题,你只能找原开发者或维护者了;
  • Kafka 是目前生态链最广、社区最活跃的消息系统了。但是其会存在消息丢失情况,通常应用在分布式日志消息处理等这些对消息丢失可容忍性的场景。目前,大数据已成主流的今天,Kafka 也逐渐成为使用的主流消息系统,因为大数据对消息丢失一般都是可容忍的,比如训练集中丢了几条数据等等,都是无相关的。而对于 支付、会员 等这些消息则不推荐用 Kafka ,可转用其他 MQ 系统。

宏观结构

下图是 Kafka 消息系统的 分布式宏观架构图,这里分别讲下各个组件的作用及其关系:

  1. Producer: 数据的生产客户端,生产数据发送到 Kafka Cluster
  2. Zookeeper: 负责整体集群的协调工作,保存 BrokerConsumer 交互的元信息,并进行数据变更监控;
  3. Broker: Broker 实际上就是单台服务器,其主要接收 ProducerConsumer 请求,持久化Message,其中会通过选举产生一个 Controller,来主持协调工作;
  4. Kafka Cluster: 由多个 Borker 和一套 Zookeeper 组成,Broker 之间无主从关系,地位平等,可任意增删节点,这主要由 Zookeeper 维护;
  5. Consumer: 数据的消费客户端,用于从 Broker 中订阅/拉取消息;

微观设计

Kafka 内部消息传递流程 如下图所示:

Topic

一个 消息主题,也就是一个分布式业务消息队列。不同的生产者将不同的业务消息分发到不同的 topic 上,这样,消费者就可以根据 topic 进行对应的业务消息消费了。

Partition

这个就是 topic 分布式的体现,由于一个 topic 就是一个业务消息,这些消息可能会源源不断来,并且有可能会同时并发很大地进入队列,将这些消息合理地分布在分布式机器中则可以保证机器的负载均衡性,同时也可以使得不同的消费者可以同时拉取不同 partition 中的消息,可提升消费者并发性能,这里总结下 partition 特性:

  • 一个 topic 分成多个 partion
  • 多个 producer 生产消息可以并行入队,多个 Consumer 可并行消费;
  • 同一个 partition 里保证消息有序, 不同 partition 则不能完全保证有序;

Consumer Group

消费者组应该是 Kafka 最大的特色了,消费者组就是消费者组成的一个组,消费者在向 Kafka 拉取数据的时候需要提供一个组名,这个名称就是消费者组名,上面的两种消息模式都可以在消费者组中得到实现:

  1. 点对点/队列模式:一个消息只能被一个消费者消费,我们只需要将这些消费者放在同一个消费者组里就可以了,这样消费者在同一个组中,那么 topic 中的一条消息只会向一个消费者组发送一次;
  2. 发布-订阅模式:一个消息可被多个消费者消费,这种情况,我们只需要将各个消费者放在各自单独的组中,各个组均订阅了此消息 topic 就可以了。

这里还有如下注意点:

  • 一个消费组消费一个 topic 的全量数据;
  • 组内消费者消费一个或多个 partition 数据,如果一个组里的消费者数量少于订阅的 topic 的 partition 数量,那么组中必有一个消费者要消费多个 partion 数据;
  • 一个组里的消费者应小于等于 topicpartition 数量,这是因为一个 partition 最多只能与一个 consumer 连接,那么如果 partition 数量大于 consumer 数量,则必定有 consumer 是空闲的,因此尽量避免这种情况;

相关链接

苟且一下