30

一种回放式压测工具的设计

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzA4NTYwOTE3MQ%3D%3D&%3Bmid=2452533091&%3Bidx=1&%3Bsn=0d769c03827872d41ff784c1890897cd&%3Bchksm=880f50dfbf78d9c9d48b8b935fd541a23207f742d43272bc18eaf5d60c3915374efd2f243bbc&%3Btoken=6309489&%3Blang=zh_CN
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

YFbUfij.jpg!web

不知道有多少人之前了解过 TCPCopyTCPReplayGoReplay 这类工具?这类工具都是将请求转发到另外一个环境中实现流量的『回放』,在量化金融领域,也有类似的概念,称之为“回测”。

这种回放型的测试其实非常适合用于压测,这要从传统压测的缺陷说起。

假设有这么一个外卖下单接口: POST /api/order ,需要传入商户ID、菜品ID等参数,如果计划用100并发线程去压这个接口,你肯定会考虑以下几个问题:

  • 是使用同一商户、同一菜品去压测?

  • 是使用同一商户、不同菜品去压测?

  • 是使用100个商户、不同菜品去压测?

  • 是使用20个商户、每个商户相同菜品去压测?

  • ...

为什么会有这么多场景?

以菜品对象为例,开发为了防止菜品出现超卖情况,需要使用锁来对菜品库存的扣减进行保护,保证竞态下扣减依然安全可靠。这样来,当使用同一个菜品去进行压测,所有请求都在竞争这把锁,当某个请求竞争得到时,其他线程只能处于等待状态,势必影响了服务的吞吐能力;相反,如果使用不同菜品压测,每个菜品的锁不存在竞争者,就不会出现其他请求等待的情况,吞吐能力更强。

以上只是举例说明,实际开发中可以追求最终一致性来提高吞吐能力

上面这个例子可以看出,传统的压测适合做基准测试,但如果要高仿真的模拟生产流量去评估容量瓶颈、规模增长影响等,较难进行流量特征的建模,因此导致评估结果也不够准确。

为什么是回放?

回放型测试能够解决上面提出的问题吗?

我们从下面这段Nginx的访问日志来展开:

47.29.201.179 - - [28/Feb/2019:13:17:10 +0000] "GET /?p=1 HTTP/2.0" 200 5316 "https://domain1.com/?p=1" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36" "2.75"

上面的日志里包含了几个关键信息:

  • 请求时间

  • 请求方法

  • 请求路径

  • 请求头(UA部分)

  • 请求参数(无Body部分)

  • 返回结果状态码

这段日志具备了构建一个HTTP请求的完整信息(暂只考虑GET请求),手动回放这个请求很简单:

curl https://domain1.com/?p=1

我们再增加几行日志,比如:

47.29.201.179 - - [28/Feb/2019:13:17:10 +0000] "GET /?p=1 HTTP/2.0" 200 5316 "https://domain1.com/?p=1" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36" "2.75"

47.29.201.179 - - [28/Feb/2019:13:17:20 +0000] "GET /?p=3 HTTP/2.0" 200 5316 "https://domain1.com/?p=3" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36" "2.75"

47.29.201.179 - - [28/Feb/2019:13:18:20 +0000] "GET /?p=3 HTTP/2.0" 200 5316 "https://domain1.com/?p=2" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36" "2.75"

那么手动回放下这个过程:

curl https://domain1.com/?p=1

curl https://domain1.com/?p=3

curl https://domain1.com/?p=2

上面这几行nginx的日志我不知道是不是已经能让你明白回放型压测的好处了: 如果我能从日志里提取出访问的接口以及接口参数,那么在压测设计时,我无需关注要用几个商户几个菜品去压测,日志内容已经告诉了我这一切。

整个流量特征建模的工作一下子就被解决了。

为什么不是GoReplay?

那是不是可以直接使用 GoReplay 这样的工具来实现?

我们先回到上面那三行nginx日志以及我们写出的回放步骤,有没有觉得不太准确?

上面的curl命令忽略了这三个请求之间的时间差:

  • 2019:13:17:10

  • 2019:13:17:20

  • 2019:13:18:20

更仿真的回放过程其实是:

curl https://domain1.com/?p=1

sleep 10

curl https://domain1.com/?p=3

sleep 60

curl https://domain1.com/?p=2

时间戳是在回放型测试中非常重要的信息,它能告知控制程序应该在哪个时间点发出这个请求,而不是一股脑的丢出去。

我之前使用 GoReplay 进行离线回放时,它并不能根据时间戳来阻塞/放行请求,这样也就做不到高仿真了。另外控制中间的等待时间,还能实现快放、慢放能力,整个行为听上去是不是很像下面这个物件?

iQnquaJ.jpg!web

设计要点

我按照以下的架构来设计一个回放型压测工具

B77NFfq.png!web

日志文件

日志文件由具体被压测服务来提供,希望包含时间戳、请求参数等信息。

不过在我使用过程中,不要把“日志文件”局限于真正的日志文件上,“日志文件”也可以是数据库记录、也可以是队列中的消息,只要包含了请求时间刻度的、包含了能够转换成请求内容的对象都可以视作“日志文件”。

Parser

提供了对日志文件的解析能力,能通过读取日志文件中的内容,提取出构建整个请求必要的信息:

  • 请求方式

  • 请求头

  • 请求路径(包含query string)

  • 请求body

  • 请求时间

因为每个服务日志内容格式是不同的, Parser 往往得由压测具体的执行者来实现,很难提供一个大而全的统一解决方案。

TimeWheel

时间轮,用来控制阻塞/放行请求。家里还有录音机的朋友,可以把这个对象想象成仓内控制磁带转动的两根轴。

控制轴的转速就能实现回放的速率了。

Repeater

在压测中,如果我们想评估业务规模增长两倍、三倍后的服务器承压情况,这时候光一比一的回放就没有价值了。Repeater提供了请求的增益(倍率)能力,能将一个请求复制出多份,从而实现对业务增长的模拟。

另外,即便一比一等量回放,我们依然要面对回放型压测中不得不解决的一个问题:幂等性。

f(x) = f(f(x))

再举一个例子:我们的创建商户接口 POST: /api/createMch ,要求传入的商户名称是唯一的(业务要求),这个时候日志里包含了一个创建商户的动作,创建了一家小吃店:天大地大小吃店。如果你从日志中提取后,重放这个请求,必然创建失败:数据库中已经存在该商户。

你当然可以选择在压测前清理数据了,但是如果需要你放大一倍的量回放呢?也就是说,回放时会发出两个请求来创建“天大地大小吃店”,那其中必然有个接口报错。这样的回放就没有考虑接口幂等特性,无法达到要求。

因此 Repeater 模块不但需要实现请求增益能力还需要解决接口幂等性的问题。

Replayer

这个功能模块就比较纯粹了,就是用于发送请求。

除了要考虑并发模型。

为什么要考虑并发模型?直接用多线程模块不好吗?先看下我之前写的这篇文章: 聊聊ab、wrk、JMeter、Locust这些压测工具的并发模型差别

再回到上面提到的curl命令中,我们注意到日志中三次请求进入的时间差异是10秒、60秒。但我们curl命令中使用了sleep阻塞时间,如果第一次调用十几秒才完成,那么第二次请求发送后,实际在服务端感知到的请求时间差了20多秒(第一次请求的处理时间+10秒),这样仿真度就不够了。

所以在回放型的压测中,建议使用异步请求来实现

开源项目

我之前在练手Python内置的 asyncio 库的时候,写过一个单机版的回放工具:log-replay,完全可以应用在一般项目中。

另外我们近期也将开源内部使用的分布式回放压测工具Havok,内部应用在生产环境的全链路压测任务中。具备以下特点:

  • Golang实现,简单高效

  • 支持多个数据源,如日志文件、Kafka、ElasticSearch、阿里云SLS

  • InfluxDB+Prometheus+Grafana,包含压测数据、自身metric

  • 丰富的内置函数库,解决接口幂等性、request、response上下文管理等问题

参考资料:

  • TCPCopy

  • TCPReplay

  • GoReplay

AnEVZzV.png!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK