- 标题:wrk 使用教程
- 分类:
性能测试
- 标签:
工具
、性能测试
- 简介:wrk是一款高性能的HTTP压力测试工具,用于评估Web服务器或应用程序的性能。
1. wrk介绍
wrk是一款高性能的HTTP压力测试工具,用于评估Web服务器或应用程序的性能。它通过多个线程来模拟多个客户端同时访问Web服务器,并且可以指定请求的并发数、请求的时长和请求的延迟,以便评估Web服务器的性能。wrk还支持自定义请求格式,可以让用户模拟不同的客户端请求,以便对Web服务器进行更细致的测试。
1.1 wrk的运行周期
wrk运行分成三个阶段, setup、 running、done。
每个阶段wrk都提供了hook函数,可以自定义测试的参数设置、运行过程以及报告输出。
每个wrk线程都有独立的脚本环境。每个hook函数运行的规则如下图所示:
1.1.1 setup - 启动阶段
启动阶段是ip地址已经被成功解析并且线程初始化完毕但是还未启动的阶段,用于将数据传递给线程。
setup()
: 每个线程调用一次setup()
, 并接收代表该线程的userdata对象。
thread:get()
/thread:set()
:设置或获取线程运行时需要的全局变量,变量值可以是布尔值,nil,数字和字符串值或表的值。thread:stop()
: 可以停止正在运行中的线程。thread.addr
:获取线程的服务地址。
1.1.2 running - 运行阶段
在运行阶段从调用init()
方法开始,之后循环调用request()
和response()
方法。即每个线程只调用一次init()
。
function init(args)
:每个线程初始化时调用。可以接受命令行参数。function request()
:为每个请求返回HTTP对象。在此函数中,我们可以修改method,headers,path,和 body。
-
- 使用wrk.format()辅助函数来生成请求对象。格式:
return wrk.format(method, path, headers, body)
- 使用wrk.format()辅助函数来生成请求对象。格式:
function delay()
:返回延迟发送下一个请求的毫秒数。function response(status, headers, body)
:在响应返回时调用。需要status(http响应状态),headers(http响应头),body(http响应体)三个参数。解析响应头和响应体非常消耗计算机资源,因此,如果调用init()方法之后全局变量response是nil,wrk将不会解析响应头和响应体。
1.1.3 done - 完成阶段
当所有请求都已经完成并且请求结果已经被统计时调用。
-
function done(summary, latency, requests)
:函数接收的字段为:summary
、latency
、requests
- summary:表类型的结果
summary = { duration = N, -- run duration in microseconds 运行持续时间(以微秒为单位) requests = N, -- total completed requests 完成的请求总数 bytes = N, -- total bytes received 接受的总字节数 errors = { connect = N, -- total socket connection errors socket连接错误的总数目 read = N, -- total socket read errors socket读取错误的总数目 write = N, -- total socket write errors socket写入错误的总数目 status = N, -- total HTTP status codes > 399 HTTp状态码大于399的总数目 timeout = N -- total request timeouts 所有请求的超时时间总和 } }
- latency:每个请求的延迟时间(以微秒为单位)
latency.min -- minimum value seen 延时最小值 latency.max -- maximum value seen 延时最大值 latency.mean -- average value seen 延时平均值 latency.stdev -- standard deviation 延时标准差 latency:percentile(99.0) -- 99th percentile value 第99%的值 latency[i] -- raw value and count 请求i的原始延迟数据
-
requests:每个线程的请求速率
1.2 原理分析
wrk使用了非阻塞I/O和多线程技术,能够充分利用多核CPU的资源,提高测试效率。
wrk的工作原理大致如下:
- 启动wrk程序,并指定要测试的URL地址、并发数和测试时间等参数。
- wrk程序根据参数创建多个线程,每个线程对应一个工作者。
- 每个工作者根据测试参数,创建多个HTTP请求,并使用非阻塞I/O发送给服务器。
- 服务器接收到请求后,处理请求并返回响应。
- 工作者接收到响应后,统计响应时间和其他指标,并继续发送下一个请求。
- 测试时间结束后,wrk程序统计测试结果,并输出报告。
什么是非阻塞I/O呢?
非阻塞I/O是指程序在执行I/O操作时,如果数据暂时无法提供,不会进入阻塞状态,而是立即返回一个特殊值。这样,程序就可以在执行I/O操作时同时处理其他事情。非阻塞I/O的好处是可以提高程序的并发性能,有效地利用CPU的资源。
非阻塞I/O与阻塞I/O相比,需要程序多写一些代码来处理数据暂时无法提供的情况。例如,在读取文件时,如果读取的数据不够,阻塞I/O会进入等待状态,直到数据可用为止;而非阻塞I/O则会立即返回一个特殊值,告诉程序数据暂时无法提供。程序需要在下一次调用I/O函数时重新尝试读取数据(程序可以根据实际情况决定下一次调用I/O函数的时间)。
例如,如果程序在调用I/O函数读取数据时,发现数据暂时无法提供,它可以等待一段时间后,再次调用I/O函数读取数据。这种方法通常称为轮询,它可以保证程序始终保持活跃,但会消耗更多的CPU资源。此外,程序还可以使用select函数等方法,来监控文件描述符或句柄的变化,并在文件描述符或句柄可读时再次调用I/O函数读取数据。
2. wrk的使用
2.1 wrk脚本
1.2.1 脚本结构
- 线程初始化设置setup
- 线程初始化数据init
- 线程发起请求request
- 线程发起请求延迟delay
- 线程接收请求响应response
- 线程执行分析的结果分析done
---
--- Created by boyizhang.
--- DateTime: 2022/11/17 11:25
--- wrk -d2s -c1 -t1 -s post.lua https://xxxx.xx/
---
function setup(thread)
-- called during thread setup
end
function init(args)
-- called when the thread is starting
-- init()每个线程初始化时调用。
-- 可用来初始化数据,可以接受命令行参数
function delay()
return math.random(10, 50)
end
end
function request()
--为每个请求返回HTTP对象。在此函数中,我们可以修改method,headers,path,和 body。可以使用wrk.format()辅助函数来生成请求对象
--return wrk.format(method, path, headers, body)
end
function response(status, headers, body)
--在响应返回时调用。需要status(http响应状态),headers(http响应头),body(http响应体)三个参数,
--解析响应头和响应体非常消耗计算机资源,因此,如果调用init()方法之后全局变量response是nil,wrk将不会解析响应头和响应体。
end
function done(summary, latency, requests)
--当所有请求都已经完成并且请求结果已经被统计时调用。
--函数接收一个包含结果数据的表(表是lua内置数据类型),以及两个统计对象:每个请求的延迟时间(以微秒为单位)、每个线程的请求速率(以每秒请求数为单位)
end
1.2.2 脚本的执行
执行wrk -v
,获取命令行参数:
$ wrk -v
wrk [kqueue] Copyright (C) 2012 Will Glozer
Usage: wrk <options> <url>
Options:
-c, --connections <N> Connections to keep open
-d, --duration <T> Duration of test
-t, --threads <N> Number of threads to use
-s, --script <S> Load Lua script file
-H, --header <H> Add header to request
--latency Print latency statistics
--timeout <T> Socket/request timeout
-v, --version Print version details
Numeric arguments may include a SI unit (1k, 1M, 1G)
Time arguments may include a time unit (2s, 2m, 2h)
-c, --connections | 连接数,连接数一定要大于线程数。每个线程可以同时打开的连接数。 | -c 1 |
---|---|---|
-t, --threads | 线程的数量。 | -t 10 |
-d, --duration | 设置测试的时长,单位为秒,默认为10s。 | -d 1s/-d 2m/-d 3h |
-s, --script | 使用指定的请求脚本进行测试 | |
-H, --header | Add header to request | |
--latency | Print latency statistics,压测结束时会打印Latency Distribution部分的内容 | |
--timeout | Socket/request timeout | --timeout 1s |
-v, --version |
一般线程数不宜过多, 核数的2到4倍足够了. 多了反而因为线程切换过多造成效率降低。 因为 wrk 不是使用每个连接一个线程的模型, 而是通过异步网络 io 提升并发量。所以网络通信不会阻塞线程执行,这也是 wrk 可以用很少的线程模拟大量网路连接的原因。
而现在很多性能工具并没有采用这种方式, 而是采用提高线程数来实现高并发. 所以并发量一旦设的很高, 测试机自身压力就很大。测试效果反而下降。
-c选项用于指定每个线程可以同时打开的连接数。并发连接是指在同一时间内,服务器能够处理的客户端连接数量。例如,如果您的服务器具有10个并发连接,那么它就能够在同一时间内处理10个客户端的请求。
并发连接数量对于服务器性能非常重要,因为它决定了服务器能够同时处理多少客户端请求。如果并发连接数量过少,服务器将无法充分利用它的性能,并且可能无法应对突发流量。反之,如果并发连接数量过多,服务器可能会因为资源不足而崩溃。因此,在进行压力测试时,并发连接数量是一个非常重要的参数。
2.2 结果分析
- 行
-
- Latency:延时时间。
- Req/Sec:每秒请求数。
- 列
-
- Avg:平均值。
- Stdev:标准差。
- Max:最大值。
- +/- Stedv:
2.3 wrk应用
2.3.1 Get请求
通过随机获取参数发起请求
---
--- Created by boxy.
--- DateTime: 2022/11/17 11:25
--- wrk -d2s -c1 -t1 -s get.lua https://xxxx.xx/
-- example script that demonstrates use of setup() to pass
-- data to and from the threads
request = function()
data = { "29", "34", "36", "52"}
resource = data[math.random(#data)]
path = "/api/tsp/transify?resource_id=" .. resource
return wrk.format(nil, path)
end
2.3.2 Post 请求
从文件将测试数据读取到内存中,随机取其中的测试数据发起请求。
---
--- Created by boxy.
--- DateTime: 2022/11/17 11:25
--- wrk -d5s -c1 -t1 -s post.lua https://xxxx.xx/
---
local counter = 1
local threads = {}
shop_ids = {}
wrk.method = "POST"
wrk.headers["Content-Type"] = "application/json"
wrk.headers["x-sp-servicekey"] = "xxxxxxxxx"
wrk.headers["x-sp-sdu"] = "xxxxxxx"
wrk.headers["Cookie"] = "xxxxxxx"
local function readFile(fileName)
local file = io.open("listing_limit_shop_ids.txt", "r")
--local file = io.open("test_shop_ids.txt", "r")
for row in file:lines() do
--print(row)
table.insert(shop_ids, tonumber(row))
end
file:close()
print("shop_ids[1]:%s", shop_ids[1])
end
function setup(thread)
-- called during thread setup
thread:set("id", counter)
table.insert(threads, thread)
counter = counter + 1
end
function init(args)
-- called when the thread is starting
-- init()每个线程初始化时调用。可以接受命令行参数
requests = 0
responses = 0
-- ready shop_ids from txt
readFile('./test_shop_ids.txt')
local msg = "thread %d created"
print(msg:format(id))
end
function request()
--为每个请求返回HTTP对象。在此函数中,我们可以修改method,headers,path,和 body。可以使用wrk.format()辅助函数来生成请求对象
--return wrk.format(method, path, headers, body)
requests = requests + 1
print("request:shop_ids:", shop_ids[1])
shop_id = shop_ids[math.random(#shop_ids)]
local body = '{"shop_ids":[%d]}'
body = string.format(body, shop_id)
path = "/sprpc/xxxxxxxxxxxx"
print("request body:" .. body)
return wrk.format('POST', path, nil, body)
end
function response(status, headers, body)
--在响应返回时调用。需要status(http响应状态),headers(http响应头),body(http响应体)三个参数,
--解析响应头和响应体非常消耗计算机资源,因此,如果调用init()方法之后全局变量response是nil,wrk将不会解析响应头和响应体。
responses = responses + 1
print("status:", status, "response:", body)
end
function done(summary, latency, requests)
--当所有请求都已经完成并且请求结果已经被统计时调用。
--函数接收一个包含结果数据的表(表是lua内置数据类型),以及两个统计对象:每个请求的延迟时间(以微秒为单位)、每个线程的请求速率(以每秒请求数为单位)
for index, thread in ipairs(threads) do
local id = thread:get("id")
local requests = thread:get("requests")
local responses = thread:get("responses")
local msg = "thread %d made %d requests and got %d responses"
print(msg:format(id, requests, responses))
end
end
3. 番外篇-Locust
locust是一款基于Python语言的开源Web压力测试工具。它的实现原理主要包括以下几个方面:
- 协程技术:locust使用协程技术,能够在单线程中并发执行多个任务。协程的实现方式是通过挂起函数和调度器来实现的。在协程中,每个任务都是一个挂起函数。当挂起函数需要等待某些条件(例如,网络请求完成)时,它会挂起执行,并返回控制权给调度器。调度器会负责调度挂起函数的执行,以便在单线程中实现多个任务的并发执行。
- 任务定义:locust通过定义任务类和任务实例,来模拟真实用户的行为。用户可以定义任务类,指定任务类中包含哪些请求和响应处理逻辑。然后,在测试过程中,locust会根据定义的任务类,生成任务实例,并在协程中执行。
- 分布式测试:locust支持分布式测试。用户可以在多台机器上运行locust程序,模拟大量的并发用户。locust会在多台机器之间进行通信,协调测试过程和统计测试结果。
- Web界面:locust提供了Web界面,方便用户实时监控测试过程和结果。用户可以通过Web界面,调整测试参数、查看实时统计信息、调试测试脚本等。Web界面的实现方式是通过Flask框架来实现的。Flask提供了丰富的API,可以方便地构建Web应用。通过在Flask框架中集成locust的测试结果,locust可以提供方便、灵活的Web界面供用户使用
什么是挂起函数?什么是调度器?
挂起函数(Coroutine)是一种轻量级的线程,它能够在单线程中实现并发执行多个任务。挂起函数在执行过程中,可以挂起执行,并返回控制权给调度器。调度器是一种特殊的程序,它负责调度挂起函数的执行,以便在单线程中实现多个任务的并发执行。
具体来说,挂起函数的执行过程如下:
- 当挂起函数需要等待某些条件(例如,网络请求完成)时,它会挂起执行,并返回控制权给调度器。
- 调度器会接收到挂起函数的控制权,并将其保存在一个任务队列中。
- 调度器会按照一定的规则,从任务队列中选择一个挂起函数,并将其控制权返回给挂起函数。
- 挂起函数接收到控制权后,会继续执行。如果在执行过程中,再次需要挂起执行,它会再次返回控制权给调度器,重复上述操作。
调度器的选择规则可以根据实际需要进行调整。例如,可以根据挂起函数的优先级,或者根据挂起函数的执行时间,来决定调度器应该选择哪个挂起函数执行。这样,就能够保证挂起函数的执行公平、合理,从而实现单线程中多个任务的并发执行。
总之,挂起函数和调度器是协程技术的两个关键部分,通过挂起函数和调度器的协作,可以实现单线程中的多个任务的并发执行。正因为如此,协程技术在Web压力测试工具(如locust)中得到了广泛应用。
通过python实现一个挂起函数和调度器
Python提供了专门的语法和库函数,能够方便地实现协程。
下面是一个简单的例子,实现了挂起函数和调度器。
首先,定义挂起函数,使用async关键字来声明挂起函数。挂起函数内部,可以使用await关键字来挂起执行,并等待某些条件(例如,网络请求完成)。
# 定义挂起函数
async def coroutine_1():
print("coroutine_1 is running")
await coroutine_2()
print("coroutine_1 is done")
async def coroutine_2():
print("coroutine_2 is running")
await coroutine_3()
print("coroutine_2 is done")
async def coroutine_3():
print("coroutine_3 is running")
print("coroutine_3 is done")
然后,定义调度器,使用asyncio库来实现调度器。调度器的实现过程如下:
- 首先,调用asyncio.get_event_loop()函数,获取事件循环对象。事件循环对象是调度器的核心部分,负责调度挂起函数的执行。
- 然后,调用asyncio.gather()函数,将挂起函数封装成任务对象。任务对象是挂起函数的包装,能够方便地调度挂起函数的执行。
- 最后,调用事件循环对象的run_until_complete()方法,执行任务对象。这样,就能够实现调度器的核心功能,即在单线程中实现多个挂起函数的并发执行。
import asyncio
# 定义挂起函数
async def coroutine_1():
print("coroutine_1 is running")
await coroutine_2()
print("coroutine_1 is done")
async def coroutine_2():
print("coroutine_2 is running")
await coroutine_3()
print("coroutine_2 is done")
async def coroutine_3():
print("coroutine_3 is running")
print("coroutine_3 is done")
# 定义调度器
async def main():
# 获取事件循环对象
loop = asyncio.get_event_loop()
# 封装挂起函数为任务对象
task = asyncio.gather(coroutine_1(), coroutine_2(), coroutine_3())
# 执行任务对象
loop.run_until_complete(task)
# 调用调度器
main()
以上代码中,定义了3个挂起函数:coroutine_1、coroutine_2和coroutine_3。每个挂起函数在执行过程中,都会使用await关键字挂起执行,并等待其他挂起函数执行完成。然后,定义了一个调度器,使用asyncio库实现了调度器的功能。调度器的核心功能是,在单线程中实现多个挂起函数的并发执行。
在执行上述代码时,会输出以下内容:
coroutine_1 is running
coroutine_2 is running
coroutine_3 is running
coroutine_3 is done
coroutine_2 is done
coroutine_1 is done
从输出内容可以看出,挂起函数的执行顺序是coroutine_1、coroutine_2、coroutine_3。此外,这些挂起函数的执行是并发进行的,从而实现了单线程中多个任务的并发执行。