Gatling模拟真实用户行为是抛弃固定脚本,引入随机性、动态数据和非固定节奏。这主要通过 Random、Feeder 和循环控制来实现。
Random注入不确定性
Gatling的Random对象是模拟用户操作随机性。
随机等待时间
真实用户在操作间会有停顿,Gatling通过pause结合Random来模拟。
scala
import scala.concurrent.duration._
// 1. 固定范围均匀分布:在2到5秒之间随机暂停(单位默认秒)
pause(2, 5)
// 2. 更专业的指定分布:使用Random对象生成
pause(Random.nextInt(5).seconds) // 0-4秒随机整数秒
pause(Random.nextDouble(5).seconds) // 0.0-5.0秒随机浮点数秒
pause(Random.nextGaussian(10, 2).seconds) // 均值10秒,标准差2秒的正态分布
建议:对于浏览列表页等情形,使用 uniform;对于“思考时间”这种集中在一定范围内的,可使用 normal;exponential 可用于模拟某些等待事件。
随机选择
模拟用户非固定的操作途径。
scala
import scala.util.Random
// 1. 随机选择一项:从序列中随机选一个
val randomSearchKeyword = Random.shuffle(List("手机", "电脑", "平板", "耳机")).head
exec(http("Search_${keyword}")
.get("/search")
.queryParam("q", randomSearchKeyword))
// 2. 按权重随机选择(随机决定树):模拟30%的用户点击详情,70%继续浏览
randomSwitch(
30.0 -> exec(http("View_Detail").get("/detail/${productId}")),
70.0 -> exec(http("Continue_Browsing").get("/list?page=${nextPage}"))
)
Feeder数据驱动和用户身份多样化
Feeder是为每个虚拟用户提供独特、真实测试数据的机制。
Feeder类型和选择方法
CSV Feeder:csv("users.csv").circular,从CSV读取,circular(循环用)、random(随机用)、queue(用完失败)。最常用。
JSON Feeder:jsonFile("data.json").random,读取JSON,适合嵌套数据结构。
JDBC Feeder:jdbcFeeder(...),从数据库直接读取,数据最新但可能给数据库加压。
Array/Map Feeder:Array(Map("foo"->"bar")).random,直接在代码中定义小型数据集,灵活。
内联JSON:jsonUrl("""[{"id":1}]""").random,测试脚本内嵌数据,无需外部文件。
用法和性能优化
scala
// 1. 数据转换和清洗:在Feeder链中即时处理数据
val cleanUserFeeder = csv("users.csv")
.transform { case (key, value) =>
if (key == "email") (key, value.toLowerCase) else (key, value)
}
.circular
// 2. 组合Feeder:合并用户身份数据和业务数据
val combinedFeeder = csv("users.csv").random
.and(jsonFile("products.json").random)
// 3. 【重点优化】共享Feeder和解耦:在Simulation顶层定义,避免重复读取
object Feeders {
val sharedProductFeeder = csv("products.csv").circular
}
class MySimulation extends Simulation {
val scn = scenario("Scenario")
.feed(Feeders.sharedProductFeeder) // 所有情形共享同一数据源
.exec(...)
}
循环创建动态用户会话流
循环控制操作序列的重复执行方式,是模拟用户不断交互的重点。
1. 基础循环
scala
// 1. 固定次数循环:重复执行5次搜索操作
repeat(5) {
exec(http("Search").get("/search?q=test"))
.pause(1)
}
// 2. 使用动态变量控制循环:每个用户的循环次数不同(从Session中取)
repeat("${desiredLoopCount}") { // 从Feeder或前置操作中获取变量
exec(...)
}
2. 高级循环和退出条件
scala
// 1. 条件循环(while):模拟用户“刷到满意为止”的行为
asLongAs(session => session("hasFoundTarget").as[Boolean] == false) {
exec(http("Next_Page").get("/list?page=${nextPage}"))
.pause(2)
// 需要在此循环内部的某个操作中,可能将 hasFoundTarget 设置为 true
.exec(session => session.set("hasFoundTarget", Random.nextBoolean())) // 示例:随机决定是不是找到
}
// 2. 时间条件循环(during):模拟用户在固定时间段内的不断活动(如1分钟内不断操作)
during(1 minute) { // 重点:during块内的执行总时间会被控制
exec(http("Do_Something").get("/api"))
.pause(Random.nextInt(5).seconds) // 每次操作后随机等待
// 注意:整个during块的时长 = 内部所有exec和pause时间的总和,直到达到1分钟
}
// 3. 永远循环(forever):模拟长时间在线的用户,一般配合 exitHereIfFailed 使用
forever {
exec(http("Polling").get("/notifications"))
.pause(5 seconds)
.exitHereIfFailed // 如果轮询失败,则此虚拟用户退出
}
实战模拟电商用户行为
一个融合了上述所有概念的完整示例,模拟一个电商用户从登录到下单的真实、非固定行为。
scala
import scala.concurrent.duration._
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.util.Random
class AdvancedEcommerceSimulation extends Simulation {
// --- 1. 定义Feeder ---
// 用户账户数据
val userFeeder = csv("data/users.csv").circular
// 商品数据,随机使用
val productFeeder = csv("data/products.csv").random
// 搜索重点词,随机使用
val keywordFeeder = Array(
Map("keyword" -> "iPhone"),
Map("keyword" -> "MacBook"),
Map("keyword" -> "AirPods")
).random
val httpProtocol = http.baseUrl("https://api.zmtests.com")
// --- 2. 定义情形行为链 ---
val scn = scenario("Realistic E-commerce User")
// A. 用户登录:每个虚拟用户从Feeder获取唯一身份
.feed(userFeeder)
.exec(
http("Login")
.post("/login")
.formParam("username", "${username}")
.formParam("password", "${password}")
.check(status.is(200))
.check(jsonPath("$.token").saveAs("authToken")) // 保存token供后续使用
)
.pause(Random.nextInt(3).seconds) // 登录后随机短暂停留
// B. 浏览行为:随机循环浏览多个页面
.repeat(Random.nextInt(3) + 1) { // 浏览1-3个页面
feed(keywordFeeder)
.exec(
http("Search Product")
.get("/search")
.queryParam("q", "${keyword}")
.header("Authorization", "Bearer ${authToken}")
.check(jsonPath("$.products[0].id").optional.saveAs("firstProductId"))
)
.pause(Random.nextDouble(2, 5).seconds) // 浏览搜索结果随机时间
// 随机决定:是不是查看第一个商品详情?
.randomSwitch(
40.0 -> exec( // 40%的概率查看详情
http("View Product Detail")
.get("/product/${firstProductId}")
.header("Authorization", "Bearer ${authToken}")
.pause(Random.nextInt(4).seconds)
),
60.0 -> exec { session => session } // 60%的概率跳过,什么都不做
)
}
// C. 模拟加购到下单:使用条件循环,模拟可能放弃的行为
.feed(productFeeder)
.exec(
http("Add to Cart")
.post("/cart")
.header("Authorization", "Bearer ${authToken}")
.body(StringBody("""{"productId": "${productId}", "quantity": 1}"""))
.asJson
)
.pause(Random.nextGaussian(10, 3).seconds) // “犹豫时间”,正态分布
// 条件循环:模拟用户可能在结算前反复修改购物车
.asLongAs(session => session("abandonCheckout").as[Boolean] == false) {
exec(
http("Checkout Preview")
.get("/checkout/preview")
.header("Authorization", "Bearer ${authToken}")
)
.pause(2 seconds)
.randomSwitch(
80.0 -> exec( // 80%的概率继续下单
http("Confirm Order")
.post("/order")
.header("Authorization", "Bearer ${authToken}")
.check(status.is(201))
.exec(session => session.set("abandonCheckout", true)) // 订单创建成功,退出循环
),
20.0 -> exec( // 20%的概率放弃或修改
randomSwitch(
50.0 -> exec(http("Remove Item").delete("/cart/item/1")), // 50%*20%=10% 概率删除商品
50.0 -> exec { session => session.set("abandonCheckout", true) } // 50%*20%=10% 概率直接放弃
)
)
)
}
// D. 下单后随机浏览或退出
.randomSwitch(
30.0 -> exec( // 30%的用户继续浏览其他页面
http("Browse Recommendations")
.get("/recommendations")
.header("Authorization", "Bearer ${authToken}")
.pause(Random.nextInt(10).seconds)
),
70.0 -> exec( // 70%的用户直接退出
pause(5 seconds) // 退出前的停留
)
)
// --- 3. 设置负载模型 ---
setUp(
scn.inject(
rampUsersPerSec(1).to(10).during(2 minutes), // 2分钟内逐渐增加到每秒10个用户
constantUsersPerSec(10).during(5 minutes) // 然后保持10用户/秒不断5分钟
)
).protocols(httpProtocol)
}
建议和规避
Session状态管理:
陷阱:在 repeat、during 内部使用 .feed(feeder.random) 可能导致每次迭代使用不同的Feeder数据,破坏情形连续性。
正确做法:如果需要在循环内使用同一用户的不同数据(就像一个用户查多个商品),应在循环外部 .feed 一次,在循环内部使用 ${变量} 引用;如果需要在循环内使用全新数据(如每次搜索新重点词),则需在循环内 .feed。
循环和时间的准确控制:
during和forever:during 保证总执行时间不超过指定时长,而 forever 会无限循环,一般需要配合 exitHereIfFailed 或全局时间限制。
负载精确:在 during 块内,Gatling只控制第一个虚拟用户进入和最后一个虚拟用户退出的时间差,不保证每个用户在整个期间不断活动。更精确的不断负载需靠 inject 注入方法控制。
随机性和可重复性:
调试:在脚本开发阶段,使用 Random.setSeed(123L) 固定随机数种子,使每次运行结果可复现。
生产压测:移除种子,保证真正的随机性。
Gatling通过 Random 模拟微观不确定性,通过 Feeder 提供宏观数据多样性,再通过循环和条件思路将这些元素编织成动态、真实的用户会话流。