wrk 及扩展支持 tcp 字节流协议压测

高性能、方便使用的 HTTP(s) 的流量压测工具,结合了多个开源项目开发而成:

  1. redis 的 ae 事件框架
  2. luajit
  3. openssl
  4. http-parser

减少造轮子、复用他人的成功项目,赞;我们定制化也走这条路线,代码见此

要支持 tcp 字节流协议压测,只需要增加一个函数 stream_response实现见此

-- data 的结果为 {"error_code":0,"error_msg":"","data":{}}
-- stream_response 表示使用tcp字节流协议压测,对返回 error_code 进行校验,为0表示状态正常。 function stream_response(data)
local t = json.decode(data)
return t["error_code"] == 0
end

lua 脚本

wrk 的第一大特色就是支持 lua 脚本,直接对 c 修改进行压测成本比较高:c 的业务开发速度较慢及每次都需要编译。

脚本则克服了开发时间过长的缺点,使用 luajit 速度可以保证在开发和运行的速度中得到一个平衡。

具体的脚本的变量和函数的逻辑见 官方文档,一定要熟读这个文档,精华部分,比其它个人的表述准确非常多。

一些官方文档之外的补充

脚本文件

分为两个文件组成

  1. wrk.lua 内置脚本,提供了基本的 API 和一些变量
  2. 命令行 -s <foo.lua>, foo.lua 用户自己使用的脚本文件,为可选项

线程

每个线程都包含一个自己的lua状态机,所以在脚本文件中定义的变量(如自增的请求计数),不同线程的中的结果是不相同的。

线程的结构是一个用户数据(userdata), 在 c 中的定义为 struct thread。关联的 addr 也同样为用户数据,在 c 中的定义为 struct addrinfo,支持取和存的操作(可以存在 = 的左右)

thread:set(key, value),value 不能够为表,在使用 get 操作后会发生 panic,应该是 script_copy_value() 函数中出现栈顶设置错误的问题

加速

如果构造的 request 内容比较耗时的话,优先放在 init() 使用提前生成并且混缓存起来,后面的 request() 直接从缓存的结果中获取。

高性能 && 请求收发逻辑

基于 redis 的 ae 事件框架是。和 ab 不同的是可以充分利用多核资源,减小线程间的切换,以此获得高性能。一般而言,ab 在请求量不是很大的情况下是ok的,但是在请求量到达上w req/s 后,自身就会成为瓶颈。

在每个线程中创建 connections / threads 个连接,并且将这些建立连接的 fd 添加至事件循环中,然后 fd 就绪后,将 readblewriteable 函数添加再添加至事件循环中;

writeable

对应着请求发送的逻辑,调用lua接口 reqeust() 获取发送内容就在其中;

在发送完成后会将自身从事件循环中删除,发送(write)可能调用多次,但是一定会等到将缓冲区中的内容全部发送完成,除非发送失败产生错误。

延时发送 delay

delay() 为 lua 的一个可选接口,发挥延迟发送的间隔,单位为毫秒(ms).

当lua脚本中出现了该函数时,writeable 就会从事件循环的文件事件删除自身,并且将 writeable 作为定时任务添加至事件循环中,从而达到延时发送的效果。

readable

对应响应接收的逻辑,对应返回的内容校验,在官方版本中,为 http 请求的解析。对解析的结果进行统计,当判断响应结束时,删除该连接在事件循环中的事件,并且重新进行最初的动作。

我们可以接管这个解析结果的过程,丰富化使用场景。

收发事件简单的时序图

统计

wrk 对两个维度对压测的结果做了统计,结果如下

  Thread Stats   Avg      Stdev     Max   +/- Stdev
Latency 19.85ms 3.71ms 49.11ms 81.44%
Req/Sec 98.50 16.32 121.00 88.33%

延迟 Latency && 请求速度 Req/Sec

统计每个请求的延迟情况

  • Avg, 平均延迟
  • Stdev, 样本标准差
  • Max, 最大延迟
  • +/- Stdev, 正负一倍标准差概率

实现

wrk 的实现为通用结构,适用延迟和请求速度的统计

typedef struct {
uint64_t count; // 样本数量
uint64_t limit; // 最大样本变量限制
uint64_t min; // 最小样本变量
uint64_t max; // 最大样本变量
uint64_t data[]; // 索引为样本变量,值为出现的次数
} stats;

limit 防止 data[] 容量不够,也起到一个剔除不满足要求的情况,如延迟超过 limit 后直接归为 timeout 中。

️ 注意 data[] 数据每个元素的值为出现的次数,而不是样本变量。

样本变量统计

__sync_* 为编译器的同步函数,wrk 将统计的变量作为一个全局存在,故多个线程内就需要一些同步操作保证正确性。

理论上可以将这些统计变量放在线程内,在所以线程结束后,汇集处理,这里就不要这些同步元语了。不过目前还算简单,这样做问题也不大。

n 为样本变量,stats->data[n] 为该样本变量出现的次数。min 和 max 为之后的统计过程加速。

int stats_record(stats *stats, uint64_t n) {
if (n >= stats->limit) return 0;
__sync_fetch_and_add(&stats->data[n], 1);
__sync_fetch_and_add(&stats->count, 1);
uint64_t min = stats->min;
uint64_t max = stats->max;
while (n < min) min = __sync_val_compare_and_swap(&stats->min, min, n);
while (n > max) max = __sync_val_compare_and_swap(&stats->max, max, n);
return 1;
}

样本标准差

数学公式为

\[\delta = \sqrt{ \frac{\Sigma(x_i-\bar{x})^2}{n-1}}
\]

wrk 实现如下,L6 处 * stats->data[i] 表示有多个样本变量为 i

 1 long double stats_stdev(stats *stats, long double mean) {
2 long double sum = 0.0;
3 if (stats->count < 2) return 0.0;
4 for (uint64_t i = stats->min; i <= stats->max; i++) {
5 if (stats->data[i]) {
6 sum += powl(i - mean, 2) * stats->data[i];
7 }
8 }
9 return sqrtl(sum / (stats->count - 1));
10 }

扩展支持 tcp 压测

由于 wrk 支持 http(s) 的压测,但实际的场景中有很多不是 http 的协议,可以就是很简单的 json 文本协议。

所以这里对 wrk 做一个简单的扩展,支持普通的4层流量压测,功能上支持 json 和 md5。json库使用 yyjson,md5 使用 nginx/md5,充分利用前人的成功经验。

提供的库功能

--  scripts/test.lua
local data = '{"host":"129.168.10.10","os":"linux","open_ports":[22,80,3306]}'
local data_tbl = json.decode(data) local function print_tables(t, indent)
local tab_indent = ""
for i = 1, indent do tab_indent = tab_indent .. "\t" end for k, v in pairs(t) do
if type(v) == "table" then
print_tables(v, indent + 1)
else
print(string.format("%s %s\t%s", tab_indent, tostring(k), tostring(v)))
end
end
end print_tables(data_tbl, 0) -- $ ./wrk -t 1 -c 1 -d3s -L -s scripts/test.lua https://www.baidu.com
-- host 129.168.10.10
-- os linux
-- 1 22
-- 2 80
-- 3 3306

json

  • json.encode, json转字符串
  • json.decode, 字符串转json
  • json.encode_empty_table_as_object, 空table时作为 object 使用(参考 openresty)

md5

  • md5.sum, 16字节md5sum
  • md5.sumhexa, 32字节16进制格式的sum

支持 tcp 文本的压测

主要修改提交为 点此查看

将wrk扩展为支持普通的tcp流量,主要是在 readable 中接管返回数据的解析过程。也就是 L23-L13,这个地方改动后应该形成一个分支,如果定义了支持 tcp,就走tcp的解析逻辑。

 1 static void socket_readable(aeEventLoop *loop, int fd, void *data, int mask) {
2 connection *c = data;
3 size_t n;
4
5 do {
6 switch (sock.read(c, &n)) {
7 case OK: break;
8 case ERROR: goto error;
9 case RETRY: return;
10 }
11
12 if (http_parser_execute(&c->parser, &parser_settings, c->buf, n) != n) goto error;
13 if (n == 0 && !http_body_is_final(&c->parser)) goto error;
14
15 c->thread->bytes += n;
16 } while (n == RECVBUF && sock.readable(c) > 0);
17
18 return;
19
20 error:
21 c->thread->errors.read++;
22 reconnect_socket(c->thread, c);
23 }

如何决定为tcp文本协议解析

想来想去,为不破坏原来的接口定义并且尽量减少改动,通过以lua脚本中是否定义 stream_response() 来决定是否支持tcp的文本协议:

bool script_want_stream_response(lua_State *L) {
return script_is_function(L, "stream_response");
}

使用函数指针 response_complete 来代替分支逻辑,减少干扰。 在程序的起始阶段,根据是否支持为tcp文本协议将具体的处理函数赋值给函数指针。

函数指针的定义为

// 旧的 response_complete -> message_complete

typedef bool (*response_complete_func)(connection *c, size_t n);
static response_complete_func response_complete;

修改后的结果为, socket_readable L12-L3 被替换为一个函数指针执行。

 1 static void socket_readable(aeEventLoop *loop, int fd, void *data, int mask) {
...
12 if (!response_complete(c, n))
13 goto error;
...
23 }

导出tcp文本协议解析

除了代码位置调整及函数名修改,旧的关于 http 流量的解析逻辑不变。

stream_response_completeresponse_complete 的具体实现,script_stream_response 将响应内容导出至lua的 stream_response 处理。

当响应的内容长度为0时,直接重连。

 1 bool stream_response_complete(connection *c, size_t n) {
2 uint64_t now = time_us();
3 thread *thread = c->thread;
4
5 thread->complete++;
6 thread->requests++;
7
8 if (!script_stream_response(thread->L, c->buf, n))
9 thread->errors.status++;
10
11 if (!stats_record(statistics.latency, now - c->start))
12 thread->errors.timeout++;
13
14 c->delayed = cfg.delay;
15 aeCreateFileEvent(thread->loop, c->fd, AE_WRITABLE, socket_writeable, c);
16
17 if (n == 0)
18 reconnect_socket(thread, c);
19
20 return true;
21 }
22
23 bool script_stream_response(lua_State *L, const char *data, size_t n){
24 lua_getglobal(L, "stream_response");
25 lua_pushlstring(L, data, n);
26 lua_call(L, 1, 1);
27 bool ok = lua_toboolean(L, -1);
28 lua_pop(L, 1);
29 return ok;
30 }

其它修改

命令行中的url参数不带 http 的scheme,则自动补全为 http 避免相关逻辑导致退出;因为对于一个 tcp 的流量压测,加一个 http 的scheme看起来怪怪的。

TODO

  1. 修复 script_copy_value() 不能够 copy 复合表的问题
  2. 增加类似端口敲门的功能:在每个tcp连接建立后,先发送一段字节流进行验证请求是否合法
  3. 支持 unix domain socket 的字节流压测

参考

  1. 官方脚本文档,对相关脚本的描述,一定要熟悉
  2. 云风的lua5.3中文文档,luajit 使用的是 lua5.1 的语法,但是云风的这个文档足够了
  3. nginx/md5, nginx 的md5模块
  4. yyjson,json解析器
  5. 导出jsoncpp给lua使用,一个正确递归处理复合表的方法
  6. zxhio/wrk, 魔改后的wrk

wrk 及扩展支持 tcp 字节流协议压测的更多相关文章

  1. NetworkComms框架介绍 完美支持TCP/UDP协议

    NetworkComms网络通信框架序言 英文文章地址 :http://www.networkcomms.net/tcp-udp-connections/ NetworkComs.Net无缝的支持TC ...

  2. jmeter实现SMTP邮件协议压测

    实现目的 通过jmeter的SMTP取样器,调用SMTP协议,批量进行邮件的发送,已达到压测的目的. 脚本实现 User Defined Variables定义用户变量 编辑SMTP Sampler取 ...

  3. 使用jmeter进行websocket协议压测

    第一步:添加websocket sampler组件 可以使用plugins manager进行添加,首先下载plugins manager组件: 下载路径:  https://jmeter-plugi ...

  4. Http压测工具wrk使用指南

    用过了很多压测工具,却一直没找到中意的那款.最近试了wrk感觉不错,写下这份使用指南给自己备忘用,如果能帮到你,那也很好. 安装 wrk支持大多数类UNIX系统,不支持windows.需要操作系统支持 ...

  5. Http压测工具wrk使用指南【转】

    用过了很多压测工具,却一直没找到中意的那款.最近试了wrk感觉不错,写下这份使用指南给自己备忘用,如果能帮到你,那也很好. 安装 wrk支持大多数类UNIX系统,不支持windows.需要操作系统支持 ...

  6. 门面模式的典型应用 Socket 和 Http(post,get)、TCP/IP 协议的关系总结

    门面模式的一个典型应用:Socket 套接字(Socket)是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元.它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息: 连接使用的 ...

  7. http、TCP/IP协议与socket之间的区别

    http.TCP/IP协议与socket之间的区别     网络由下往上分为:  www.2cto.com   物理层--                       数据链路层-- 网络层--   ...

  8. TCP/IP协议、HTTP协议、SOCKET通讯详解

    1.TCP连接TCP(Transmission Control Protocol) 传输控制协议.TCP是主机对主机层的传输控制协议,提供可靠的连接服务,采用三次握确认建立一个连接.位码即tcp标志位 ...

  9. http、TCP/IP协议与socket之间的区别(转载)

    http.TCP/IP协议与socket之间的区别  https://www.cnblogs.com/iOS-mt/p/4264675.html http.TCP/IP协议与socket之间的区别   ...

随机推荐

  1. 一个软件工程师的硬件修养:ESP8266 入门(普通动感单车-变智能)

    前言 一直在开发软件.今日突然心血来潮想尝试一下硬件. 于是就买了这样一个板子: 买的淘宝上大佬帮忙找的一个套装. 除了板子之外还有一些线和其他配件:温湿度传感器,气压传感器,光线传感器,小屏幕. 板 ...

  2. JDK 16 正式发布,一次性发布 17 个新特性…不服不行!

    上一篇:Java 15 正式发布, 14 个新特性 JDK 16 正式发布 牛逼啊,JDK 15 刚发布半年(2020/09/15),JDK 16 又如期而至(2021/03/16),老铁们,跟上. ...

  3. 从新建文件夹开始构建UtopiaEngine(1)

    序言 在苦等了半年多之后,我终于开始了向往已久的实时NPR游戏引擎项目--Utopia Engine,这半年多一直为了构建这个引擎在做很多准备:多线程.动态链接库.脚本引擎.立即渲染GUI--统统吃了 ...

  4. SpringBoot整合Swagger2及使用

    简介 swagger是一个流行的API开发框架,这个框架以"开放API声明"(OpenAPI Specification,OAS)为基础, 对整个API的开发周期都提供了相应的解决 ...

  5. UML相关汇总

    类图 类图是UML最常用的图之一,用于描述面向对象程序设计中,类.接口等结构之间的关系,如图 类图中涉及到以下几种类型的对象 UMLClass 如图中Class1,代表类 UMLOperation 如 ...

  6. Object o = new Object()占多少个字节?-对象的内存布局

    一.先上答案 这个问题有坑,有两种回答 第一种解释: object实例对象,占16个字节. 第二种解释: Object o:普通对象指针(ordinary object pointer),占4个字节. ...

  7. docker安装mysql5.6镜像并进行主从配置

    docker安装mysql镜像并进行主从配置 1.去DaoCloud官网(dockerhub可能因为网速问题下载的慢)查找需要的mysql版本镜像 docker pull daocloud.io/li ...

  8. JAVAEE_Servlet_19_重定向可以解决页面刷新问题(sendRedirect)

    重定向可以解决页面刷新问题(sendRedirect) 在向数据库中添加数据的时候,如果使用转发(getRequestDispatcher),数据插入成功后,转发到提示插入成功页面,在数据插入成功页面 ...

  9. 基于MATLAB的手写公式识别(9)

    基于MATLAB的手写公式识别(9) 1.2图像的二值化 close all; clear all; Img=imread('drink.jpg'); %灰度化 Img_Gray=rgb2gray(I ...

  10. 通过钉钉网页上的js学习xss打cookie

    做完了一个项目,然后没啥事做,无意看到了一个钉钉的外部链接: 题外话1: 查看源码,复制其中的代码: try { var search = location.search; if (search &a ...