代码改变世界

如何测试MQ(RocketMQ 4.X的测试分析)

2024-07-01 18:41  第二个卿老师  阅读(326)  评论(0编辑  收藏  举报

官网了解rocketmq

背景

这次新项目用了RocketMQ,之前没有过多了解,简单学习后整理如下:
我们知道一般消息中间件的基础消费模型如下,生产者产生一类主题消息,而消费者就消费一类主题消息。
Rocket也是采用该模型,并进行了扩展,实现了多人发不同的topic且多人消费的场景。
上面还能看出,一个Topic下有多个队列,可以在不同Broker上。再结合一下部署架构图看看
一共分为4个模块,Producer,NameServer,Broker,Consumer,它们可以是单个也可以是集群。
所有最开始启动nameserver(名称服务器),作为路由控制器(类型微服务中的网关服务),再启动Broker(代理服务器)注册到nameserver中,这时可以创建Topic并指定在那台Broker上,并通过心跳同步数据到Nameserver,接着Producer启动并与某一个NameServer建立连接,选择Topic的一个队列,并对所属的Broker建立连接发送消息,最后Consumer启动也是类似,与某一个NameServer建立连接,选择Topic的一个队列进行消费。

测试分析

一般开发人员使用中间件会把流程走通,代表整个消息链路是ok的。基于质量分析如下

功能性

发消息
  1. 在各种条件下发送消息的Topic、Body、Tags等是否正确
  2. 触发消息发送的时机是否正确,一般是业务完全处理成功后才会触发
  3. 在某种条件下是否会重复发送消息
消费消息
  1. 消息的消息体不完整能否正确消费
  2. 重复的消息是否会重复消费,开发需要做幂等处理
  3. 在消费者集群的情况下,各消费者分配策略是否正确
  4. 在消费者集群的情况下,各消费者是否处理了重复消息消费
  5. 在消费者集群的情况下,如果需要顺序消费,是否处理了并发消费的情况
  6. 如果有批量方式消费,批量消费是否正确
  7. 如果消息堆积时,有跳过处理,跳过处理功能正确
  8. 同一个消费者组下所有消费者实例所订阅的Topic、Tag是否完全一致

可靠性

发消息
  1. 如果发送的是同步消息,是否做了发送失败的兜底机制
  2. 如果发送的是异步消息,是否做了MQ回调的异常处理
  3. 如果需要的是顺序消息场景,发送顺序消息的配置是否正确
  4. 发送批量消息时,消息占用是否可能大于1M
  5. 发送事务消息时,是否做了MQ导致的事务强制回滚处理
  6. 发送消息失败后,是否存在重试机制,重试功能是否正确
消费消息
  1. 消息消费失败是否做了错误处理(之前遇到一个字段为"",开发未做错误处理,会导致消费者无限重试消费)
  2. 如果需要的是顺序消息场景,消费顺序消息的配置是否正确
集群配置
  1. 生产是否会配置NamServer集群,测试环境需要验证部署方式正确
  2. 生产是否会配置Broker集群,测试环境需求验证部署方式正确
混沌测试(根据线上部署方式验证)
  1. 集群中单个NameServer宕机,发送多次消息,验证是否存在消息丢失、重复情况
  2. 集群中所有NameServer宕机,发送多次消息,验证消息丢失、重复情况,以及相关的报警触发机制
  3. 集群中单个Broker宕机,发送多次消息,验证是否存在消息丢失、重复情况
  4. 集群中所有Broker宕机,发送多次消息,验证消息丢失、重复情况,以及相关的报警触发机制
  5. 如果Broker部署了副本模式可分别测试Master与Slave分别宕机的情况

易用性

配置与部署
  1. 开发或运维是否输出Rocket的相关设计、配置、部署方式等文档

效率

发消息
  1. 如果发送的是延迟消息,是否避免设置了同一时刻
消费消息
  1. 消费者采用PUSH方式消费消息时,消费者客户端最大的消费吞吐量
  2. 消费者采用PULL方式消费消息时,服务端有大量消息时,拉取频率是否设置准确
  3. 消费者集群采用广播模式消费时,消费者客户端最大的消费吞吐量

可维护性

发送消息
  1. 消息发送成功或者失败是否打印消息日志,官方建议打印SendResult和key字段
  2. 消息子类型则可以用tags来标识,如果使用Tag,其设置是否准确,以方便后续消息过滤
  3. 每个消息是否设置keys字段,官方建议key尽可能唯一,方便将来定位消息丢失问题
消费消息
  1. 测试环境是否打印消费耗时,后续方便排查消费慢问题
消息轨迹
  1. 有额外的消息轨迹处理,则需要验证其功能正确

可移植性

数据交换
  1. 如果有rocket数据监控,验证配置正确
  2. 如果采用DLedger集群部署,需要单独验证功能

安全性

权限控制
  1. 如果做了权限认证,需要验证权限配置是否有效

兼容性

  1. 如果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>