分段随机实践—模拟线上流量

在日常的性能测试中,流量回放是一个非常优秀的方式,不仅可以快速构建测试用例且无限贴近预设场景,而且借助许多优秀的回放框架如goreplay等,可以极大地降低测试门槛,节省时间成本。此方式非常适合进行快速的性能排查,于在有限的资源下快速发现解决问题。

而另外一种方式就是重新构建测试场景的请求。常用的单接口测试、多接口混合测试、链路测试、全链路测试等等都是基于这个方式。单相比于流量回放能够实现的场景更加灵活,对于测试过程的控制也更加精细,对于集成CI/CD以及性能巡检也更加容易。但这类测试用例所面临的最大问题就是如何更加贴近真实流量。从单个接口来说就是用户在请求的时候参数分布,从业务链来说用户操作链路分叉。如果是针对某些特定场景的用例,所需要考虑的这些因素就比较少,但是在外扩方向上的全量负载测试的情况和内缩方向的精细化性能测试来说就显得有些捉襟见肘,复杂度就会飙升。

在之前很早的文章如何统一接口测试的功能、自动化和性能测试用例中,提到将所有的接口封装成方法,将功能和性能测试对象都转化成对这个方法的测试。通过构造参数、请求方法、处理返回值,这三步将执行测试用例的整个过程放在Java或者Groovy脚本中实现,这样用例的复杂程度很降低可读性会增高,也能极大提高测试框架的拓展性。

在实践过程中有一个必经之路的小石头:分段随机,就是根据线上流量分析出一个预设场景下的流量模型,主要信息就是接口请求比例,接口参数比例。然后根据这个比例发送不同的请求,携带不同的参数,使得压测流量更加贴近真实流量。

功能实现

这里我写了两个方法,思路前半部分重合如下:

  • 先将map分成两个对应的list,一个保存key一个保存value

后半部分不尽相同:

第一种:

  1. values转换成第i项为旧listi项之和的形式
  2. 随机函数从1~values最后一项(即旧list所有项之和)一个整型数字
  3. 循环判断该值落入某两个index下标的value之间,取出keys中响应的key返回
    /**
     * 根据不同的概率随机出一个对象
     * 消耗CPU多
     *
     * @param count
     * @param <F>
     * @return
     */
    public static <F> F randomCpu(Map<F, Integer> count) {
        List<F> keys = new ArrayList<>();
        List<Integer> values = new ArrayList<>();
        count.entrySet().forEach(f -> {
            keys.add(f.getKey());
            values.add(f.getValue());
        });
        int t = 0;
        for (int i = 0; i < values.size(); i++) {
            t = t + values.get(i);
            values.set(i, t);
        }
        int r = getRandomInt(values.get(values.size() - 1));
        for (int i = 1; i < values.size(); i++) {
            if (r <= values.get(i)) return keys.get(i);
        }
        return null;
    }

第二种:

  1. 遍历将values,将对应indexkey复制value-1次重新放入keys
  2. 从新的keys中随机一个key返回
    /**
     * 根据不同的概率随机出一个对象
     * 消耗内存多
     *
     * @param count
     * @param <F>
     * @return
     */
    public static <F> F randomMem(Map<F, Integer> count) {
        List<F> keys = new ArrayList<>();
        List<Integer> values = new ArrayList<>();
        count.entrySet().forEach(f -> {
            keys.add(f.getKey());
            values.add(f.getValue());
        });
        for (int i = 0; i < values.size(); i++) {
            for (int j = 0; j < values.get(i) - 1; j++) {
                keys.add(keys.get(i));
            }
        }
        return random(keys);
    }

测试

测试脚本

这里我设置了几项字符串以及对应的比例,然后执行N次,统计结果。

    public static void test0() {
        Map<String, Integer> map = new HashMap<>();
        map.put("a", 10);
        map.put("b", 20);
        map.put("c", 30);
        map.put("d", 1);
        map.put("e", 2);
        List<String> aa = new ArrayList<>();
        for (int i = 0; i < 1000000; i++) {
            String random = randomMem(map);
            aa.add(random);
        }
        CountUtil.count(aa);
    }

控制台输出

INFO-> 当前用户:oker,工作目录:/Users/oker/IdeaProjects/funtester/,系统编码格式:UTF-8,系统Mac OS X版本:10.16
INFO-> 元素:a,次数:159066
INFO-> 元素:b,次数:318615
INFO-> 元素:c,次数:474458
INFO-> 元素:d,次数:16196
INFO-> 元素:e,次数:31665

Process finished with exit code 0

基本符合设置。

项目实践

测试项目类

这里我虚拟了一个项目的一个功能类,然后有三个接口,每个接口有一个int类型参数,三个接口的请求比例和参数的分布比例我写在代码中了。多余三个list对象是为了方便验证实际结果的,在实际项目中并不存在。

package com.funtest.javatest;

import com.funtester.frame.SourceCode;
import com.funtester.utils.CountUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.*;

/**
 * FunTester测试分段随机类
 */
public class R_FunTester extends SourceCode {

    private static final Logger logger = LogManager.getLogger(R_FunTester.class);

    public static List<Integer> ts1 = new ArrayList<>();

    public static List<Integer> ts2 = new ArrayList<>();

    public static List<Integer> ts3 = new ArrayList<>();

    /**
     * 方法随机值
     */
    public static Map<Integer, Integer> ms = new HashMap<Integer, Integer>() {{
        put(1, 10);
        put(2, 20);
        put(3, 40);
    }};

    /**
     * 参数随机值
     */
    public static Map<Integer, Integer> ps = new HashMap<Integer, Integer>() {{
        put(10, 10);
        put(20, 20);
        put(30, 40);
    }};

    public void test1(int a) {
        ts1.add(a);
    }

    public void test2(int a) {
        ts2.add(a);
    }

    public void test3(int a) {
        ts3.add(a);
    }

    public void online() {
        Integer m = randomMem(ms);
        switch (m) {
            case 1:
                test1(randomMem(ps));
                break;
            case 2:
                test2(randomMem(ps));
                break;
            case 3:
                test3(randomMem(ps));
                break;
            default:
                break;
        }
    }

}

测试脚本

这里我通过三个listsize统计方法执行次数,通过list中元素统计,验证方法参数的随机性。

    public static void main(String[] args) {
        R_FunTester driver = new R_FunTester();
        range(1000000).forEach(f -> driver.online());
        output(ts1.size() + TAB + ts2.size() + TAB + ts3.size());
        CountUtil.count(ts1);
        CountUtil.count(ts2);
        CountUtil.count(ts3);
        test0();
    }

控制台输出

INFO-> 当前用户:oker,工作目录:/Users/oker/IdeaProjects/funtester/,系统编码格式:UTF-8,系统Mac OS X版本:10.16
INFO-> 142168	286236	571596
INFO-> 元素:20,次数:40563
INFO-> 元素:10,次数:20468
INFO-> 元素:30,次数:81137
INFO-> 元素:20,次数:81508
INFO-> 元素:10,次数:40873
INFO-> 元素:30,次数:163855
INFO-> 元素:20,次数:163643
INFO-> 元素:10,次数:81117
INFO-> 元素:30,次数:326836

Process finished with exit code 0

也都是符合预期的,完活儿!继续搬砖!


FunTester腾讯云年度作者Boss直聘签约作者GDevOps官方合作媒体,非著名测试开发。