wrk(压测工具)源码笔记

wrk是一款比较常用的压测工具,支持多核,多线程,同时支持Lua脚本对返回结果和统计数据进行定制。详见github: https://github.com/wg/wrk

wrk源码部分结合了很多优秀的github开源项目,例如Redis数据库中的事件循环ae,用于nginx、nodejs、joyent的解析http请求相应的模块http-parser,以及Mike Pall所写的及时lua脚本编译器LuaJIT–lua Just in Time。这次我着重追踪了下wrk代码本身、以及ae时间循环的部分,至于http-parser以及luaJIT由于不涉及主要逻辑,暂时没有深究。抛砖引玉,一起探讨哈~

AE(Redis EventLoop)

ae是Redis内部使用的事件循环,由于我没有仔细研读过Redis的源码,所以对ae本身并不熟悉。只是ae的实现,和接触过的node的实现类似(Event-Driven Design),结合之前实践时对Redis的零星记忆(例如Redis单线程、命令串式执行等),大概明白其在wrk中的作用。
ae的event-loop监听的事件分为两种,一种是AE_FILE_EVENTS,另一种是AE_TIME_EVENTS

AE_FILE_EVENTS

AE_FILE_EVENTS在Linux下是利用epoll相关机制实现的。例如:

aeCreateEventLoop(); // 调用epoll_create
aeCreateFileEvent(); // 调用epoll_ctl
aeProcessEvents(); // 调用epoll_wait

以wrk的调用为例:
main()->(每个thread)aeCreateEventLoop()->aeApiCreate()->epoll_create()
thread_main()->connect_sock()->aeCreateFileEvent()->aeApiAddEvent()->epoll_ctl()
thread_main()->aeMain()->aeProcessEvents()->aeApiPoll()->epoll_wait()

AE_TIME_EVENT

至于AE_TIME_EVENT这个实现就很简单了。

aeCreateTimeEvent(); // 添加一个aeTimeEvent对象到loop->timeEventHead上去
processTimeEvents(); // 遍历loop->timeEventHead链表,找出已过期的时间,执行回调函数

EVENT_LOOP实现原理

ae的event-loop是在aeMain()中实现的。
这里有个疑点:AE_DONT_WAIT这个标志位没有搞明白。(AE_DONT_WAIT指的是,在aeProcessEvents()中,不取最近的那个AE_TIME_EVENT事件,直接处理AE_FILE_EVENT事件->一直阻塞直到有io事件发生)

while(!eventLoop->stop) {
    if(event->beforeSleep != NULL) 
        eventLoop->beforeSleep(eventLoop);
    aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}

里面的eventLoop->stop由调用方调用aeStop()进行设置。

下面说下aeProcessEvents()这个函数的大致思路:
1. 找到距离当前时间最近的一个AE_TIME_EVENT(遍历loop->timeEventHead),得到距现在毫秒数M.
2. 调用aeApiPoll()->epoll_wait(),阻塞M毫秒。
3. 检查epoll返回的事件,即M毫秒内,epoll所监听的fd返回的事件。分别调用这些事件的回调函数
4. 调用processTimeEvents()处理AE_TIME_EVENTS。

Wrk 源码逻辑

wrk的一般用法如下: wrk -t4 -c1200 -d30s http://127.0.0.1:8080/index.html.其中t表示开启的线程数,c表示同时发起的连接数,d表示测试持续时间。

废话不说,直接上干货。

main():
1. 解析参数,初始化
2. for 每个 thread,进行:
    a. aeCreateEventLoop(),初始化thread变量
    b. pthread_create(回调执行thread_main())
3. join每个thread
4. 打印stat数据

thread_main(): // 子线程执行函数
1. 分配connections,如上例,4个线程1200个连接,那么每个线程需要建立300个socket连接。
2. for 每个 connection:
    a. 执行connect_socket()
3. aeCreateTimeEvent() // 每100ms记录一次数据, stat
4. aeMain(), ae EventLoop
5. aeDeleteEventLoop(), 清理内存

connect_socket() // 连接
1. 获取socket fd,设置非阻塞,connect
2. aeCreateFileEvent() // 监听fd可读,可写, 回调函数为socket_connected

socket_connected() // 回调函数
1. 初始化http-parser
2. aeCreateFileEvent() // 监听可读,可写,设置回调函数(socket_readable, socket_writeable)

统计数据

wrk 采用struct stat统计requests和latency,会计算平均值、标准差、方差。而计算这些统计数据依赖的原始数据存储在stat.data[]数组里,运行中每100ms会写入一次值进data[]里(AE_TIME_EVENT)。
* 由于是多线程操作同一个数据结构latency,使用线程安全的原子操作,更新数值,如__sync_fetch_and_add()等系列函数。
* latency计算方法: socket变为writable的时候,记一个connection->start, socket变为readable的时候,开始调用http-parser解析响应,解析完成后,得到时间差,调用stats_record函数记入。

Socket连接

1个thread建立100个连接,那么在每个连接writable的时候,是
1. 先写一个http请求过去,然后一直等着这个socket变为readable,读取解析完响应后才写下一个Http请求过去呢?
2. 还是说,如果socket是writable,就一直不停地写http请求过去,不必等待socket变为readable,按序解析完相应呢?
我是个网络编程新手哈,^-^!

wrk是按照第一种方式来的。但是这就有一个问题,在写完一个http请求之后,先要aeDeleteFileEvent(AE_WRITABLE);而在读取解析完服务器的响应后,要再度添加aeCreateFileEvent(AE_READABLE).
wrk确实是这么实现的,当写完一个请求后,socket_writeable()里面删除了AE_WRIABLE的监听事件,在socket_readable()里面,执行了http_parser_execute()解析响应。解析完成后,调用response_complete,再度添加了可写的监听事件。
但是这里的逻辑,有两处疑点:
1. 何时调用response_complete,这个需要结合http-parser去看源码,感觉http-parser内部也像是一个状态机,实现了监听状态变化。
2. 这里,判断解析结束,使用了

if(--c->pending == 0) {
    stats_record()
    aeCreateFileEvent(AE_WRITABLE)
}

这个pending字段简直不知道啥意思,没有注释。加了debug后,验证了,如上面所述,但是这个pending字段确实太魔性。


总的来说,wrk的代码很优雅,简洁,没有绕来绕去的逻辑,思路很顺,尤其是使用了很多优秀的第三方库的源码。对以后工作中的编码,帮助很大。

你可能感兴趣的:(linux)