其实在压测的过程中我们主要也是压测 http 请求,所以在示例和后续的使用我们将以 http 请求为主
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class BaiduSimulation extends Simulation {
/*
*设置请求的方式和路径并命名此请求名称,建议请求名称全局唯一
*/
object HttpDemo {
val demo = exec(http("Request").get("/"))
}
/*
设置请求属于归属方案
*/
val scn = scenario("scenarioRequest").exec(HttpDemo.demo)
//设置请求的的域名和一些公用的请求头信息
val http = http
.baseURL("https://www.baidu.com")
.acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
.acceptEncodingHeader("gzip, deflate")
.acceptLanguageHeader("zh-CN,en-US;q=0.7,en;q=0.3")
.userAgentHeader("Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0")
//设置请求场景
setUp(scn.inject(atOnceUsers(1))).protocols(httpProtocol)
}
上述代码只是简单的描述了一个压测场景,其实在我们使用的过程中会有更复杂的情况,总体来说可以将上述定义步骤分位四个模块
通过这四部分我们就可以完成对 Gatling 的脚本设置,下面我来详细说下这四部分的配置
1.请求体的构建
对于 HTTP/HTTPS 请求 gatling 支持主流的请求类型GET
/POST
/PUT
/PATCH
/HEAD
/DELETE
/OPTIONS
比如示例代码中我们以 GET 请求去访问百度首页,所以我们构建请求体为下方这样
object HttpDemo {
val demo = exec(http("Request").get("/"))
}
exec(...)
里的参数就是我们的执行动作,http(...)
http 中内容代表着这个请求的命名,可以是中文或者英文但是需要全局唯一,get(...)
get 代表着这个请求的方式,可以为 post(...)、put(...) 等等
但是在日常中我们使用可能不简单的是发送一个 url 过去可能会有多种情况发生,所以 gatling 还支持许多参数化的传参
object Demo {
val demo = exec(
http("Post")
.post("/computers") // 发送post请求
.queryParam("key","value") // 增加query参数
.queryParamMap(Map("key"->"value")) // 以Map的形式增加query参数
.formParam("key", "value") // form表单格式发送参数
.formParamMap(Map("key"->"value")) // 以Map的形式增加form表单格式参数
.body(StringBody("""{"key":"value"}""")).asJson // 请求的body内容格式为json
.multivaluedFormParam("key","value") // 发送multivalue格式的from表单
)
}
val demotwo = exec(
http("get")
.httpRequest("get","/") // 发送get请求根目录
)
2.方案配置
方案配置是决定上述定义的请求提如何在何场景实现,也可已经请求体设置在不同的方案中用于不同的请求场景
val scn = scenario("scenarioRequest").exec(Demo.demo) //将上述定义的Demo.demo中方法在命名为“scenarioRequest”的方案中执行
/*
val scn = scenario("scenarioRequest").exec(Demo.demo,Demo.demotwo)
或
val scn = scenario("scenarioRequest").exec(Demo.demo)
val scn1 = scenario("scenarioRequestTwo").exec(Demo.demotwo)
*/
除了上述的定义方案外,gatling 还有支持其他定义方法例如repeat/during
等等,当我们压测部分请求时里面有很多参数是循环迭代可以得到的,例如我们请求某个分页接口,里面需要对每页的数据遍历,如果在造数据时我们使用同样的数据只是页码变化的话这样我们重复的工作就太多了。所以 gatling 给出了循环的方法
val browse = repeat(5, "n") { // 1
exec(http("Page ${n}")
.get("/computers?p=${n}")) // 2
.pause(1)
}
或者我们将在一段时间内循环某个页面
during(duration, counterName, exitASAP) {
myChain
}
等等的方法
3.请求公共配置
val http = http
.baseURL("https://www.baidu.com") // 请求域名
.acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") // 公共请求头
.acceptEncodingHeader("gzip, deflate") // 公共请求头
.acceptLanguageHeader("zh-CN,en-US;q=0.7,en;q=0.3") // 公共请求头
.userAgentHeader("Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0") // 公共请求头
除了上述的定义的元素在 HTTP PROTOCOL 中还可以定义代理服务器,自动预热等等功能
自动预热
Java/NIO 引擎启动会在要执行的第一个请求上产生开销。为了补偿这种影响,Gatling 自动执行对https://gatling.io
的请求。
要禁用此功能,只需添加.disableWarmUp
到 HTTP 协议配置定义。要更改预热网址,只需添加.warmUp("newUrl")
// override warm up URL to http://www.google.com
val httpProtocol = http.warmUp("http://www.google.com")
// disable warm up
val httpProtocolNoWarmUp = http.disableWarmUp
虚拟主机
当我们压测过程中某个请求并不想访问这个集群,而是正对某个 ip 地址进行访问时,我们又不想每次重写本地 host,使用 gatling 时可以通过使用虚拟主机的方式,使域名和 ip 绑定访问特定主机的域名
val httpProtocol1 = http
.baseUrl("http://127.0.0.1")
.virtualHost("www.baidu.com")
又或者我们相对多台服务器的同一个域名进行压测时我们可以按以下的写法来写
val httpProtocol1 = http
.baseUrls("127.0.0.1","127.0.0.2")
.virtualHost("www.baidu.com")
4.请求模拟场景设置
在上篇文章中我们讲述了场景设置中的开放模型和封闭模型,所以在这里对这些不在做过多解释。在一般使用过程中我比较喜欢使用开放模型,这种我可以看到更多的因为连接数导致的问题
scn.inject(
nothingFor(4 seconds), // 1
atOnceUsers(10), // 2
rampUsers(10) during (5 seconds), // 3
constantUsersPerSec(20) during (15 seconds), // 4
constantUsersPerSec(20) during (15 seconds) randomized, // 5
rampUsersPerSec(10) to 20 during (10 minutes), // 6
rampUsersPerSec(10) to 20 during (10 minutes) randomized, // 7
heavisideUsers(1000) during (20 seconds) // 8
).protocols(httpProtocol)
)
在我们日常的测试中会发现其实有时候,请求接口时我们不光是需要请求参数有时候我们还需要验签信息,如果我们关闭验签功能时虽然可以继续后续的操作,但是这等于我们放弃了一部分的真实场景来压测,这与我们预想的过程不相同,所以 gatling 还提供了验签的功能
exec(
http("Request")
.get("/foo/bar?baz=qix")
.sign(new SignatureCalculator {
override def sign(request: Request): Unit = {
import java.util.Base64
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
val mac = Mac.getInstance("HmacSHA256")
mac.init(new SecretKeySpec("THE_SECRET_KEY".getBytes("UTF-8"), "HmacSHA256"))
val rawSignature = mac.doFinal(request.getUri.getQuery.getBytes("UTF-8"))
val authorization = Base64.getEncoder.encodeToString(rawSignature)
request.getHeaders.add("Authorization", authorization)
}
})
)
从官方是示例来看我们可以再 sign 方法中通过重写 SignatureCalculator 类中的 sign 方法来使先自主验签的过程,这里有一个有趣的事情,就是我在 gatling 的早期版本,可能在 gatling3.0 左右的版本通过request.getUri.getEncodedQueryParams.add
方法可以再 query 中重写 uri 实现在 query 中增加参数,但是随着 gatling 更新我在以前的版本和现在的版本发现并不能重写 uri 了,所以我请教了 gatling 的作者,从他的回答中我们可以看出 gatling 不支持重写,随后我还不死心的去阅读了源码,发现不管是哪个版本都没有重写 uri,如果各位发现可以重写 uri 的方法请告知我
在我们使用时,如果我们对某一条链路进行压测时,对于用户来说不可能是 100% 转化的,每一个层级用户会减少,例如当有 100 个用户进入我们的首页,发现需要登录才能访问这是只有 80 个用户选择了登录,然后这启动有 50% 的用户选择去查看我们推送的广告,只有 10% 的用户在我们广告页面中下单,所以我们需要定义不同的模型来模拟用户真实的场景,所以我们可以使用 gatling 的条件语句来进行判断
doIf
gatling 的 DSL 具有条件执行支持。如果仅在满足某些条件时才想执行特定的请求链,则可以使用 doIf 方法执行操作。
doIf("${myBoolean}") {
// 如果“myBoolean”中存储的session中的值为true,则执行下面的请求体
exec(http("...").get("..."))
}
如果要测试复杂的条件,则必须通过 Expression[Boolean]:
doIf(session => session("myKey").as[String].startsWith("admin")) {
// 如果存储在session中的key“myKey”中的值以“admin”开头,则执行下面的请求体
exec(http("if true").get("..."))
}
doIfEquals
如果您的测试条件只是比较两个值,则可以简单地使用 doIfEquals:
doIfEquals("${actualValue}", "expectedValue") {
// 如果session中的key“actualValue”中的值等于“expectedValue”,则执行下面的请求体
exec(http("...").get("..."))
}
doIfOrElse
与相似 doIf,但是如果条件的计算结果为 false,则执行第二条请求链
doIfOrElse(session => session("myKey").as[String].startsWith("admin")) {
// 如果session中的key为“myKey”中值以“admin”开头,则执行下面的请求体
exec(http("if true").get("..."))
} {
// 否则执行此部分
exec(http("if false").get("..."))
}
doIfEqualsOrElse
与 doIfEquals 条件类似,但条件为 false 时会执行第二条请求链
doIfEqualsOrElse(session => session("actualValue").as[String], "expectedValue") {
// 如果session中的key为“actualValue”中值等于“expectedValue”,则执行
exec(http("if true").get("..."))
} {
// 否则执行此部分
exec(http("if false").get("..."))
}
doSwitch
与 java 请求中的 switch 相同更具 key 的不同值,执行不同的请求体
doSwitch("${myKey}")( // 注意:这里使用的是小括号不是大括号 与其他不相同
key1 -> chain1,
key1 -> chain2
)
doSwitchOrElse
与相似 doSwitch,但如果任何 case 中时,则执行备用部分的请求体
doSwitchOrElse("${myKey}")( // 这里也是小括号
key1 -> chain1,
key1 -> chain2
)(
myFallbackChain
)
randomSwitch
设置的概率值必须小于 100%,命中概率不相等
randomSwitch( // 这里也是小括号
percentage1 -> chain1,
percentage2 -> chain2
)
randomSwitchOrElse
与 randomSwitch 相似,但如果未选择任何 case,则执行备用请求体(即:随机数超过百分比总和)
randomSwitchOrElse( // 这里也是小括号
percentage1 -> chain1,
percentage2 -> chain2
) {
myFallbackChain
}
uniformRandomSwitch
与 randomSwitch 相似,命中概率
uniformRandomSwitch( // 这里还是小括号
chain1,
chain2
)
roundRobinSwitch
与 randomSwitch 相似,但是是循环执行
roundRobinSwitch( // 不好意思,这里也是小括号
chain1,
chain2
)
在性能测试的过程中我们有时候获取到外部的状态码可能只是知道服务端对请求完成了,但是我们并不知道,请求内部的返回是否符合我们的需求,所以 gatling 给我们提供了 checks 的方法,check 在 gatling 中的作用是:
java
http("demo").get("/").check(status.is(200))
.check(status.not(404), status.not(500)) //进行多项检查
又或者我们需要获取某个请求内返回的参数是否正确
java
.check(jsonPath("$.status").is("200")) //如果返回请求是json可以通过jsonpath来定位到指定元素
.check(regex("""<td class="number">ACC${account_id}</td>""").notNull) // 通过正则去获取判断元素
如果我们在上下流请求有强依赖问题,下流解决强依赖上流的某个返回值可以通过 check 将值获取并保存到 session 中供下流接口使用
java
substring("foo") // 与 substring("foo").find.exists 相同
substring("foo").findAll.saveAs("indices") // 找到所有的foo并保存至key indices中
substring("foo").count.saveAs("counts") // 获取数量并保存
在下流使用此参数时只需按动态变量使用就行${mykey}
# 5.Feeder
Gatling 由于 DSL 会预编译,在整个执行过程中是静态的,因此有的方法在运行过程中就已经静态化了,不会再执行,所以 gatling 提供了 Feeder 方法,Feeder 是 gatling 用于实现注入动态参数或变量的,Gatling 的 Feeder 支持多种格式注入数据分别包含csv/tsv/ssv/jsonFile/jsonUrl/jdbc/redis
几种方式导入数据
基于 Iterator[Map[String, T]]
我们在使用过程中一些参数不需要使用文件导入,例如自增数据或者随机数据这时我们可以使用 Iterator[Map[String, T]] 方法来实现数据的生成,减少我们投入的成本
java
import scala.util.Random
val feeder = Iterator.continually(Map("email" -> (Random.alphanumeric.take(20).mkString + "@foo.com")))
如果我们要使用我们定义的feeder
需要在使用的地方将其引入
java
feed(feeder)
这样就定义了一个注入的步骤,其中每个虚拟用户都在同一个 Feeder 上进行 Feed
每次虚拟用户到达此步骤时,它将从 Feeder 中获取一条数据,该数据将注入到用户的 Session 中,从而产生一个新的 Session 实例以供后续使用,但是如果 Feeder 无法产生足够的数据,Gatling 将报错并且脚本将停止文件导入
Gatling 提供了各种支持多种文件导入的方法,但是在使用时,文件必须位于 user-files/resources 目录中,当使用诸如 maven 之类的构建工具时,必须将文件放置在 src/main/resources 或中 src/test/resources,或者在 gatling 的 config 文件中配置配置路径,gatling 将会从根目录下去寻找配置的路径,在使用过程中我配置的为绝对路径,gatling 也可以找到对应的数据,gatling 提供了以下的方法用于数据的读取
.queue // 默认行为,按顺序读取数据,当数据读取完后停止脚本,需要确保数据的量足够多
.random // 随机从数据中读取
.shuffle // 重洗数据排序,然后和queue一样读取
.circular // 和queue一样读取,但是当数据读取完成后会返回头部重新读取一遍
csv/tsv/ssv
Gatling 提供了几个内置函数来读取以字符分隔的值文件,其解析器遵循 RFC4180 规范,或根据文件首行列值作为 session 的 key 使用
val csvFeeder = csv("foo.csv") // 使用逗号分隔
val tsvFeeder = tsv("foo.tsv") // 使用制表符分隔
val ssvFeeder = ssv("foo.ssv") // 使用分号分隔符
val customSeparatorFeeder = separatedValues("foo.txt", '#') // 使用自定义分隔符
gatling 读取文件的方法是将数据加载到内存中,并且提供了几种加载的选项用于自定义调节
eager
eager
在模拟开始之前,将整个数据加载到内存中,从而在运行时节省磁盘访问。此模式最适合处理较小的文件,这些文件可以快速解析而不会延迟模拟开始时间,并且可以轻松地放在内存中。当文件过大时或者内存不够加载文件时,这个模式加载文件可能会出现脚本崩溃等行为
val csvFeeder = csv("foo.csv").eager.random
batch
batch
在大文件读取时是按块开始的,所以在大文件处理的方面这个方法会更加好,当 batch 模式,random 并 shuffle 不能当然的全数据进行操作,只能在记录的内部缓冲区进行操作。该缓冲区的默认大小为 2000,可以进行更改。
val csvFeeder = csv("foo.csv").batch.random
val csvFeeder2 = csv("foo.csv").batch(200).random // 这里将每次读取数据缩减为200条
压缩文件
如果文件很大,则可以使用 gatling 提供的压缩文件处理的方法unzip
val csvFeeder = csv("foo.csv.zip").unzip
JSON 文件
如果你希望读取的数据是 json 文件而不是 csv 文件则可以使用 gatling 的 json 读取方法jsonFile
val jsonFileFeeder = jsonFile("foo.json")
val jsonUrlFeeder = jsonUrl("http://me.com/foo.json")
/*
*文件类型
*/
{
"id":19434,
"foo":1
},
{
"id":19435,
"foo":2
}
]
JDBC
当我们使用的数据希望是从数据库拉取而不是使用本地的数据可以使用jdbcFeeder
此方法,但是不建议使用这种方法,因为可能会因为网络 io 等元素导致脚本压力无法上去
import io.gatling.jdbc.Predef._
//注意这里要导入jdbc模块
jdbcFeeder("databaseUrl", "username", "password", "SELECT * FROM users")
Redis
import io.gatling.redis.Predef._
val redisPool = new RedisClientPool("localhost", 6379)
// 存为一个列表使用,根据"foo"这个key获取数据
val feeder = redisFeeder(redisPool, "foo")
然后,您可以覆盖所需的 Redis 命令:
// 使用SPOP命令从名为“foo”的集合读取数据
val feeder = redisFeeder(redisPool, "foo").SPOP
// 使用SRANDMEMBER命令从名为“foo”的集合读取数据
val feeder = redisFeeder(redisPool, "foo").SRANDMEMBER