Gatling是什么
Gatling是一個使用Scala編寫的開源的負載測試框架,基于Akka和Netty,具有以下亮點:
- 高性能
- 友好的HTML報告
- 基于情境的記錄器(recoder),對開發友好的DSL
Gatling VS Jmeter
Jmeter是目前非常成熟的負載測試工具,支持相當多的協議,支持插件,可以輕松的擴展。
而Gatling性能上更有優勢,并且使用Scala DSL代替xml做配置,相比jmeter要更靈活,而且更容易修改和維護。
關于Jmeter和Gatling的一個比較好的對比可以參見infoq的文章
同時,Gatling也對Maven
和Gradle
這樣的構建工具比較友好,易于集成到Jenkins
中,輕松加入到CI流程中。
TIPS: 在實際使用中建議版本化管理gatling的配置,使用maven插件或gradle插件形成對應的maven/gradle工程項目管理,更容易,而且容量更小,升級gatling也會更方便,減少了很多手工的操作。
Gatling的基本使用
從官方網站下載zip壓縮包,解壓就行了,需要預先安裝有JDK,并設置好JAVA_HOME
,熟悉JAVA的朋友應該都懂,就不細說了。
Gatling的目錄結構看起來像這樣:
│ LICENSE
│
├─bin
│ gatling.bat
│ gatling.sh
│ recorder.bat
│ recorder.sh
│
├─conf
│ gatling-akka.conf
│ gatling.conf
│ logback.xml
│ recorder.conf
│
├─lib
├─results
│ .keep
│
└─user-files
├─bodies
│ .keep
│
├─data
│ search.csv
│
└─simulations
└─computerdatabase
│ BasicSimulation.scala
│
└─advanced
AdvancedSimulationStep01.scala
AdvancedSimulationStep02.scala
AdvancedSimulationStep03.scala
AdvancedSimulationStep04.scala
AdvancedSimulationStep05.scala
bin/
目錄存放gatling的可執行文件,conf/
存放配置,通常保持默認即可,lib/
存放gatling本身的依賴,用戶不用管,results/
存放報告,user-files/
是用戶最主要使用的目錄,用戶定義的測試場景相關的代碼均存放于此目錄下。
zip包解壓縮以后已經帶有了一個官方的示例文件BasicSimulation.scala
,想看看演示效果的直接使用bin/gatling.(bat|sh)
啟動就可以了。這個演示的場景描述見官方文檔。那幾個AdvancedSimulationStep
其實效果上和BasicSimulation
完全一致,只是官方提供了一些參考的DSL寫法而已。
一些實戰中的DSL參考范例
盡管gatling和jmeter一樣,帶有一個圖形化的recorder,但是功能極其簡陋,只能模擬一個用戶,并且沒有結構化代碼架構。因此只能用來生成最基本的框架,絕大多數情況需要用戶自己編寫DSL,其實官方文檔中幾乎已經涵蓋了大部分的用例,照著抄就可以了。這里提供幾個參考的DSL
Random不起作用?
有時候我們需要在測試場景中引入隨機數,從而更好的模擬大量用戶請求的場景。很自然的想到幾乎各個編程語言都帶有Random
函數庫。而Scala自然也不例外,帶有一個scala.util.Random類庫。但是實際使用的時候可能會發現沒用。比如下面這個例子:
forever(
exec(http("Random id browse")
.get("/articles/" + scala.util.Random.nextInt(100))
.check(status.is(200))
)
這個scala.util.Random.nextInt(100)
會發現只有第一次會隨機生成一個數字,后面都不變。按照gatling的官方文檔的解釋,由于DSL會預編譯,在整個執行過程中是靜態的。因此Random在運行過程中就已經靜態化了,不會再執行。應改為Feeder
實現。Feeder是gatling用于實現注入動態參數或變量的。改用Feeder
實現:
val randomIdFeeder =
Iterator.continually(Map("id" ->
(scala.util.Random.nextInt(100))))
forever(
feed(randomIdFeeder)
.exec(http("Random id browse")
.get("/articles/${id}"))
.check(status.is(200))
)
feed()
在每次執行時都會從Iterator[Map[String, T]]
對象中取出一個值,這樣才能實現這個需求。
使用import
引入外部方法
例如專門寫一個Feeders.scala
文件,存儲著各種需要用到的Feeder
:
import scala.util.Random
object LinchangFeeders {
def randomGeoFeeder() : Iterator[Map[String, Number]] = {
val LNG_RANGE = List(108.75, 109.1)
val LAT_RANGE = List(34.0, 34.4)
return Iterator.continually(
Map(
"lng" -> (
Random.nextFloat() * (LNG_RANGE(1)
- LNG_RANGE(0)) + LNG_RANGE(0)
)
,"lat" -> (
Random.nextFloat() * (LAT_RANGE(1)
- LAT_RANGE(0)) + LAT_RANGE(0)
)
)
)
}
def randomOffsetFeeder() : Iterator[Map[String, Number]] = {
Iterator.continually(Map("offset" -> Random.nextInt(100)))
}
}
然后在MySimulation.scala
就可以import
,使用里面定義好的方法了:
import scala.concurrent.duration._
import scala.util.Random
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import io.gatling.jdbc.Predef._
import Feeders._
class MySimulation extends Simulation {
val brownse = feed(randomOffsetFeeder)
.exec(
home
)
}
用戶注入策略
- <= 10: 一把注入
- > 10: 每10秒注入10個用戶
val injectStrategy =
if (USERS_COUNT > 10) {
splitUsers(USERS_COUNT) into(
rampUsers(10) over(5 seconds)
) separatedBy(10 seconds)
} else {
atOnceUsers(USERS_COUNT)
}
壓測時間策略
- = 0: 所有模擬用戶不循環,執行完測試場景即退出
- > 0: 所有模擬用戶循環執行測試場景,直到達到指定時間
val scn = scenario("My test scenario")
.doIfOrElse(DURATION > 0) {
forever(
exec(steps)
)
} {
exec(steps)
}
val setup = setUp(
scn.inject(
injectStrategy
).protocols(httpProtocol)
)
if (DURATION > 0) {
setup.maxDuration(DURATION minutes)
}
錯誤處理
這個是gatling的一大亮點。在壓力測試的過程中,無可避免會遇到各種花式錯誤。比如服務器超時無響應,服務端執行錯誤返回了非預期結果等等。這些錯誤如果不進行處理,將會影響后續測試。
比如后續所有鏈接請求都依賴于登錄成功,一旦登錄失敗,后續請求將無任何意義,而且會影響到最終匯總的測試結果。
gatling可以通過check
指令檢測URL的返回結果是否符合預期(如返回的HTTP code,返回的內容是否包含預期的內容等等)。通過tryMax
, doIf
等指令進行失敗重試以及處理鏈接之間的依賴問題。
更多關于失敗處理可以參考: http://gatling.io/docs/2.2.3/advanced_tutorial.html#step-05-check-and-failure-management
比如一個簡單的登錄請求的DSL:
val login = tryMax(MAX_RETRY) {
pause(PAUSE_BEFORE_RETRY)
.exec(http("登錄系統")
.post("/login")
.formParam("code", "${code}")
.headers(jwtRequestHeader)
.check(status.is(200),
jsonPath("$.token").find.saveAs("token")))
}
val brownse = doIf("${token.exists()}") {
exec(
// other steps
)
}
登錄成功會返回一個JSON
,包含有token
屬性,將token
存儲于session
(這個session指gatling的session,作用是存儲每個虛擬用戶各自的屬性,并不是服務器端的session),用于以后的登錄請求。通過check
期望返回200 OK
,并且期望返回一個token
屬性。
由于后續的請求都必須依賴于token
屬性存在,因此使用doIf
來確保這個依賴關系成立,遇到登錄失敗時將不會繼續向下請求。