java应用容灾方案_Java服务MQ消息队列容灾方案-Go语言中文社区

背景介绍

在前一段时间线上出现过一次事故,一个非常重要的消息生产者服务,由于MQ出现问题,消息大量积压,导致了该服务线程被打满,外部请求返回502,服务采用Springboot搭建,使用Springboot的Tomcat容器。

原因分析

消息的生产者服务是一个高并发量的服务,接受外部方的接口调用,并将消息推送至MQ,调用流程示意图如下:

java应用容灾方案_Java服务MQ消息队列容灾方案-Go语言中文社区_第1张图片

而事故当天的情况是,MQ消息大量积压,基本等同于MQ挂掉,

java应用容灾方案_Java服务MQ消息队列容灾方案-Go语言中文社区_第2张图片

大量的请求积压在推送消息到MQ的地方,导致外部的大量的请求在Tomcat的线程池积压,当Tomcat的线程池全部被打满后,服务不能再接受新的请求进入,导致抛出大量的502错误。

容灾方案分析

一、发送消息超时时间设定

首先可以想到的是,在MQ发送消息处,设置推送消息的超时时间,超过超时时间,认为消息发送失败,将消息写入文件中,当时这个方案并没有根本上解决,如果MQ挂掉,Tomcat不被打满的问题,虽然可以解决目前的生产场景的情况,但是当后续请求量更大时候,不能保证Tomcat不被打满,同时,消息推送的超时时间的设定也不好进行把握,如果由于网络波动或其他情况,导致消息推送慢,但是是可以推送成功的,但是万一超过了超时时间,消息直接不会发送,反而会影响目前的业务逻辑。

java应用容灾方案_Java服务MQ消息队列容灾方案-Go语言中文社区_第3张图片

二、调整Tomcat线程池大小

事故的起因是因为Tomcat被打满,那调整Tomcat的线程池大小,调整大一些不就可以了吗?但是这是一种治标不治本的方法,并没有根本上解决MQ挂掉后,Tomcat被打满的情况,只能是延迟了被打满的时间,但是根据目前线上机器配置的情况,Tomcat增大线程池大小并不是一个合适的选择。

三、启用异步

问题的根本是MQ挂掉,主线程全部卡在MQ发送消息的部分,那么是否可以考虑,将MQ发送消息的这个操作异步化,让Tomcat主线程不在此等待,而是转由异步线程执行发送消息的操作?这个方案看来还是比较靠谱的,这里我首先考虑引入线程池,进行异步化处理,

java应用容灾方案_Java服务MQ消息队列容灾方案-Go语言中文社区_第4张图片

OK,方案确认,那么线程池的参数设置需要进行考虑,一般常规的线程池线程数设置为:CPU core * 2 +1,也有其他的线程池估算算法:估算线程池数目大小 ,这里我采用传统的设置方式,初始化线程池核心数为 CPU core * 2 +1,最大线程数:4 * (CPU core * 2 +1),阻塞队列:1000。

由于我们采用了线程池,那么对于线程池的监控是必须的,这里我设置为线程数达到最大线程数的80%会进行告警,因为这时候说明MQ推送消息可能已经出现堆积的情况了,下面给出代码的实例:

消息推送异步化:

@Component

public class MessageProducer InitializingBean {

@Autowired

private MqService mqService;

private static ThreadPoolExecutor pool = null;

@Override

public void sendToMessageBus(String message) {

//线程池异步处理

try {

pool.execute(() -> {

try {

//推送消息

mqService.send(message);

monitorThreadPool();

} catch (Exception e) {

log.error("send message to message bus error, cause : {}", e);

handleFailMessage(message);

}

});

} catch (Exception e) {

log.error("commit send message to thread pool error, prepare to save message in file......");

handleFailMessage(message);

}

}

@Override

public void afterPropertiesSet() throws Exception {

pool = new ThreadPoolExecutor(5, 20, 0L, TimeUnit.MILLISECONDS,

new LinkedBlockingQueue<>(1000),

new ThreadPoolExecutor.AbortPolicy());

//启动消息失败处理线程

FailMessageHandlerProducer republishThread = new FailMessageHandlerProducer();

republishThread.setName("republish-thread");

republishThread.start();

}

public static void shutdown() {

if (pool != null) {

pool.shutdown();

}

}

private void handleFailMessage(String message) {

JSONObject jsonObject = new JSONObject();

jsonObject.put("message", message);

FailMessageHandlerProducer.pushEvent(jsonObject);

}

private void monitorThreadPool() {

try {

//一级告警,线程池当前活动线程数大于阈值

if (pool.getActiveCount() > 16) {

//告警处理

}

//二级告警,线程池阻塞队列当前对象数大于阈值

if (pool.getQueue().size() > 100) {

//告警处理

}

} catch (Exception e) {

log.error("monitorThreadPool alarm error, cause : {}", e);

}

}

}

失败消息处理:

public class FailMessageHandlerProducer extends Thread {

private static LinkedBlockingQueue queue = new LinkedBlockingQueue<>(10000);

private static volatile boolean terminate = false;

private long timeout = 10;

public FailMessageHandlerProducer() {

}

public static synchronized void pushEvent(JSONObject republishData) {

try {

queue.put(republishData);

} catch (Exception e) {

log.error("FailMessageHandlerProducer push error", e);

}

}

@Override

public void run() {

while (!terminate) {

try {

JSONObject republishData = queue.poll(timeout, TimeUnit.SECONDS);

if (republishData != null) {

//写入文件

} else {

//停顿2s

Thread.sleep(2 * 1000);

}

} catch (Exception e) {

log.error("republish message error, cause : {}", e);

}

}

}

public static void setTerminate() {

terminate = false;

}

}

这里我采用了线程池进行异步化发送消息,当MQ挂掉或者推送消息特别慢的时候,线程池中的线程首先会进行积压,直到线程池最大线程数,在之后进入的线程会进入阻塞队列,当阻塞队列被打满后,线程池会抛出异常,捕获异常后将消息写入文件。关于线程池的机制可以看一下我的另一篇博文:Java ThreadPoolExecutor线程池概述

压力测试

压力测试的工具,我使用是Jmeter,Jmeter使用,在PC环境下的压力测试数据如下,环境Intel 八代i5 4核 + 16G,

条件

并发

请求次数

每秒吞吐使用线程池(初始化5,最大线程数20,阻塞队列1000)

1000并发/1s

2000

220.8

使用线程池(初始化5,最大线程数20,阻塞队列1000)

1000并发/1s

2000

257.2

使用线程池(初始化5,最大线程数20,阻塞队列1000)

1000并发/1s

2000

217.5

不使用线程池

1000并发/1s

2000

16.6

使用线程池(初始化50,最大线程数100,阻塞队列1000)

2000并发/1s

10000

181

使用线程池(初始化30,最大线程数80,阻塞队列1000)

2000并发/1s

10000

159

不使用线程池

2000并发/1s

6000

16.8

可以看到,在高并发场景下,没有线程池的场景,吞吐量差距非常巨大,但是也可以看到,线程池的线程数并不是越大越大的,需要根据服务器的配置情况,设定好合适的线程池配置。

结语

本文结合我自己遇见的一次线上事故,采取的容灾方案,这个方案肯定不是很完美或者或者说设计的很好的,因为当MQ挂掉后,很多消息会写入文件,将这部分消息重新处理也是一个比较麻烦的事情,本文就是一个抛砖引玉的给出一个大概思路,如果你有更好的方案,欢迎留言我们讨论!

你可能感兴趣的:(java应用容灾方案)