简介

消息队列是现代互联网不可缺少的基础组件之一,承载着异步、削峰、解耦等重要特性。
Mafka是美团基础架构推出的消息队列产品,开发源于 2014 年底,成型于 2016 年初第二个版本-Mafka 2.0,现已经演进到了 3 .0版本。
MQ团队运营着美团数千个服务的Mafka消息队列,线上队列超过 4 万个,日消息调用量(生产+消费)超过 2 万亿(线上业务消息,不包含离线大数据计算消息量),服务可用率SLA是9 9.999%,队列覆盖到店、到家、
金融、猫眼、两轮、广平等各美团事业群和业务。

功能介绍

从产品上来看,Mafka主要包含三个子类型产品:

  1. Mafka 普通消息: 普通的消息收发、订阅,默认支持 7 天内的消息存储和回放。
  2. Mafka 延迟消息: 延迟类消息,支持 5 s到N天内的任意时间延迟,以下称DelayServer。
  3. Mafka Push消费端: push类消息消费端,支持以RPC调用方式消费消息,消费方对Mafka Partition透明,以下简称push消费。

Mafka普通消息

Mafka普通消息客户端以java、c ++为主,还包含python/go/node.js等小语言客户端。

分区和offset概念

生产者在将消息发送给Mafka后,Mafka内部会有分区的概念,一个分区即一个消息块,生产者可以决定将消息发送给哪个分区,这个涉及到消息保序一类,在后边会看到。
offset是消息在分区内的偏移量,消费者在消费某一个分区的消息时,会批量提交offset,表明自己消费到哪个位置了,这个信息记录在服务器上。

消息生产和消费

Mafka普通消息就是传统的消息队列,支持多个渠道并发生产,多业务并发消费。用户在Mafka用户后台来申请队列,填入必要的信息,即可生成队列资源。

用户生产时填写appKey(服务标识)、Topic(队列名称)、NameSpace(事业部)即可开始生产,不同的服务可以同时往一个队列中生产,队列Owner也可以设置权限控制,只允许自己生产或消费。
消费时以组的方式进行,一个消费组内的所有消费者共同消费队列内的所有消息,一个消费组可以有多个实例(机器),也可以有一个实例(机器),如下图:

消费组A有两台机器组成,消费组B有一台机器组成, 1 92.168.1.1/2机器共同消费TopicA上的所有消息,一个机器只消费全部消息的一部分。 19 2.168.1.3消费TopicA内的所有消息。
Mafka集群是以事业部(BG)为单位部署的,一个BG下有多个集群,每个集群部署在不同的机房。默认情况下,申请的队列至少有两个集群,分布在两个机房,能保持机房粒度的容灾,后续还可以动态添加或减少集
群,实现机房内、机房间的水平扩容,同时队列可以在多个集群之间迁移变动,如下图:

topicA创建在A/B两个集群上,消息生产时,可以有两种模式:

  1. 单集群生产
  • 单集群模式下,系统会根据生产者的地域位置信息,默认生产到相同机房的集群上,如果同机房没有,则会生产到同地域的集群上,如果同地域没有集群,则会生产到本BG下的其他地域的集群上。
  1. 多集群生产
  • 开启多集群生产后,消息会以roundRobin方式生产到A/B两个集群上。

消息消费也分几种策略:

  1. 同机房消费: 消费端只会消费本机房内集群上的消息
  2. 同地域消费: 消费端只会参与消费本地域内集群上的消息
  3. 全地域消费: 消费方会参与消费所有集群上的消息

消费端消费时,客户端每 5 秒钟,提交一次offset,即消息的消费偏移量,offset以主题方式存储在集群上。如果消息来自于不同的集群,每个集群各自存储自己的offset。

生产和消费接口

Mafka的生产接口分同步生产和异步生产。
同步生产情况下,客户端发送一条消息会同步等待生产结果,也可以同步批量发送。
异步发送时,可以设置发送结果回调,不论是生产成功或失败,因为异步发送本质先发往本机的缓冲区,再由后台线程批量发送给服务器,减少了单个消息的来回网络请求,发送吞吐会更大,发送效率也更高。
不过消息会临时存放在本机缓冲区,会有宕机丢失的⻛险,需要做好异步回调的补偿处理。Mafka默认设置本机缓冲区大小为 6 4M,超过后可以选择报错,或选择阻塞。

死信

死信是消息队里的最基本功能之一,Mafka死信背后是依靠延迟消息来完成的。
当用户遇到暂时不能处理的消息后,可以先投递到死信队列,继续消费后边的消息,等设定的时间到达后,再次消费之前未成功的消息。
同时Mafka还支持设置最大消费次数,达到次数之后不再重复投递,打印消息到日志里后跳过。

消息保序

传统的队列都要求有序,先进先出,保持严格的先后顺序。但随着多核CPU的出现,为了充分利用硬件的性能,对队列的需求不再要求严格有序,只要能保障大致上的有序即可。

Mafka能满足三种级别的保序要求,如下:

全局有序 :如下图,全局有序意味着,只能有一个生产者或消费者,多于一个的话,生产时或消费时都会乱序,这时申请Mafka队列只能有一个分区。在当今CPU多核的时代,这种模式势必很低效。

局部有序 :如下图,一个简单的奇数、偶数hash,将单数的消息发往分区 1 ,偶数的消息发往分区 2 ,此种场景下,能保障一个分区内消息有序,不管两个消费者的消费速度快慢,比如奇数分区 1 内, 3 一定会在 1
之后被消费。
这种场景也被用来保障同一类的消息由同一个消费者消费,比如订单的状态变化,通过hash算法将订单分散到不同的分区里,但同一个订单id的消息一定会落在同一个分区内,保障这个订单的处理有严格的先后
顺序。
但是在此种场景下,也可能会造成局部消息积压,比如因为hash函数的选择,会导致某个分区的消息特别多,某些分区的消息特别少,消息多的分区消费的速度要慢一些,会造成积压。
如果通过再hash的话解决的话,会打乱原有的消息顺序,整个消费系统无法轻松的横向扩展。

大致有序 :通用,高效的消息模式,推荐使用。生产者普通方式下生产,消息在各个分区内以均匀分布。随着消息量的增加,用户可以扩展分区或消费者,每个消费者是等位的,没有区别,因此系统可以很容易实现横向扩容,让整个业务系统的拥有最大弹性。这种模式下,消息虽然不是严格有序,但是却是大致是有序的。

可靠性、持久性保障

Mafka的消息投递语义也是保障at least once,同一条消息至少生产或消费一次,消息绝不会丢失,但是可能会重复发送或消费。
消息生产时,可能因为网络原因,客户端没有收到服务端的生产响应,因而会重复投递消息。同样,客户端在消息消费后,提交offset之前宕机,会导致同一批消息再次被重复消费。

生产端保障

先来看下消息生产,生产端SDK通过RPC将消息传输到Mafka集群,产生一个request,M afka集群在接收到消息后,返回给生产端一个response,这时生产端认为这条消息 生产成功 ,如下图所示:

看下生产时可能遇到的异常case:

  1. request因为网络抖动丢失。
  2. response因为网络抖动丢失。
  3. mafka集群故障。
    不管上边那种情况,生产端都会重试这个request,理论上 3 0s内的网络抖动都可以克服。
    如果网络抖动超过 3 0s,或者是因为一个Mafka集群有故障,生产端都会重试生产到另外一个集群。
    所以除非两个机房的集群同时不可用,生产端基本不可能丢失消息,只会因为发生因为网络抖动时,重试产生小部分重复消息,所以Mafka的生产语义是at least once,至少生产一次。

消费端保障

消息生产到集群内后,用户就可以消费到了,对于每个队列,Mafka集群都会记录一个“点位( Offset)”,这点位是消费者在队列上的 消费位置记录 ,如下所示:

消费者从Mafka集群拉一批消息,在本地消费,SDK内部线程会定时提交消费点位给Mafka集群,集群将这个点位记录下来,以便下次从这里继续消费。如果消费者在提交点位前或后宕机了,那么顶多是本次拉到
的消息重复消费一遍,但绝不会错过消息,造成“消息丢失”。

Mafka集群保障

消息在发给Mafka集群后,Mafka是靠“副本复制”机制确保高可用的,如下图所示:

生产者将消息发送给Mafka集群,实际是发送到了BrokerA的” topicA-分区1 “上,这个称为主副本,“topicA-分区1 rep1” 和 “topicA-分区1 rep2” 是这个分区 1 的两个副本,t opicA-分区 1 的两个副本所在的机器,
会实时从主本拉取消息。
主本和副本其实都是一个分区的两块相同内容的数据块,为的保障容灾和高可用,分布在不同的服务器上,当某台服务器宕机后,分区数据不会丢失。
在ack为- 1的情况下,这两个副本只有读拉取到生产者本次生产的消息后,这次生产才算完成。所以只要生产者将消息发送给了Mafka集群,即便同时有两台机器宕机,这条消息仍然不会丢失。
在只有单台机器宕机时,分区的一个副本会转化为主本,继续服务生产和消费,如下图所示:

上边所说的ack是消息生产者的一个属性配置,有三个可选值- 1 , 0 , 1 。M afka的t opic副本数量>=2,生产消息时,用户可以选择ack值,设置为 1 时,只要主本收到即可,设置为- 1 时,必须主本和副本都收到后才
算发送成功。
设置为 0 时,发送端不需要等待服务端的响应,默认发送成功。Mafka 队列的ack默认配置都是- 1 ,需要主本、副本都收到消息,但对于每分钟超过 600 万的主题,我们建议视业务情况,将ack值设置为 1 ,以取得更
好的发送吞吐。

流量隔离

为了方便业务开发和测试,Mafka提供了两种流量隔离方案, 环境隔离和泳道
如果队列开启 环境隔离 ,只有相应环境下的消费端才能消费消息,如下图所示:

同一个队列,生产端会因为自身环境生产不同类型的消息,消费端也只会消费与自身环境匹配的消息。

环境隔离能解决不同环境下的开发和测试问题,但是当业务链路比较⻓,业务逻辑比较复杂,而且多个QA同时进行测试,有限的几个环境是不能满足测试需求的。

为此演化出了一个新的流量隔离方案, 泳道 。每个QA测试时,创建一个新的泳道,这条泳道上的消息就是自己独享的,而且当消费方不存在时,泳道上的消息还能回流到⻣干上去,如下图所示:

上图中,自身处于泳道A的生产者,生产的消息只能被泳道A环境下的消费者消费,如果泳道A环境下没有部署消费者,那么泳道A的消息将被⻣干消费者消费到。

事务消息

在一些业务场景下,业务RD需要执行一个“操作”后,再发送一条MQ消息通知下游。而且希望这个“操作”和消息是绑定的,只有“操作”成功执行后再发MQ消息,如果“操作”失败了,就不发MQ消息。这就是事务消息使用场景,实现 (“操作”+发消息)的绑定关系,类似数据库里的事务,实现(删除一条数据+插入新表一条数据)两个操作的绑定,即原子性。

Mafka事务消息就解决了这个问题。事务消息能保证本地事务的执行和消息发送两个操作的原子性,要么两个操作都成功,要么都失败。比如本地事务是一个数据库操作,那Mafka事务消息能保证数据库操作和发
消息这两个操作的原子性,要么数据库操作和消息发送都成功了,要么都失败了,不会存在一个成功而另外一个操作失败的场景。
使用也比较简单,发送事务消息时,实现一个设定好的接口即可,如下:

1
2
3
4
// 创建事务消息生产者实例,开启事务消息
final IProducerProcessor<String, Object> producer = MafkaClient.buildTxnProduceFactory(properties, "my-txn-msg", new MyListener());
// 发送事务消息
ProducerResult result = producer.sendMessageInTransaction("transaction message", null);

注意第三个参数MyListener,这里就是我们实现事务消息逻辑的地方,这个参数必须实现一个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface TransactionListener {
/**
* 发送消息成功后,会调用这个方法来执行用户的本地事务
*
* @param msg 消息
* @param arg 本地事务执行时需要的参数,用户自定义
* @return Transaction state 返回本地事务执行的结果:提交消息 or 回滚消息(成功or失败)
*/
LocalTransactionState executeLocalTransaction(final HalfMessage msg, final Object arg);

/**
* 如果本地事务执行结果不可知,那么Mafka server端会定期回查本地事务的执行结果,用户在这个方法里定义回查逻辑
*
* @param msg 消息
* @return Transaction state 返回本地事务执行的结果:提交消息 or 回滚消息(成功or失败)
*/
LocalTransactionState checkLocalTransaction(final HalfMessage msg);
}

在这个接口里定义本地事务的执行逻辑,如果本地事务执行成功,返回了LocalTransactionState.COMMIT_MESSAGE,那么Mafka就会提交之前发送的消息,如果返回
LocalTransactionState.ROLLBACK_MESSAGE,Mafka就会撤回之前发送的消息。
同样,消费事务消息也非常简单,在创建消费者实例时,只需要额外配置一个消息消费隔离Level,类似Mysql的事务隔离级别:

1
2
3
4
5
//配置消息消费隔离级别
properties.setProperty(ConsumerConstants.ISOLATION_LEVEL, IsolationLevel.READ_ALL);
//创建消费者实例
final IConsumerProcessor consumer = MafkaClient.buildConsumerFactory(properties, "myTopic");

根据这个消费级别,消费你想要的消息。消费隔离级别含有下边几种:

  1. read_committed 只消费提交的消息 (default)
  2. read_rollback 只消费回滚的消息
  3. read_unknown 消费未知状态的消息
  4. read_uncommitted(包含 2 和 3 )
  5. read_all (包含 1 、 2 、3 )

优先级消息

优先级消息通常是需要区别对待消息的消费时间要求,后到的消息因为优先级比较高,需要尽可能早的消费,不同优先级的消息到达后,需要按设定好的优先级顺序消费。

Mafka优先级消息支持 0~9个优先级,如下所示:

1
2
3
4
IPriorityProducerProcessor producer = MafkaClient.buildDelayPriorityProducerFactory(properties, "mafka.xxx.priority.topic");
int level = 1; // 该级别参数需要业务侧根据业务情况和申请情况进行设置,一般是0-9范围的一个整型值
ProducerResult result = producer.sendDelayMessage(level,"send sync message" + i);

在内部,Mafka使用多个队列来接收不同优先级的消息。业务启动生产者实例后,内部会根据业务设定的优先级发到指定的队列里。
消息消费时,仍然是普通的消费接口,优先消费高优先级的,高优先级消费完后再消费低优先级的;当消费低优先级时,有高优先级的消息进来,能够优先消费高优先级的消息。
在消费者内部,Mafka会确保每个分配到Parition(分区)的消费者,能分配到Topic的所有Parition,以此确保高优先级的会被优先消费,如下图:

topic-0、t opic-1、t opic-2分别代表 3 中不同级别的优先级:0(高)、1 (中)、2 (低)。Mafka收到消息后,会根据消息的优先级级别,放在三个不同的队列上。
每个优先级队列又有多个分区,确保生产和消费吞吐速度,比如topic-0的p artition-0,p artition-1,p artition-2。c onsume-1、c onsume-2、c onsume-3分别代表不同的消费者,
每个消费者上线以后,Mafka调度会负责将所有优先级级别队列的相同分区分配给同一个消费者,比如consume-2分配到了topic-0的 1 号分区,topic-1的 1 号分区,topic-2的 1 号分区。
这样consume-2在消费时,会从高到低的扫描各队列是否有消息进来,如果有户优先消费级别高的队列里的消息,没有高级别的消息时,再去消费低级别的消息,每消费一条消息会再次扫描所有级别的队列,再次
给高优先级的队列消费机会。
Mafka优先级消息还支持消息延迟投递,同样也支持死信方式投递和消费,消费失败后,需要支持根据优先级投递到指定优先级对应的死信队列中。在消费时,同一优先级下,优先消费死信队列中的消息

其他消息队列特性支持

除了以上介绍的功能之外,Mafka支持大部分传统消息队列的特性,包括以下几点:

  1. 基于消息的发送时间、offset回溯
  2. 队列粒度的消息消费延迟
  3. 消息的并行生产、并行消费
  4. 消息查找、消息轨迹
  5. 批量消费功能
  6. 消费异常重试次数自定义,或跳过有问题的消息
  7. 消费端在消费消息时,消费者上下线,支持粘性分配,减少分区变动

美团公司特有功能支持

  1. 支持故障注入,为客户端提供故障模拟功能,用于线上事故演练等场景
  2. 支持消息生产和消费以set方式隔离
  3. 灰度链路,支持业务线上灰度链路发布
  4. 线上蓝绿发布支持
  5. 全链路压测支持,方便业务在线上做全链路压测
  6. 主题鉴权,方便业务对队列做生产和消费权限控制
  7. 支持以Storm/Flink方式接入生产和消费

Mafka延迟消息

延迟消息是消息队列的主要功能之一,支持每个消息以特定时间的延迟投递。Mafka延迟消息支持 5 s ~ N天的自定义延迟时间投递,同时还支持改签、撤销、统计等功能。
用户使用时,在客户端SDK内初始化延迟队列的生产实例后,即可发送延迟消息。

1
public ProducerResult sendDelayMessage(V message, long delayTime) throws Exception ;

已经发送给Mafka的延迟消息,可以在消息到期之前重新设定投递时间,或撤销掉,另外还可以在平台上查询统计到未来一段时间即将到期的被投递的消息数量。

Mafka Push消费端

分片和消费者数量紧耦合问题

普通消息的客户端,是在SDK内通过和服务端的私有二进制协议来拉取消息的,而且消费者分配方式也是遵循队列的一个分区只能分配给一个消费者来消费。

这样的模型导致消费者和分区之间的比较紧的耦合关系,当用户的消费能力不足,需要增加消费者时,分区数量也必须增加。比如有 3 个消费者,分别消费 3 个分区,每个消费者负责消费一个分区,当消费者数量增加到 5 个时,分区同时也需要增加到 5 个,以便新增的 2 个消费者也有消息可消费。

当消费者数量比较少时,这样的匹配关系好,但是当消费者数量是 100 甚至 200 个,队列消息量又不大,而分区如果也跟着扩容到 100 或 200 时,资源就会比较浪费。

消费组Rebalance问题

消费者在消费Mafka消息时,需要持有分区,如果一个队列有 6 个分区: P0,P1,P2,P3,P4,P5, 2 个消费者,那么这两个消费者分别会持有 3 个分区,如下图所示:
消费者 1 持有P0 、P1、P2,消费者2持有P3 、P4、P5,如果再添加第三个消费者,Mafka会充分利用所有的消费者资源,根据现有消费者个数重新分配分区,理想情况下平均分配,消费者 1 分到P1 、P2,消费者 2 分到P4、P5,消费者 3 分到P0 、P3。

这里P0 和P3分区,是从原来的消费者1和消费者2持有的分区里调出来的,这两个分区从原来的消费者停止消费,到消费者3开始消费,为了防止消息被重复消费,中间需要一个平滑的切换过程。比如从消费者 1 内调出P0后,需要挂起P0 分区,等待一段时间让客户端将P0分区的Offset提交完成,然后再将P0 分区分配给消费者3来继续消费,整个过程,挂起P0 分区那段时间会造成P0 分区的短暂积压,这就是比较知名的“消费组rebalance”问题。

虽然Mafka在服务端使用调度器和客户端做紧密的协作,能将问题积压时间控制在秒级,但仍然不能完全消除这个时间。

Push消费组来解决

针对上述的两个问题,Mafka推出Push消费组服务来解决,这个服务简称PushServer。

PushServer是一个RPC服务,消费者不再直接从Mafka Broker拉取消息,而是改从PushServer代理获取消息。这里PushServer持有这个队列的所有分区,先把消息统一拉取到本地,然后供客户端通过RPC调用方
式来消费。
这样做的好处第一是解耦了客户端数量和服务端分区数量之间的紧耦合,即上边所说的第一个问题,因为客户端现在不需要持有任何分区,可以以RPC方式发起调用即可拉取到一条消息来消费,第二个好处就是消
费者数量可以无限扩展,不受服务端分区资源数量的限制,而且不需要做消费者之间的分区调配,即上边所属的第二个问题—“消费组Rebalance”。
PushServer很好的解决了对积压非常敏感,不希望在消费过程中因为消费者上下线导致消费抖动,而且有大量消费者参与消费的队列。但是因为中间多了一层代理,pushServer从所有分区拉消息,然后以RPC接口
方式提供消费,打乱了分区内的消费顺序。因为从一个分区内拉取的消息,有可能并发的被多个客户端同时消费,所以使用PushServer代消费时,消息的保序只能做到上边所说的大致有序。

监控&告警

在M afka的管理平台,用户可以实时查看自己的队列生产和消费情况。

生产监控

如下图所示,平台会监控生产速度,以及生产延迟,查看48h内的队列发送信息,还可以查看和搜索实时的生产者实例信息。

消费监控

消费组实时消费速度,如下图所示,两种颜色分别代表两个机房的同一队列的消费速度:

消费组积压信息,按分区来展示,消费组消费时延监控,消息的延迟监控:

消费者实例信息,以及消费状态信息:

告警

Mafka的告警支持自定义配置,包括消息积压,消费速率,消费者数量等告警项,持续时间、生效时间,阈值等配置项,如下图所示:

容灾、故障转移

Mafka容灾等级分为单节点、集群、机房、地域,故障转移方式分为 主动和被动 ,主动就是业务客户端做发起故障转移,被动就是服务端发起故障转移。

主动故障转移

主动故障转移分为分片故障转移,节点故障转移,集群故障转移。

分片故障转移是指,当某个分片暂时不可用时,客户端主动将此分片的消息发往其他可用分片。

节点故障转移是指,当某个节点暂时不可用时,将此节点暂时拉黑,把消息发往其他可用节点上的分片。

集群故障转移是指,当某个集群暂时不可用时,将此集群暂时拉黑,把消息发往其他可用的集群上。如上所述,用户在申请Mafka消息队列时,Mafka至少为此队列配置 2 个可用的集群。

如以上的Makfa集群A,有四个节点,节点A、B、C、D,假设有一个队列topicA,有四个分区P0 、P1、P2、P3分别分布在A、B、C、D四台机器上。当分片P0 暂时不可用时,Mafka会将消息发往P1/P2/P3分片
上。
如果当节点A不可用时,原本发往节点A上分片的消息发往其他节点的分片上。
如下图,对于同一个队列topicA,在集群A上有 4 个分区,在集群B上有 4 个分区,A、B两集群处于不同的机房,正常情况下,消息会发往 2 个集群的 8 个分区,但如果集群A不可用时,比如机房断网、断电时,客户端
会主动熔断,将消息全部发往集群B。

被动故障转移

主动故障转移是客户端自动探测并执行的,整个过程依据默认参数设置比如网络timeout,冻结时间等自动触发的。被动故障转移是人工执行的,比如当某个机房断网、断电时,人工执行切换操作。
这个操作是人工在服务端设置某个集群不可用,让服务端下发指令给客户端,发起故障转移,停止发送消息到某一个集群。
整个操作如下图所示:

高可靠集群模式

不管是主动还是被动的故障转移,其实质就是停止向故障机房的集群发送或消费消息,生产和消费客户端都与他们断开连接,如下图所示:

这种容灾模式下,当机房故障能在短时间内恢复,或集群恢复后,都可以继续使用。因为消息队列的特性,业务方大多数只关心增量消息的可用性,故障产生后,比如只要增量的订单消息能继续生产或消费,对业务的影响都不是很大。存量未消费的消息,在上图中标为红色的部分,一方面因为对积压敏感的用户,生产和消费追的都会比较紧,基本不会产生多少条积压,所以影响不大,对积压不敏感的用户完全可以在机房或集群恢复后,继续生产和消费,只影响短暂的消费可用性。所以,这种双机房容灾模式,能满足绝大多数用户的需求。

但是对于消息可靠性要求非常高的业务场景,比如金融交易,或是上下游需要严格对账、审计类的消息,暂时几条消息不能消费的话,对业务影响会比较大。

针对这种场景,Mafka有跨机房集群来解决,如下图所示:

整个集群包含四个节点,BrokerA、B rokerB,B rokerC,BrokerD,每个分区的主本、副本被均匀的分布在两个机房,比如分区P0 的主本,P1 的副本都在机房A,分区P1 的主本、P0 的副本在机房B。
当客户端生产一条消息后,消息的主本存储了消息后,消息的副本也会存储,所有消息经过跨机房复制,保持副本一致。这样当一个机房故障后,可用机房的消息的副本会切换为主本,继续提供生产、消费服务,
保持高可用,高可靠。
当然,和其他高可用组件一样,通过跨机房复制数据,来严格保障数据的高可靠,会因机房间网络延迟和质量的影响,降低集群一定的吞吐率,所以具体还要根据业务实际情况来建设集群。

运营

管理平台

Mafka有两个运营平台,一个是管理平台,面向的用户是业务方使用人员,主要进行主题、消费组的创建、配置、监控和管理等,如上述所说的监控和报警就属于管理平台的一部分,主⻚界面如下:

在主题⻚面,可以进行各类的配置和管理:

消费组⻚面:

运维平台

另外一个是运维平台,主要面向的是SRE运维人员和Mafka RD开发人员。运维平台主要功能包括集群创建、版本发布、监控和管理,主界面如下图所示:

运维平台上,查看某个集群的节点信息:

集群负载:

集群metrics:

另外还包含其他运维功能,在这里不一一详述:

架构概览

从架构上来看,Mafka主要包含以下几个模块:

  1. Castle: 消息队列中控调度器
  2. Broker: 消息收发和存储,基于kafka 0.9.01版本自研
  3. ClientSDK: 业务使用的消息服务SDK,基于kafka client 0.8和0 .9版本自研
  4. DelayServer API & Scheduler: 延迟消息的两个组件API和Scheduler调度器
  5. Zookeeper: Broker使用的消息队列元数据存储
  6. PushServer: Push代消费服务
  7. TOM: 集群资源控制器,负责Broker上主题、消费组资源的增加、删除,分区、leader、副本分布管理
  8. Scanner: Broker存活状态扫描服务,会将dead的b roker从zookeeper中摘除
  9. Monitor: 用户主题、消费组监控服务,比如主题生产消费速度、延迟、积压等监控
  10. 管理平台:面向用户的mafka消息队列管理界面服务
  11. 运维平台:面向SRE、研发RD的集群、服务运营工具服务
  12. 外部KV存储: KV存储,服务于延迟消息的存储服务,以及线上主题及消费组配置的快速缓存

如上图所示,消息的生产、消费交互流程如下:
  1. 生产端和消费端服务在启动时,会先通过RPC调用和castle形成一个心跳机制,定期获取配置信息和控制指令。
  2. 拿到配置信息后,客户端获取到了Broker集群地址和配置等信息,就可以开始生产和消费消息了。
  3. 如果是延迟类消息,需要将消息以RPC方式发送给DelayServer API服务,将消息暂存在基础存储中,Scheduler调度器定期根据延迟时间分发消息到broker,然后消费方才能获取到消息,进行消费。
  4. 如果消费方希望以RPC接口调用方式消费,可以调用PushServer服务,获取消息消费。

辅助系统交互逻辑:

  1. 用户通过管理平台创建主题、消费组,管理平台协调TOM在b roker上生成物理资源,创建成功后将数据落入mysql,并将数据缓存一份在KV存储中。
  2. TOM负责在broker上创建主题、消费组,并将结果反馈给管理平台。
  3. TOM不仅负责真实资源的创建,还负责Broker集群上分区的分布,副本以及leader的分布平衡。他会自动采集午高峰、晚高峰的主题实际生产和消费量,根据分区分布,leader分布,分区读写QPS信息,分区
    磁盘占用量等各项指标做综合分析,按照结果将集群内分片和leader信息平衡起来。
  4. 运维平台负责集群的创建,集群配置管理,节点上下线,集群内节点信息的界面展示等功能,是人工运营Broker集群的直接工具。
  5. Scanner负责监控集群内Broker的存活状态,不断循环扫描broker的服务端口,判断是否宕机,将宕机的服务从zookeeper上摘除掉。原生的Kafka集群在zookeeper上使用的是临时结点,Mafka将其改造为持
    久节点,依靠scanner来主动判断broker是否宕机。

演进方向前瞻

云原生时代,Mafka的挑战

现代的微服务架构充分使用了云的基础设施,实现高扩展性,高可用性,以及高韧性,面对市场形势多变的特点,以及互联网业务发展迅猛的形势,企业必须对业务进行快速的开发和迭代,以抢占瞬息万变的市场机会。

云原生应用的最大特点就是快速拥抱变化,他们可以在几分钟内快速的扩容,缩容,迁移,销毁,以应对业务和市场快速的需求变化。这对应用和服务的周边配套设施,如构建,部署,配置,监控,PaaS,IaaS等
支持服务提出了更高的要求,包括Mafka。
Mafka将通过以下几个架构调整来满足云原生代的业务需求:

  1. 组件各模块都接入k8s和容器,提升集群的扩展能力
  2. 去除ZooKeeper依赖,提升broker的扩展性
  3. 使用分层存储,减少本机存储的消息量,提升存量分区的迁移速度,提升扩展速度和效率

业务需求对Mafka的挑战

流量隔离、hash生产、消费模型对Mafka带来的资源压力

由于业务发展的需求,Mafka支持多重流量隔离方案,比如泳道,方便了QA人员同时开展多条测试链路,每个泳道的消费者只消费本泳道生产者生产的消息,如果下游没有泳道消费者,⻣干的消费者需要把泳道的消息也消费掉。

类似的流量隔离方案,还有环境隔离,比如test/dev/prod/stage各类测试和生产环境;Set化隔离,比如North、East、West、South等各种Set。Mafka在满足这些流量隔离时,采用的是使用队列模拟,但这种方式在底层实现时,经过各种条件的排列组合,会产生很多的队列资源消耗。

另外一方面,Mafka的消息队列在被业务客户端消费时,受限于Mafka的消费模型,一个队列的一个partition只能被一个消费者消费,当用户使用hash key的方式来生产时,或用户的消费者很多的时候,这时Mafka
就必须为业务扩容partition,增加更多的分区,以便新创建的消费者能消费到消息。在这种场景下,Mafka partition资源的消耗并不是来自于队列流量的增加,而仅仅是业务消费者数量的增加,消费者数量和
partition数量有紧密的耦合关系,导致partition资源的消耗。

Mafka4 读写分离来解决

以上所说的三个问题,不管是队列资源的消耗,还是partition资源的消耗,都会让整个集群的partition数量的增⻓,从而消耗整个集群的容量。最明显的问题,就是整个集群的partition数量增加到一定程度后,用户的生产发送耗时会增加,集群吞吐量会降低。
对于这个问题,业界和Kafka社区并没有现成的策略和办法来参考。经过我们的测试和调研,提出了Mafka 4解决方案,通过队列合并来,分离读写节点来解决这个问题。

如上图所示,Mafka4在原有分区的概念之上,引入了“虚拟分区”的概念,来承担消费端的读取,原有分区只用来承担写入,为了区分两种分区,写入消息的分区称为“物理分区”。

消息发送到Mafka之后,先写入物理分区,进过集群内节点之间的数据复制和拆分,再同步到虚拟分区上。
通过这种设计,整个集群内物理分区的数量只会与写入速度相关,消费端新增消费者时,只需要扩充虚拟分区即可,不再需要增加物理分区数量。
Mafka 4 改变了原有Kafka的读写模型,与原生Kafka副本通过ISR概念一样,在读节点上引入了“RISR”概念来管理副本的状态。
Mafka 4目前正在研发阶段,第一期研发已经完成,通过初步的性能测试,Mafka 4集群比现有Mafka 3 写入延迟tp999下降了将近 10 倍,集群机器节点使用量降低了近 1 倍!
2022 年Q 1会完成Mafka 4所有模块的研发,届时会上线实际接入用户队列,同时也会有专项的技术介绍blog发布,敬请期待。

实时计算业务的痛点

引入Kafka Stream 来构建一站式流式计算

随着业务的发展,实时计算技术近年来逐渐成熟起来,比如storm/flink一类实时计算框架,我们公司大数据业务也有相应的服务。
通常情况,在线业务在使用实时计算时采用的如下的架构:

业务先将数据发送到Mafka,然后再storm、flink平台上消费Mafka的消息,来做实时计算,计算完成后将数据再推送回Mafka,供在线业务消费和展示。

  1. 这种架构需要先将从Mafka消息搬迁到实时计算平台上,多了一次传输,浪费了一些时间和效率。
  2. 而且在问题排查方面,storm和f link非常不友好,因为业务需要将自己的代码上传到这两个平台上,程序实际是在远程平台上执行的,而远程平台又是大集群、多用户,调查问题、调试程序非常麻烦,效率低下。
  3. 另外,storm和f link自身有一定的复杂度,入⻔成本也比较高,对于一些只需要做一个简单窗口聚合计算的用户来说比较重,需要花时间和精力先学习。

实际上Kafka官方本身就支持轻量的实时流式计算服务,叫做Kafka Stream。Kafka Stream是集成在Kafka内部的轻量流式计算库,他跟Kafka集成在一起。
不同于Flink和Storm,Kafka Stream不是一个平台,不需要用户将代码打包上传到平台上,他只是一个简单的lib库,用户像写应用程序一样写流式计算服务,编译打包成功后,运行在用户自己的机器上。
Mafka将引入Kafka stream来承担一部分轻量式的流式计算业务,提升业务的接入和开发流式计算服务的效率。

团队介绍

作者简介:王军⻜(wangjunfei02)丨基础技术部-中间件研发中心-消息中间件组,消息中间件团队负责人。

团队介绍:美团基础技术部-基础架构团队诚招高级、资深技术专家,Base 北京、上海。我们致力于建设美团全公司统一的高并发高性能分布式基础架构平台,涵盖数据库、分布式监控、服务治理、高性能通信、消息中间件、基础存储、容器化、集群调度等基础架构主要的技术领域。