如何测试MQ(RocketMQ 4.X的测试分析)
2024-07-01 18:41 第二个卿老师 阅读(326) 评论(0) 编辑 收藏 举报背景
这次新项目用了RocketMQ,之前没有过多了解,简单学习后整理如下:
我们知道一般消息中间件的基础消费模型如下,生产者产生一类主题消息,而消费者就消费一类主题消息。
Rocket也是采用该模型,并进行了扩展,实现了多人发不同的topic且多人消费的场景。
上面还能看出,一个Topic下有多个队列,可以在不同Broker上。再结合一下部署架构图看看
一共分为4个模块,Producer,NameServer,Broker,Consumer,它们可以是单个也可以是集群。
所有最开始启动nameserver(名称服务器),作为路由控制器(类型微服务中的网关服务),再启动Broker(代理服务器)注册到nameserver中,这时可以创建Topic并指定在那台Broker上,并通过心跳同步数据到Nameserver,接着Producer启动并与某一个NameServer建立连接,选择Topic的一个队列,并对所属的Broker建立连接发送消息,最后Consumer启动也是类似,与某一个NameServer建立连接,选择Topic的一个队列进行消费。
测试分析
一般开发人员使用中间件会把流程走通,代表整个消息链路是ok的。基于质量分析如下
功能性
发消息
- 在各种条件下发送消息的Topic、Body、Tags等是否正确
- 触发消息发送的时机是否正确,一般是业务完全处理成功后才会触发
- 在某种条件下是否会重复发送消息
消费消息
- 消息的消息体不完整能否正确消费
- 重复的消息是否会重复消费,开发需要做幂等处理
- 在消费者集群的情况下,各消费者分配策略是否正确
- 在消费者集群的情况下,各消费者是否处理了重复消息消费
- 在消费者集群的情况下,如果需要顺序消费,是否处理了并发消费的情况
- 如果有批量方式消费,批量消费是否正确
- 如果消息堆积时,有跳过处理,跳过处理功能正确
- 同一个消费者组下所有消费者实例所订阅的Topic、Tag是否完全一致
可靠性
发消息
- 如果发送的是同步消息,是否做了发送失败的兜底机制
- 如果发送的是异步消息,是否做了MQ回调的异常处理
- 如果需要的是顺序消息场景,发送顺序消息的配置是否正确
- 发送批量消息时,消息占用是否可能大于1M
- 发送事务消息时,是否做了MQ导致的事务强制回滚处理
- 发送消息失败后,是否存在重试机制,重试功能是否正确
消费消息
- 消息消费失败是否做了错误处理(之前遇到一个字段为"",开发未做错误处理,会导致消费者无限重试消费)
- 如果需要的是顺序消息场景,消费顺序消息的配置是否正确
集群配置
- 生产是否会配置NamServer集群,测试环境需要验证部署方式正确
- 生产是否会配置Broker集群,测试环境需求验证部署方式正确
混沌测试(根据线上部署方式验证)
- 集群中单个NameServer宕机,发送多次消息,验证是否存在消息丢失、重复情况
- 集群中所有NameServer宕机,发送多次消息,验证消息丢失、重复情况,以及相关的报警触发机制
- 集群中单个Broker宕机,发送多次消息,验证是否存在消息丢失、重复情况
- 集群中所有Broker宕机,发送多次消息,验证消息丢失、重复情况,以及相关的报警触发机制
- 如果Broker部署了副本模式可分别测试Master与Slave分别宕机的情况
易用性
配置与部署
- 开发或运维是否输出Rocket的相关设计、配置、部署方式等文档
效率
发消息
- 如果发送的是延迟消息,是否避免设置了同一时刻
消费消息
- 消费者采用PUSH方式消费消息时,消费者客户端最大的消费吞吐量
- 消费者采用PULL方式消费消息时,服务端有大量消息时,拉取频率是否设置准确
- 消费者集群采用广播模式消费时,消费者客户端最大的消费吞吐量
可维护性
发送消息
- 消息发送成功或者失败是否打印消息日志,官方建议打印SendResult和key字段
- 消息子类型则可以用tags来标识,如果使用Tag,其设置是否准确,以方便后续消息过滤
- 每个消息是否设置keys字段,官方建议key尽可能唯一,方便将来定位消息丢失问题
消费消息
- 测试环境是否打印消费耗时,后续方便排查消费慢问题
消息轨迹
- 有额外的消息轨迹处理,则需要验证其功能正确
可移植性
数据交换
- 如果有rocket数据监控,验证配置正确
- 如果采用DLedger集群部署,需要单独验证功能
安全性
权限控制
- 如果做了权限认证,需要验证权限配置是否有效
兼容性
- 如果Rocket升级版本,需要进行兼容性测试
以上测试点根据当前质量要求与实施成本可自行删减,个人建议高优先级的已标红。
测试代码(java)
有点遗憾rocketmq在window上没有python版本的包,以下是java代码
import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.remoting.common.RemotingHelper; import java.util.Date; import java.text.SimpleDateFormat; import java.util.List; public class rocketmqJmeter { public static volatile boolean running = true; // 发送消息 public static void main(String[] args) throws Exception { // 初始化一个producer并设置Producer group name DefaultMQProducer producer = new DefaultMQProducer("qgc_producer_name"); //(1) // 设置NameServer地址 producer.setNamesrvAddr("xxx.xxx.xxx.xxx:9876"); //(2) // 启动producer producer.start(); for (int i = 0; i < 1; i++) { Date date = new Date(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒"); String currentTime = sdf.format(date); String originMsg = "{\"buyerAddress\":\"0xb270b2fa0\",\"numbers\":[{\"number\":\"\",\"price\":19.9,\"tbaAccount\":\"0x8adcca160846955a3fa93c1f87\"}],\"timestamp\":1718431824000,\"uid\":1729102500008869888}"; String LotteryDrawingMessage = "{\"round\":2,\"winners\":[{\"awardType\":\"SECOND\",\"desc\":\"账户:0x1f6f5d24cd\",\"prize\":\"2.28\",\"tbaAccount\":\"0x1f6fc8cdfeb806e3a\"},{\"awardType\":\"FIRST\",\"desc\":\"账户:0xa8268b9511ca56获得一等奖,发放9.12奖励\",\"prize\":\"9.12\",\"tbaAccount\":\"0xa8268b9511ca581afe1dab0bffcfebc6\"}]}"; String LotteryDrawingResultMessage ="{\"quota\":0,\"round\":3}"; // 创建一条消息,并指定topic、tag、body等信息,tag可以理解成标签,对消息进行再归类,RocketMQ可以在消费端对tag进行过滤 // Message msg = new Message("qgc_TopicTest" /* Topic */, // "qgc_TagA" /* Tag */, // ("Hello RocketMQ " + i +" " + currentTime).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ // ); //(3) Message msg = new Message("qgc_TopicTest", "", LotteryDrawingMessage.getBytes(RemotingHelper.DEFAULT_CHARSET)); // 利用producer进行发送,并同步等待发送结果 SendResult sendResult = producer.send(msg); //(4) System.out.printf("{\"code\": \"200\", \"SendResult\" : %s}", sendResult); } // 一旦producer不再使用,关闭producer producer.shutdown(); } //消费消息Lite Pull Consumer // DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer("qgc_consumer_name"); // litePullConsumer.setNamesrvAddr("xxx.xxx.xxx.xx:9876"); // litePullConsumer.subscribe("qgc_TopicTest", "*"); // litePullConsumer.setPullBatchSize(1); // litePullConsumer.start(); // try { // while (running) { // List<MessageExt> messageExts = litePullConsumer.poll(); // System.out.printf("%s%n", messageExts); // } // } finally { // litePullConsumer.shutdown(); // } }
maven的pom.xml配置
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.qgc.maven</groupId> <artifactId>rocketmqJmeter</artifactId> <version>1.0-SNAPSHOT</version> <name>rocketmqJmeter</name> <!-- FIXME change it to the project's website --> <url>http://www.example.com</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.7</maven.compiler.source> <maven.compiler.target>1.7</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.9.4</version> </dependency> <dependency> <groupId>org.apache.jmeter</groupId> <artifactId>ApacheJMeter_core</artifactId> <version>5.4.3</version> </dependency> <dependency> <groupId>org.apache.jmeter</groupId> <artifactId>ApacheJMeter_java</artifactId> <version>5.4.3</version> </dependency> </dependencies> <build> <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) --> <plugins> <!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle --> <plugin> <artifactId>maven-clean-plugin</artifactId> <version>3.1.0</version> </plugin> <!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging --> <plugin> <artifactId>maven-resources-plugin</artifactId> <version>3.0.2</version> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.1</version> </plugin> <plugin> <artifactId>maven-jar-plugin</artifactId> <version>3.0.2</version> </plugin> <plugin> <artifactId>maven-install-plugin</artifactId> <version>2.5.2</version> </plugin> <plugin> <artifactId>maven-deploy-plugin</artifactId> <version>2.8.2</version> </plugin> <!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle --> <plugin> <artifactId>maven-site-plugin</artifactId> <version>3.7.1</version> </plugin> <plugin> <artifactId>maven-project-info-reports-plugin</artifactId> <version>3.0.0</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.6</version> <configuration> <archive> <manifest> <!--会在MANIFES.MF中生成Class-Path项目--> <!--系统会根据Class-Path 项配置的路径加载依赖 --> <addClasspath>true</addClasspath> <!--指定依赖包所在目录,相对于项目最终Jar包的路径 --> <classpathPrefix>lib/</classpathPrefix> <!--指定MainClass --> <mainClass>com.idongjia.MySpringBootApplication</mainClass> </manifest> </archive> </configuration> </plugin> <!--配置依赖包 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>2.10</version> <!--相当于执行mvn 命令将依赖包打包到指定目录 --> <executions> <execution> <id>copy-dependencies</id> <phase>package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <!--将依赖包打包到target下的指定lib目录--> <outputDirectory>${project.build.directory}/lib</outputDirectory> </configuration> </execution> </executions> </plugin> </plugins> </pluginManagement> </build> </project>