原文地址
https://mp.weixin.qq.com/s/pLQqO63bQMnMbNADuBFhlw
1. 背景
在流量采集和分析的场景中,一种常见架构如下所示:
在上述架构中,交换机通过流量镜像的方式,将用户与应用服务器之间的流量“复制”给流量采集/分析服务器。流量服务
器上部署的采集探针负责协议数据包的重组,以及一部分流量分析工作,比如判断数据包是否触发某些规则。此时,需要对
流量采集探针进行两方面的测试工作:
性能测试:如果采集探针重组和分析数据包的性能不够高,那么将导致丢包,进而影响后续的进一步分析
功能测试:从大量的流量中,准确地识别出风险事件、敏感数据等是流量分析的基础工作,如果无法做好这些工作,那么流
量采集和分析将失去其意义
为进行性能测试,需要在模拟的用户和应用服务器之间,发送大量请求。为进行功能测试,需要在模拟的用户和应用服务器
之间发送多种具有特定特征的流量。当前最主流的应用层协议非 HTTP 莫属。接下来将讲述如何使用 Lua 语言扩展 Nginx
和 Wrk,实现针对 HTTP 协议的性能测试和功能测试。
2. 测试环境
操作系统:CentOS 7.9
3. 安装 Openresty
Openresty 是完全成熟的 Web 应用服务器,它捆绑了标准的 Nginx 核心,大量的第三方模块,以及它们的大部分外部依
赖。
3.1. 安装依赖包
sudo yum install -y pcre pcre-devel openssl openssl-devel perl make gcc curl zlib zlib-devel
3.2. 下载源码包
去官网的 Download 页面,下载 Openrestry 源码包。本文使用的是 openresty-1.19.9.1.tar.gz。
wget https://openresty.org/download/openresty-1.19.9.1.tar.gz
3.3. 安装
tar zxf openresty-1.19.9.1.tar.gz
cd openresty-1.19.9.1/
./configure --with-luajit --with-http_iconv_module
make -j8 && sudo make install
Openresty 默认被安装到 /usr/local/openresty/
3.4. 验证
/usr/local/openresty/bin/openresty -V
4. 安装 Wrk
wrk 是现代的 HTTP 基准测试工具,当在单个多核 CPU 上运行时,能够产生显著的负载。它结合多线程设计和可扩展的事
件通知系统,比如 epoll 和 kqueue。
可选的 LuaJIT 脚本可以执行 HTTP 请求生成、响应处理和自定义报告。
4.1. 安装依赖包
sudo yum install -y gcc openssl openssl-devel git curl
4.2. 克隆源码
git clone https://github.com/wg/wrk.git wrk
4.3. 编译
cd wrk/
make
编译完成后,生成的二进制可执行文件 被保存当前目录中。可以将其移动到 中的某个目录下。 wrk PATH
4.4 验证
./wrk -v
5. Wrk 脚本简介
5.1. 概览
Wrk 支持在三个不同阶段期间执行 LuaJIT 脚本:Setup、Running 和 Done。每个 Wrk 线程拥有独立的脚本环境,Setup
和 Done 阶段在单独的环境中执行,该环境不参与 Running 阶段。 公有 Lua API 包含全局表和多个全局函数:
wrk = {
scheme = "http",
host = "localhost",
port = nil,
method = "GET",
path = "/",
headers = {},
body = nil,
thread userdata= < >,
}
wrk format method path headers bodyfunction . ( , , , )
wrk.format 返回由传入参数与 wrk 表中的值合并得到的 HTTP 请求字符串。
wrk lookup host servicefunction . ( , )
wrk.lookup 返回包含 host 和 service 对的所有已知地址的表。与 POSIX 函数对应。 getaddrinfo()
wrk connect addrfunction . ( )
如果能够连接到 addr,wrk.connect 返回 true,否则返回 false。addr 必须是从 wrk.lookup 返回的地址。
如下全局变量是可选的,如果定义,那么必须是函数:
global setup – 在线程 Setup 期间调用
global init – 在线程启动时调用
global delay – 用于获取请求延迟
global request – 用于生成 HTTP 请求
global response – 使用 HTTP 响应数据调用
global done – 使用运行结果调用
5.2. Setup
setup threadfunction ( )
在已解析目标 IP 地址,并且所有线程已初始化,但尚未启动之后,Setup 阶段开始。
为每个线程,调用一次 ,该函数接收代表线程的 userdata 对象。 setup()
thread.addr - 获取或设置线程的服务端地址
thread:get(name) - 获取线程环境中的全局变量的值
thread:set(name, value) - 设置线程环境中的全局变量的值
thread:stop() - 停止线程
只有布尔值、 和字符串值或相同的表可以通过 / 传递, 只能在线程运行时 nil number get() set() thread:stop()
调用。
5.3. Running
init argsfunction ( )
delayfunction ()
requestfunction ()
response status headers bodyfunction ( , , )
Running 阶段从对 的单次调用开始,接下来为每个请求周期调用 init() request() response()
函数为脚本接受额外的命令行参数,必须用 “–” 将其与 wrk 参数隔开。 init()
返回延迟发送下个请求的毫秒数。 delay()
返回包含 HTTP 请求的字符串。在测试高性能服务器时,每次都构建新请求代价很大。一个方案是在 request() init()
预生成所有请求,然后在 中进行快速查询。 request()
使用 HTTP 响应状态码、头和体调用 。解析头和体代价很大,因此如果在调用 后,response 全局变 response() init()
使用 HTTP 响应状态码、头和体调用 。解析头和体代价很大,因此如果在调用 后,response 全局变 response() init()
量是 nil,wrk 将忽略头和体。
5.4. Done
done summary latency requestsfunction ( , , )
函数接收包含结果数据,以及代表每个请求延迟和每个线程请求速率的两个统计对象的表。持续时间和延迟都是微 done()
秒值,而速率以每秒的请求数来衡量。
latency.min – 所见的最小值
latency.max – 所见的最大值
latency.mean – 所见的平均值
latency.stdev – 标准偏差
latency:percentile(99.0) – 百分之 99 的值
latency(i) – 原始值和计数
summary = {
duration = N,
-- 运行持续时间,单位为微秒
requests = N,
-- 已完成的请求总数
bytes = N,
-- 接收的总字节数
errors = {
connect = N,
-- Socket 连接错误总数
read = N,
-- Socket 读取错误总数
write = N,
-- Socket 写错误总数
status = N,
-- 大于 399 的 HTTP 状态码总数
timeout = N
-- 请求超时总数
}
}
6. 使用 Python 生成随机图片
图片是非常常见的资源类型,常见图片格式包括 JPG、PNG、GIF 等。测试过程中,可能希望模拟的服务端返回具有指定宽
度和高度的图片。Pillow 是 Python 中强大的图片处理库,接下来使用 Pillow 生成随机的 JPG、PNG、GIF 图片。
首先,需要安装 Pillow:
pip install pillow
下面是实现代码:
stringimport
typingimport
optparse OptionParserfrom import
randomimport
osimport
PIL Image, ImageDrawfrom import
generate_jpg(width: , height: , output: ) :def int int str -> None
"""
生成一张随机的 JPG 图片
:param width: 生成的图片的宽度
:param height: 生成的图片的高度
:param output: 输出文件名称
"""
img: Image Image.new( , (width, height))= "RGB"
pixels img.load()=
x (width):for in range
y (height):for in range
r random.randint( , )= 0 255
g random.randint( , )= 0 255
b random.randint( , )= 0 255
pixels[x, y] (r, g, b)=
img.save(output, )format="JPEG"
( output stat(output) st_size )print f"the generated JPEG image is stored in { }, file size is {os. . / 1024} KB"
generate_png(width: , height: , output: ) :def int int str -> None
"""
生成一张随机的 PNG 图片
:param width: 生成的图片的宽度
:param height: 生成的图片的高度
:param output: 输出文件名称
"""
img: Image Image.new( , (width, height))= "RGBA"
draw: ImageDraw ImageDraw.Draw(img)=
x (width):for in range
y (height):for in range
alpha random.randint( , )= 0 255
r random.randint( , )= 0 255
g random.randint( , )= 0 255
b random.randint( , )= 0 255
draw.point((x, y), fill (r, g, b, alpha))=
img.save(output, )format="PNG"
( output stat(output) st_size )print f"the generated PNG image is stored in { }, file size is {os. . / 1024} KB"
generate_gif(width: , height: , num_frames: , output: ) :def int int int str -> None
"""
生成一张随机的 GIF 图片
:param width: 生成的图片的宽度
:param height: 生成的图片的高度
:param num_frames: 生成的图片的桢数
:param output: 输出文件名称
"""
frames: typing.List[Image] []=
_ (num_frames):for in range
# 生成每一帧的随机图像
image Image.new( , (width, height))= "RGB"
x (width):for in range
y (height):for in range
r random.randint( , )= 0 255
g random.randint( , )= 0 255
b random.randint( , )= 0 255
image.putpixel((x, y), (r, g, b))
# 将当前帧添加到帧列表中
frames.append(image)
# 保存图像
frames[ ]