压测工具
发布于: 2020 年 07 月 23 日
Web性能压测工具
输入参数: URL, 请求总次数,并发数。
输出参数:平均响应时间, 95%响应时间。
测试以10并发,100寸请求进行压测
写了一个可扩展的压测框架,方便以后扩展
HTTP请求客户端相关:
/** * HttpClient接口,便于扩展 */interface HttpClient { suspend fun executeHttp(request: HttpRequest): HttpStatus fun close()}/** * 基于KtorClient实现 */class KtorHttpClient :HttpClient{ private val client = io.ktor.client.HttpClient(Apache) override suspend fun executeHttp(request: HttpRequest): HttpStatus { val call = client.request<HttpResponse> { method = when(request){ is Get -> HttpMethod.Get is Post -> HttpMethod.Post is Put -> HttpMethod.Post is Delete -> HttpMethod.Post is Patch -> HttpMethod.Post } url{ host= request.host port = request.port } headers.clear() request.headers.forEach { (name, value) -> header(name, value) } body = request.body } return HttpStatus(call.status.value) } override fun close() { client.close() }}data class Scenario(val steps: List<Step>)sealed class Stepdata class RequestStep(val request: HttpRequest) : Step()data class PauseStep(val duration: Duration) : Step()
标准Restful HTTP请求封装:
sealed class HttpRequest(val host: String, val port: Int, val path: String, var headers: Map<String, String>, var body: Any) { override fun toString(): String { return "HttpRequest(host='$host', port=$port, path='$path', headers=$headers, body=${body})" } val name: String get() = this::class.java.simpleName.toUpperCase()}class Get(host: String, port: Int, path: String, headers: Map<String, String> = emptyMap(), body: Any = ByteArray(0)) : HttpRequest(host, port, path, headers, body)class Post(host: String, port: Int, path: String, headers: Map<String, String> = emptyMap(), body: ByteArray = ByteArray(0)) : HttpRequest(host, port, path, headers, body)class Put(host: String, port: Int, path: String, headers: Map<String, String> = emptyMap(), body: ByteArray = ByteArray(0)) : HttpRequest(host, port, path, headers, body)class Delete(host: String, port: Int, path: String, headers: Map<String, String> = emptyMap(), body: ByteArray = ByteArray(0)) : HttpRequest(host, port, path, headers, body)class Patch(host: String, port: Int, path: String, headers: Map<String, String> = emptyMap(), body: ByteArray = ByteArray(0)) : HttpRequest(host, port, path, headers, body) data class HttpStatus(val code: Int) { val isInformative: Boolean get() = isCodeIn(100..199) val isSuccess: Boolean get() = isCodeIn(200..299) val isRedirect: Boolean get() = isCodeIn(300..399) val isClientError: Boolean get() = isCodeIn(400..499) val isServerError: Boolean get() = isCodeIn(500..599) val isUnknown: Boolean get() = !isCodeIn(100..599) private fun isCodeIn(range: IntRange) = code in range}fun HttpRequest.headers(builderAction: MutableMap<String, String>.() -> Unit) { this.headers = LinkedHashMap<String, String>().apply(builderAction)}
构建用户模拟请求步骤:
data class Scenario(val steps: List<Step>)sealed class Stepdata class RequestStep(val request: HttpRequest) : Step()data class PauseStep(val duration: Duration) : Step()class HttpRequestBuilder( private val host: String, private val port: Int) { private val steps: MutableList<Step> = mutableListOf() fun get(path: String) { steps.add(RequestStep(Get(host, port, path))) } fun get(path: String, block: Get.() -> Unit) { steps.add(RequestStep(Get(host, port, path).apply(block))) } fun post(path: String, body: ByteArray = byteArrayOf(), block : Post.() -> Unit) { steps.add(RequestStep(Post(host, port, path, body = body).apply(block))) } fun put(path: String, body: ByteArray) { steps.add(RequestStep(Put(host, port, path, body = body))) } fun delete(path: String) { steps.add(RequestStep(Delete(host, port, path))) } fun patch(path: String) { steps.add(RequestStep(Patch(host, port, path))) } fun pause(duration: Duration) { steps.add(PauseStep(duration)) } fun build() = Scenario(steps)}
模拟场景构建:
这里使用协程进行并发行为模拟
interface ScenarioRunner { var scenario: Scenario var runnerConf: RunnerConf fun run() fun config(build: RunnerConf.() -> Unit) fun scenario(host: String, port: Int = 80, requestBuilder: HttpRequestBuilder.() -> Unit)}fun scenarioRunner( runnerFactory: ScenarioRunnerFactory, block: ScenarioRunner.() -> Unit = {}): ScenarioRunner { return runnerFactory.create().apply(block)}interface ScenarioRunnerFactory { fun create(): ScenarioRunner}object Coroutine :ScenarioRunnerFactory { override fun create(): ScenarioRunner { return CoroutineScenarioRunner(KtorHttpClient(), StdOutStatisticsPrinter()) }}data class RunnerConf( val simUsers: Int = 1, val repeatPerUser: Int = 1)internal class CoroutineScenarioRunner( private val httpClient: HttpClient, private val statisticsPrinter: StatisticsPrinter) : ScenarioRunner { private lateinit var scenarioStatistics: ScenarioStatistics override var scenario: Scenario = Scenario(emptyList()) override var runnerConf: RunnerConf = RunnerConf() private fun run(scenario: Scenario, conf: RunnerConf) { scenarioStatistics = ConcurrentScenarioStatistics(Instant.now()) runScenario(scenario, conf) } override fun run() { run(scenario, runnerConf) } override fun config(build: RunnerConf.() -> Unit){ runnerConf.apply(build) } override fun scenario(host: String, port: Int, requestBuilder: HttpRequestBuilder.() -> Unit) { scenario = HttpRequestBuilder(host, port).apply(requestBuilder).build() } private fun runScenario(scenario: Scenario, conf: RunnerConf) { runBlocking { coroutineScope { repeat(conf.simUsers) { launch { repeat(conf.repeatPerUser){ handleSteps(scenario.steps) } } } } httpClient.close() scenarioStatistics.endTime = Instant.now() statisticsPrinter.print(scenarioStatistics.getSummary()) } } private suspend fun handleSteps(steps: List<Step>) = steps.forEach { when (it) { is RequestStep -> executeHttp(it.request) is PauseStep -> pauseFor(it.duration) } } private suspend fun pauseFor(duration: Duration) { delay(duration.toMillis()) scenarioStatistics.gather(PauseStatistics(duration)) } private suspend fun executeHttp(request: HttpRequest) { val startTime = Instant.now() val httpStatus = httpClient.executeHttp(request) val timeElapsed = Duration.between(startTime, Instant.now()) scenarioStatistics.gather(RequestStatistics(request, timeElapsed, httpStatus)) }}
分析统计:
internal sealed class Statisticsinternal data class RequestStatistics(val request: HttpRequest, val timeTaken: Duration, val httpStatus: HttpStatus) : Statistics()internal data class PauseStatistics(val duration: Duration) : Statistics()internal data class ScenarioSummary(val statistics: Collection<Statistics>, private val startTime: Instant, private val endTime: Instant) { fun hasRequests() = statistics.any { it is RequestStatistics } val slowestRequest: RequestStatistics? get() = this.statistics .filterIsInstance<RequestStatistics>() .maxBy { it.timeTaken } val fastestRequest: RequestStatistics? get() = this.statistics .filterIsInstance<RequestStatistics>() .minBy { it.timeTaken } val averageRequestTime: Duration get() = this.statistics .filterIsInstance<RequestStatistics>() .map { it.timeTaken.toMillis() } .average() .let { Duration.ofMillis(it.toLong()) } val requestTime95 : RequestStatistics? get() = statistics .filterIsInstance<RequestStatistics>() .sortedBy { it.timeTaken.toMillis() } .let { it[(it.size * 0.95).toInt()] } val scenarioTime: Duration get() = Duration.between(startTime, endTime)}internal interface ScenarioStatistics { fun gather(statistics: Statistics) fun getSummary(): ScenarioSummary val startTime: Instant var endTime: Instant}internal class ConcurrentScenarioStatistics(override val startTime: Instant) : ScenarioStatistics { private val scenarioStats = ConcurrentLinkedQueue<Statistics>() override lateinit var endTime: Instant override fun getSummary() = ScenarioSummary(scenarioStats, startTime, endTime) override fun gather(statistics: Statistics) { this.scenarioStats.add(statistics) }}
统计结果输出:
通过接口可以实现不同的输出方式
internal interface StatisticsPrinter { fun print(scenarioSummary: ScenarioSummary)}internal class StdOutStatisticsPrinter : StatisticsPrinter { override fun print(scenarioSummary: ScenarioSummary) { if (!scenarioSummary.hasRequests()) { println("No requests run!") } else { val fastest = scenarioSummary.fastestRequest!! val slowest = scenarioSummary.slowestRequest!! val average = scenarioSummary.averageRequestTime val request95 = scenarioSummary.requestTime95!! println(("Scenario Duration: %s\n" + "Fastest request - time: %s, status: %d, path: %s\n" + "Slowest request - time: %s, status: %d, path: %s\n" + "95%% request - time: %s, status: %d, path: %s\n" + "Average request - time: %s\n" ) .format( formatDuration(scenarioSummary.scenarioTime), formatDuration(fastest.timeTaken), fastest.httpStatus.code, fastest.request.path, formatDuration(slowest.timeTaken), slowest.httpStatus.code, slowest.request.path, formatDuration(request95.timeTaken), request95.httpStatus.code, request95.request.path, formatDuration(average) )) } } private fun formatDuration(duration: Duration): String { val seconds = duration.seconds val positive = String.format( "%d:%02d:%02d.%03d", duration.toHours(), duration.toMinutes() % 60, duration.seconds % 60, duration.toMillis() % 1000) return if (seconds < 0) "-$positive" else positive }}
测试:
fun main() { val host = "localhost" val testPath = "/demo" scenarioRunner(Coroutine) { config { simUsers = 10 // 模拟10个用户 repeatPerUser = 10 // 每个用户执行10次操作 } scenario(host, 8080){ get(testPath) // 执行一次Get请求 pause(Duration.ofSeconds(1)) // 暂停1秒 } }.run()}
输出:
Scenario Duration: 0:00:00.475Fastest request - time: 0:00:00.004, status: 200, path: /demoSlowest request - time: 0:00:00.326, status: 200, path: /demo95% request - time: 0:00:00.240, status: 200, path: /demoAverage request - time: 0:00:00.037Process finished with exit code 0
划线
评论
复制
发布于: 2020 年 07 月 23 日 阅读数: 37
Acker飏
关注
还未添加个人签名 2018.05.03 加入
还未添加个人简介
评论