MQ002——消息引擎系统的基本常识

消息引擎系统的基本概念

前言——什么时候会用 MQ??

过上一节 blog 的内容,以及了解了有关 MQ 的前置知识。那么是否会有想过 MQ 会用在哪些场景呢?

在现如今的系统架构中,MQ 的定位就是总线和管道,主要起到解耦上下游系统、数据缓存的作用,通俗点就是“削峰填谷”。这个时候肯定会有人会想到 Redis 之类的数据库,与之不同的是它的主要操作就是生产和消费,而不太会关注计算、聚合和查询的逻辑。所以,在业务中不管使用哪款 MQ,其核心的操作永远是生产和消费数据。

上次提到的订单下单流程就是一个典型的系统解耦、消息分发的场景,一份数据需要被多个下游系统处理。在大数据领域中,比较经典的就是日志采集流程,一般日志数据都很大,而且是实时产生的,直接发到下游,下游系统可能会扛不住崩溃,所以会把数据先缓存到 MQ 中。实际现如今的数仓,不仅仅是日志文件写入 Kafka 之类的消息引擎系统,有时候也会选择被存储在数据库中的业务数据,通过增量同步的方式传入到 MQ,然后再统一采集到 HDFS 上。

针对以上场景,一款优秀的消息引擎系统必须满足:高性能、高吞吐和低延时等基本特性。

架构层面的基本概念

mq002_basic

  • Topic:在大部分 MQ 中,topic 都是指用来组织分区关系的一个逻辑概念。通常情况下,一个 topic 会包含多个分区。在 Kafka 中,发布订阅的是 topic,可以为每个业务、每个应用甚至是每类数据都创建专属的 topic;

  • Producer(生产者):向 topic 发布消息的客户端应用程序称为生产者,生产者程序通常会持续不断地向一个或多个主题发送消息。简单来说就是:指消息的发送方,发送消息的客户端;

  • Consumer(消费者):订阅这些主题消息的客户端应用程序称为消费者,和生产者类似,消费者也能够同时订阅多个注意的消息。说通俗点就是,指消息的接收方,即接收消息的客户端;

  • Broker:Broker 负责接收和处理客户端发送过来的请求,以及对消息进行持久化。Broker 本质上是一个进程。Kafka 的服务器端由被称为 Broker 的服务进程构成,即一个 Kafka 集群由多个 Broker 组成。虽然多个 Broker 进程能够运行在同一台机器上,但更常见的做法是将不同的 Broker 分散运行在不同的机器上,这样如果集群中某一台机器宕机,即使在它上面运行的所有 Broker 进程都挂掉了,其他机器上的 Broker 也依然能够对外提供服务。这其实就是 Kafka 提供高可用的手段之一;

  • ConsumerGroup/Subscription(消费分组 / 订阅):一般情况下,消息队列中消费分组和订阅是同一个概念,后面统一用消费分组来称呼。它是用来组织消费者和分区关系的逻辑概念,也有保存消费进度的作用。

  • Message(消息):指一条真实的业务数据,消息队列的每条数据一般都叫做一条消息。

  • Offset/ConsumerOffset/Cursor(位点 / 消费位点 / 游标):指消费者消费分区的进度,即每个消费者都会去消费分区,为了避免重复消费进度,都会保存消费者消费分区的进度信息。

  • ACK/OffsetCommit(确认 / 位点提交):确认和位点提交一般都是指提交消费进度的操作,即数据消费成功后,提交当前的消费位点,确保不重复消费。

  • Leader/Follower(领导者 / 追随者,主副本 / 从副本):Leader 和 Follower 一般是分区维度副本的概念,即集群中的分区一般会有多个副本。此时就会有主从副本的概念,一般是一个主副本配上一个或多个从副本。

  • Segment(段 / 数据分段):段是指消息数据在底层具体存储时,分为多个文件存储时的文件,这个文件就叫做分区的数据段。即比如每超过 1G 的文件就新起一个文件来存储,这个文件就是 Segment。基本所有的消息队列都有段的概念,比如 Kakfa 的 Segment、Pulsar 的 Ledger 等等。

  • StartOffset/EndOffset(起始位点 / 结束位点):起始位点和结束位点是分区维度的概念。即数据是顺序写入到分区的,一般从 0 的位置开始往后写,此时起始位点就是 0。因为数据有过期的概念,分区维度较早的数据会被清理。此时起始位点就会往后移,表示当前阶段最早那条有效消息的位点。结束位点是指最新的那条数据的写入位置。因为数据一直在写入分区,所以起始位点和结束位点是一直动态变化的。

  • ACL(访问控制技术):ACL 全称是 Access Control List,用来对集群中的资源进行权限控制,比如控制分区或 Topic 的读和写等。

功能层面的基础概念

讲完了架构层面的基本概念,我们来看看功能层面的基本概念。

相比于数据库的基本操作是增删改查,消息队列的基本操作就是生产和消费,即读和写。消息队列一般是不支持客户端修改和删除单条数据的。接下来我们就从功能的角度,来了解一些常见的基本概念。

  • 顺序消息:是指从生产者和消费者的视角来看,生产者按顺序写入 Topic 的消息,在消费者这边能不能按生产者写入的顺序消费到消息,如果能就是顺序消息。
  • 延时消息 / 定时消息:都是指生产者发送消息到 Broker 时,可以设置这条消息在多久后能被消费到,当时间到了后,消息就会被消费到。延时的意思就是指以 Broker 收到消息的时间为准,多久后消息能被消费者消费,比如消息发送成功后的 30 分钟才能被消费。定时是指可以指定消息在设置的时间才能被看到,比如设置明天的 20:00 才能被消费。从技术上来看,两者是一样的;从客户端的角度,功能上稍微有细微的差别;从内核的角度,一般两种消息是以同一个概念出现的。
  • 事务消息:消息队列的事务因为在不同的消息队列中的实现方式不一样,所以定义也不太一样。正常情况下,事务表示多个操作的原子性,即一批操作要么一起成功,要么一起失败。在消息队列中,一般指发送一批消息,要么同时成功,要么同时失败。
  • 消息重试:消息重试分为生产者重试和消费者重试。生产者重试是指当消息发送失败后,可以设置重试逻辑,比如重试几次、多久后重试、重试间隔多少。消费者重试是指当消费的消息处理失败后,会自动重试消费消息。
  • 消息回溯:是指当允许消息被多次消费,即某条消息消费成功后,这条消息不会被删除,还能再重复到这条消息。
  • 广播消费:广播听起来是一个主动的,即 Broker 将一条消息广播发送给多个消费者。但是在消息队列中,广播本质上是指一条消息能不能被很多个消费者消费到。只要能被多个消费者消费到,就能起到广播消费的效果,就可以叫做广播消费。
  • 死信队列:死信队列是一个功能,不是一个像分区一样的实体概念。它是指当某条消息无法处理成功时,则把这条消息写入到死信队列,将这条消息保存起来,从而可以处理后续的消息的功能。大部分情况下,死信队列在消费端使用得比较多,即消费到的消息无法处理成功,则将数据先保存到死信队列,然后可以继续处理其他消息。当然,在生产的时候也会有死信队列的概念,即某条消息无法写入 Topic,则可以先写入到死信队列。从功能上来看,死信队列的功能业务也可以自己去实现。消息队列中死信队列的意思是,消息队列的 SDK 已经集成了这部分功能,从而让业务使用起来就很简单。
  • 优先级队列:优先级队列是指可以给在一个分区或队列中的消息设置权重,权重大的消息能够被优先消费到。大部分情况下,消息队列的消息处理是 FIFO 先进先出的规则。此时如果某些消息需要被优先处理,基于这个规则就无法实现。所以就有了优先级队列的概念,优先级是消息维度设置的。
  • 消息过滤:是指可以给每条消息打上标签,在消费的时候可以根据标签信息去消费消息。可以理解为一个简单的查询消息的功能,即通过标签去查询过滤消息。消息过滤主要在消费端生效。
  • 消息过期 / 删除(TTL):是指消息队列中的消息会在一定时间或者超过一定大小后会被删除。因为消息队列主要是缓冲作用,所以一般会要求消息在一定的策略后会自动被清理。
  • 消息轨迹:是指记录一条消息从生产端发送、服务端保存、消费端消费的全生命周期的流程信息。用来追溯消息什么时候被发送、是否发送成功、什么时候发送成功、服务端是否保存成功、什么时候保存成功、被哪些消费者消费、是否消费成功、什么时候被消费等等信息
  • 消息查询:是指能够根据某些信息查询到消息队列中的信息。比如根据消息 ID 或根据消费位点来查询消息,可以理解为数据库里面的固定条件的 select 操作。
  • 消息压缩:是指生产端发送消息的时候,是否支持将消息进行压缩,以节省物理资源(比如网卡、硬盘)。压缩可以在 SDK 完成,也可以在 Broker 完成,并没有严格限制。通常来看,压缩在客户端完成会比较合理。
  • 多租户:是指同一个集群是否有逻辑隔离,比如一个物理集群能否创建两个名称都为 test 的主题。此时一般会有一个逻辑概念 Namespace(命名空间)和 Tenant(租户)来做隔离,一般有这两个概念的就是支持多租户。
  • 消息持久化:是指消息发送到 Broker 后,会不会持久化存储,比如存储到硬盘。有些消息队列为了保证性能,只会把消息存储在内存,此时节点重启后数据就会丢失。
  • 消息流控:是指能否对写入集群的消息进行限制。一般会支持 Topic、分区、消费分组、集群等维度的限流。

总结(扩展)

看完了上面的基础概念,下面就以 Kafka 为例,整体来看一下~~

Kafka 的三层消息架构:

  • 第一层是主题层,每个主题可以配置 M 个分区,而每个分区又可以配置 N 个副本。

  • 第二层是分区层,每个分区的 N 个副本中只能有一个充当领导者角色,对外提供服务;其他 N-1 个副本是追随者副本,只是提供数据冗余之用。

  • 第三层是消息层,分区中包含若干条消息,每条消息的位移从 0 开始,依次递增。

  • 最后,客户端程序只能与分区的领导者副本进行交互。

Kafka Broker 如何持久化数据??

总的来说,Kafka 使用消息日志(Log)来保存数据,一个日志就是磁盘上一个只能追加写(Append-only)消息的物理文件。因为只能追加写入,故避免了缓慢的随机 I/O 操作,改为性能较好的顺序 I/O 写操作,这也是实现 Kafka 高吞吐量特性的一个重要手段。不过如果你不停地向一个日志写入消息,最终也会耗尽所有的磁盘空间,因此 Kafka 必然要定期地删除消息以回收磁盘。怎么删除呢?简单来说就是通过日志段(Log Segment)机制。在 Kafka 底层,一个日志又进一步细分成多个日志段,消息被追加写到当前最新的日志段中,当写满了一个日志段后,Kafka 会自动切分出一个新的日志段,并将老的日志段封存起来。Kafka 在后台还有定时任务会定期地检查老的日志段是否能够被删除,从而实现回收磁盘空间的目的。

谈谈消费者

这里再重点说说消费者。上一篇博客中提到过两种消息模型,即点对点模型(Peer to Peer,P2P)和发布订阅模型。这里面的点对点指的是同一条消息只能被下游的一个消费者消费,其他消费者则不能染指。在 Kafka 中实现这种 P2P 模型的方法就是引入了消费者组(Consumer Group)。

所谓的消费者组,指的是多个消费者实例共同组成一个组来消费一组主题。这组主题中的每个分区都只会被组内的一个消费者实例消费,其他消费者实例不能消费它。为什么要引入消费者组呢?**主要是为了提升消费者端的吞吐量。**多个消费者实例同时消费,加速整个消费端的吞吐量(TPS)。BTW 这里的消费者实例可以是运行消费者应用的进程,也可以是一个线程,它们都称为一个消费者实例(Consumer Instance)。

消费者组里面的所有消费者实例不仅“瓜分”订阅主题的数据,而且更牛掰的的是它们还能彼此协助。假设组内某个实例挂掉了,Kafka 能够自动检测到,然后把这个 Failed 实例之前负责的分区转移给其他活着的消费者。这个过程就是 Kafka 中大名鼎鼎的“重平衡”(Rebalance)。嗯,其实既是大名鼎鼎,也是臭名昭著,因为由重平衡引发的消费者问题比比皆是。事实上,目前很多重平衡的 Bug 社区都无力解决。每个消费者在消费消息的过程中必然需要有个字段记录它当前消费到了分区的哪个位置上,这个字段就是消费者位移(Consumer Offset)。

注意,这和上面所说的位移完全不是一个概念。上面的“位移”表征的是分区内的消息位置,它是不变的,即一旦消息被成功写入到一个分区上,它的位移值就是固定的了。而消费者位移则不同,它可能是随时变化的,毕竟它是消费者消费进度的指示器嘛。另外每个消费者有着自己的消费者位移,因此一定要区分这两类位移的区别。我个人把消息在分区中的位移称为分区位移,而把消费者端的位移称为消费者位移。