赞
踩
Gatling是一个基于AKKA和Netty开发的高性能压测工具,使用非常简单。
Gatling可以直接下载使用,也可以通过maven插件在项目中集成,通过命令行执行。
直接使用参见官方网站(见参考资料1)。这里介绍通过maven插件来使用。
首先引入依赖包,这个是测试脚本需要的:
<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>
这个插件中skipLTandPTs
和testTarget
参数是从父文件中继承过来的。父文件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等。
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)
);
场景名可以是除了\t
之外的任何字符:
ScenarioBuilder scn = scenario();
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。主要有三个参数:
// 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);
使用injectOpen和injectClosed方法来定义用户的注入配置信息(与Scala中的inject作用相同),该方法参数为一系列的注入步骤,处理时也按顺序处理。
Open和Closed工作负载模型
当谈到负载模型时,通常有两种类型的系统:
封闭系统中并发用户数是有上限的,当并发数达到上限时,只有当有用户退出时,新的用户才能进入系统。
这与线程池工作模式是类似的,当工作线程占用满了的情况下,新的请求进入任务队列,等待有线程空闲下来才能继续处理。
售票业务系统一般需要采用封闭模型。
封闭模型适用于可以异步获取结果的系统
而开放系统与之相反,在开放系统中无法控制并发用户数量,即使业务系统已经不能处理多余的请求了,新的用户还是会持续不断地到来并发起请求。
大部分业务系统均是这种情况。
注意:请根据系统业务类型来决定采用哪一种测试模型。如果实际业务类型与测试的模型不匹配,就无法达到预期效果。
开放模型与封闭模型具有相反的含义,不要在同一个注入配置中混用。
下面是一个开放模型的例子(其他语言见参考资料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) // 在指定时间段内,虚拟用户数从一个值线性增长到另一个值
)
);
在测试之前,通常我们并不知道系统吞吐量是多少。为了测试瓶颈值,可能会用不同的数值去做重复的操作来尝试,例如:
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
用于封闭模型负载测试。separatedByRampsLasting
和startingFrom
都是可选的。
如果未指定坡度,则虚拟用户增长方式是跳跃的。如果未指定起始用户数,将从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 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))
,这样只有第一次请求是随机值,后面的请求都使用第一次生成的值。
在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一般通过在父对象上调用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,可以链接以下多个操作:
下面是一些常规的检查类型,并被大多数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
。
is和not
// 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")) )
isNull和notNull
// isNull
.check(
jmesPath("foo")
.isNull()
)
// notNull
.check(
jmesPath("foo").notNull()
)
exists和notExists
.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 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是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));
}
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/
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。