【源码解析】压测工具vegeta
序言
github地址:https://github.com/tsenart/vegeta
第一次写源码解析的博客,就拿自己最熟悉的压测工具vegeta(贝吉塔)来介绍。本篇文章只介绍vegeta的lib库,也就是vegeta核心的发压功能。
实现思路
首先看下lib库里面的文件目录。
.
├── attack.go // 起压力
├── attack_test.go
├── histogram.go // 柱状图,用于结果统计
├── histogram_test.go
├── lttb
├── metrics.go // 统计指标,进行结果处理
├── metrics_test.go
├── pacer.go // 定速器,用于控制发压速率
├── pacer_test.go
├── plot
├── reporters.go // 产生报告
├── results.go // 一次http请求后的结果
├── results_easyjson.go
├── results_test.go
├── target.schema.json
├── targets.go // 压测目标, 代表http请求
├── targets_easyjson.go
└── targets_test.go
http压测工具发压的过程就像是一次进攻一样。 这里用attack来表示发压的动作, 一次打击(hit)代表一次http请求。 打击的目标(target)代表http接口。 发压的qps叫做打击的速率(rate),用专门的定速器(pacer)来控制发压的qps。
部署环境
分析源码,首先要部署一个能看到源码的环境。平时开发使用vim, 这里就只演示在终端下的操作。
go get -u github.com/tsenart/vegeta
如果遇到timeout的情况,更新go版本到1.12及以上, 设置环境变量
export GOPROXY=https://goproxy.io
export GO111MODULE=on
平常go的项目被我放在~/Workspace/golang/mod
目录下,这里我就新建一个文件夹test. 并执行go mod init test
初始化一个GO项目。
创建一个main.go的文件,并从github上复制粘贴示例代码,加上注释后:
package mainimport ("fmt""time"vegeta "github.com/tsenart/vegeta/lib"
)func main() {// 1. 压测时长&速率rate := vegeta.Rate{Freq: 100, Per: time.Second}duration := 4 * time.Second// 2. 压测接口targeter := vegeta.NewStaticTargeter(vegeta.Target{Method: "GET",URL: "http://localhost:9100/",})// 3. 启动压测并收集结果var metrics vegeta.Metricsattacker := vegeta.NewAttacker()for res := range attacker.Attack(targeter, rate, duration, "Big Bang!") {metrics.Add(res)}// 4. 处理结果metrics.Close()// 5. 打印感兴趣的指标fmt.Printf("99th percentile: %s\n", metrics.Latencies.P99)
}
开始
程序执行的过程很简单, 看上面的代码就一目了然了。先定义压测时长和速率,说明压测哪个或哪些接口。 发起压力,并收集结果。发完压力后统计结果。然后输出感兴趣的指标。
一、定义压测时长和速率
// 1. 压测时长&速率rate := vegeta.Rate{Freq: 100, Per: time.Second}duration := 4 * time.Second
这里定义了rate, 跳转到源码:
type Rate = ConstantPacer// 定义了一个恒定的定速装置
type ConstantPacer struct {Freq int // Frequency (number of occurrences) per ...Per time.Duration // Time unit, usually 1s
}
使用vim的Tagbar插件看到ConstantPacer这个结构体有哪些方法。
github.com/tsenart/vegeta/lib/pacer.go
Pace方法, 传入消逝的时间,和打击次数, 计算出要sleep多久后,开始下一次打击。
hit 和 sleep操作交替进行。(hit指发起一次进攻,这里是一次http请求)。从而控制发压的速率。
二、定义压测接口
// 2. 压测接口targeter := vegeta.NewStaticTargeter(vegeta.Target{Method: "GET",URL: "http://localhost:9100/",})
直接看NewStaticTargeter有点费劲,因为你不是道Target是用来干嘛的。所以看一下vegeta.Target的定义。
注释写的清清楚楚,target代表了一个http请求的样式。
- Method: 请求方式, GET POST等
- URL:请求地址
- Body :请求体
- Header :请求头部, cookie放在这里
同样这里也看下Target有哪些方法:
Request方法,看方法签名就知道是用来生成http.Request的。
ps:http包抽象了http协议,这里的http.Request就是http请求报文的抽象。
Target其实是代表了一个http请求的样式,可以通过Request方法生成一个http.Request用于发请求。
接下来看vegeta.NewStaticTargeter方法,
// A Targeter decodes a Target or returns an error in case of failure.
// Implementations must be safe for concurrent use.
type Targeter func(*Target) error// NewStaticTargeter returns a Targeter which round-robins over the passed
// Targets.
func NewStaticTargeter(tgts ...Target) Targeter {i := int64(-1)return func(tgt *Target) error {if tgt == nil {return ErrNilTarget}*tgt = tgts[atomic.AddInt64(&i, 1)%int64(len(tgts))]return nil}
}
NewStaticTargeter方法接受不定数量的Target对象。返回一个Targeter类型的参数。Targeter是形如 func(*Target) error的函数的代表。
NewStaticTargeter方法的实现使用了闭包。 让返回的Targeter仍然能够使用NewStaticTargeter方法传入的target的slice。 并且以Targeter方法定义的顺序返回。 这里闭包,包含了两个父函数的变量, 一个是tgts, 一个是i这个int64值。 返回的Targeter方法,每次轮询的从tgts中取一个target对象,赋予传进来的tgt参数。
这里不理解也没有关系。等后面用到的时候,就知道为什么会这么写了。
到这里,main.go定义了一个targeter变量, 通过这个变量,每次调用一次targeter就能获得一个targe对象。
ps 闭包:闭包是一种特殊的对象,由两部分组成,函数,以及创建该函数的环境。 闭包能够让我们仍然能够使用之前函数内部的变量。
三、启动压测
// 3. 启动压测并收集结果var metrics vegeta.Metricsattacker := vegeta.NewAttacker()for res := range attacker.Attack(targeter, rate, duration, "Big Bang!") {metrics.Add(res)}// 4. 处理结果metrics.Close()
启动压测前,定义了一个metrics变量,用于度量压测过程的相关信息。
查看vegeta.Metrics结构体的定义:
可以看到,压测过程中。会收集响应时延,响应数据总大小,请求数据总大小,结束时间,耗时,请求数量,请求速率,成功数量,状态码和错误信息。
新建收集指标的metrics后,就可以开始发压力了。
调用attacker的Attack方法,传入压测目标,定速装置,请求时长就可以得到一个请求结果的管道,这个请求结果,是每次hit的结果。 每次hit把相关指标收集后,放到管道里。然后由metrics变量收集(metrics.Add(res)). 并处理(metrics.Close())。
Attack方法的签名如下:
attacker.Attack方法是vegeta的核心发压逻辑。在介绍这个方法之前,先简单的梳理下思路。
为了能在短时间内发出很高的压力,单线程的方式肯定不行, 发压算法需要有以下几个功能:
- 多线程,动态调整goroutine数量
假定发送100qps的压力,1s内就要同时有100个goroutine去发送请求,并且是同时的。每个请求都由一个goroutine发出。如果100qps起100个goroutine的话,第一秒的请求肯定能达到100qps, 但是下一秒呢? 万一有个请求因为网络拥堵,或者接口本身耗时就超过1s, 第二秒可能就达不到100qps了。所以不能简单根据qps来定起多少goroutine, 发压算法本身需要支持动态调整goroutine的数量来保证恒定的发压速率。 - 异步的结果处理
假设在1s内,一个gouroutine发送完了请求,并记录下了这个hit过程中的相关指标。不能马上去统计,否则发压过程中有多余的计算,而且,也需要同步各个goroutine的结果计算。这里可以利用golang的channel,将结果送到管道里,由另外的一个goroutine去做专门的统计。压测结果串行计算就可以了。 - 随时可以停止
发压的过程有很多不可控的因素,需要能马上停止发压逻辑。这里用select语法结合channel就可以做到。
知道要实现的功能了,接下来就可以看看Attack方法的源码了。 然后根据功能来一一对应源码。
相关代码是:
var wg sync.WaitGroupworkers := a.workersif workers > a.maxWorkers {workers = a.maxWorkers}
代码一开头,就先定义了WaitGroup来同步发压的goroutine。 10s的发压时间,并不是说10s内一定能发完压力并获得到响应结果。10s之后不会产生新的hit, 但是要保证10s内已经发起hit的goroutine能顺顺利利的完成自己的任务。不能10s一到就强行结束了。这里还限制了maxWorkers,防止goroutine过多一直在争抢资源。
for i := uint64(0); i < workers; i++ {wg.Add(1)go a.attack(tr, name, &wg, ticks, results)}
上面这个for循环就没有什么好讲的了。WaitGroup的用法。
下面来看这个select的用法:
select {case ticks <- struct{}{}:count++continuecase <-a.stopch:returndefault:// all workers are blocked. start one more and try againworkers++wg.Add(1)go a.attack(tr, name, &wg, ticks, results)}
select监听了2个管道,看名字就知道它的作用了。一个是ticks, 这个是用来同步发压的goroutine, 稍后讲解。另一个是-a.stopch, 这个是停止发压的管道。
select 的特点是,监听所有管道,如果有IO操作则执行相应case逻辑,如果没有执行default逻辑。 这里的意思就是如果没有发压信号,也没有停止的信号,则说明所有worker都在忙碌,这个时候需要新增worker,否则下一次tick信号来了,就没有worker去hit了。这里就实现了动态增加goroutine数量。
很多个goroutine如何保证恒定速率发压力呢?time包里有个Tick方法,它会返回一个管道,并每过一段时间往管道塞入一个值。这里vegeta的作者模仿了这个逻辑:
首先定义一个ticks的管道,注意长度为0. 在for循环外获取当前时间, 当进入循环后,获得从开始发压到现在过了多久,将这个时间和当前是第几次发压传给定速装置,定速装置计算出下次要等待的时间, 和一个是否要关闭的标志stop。 这个stop和a.stopch并不一样,这个stop表示已经发完最后一个hit了,接下来程序要结束了。 stopch是用户用来停止程序的。 获得到等待时间后,就 time.Sleep(wait)。在select中,如果发现ticks没有阻塞,就往里面塞值,发压的所有的goroutine都在消费ticks里的信号,ticks一旦有数据,就会有一个goroutine来消费这个信号,去发请求。当所有goroutine都忙的时候,就根据goroutine的数量来决定是否新增goroutine。
这个模型和公司运营模式一样,老板来分配任务,每个员工争相来处理。当员工数量不够了,老板再去聘请新的员工。老板的预算有限,所以不能无休止的聘请新员工。
【源码解析】压测工具vegeta相关推荐
- golang 压测工具vegeta改造-支持自定义压测任务
背景 之前在公司做压测工作的时候,使用了web压测工具vegeta.后续又接到过dns的性能压测.redis的性能压测等任务.http的压测工具vegeta并不能满足需求了.于是模仿vegeta的li ...
- Go语言使用之JSO使用、源码解析和JSON工具类
在go语言网络编程中,经常会有这样的需求:保存结构体和读取结构体数据.如果你使用redis数据库存储数据,你怎么做?Redis仅支持五种数据类型( String(字符串) .Hash (哈希).Lis ...
- golang tollbooth 中间件 压测工具 vegeta
参考: 1.[译] Go 中基于 IP 地址的 HTTP 限流 2.Tollbooth - Fasthttp integration layer 3.didip/tollbooth Simple mi ...
- 网站压测工具 Webbench 源码分析
介绍 Webbench是一个在Linux下使用的非常简单的网站压测工具.它的源代码只有500多行,挺值得一看的开源项目. 实现原理 只是简单的fork()出多个子进程模拟客户端去访问设定的URL,测试 ...
- 多线程与高并发(九):单机压测工具JMH,单机最快MQ - Disruptor原理解析
单机压测工具JMH JMH Java准测试工具套件 什么是JMH 官网 http://openjdk.java.net/projects/code-tools/jmh/ 创建JMH测试 1.创建Mav ...
- Python|excel表格数据一键转json格式小工具|支持xlsx、xls格式转json|【源码+解析】
背景 最近在使用JavaScript编写一些浏览器RPA脚本,脚本使用过程中遇到一些问题,脚本使用的数据往往存放在excel表,但运行时只能读取json数据,导致频繁人工excel转json,效 ...
- 免Root 实现App加载Xposed插件的工具Xpatch源码解析(一)
前言 Xpatch是一款免Root实现App加载Xposed插件的工具,可以非常方便地实现App的逆向破解(再也不用改smali代码了),源码也已经上传到Github上,欢迎各位Fork and St ...
- Nginx HLS压测工具之vegeta
HLS压测工具之vegeta 1. MAC安装 brew update && brew install vegeta 2. 构造target.txt 创建target.txt文件,内容 ...
- shiro反序列化工具_Apache Shiro 1.2.4反序列化漏洞(CVE-2016-4437)源码解析
Apache Shiro Apache Shiro是一个功能强大且灵活的开源安全框架,主要功能包括用户认证.授权.会话管理以及加密.在了解该漏洞之前,建议学习下Apache Shiro是怎么使用. d ...
最新文章
- 【Python小游戏】扫雷游戏竟有世界排行榜,中国90后00后霸占半壁江山?
- 老娘不就是没化妆吗?你几个意思?
- codeforces438 D. The Child and Sequence
- 最实用的Git命令总结:新建本地分支、远程分支、关联和取消关联分支、清除本地和远程分支、合并分支、版本还原、tag命令、中文乱码解决方案、如何fork一个分支和修改后发起合并请求
- 【网络信息安全】密码学入门笔记
- 160505、oracle 修改字符集 修改为ZHS16GBK
- Java I/O(输入输出流)
- 修改箱线图的横坐标顺序
- 数学建模与数学实验3.4习题1
- 使用Tor绕过防火墙进行远程匿名访问
- 深入浅出node.js第9章玩转进程摘录
- 华三路由交换配置命令_华三华为交换机路由器配置常用命令
- 开源机器学习平台tipdm
- 每天睡6小时和8小时的区别 看完再不敢熬夜了
- 移动硬盘突然在电脑上无法显示
- Aspose.3D使用教程:使用 Java 将 FBX 转换为 RVM 或 RVM 转换为 FBX 文件
- C/C++面试高频知识点八股文
- 项目管理-5大过程组-10大知识领域-47过程
- 递归学习_组合_全组合排列
- 超好看的导航页面(静态页面)
热门文章
- 【科大讯飞】全球首款,Mobius莫比斯同声翻译耳机 ,AI智能运动耳机 ,支持英日法韩俄西班牙6种语音...
- 菜鸟学院~2020年谁在挑战云上“霸权”?
- PostCSS及其常用插件介绍
- LAMP安装明细(apache,mysql,php)
- 好看的皮囊千篇一律,内涵的“可视化大屏”万里挑一
- java代码审计入门--01
- 小白联通300M流量领取工具
- 远程桌面连接报错(CredSSP加密数据库修正)解决方案
- iframe跨域的几种常用方法
- 【shell】shuf命令提取文件的随机行