1.本篇主要介绍实际的生产项目中,在消费者集群资源有限的前提下,通过哪些优化手段可以去提高 RabbitMQ 消费端的消费速度。
2.为了帮助大家能够更清晰的认识问题,文中特意将优化前和优化后的耗时进行了一个总结对比,文末提供有 demo 下载地址。
3.可以访问 这里 查看更多关于大数据平台建设的原创文章。

一. 魔盒简介

  1. 魔盒是禧云数芯大数据开发平台中的一个开发协作平台;
  2. 数据开发人员通过魔盒可以很方便的完成离线任务和实时任务的打包、测试、发布上线;
  3. 支持离线任务的串行、并行工作流设置;
  4. 提供完善的任务运行监控报警体系。

二. 魔盒离线任务打包流程

流程图

魔盒打包流程.png

流程梳理

  1. 数据开发人员在线下收到需求代码开发完毕之后,可以通过魔盒在测试环境创建 Spark 任务;
  2. 数据开发人员通过点击页面的项目构建按钮,发起一个对该 Spark 任务进行打包的请求;

packaging1.png

  1. 服务端收到打包请求后,会更新该条数据的状态为待构建;
  2. 定时任务每隔 1 秒从数据库中扫描一次,发现有待构建的任务,则就将任务放入队列中;
  3. 消息的消费者收到消息(即将打包的数据),开始对服务器上的项目进行打包(打包过程可能需要1-5分钟左右),通过 WebSocket 服务将打包日志返回给前端;

packaging3.png

  1. 前端(WebSocket 客户端)收到打包日志后,在界面窗口内实时展示,直到打包完成。

packaging2.png

三. 使用RabbitMQ的场景

  1. 服务端的定时任务只要从数据库中查找到有待构建的任务,就将该任务放入队列中,不用考虑该消息什么时候被消费掉,在这里充当的就是RabbitMQ 生产端的角色;
  2. RabbitMQ 消费端监听该队列,收到消息后,执行业务逻辑,通过后台执行 mvn 命令进行打包;
  3. 为了保障打包流程应用的健壮性,我们将消息的生产端和消息的消费端部署为两个服务。

四. 存在的问题

  1. 由于 mvn 打包的过程比较耗时(打包过程可能需要耗时1-5分钟左右),所以消费端消费消息会比较缓慢,会有较多 unack 状态的消息,导致产生消息积压;
  2. 当前端发起过多的打包请求时,由于队列中有较多的消息等待消费,因此吞吐量大大降低,反映出来的现象就是你的打包请求可能要等较长的时间后才能被服务端消费掉,因此在界面上要等待较长的时间才能看到打包日志的输出

五. 优化思路

1. 开启消费者多线程

在RabbitMQ中,我们可以创建多个消费者来消费同一个队列,从而提升消费速度。
多个消费者.png

添加容器工厂配置
/**

 * RabbitMQ配置

 * 开启消费者多线程,同时消费消息

 * @author lyf

 * @公众号 全栈在路上

 * @GitHub https://github.com/liuyongfei1

 * @date 2020-06-02 06:10

 */

@Slf4j

@Configuration

public class RabbitConsumerConfig {

@Bean("customContainerFactory")

public SimpleRabbitListenerContainerFactory containerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer, ConnectionFactory connectionFactory) {

SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();

// 设置线程数

factory.setConcurrentConsumers(10);

// 设置最大线程数

factory.setMaxConcurrentConsumers(10);

configurer.configure(factory,connectionFactory);

return factory;

}

}
消费端使用该容器工厂
@RabbitListener(queues = {QueueConstants.QUEUE_NAME}, containerFactory = "customContainerFactory")
查看设置效果

重启服务后,打开 RabbitMQ 管理界面,找到本次发送消息使用的队列并点击,会跳转到队列详情页里,在 Consumers 这一栏里会显示消费者的一些信息:
Consumers1.png

从图中可以看出,现在一共有 10 个消费者,证明我们刚刚的配置是生效的。

2. 消费端限流机制

轮询分发

在 RabbitMQ 中,默认的消息分发机制是 轮询分发。多个消费者会从队列里依次轮序去消费消息:

比如当前总共有 10 条消息,2个消费者, RabbitMQ Server 不会考虑当前哪个消费者是空闲状态还是繁忙状态,而是会一次性分配给 2个消费者每个消费者 5 条消息,平均每个消费者会获得相同数量的消息。

下面通过一个简单的 demo 去感受一下这种消息分发机制会有什么弊端。

  1. 生产端
/**

 * 消息生产端

 *

 * @author lyf

 * @公众号 全栈在路上

 * @GitHub https://github.com/liuyongfei1

 * @date 2020-06-11 06:30

 */

@Slf4j

@Data

@RestController

public class ProducerController {

@Autowired

private RabbitTemplate rabbitTemplate;

@GetMapping("/send")

public String send() {

for (int i = 1; i < 11; i++) {

String message = "NO. " + i;

String msgId = UUID.randomUUID().toString();

try {

TimeUnit.MILLISECONDS.sleep(100);

} catch (InterruptedException e) {

e.printStackTrace();

}

rabbitTemplate.convertAndSend("", QueueConstants.QUEUE_NAME, message, new CorrelationData(msgId));

log.info("生产端发送消息:[{}] 成功。", message);

}

return "发送消息成功";

}

}
  1. 消费端A
/**

 * 消息消费端A

 *

 * @author Liuyongfei

 * @公众号 全栈在路上

 * @GitHub https://github.com/liuyongfei1

 * @date 2020-06-11 13:30

 */

@Slf4j

@Data

@Component

public class Consumer1Controller {

@RabbitListener(queues = {QueueConstants.QUEUE\_NAME})

public void work(Message message, Channel channel) {

// 获取消息

String info = (String) message.getPayload();

log.info("消费者A获取到消息: {}", info);

try {

// 获取header

MessageHeaders headers = message.getHeaders();

Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);

TimeUnit.MILLISECONDS.sleep(200);

channel.basicAck(tag, false);

} catch (InterruptedException | IOException e) {

log.error("获取消息发生异常: " + e.getMessage());

}

}

}
  1. 消费端B
/**

 * 消息消费端B

 *

 * @author Liuyongfei

 * @公众号 全栈在路上

 * @GitHub https://github.com/liuyongfei1

 * @date 2020-06-11 13:50

*/

@Slf4j

@Data

@Component

public class Consumer2Controller {

@RabbitListener(queues = {QueueConstants.QUEUE_NAME})

public void work(Message message, Channel channel) {

// 获取消息

String info = (String) message.getPayload();

log.info("消费者B获取到消息: {}", info);

try {

// 获取header

MessageHeaders headers = message.getHeaders();

Long tag = (Long) headers.get(AmqpHeaders.DELIVERY\_TAG);

TimeUnit.MILLISECONDS.sleep(200);

channel.basicAck(tag, false);

} catch (InterruptedException | IOException e) {

log.error("获取消息发生异常: " + e.getMessage());

}

}

}
  1. 查看消费情况

consumer-console.png
我们从 idea console 控制台里可以看到两个消费者的消费情况,Queue 中的消息会被平摊给多个消费者。

  1. 弊端

    • 如果每个消息的处理时间不同,就有可能会导致某些消费者一直在忙,而另外一些消费者很快就处理完手头工作并一直空闲的情况;
    • 具体到我们的实际项目中,有的 Spark 任务在打包的时候可能需要依赖的 jar 包比较少,有的可能需要依赖的 jar 包比较多,还有打包时网络的影响,因此每个消费端处理具体的打包动作时的耗时是不一样的,所以使用轮询分发而不是按照消费者的实际能力去分发这种机制肯定会大大降低消费端的吞吐量。
公平分发

基于轮询分发机制会遇到的各种问题,那么怎样才能做到按照消费者的能力去公平的消费消息呢?
我们使用消费端限流和 ack 确认机制,来修改 RabbitMQ 的默认消息分发机制:
消息公平分发.jpeg

确保每个消费者在同一个时间点最多只处理一个 Message,换句话说,在接收到该消费者的 ack 之前,RabbitMQ Server不会将新的 Message 分发给该消费者。

  • application.properties
    在配置文件application.properties里添加以下两行配置:
# 开启 ACK(消费者接收到消息时手动确认)
spring.rabbitmq.listener.simple.acknowledge-mode=manual

# 设置当前信道最大预获取消息量为1
spring.rabbitmq.listener.simple.prefetch=1

重启服务,从 RabbitMQ 的管理界面查看:
公平分发1.png

  • 查看消费情况

我们将上面的消费者B的 sleep 时间调大为 1000 毫秒,用来模拟消费者B处理比较耗时的任务。然后重启服务,再次查看消费情况:
consumer-console2.png
从 idea console 控制台可以看出,经过设置后 RabbitMQ 已经按消费者的实际能力去分配消息了。

六. 打包任务

我们假设现在魔盒的数据库中有 5 条需要打包的数据,下面我们就使用这 5 条数据来作为后边的测试数据:
将要打包的数据1.png

备注:

  1. 以下测试均在本机完成,打包耗时会受到本地机器配置和网络环境的影响;
  2. 后边测试中的耗时时间包含以下几个流程:

    • 先从 git 仓库拉取所使用的分支代码;
    • 对当前项目代码进行打包;
    • 打包完成后自动将 jar 包上传至 HDFS 中去。

七. 优化前

1. 打包数据放入队列

打包console1.png

从图中我们可以看出这 5 条数据在 15:06 分的时候按照 version_id 由小到大的顺序被放入到 RabbitMQ 的队列中去:
queues1.png

消费端使用的是 1 个消费者:
consumers2.png

2. 消费时间

通过查询打包日志,可以找到最后一条消息的消费情况:

打包2.png
打包3.png

可以看到:

  • 最后一条消息在 15:10 开始消费;
  • 于 15:13 打包完毕;
  • 从 15:06 到 15:13 最后一个任务打包完毕,总共持续大约 7分钟。

八. 优化后

在消费端使用的是 10 个消费者,且设置当前信道最大预获取消息量(prefetch count)为1:

优化后prefetch count.png

1. 打包数据放入队列

优化后消息入列.png
从图中可以看到消息在 18:17 放入到队列中去。

2. 消费时间

通过查询打包日志,我们可以看到启动了多个线程同时消费消息:

2020-06-12 18:17:50,931 INFO  LOG_PACKAGING [] - [TaskSparkVersionEntity(versionId=388......

......

2020-06-12 18:17:50,932 INFO  LOG_PACKAGING [] - [TaskSparkVersionEntity(versionId=390, ......

......

2020-06-12 18:17:50,932 INFO  LOG_PACKAGING [] - [TaskSparkVersionEntity(versionId=386, ......

......

2020-06-12 18:17:50,932 INFO  LOG_PACKAGING [] - [TaskSparkVersionEntity(versionId=387, ......

......

2020-06-12 18:17:50,932 INFO  LOG_PACKAGING [] - [TaskSparkVersionEntity(versionId=389, ......

可以看到最后一条消息的消费情况:
优化后打包时间.png

可以看到:

  • 启动了多个线程,一共10个消费者;
  • 从 18:17:50 到18:21 最后一个任务打包完毕,总共持续大约 3分钟左右;
  • 从优化前的 7 分钟 到 优化后的 3 分钟,耗时大大缩短,消费端的吞吐量得到了很大的提高。

九. 代码下载地址

更多文章 

欢迎访问更多关于消息中间件的原创文章:

关注微信公众号

欢迎大家关注我的微信公众号阅读更多文章:
微信公众号二维码.jpg


liuyongfei
445 声望30 粉丝

可以访问 这里 查看更多关于大数据平台建设的原创文章。