需求:模拟马拉松比赛时,多人使用跑步软件上传轨迹数据,另有多人下载跑步者的轨迹数据,达到实时观看马拉松跑步轨迹的目的。

压力测试的步骤 1:创建虚拟用户

浏览器中打开网页: http://12.34.56.78/marathon/useradmin/create_chunk_users.php?phonestart=1231231000&usercount=10&prefix=uc

说明:

php 脚本生成的网页内容:

runnerName,runnerPassword,runnerNickname,watcher1Name,watcher1Password,watcher1Nickname,watcher2Name,watcher2Password,watcher2Nickname,watcher3Name,watcher3Password,watcher3Nickname,watcher4Name,watcher4Password,watcher4Nickname
1231231000,123456,ucr0,1231231001,123456,ucr0w1,1231231002,123456,ucr0w2,1231231003,123456,ucr0w3,1231231004,123456,ucr0w4
1231231005,123456,ucr1,1231231006,123456,ucr1w1,1231231007,123456,ucr1w2,1231231008,123456,ucr1w3,1231231009,123456,ucr1w4
1231231010,123456,ucr2,1231231011,123456,ucr2w1,1231231012,123456,ucr2w2,1231231013,123456,ucr2w3,1231231014,123456,ucr2w4
1231231015,123456,ucr3,1231231016,123456,ucr3w1,1231231017,123456,ucr3w2,1231231018,123456,ucr3w3,1231231019,123456,ucr3w4
1231231020,123456,ucr4,1231231021,123456,ucr4w1,1231231022,123456,ucr4w2,1231231023,123456,ucr4w3,1231231024,123456,ucr4w4
1231231025,123456,ucr5,1231231026,123456,ucr5w1,1231231027,123456,ucr5w2,1231231028,123456,ucr5w3,1231231029,123456,ucr5w4
1231231030,123456,ucr6,1231231031,123456,ucr6w1,1231231032,123456,ucr6w2,1231231033,123456,ucr6w3,1231231034,123456,ucr6w4
1231231035,123456,ucr7,1231231036,123456,ucr7w1,1231231037,123456,ucr7w2,1231231038,123456,ucr7w3,1231231039,123456,ucr7w4
1231231040,123456,ucr8,1231231041,123456,ucr8w1,1231231042,123456,ucr8w2,1231231043,123456,ucr8w3,1231231044,123456,ucr8w4
1231231045,123456,ucr9,1231231046,123456,ucr9w1,1231231047,123456,ucr9w2,1231231048,123456,ucr9w3,1231231049,123456

内容是 excel 格式的每组帐号的手机号、密码、昵称

此网页内容保存到压力测试服务器的 ~/gatling-charts-highcharts-bundle-2.1.7/user-files/data/runner-watcher-10.csv 中

压力测试的步骤 2:生成跑步轨迹数据文件

浏览器中打开网页:

http://12.34.56.78/marathon/dump/locations.php?starttime=2015-12-22%2015:38:54&userid=12345
可以导出帐号 12345 的已结束跑步的轨迹数据,示例如下:

toff1,lat1,lng1,toff2,lat2,lng2,toff3,lat3,lng3,toff4,lat4,lng4,toff5,lat5,lng5
2,31.328442,120.432391,4,31.328441034483,120.43249606897,6,31.328440068966,120.43260113793,8,31.328439103448,120.4327062069,10,31.328438137931,120.43281127586,
12,31.328437172414,120.43291634483,14,31.328436206897,120.43302141379,16,31.328435241379,120.43312648276,18,31.328434275862,120.43323155172,20,31.328433310345,120.43333662069,
22,31.328432344828,120.43344168966,24,31.32843137931,120.43354675862,26,31.328430413793,120.43365182759,28,31.328429448276,120.43375689655,30,31.328428482759,120.43386196552,
32,31.328427517241,120.43396703448,34,31.328426551724,120.43407210345,36,31.328425586207,120.43417717241,38,31.32842462069,120.43428224138,40,31.328423655172,120.43438731034,
42,31.328422689655,120.43449237931,44,31.328421724138,120.43459744828,46,31.328420758621,120.43470251724,48,31.328419793103,120.43480758621,50,31.328418827586,120.43491265517,
52,31.328417862069,120.43501772414,54,31.328416896552,120.4351227931,56,31.328415931034,120.43522786207,58,31.328414965517,120.43533293103,60,31.328414,120.435438,
62,31.328413228571,120.435541,64,31.328412457143,120.435644,66,31.328411685714,120.435747,68,31.328410914286,120.43585,70,31.328410142857,120.435953,
72,31.328409371429,120.436056,74,31.3284086,120.436159,76,31.328407828571,120.436262,78,31.328407057143,120.436365,80,31.328406285714,120.436468,
82,31.328405514286,120.436571,84,31.328404742857,120.436674,86,31.328403971429,120.436777,88,31.3284032,120.43688,90,31.328402428571,120.436983,
92,31.328401657143,120.437086,94,31.328400885714,120.437189,96,31.328400114286,120.437292,98,31.328399342857,120.437395,100,31.328398571429,120.437498,

内容为 excel 格式的轨迹数据,每行数据为每 10 秒上传的 5 个点。

此网页内容保存到压力测试服务器的~/gatling-charts-highcharts-bundle-2.1.7/user-files/data/location-taihu-42km.csv 中

截取 location-taihu-42km.csv 的 229 行,可以另存为 location-taihu-10km.csv。

压力测试的步骤 3:运行

压力测试服务器上,输入 ~/gatling-charts-highcharts-bundle-2.1.7/bin$ ./gatling.sh
输入 0 选择第一个测试用例 Marathon.RunningSimulation,再按两次回车,就开始压力测试了

shen@debian:~/gatling-charts-highcharts-bundle-2.1.7/bin$ ./gatling.sh 
GATLING_HOME is set to /home/shen/gatling-charts-highcharts-bundle-2.1.7
Choose a simulation number:
     [0] Marathon.RunningSimulation
     [1] computerdatabase.BasicSimulation
     [2] computerdatabase.advanced.AdvancedSimulationStep01
     [3] computerdatabase.advanced.AdvancedSimulationStep02
     [4] computerdatabase.advanced.AdvancedSimulationStep03
     [5] computerdatabase.advanced.AdvancedSimulationStep04
     [6] computerdatabase.advanced.AdvancedSimulationStep05
0
Select simulation id (default is 'runningsimulation'). Accepted characters are a-z, A-Z, 0-9, - and _

Select run description (optional)

运行过程中的输出:

================================================================================
2015-12-23 13:51:38                                         170s elapsed
---- running -------------------------------------------------------------------
[--------------------------------------------------------------------------]  0%
          waiting: 0      / active: 1      / done:0     
---- Requests ------------------------------------------------------------------
> Global                                                   (OK=198    KO=0     )
> login                                                    (OK=5      KO=0     )
> start_workout                                            (OK=1      KO=0     )
> start_session                                            (OK=1      KO=0     )
> upload_location                                          (OK=15     KO=0     )
> get_last_location                                        (OK=176    KO=0     )
================================================================================

Gatling 源代码摘要:

RunningSimulation.scala

package Marathon

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
import java.text.SimpleDateFormat
import java.util.Date

class RunningSimulation extends Simulation {

  val httpConf = http
    .baseURL("http://12.34.56.78/marathon/api")

  val userFeeder = csv("runner-watcher-10.csv").circular

  object RunAndWatch {

    val records = csv("location-taihu-10km.csv").records

    val runAndWatch = foreach(records, "record") {
        exec(flattenMapIntoAttributes("${record}"))
        .exec(
          http("upload_location")
          .post("/Workout/saveWorkoutSegment")
          .headers(headers_urlencoded)
          .formParam("userid", "${runner_uid}")
          .formParam("sessionid", "abcd")
          .formParam("workout", """{
            "contenttype": "sessiondata",
            "users_id": "${runner_uid}",
            "starttime": "${w_starttime}",
            "lap": [
              {
                "starttime": "${s_starttime}",
                "locationdata": [
                  {
                      "timeoffset": "${toff1}",
                      "latitude": "${lat1}",
                      "longitude": "${lng1}",
                  },
                  {
                      "timeoffset": "${toff2}",
                      "latitude": "${lat2}",
                      "longitude": "${lng2}",
                  },
                  {
                      "timeoffset": "${toff3}",
                      "latitude": "${lat3}",
                      "longitude": "${lng3}",
                  },
                  {
                      "timeoffset": "${toff4}",
                      "latitude": "${lat4}",
                      "longitude": "${lng4}",
                  },
                  {
                      "timeoffset": "${toff5}",
                      "latitude": "${lat5}",
                      "longitude": "${lng5}",
                  }
                ]
              }
            ]
          }""")
        )
        .pause(3)
        .exec(
          http("get_all_location")
          .post("/Workout/getLatestData")
          .headers(headers_urlencoded)
          .formParam("userid", "${watcher1_uid}")
          .formParam("sessionid", "abcd")
          .formParam("id", "${runner_uid}")
          .formParam("startsplit", "0")
          //.check(bodyString.saveAs("watcher1_location_body"))
        )
        .exec{ session =>
          val df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
          val now = df.format(new Date())
          session.set("watcher1_lasttime", now)
        }
        .pause(3)
        .exec(
          http("get_all_location")
          .post("/Workout/getLatestData")
          .headers(headers_urlencoded)
          .formParam("userid", "${watcher1_uid}")
          .formParam("sessionid", "abcd")
          .formParam("id", "${runner_uid}")
          .formParam("startsplit", "0")
          //.check(bodyString.saveAs("watcher1_location_body"))
        )
        .exec{ session =>
          val df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
          val now = df.format(new Date())
          session.set("watcher1_lasttime", now)
        }
        .pause(3)
        .exec(
          http("get_all_location")
          .post("/Workout/getLatestData")
          .headers(headers_urlencoded)
          .formParam("userid", "${watcher1_uid}")
          .formParam("sessionid", "abcd")
          .formParam("id", "${runner_uid}")
          .formParam("startsplit", "0")
          //.check(bodyString.saveAs("watcher1_location_body"))
        )
        .exec{ session =>
          val df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
          val now = df.format(new Date())
          session.set("watcher1_lasttime", now)
        }
        .pause(1)
    }
  }

  val headers_urlencoded = Map("Content-Type" -> "application/x-www-form-urlencoded")

  val scn = scenario("running")
    .feed(userFeeder)
    .exec{ session =>
      //println(session)
      session
    }
    .exec(
      http("login")
      .post("/User/login")
      .headers(headers_urlencoded)
      .formParam("name", "${runnerName}")
      .formParam("password", "${runnerPassword}")
      .check(jsonPath("$.result.id").saveAs("runner_uid"))
    )
    .pause(1)
    .exec(
      http("login")
      .post("/User/login")
      .headers(headers_urlencoded)
      .formParam("name", "${watcher1Name}")
      .formParam("password", "${watcher1Password}")
      .check(jsonPath("$.result.id").saveAs("watcher1_uid"))
    )
    .pause(60)
    .exec{ session =>
      //println(session)
      val df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
      val now = df.format(new Date())
      session.set("w_starttime", now)
    }
    .exec(
      http("start_workout")
      .post("/Workout/saveWorkoutSegment")
      .headers(headers_urlencoded)
      .formParam("userid", "${runner_uid}")
      .formParam("sessionid", "abcd")
      .formParam("workout", """{
          "contenttype": "workouthead",
          "users_id": "${runner_uid}",
          "starttime": "${w_starttime}",
          "type": "1"
        }""")
      .check(bodyString.saveAs("start_workout_body"))
      .check(jsonPath("$.result.id").saveAs("workout_id"))
    )
    .pause(1)
    )
    .exec{ session =>
      val df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
      val now = df.format(new Date())
      session.set("watcher1_lasttime", now)
    }
    .exec(RunAndWatch.runAndWatch)
    .exec{ session =>
      //println(session)
      session
    }
    )
    .pause(1)
    .exec(
      http("end_workout")
      .post("/Workout/saveWorkoutSegment")
      .headers(headers_urlencoded)
      .formParam("userid", "${runner_uid}")
      .formParam("sessionid", "abcd")
      .formParam("workout", """{
          "contenttype": "workoutend",
          "users_id": "${runner_uid}",
          "starttime": "${w_starttime}",
        }""")
    )

  setUp(scn.inject(rampUsers(10) over(60 seconds)).protocols(httpConf))
  //setUp(scn.inject(atOnceUsers(1)).protocols(httpConf))
}

10 个跑步者和 10 个观看者在 60 秒内登录完成,然后开始跑步和观看。1 人跑,1 人看,共 10 组用户。

测试报告是这样的:

最主要指标:95th percentile 1436
表示 95% upload_location 请求的响应时间低于 1436 毫秒,上图是 800 组用户的情况,已经到达性能瓶颈。
如果总共 100 个请求,那么从快到慢的第 95 个请求的响应时间为 1436 毫秒。


↙↙↙阅读原文可查看相关链接,并与作者交流