Gatling 性能测试

Gatling是一个基于AKKA和Netty开发的高性能压测工具,使用非常简单。


概述和使用

Gatling可以直接下载使用,也可以通过maven插件在项目中集成,通过命令行执行。

直接使用参见官方网站(见参考资料1)。这里介绍通过maven插件来使用。

gatling-maven-plugin

首先引入依赖包,这个是测试脚本需要的:

<dependency>
   <groupId>io.gatling.highcharts</groupId>
   <artifactId>gatling-charts-highcharts</artifactId>
   <version>MANUALLY_REPLACE_WITH_LATEST_VERSION</version>
   <scope>test</scope>
 </dependency>

然后引入依赖插件:

<plugin>
  <groupId>io.gatling</groupId>
  <artifactId>gatling-maven-plugin</artifactId>
  <version>MANUALLY_REPLACE_WITH_LATEST_VERSION</version>
</plugin>

最好指定明确的插件版本。如果不指定插件版本,系统会自动查找最新的版本,这样可能无法保证构建可重复。因为无法保证插件将来的行为和当前是一致的。

对于Scala开发来说,从4.x版本开始,该插件不再编译Scala代码,如果测试类是用scala来写的,则必须要用scala-maven-plugin插件来代替。

为了方便,本文以前文创建好的项目代码为基础。新增一个hello-world-test-perform子模块,专门用来做负载测试。

在test包下创建测试类BasicSimulation:

import io.gatling.javaapi.core.*;
import io.gatling.javaapi.http.*;

import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;

public class BasicSimulation extends Simulation {

    public final String hostname = System.getProperty("url");

    HttpProtocolBuilder httpProtocol = http
            .baseUrl(hostname)
            .acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
            .doNotTrackHeader("1")
            .acceptLanguageHeader("en-US,en;q=0.5")
            .acceptEncodingHeader("gzip, deflate")
            .userAgentHeader("Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0");

    ScenarioBuilder scn = scenario("HelloWorldSimulation")
            .exec(http("request_1").get("/data/hello"))
            .pause(5);

    {
        //注入用户,刚开始就一个,协议是http
        setUp(scn.injectOpen(atOnceUsers(1))).protocols(httpProtocol);
    }
}

其中,hostname是从系统变量中获取的,这个是在plugin中配置的:

<plugin>
    <groupId>io.gatling</groupId>
    <artifactId>gatling-maven-plugin</artifactId>
    <configuration>
        <skip>${skipLTandPTs}</skip>
        <jvmArgs>
            <jvmArg>-Durl=${testTarget}</jvmArg>
        </jvmArgs>
    </configuration>
    <executions>
        <execution>
            <phase>test</phase>
            <goals>
                <goal>test</goal>
            </goals>
        </execution>
    </executions>
</plugin>

这个插件中skipLTandPTstestTarget参数是从父文件中继承过来的。父文件pom.xml的配置如下:

...
<profiles>
    <profile>
        <id>local</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <properties>
            <environment>dev</environment>
            <testTarget>http://localhost:8080/tt</testTarget>
        </properties>
    </profile>

    <profile>
        <id>pt</id>
        <properties>
            <skipTests>true</skipTests>
            <skipLTandPTs>false</skipLTandPTs>
        </properties>
    </profile>
</profiles>

这样,运行hello-world项目后,在命令行执行:

 mvn clean test -P local,pt

就能进行负载测试了。生成的结果默认放在target/gatling目录下,在浏览器中访问index.html如下:
在这里插入图片描述

基本概念

gatling中主要的对象包括:Simulation,Injection,Scenario,Session等。

Simulation

setUp

setUp是Simulation中的必要部分:

ScenarioBuilder scn = scenario("scn"); // etc...

{
	setUp(
	  scn.injectOpen(atOnceUsers(1))
	);
}

协议配置

协议配置可以写在setUp方法外面,表示对所有的场景生效;也可以写在每个场景上,表示对当前场景生效。如下所示:

// HttpProtocol configured globally
setUp(
  scn1.injectOpen(atOnceUsers(1)),
  scn2.injectOpen(atOnceUsers(1))
).protocols(httpProtocol);

// different HttpProtocols configured on each population
setUp(
  scn1.injectOpen(atOnceUsers(1))
    .protocols(httpProtocol1),
  scn2.injectOpen(atOnceUsers(1))
    .protocols(httpProtocol2)
);

验收条件

验收条件决定了本次负载测试能否通过,在setUp方法上配置。

setUp(scn.injectOpen(atOnceUsers(1)))
  .assertions(global().failedRequests().count().is(0L));
  
setUp(scn.injectOpen(atOnceUsers(1)))
 .assertions(
    global().responseTime().mean().lt(2000),
    global().successfulRequests().percent().gt(99.0)
);

Scenario

场景名可以是除了\t之外的任何字符:

ScenarioBuilder scn = scenario();

exec

exec方法用于执行模拟的接口调用,支持HTTP,LDAP,POP,IMAP等协议。

下面是HTTP协议的模拟请求示例:

// 绑定到scenario
scenario("Scenario")
  .exec(http("Home").get("https://gatling.io"));

// 直接创建以便于后续引用
ChainBuilder chain = exec(http("Home").get("https://gatling.io"));

// 绑定到其他
exec(http("Home").get("https://gatling.io"))
  .exec(http("Enterprise").get("https://gatling.io/enterprise"));

exec也可用于传递函数。通过这一特性可方便地设置和调试Session

exec(session -> {
  // displays the content of the session in the console (debugging only)
  System.out.println(session);
  // return the original session
  return session;
});

exec(session ->
  // return a new session instance
  // with a new "foo" attribute whose value is "bar"
  session.set("foo", "bar")
);

停顿

通常,当一个用户浏览页面时,两次操作之间会有时间间隔。为了模拟这一行为,gatling提高了停顿方法。

下面是一些使用方式:

暂停时间固定

pause(10); // with a number of seconds
pause(Duration.ofMillis(100)); // with a java.time.Duration
pause("#{pause}"); // with a Gatling EL string resolving to a number of seconds or a java.time.Duration
pause(session -> Duration.ofMillis(100)); // with a function that returns a java.time.Duration

暂时时间随机

pause(10, 20); // with a number of seconds
pause(Duration.ofMillis(100), Duration.ofMillis(200)); // with a java.time.Duration
pause("#{min}", "#{max}"); // // with a Gatling EL strings
pause(session -> Duration.ofMillis(100), session -> Duration.ofMillis(200)); // with a function that returns a java.time.Duration

循环

重复

如果某个请求重复发生,可以通过repeat来模拟。

// with an Int times
repeat(5).on(
  exec(http("name").get("/"))
);
// with a Gatling EL string resolving an Int
repeat("#{times}").on(
  exec(http("name").get("/"))
);
// with a function times
repeat(session -> 5).on(
  exec(http("name").get("/"))
);
// with a counter name
repeat(5, "counter").on(
  exec(session -> {
    System.out.println(session.getInt("counter"));
    return session;
  })
);

遍历

可以按照指定顺序对列表中的每个元素依次执行action。主要有三个参数:

  • seq:需要遍历的元素序列,可以是列表或Gatling EL表达式或函数
  • elementName:key
  • counterName(可选):循环计数器,从0开始
// with a static List
foreach(Arrays.asList("elt1", "elt2"), "elt").on(
  exec(http("name").get("/"))
);
// with a Gatling EL string
foreach("#{elts}", "elt").on(
  exec(http("name").get("/"))
);
// with a function
foreach(session -> Arrays.asList("elt1", "elt2"), "elt").on(
  exec(http("name").get("/"))
);
// with a counter name
foreach(Arrays.asList("elt1", "elt2"), "elt", "counter").on(
  exec(session -> {
    System.out.println(session.getString("elt2"));
    return session;
  })
);

还有其他的一些循环操作,这里就不一一列举了。

错误处理

tryMax

tryMax可以指定重试次数,当action执行失败时,会进行重试。

tryMax(5).on(
  exec(http("name").get("/"))
);

// with a counter name
tryMax(5, "counter").on(
  exec(http("name").get("/"))
);

exitBlockOnFail

失败立即退出:

exitBlockOnFail(
  exec(http("name").get("/"))
);

exitHere

指定虚拟用户从scenario退出:

exitHere();

exitHereIf

根据条件退出:

exitHereIf("#{myBoolean}");
exitHereIf(session -> true);

Injection

使用injectOpen和injectClosed方法来定义用户的注入配置信息(与Scala中的inject作用相同),该方法参数为一系列的注入步骤,处理时也按顺序处理。

Open和Closed工作负载模型

当谈到负载模型时,通常有两种类型的系统:

  • Closed系统,可以用来控制并发用户数量
  • Open系统,可以用来控制用户的到达率

封闭系统中并发用户数是有上限的,当并发数达到上限时,只有当有用户退出时,新的用户才能进入系统。
这与线程池工作模式是类似的,当工作线程占用满了的情况下,新的请求进入任务队列,等待有线程空闲下来才能继续处理。
售票业务系统一般需要采用封闭模型。

封闭模型适用于可以异步获取结果的系统

而开放系统与之相反,在开放系统中无法控制并发用户数量,即使业务系统已经不能处理多余的请求了,新的用户还是会持续不断地到来并发起请求。
大部分业务系统均是这种情况。

注意:请根据系统业务类型来决定采用哪一种测试模型。如果实际业务类型与测试的模型不匹配,就无法达到预期效果。

开放模型与封闭模型具有相反的含义,不要在同一个注入配置中混用。

开放模型

下面是一个开放模型的例子(其他语言见参考资料2):

setUp(
  scn.injectOpen(
    nothingFor(4), // 设置一段停止时间,在此时间内,什么都不做
    atOnceUsers(10), // 立即注入指定数量的虚拟用户
    rampUsers(10).during(5), // 在指定时间段内,逐步注入指定数量的虚拟用户
    constantUsersPerSec(20).during(15), // 在指定时间段内,每秒注入指定数量的虚拟用户
    constantUsersPerSec(20).during(15).randomized(), // 在指定时间段内,每秒注入围绕指定数量随机增减的虚拟用户
    rampUsersPerSec(10).to(20).during(10), // 在指定时间段内,注入的虚拟用户数从一个值逐渐(线性)增加到另一个值
    rampUsersPerSec(10).to(20).during(10).randomized(), // 在指定时间段内,注入的虚拟用户数从一个值增加到另一个值,但增长过程不是线性的的,而是随机跳跃
    stressPeakUsers(1000).during(20) // 在指定时间段内,按照 heaviside step 函数的平滑近似值注入指定数量的用户
  ).protocols(httpProtocol)
);

封闭模型

下面是一个封闭模型的例子:

setUp(
  scn.injectClosed(
    constantConcurrentUsers(10).during(10), // 在指定时间段内保持恒定的虚拟用户数。注意,常态并发用户意味着当某个用户的scenario完成后,gatling会创建一个新的用户,以此来保持并发用户数的恒定。因此,active user可能会大于constantConcurrentUsers
    rampConcurrentUsers(10).to(20).during(10) // 在指定时间段内,虚拟用户数从一个值线性增长到另一个值
  )
);

Meta DSL

在测试之前,通常我们并不知道系统吞吐量是多少。为了测试瓶颈值,可能会用不同的数值去做重复的操作来尝试,例如:

rampUsersPerSec(10).to(20).during(10),
rampUsersPerSec(20).to(30).during(10),
rampUsersPerSec(30).to(50).during(10),
rampUsersPerSec(50).to(70).during(10),
rampUsersPerSec(70).to(100).during(10),
);

为了解决这一问题,Gatling在3.0中增加了一种Meta DSL新的方法来方便我们操作。

incrementUsersPerSec(usersPerSecAddedByStage)

setUp(
  // generate an open workload injection profile
  // with levels of 10, 15, 20, 25 and 30 arriving users per second
  // each level lasting 10 seconds
  // separated by linear ramps lasting 10 seconds
  scn.injectOpen(
    incrementUsersPerSec(5.0)
      .times(5)
      .eachLevelLasting(10)
      .separatedByRampsLasting(10)
      .startingFrom(10) // Double
  )
);

incrementConcurrentUsers(concurrentUsersAddedByStage)

setUp(
  // generate a closed workload injection profile
  // with levels of 10, 15, 20, 25 and 30 concurrent users
  // each level lasting 10 seconds
  // separated by linear ramps lasting 10 seconds
  scn.injectClosed(
    incrementConcurrentUsers(5)
      .times(5)
      .eachLevelLasting(10)
      .separatedByRampsLasting(10)
      .startingFrom(10) // Int
  )
);

incrementUsersPerSec用于开放模型负载测试,incrementConcurrentUsers用于封闭模型负载测试。separatedByRampsLastingstartingFrom 都是可选的。
如果未指定坡度,则虚拟用户增长方式是跳跃的。如果未指定起始用户数,将从0开始。

并发场景

在同一个setUp中可以同时设置多个场景注入,然后同时并发执行

setUp(
	scenario1.injectOpen(injectionProfile1),
	scenario2.injectOpen(injectionProfile2)
);

有序场景

除了并发场景外,有的场景是有序的,可以通过andThen来设置有序场景。有序场景中,只有当父场景执行完成后,子场景才开始执行。

setUp(
  parent.injectClosed(injectionProfile)
    // child1 and child2 will start at the same time when last parent user will terminate
    .andThen(
      child1.injectClosed(injectionProfile)
        // grandChild will start when last child1 user will terminate
        .andThen(grandChild.injectClosed(injectionProfile)),
      child2.injectClosed(injectionProfile)
    )
);

Session

Session API

Session API可以编程式地处理用户数据。

大多数情况下,负载测试时有一点比较重要,那就是要保证虚拟用户的请求参数是不一样的。如果每个虚拟用户都使用相同的参数,那可能是在测试缓存,而不是测试实际系统负载。

更甚者,当你在Java虚拟机上执行测试用例时,JVM本身通过即时编译器(JIT)对代码进行了优化,从而导致得到了与实际生产环境中不同的性能结果。

Session

Session是虚拟用户的状态。

通常来说,session是一个Map<String, Object>结构。在Gatling中,session中的所有的键值对都是Session属性。

Gatling中的scenario是一个工作流,工作流中的每一步是一个Action,Session可以在工作流中传递数据。

设置属性

// set one single attribute
Session newSession1 = session.set("key", "whateverValue");
// set multiple attributes
Session newSession2 = session.setAll(Collections.singletonMap("key", "value"));
// remove one single attribute
Session newSession3 = session.remove("key");
// remove multiple attributes
Session newSession4 = session.removeAll("key1", "key2");
// remove all non Gatling internal attributes
Session newSession5 = session.reset();

Session 是不可变类,这意味着调用set方法后,将返回一个新的实例,而不是原来的实例。


// 错误用法: result from Session#set is discarded
exec(session -> {
  session.set("foo", "bar");
  System.out.println(session); 
  return session;
});

// 正确用法
exec(session -> {
  Session newSession = session.set("foo", "bar");
  System.out.println(newSession);
  return newSession;
});

从session中获取用户属性

// the unique id of this virtual user
long userId = session.userId();
// the name of the scenario this virtual user executes
String scenario = session.scenario();
// the groups this virtual user is currently in
List<String> groups = session.groups();

函数

使用函数可以生成动态参数。如下所示:

// inline usage with a Java lamdba
exec(http("name")
  .get(session -> "/foo/" + session.getString("param").toLowerCase(Locale.getDefault())));

// passing a reference to a function
Function<Session, String> f =
    session -> "/foo/" + session.getString("param").toLowerCase(Locale.getDefault());
exec(http("name").get(f));

如果要使用随机生成的参数,必须要在函数中生成,如
.header("uuid", x -> RandomStringUtils.randomAlphanumeric(5)),而不能直接使用 .header("uuid", RandomStringUtils.randomAlphanumeric(5)),这样只有第一次请求是随机值,后面的请求都使用第一次生成的值。

Feeders

在gatling中,可以通过外部方式如csv文件,向虚拟用户注入数据,此时需要用到Feeder。

Feeder实际上是迭代器Iterator<Map<String, T>>的别称。

下面是一个构造feeder的例子:

// import org.apache.commons.lang3.RandomStringUtils
Iterator<Map<String, Object>> feeder =
  Stream.generate((Supplier<Map<String, Object>>) () -> {
      String email = RandomStringUtils.randomAlphanumeric(20) + "@foo.com";
      return Collections.singletonMap("email", email);
    }
  ).iterator();

外部数据源使用策略

外部数据源的使用策略有多种:

// 默认: 对基础系列使用迭代器
csv("foo").queue();
// 随机选择序列中的记录
csv("foo").random();
// 按打乱之后的顺序取记录
csv("foo").shuffle();
// 当数据(从头到尾)取完后又从头开始
csv("foo").circular();

当使用queue和shuffle策略时,请保证你的数据量是足够的,一旦数据用完,gatling将会自动关闭。

使用列表和数组

当然,也可以使用内存中的列表或数组来给虚拟用户注入数据:

// using an array
arrayFeeder(new Map[] {
  Collections.singletonMap("foo", "foo1"),
  Collections.singletonMap("foo", "foo2"),
  Collections.singletonMap("foo", "foo3")
}).random();

// using a List
listFeeder(Arrays.asList(
  Collections.singletonMap("foo", "foo1"),
  Collections.singletonMap("foo", "foo2"),
  Collections.singletonMap("foo", "foo3")
)).random();

基于文件的Feeders

上面说到了外部数据注入时取数据的策略,如csv("foo").queue()。那这个csv文件应该放到哪里呢?

当使用构建工具如maven,gradle或sbt时,文件必须放在src/main/resourcese或者src/test/resources目录下。

文件路径不要使用相对路径src/main/resources/data/file.csv,应该使用类路径:data/file.csv

除了csv文件外,还有tsv/ssv/jsonFile/jsonUrl/jdbc/redis几种方式导入数据,具体见参考资料Session->Feeders章节。

Checks

Checks可以用来验证请求结果,并且可以提取返回结果信息以便复用。

Checks一般通过在父对象上调用check方法来实现,如下所示是一个http请求的checks:

http("Gatling").get("https://gatling.io")
  .check(status().is(200))

当然,也可以一次定义多个checks:

http("Gatling").get("https://gatling.io")
  .check(
    status().not(404),
    status().not(500)
  )

check API提供了一个专用DSL,可以链接以下多个操作:

  • 定义check类型
  • 提取
  • 转换
  • 验证
  • 命名
  • 保存

常规检查类型

下面是一些常规的检查类型,并被大多数gatling支持的官方协议所实现。

responseTimeInMillis

.check(responseTimeInMillis().lte(100))   // 响应时间不大于100ms

bodyString

.check(
  bodyString().is("{\"foo\": \"bar\"}"),
  bodyString().is(ElFileBody("expected-template.json"))
)

bodyBytes

.check(
  bodyBytes().is("{\"foo\": \"bar\"}".getBytes(StandardCharsets.UTF_8)),
  bodyBytes().is(RawFileBody("expected.json"))
)

bodyLength

.check(bodyLength().is(1024))

bodyStream

返回完整响应体数据字节的输入流。某些情况下,当需要对返回数据在数据处理之前进行格式转换时使用。

.check(bodyStream().transform(is -> {
  // 将Base64格式转换成String
  try (InputStream base64Is = Base64.getDecoder().wrap(is)) {
      return org.apache.commons.io.IOUtils.toString(base64Is, StandardCharsets.UTF_8.name());
  } catch (IOException e) {
      throw new RuntimeException("Impossible to decode Base64 stream");
  }
}))

subString

该检查返回指定子字符串在响应文本中出现的索引位置。

通常用于检查子字符串是否存在,它比正则表达式的CPU效率更高。

.check(
  // with a static value
  // (identical to substring("expected").find().exists())
  substring("expected"),
  // with a Gatling EL
  substring("#{expectedKey}"),
  // with a function
  substring(session -> "expectedValue"),
  substring("Error:").notExists(),
  // this will save a List<Int>
  substring("foo").findAll().saveAs("indices"),
  // this will save the number of occurrences of foo
  substring("foo").count().saveAs("counts")
)

regex

.check(
  // with a static value without capture groups
  regex("<td class=\"number\">"),
  // with a Gatling EL without capture groups
  regex("<td class=\"number\">ACC#{account_id}</td>"),
  // with a static value with one single capture group
  regex("/private/bank/account/(ACC[0-9]*)/operations.html")
)

在Java15+,Scala和Kotlin中,你可以使用这种转移字符串:“”“my “non-escaped” string”“”,而无需用’’

XPath

该检查对XML响应体生效

.check(
  // simple expression for a document that doesn't use namespaces
  xpath("//input[@id='text1']/@value"),
  // mandatory namespaces parameter for a document that uses namespaces
  xpath("//foo:input[@id='text1']/@value", Collections.singletonMap("foo", "http://foo.com"))
)

更多Checks具体见参考资料Checks章节。

提取

提取操作可以让你过滤出期望的结果,然后就可以在后续的步骤中对结果进行处理了。

如果未显式定义提取操作,Gatling会默认执行find

find

find可以过滤出单个元素。如果目标多次出现,find等同于find(0)

.check(
  // 下面两个是等效的。因为jjmesPath只返回一个值,所以find可以省略
  jmesPath("foo"),
  jmesPath("foo").find(),
  
  // jsonPath可能返回多个值
  // 下面三个是等效的,所以find可以省略
  jsonPath("$.foo"),
  jsonPath("$.foo").find(),
  jsonPath("$.foo").find(0),
  
  // 捕获第二次出现的元素
  jsonPath("$.foo").find(1)
)

findAll

返回值有多个时生效

.check(
  jsonPath("$.foo").findAll()
)

findRandom

.check(
  // identical to findRandom(1, false)
  jsonPath("$.foo").findRandom(),
  // identical to findRandom(1, false)
  jsonPath("$.foo").findRandom(1),
  // identical to findRandom(3, false)
  // best effort to pick 3 entries, less if not enough
  jsonPath("$.foo").findRandom(3),
  // fail if less than 3 overall captured values
  jsonPath("$.foo").findRandom(3, true)
)

count

.check(
  jsonPath("$.foo").count()
)

转换

转换是一个可选步骤。在上面的提取步骤之后,我们得到了相应的结果,在对结果进行匹配或者保存之前,你可能希望对结果进行格式转换,此时就要用到转换了。

withDefault

如果在上一步(提取)中没有获取到值,那么可以通过withDefault设置默认值。

.check(
  jsonPath("$.foo")  // 省略了find()
    .withDefault("defaultValue")
)

transform

transform的参数是一个函数,该函数用于对提取到的值进行转换,要求上一步结果不能为空。

.check(
  jsonPath("$.foo")
    // append "bar" to the value captured in the previous step
    .transform(string -> string + "bar")
)

transformWithSession

此步骤实际上是transform的一个变种,可以访问Session

.check(
  jsonPath("$.foo")
    // append the value of the "bar" attribute
    // to the value captured in the previous step
    .transformWithSession((string, session) -> string + session.getString("bar"))
)

transformOption

transfrom相反的是,该操作即使在上一步没有获取到结果也能执行。

.check(
  jmesPath("foo")
    // extract can be null
    .transform(extract -> Optional.of(extract).orElse("default"))
)

当然,如果你的目的仅仅是设置一个默认值,那直接使用withDefault可能更方便。

transformOptionWithSession

.check(
  jmesPath("foo")
    // extract can be null
    .transformWithSession((extract, session) ->
      Optional.of(extract).orElse(session.getString("default"))
    )
)

验证

同提取一样,如果没有显式指定,gatling会默认执行exists

isnot

// is
.check(
  // with a static value
  jmesPath("foo").is("expected"),
  // with a Gatling EL String (BEWARE DIFFERENT METHOD)
  jmesPath("foo").isEL("#{expected}"),
  // with a function
  jmesPath("foo").is(session -> session.getString("expected"))
)

// not
.check(
  // with a static value
  jmesPath("foo").not("unexpected"),
  // with a Gatling EL String (BEWARE DIFFERENT METHOD)
  jmesPath("foo").notEL("#{unexpected}"),
  // with a function
  jmesPath("foo").not(session -> session.getString("unexpected"))
)

isNullnotNull

// isNull
.check(
  jmesPath("foo")
    .isNull()
)

// notNull
.check(
  jmesPath("foo").notNull()
)

existsnotExists

.check(
  jmesPath("foo").exists()
)

// not exists
.check(
  jmesPath("foo").notExists()
)

in

.check(
  // with a static values varargs
  jmesPath("foo").in("value1", "value2"),
  // with a static values List
  jmesPath("foo").in(Arrays.asList("value1", "value2")),
  // with a Gatling EL String that points to a List in Session (BEWARE DIFFERENT METHOD)
  jmesPath("foo").inEL("#{expectedValues}"),
  // with a function
  jmesPath("foo").in(session -> Arrays.asList("value1", "value2"))
)

validate

.check(
  jmesPath("foo")
    .validate(
      "MyCustomValidator",
      (actual, session) -> {
        String prefix = session.getString("prefix");
        if (actual == null) {
          throw new NullPointerException("Value is missing");
        } else if (!actual.startsWith(prefix)) {
          throw new IllegalArgumentException("Value " + actual + " should start with " + prefix);
        }
        return actual;
      })
)

命名

命名主要是为了防止万一出现错误,就可以在错误消息里显示check名称了。

.check(
  jmesPath("foo").name("My custom error message")
)

保存

保存也是一个可选操作,用于将前一步提取或转换的结果保存到虚拟用户的Session中,以便后续复用。

saveAs

.check(
  jmesPath("foo").saveAs("key")
)

条件检查

checkIf

// with a Gatling EL String condition that resolves a Boolean
.checkIf("#{bool}").then(
  jmesPath("foo")
)
// with a function
.checkIf(session -> session.getString("key").equals("executeCheck")).then(
  jmesPath("foo")
)

完整Check

以上的所有步骤: 确定检查类型,提取,转换,验证,保存,都是Check的工作流的一部分,通常会将一些步骤结合起来使用。

下面是一些例子:

.check(
  // check the HTTP status is 200
  status().is(200),

  // check the HTTP status is in [200, 210]
  status().in(200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210),

  // check the response body contains 5 https links
  regex("https://(.*)").count().is(5),

  // check the response body contains 2 https links,
  // the first one to www.google.com and the second one to gatling.io
  regex("https://(.*)/.*").findAll().is(Arrays.asList("www.google.com", "gatling.io")),

  // check the response body contains a second occurrence of "someString"
  substring("someString").find(1).exists(),

  // check the response body does not contain "someString"
  substring("someString").notExists()
)

HTTP

HTTP是Gatling协议的主要目标,因此这是我们的主要关注点。

Gatling允许你对web应用,服务和网站进行负载测试。它几乎支持HTTP和HTTPS的全部特性,包括缓存、cookies和转发等。

下面是一个最基本的http负载测试的例子:

HttpProtocolBuilder httpProtocol = http.baseUrl("https://gatling.io");

ScenarioBuilder scn = scenario("Scenario"); // etc...

{
	setUp(scn.injectOpen(atOnceUsers(1)).protocols(httpProtocol));
}

HTTP引擎

warmUp

Java/NIO引擎在启动并执行第一个请求的时候会有额外的开销,为了抵消此影响,Gatling会先自动向https://gatling.io.发送一个请求来预热。当然,你可以更改预热地址,或者禁用预热。

// 更改warmUp地址
http.warmUp("https://www.google.com);
// 禁用预热
http.disableWarmUp();

maxConnectionsPerHost

为了模拟真实的浏览器,gatling可以同时为每个虚拟用户建立多个到同一主机上的连接。默认情况下,针对同一虚拟用户到同一个远程主机上的并发连接数,gatling将其限制为6。你可以通过maxConnectionsPerHost来修改它。

http.maxConnectionsPerHost(10);

shareConnections

默认情况下,每个虚拟用户都有自己的连接池和SSLContext。这其实是模拟web浏览器访问服务器的场景,每个虚拟用户就是一个浏览器。

而如果你想模拟服务器到服务器的场景,在这种情况下,每个客户端(请求发起方)都有一个长期存在的连接池,可能让虚拟用户共享一个全局的连接池更合适。

http.shareConnections();

enableHttp2

可以通过enableHttp2设置来开启HTTP2协议支持。

http.enableHttp2();

注意,要开启HTTP2功能,要么使用JDK9以上的版本,要么确保gatling配置里gatling.http.ahc.useOpenSsl不是false

实践比较

Gatling出现得比较晚,用到的技术更新,理论上性能会更好。但在实际压测过程中发现,Gatling压测出来的结果是不如Jmeter和Apache benchmark的,以访问AWS S3资源为例,1000个并发用户访问10个S3资源,Jmeter测出来的RPS达到了接近2000,而Gatling只有几百。访问同一个资源,Gatling测出来的结果也比Jmeter和AB小。

RPS并不是TPS,RPS是服务端的吞吐量,TPS是"并发/响应时间",在Gatling中无法直观看到TPS,Jmeter可以下载插件来显示,其结果是一个曲线图。

经过一些调整后,Gatling的RPS能够上来不少。主要是两个点:一个是scenario,原来每个scenario只执行一个请求,这不符合实际的用户行为。实际场景中,用户的行为是连贯的,是存在一定的生命周期的。如果每次执行完一个请求就结束,系统就要新创建一个用户来补充并发用户数量,会消耗不少资源。另一个点就是连接复用,这个在上文中介绍过,根据实际需要来选择是否开启。

经过调整后,两者的测试结果差不多。下面对官方测试网站进行压测,并发用户是500,时间是30s(超过30s就可能触发网站的qps限制)。Gatling脚本如下:

class PerformanceSimulation extends BaseSimulation {

  private val headers = Map("Content-Type" -> "application/json")

  val httpConf: HttpProtocolBuilder = http
    .baseUrl("https://computer-database.gatling.io")
    .headers(headers)
    .acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
    .shareConnections()
    .doNotTrackHeader("1")
    .acceptLanguageHeader("en-US,en;q=0.5")
    .acceptEncodingHeader("gzip, deflate")
    .userAgentHeader("Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0")
    .disableCaching

  val scn: ScenarioBuilder = scenario("Get Request")
    .exec(http("Request 1").get("/computers").check(status is 200)).pause(1)
    .exec(http("Request 2").get("/computers/6").check(status is 200))

  {
    //注入用户,刚开始就500个
    setUp(scn.inject(constantConcurrentUsers(500).during(30)).protocols(httpConf))
  }
}

结果如下:
在这里插入图片描述

Jmeter也设置相同的条件,结果如下:
在这里插入图片描述

参考资料

[1]. https://gatling.io/docs/gatling/tutorials/quickstart/
[2]. https://gatling.io/docs/gatling/reference/current/extensions/maven_plugin/

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值