压测工具

用户头像
Acker飏
关注
发布于: 17 小时前

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 Step
data 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 Step
data 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 Statistics
internal 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.475
Fastest request - time: 0:00:00.004, status: 200, path: /demo
Slowest request - time: 0:00:00.326, status: 200, path: /demo
95% request - time: 0:00:00.240, status: 200, path: /demo
Average request - time: 0:00:00.037

Process finished with exit code 0



用户头像

Acker飏

关注

还未添加个人签名 2018.05.03 加入

还未添加个人简介

评论

发布
暂无评论
压测工具